4"""Enforce repository source/include/test layout for first-party code."""
6from __future__
import annotations
12from pathlib
import Path, PurePosixPath
14SCOPED_ROOTS = frozenset({
"apps",
"examples",
"libs",
"port",
"tests",
"tools"})
15IMPLEMENTATION_SUFFIXES = frozenset({
".c",
".cc",
".cpp",
".cxx",
".m"})
16HEADER_SUFFIXES = frozenset({
".h",
".hh",
".hpp",
".hxx"})
17COMPONENT_HELPER_SUFFIXES = frozenset({
".py",
".sh"})
18COMPONENT_ROOTS = frozenset({
"apps",
"examples"})
19EXCLUDED_PARTS = frozenset({
"_deps",
"build"})
20TOOL_VENDOR_ROOT_PARTS = 3
21TOOL_FILE_MIN_PARTS = 3
23 PurePosixPath(
"libs/ra8_fonts"),
24 PurePosixPath(
"tools/vela/generated"),
26APP_OWNED_VENDORS = frozenset({
"libwebp",
"litehtml",
"miniz",
"stb",
"xz_embedded"})
27APP_COMPRESS_FILES = frozenset(
29 PurePosixPath(
"apps/shared_libs/compress/inc/ra8_compress.h"),
30 PurePosixPath(
"apps/shared_libs/compress/inc/ra8_vfs_compress.h"),
31 PurePosixPath(
"apps/shared_libs/compress/src/ra8_compress.c"),
32 PurePosixPath(
"apps/shared_libs/compress/src/ra8_vfs_compress.c"),
35LEGACY_COMPRESS_FILES = frozenset(
37 PurePosixPath(
"libs/ra8_io/inc/ra8_io_compress.h"),
38 PurePosixPath(
"libs/ra8_io/inc/ra8_io_vfs_compress.h"),
39 PurePosixPath(
"libs/ra8_io/src/ra8_io_compress.c"),
40 PurePosixPath(
"libs/ra8_io/src/ra8_io_vfs_compress.c"),
45def is_vendor_path(path: PurePosixPath) -> bool:
46 """Return whether ``path`` has a supported vendored-component shape.
48 The SBOM gate separately requires every directory under these roots to be
49 registered. A tool-private root is deliberately narrow: only
50 ``tools/<tool>/third_party/<component>`` qualifies, so a generic tools
51 vendor dumping ground cannot silently appear.
55 parts[:2] == (
"libs",
"third_party")
56 or parts[:3] == (
"apps",
"shared_libs",
"third_party")
58 len(parts) >= TOOL_VENDOR_ROOT_PARTS
59 and parts[0] ==
"tools"
60 and parts[2] ==
"third_party"
65def is_excluded(path: PurePosixPath) -> bool:
66 """Return whether a path is vendored, generated, or build output."""
67 if is_vendor_path(path)
or any(path.is_relative_to(prefix)
for prefix
in EXCLUDED_PREFIXES):
69 return any(part
in EXCLUDED_PARTS
or part.startswith(
"build-")
for part
in path.parts)
72def layout_error(path: PurePosixPath) -> str |
None:
73 """Return the layout error for one repository-relative path, if any."""
74 if not path.parts
or path.parts[0]
not in SCOPED_ROOTS
or is_excluded(path):
76 suffix = path.suffix.lower()
77 directory_parts = path.parts[1:-1]
78 if suffix
in IMPLEMENTATION_SUFFIXES
and "src" not in directory_parts:
79 return "implementation file is not under src/"
80 if suffix
in HEADER_SUFFIXES
and not ({
"inc",
"src"} & set(directory_parts)):
81 return "header is not under inc/ or src/"
85def tracked_paths(root: Path) -> list[PurePosixPath]:
86 """Return present tracked and untracked paths without ignored build output."""
87 git = shutil.which(
"git")
89 raise FileNotFoundError
90 result = subprocess.run(
91 [git,
"ls-files",
"--cached",
"--others",
"--exclude-standard"],
97 paths: list[PurePosixPath] = []
98 for line
in result.stdout.splitlines():
99 candidate = PurePosixPath(line)
100 if (root / candidate).is_file():
101 paths.append(candidate)
105def vendor_ownership_errors(existing: set[PurePosixPath]) -> list[str]:
106 """Reject app-owned vendors in the platform tree and removed vendor debris."""
107 errors: list[str] = []
108 for name
in sorted(APP_OWNED_VENDORS):
109 platform = PurePosixPath(
"libs/third_party") / name
110 app = PurePosixPath(
"apps/shared_libs/third_party") / name
111 if platform
in existing:
112 errors.append(f
"{platform}: app-owned vendor must live at {app}")
113 if app
not in existing:
114 errors.append(f
"{app}: required app-owned vendor is missing")
118def compress_ownership_errors(existing: set[PurePosixPath]) -> list[str]:
119 """Reject the retired ra8_io compression seam and require its app module."""
121 f
"{path}: compression is app-owned; remove this legacy ra8_io path"
122 for path
in sorted(LEGACY_COMPRESS_FILES & existing)
125 f
"{path}: required app-owned compression module file is missing"
126 for path
in sorted(APP_COMPRESS_FILES - existing)
131def is_python_test_module(path: PurePosixPath) -> bool:
132 """Return whether a Python filename identifies a test module."""
133 return path.stem.startswith(
"test_")
or path.stem.endswith((
"_test",
"_selftest"))
136def python_tool_layout_errors(
137 existing: set[PurePosixPath],
138) -> list[tuple[PurePosixPath, str]]:
139 """Reject flat multi-module tools and regressions from an adopted src layout."""
140 root_modules: dict[PurePosixPath, list[PurePosixPath]] = {}
141 module_counts: dict[PurePosixPath, int] = {}
142 src_roots: set[PurePosixPath] = set()
143 errors: list[tuple[PurePosixPath, str]] = []
144 for path
in sorted(existing):
147 or len(path.parts) < TOOL_FILE_MIN_PARTS
148 or path.parts[0] !=
"tools"
151 if is_excluded(path):
153 tool_root = PurePosixPath(*path.parts[:2])
154 module_counts[tool_root] = module_counts.get(tool_root, 0) + 1
155 relative = path.relative_to(tool_root)
156 if len(relative.parts) == 1:
157 root_modules.setdefault(tool_root, []).append(path)
158 elif relative.parts[0] ==
"src":
159 src_roots.add(tool_root)
160 if is_python_test_module(path):
161 errors.append((path,
"Python test module is not under tests/"))
163 for tool_root, modules
in sorted(root_modules.items()):
164 if tool_root
in src_roots:
165 reason =
"Python module is at tool root after src/ layout was adopted"
166 elif module_counts[tool_root] > 1:
167 reason =
"multi-module Python tool must place production modules under src/"
170 errors.extend((path, reason)
for path
in modules)
174def component_helper_layout_errors(
175 existing: set[PurePosixPath],
176) -> list[tuple[PurePosixPath, str]]:
177 """Reject executable helpers placed directly at app/example component roots.
179 A sibling CMakeLists.txt identifies a component root without encoding the
180 repository's varying app/example nesting depths. Build and generation
181 helpers belong under scripts/; verification gates belong under
184 errors: list[tuple[PurePosixPath, str]] = []
185 for path
in sorted(existing):
188 or path.parts[0]
not in COMPONENT_ROOTS
189 or path.suffix.lower()
not in COMPONENT_HELPER_SUFFIXES
193 if path.parent /
"CMakeLists.txt" in existing:
197 "component helper is at the component root; use scripts/ or tests/scripts/",
203def _path_layout_selftest_cases() -> dict[str, bool]:
204 """Return pass/fail results for direct C-family path classification."""
206 "examples/board/app/main.c":
True,
207 "tests/hal/test_driver.c":
True,
208 "tools/widget/widget.h":
True,
209 "apps/host/widget/src/main.c":
False,
210 "apps/host/widget/src/widget_internal.h":
False,
211 "libs/widget/inc/widget.h":
False,
212 "tests/hal/src/test_driver.c":
False,
213 "tests/hal/inc/test_fixture.h":
False,
214 "libs/third_party/vendor.c":
False,
215 "apps/shared_libs/third_party/vendor.c":
False,
216 "tools/viewer/third_party/vendor/source.c":
False,
217 "tools/viewer/src/source.c":
False,
218 "apps/shared_libs/widget/widget.c":
True,
219 "tools/vela/generated/model.h":
False,
222 name: (layout_error(PurePosixPath(name))
is not None) == should_fail
223 for name, should_fail
in expectations.items()
227def _ownership_selftest_cases() -> dict[str, bool]:
228 """Return vendor and compression ownership selftest results."""
229 correct = {PurePosixPath(
"apps/shared_libs/third_party") / name
for name
in APP_OWNED_VENDORS}
231 "correct ownership stays quiet":
not vendor_ownership_errors(correct),
232 "wrong platform root fires": bool(
233 vendor_ownership_errors(
234 correct - {PurePosixPath(
"apps/shared_libs/third_party/miniz")}
235 | {PurePosixPath(
"libs/third_party/miniz")}
238 "app compression seam stays quiet":
not compress_ownership_errors(set(APP_COMPRESS_FILES)),
239 "legacy ra8_io compression seam fires": bool(
240 compress_ownership_errors(set(APP_COMPRESS_FILES) | set(LEGACY_COMPRESS_FILES))
245def _python_tool_selftest_cases() -> dict[str, bool]:
246 """Return flat-versus-structured Python tool layout results."""
248 "single-file Python tool stays quiet":
not python_tool_layout_errors(
249 {PurePosixPath(
"tools/solo/runner.py")}
251 "data-only tool stays quiet":
not python_tool_layout_errors(
252 {PurePosixPath(
"tools/model/weights.bin")}
254 "flat multi-module Python tool fires": bool(
255 python_tool_layout_errors(
257 PurePosixPath(
"tools/flat/main.py"),
258 PurePosixPath(
"tools/flat/helper.py"),
262 "root module plus nested helper fires": bool(
263 python_tool_layout_errors(
265 PurePosixPath(
"tools/nested/main.py"),
266 PurePosixPath(
"tools/nested/lib/helper.py"),
270 "structured Python tool stays quiet":
not python_tool_layout_errors(
272 PurePosixPath(
"tools/structured/src/main.py"),
273 PurePosixPath(
"tools/structured/src/helper.py"),
274 PurePosixPath(
"tools/structured/tests/main_selftest.py"),
277 "root module beside src fires": bool(
278 python_tool_layout_errors(
280 PurePosixPath(
"tools/structured/src/main.py"),
281 PurePosixPath(
"tools/structured/helper.py"),
285 "test module under src fires": bool(
286 python_tool_layout_errors({PurePosixPath(
"tools/structured/src/main_selftest.py")})
291def _component_helper_selftest_cases() -> dict[str, bool]:
292 """Return app/example executable-helper layout results."""
294 "app-root Python helper fires": bool(
295 component_helper_layout_errors(
297 PurePosixPath(
"apps/board/demo/CMakeLists.txt"),
298 PurePosixPath(
"apps/board/demo/generate.py"),
302 "example-root shell helper fires": bool(
303 component_helper_layout_errors(
305 PurePosixPath(
"examples/board/demo/CMakeLists.txt"),
306 PurePosixPath(
"examples/board/demo/build_payload.sh"),
310 "component scripts helper stays quiet":
not component_helper_layout_errors(
312 PurePosixPath(
"examples/board/demo/CMakeLists.txt"),
313 PurePosixPath(
"examples/board/demo/scripts/generate.py"),
316 "test gate under tests/scripts stays quiet":
not component_helper_layout_errors(
318 PurePosixPath(
"apps/board/demo/CMakeLists.txt"),
319 PurePosixPath(
"apps/board/demo/tests/scripts/emulator_gate.sh"),
325def run_selftest() -> int:
326 """Prove both rejection and acceptance directions of the layout rule."""
327 path_cases = _path_layout_selftest_cases()
328 path_failed = [name
for name, passed
in path_cases.items()
if not passed]
331 f
"check_source_layout.py: selftest failed: {', '.join(path_failed)}",
336 **_ownership_selftest_cases(),
337 **_python_tool_selftest_cases(),
338 **_component_helper_selftest_cases(),
340 policy_failed = [name
for name, passed
in policy_cases.items()
if not passed]
343 f
"check_source_layout.py: policy selftest failed: {', '.join(policy_failed)}",
347 print(f
"check_source_layout.py: selftest passed ({len(path_cases) + len(policy_cases)} cases).")
351def check_tree(root: Path) -> int:
352 """Check every first-party C-family, Python tool, and helper layout."""
354 violations: list[tuple[PurePosixPath, str]] = []
355 paths = tracked_paths(root)
357 if path.suffix.lower()
not in IMPLEMENTATION_SUFFIXES | HEADER_SUFFIXES:
359 if path.parts
and path.parts[0]
in SCOPED_ROOTS
and not is_excluded(path):
361 error = layout_error(path)
362 if error
is not None:
363 violations.append((path, error))
364 existing_paths = set(paths)
366 PurePosixPath(base, child.name)
367 for base
in (
"libs/third_party",
"apps/shared_libs/third_party")
368 for child
in (root / base).iterdir()
371 ownership_errors = vendor_ownership_errors(vendor_dirs) + compress_ownership_errors(
374 python_violations = python_tool_layout_errors(existing_paths)
375 component_helper_violations = component_helper_layout_errors(existing_paths)
376 if violations
or ownership_errors
or python_violations
or component_helper_violations:
377 print(
"Source layout violations:", file=sys.stderr)
378 for path, error
in violations:
379 print(f
" {path}: {error}", file=sys.stderr)
380 for error
in ownership_errors:
381 print(f
" {error}", file=sys.stderr)
382 for path, error
in python_violations:
383 print(f
" {path}: {error}", file=sys.stderr)
384 for path, error
in component_helper_violations:
385 print(f
" {path}: {error}", file=sys.stderr)
387 "Move code to src/, public C headers to inc/, tests to tests/, "
388 "and component helpers to scripts/ or tests/scripts/.",
392 print(f
"check_source_layout.py: {checked} first-party C-family file(s) follow layout.")
397 """Parse arguments and run the requested validation mode."""
398 parser = argparse.ArgumentParser(description=__doc__)
399 parser.add_argument(
"--selftest", action=
"store_true")
400 args = parser.parse_args()
402 return run_selftest()
403 return check_tree(Path(__file__).resolve().parents[2])
406if __name__ ==
"__main__":
407 raise SystemExit(
main())
void main(void)
The application entry point Reset_Handler hands control to.