4"""Gate: the tier dependency arrow points one way, and nothing may reverse it.
6The tree has three tiers (#718), and the products tier has an internal layer of
9* PLATFORM -- ``libs/``, ``port/``, ``tools/``. General-purpose,
10 reusable, knowing nothing about any one product.
11* PRODUCTS -- ``apps/``, split by FORM. ``apps/host/`` holds hosted products,
12 ``apps/board/`` holds firmware products, and ``apps/shared_libs/`` holds the
13 portable product-tier code both forms consume.
14* CONSUMERS -- ``examples/`` and ``tests/``, which exist to demonstrate and to
15 compile the other two and therefore legitimately name any path in the tree.
17Two rules follow from that shape, and this gate is both of them:
19**Rule 1 -- the platform never imports a product.** Nothing under a platform
20root may ``#include`` a header from ``apps/`` or name an ``apps/`` path in a
21CMake source list or include directory. Any category: a platform file reaching
22into ``apps/shared_libs/`` is the same defect as one reaching into
23``apps/board/``. The moment it does, the platform has stopped being
24general-purpose and the tier boundary is a comment rather than a fact.
26**Rule 2 -- shared product code never imports a product FORM.**
27``apps/shared_libs/`` sits below ``apps/host/`` and ``apps/board/``:
28the forms consume shared freely, and shared must not reach back up. Shared code
29that knows which form is compiling it is not shared, it is a copy of one form
32Both rules are the same shape -- a layer, and the region it may not reach into
33-- so both are expressed as one ``Layer`` row and scanned by one scanner. That
34model lives in ``tier_layers.py``: the layers, their roots, the populations the
35non-vacuity floors are asserted on, and the exclusive-basename census. This
36file is the scanner and the gate entry point. The split is the reason the rule
37set is keyed on CATEGORY DIRECTORY NAMES rather than on a file list -- a
38product moving between categories is a change to the model and to nothing here,
39so the gate keeps working across a layout change.
41``tier_layers.py`` carries no selftest of its own by design: it is a model, not
42a detector, and every predicate and floor in it is proved in both directions
43below, through the same entry point CI drives.
45TWO HALVES, BECAUSE THERE ARE TWO WAYS IN
46-----------------------------------------
48* **Includes.** A C/C++ file must not ``#include`` a header from a forbidden
50* **CMake.** A listfile must not name a forbidden region's path in a source
51 list, an include directory, or anywhere else -- compiling another layer's
52 translation unit into your target is the same coupling in another language.
54PRECISION VERSUS RECALL
55-----------------------
57Calibrated for ZERO false positives on the current tree, because a boundary
58gate that cries wolf gets bypassed and then the boundary is gone again. Three
59deliberate choices buy that:
611. **Comments are stripped first, in both languages.** A naive ``grep`` for
62 ``apps/`` over the platform listfiles reports three hits today
63 (``tools/rabook_imagepack/CMakeLists.txt``, ``cmake/ra8_app/sources.cmake``,
64 ``cmake/ra8_webp_vendor.cmake``) and all three are PROSE -- comments
65 explaining which producers exist and which two files once faked a WebP
66 symbol. Those are exactly the false positives that would sink the gate, so
67 ``#`` and ``#[[ ]]`` comments in CMake, and ``//`` and block comments in C,
68 are removed before matching.
692. **The include directive is anchored to the start of its line.** A C string
70 literal containing the text of an include (a code generator emitting C)
71 cannot start a line with ``#``, so the anchor costs no real recall and
72 removes a whole false-positive class without needing a full C lexer.
733. **The bare-name rule is keyed on an EXCLUSIVE basename census.** A file that
74 writes ``#include "mdl_cache.h"`` names no directory, yet it can only ever
75 resolve to a product header. The gate therefore builds a repo-wide
76 header-basename census first and flags a bare include only when its basename
77 exists inside the forbidden region and NOWHERE else in the tree. Measured
78 2026-08-17 for Rule 1: 52 headers under ``apps/``, and the intersection with
79 the basenames of every other header in the repository -- first-party,
80 vendored SOUP and generated alike -- is EMPTY, so every one of the 52 is
83The recall this gives up is stated rather than hidden:
85* A bare include whose basename ALSO exists outside the forbidden region is not
86 flagged. Under every include-directory ordering this tree's builds use, such
87 an include resolves to the legal copy, so flagging it would be a guess about
88 the build rather than a fact about the source.
89* Include search paths are not replayed per translation unit. Doing that would
90 make the gate depend on a configured compile database, and therefore on a
91 build succeeding, which is precisely how a checker ends up silently scoped to
92 whatever last configured.
93* Rule 2's bare-name half is only as strong as the forms' exclusive census, and
94 that census is legitimately allowed to be EMPTY -- ``apps/shared_libs/`` may hold
95 every header while a form is a single ``main.c``. Rule 2's literal-path and
96 CMake halves do not depend on the census and are always live, which is why
97 the non-vacuity floors below apply to the tier populations rather than to
100What is caught with no ambiguity at all is the literal form: any include whose
101path carries the forbidden region's DIRECTORY components -- ``apps/...``,
102``../../apps/...``, ``apps/board/...`` -- fires regardless of the census.
103No platform path in this tree contains a component named ``apps`` (measured:
104zero), so that rule cannot misfire either.
106THE CMAKE EXEMPTION IS ENUMERATED, NOT WILDCARDED
107-------------------------------------------------
109Orchestration has to be able to name a product: something must eventually
110``add_subdirectory()`` one. That permission is granted to an explicit list of
111exact paths (``ORCHESTRATION_EXEMPT``), never to a pattern, and even inside
112those files it is narrow: only an ``add_subdirectory`` line may name a
113forbidden region. A source list or an include directory in an exempt file still
114fails, because "this file is allowed to know a product exists" is a different
115claim from "this file is allowed to compile one".
119 check_tier_imports.py --all # the gate: sweep every ruled layer
120 check_tier_imports.py FILE ... # scan named files
121 check_tier_imports.py --selftest # prove it in both directions
123Exit 0 when clean, 1 on a tier violation, 2 when the scan itself collapsed.
126from __future__
import annotations
133from collections.abc
import Iterable
134from pathlib
import Path
136sys.path.insert(0, str(Path(__file__).resolve().parent))
138from selftest_assert
import expect, report
139from tier_layers
import (
144 CMAKE_TOTAL_FILE_FLOOR,
145 EXEMPT_CONSUMER_ROOTS,
147 ORCHESTRATION_EXEMPT,
167INCLUDE_RE = re.compile(
r'^\s*#\s*include\s*([<"])([^>"]+)[>"]')
169ADD_SUBDIRECTORY_RE = re.compile(
r"\badd_subdirectory\s*\(")
172KIND_HEADER =
"HEADER"
180Finding = tuple[str, str, str, int, str, str]
183Target = tuple[Path, str]
186def strip_c_comments(text: str) -> list[str]:
187 """Return `text`'s lines with `//` and `/* */` comment content removed.
189 String literals are left intact -- unlike the shared ``blank_noncode``
190 lexer, which blanks them and would erase the very ``"header.h"`` operand
194 text: Whole C/C++ translation unit or header.
197 One string per input line, comment bytes dropped, so line numbers still
198 index the original file.
200 lines: list[str] = []
202 for raw
in text.splitlines():
205 while index < len(raw):
206 pair = raw[index : index + 2]
208 in_block = pair !=
"*/"
209 index += 1
if in_block
else 2
216 kept.append(raw[index])
218 lines.append(
"".join(kept))
222def _first_hash_outside_quotes(line: str) -> int:
223 """Return the index of the first `#` not inside a double-quoted string.
226 line: One CMake source line, bracket comments already removed.
229 The index, or -1 when the line carries no line comment.
233 for index, char
in enumerate(line):
239 in_quote =
not in_quote
240 elif char ==
"#" and not in_quote:
245def strip_cmake_comments(text: str) -> list[str]:
246 """Return `text`'s lines with CMake line and bracket comments removed.
249 text: Whole ``CMakeLists.txt`` or ``*.cmake`` file.
252 One string per input line, so line numbers still index the original.
254 lines: list[str] = []
256 for raw
in text.splitlines():
259 end = line.find(
"]]")
263 line = line[end + 2 :]
265 start = line.find(
"#[[")
267 end = line.find(
"]]", start + 3)
272 line = line[:start] + line[end + 2 :]
273 hash_index = _first_hash_outside_quotes(line)
274 lines.append(line
if hash_index < 0
else line[:hash_index])
279 target: str, forbidden: tuple[str, ...], exclusive: frozenset[str]
280) -> tuple[str, str] |
None:
281 """Classify one include operand against a layer's forbidden regions.
284 target: The text between the quotes or angle brackets.
285 forbidden: Region prefixes the including layer may not reach into.
286 exclusive: Header basenames that exist inside those regions and nowhere
287 else in the repository.
290 ``(kind, what was named)`` for a violation, or None when the include is
291 legal. ``PATH`` means the operand spells the region's directory
292 components; ``HEADER`` means its basename can only be a header from it.
294 parts = [part
for part
in target.replace(
"\\",
"/").split(
"/")
if part
not in (
"",
".")]
295 while parts
and parts[0] ==
"..":
299 for prefix
in forbidden:
300 region = region_parts(prefix)
302 for start
in range(len(parts) - span):
303 if tuple(parts[start : start + span]) == region:
304 return (KIND_PATH, prefix +
"/".join(parts[start + span :]))
305 if parts[-1]
in exclusive:
306 return (KIND_HEADER, parts[-1])
310def scan_c_text(text: str, rel: str, layer: Layer, exclusive: frozenset[str]) -> list[Finding]:
311 """Return every cross-layer include in one C-family file.
315 rel: Repo-relative display path.
316 layer: The ruled layer that owns the file.
317 exclusive: Exclusive header basenames for that layer's forbidden
321 One finding per offending ``#include`` line.
323 findings: list[Finding] = []
324 for lineno, line
in enumerate(strip_c_comments(text), 1):
325 match = INCLUDE_RE.match(line)
328 verdict = classify_include(match.group(2).strip(), layer.forbidden, exclusive)
329 if verdict
is not None:
330 kind, named = verdict
331 findings.append((layer.name, kind, rel, lineno, named, line.strip()))
335def scan_cmake_text(text: str, rel: str, layer: Layer) -> list[Finding]:
336 """Return every cross-layer path reference in one CMake listfile.
340 rel: Repo-relative display path; decides orchestration exemption.
341 layer: The ruled layer that owns the listfile.
344 One finding per offending line. In an ``ORCHESTRATION_EXEMPT`` listfile
345 an ``add_subdirectory`` line is allowed and everything else still
348 exempt = normalize_rel(rel)
in ORCHESTRATION_EXEMPT
349 findings: list[Finding] = []
350 for lineno, line
in enumerate(strip_cmake_comments(text), 1):
351 for prefix
in layer.forbidden:
352 match = CMAKE_REGION_RES[prefix].
search(line)
355 if exempt
and ADD_SUBDIRECTORY_RE.search(line):
357 named = line[match.start() :].split()[0].rstrip(
")\"'")
358 findings.append((layer.name, KIND_CMAKE, rel, lineno, named, line.strip()))
363def _git_working_tree() -> list[str]:
364 """Enumerate tracked and newly-added (non-ignored) paths.
367 Repo-relative paths, so a brand-new file is judged the moment it is
368 written rather than the moment it is committed.
371 SystemExit: When git cannot enumerate the working tree; a gate that
372 cannot see the tree must fail, never report clean.
374 proc = subprocess.run(
375 [
"git",
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard"],
381 if proc.returncode != 0:
382 sys.stderr.write(proc.stderr)
383 sys.stderr.write(
"check_tier_imports.py: FATAL -- git working-tree enumeration failed\n")
384 raise SystemExit(EXIT_VACUOUS)
385 return [rel
for rel
in proc.stdout.split(
"\0")
if rel]
388def _working_scope() -> tuple[list[Target], list[Target], dict[str, frozenset[str]]]:
389 """Enumerate every ruled layer and prove the enumeration is not vacuous.
392 The C-family targets, the CMake targets, and each layer's exclusive
393 header-basename census.
396 SystemExit: When any non-vacuity floor is violated.
398 rels = _git_working_tree()
399 errors = census_floor_errors(measure(rels))
401 sys.stderr.write(
"check_tier_imports.py: FATAL -- " +
"; ".join(errors) +
"\n")
402 raise SystemExit(EXIT_VACUOUS)
403 ordered = sorted(rels)
405 (REPO_ROOT / rel, normalize_rel(rel))
for rel
in ordered
if layer_for_c(rel)
is not None
408 (REPO_ROOT / rel, normalize_rel(rel))
for rel
in ordered
if layer_for_cmake(rel)
is not None
411 [target
for target
in c_targets
if target[0].is_file()],
412 [target
for target
in cmake_targets
if target[0].is_file()],
413 build_exclusive(rels),
417def _explicit_scope(raw_paths: Iterable[str]) -> tuple[list[Target], list[Target]]:
418 """Filter caller-named files through the same layer-ownership policy.
421 raw_paths: Paths from argv.
424 The in-scope C-family targets and CMake targets.
426 c_targets: list[Target] = []
427 cmake_targets: list[Target] = []
428 for raw
in raw_paths:
430 absolute = path
if path.is_absolute()
else REPO_ROOT / path
432 rel = absolute.resolve().relative_to(REPO_ROOT).as_posix()
435 if not absolute.is_file():
437 if layer_for_c(rel)
is not None:
438 c_targets.append((absolute, rel))
439 elif layer_for_cmake(rel)
is not None:
440 cmake_targets.append((absolute, rel))
441 return sorted(set(c_targets)), sorted(set(cmake_targets))
445 c_targets: Iterable[Target],
446 cmake_targets: Iterable[Target],
447 exclusive: dict[str, frozenset[str]],
448) -> tuple[int, list[Finding]]:
449 """Scan both halves of every ruled layer.
451 This is the one scanning entry point ``--all``, an explicit file list and
452 ``--selftest`` all drive, so the selftest cannot prove a code path CI does
453 not run. Layer ownership is decided HERE, so handing it a file from an
454 unruled layer (a product form, an example, a test) is a no-op -- which is
455 what makes "a form including a shared header stays quiet" a property of the
456 scanner rather than of the caller's filtering.
459 c_targets: Candidate C-family files.
460 cmake_targets: Candidate CMake listfiles.
461 exclusive: Per-layer exclusive header-basename census.
464 The number of files actually scanned and every finding, in scan order.
466 findings: list[Finding] = []
468 for path, rel
in c_targets:
469 layer = layer_for_c(rel)
472 text = path.read_text(encoding=
"utf-8", errors=
"replace")
473 findings.extend(scan_c_text(text, rel, layer, exclusive.get(layer.name, frozenset())))
475 for path, rel
in cmake_targets:
476 layer = layer_for_cmake(rel)
479 text = path.read_text(encoding=
"utf-8", errors=
"replace")
480 findings.extend(scan_cmake_text(text, rel, layer))
482 return scanned, findings
485def _write(root: Path, rel: str, text: str) -> Target:
486 """Materialise one selftest fixture and return it as a scan target.
489 root: Temporary directory standing in for the repository root.
490 rel: Repo-relative path the fixture pretends to occupy.
494 The ``(path, rel)`` pair ``scan_targets`` consumes.
497 path.parent.mkdir(parents=
True, exist_ok=
True)
498 path.write_text(text, encoding=
"utf-8")
504 "libs/ra8_tier/src/literal.c":
'#include "apps/host/mdl/inc/mdl_cli.h"\n',
505 "libs/ra8_tier/src/shared.c":
'#include "apps/board/ereader/inc/ereader.h"\n',
506 "libs/ra8_tier/src/relative.c":
'#include "../../../apps/board/thing/inc/mdl_cache.h"\n',
507 "libs/ra8_tier/src/bare.c":
'#include "mdl_cli.h"\n',
508 "tools/tier_tool/src/angle.c":
"#include <mdl_cli.h>\n",
509 "port/posix/src/nested.c":
'#include "apps/host/mdl/inc/mdl_cli.h"\n',
510 "libs/ra8_secure_app/src/deep.c":
'#include "apps/board/dl/inc/dl.h"\n',
515 "apps/shared_libs/mdl/src/up_standalone.c": (
'#include "apps/host/mdl/inc/mdl_cli.h"\n'),
516 "apps/shared_libs/mdl/src/up_threadx.c": (
517 '#include "apps/board/threadx_modules/downloader/inc/dl_module.h"\n'
519 "apps/shared_libs/mdl/src/up_bare.c":
'#include "mdl_cli.h"\n',
523 "libs/ra8_tier/CMakeLists.txt": (
524 "target_sources(ra8_tier PRIVATE\n ${FW_ROOT}/apps/host/mdl/src/mdl_cli.c)\n"
526 "cmake/tier.cmake": (
"target_include_directories(t PRIVATE ${FW_ROOT}/apps/host/mdl/inc)\n"),
529 "CMakeLists.txt":
"target_sources(all PRIVATE apps/host/mdl/src/main.c)\n",
530 "apps/shared_libs/mdl/CMakeLists.txt": (
531 "target_sources(mdl_core PRIVATE ${FW_ROOT}/apps/host/mdl/src/mdl_cli.c)\n"
536def _selftest_must_fire(
537 root: Path, exclusive: dict[str, frozenset[str]], failures: list[str]
539 """Prove every violation shape, for both rules, reaches the real scanner.
542 root: Temporary fixture root.
543 exclusive: Census the fixtures are written against.
544 failures: Accumulator from ``selftest_assert``.
546 for label, fixtures
in ((
"rule 1", _FIRE_C_RULE1), (
"rule 2", _FIRE_C_RULE2)):
547 for rel, text
in fixtures.items():
548 _, found = scan_targets([_write(root, rel, text)], [], exclusive)
549 expect(len(found) == 1, f
"{label} include fires: {rel}", failures)
550 for rel, text
in _FIRE_CMAKE.items():
551 _, found = scan_targets([], [_write(root, rel, text)], exclusive)
552 expect(len(found) == 1, f
"listfile fires: {rel}", failures)
556 "libs/ra8_tier/src/legal.c": (
557 '#include "ra8_attributes.h"\n'
558 '#include "ra8_check.h"\n'
559 "#include <stdint.h>\n"
560 '/* #include "apps/host/mdl/inc/mdl_cache.h" -- not compiled */\n'
561 '// #include "mdl_cache.h"\n'
562 'static const char *k_help = "#include \\"mdl_cache.h\\"";\n'
563 'static const char *k_path = "apps/host/mdl";\n'
565 "libs/ra8_tier/src/lookalike.c":
'#include "myapps/thing.h"\n#include "ra8_apps_registry.h"\n',
567 "apps/shared_libs/mdl/src/legal.c": (
568 '#include "ra8_check.h"\n'
569 '#include "apps/shared_libs/mdl/inc/mdl_cache.h"\n'
570 '#include "mdl_cache.h"\n'
571 "#include <stdint.h>\n"
575 "apps/host/mdl/src/main.c": (
576 '#include "apps/shared_libs/mdl/inc/mdl_cache.h"\n#include "mdl_cache.h"\n'
578 "apps/board/threadx_modules/downloader/src/mod.c": (
579 '#include "apps/shared_libs/mdl/inc/mdl_cache.h"\n'
584 "tools/rabook_imagepack/CMakeLists.txt": (
585 "# The single-unit counterpart to the batch producers "
586 "(apps/host/mdl, ...)\n"
587 "add_executable(rabook_imagepack src/main.c)\n"
589 "cmake/ra8_webp_vendor.cmake": (
590 "#[[ tools/rabook_imagepack and apps/host/mdl each faked\n"
591 " jof_priv_webp_transcode() ]]\n"
592 "add_library(ra8_webp STATIC ${RA8_WEBP_SRCS})\n"
594 "CMakeLists.txt":
"add_subdirectory(apps/host/mdl)\n",
595 "libs/ra8_tier/CMakeLists.txt":
"target_sources(ra8_tier PRIVATE src/tier.c)\n",
596 "apps/shared_libs/mdl/CMakeLists.txt": (
597 "target_sources(mdl_core PRIVATE src/mdl_hash.c)\n"
598 "target_include_directories(mdl_core PUBLIC ${FW_ROOT}/apps/shared_libs/mdl/inc)\n"
600 "apps/host/mdl/CMakeLists.txt": (
601 "target_sources(mdl PRIVATE ${FW_ROOT}/apps/shared_libs/mdl/src/mdl_hash.c)\n"
606def _selftest_must_stay_quiet(
607 root: Path, exclusive: dict[str, frozenset[str]], failures: list[str]
609 """Prove legal code, legal orchestration and prose produce no finding.
612 root: Temporary fixture root.
613 exclusive: Census the fixtures are written against.
614 failures: Accumulator from ``selftest_assert``.
616 for rel, text
in _QUIET_C.items():
617 _, found = scan_targets([_write(root, rel, text)], [], exclusive)
618 expect(
not found, f
"legal source stays quiet: {rel}", failures)
619 for rel, text
in _QUIET_CMAKE.items():
620 _, found = scan_targets([], [_write(root, rel, text)], exclusive)
621 expect(
not found, f
"legal listfile stays quiet: {rel}", failures)
624def _selftest_scope(failures: list[str]) ->
None:
625 """Prove the layer partition: ruled layers in, forms and consumers out."""
626 for root
in PLATFORM_C_ROOTS:
627 layer = layer_for_c(f
"{root}mod/src/thing.c")
629 layer
is not None and layer.name == PLATFORM_LAYER_NAME, f
"{root} is platform", failures
631 shared = layer_for_c(
"apps/shared_libs/mdl/src/core.c")
633 shared
is not None and shared.name == SHARED_LAYER_NAME,
634 "apps/shared_libs is the product-shared layer",
637 for category
in FORM_CATEGORIES:
639 layer_for_c(f
"{category}thing/src/main.c")
is None,
640 f
"{category} is a form and consumes freely",
643 for root
in EXEMPT_CONSUMER_ROOTS:
644 expect(layer_for_c(f
"{root}app/main.c")
is None, f
"{root} is an exempt consumer", failures)
646 layer_for_cmake(f
"{root}cmake/unit_tests.cmake")
is None,
647 f
"{root} listfiles are exempt consumers",
651 layer_for_c(
"apps/shared_libs/third_party/miniz/miniz.c")
is None,
652 "vendored SOUP is out",
656 layer_for_c(
"tools/foo/build/CMakeFiles/probe.c")
is None,
"build output is out", failures
658 expect(layer_for_cmake(
"CMakeLists.txt")
is not None,
"the root listfile is scanned", failures)
660 layer_for_cmake(
"cmake/ra8_add_app.cmake")
is not None,
661 "the shared cmake modules are scanned",
665 "cmake/ra8_add_app.cmake" not in ORCHESTRATION_EXEMPT,
666 "the exemption does not cover the shared cmake modules",
671def _selftest_census(failures: list[str]) ->
None:
672 """Prove the exclusive-basename census and its ambiguity rule."""
674 "apps/shared_libs/mdl/inc/mdl_cache.h",
675 "apps/host/mdl/inc/mdl_cli.h",
676 "apps/host/mdl/inc/shared_name.h",
677 "apps/shared_libs/mdl/inc/shared_name.h",
678 "libs/ra8_core/inc/ra8_check.h",
679 "apps/shared_libs/mdl/build/CMakeFiles/generated.h",
681 census = build_exclusive(rels)
683 census[PLATFORM_LAYER_NAME] == {
"mdl_cli.h"},
684 "rule 1's census is every form-exclusive header basename",
688 census[SHARED_LAYER_NAME] == {
"mdl_cli.h"},
689 "rule 2's census is form-exclusive only -- a shared sibling is not in it",
693 "generated.h" not in census[PLATFORM_LAYER_NAME],
694 "build output never enters the census",
698 build_exclusive([r
for r
in rels
if not r.startswith(SHARED_CATEGORY)])[SHARED_LAYER_NAME]
699 == {
"mdl_cli.h",
"shared_name.h"},
700 "an EMPTY apps/shared_libs is a legal layout the census still handles",
704 classify_include(
"apps/x/y.h", (PRODUCTS_ROOT,), frozenset())[0] == KIND_PATH,
705 "the literal path rule needs no census",
709 classify_include(
"shared_name.h", FORM_CATEGORIES, census[SHARED_LAYER_NAME])
is None,
710 "an ambiguous bare include is not a finding",
714 classify_include(
"apps/shared_libs/x.h", FORM_CATEGORIES, frozenset())
is None,
715 "shared is not forbidden to itself",
720_MEASURED_C_COUNTS = {
"libs/": 938,
"port/": 98,
"src/": 16,
"tools/": 214}
722_MEASURED_APPS_C = 137
723_MEASURED_APPS_HEADERS = 52
726def _selftest_floors(failures: list[str]) ->
None:
727 """Prove every non-vacuity floor bites, and a healthy census does not."""
731 c_counts: dict[str, int] |
None =
None,
732 cmake_count: int = _MEASURED_CMAKE,
733 apps_c_count: int = _MEASURED_APPS_C,
734 apps_header_count: int = _MEASURED_APPS_HEADERS,
736 """Build a Census from the measured tree with one value perturbed."""
738 c_counts=dict(_MEASURED_C_COUNTS)
if c_counts
is None else c_counts,
739 cmake_count=cmake_count,
740 apps_c_count=apps_c_count,
741 apps_header_count=apps_header_count,
744 expect(
not census_floor_errors(census()),
"the measured census clears every floor", failures)
745 narrowed = dict(_MEASURED_C_COUNTS)
746 narrowed[
"port/"] = C_ROOT_FILE_FLOORS[
"port/"] - 1
748 bool(census_floor_errors(census(c_counts=narrowed))),
749 "a narrowed platform root fails",
754 bool(census_floor_errors(census(c_counts=dict(C_ROOT_FILE_FLOORS)))),
755 "the aggregate platform C floor fails",
759 bool(census_floor_errors(census(cmake_count=CMAKE_TOTAL_FILE_FLOOR - 1))),
760 "a collapsed platform listfile scan fails",
764 bool(census_floor_errors(census(apps_c_count=APPS_C_FILE_FLOOR - 1))),
765 "a collapsed PRODUCTS census fails independently of the platform one",
769 bool(census_floor_errors(census(apps_header_count=APPS_HEADER_FLOOR - 1))),
770 "a collapsed products header census fails, so bare-name cannot go vacuous",
775def selftest() -> int:
776 """Prove both rules fire, stay quiet, partition the layers, and are not vacuous.
779 0 when every assertion held, 1 otherwise.
781 print(
"check_tier_imports.py --selftest")
782 failures: list[str] = []
784 PLATFORM_LAYER_NAME: frozenset({
"mdl_cache.h",
"mdl_cli.h",
"dl_module.h"}),
785 SHARED_LAYER_NAME: frozenset({
"mdl_cli.h",
"dl_module.h"}),
787 with tempfile.TemporaryDirectory()
as tmp:
789 _selftest_must_fire(root, exclusive, failures)
790 _selftest_must_stay_quiet(root, exclusive, failures)
791 _selftest_scope(failures)
792 _selftest_census(failures)
793 _selftest_floors(failures)
794 return report(failures)
797def _report(scanned: int, findings: list[Finding]) -> int:
798 """Print the zero-baseline verdict and return its exit status.
802 findings: Every tier violation.
805 0 when clean, 1 when a layer reached into a region above it.
808 print(f
"check_tier_imports.py: {scanned} file(s) across the ruled layers, 0 violations.")
811 "check_tier_imports.py: tier boundary violated -- the platform must not import "
812 "apps/, and apps/shared_libs/ must not import a product form. Move the shared "
813 "code down (libs/ or apps/shared_libs/) or invert the dependency:\n"
815 for layer, kind, rel, lineno, named, source
in findings:
816 sys.stderr.write(f
" {rel}:{lineno}: [{layer}/{kind}] {named}\n {source}\n")
817 sys.stderr.write(f
"\n{len(findings)} finding(s); baseline is zero.\n")
818 return EXIT_VIOLATION
821def main(argv: list[str]) -> int:
822 """Dispatch the selftest, the full layer sweep, or an explicit scan.
828 0 clean, 1 violation, 2 usage or collapsed scan.
830 parser = argparse.ArgumentParser(description=
"tier-import boundary gate")
831 parser.add_argument(
"--all", action=
"store_true", help=
"sweep every ruled layer")
832 parser.add_argument(
"--selftest", action=
"store_true", help=
"prove the gate both ways")
833 parser.add_argument(
"files", nargs=
"*", help=
"explicit files")
834 args = parser.parse_args(argv[1:])
836 if args.all
or args.files:
837 parser.error(
"--selftest accepts no other arguments")
839 if args.all
and args.files:
840 parser.error(
"--all accepts no explicit files")
841 if not args.all
and not args.files:
842 parser.error(
"provide --all or at least one file")
844 c_targets, cmake_targets, exclusive = _working_scope()
846 c_targets, cmake_targets = _explicit_scope(args.files)
847 exclusive = build_exclusive(_git_working_tree())
849 scanned, findings = scan_targets(c_targets, cmake_targets, exclusive)
850 except OSError
as exc:
851 sys.stderr.write(f
"check_tier_imports.py: FATAL -- {exc}\n")
853 return _report(scanned, findings)
856if __name__ ==
"__main__":
857 raise SystemExit(
main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.