|
| 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() |
0 commit comments