4"""Reject legacy repository task invocations in authored surfaces.
6The repository task runner is Just. GNU Make can still be a real dependency
7of CMake or an upstream source build, so this checker deliberately matches
8only command-shaped task invocations: an executable shell/YAML/Docker line, a
9shell command array, or a command presented in quotes/backticks or after a
10user-guidance verb. Natural English, CMake/Makefile names, tool lists, and
11dependency probes such as ``command -v make`` stay outside that shape. A real
12upstream build belongs outside the CI, Just, developer-script, and MCP
13task-entry-point scope; adding one there requires a narrow, path-specific
14exception and a negative self-test.
17from __future__
import annotations
23from pathlib
import Path
25REPO_ROOT = Path(__file__).resolve().parents[2]
26SELF =
"scripts/checks/check_no_legacy_make.py"
27EXACT_FILES = frozenset(
28 {
".clangd",
".cppcheck-suppressions",
".env.example",
"CMakePresets.json",
"justfile"}
38DOC_SUFFIXES = frozenset({
".md",
".mdx",
".rst"})
40 "docs/sbom/upstream/",
42 "apps/shared_libs/third_party/",
49BASELINE_RE = re.compile(
r"^\.github/[^/]*baseline[^/]*\.txt$")
54MAKE_EXECUTABLE =
r'(?:g?make|"g?make"|\'g?make\')'
60ACTIVE_COMMAND_RE = re.compile(
61 rf
"^\s*(?:(?:RUN|run:)\s+)?({MAKE_EXECUTABLE})(?=\s|$)"
62 r"(?:\s+([^\s#;&|]+))?"
67ARRAY_COMMAND_RE = re.compile(
68 rf
"^\s*[A-Za-z_][A-Za-z0-9_]*\s*=\(\s*({MAKE_EXECUTABLE})(?=\s|\))"
74COMMENT_COMMAND_RE = re.compile(rf
"^\s*#\s*({MAKE_EXECUTABLE})\s+([^\s]+)\s*[.`'\"]?\s*$")
79QUOTED_COMMAND_RE = re.compile(
80 r"(?:`|'|\")(g?make)(?:\s+([^\s`'\"]+)|(?:`|'|\")+\s+(?:target|recipe|task)\b)"
85GUIDANCE_COMMAND_RE = re.compile(
86 rf
"\b(?:run|use|invoke|try|rerun|execute)\s+({MAKE_EXECUTABLE})\s+"
92def scoped_files() -> list[str]:
93 """Return authored documentation and automation covered by the migration contract."""
94 git_bin = shutil.which(
"git")
or "git"
95 proc = subprocess.run(
101 "--exclude-standard",
108 rels = proc.stdout.decode(
"utf-8", errors=
"strict").split(
"\0")
111 if not rel
or rel.startswith(EXCLUDED_PREFIXES):
114 if not (REPO_ROOT / path).is_file():
118 or rel.startswith(PREFIXES)
119 or BASELINE_RE.match(rel)
is not None
120 or path.suffix.lower()
in DOC_SUFFIXES
121 or path.name ==
"Dockerfile"
125 if (REPO_ROOT / SELF).is_file():
127 return sorted(selected)
130def legacy_invocation(line: str, *, active_commands: bool =
True) -> str |
None:
131 """Return the command-shaped legacy invocation on ``line``, if any."""
132 patterns = [COMMENT_COMMAND_RE, QUOTED_COMMAND_RE, GUIDANCE_COMMAND_RE]
134 patterns[0:0] = [ACTIVE_COMMAND_RE, ARRAY_COMMAND_RE]
135 for pattern
in patterns:
136 match = pattern.search(line)
137 if match
is not None:
138 executable = match.group(1).strip(
"\"'")
139 first_arg = match.group(2)
140 return f
"{executable} {first_arg}" if first_arg
else executable
144def scan(rels: list[str]) -> list[str]:
145 """Return path/line findings for every command-shaped legacy reference."""
146 findings: list[str] = []
148 path = REPO_ROOT / rel
149 active_commands = rel.endswith((
".sh",
".yml",
".yaml"))
or path.name ==
"Dockerfile"
151 text = path.read_text(encoding=
"utf-8")
152 except UnicodeDecodeError:
154 for number, line
in enumerate(text.splitlines(), start=1):
155 invocation = legacy_invocation(line, active_commands=active_commands)
156 if invocation
is not None:
157 findings.append(f
"{rel}:{number}: legacy repository task: {invocation}")
161def selftest() -> int:
162 """Prove command forms fire and legitimate Make mentions stay quiet."""
163 command =
"ma" +
"ke"
164 gnu_command =
"g" + command
166 (f
"{command} ci",
True,
"a direct shell task fires"),
167 (f
"{command} -C apps/board/stand_alone/blink build",
True,
"a -C task fires"),
168 (f
"{gnu_command} ci",
True,
"a gmake task fires"),
169 (f
"cmd=({command} -C apps/blink)",
True,
"a command array fires"),
170 (f
'cmd=("{command}" "-C" apps/blink)',
True,
"a quoted array command fires"),
171 (f
'"{command}" -C apps/blink',
True,
"a quoted executable fires"),
172 (f
"run: {command} -C apps/blink",
True,
"a one-line YAML command fires"),
173 (f
"RUN {command} coverage",
True,
"a Dockerfile task fires"),
174 (f
"# {command} ci-native",
True,
"a bare comment hint fires"),
175 (f
"# `{command} sbom` regenerates it",
True,
"a backticked hint fires"),
177 f
"CI (or a local ``{command}`` target) catches drift",
179 "a quoted legacy task-runner reference fires",
181 (f
"Please run {command} misra",
True,
"an unquoted user hint fires"),
182 (
"command -v make || missing=build-essential",
False,
"a dependency probe stays quiet"),
183 (
"command -v gmake || missing=build-essential",
False,
"a gmake probe stays quiet"),
184 (
"for tool in curl cmake make tar cc; do",
False,
"an upstream tool list stays quiet"),
185 (
"these controls make an empty scan fail",
False,
"natural English stays quiet"),
186 (
"# make the detector fail",
False,
"natural comment prose stays quiet"),
187 (
"CMakeLists.txt and GNUmakefile",
False,
"build-system filenames stay quiet"),
189 "# Make is required by an upstream source build",
191 "an explanatory mention stays quiet",
195 label
for line, expected, label
in cases
if bool(legacy_invocation(line)) != expected
198 for failure
in failures:
199 print(f
"check_no_legacy_make.py --selftest: FAIL: {failure}", file=sys.stderr)
201 print(f
"check_no_legacy_make.py --selftest: PASS ({len(cases)} both-direction cases)")
206 """Run detector self-tests or scan the live tracked scope."""
207 if sys.argv[1:] == [
"--selftest"]:
210 print(
"usage: check_no_legacy_make.py [--selftest]", file=sys.stderr)
213 rels = scoped_files()
214 except (OSError, subprocess.CalledProcessError, UnicodeError)
as exc:
215 print(f
"check_no_legacy_make.py: cannot enumerate tracked files: {exc}", file=sys.stderr)
217 if len(rels) < MIN_SCOPED_FILES
or SELF
not in rels:
219 f
"check_no_legacy_make.py: scope collapsed to {len(rels)} file(s); "
220 f
"expected at least {MIN_SCOPED_FILES} including {SELF}",
224 findings = scan(rels)
226 print(
"check_no_legacy_make.py: legacy repository task references:", file=sys.stderr)
227 for finding
in findings:
228 print(f
" {finding}", file=sys.stderr)
229 print(
"Use the authoritative namespaced Just recipe instead.", file=sys.stderr)
231 print(f
"check_no_legacy_make.py: clean ({len(rels)} authored files)")
235if __name__ ==
"__main__":
236 raise SystemExit(
main())
void main(void)
The application entry point Reset_Handler hands control to.