Skip to content

Commit 5a16ab5

Browse files
committed
fmt: Replace style.sh with style.py
We need a bit more flexibility in the formatting, which would be difficult to achieve with bash. Rewrite the style check to a python script that will be easier to extend. This is also faster than using inplace perl, an now prints a diff when semver files aren't sorted. (backport <#4842>) (cherry picked from commit 9713fd1)
1 parent d18a507 commit 5a16ab5

File tree

4 files changed

+167
-92
lines changed

4 files changed

+167
-92
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Setup Rust toolchain
3030
run: ./ci/install-rust.sh && rustup component add rustfmt
3131
- name: Check style
32-
run: ./ci/style.sh
32+
run: ./ci/style.py
3333

3434
clippy:
3535
name: Clippy on ${{ matrix.os }}

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ We have two automated tests running on
9393
- `cd libc-test && cargo test`
9494
- Use the `skip_*()` functions in `build.rs` if you really need a workaround.
9595
2. Style checker
96-
- [`./ci/style.sh`](https://github.com/rust-lang/libc/blob/main/ci/style.sh)
96+
- [`./ci/style.py`](https://github.com/rust-lang/libc/blob/main/ci/style.py)
9797

9898
## Breaking change policy
9999

ci/style.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import re
5+
import subprocess as sp
6+
import sys
7+
8+
from difflib import unified_diff
9+
from glob import iglob
10+
from pathlib import Path
11+
12+
13+
FMT_DIRS = ["src", "ci"]
14+
IGNORE_FILES = [
15+
# Too much special syntax that we don't want to format
16+
"src/macros.rs"
17+
]
18+
19+
20+
def main():
21+
# if `CI` is set, do a check rather than overwriting
22+
check_only = os.getenv("CI") is not None
23+
run(["rustfmt", "-V"])
24+
25+
fmt_files = []
26+
for dir in FMT_DIRS:
27+
fmt_files.extend(iglob(f"{dir}/**/*.rs", recursive=True))
28+
29+
for file in fmt_files:
30+
if file in IGNORE_FILES:
31+
continue
32+
fmt_one(Path(file), check_only)
33+
34+
# Run once from workspace root to get everything that wasn't handled as an
35+
# individual file.
36+
if check_only:
37+
run(["cargo", "fmt", "--check"])
38+
else:
39+
run(["cargo", "fmt"])
40+
41+
for file in iglob("libc-test/semver/*.txt"):
42+
check_semver_file(Path(file))
43+
44+
# Style tests
45+
run(
46+
[
47+
"cargo",
48+
"test",
49+
"--manifest-path=libc-test/Cargo.toml",
50+
"--test=style",
51+
"--",
52+
"--nocapture",
53+
]
54+
)
55+
56+
try:
57+
run(["shellcheck", "--version"])
58+
except sp.CalledProcessError:
59+
eprint("ERROR: shellcheck not found")
60+
exit(1)
61+
62+
for file in iglob("**/*.sh", recursive=True):
63+
run(["shellcheck", file])
64+
65+
66+
def fmt_one(fpath: Path, check_only: bool):
67+
eprint(f"Formatting {fpath}")
68+
text = fpath.read_text()
69+
70+
# Rustfmt doesn't format the bodies of `{ ... }` macros, which is most of `libc`. To
71+
# make things usable, we do some hacks to replace macros with some kind of
72+
# alternative syntax that gets formatted about how we want, then reset the changes
73+
# after formatting.
74+
75+
# Turn all braced macro `foo! { /* ... */ }` invocations into
76+
# `fn foo_fmt_tmp() { /* ... */ }`, since our macro bodies are usually valid in
77+
# a function context.
78+
text = re.sub(r"(?!macro_rules)\b(\w+)!\s*\{", r"fn \1_fmt_tmp() {", text)
79+
80+
# Replace `if #[cfg(...)]` within `cfg_if` with `if cfg_tmp!([...])` which
81+
# `rustfmt` will format. We put brackets within the parens so it is easy to
82+
# match (trying to match parentheses would catch the first closing `)` which
83+
# wouldn't be correct for something like `all(any(...), ...)`).
84+
text = re.sub(r"if #\[cfg\((.*?)\)\]", r"if cfg_tmp!([\1])", text, flags=re.DOTALL)
85+
86+
# The `c_enum!` macro allows anonymous enums without names, which isn't valid
87+
# syntax. Replace it with a dummy name.
88+
text = re.sub(r"enum #anon\b", r"enum _fmt_anon", text)
89+
90+
# Invoke rustfmt passing via stdin/stdout so we don't need to write the file. Exits
91+
# on failure.
92+
cmd = ["rustfmt", "--config-path=.rustfmt.toml"]
93+
if check_only:
94+
res = check_output(cmd + ["--check"], input=text)
95+
96+
# Unfortunately rustfmt on stdin always completes with 0 exit code even if
97+
# there are errors, so we need to pick between writing the file to disk or
98+
# relying on empty stdout to indicate success.
99+
# <https://github.com/rust-lang/rustfmt/issues/5376>.
100+
if len(res) == 0:
101+
return
102+
eprint(f"ERROR: File {fpath} is not properly formatted")
103+
print(res)
104+
exit(1)
105+
else:
106+
text = check_output(cmd, input=text)
107+
108+
# Restore all changes in the formatted text
109+
text = re.sub(r"fn (\w+)_fmt_tmp\(\)", r"\1!", text)
110+
text = re.sub(r"cfg_tmp!\(\[(.*?)\]\)", r"#[cfg(\1)]", text, flags=re.DOTALL)
111+
text = re.sub(r"enum _fmt_anon", r"enum #anon", text)
112+
113+
# And write the formatted file back
114+
fpath.write_text(text)
115+
116+
117+
def check_semver_file(fpath: Path):
118+
if "TODO" in str(fpath):
119+
eprint(f"Skipping semver file {fpath}")
120+
return
121+
122+
eprint(f"Checking semver file {fpath}")
123+
124+
text = fpath.read_text()
125+
lines = text.splitlines()
126+
sort = sorted(lines)
127+
if lines != sort:
128+
eprint(f"ERROR: Unsorted semver file {fpath}")
129+
eprint("\n".join(unified_diff(lines, sort, lineterm="")))
130+
exit(1)
131+
132+
duplicates = []
133+
seen = set()
134+
for line in lines:
135+
if line in seen:
136+
duplicates.append(line)
137+
seen.add(line)
138+
139+
if len(duplicates) > 0:
140+
eprint(f"ERROR: Duplicates in semver file {fpath}")
141+
eprint(duplicates)
142+
exit(1)
143+
144+
145+
def check_output(args: list[str], **kw) -> str:
146+
xtrace(args)
147+
return sp.check_output(args, encoding="utf8", text=True, **kw)
148+
149+
150+
def run(args: list[str], **kw) -> sp.CompletedProcess:
151+
xtrace(args)
152+
return sp.run(args, check=True, text=True, **kw)
153+
154+
155+
def xtrace(args: list[str]):
156+
astr = " ".join(args)
157+
eprint(f"+ {astr}")
158+
159+
160+
def eprint(*args, **kw):
161+
print(*args, file=sys.stderr, **kw)
162+
163+
164+
if __name__ == "__main__":
165+
main()

ci/style.sh

Lines changed: 0 additions & 90 deletions
This file was deleted.

0 commit comments

Comments
 (0)