4"""Keep native Just builds on the C23 compiler and complete tool registry."""
6from __future__
import annotations
13from pathlib
import Path
15REPO_ROOT = Path(__file__).resolve().parents[2]
16COMPILED = frozenset({
".c",
".cc",
".cpp",
".cxx",
".m",
".mm"})
17RECIPE = re.compile(
r"^([A-Za-z_][A-Za-z0-9_-]*)(?:\s+[^:]*)?:\s*$")
18RAW_CMAKE_CONFIGURE = re.compile(
r"(?:^|\s)cmake\s+(?!--(?:build|install)\b)")
19RAW_COMPILER = re.compile(
20 r"(?:^|\s)(?:\$\{?cc\}?|\$cc|cc|gcc(?:-[0-9]+)?|clang(?:-[0-9]+)?)\s+"
21 r"[^\n]*(?:-std=|(?:^|\s)-c(?:\s|$)|(?:^|\s)-o(?:\s|$))"
25def _recipe_bodies(text: str) -> list[tuple[str, str]]:
26 """Return recipe names and bodies from one Just module."""
27 bodies: list[tuple[str, str]] = []
30 for line
in text.splitlines():
31 match = RECIPE.match(line)
34 bodies.append((name,
"\n".join(lines)))
35 name, lines = match.group(1).strip(), []
36 elif name
and (line.startswith((
" ",
"\t"))
or not line.strip()):
37 if not line.lstrip().startswith(
"#"):
40 bodies.append((name,
"\n".join(lines)))
44def _shell_commands(body: str) -> list[str]:
45 """Join shell continuation lines into configure-command units."""
46 commands: list[str] = []
48 for raw
in body.splitlines():
50 current = f
"{current} {line}".strip()
51 if current.endswith(
"\\"):
52 current = current[:-1].rstrip()
54 commands.append(current)
57 commands.append(current)
61def _recipe_errors(label: str, text: str) -> list[str]:
62 """Reject raw native CMake configure and compile-driver bodies."""
63 errors: list[str] = []
64 for name, body
in _recipe_bodies(text):
66 f
"{label}: recipe {name}: raw native CMake bypasses host_cmake.sh"
67 for command
in _shell_commands(body)
68 if RAW_CMAKE_CONFIGURE.search(command)
and "CMAKE_TOOLCHAIN_FILE" not in command
70 if RAW_COMPILER.search(body):
71 errors.append(f
"{label}: recipe {name}: raw host compiler invocation bypasses CMake")
75def _just_files(root: Path) -> list[Path]:
76 """Discover the root Just entry point and every module."""
77 return [root /
"justfile", *sorted((root /
"just").glob(
"*.just"))]
80def _compiled_tools(root: Path) -> set[str]:
81 """Discover tool roots containing authored compiled implementation."""
82 found: set[str] = set()
83 tools = root /
"tools"
84 for path
in tools.glob(
"*/src/**/*"):
85 if path.is_file()
and path.suffix
in COMPILED:
86 found.add(path.relative_to(tools).parts[0])
90def _cmake_tools(root: Path) -> set[str]:
91 """Discover tool roots managed by the CMake dispatcher."""
92 return {path.parent.name
for path
in (root /
"tools").glob(
"*/CMakeLists.txt")}
95def _standalone_cmake(text: str) -> bool:
96 """Whether a listfile declares itself as a top-level CMake project."""
97 return bool(re.search(
r"^\s*project\s*\(", text, flags=re.MULTILINE))
100def _shared_dispatch_errors(root: Path) -> list[str]:
101 """Ensure shared consumer fragments cannot be configured vacuously."""
102 just_text = (root /
"just" /
"shared.just").read_text(encoding=
"utf-8")
103 dispatcher = root /
"scripts" /
"builders" /
"build_shared_libs.sh"
105 if 'build_shared_libs.sh "{{ lib }}"' not in just_text:
106 errors.append(
"just/shared.just does not delegate to the shared-library dispatcher")
107 text = dispatcher.read_text(encoding=
"utf-8")
if dispatcher.is_file()
else ""
109 f
"shared-library dispatcher lacks contract: {required}"
110 for required
in (
"is_standalone",
"host_cmake.sh",
"apps::shared::test")
111 if required
not in text
116def _inventory_errors(root: Path) -> list[str]:
117 """Require every compiled tool to join the discovery-managed registry."""
118 missing = sorted(_compiled_tools(root) - _cmake_tools(root))
119 return [f
"tools/{name}: compiled tool has no CMakeLists.txt" for name
in missing]
122def _dispatcher_errors(root: Path, listed: set[str]) -> list[str]:
123 """Check discovery output and the Just/wrapper contracts."""
124 expected = _cmake_tools(root)
125 errors = [f
"tool dispatcher omits {name}" for name
in sorted(expected - listed)]
126 errors += [f
"tool dispatcher invents {name}" for name
in sorted(listed - expected)]
127 tools_just = (root /
"just" /
"tools.just").read_text(encoding=
"utf-8")
131 'build_host_tools.sh build "{{ tool }}"',
132 'build_host_tools.sh clean "{{ tool }}"',
134 if required
not in tools_just:
135 errors.append(f
"just/tools.just lacks discovery contract: {required}")
136 wrapper = (root /
"scripts" /
"builders" /
"host_cmake.sh").read_text(encoding=
"utf-8")
138 "ra8_select_host_compiler",
139 "ra8_select_emulator_compiler",
140 "ra8_cmake_reset_if_incompatible",
141 "-DCMAKE_C_COMPILER=",
142 "-DCMAKE_CXX_COMPILER=",
144 if required
not in wrapper:
145 errors.append(f
"host_cmake.sh lacks compiler/cache contract: {required}")
146 dispatcher = (root /
"scripts" /
"builders" /
"build_host_tools.sh").read_text(encoding=
"utf-8")
147 for required
in (
"clean_one",
'"$dir/cache_bench"',
'"$dir/miniz_host.o"',
'"$dir"/*.trace'):
148 if required
not in dispatcher:
149 errors.append(f
"tool dispatcher lacks legacy-clean contract: {required}")
153def _live_dispatch(root: Path) -> tuple[set[str], list[str]]:
154 """Run the read-only dispatcher list mode."""
155 script = root /
"scripts" /
"builders" /
"build_host_tools.sh"
156 proc = subprocess.run(
157 [str(script),
"list"], text=
True, capture_output=
True, check=
False
160 return set(), [f
"tool dispatcher list failed: {proc.stderr.strip()}"]
161 return set(proc.stdout.splitlines()), []
164def _selftest() -> int:
165 """Prove raw builds and registry omissions fire while legal forms stay quiet."""
166 failures: list[str] = []
167 good =
"build:\n bash scripts/builders/host_cmake.sh tools/x tools/x/build\n"
169 cmake -S x -B b -DCMAKE_TOOLCHAIN_FILE=cmake/arm.cmake
172 bad_cmake =
"build:\n cmake -S tools/x -B tools/x/build\n"
173 mixed_cmake =
"""build:
174 cmake -S arm -B arm/build -DCMAKE_TOOLCHAIN_FILE=cmake/arm.cmake
175 cmake -S tools/x -B tools/x/build
177 bad_cc =
"build:\n cc -std=gnu23 src/main.c -o tool\n"
178 if _recipe_errors(
"good", good)
or _recipe_errors(
"cross", cross):
179 failures.append(
"wrapper or ARM-toolchain fixture was rejected")
180 if not _recipe_errors(
"bad-cmake", bad_cmake):
181 failures.append(
"raw native CMake fixture was accepted")
182 if not _recipe_errors(
"mixed-cmake", mixed_cmake):
183 failures.append(
"raw native CMake hidden beside an ARM configure was accepted")
184 if not _recipe_errors(
"bad-cc", bad_cc):
185 failures.append(
"raw compiler fixture was accepted")
186 if _standalone_cmake(
"target_sources(app PRIVATE src/x.c)\n"):
187 failures.append(
"consumer CMake fragment was classified as standalone")
188 if not _standalone_cmake(
"project(shared LANGUAGES C)\n"):
189 failures.append(
"standalone shared CMake project was classified as a fragment")
190 with tempfile.TemporaryDirectory()
as tmp:
192 (root /
"just").mkdir()
193 (root /
"justfile").write_text(
"default:\n true\n")
194 (root /
"just" /
"future.just").write_text(bad_cmake)
195 discovered = {path.relative_to(root)
for path
in _just_files(root)}
196 if discovered != {Path(
"justfile"), Path(
"just/future.just")}:
197 failures.append(
"new Just module was omitted from discovery")
198 (root /
"tools" /
"native" /
"src").mkdir(parents=
True)
199 (root /
"tools" /
"native" /
"src" /
"main.c").write_text(
"int main(void){}\n")
200 if not _inventory_errors(root):
201 failures.append(
"compiled tool without CMake was accepted")
202 (root /
"tools" /
"native" /
"CMakeLists.txt").write_text(
"project(native C)\n")
203 if _inventory_errors(root):
204 failures.append(
"compiled tool with CMake was rejected")
205 if not _dispatcher_errors_fixture({
"native"}, set()):
206 failures.append(
"dispatcher omission was accepted")
207 if _dispatcher_errors_fixture({
"native"}, {
"native"}):
208 failures.append(
"complete dispatcher fixture was rejected")
210 print(
"check_host_build_entrypoints.py --selftest FAILED:", file=sys.stderr)
211 print(
"\n".join(f
" {failure}" for failure
in failures), file=sys.stderr)
213 print(
"check_host_build_entrypoints.py --selftest: PASS (12 both-direction cases)")
217def _dispatcher_errors_fixture(expected: set[str], listed: set[str]) -> list[str]:
218 """Pure set comparison used to prove dispatcher coverage both ways."""
220 *(f
"missing {x}" for x
in expected - listed),
221 *(f
"extra {x}" for x
in listed - expected),
226 """Run selftest or the live host-build surface audit."""
227 parser = argparse.ArgumentParser(description=__doc__)
228 parser.add_argument(
"--selftest", action=
"store_true")
229 args = parser.parse_args()
232 errors: list[str] = []
233 just_files = _just_files(REPO_ROOT)
234 for path
in just_files:
235 errors += _recipe_errors(str(path.relative_to(REPO_ROOT)), path.read_text(encoding=
"utf-8"))
236 errors += _inventory_errors(REPO_ROOT)
237 errors += _shared_dispatch_errors(REPO_ROOT)
238 listed, list_errors = _live_dispatch(REPO_ROOT)
239 errors += list_errors + _dispatcher_errors(REPO_ROOT, listed)
241 print(
"check_host_build_entrypoints.py: host build contract violations:", file=sys.stderr)
242 print(
"\n".join(f
" {error}" for error
in errors), file=sys.stderr)
245 f
"check_host_build_entrypoints.py: clean ({len(just_files)} Just files, "
246 f
"{len(listed)} compiled tools)"
251if __name__ ==
"__main__":
252 raise SystemExit(
main())
void main(void)
The application entry point Reset_Handler hands control to.