ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_tool_warning_flags.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Assert the host tools compile first-party sources at the project warning bar.
5
6Why this exists
7---------------
8``apps/host/mdl`` compiled ``apps/shared_libs/jof/*``, ``libs/ra8_hal/ra8_jpeg_sw*``,
9``apps/shared_libs/compress/src/ra8_compress.c`` and three ``libs/ra8_core`` files under a
10blanket ``-w``, with a comment claiming "the repo lints these via its own
11build". It does not: ``-w`` turns off every diagnostic, so the one build in
12the tree that compiles those files for a 64-bit host could not report a
13host-only finding, and neither tool carried ``-Werror`` at all (#309).
14Removing the ``-w`` is a one-line edit that nothing stops a later change from
15re-adding, and it would come back silently -- the build would still pass.
16
17So the removal is checked, not just done. The check reads the compile
18database CMake emits, which records the exact argv per translation unit, and
19holds every first-party source in it to NASA Power of 10 Rule 10:
20
21 * blanket ``-Werror`` is present, and
22 * blanket ``-w`` is absent.
23
24Vendored SOUP (``libs/third_party/``) and generated data (``libs/ra8_fonts/``) are
25exempt by construction -- they are not hand-authored here and CLAUDE.md scopes
26the style bar to first-party code.
27
28Precision
29---------
30The traps this deliberately avoids, each of which a substring test gets wrong:
31
32 * ``-Werror=return-type`` promotes ONE diagnostic and is not blanket
33 ``-Werror``; ``"-Werror" in command`` would wrongly accept it.
34 * ``-Wno-error`` later on the line cancels an earlier ``-Werror``.
35 * ``-Wall`` / ``-Wwrite-strings`` are not ``-w``; a prefix test would
36 wrongly reject them.
37
38Matching is therefore on exact argv arguments, in order, never on substrings.
39
40Second compiler arm (#356)
41--------------------------
42gcc-14 catches warning families clang-18 misses -- its ``-Wformat-truncation``
43found a silent PATH_MAX path-join truncation in ``apps/host/mdl`` that clang
44did not flag. The tools-build gate therefore compiles the host tools under BOTH
45pinned compilers and passes both sets of databases here. ``--require-compilers
46clang,gcc`` makes a silently-dropped arm a hard failure rather than a vacuous
47pass (the #348/#355 class): each named family must drive at least one database.
48
49The apps tier (#718)
50--------------------
51``--require-all-cmake-tools`` discovers the host CMake projects itself so the
52gate cannot quietly stop covering one. When the products tier landed, that
53discovery predated it: the ``apps/*/*/CMakeLists.txt`` glob swept in
54``apps/board/stand_alone/ereader`` -- a cross-compiled TrustZone image no host build
55produces -- and the gate failed demanding a tools-build database that must
56never exist. Firmware products are now excluded by
57``lint_targets.firmware_app_dirs()``, the tree's one definition of "linked into
58an image rather than started by a C runtime", and representation is decided by
59the sources the databases record rather than by build-tree names, because two
60products may legitimately share a name across categories.
61
62Usage
63-----
64 check_tool_warning_flags.py COMPILE_COMMANDS_JSON [...]
65 check_tool_warning_flags.py --require-compilers clang,gcc COMPILE_COMMANDS_JSON [...]
66 check_tool_warning_flags.py --require-all-cmake-tools COMPILE_COMMANDS_JSON [...]
67 check_tool_warning_flags.py --list-missing-cmake-tools COMPILE_COMMANDS_JSON [...]
68 check_tool_warning_flags.py --selftest
69"""
70
71from __future__ import annotations
72
73import argparse
74import json
75import posixpath
76import re
77import shlex
78import sys
79import tempfile
80from collections.abc import Iterable
81from pathlib import Path
82
83sys.path.insert(0, str(Path(__file__).resolve().parent))
84
85from lint_targets import firmware_app_dirs, is_build_output # needs the sys.path line above
86
87# Path fragments whose sources are not hand-authored under this project's
88# style rules. Kept identical in spirit to check_no_silent_stubs.py's EXCLUDED.
89EXEMPT_FRAGMENTS = ("libs/third_party/", "apps/shared_libs/third_party/", "libs/ra8_fonts/")
90
91#: Non-vacuity floor on host-project discovery. Measured 12 on the current tree:
92#: seven under ``tools/`` plus five real buildable projects under ``apps/``,
93#: with firmware products and source-only libraries excluded. Source-only
94#: libraries may retain a comment-only ``CMakeLists.txt`` as a composition note;
95#: their consuming product/test is what compiles them. The floor exists because
96#: a collapsed glob reports "no project is missing" and reads as a pass.
97#: At-count, not below: this is a trip-wire on discovery, not a policy on
98#: project count.
99MIN_CMAKE_TOOL_PROJECTS = 12
100
101CMAKE_COMMAND_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*[ \t]*\‍(")
102CMAKE_BRACKET_COMMENT_RE = re.compile(r"#\‍[(=*)\‍[.*?\‍]\1\‍]", re.DOTALL)
103
104
105def is_exempt(source: str) -> bool:
106 """True when `source` is vendored SOUP or generated data."""
107 normalised = source.replace("\\", "/")
108 return any(fragment in normalised for fragment in EXEMPT_FRAGMENTS)
109
110
111def tokens_of(entry: dict) -> list[str]:
112 """Return the argv of one compile-database entry.
113
114 CMake emits either `arguments` (a list) or `command` (a single string)
115 depending on generator and version; both spellings are accepted.
116 """
117 if isinstance(entry.get("arguments"), list):
118 return [str(arg) for arg in entry["arguments"]]
119 return shlex.split(str(entry.get("command", "")))
120
121
122def compiler_of(argv: list[str]) -> str:
123 """Classify the compiler family driving one compile-database entry.
124
125 Returns ``"clang"`` or ``"gcc"`` for the two pinned host-tool arms, or
126 ``"other"`` for anything else. The first recognised driver token wins, so a
127 launcher prefix (``ccache gcc-14 ...``) still classifies as gcc.
128 """
129 for token in argv:
130 base = token.replace("\\", "/").rsplit("/", 1)[-1]
131 if "clang" in base:
132 return "clang"
133 if base in ("gcc", "g++") or base.startswith(("gcc-", "g++-")):
134 return "gcc"
135 return "other"
136
137
138def missing_required_compilers(seen: set[str], required: list[str]) -> list[str]:
139 """Return the required compiler families that no database exercised.
140
141 This is the #356 second-arm guard: the tools-build gate compiles the host
142 tools under clang-18 AND gcc-14 and passes both sets of databases here. If
143 the gcc arm is ever silently dropped, its family never appears in `seen`,
144 and a silently-dropped arm otherwise reads as a pass -- the #348/#355
145 failure class. ``--selftest`` asserts this fires when an arm is absent.
146 """
147 return sorted(set(required) - set(seen))
148
149
150def missing_tool_projects(projects: Iterable[str], sources: Iterable[str]) -> list[str]:
151 """Return host CMake project directories whose code the gate never compiled.
152
153 Representation is decided by the SOURCES the compile databases record, not
154 by the name of the build tree a database sits in. Two things follow, and
155 both are the point:
156
157 * two products may legitimately share a name in different categories
158 (``apps/shared_libs/mdl`` is the portable core, ``apps/host/mdl`` the
159 CLI form). A basename key cannot tell them apart, so it
160 silently reports the second covered because the first was built -- the
161 same collapse that made scan_build.sh configure one into the other's
162 cache;
163 * a project a sibling COMPOSES with ``add_subdirectory`` has no build
164 tree of its own, yet its translation units really are compiled and
165 really are held to the warning bar here. That is exactly the shared
166 mdl core, and a build-tree-name test would demand a standalone
167 build that has no reason to exist.
168
169 Paths are collapsed before matching. A form reaches its shared core through
170 a repo-root variable spelled ``${CMAKE_CURRENT_SOURCE_DIR}/../../..``, so a
171 database entry can name a source as ``.../apps/host/mdl/../../
172 ../apps/shared_libs/mdl/src/x.c`` -- which contains BOTH project paths as
173 substrings and would mark a project represented by a file that is not its
174 own.
175
176 Args:
177 projects: Repo-relative host CMake project directories.
178 sources: Every ``file`` entry across the databases handed to the gate.
179
180 Returns:
181 The project directories no database compiled a single file from,
182 sorted.
183 """
184 normalised = {posixpath.normpath(str(source).replace("\\", "/")) for source in sources}
185 return sorted(
186 project
187 for project in projects
188 if not any(
189 f"/{project}/" in source or source.startswith(f"{project}/") for source in normalised
190 )
191 )
192
193
194def cmake_listfile_has_commands(path: Path) -> bool:
195 """Return whether ``path`` contains an active CMake command.
196
197 A source-only library can keep a ``CMakeLists.txt`` as a documented
198 composition boundary without defining a standalone configuration. CMake
199 accepts that comment-only file, but it emits no compile database, so
200 treating its directory as a host project makes the tools gate demand a
201 build that cannot represent anything.
202
203 Command-looking text inside line or bracket comments is deliberately
204 ignored. The first active command is enough: later project representation
205 is still proved from compile-database sources by ``missing_tool_projects``.
206 """
207 text = CMAKE_BRACKET_COMMENT_RE.sub("", path.read_text())
208 for raw_line in text.splitlines():
209 line = raw_line.lstrip()
210 if line.startswith("#"):
211 continue
212 if CMAKE_COMMAND_RE.match(line):
213 return True
214 return False
215
216
217def cmake_tool_projects(repo_root: Path, firmware: Iterable[str] | None = None) -> tuple[str, ...]:
218 """Discover every HOST CMake project under ``tools/`` and ``apps/``.
219
220 The two roots nest differently and the discovery has to say so. A tool is a
221 direct child of ``tools/``; products sit under ``apps/host/``,
222 ``apps/shared_libs/``, or a deeper board form such as
223 ``apps/board/stand_alone/``. A fixed-depth glob silently misses one of
224 those layouts, so apps are discovered recursively and firmware products
225 are then removed using the shared classifier below.
226
227 Matching the glob is not enough to be a host tool, though, and that is the
228 half this originally got wrong. A comment-only listfile documents a
229 source-only library's composition but defines no standalone build, and
230 therefore cannot emit the compile database this gate consumes. ``apps/``
231 also carries BOTH kinds of active build:
232 ``apps/board/stand_alone/ereader`` is a cross-compiled TrustZone image with a
233 linker script and a reset path, built by the firmware gates and by nothing
234 here, and demanding a tools-build database for it failed the gate on a
235 project that must never have one. The discriminator is not re-derived --
236 ``lint_targets.firmware_app_dirs()`` is the one definition of "linked into
237 an image rather than started by a C runtime", shared with the tier gates.
238
239 Args:
240 repo_root: Tree to discover in.
241 firmware: Repo-relative firmware app directories to exclude. Defaults
242 to the live tree's; the parameter exists so the selftest can drive
243 the rule with a fixture.
244
245 Returns:
246 The repo-relative host project directories, sorted.
247 """
248 excluded = set(firmware_app_dirs() if firmware is None else firmware)
249 candidates = [
250 *(repo_root / "tools").glob("*/CMakeLists.txt"),
251 *(repo_root / "apps").glob("**/CMakeLists.txt"),
252 ]
253 found = {
254 path.parent.relative_to(repo_root).as_posix()
255 for path in candidates
256 if not is_build_output(path.relative_to(repo_root).as_posix())
257 and cmake_listfile_has_commands(path)
258 }
259 return tuple(sorted(found - excluded))
260
261
262def flag_problem(argv: list[str]) -> str | None:
263 """Return a problem description for `argv`, or None when it is compliant.
264
265 Blanket `-Werror` must be in force at the end of the line and blanket `-w`
266 must never appear. Both are exact-token tests: `-Werror=xxx` is a targeted
267 promotion rather than the blanket flag, and `-Wno-error` cancels a blanket
268 `-Werror` that precedes it.
269 """
270 werror = False
271 for arg in argv:
272 if arg == "-Werror":
273 werror = True
274 elif arg in ("-Wno-error", "-Wno-error=all"):
275 werror = False
276 elif arg == "-w":
277 return "compiled with -w (all diagnostics disabled)"
278 if not werror:
279 return "compiled without blanket -Werror"
280 return None
281
282
283def check_database(path: Path) -> tuple[list[str], set[str], set[str]]:
284 """Return (violation lines, compiler families, compiled sources) for one database.
285
286 The compiler set is the #356 second-arm evidence: every translation unit in
287 one database is compiled by the same driver, so the union across the clang
288 and gcc databases the gate passes must contain both families.
289
290 The source set is the project-scope evidence: it is what
291 ``missing_tool_projects`` reads to decide whether a host CMake project's
292 code reached the gate at all.
293 """
294 try:
295 entries = json.loads(path.read_text())
296 except (OSError, json.JSONDecodeError) as exc:
297 sys.stderr.write(f"check_tool_warning_flags.py: FATAL -- cannot read {path}: {exc}\n")
298 raise SystemExit(2) from exc
299
300 if not entries:
301 sys.stderr.write(
302 f"check_tool_warning_flags.py: FATAL -- {path} lists no translation\n"
303 " units. An empty compile database would let this gate report a\n"
304 " pass for a build that never happened.\n"
305 )
306 raise SystemExit(2)
307
308 violations: list[str] = []
309 compilers: set[str] = set()
310 sources: set[str] = set()
311 for entry in entries:
312 argv = tokens_of(entry)
313 compilers.add(compiler_of(argv))
314 source = str(entry.get("file", ""))
315 if not source:
316 continue
317 sources.add(source)
318 if is_exempt(source):
319 continue
320 problem = flag_problem(argv)
321 if problem is not None:
322 violations.append(f"{source}: {problem}")
323 return violations, compilers, sources
324
325
326# ---------------------------------------------------------------------------
327# Selftest: the detector must fire on a genuinely broken input AND stay quiet
328# on the legal-but-tricky forms. A checker asserted in only one direction can
329# be vacuously green forever.
330# ---------------------------------------------------------------------------
331SELFTEST_CASES: list[tuple[str, str, list[str], bool]] = [
332 # NOTE: this case carries -Werror deliberately. An earlier revision omitted
333 # it, so the case fired on the MISSING -Werror and would have passed even
334 # with the -w detection ripped out entirely -- a must-fire case that proved
335 # nothing about the rule it was named for. Every must-fire case below is
336 # compliant except for the one property it is testing.
337 (
338 "first-party with -w on top of an otherwise-compliant line",
339 "apps/shared_libs/jof/src/jof.c",
340 ["cc", "-Wall", "-Wextra", "-Werror", "-w", "-fno-strict-aliasing", "-c"],
341 True,
342 ),
343 (
344 "first-party without -Werror",
345 "libs/ra8_core/src/ra8_log.c",
346 ["cc", "-Wall", "-Wextra", "-c"],
347 True,
348 ),
349 (
350 "first-party with only a targeted -Werror=",
351 "apps/host/mdl/src/main.c",
352 ["cc", "-Wall", "-Wextra", "-Werror=return-type", "-c"],
353 True,
354 ),
355 (
356 "first-party whose -Werror is cancelled later",
357 "apps/shared_libs/mdl/src/mdl_export.c",
358 ["cc", "-Wall", "-Werror", "-Wno-error", "-c"],
359 True,
360 ),
361 (
362 "compliant first-party source",
363 "apps/shared_libs/jof/src/jof_produce.c",
364 ["cc", "-Wall", "-Wextra", "-Werror", "-c"],
365 False,
366 ),
367 (
368 "-Wall is not -w",
369 "apps/shared_libs/compress/src/ra8_compress.c",
370 ["cc", "-Wall", "-Wwrite-strings", "-Werror", "-c"],
371 False,
372 ),
373 (
374 "vendored SOUP may keep -w",
375 "apps/shared_libs/third_party/miniz/miniz.c",
376 ["cc", "-w", "-fno-strict-aliasing", "-c"],
377 False,
378 ),
379 (
380 "generated font data is exempt",
381 "libs/ra8_fonts/ra8_font_dejavu.c",
382 ["cc", "-w", "-c"],
383 False,
384 ),
385]
386
387# Compiler-classification cases: compiler_of must name the driver family from
388# an argv the way the real databases spell it (absolute path, versioned name,
389# a launcher prefix). Asserted so the #356 second-arm guard cannot be defeated
390# by a classifier that quietly folds gcc into clang or "other".
391COMPILER_SELFTEST_CASES: list[tuple[str, list[str], str]] = [
392 ("clang-18 absolute path", ["/usr/bin/clang-18", "-c", "f.c"], "clang"),
393 ("clang++-18 driver", ["clang++-18", "-c", "f.cc"], "clang"),
394 ("gcc-14 absolute path", ["/usr/local/bin/gcc-14", "-c", "f.c"], "gcc"),
395 ("g++-14 driver", ["g++-14", "-c", "f.cc"], "gcc"),
396 ("bare gcc", ["gcc", "-c", "f.c"], "gcc"),
397 ("ccache-wrapped gcc-14", ["ccache", "gcc-14", "-c", "f.c"], "gcc"),
398 ("unknown driver", ["tcc", "-c", "f.c"], "other"),
399]
400
401# Second-arm coverage cases: with both arms required, a missing family must be
402# reported (fires) and a complete set must report nothing (quiet). This is the
403# property the #356 acceptance names -- a silently-dropped gcc arm reads as a
404# pass unless its absence is turned into a hard finding here.
405COVERAGE_SELFTEST_CASES: list[tuple[str, set[str], list[str], list[str]]] = [
406 ("only clang seen -> gcc missing", {"clang"}, ["clang", "gcc"], ["gcc"]),
407 ("only gcc seen -> clang missing", {"gcc"}, ["clang", "gcc"], ["clang"]),
408 ("both seen -> nothing missing", {"clang", "gcc"}, ["clang", "gcc"], []),
409]
410
411# Project-scope cases: a host CMake project whose code the gate never compiled
412# must be reported, and one whose code it did -- including a shared core reached
413# only through a sibling's build tree -- must stay quiet. The two same-named
414# products are the case a basename key gets wrong, so they are asserted apart.
415PROJECT_SELFTEST_CASES: list[tuple[str, list[str], list[str], list[str]]] = [
416 (
417 "a host project nothing compiled is reported",
418 ["tools/ra8_emulator", "apps/host/mdl"],
419 ["/w/apps/host/mdl/src/main.c"],
420 ["tools/ra8_emulator"],
421 ),
422 (
423 "a shared core composed into a sibling's build tree stays quiet",
424 ["apps/shared_libs/mdl", "apps/host/mdl"],
425 [
426 "/w/apps/host/mdl/src/main.c",
427 "/w/apps/shared_libs/mdl/src/mdl_cache.c",
428 ],
429 [],
430 ),
431 (
432 "a same-named product in another category is NOT covered by its twin",
433 ["apps/shared_libs/mdl", "apps/host/mdl"],
434 ["/w/apps/host/mdl/src/main.c"],
435 ["apps/shared_libs/mdl"],
436 ),
437 (
438 "every project compiled stays quiet",
439 ["tools/ra8_emulator", "tools/mkbookimg"],
440 [
441 "/w/tools/ra8_emulator/src/emu.c",
442 "/w/tools/mkbookimg/src/mkbookimg.c",
443 ],
444 [],
445 ),
446 (
447 "a source reached through .. counts only for the project it resolves to",
448 ["apps/shared_libs/mdl", "apps/host/mdl"],
449 ["/w/apps/host/mdl/../../../apps/shared_libs/mdl/src/mdl_cache.c"],
450 ["apps/host/mdl"],
451 ),
452]
453
454
455def _discovery_fixture(root: Path) -> None:
456 """Write the smallest tree that exercises both discovery depths.
457
458 A host tool at ``tools/<tool>/``, a host product one level deeper under a
459 category, and a firmware product at the same depth carrying the linker
460 script + vector table that mark an image.
461 """
462 for rel in (
463 "tools/widget",
464 "apps/shared_libs/core",
465 "apps/board/stand_alone/blinky",
466 ):
467 (root / rel).mkdir(parents=True)
468 (root / rel / "CMakeLists.txt").write_text("project(x C)\n")
469 source_only = root / "apps/shared_libs/source_only"
470 source_only.mkdir(parents=True)
471 (source_only / "CMakeLists.txt").write_text(
472 "# Source-only composition note.\n"
473 "#[[ A bracket comment may mention a command without defining one.\n"
474 "project(not_a_real_project C)\n"
475 "]]\n"
476 )
477 (root / "apps/board/stand_alone/blinky/linker_script.ld").write_text("MEMORY {}\n")
478 firmware_src = root / "apps/board/stand_alone/blinky/src"
479 firmware_src.mkdir()
480 (firmware_src / "vector_table.c").write_text("int v;\n")
481
482
483def _discovery_selftest() -> list[str]:
484 """Drive discovery over a fixture tree; return failure lines.
485
486 Both directions, and through the REAL discriminator: the firmware set is
487 computed by ``lint_targets.firmware_app_dirs()`` over the fixture's paths
488 rather than hand-written here, so a change that stopped classifying
489 firmware apps fails this test instead of sailing through it.
490 """
491 failures: list[str] = []
492 with tempfile.TemporaryDirectory() as name:
493 root = Path(name)
494 _discovery_fixture(root)
495 paths = [str(path.relative_to(root)) for path in root.rglob("*") if path.is_file()]
496 firmware = firmware_app_dirs(paths)
497 if firmware != ("apps/board/stand_alone/blinky",):
498 failures.append(f" discovery: firmware apps {firmware}, want the blinky image")
499 got = cmake_tool_projects(root, firmware=firmware)
500 want = ("apps/shared_libs/core", "tools/widget")
501 if got != want:
502 failures.append(f" discovery: host projects {got}, want {want}")
503 return failures
504
505
506def selftest() -> int:
507 """Prove the detector fires and stays quiet where it must."""
508 failures: list[str] = []
509 for name, source, argv, should_fire in SELFTEST_CASES:
510 fired = (not is_exempt(source)) and (flag_problem(argv) is not None)
511 if fired != should_fire:
512 failures.append(
513 f" {name}: expected {'a violation' if should_fire else 'no violation'}, "
514 f"got {'a violation' if fired else 'none'}"
515 )
516
517 for name, argv, want in COMPILER_SELFTEST_CASES:
518 got = compiler_of(argv)
519 if got != want:
520 failures.append(f" compiler_of {name}: expected {want!r}, got {got!r}")
521
522 for name, seen, required, want_missing in COVERAGE_SELFTEST_CASES:
523 got_missing = missing_required_compilers(seen, required)
524 if got_missing != want_missing:
525 failures.append(
526 f" coverage {name}: expected missing {want_missing}, got {got_missing}"
527 )
528
529 for name, projects, sources, want_missing in PROJECT_SELFTEST_CASES:
530 got_missing = missing_tool_projects(projects, sources)
531 if got_missing != want_missing:
532 failures.append(
533 f" project scope {name}: expected missing {want_missing}, got {got_missing}"
534 )
535
536 failures += _discovery_selftest()
537
538 if failures:
539 sys.stderr.write("check_tool_warning_flags.py --selftest: FAILED\n")
540 sys.stderr.write("\n".join(failures) + "\n")
541 return 1
542
543 fires = sum(1 for case in SELFTEST_CASES if case[3])
544 quiet = len(SELFTEST_CASES) - fires
545 print(
546 f"check_tool_warning_flags.py --selftest: PASS "
547 f"({fires} must-fire, {quiet} must-stay-quiet flag cases; "
548 f"{len(COMPILER_SELFTEST_CASES)} classifier, "
549 f"{len(COVERAGE_SELFTEST_CASES)} second-arm coverage, "
550 f"{len(PROJECT_SELFTEST_CASES)} project-scope cases; "
551 "discovery excludes firmware apps and comment-only source libraries "
552 "while keeping host products at category depth)"
553 )
554 return 0
555
556
557def _scan_databases(paths: list[str]) -> tuple[list[str], set[str], set[str]]:
558 """Scan every database path.
559
560 Returns (violations, compiler families seen, sources compiled).
561
562 Exits (SystemExit 2) when a path is missing, matching check_database's
563 fatal handling of an empty or unreadable database -- a gate must never
564 report a pass for a database it could not inspect.
565 """
566 violations: list[str] = []
567 seen: set[str] = set()
568 sources: set[str] = set()
569 for name in paths:
570 path = Path(name)
571 if not path.is_file():
572 sys.stderr.write(
573 f"check_tool_warning_flags.py: FATAL -- {path} does not exist.\n"
574 " Configure the tool with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON first.\n"
575 )
576 raise SystemExit(2)
577 db_violations, db_compilers, db_sources = check_database(path)
578 violations += db_violations
579 seen |= db_compilers
580 sources |= db_sources
581 return violations, seen, sources
582
583
584def _report_violations(violations: list[str]) -> None:
585 """Print every first-party translation unit found below the warning bar."""
586 sys.stderr.write(
587 f"check_tool_warning_flags.py: {len(violations)} first-party "
588 "translation unit(s) below the project warning bar:\n\n"
589 )
590 for line in sorted(violations):
591 sys.stderr.write(f" {line}\n")
592 sys.stderr.write(
593 "\nFirst-party sources are held to -Wall -Wextra -Werror everywhere\n"
594 "else in this tree (NASA Power of 10 Rule 10). Only libs/third_party/\n"
595 "SOUP may be compiled -w. Fix the warning; do not re-suppress it.\n"
596 )
597
598
599def _discover_projects() -> tuple[str, ...]:
600 """Host CMake projects in the tree, enforcing the non-vacuity floor.
601
602 The floor is checked here rather than at the call site so a collapsed glob
603 can never be mistaken for "nothing is missing": `cmake_tool_projects`
604 returning an empty tuple makes `missing_tool_projects` return an empty
605 list, which is indistinguishable from a clean pass.
606
607 Raises:
608 SystemExit: 2 when discovery falls below MIN_CMAKE_TOOL_PROJECTS,
609 matching _scan_databases' fatal handling of an input it could not
610 trust.
611 """
612 projects = cmake_tool_projects(Path.cwd())
613 if len(projects) >= MIN_CMAKE_TOOL_PROJECTS:
614 return projects
615 sys.stderr.write(
616 f"check_tool_warning_flags.py: FATAL -- discovered {len(projects)} host "
617 f"CMake project(s); the floor is {MIN_CMAKE_TOOL_PROJECTS}.\n"
618 f" found: {', '.join(projects) or '(none)'}\n"
619 " Discovery collapsed. An empty scope reports no missing project and\n"
620 " reads as a pass, which is the failure this floor exists to catch.\n"
621 )
622 raise SystemExit(2)
623
624
625def _report_missing_projects(missing: list[str]) -> None:
626 """Print the host CMake projects whose code the gate never compiled."""
627 sys.stderr.write(
628 "check_tool_warning_flags.py: FATAL -- host CMake project(s) whose "
629 f"sources tools-build never compiled: {', '.join(missing)}\n"
630 " Every host CMake project must reach the gate, either through its\n"
631 " own build tree or by being composed into a sibling's with\n"
632 " add_subdirectory. A project nothing builds is a project nothing\n"
633 " holds to -Wall -Wextra -Werror.\n"
634 )
635
636
637def _report_missing_arm(missing: list[str], seen: set[str]) -> None:
638 """Print the silently-dropped compiler-arm failure (#356)."""
639 sys.stderr.write(
640 "check_tool_warning_flags.py: FATAL -- required compiler arm(s) "
641 f"never exercised: {', '.join(missing)}\n"
642 f" databases were compiled by: {', '.join(sorted(seen)) or '(none)'}\n"
643 " The tools-build gate compiles the host tools under clang-18 AND\n"
644 " gcc-14 (#356) so the warnings each catches but the other misses\n"
645 " are both held. A silently-dropped arm would read as a pass.\n"
646 )
647
648
649def _report_verdict(
650 *,
651 databases: list[str],
652 required: list[str],
653 violations: list[str],
654 seen: set[str],
655 missing_projects: list[str],
656) -> int:
657 """Render one discovery, warning, and compiler-arm verdict."""
658 if missing_projects:
659 _report_missing_projects(missing_projects)
660 return 2
661 if violations:
662 _report_violations(violations)
663 return 1
664 missing = missing_required_compilers(seen, required)
665 if missing:
666 _report_missing_arm(missing, seen)
667 return 1
668
669 arms = f", arms: {', '.join(sorted(seen))}" if required else ""
670 print(
671 f"check_tool_warning_flags.py: OK ({len(databases)} compile "
672 f"database(s), every first-party TU at -Wall -Wextra -Werror{arms})"
673 )
674 return 0
675
676
677def main() -> int:
678 """Entry point."""
679 parser = argparse.ArgumentParser(description=__doc__)
680 parser.add_argument("databases", nargs="*", help="compile_commands.json paths")
681 parser.add_argument(
682 "--selftest",
683 action="store_true",
684 help="prove the detector fires and stays quiet, then exit",
685 )
686 parser.add_argument(
687 "--require-compilers",
688 default="",
689 metavar="FAMILY[,FAMILY...]",
690 help=(
691 "comma-separated compiler families (e.g. clang,gcc) that must each "
692 "drive at least one database; the #356 second-arm guard. A single "
693 "comma-joined value keeps it order-independent of the database list."
694 ),
695 )
696 parser.add_argument(
697 "--require-all-cmake-tools",
698 action="store_true",
699 help="fail when any host CMake project's sources are absent from the databases",
700 )
701 parser.add_argument(
702 "--list-missing-cmake-tools",
703 action="store_true",
704 help="print uncovered host CMake project directories, one per line",
705 )
706 args = parser.parse_args()
707
708 if args.selftest:
709 return selftest()
710 if not args.databases:
711 sys.stderr.write(
712 "check_tool_warning_flags.py: FATAL -- no compile database given.\n"
713 " This gate must never run with nothing to inspect: that reports a\n"
714 " pass for work never done.\n"
715 )
716 return 2
717
718 projects: tuple[str, ...] = ()
719 if args.require_all_cmake_tools or args.list_missing_cmake_tools:
720 projects = _discover_projects()
721
722 required = [fam for fam in args.require_compilers.split(",") if fam]
723 violations, seen, sources = _scan_databases(args.databases)
724 missing_projects = missing_tool_projects(projects, sources)
725 if args.list_missing_cmake_tools:
726 for project in missing_projects:
727 print(project)
728 return 0
729 return _report_verdict(
730 databases=args.databases,
731 required=required,
732 violations=violations,
733 seen=seen,
734 missing_projects=missing_projects,
735 )
736
737
738if __name__ == "__main__":
739 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298