From 31f643b2d669b62a6b46e80eff8c7135bb75244e Mon Sep 17 00:00:00 2001 From: ghangz <152254226+ghangz@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:42:41 +0800 Subject: [PATCH 1/2] Add build script linter --- tools/build_script_lint.py | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tools/build_script_lint.py diff --git a/tools/build_script_lint.py b/tools/build_script_lint.py new file mode 100644 index 0000000..3967a0c --- /dev/null +++ b/tools/build_script_lint.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Lint launch scripts for common MACA deployment footguns.""" + +from __future__ import annotations + + +import argparse +import json +from pathlib import Path + + +GLOBS = ['*.sh', '**/*.sh'] +REQUIRED_TOKENS = ['MACA_HOME', 'mxcc'] + + +def lint(root: Path) -> dict[str, object]: + findings: list[dict[str, str]] = [] + for pattern in GLOBS: + for path in sorted(root.glob(pattern)): + if not path.is_file(): + continue + text = path.read_text(encoding="utf-8", errors="replace") + rel = path.relative_to(root).as_posix() + if "\r\n" in text: + findings.append({"path": rel, "severity": "warning", "message": "script uses CRLF line endings"}) + if "set -e" not in text and path.suffix in (".sh", ""): + findings.append({"path": rel, "severity": "warning", "message": "shell script does not enable fail-fast mode"}) + for token in REQUIRED_TOKENS: + if token not in text: + findings.append({"path": rel, "severity": "info", "message": f"missing optional token: {token}"}) + return {"finding_count": len(findings), "findings": findings} + + +def self_test() -> None: + data = lint(Path.cwd()) + assert "findings" in data + print(json.dumps({"ok": True, "finding_count": data["finding_count"]}, ensure_ascii=False)) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", default=".") + parser.add_argument("--self-test", action="store_true") + args = parser.parse_args() + if args.self_test: + self_test() + return 0 + print(json.dumps(lint(Path(args.root)), ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 5a614ab31df01d2c8a3272c968ba36dd86a89526 Mon Sep 17 00:00:00 2001 From: ghangz <152254226+ghangz@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:45:10 +0800 Subject: [PATCH 2/2] Harden build script lint checks --- tools/build_script_lint.py | 71 ++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/tools/build_script_lint.py b/tools/build_script_lint.py index 3967a0c..e781903 100644 --- a/tools/build_script_lint.py +++ b/tools/build_script_lint.py @@ -6,34 +6,66 @@ import argparse import json +import re from pathlib import Path +from tempfile import TemporaryDirectory -GLOBS = ['*.sh', '**/*.sh'] -REQUIRED_TOKENS = ['MACA_HOME', 'mxcc'] +GLOBS = ["*.sh", "**/*.sh"] +REQUIRED_TOKENS = ["MACA_HOME", "mxcc"] +FAIL_FAST_RE = re.compile(r"^\s*set\s+(?:-[A-Za-z]*e[A-Za-z]*|-o\s+errexit)\b", re.M) def lint(root: Path) -> dict[str, object]: + if not root.is_dir(): + raise NotADirectoryError(f"lint root is not a directory: {root}") + findings: list[dict[str, str]] = [] - for pattern in GLOBS: - for path in sorted(root.glob(pattern)): - if not path.is_file(): - continue - text = path.read_text(encoding="utf-8", errors="replace") - rel = path.relative_to(root).as_posix() - if "\r\n" in text: - findings.append({"path": rel, "severity": "warning", "message": "script uses CRLF line endings"}) - if "set -e" not in text and path.suffix in (".sh", ""): - findings.append({"path": rel, "severity": "warning", "message": "shell script does not enable fail-fast mode"}) - for token in REQUIRED_TOKENS: - if token not in text: - findings.append({"path": rel, "severity": "info", "message": f"missing optional token: {token}"}) + scripts = { + path + for pattern in GLOBS + for path in root.glob(pattern) + if path.is_file() + } + for path in sorted(scripts): + text = path.read_text(encoding="utf-8", errors="replace") + rel = path.relative_to(root).as_posix() + if "\r\n" in text: + findings.append( + { + "path": rel, + "severity": "warning", + "message": "script uses CRLF line endings", + } + ) + if not FAIL_FAST_RE.search(text) and path.suffix in (".sh", ""): + findings.append( + { + "path": rel, + "severity": "warning", + "message": "shell script does not enable fail-fast mode", + } + ) + for token in REQUIRED_TOKENS: + if token not in text: + findings.append( + { + "path": rel, + "severity": "info", + "message": f"missing optional token: {token}", + } + ) return {"finding_count": len(findings), "findings": findings} def self_test() -> None: - data = lint(Path.cwd()) - assert "findings" in data + with TemporaryDirectory() as tmp_dir: + root = Path(tmp_dir) + script = root / "build.sh" + script.write_text("#!/usr/bin/env bash\nset -ex\nmxcc --version\n", encoding="utf-8") + data = lint(root) + if "findings" not in data: + raise RuntimeError(f"self-test failed: {data}") print(json.dumps({"ok": True, "finding_count": data["finding_count"]}, ensure_ascii=False)) @@ -45,7 +77,10 @@ def main() -> int: if args.self_test: self_test() return 0 - print(json.dumps(lint(Path(args.root)), ensure_ascii=False, indent=2)) + root = Path(args.root) + if not root.is_dir(): + parser.error(f"--root is not a directory: {root}") + print(json.dumps(lint(root), ensure_ascii=False, indent=2)) return 0