Front door

The command line, the run’s sequence, and the exit code. This is the only stage that decides what happens next rather than computing a value: everything below it is called from rigc.cli._expand() in order, and every rejection funnels back through one place.

rigc.cli

rigc CLI – the front door.

expand <rig_yml> takes –shield-dir* –board –board-dts –build-info –bindings-dir* –include-dir* –connector-dir* –revision –variant –out-dir (* = repeatable). –board-dts/–build-info/–bindings-dir feed the board reader (board/); –shield-dir/ –include-dir/–connector-dir/–revision/–variant feed the loader. –board feeds the loader too, and is the ONLY source of rig.board (rig.yml has no board: key of its own): omitted, rig.board is simply “” – legal through the loader, and a diagnostic only once this file is about to read a real board devicetree (see the board-empty check right before load_board, below). A clean analysis emits the rig artifacts (emitter.emit) plus the build-glue handoff (emitter.context.render) through the one writer (emitter.write_ artifacts) and returns 0. main(argv) -> int is callable in-process, so the argv contract has subprocess-free unit tests.

The positional rig and –promote <shield-name> are mutually exclusive alternatives for the SAME slot: a promoted shield has no rig.yml on disk, so –promote makes _expand synthesize promote.promote_shield’s own pair straight into this run’s workdir and load THAT by path – the loader, deps, diagnostics and emitter never learn the difference. –revision alongside –promote means the SHIELD’s own revision (baked into the synthesized content file), never a rig-level axis – a promoted rig declares no revisions of its own, so it is never forwarded to loader.load.

–promote’s value may also be a ;-separated LIST of shield targets: promote.promote_shield_list synthesizes the N-instance pair instead, and –revision plays no part (each element carries its own @rev inline in the list text, since one scalar flag cannot carry N per-element revisions).

Exit vocabulary: 0 accept, 1 rejected input, 2 usage error (argparse’s own), 3 not implemented (see unimplemented.py).

The workdir lives inside `–out-dir`, as `<out-dir>/rigc-generated`, never in /tmp, so it inherits the owning build directory’s lifetime: west build -p, rm -rf build/ and pytest’s own tmp_path retention each reap it for free. Its name is DETERMINISTIC (no mkdtemp suffix), it is wiped on entry so a previous run’s intermediates can never be mistaken for this run’s, and it is KEPT on every exit – it is the only record of what this run actually fed its own parsers (a promoted shield’s synthesized rig.yml/content pair, each shield’s .dts and its cpp-preprocessed .pre, the board’s included), and an accepted run is exactly the run whose emitted overlay someone later questions.

The workdir NAME is NOT cosmetic: the test harness’s own normalize() (tests/integration/conftest.py’s _WORKDIR_RE) strips a path ending in rigc-generated to a stable placeholder before comparing rendered stderr against a golden. A cpp-preprocess-failure detail (e.g. param-missing-header) embeds this path verbatim inside gcc’s own stderr text, so the trailing component MUST stay literally rigc-generated or the comparison sees an un-normalized absolute path and byte-mismatches a golden that has nothing else wrong with it.

rigc.__main__

python -m rigc … entry point – delegates to cli.main(). The exit vocabulary is cli.py’s: 0 accept, 1 rejected input, 2 usage error, 3 not implemented.

rigc.promote

The –rig <shield> desugaring and the namespace rule that decides when a bare name resolves as a shield at all – everything west rigs –explain (west_commands/rigs.py) needs, factored so the printer stays a thin caller over pure values.

Three things:

promote_shield() – the natural mapping a -> [a], PURE: a shield

name (+ optional revision) -> the rig.yml/ content-file TEXT a checked-in rig meaning the same thing would have to contain.

discover_shields() – the IO edge: which names ARE shields at all,

reusing loader/library.py’s OWN scan verbatim (never a second glob restating it, so “resolvable by rigc” and “known here” cannot drift apart) – plus, per shield.yml, its template: flag, the SECOND authority that marks a shield PROMOTABLE.

check_promotable() / both_paths_error() – the promotability gate and

the namespace rule’s “both” branch, pure decisions over already-discovered facts.

No cmake, no cpp, no board: printing a promoted shield’s two documents needs none of rigc’s heavier machinery – promote_shield never touches a filesystem. rigc.loader.load is what PROVES the printed text is real (the round-trip test); this module never imports it.

rigc.deps

Dependency data: the real source-tree files one load actually touched (rig.yml, its content file, .shield templates + their cpp-included files, connector bindings, index headers) – the input side of the RIG_DEPENDS handoff. emitter.context.render serializes a value of this type into the sorted, escaped RIG_DEPENDS list cmake/modules/dts.cmake appends to CMAKE_CONFIGURE_DEPENDS.

Dependency data is a RETURNED/threaded VALUE, never a mutable accumulator passed down and written into. Every function that opens a real file returns the paths it touched as part of its own result; callers compose them upward with union, the same way diagnostics compose upward as list concatenation.

rigc.unimplemented

The loud-refusal channel for functionality rigc does not have yet.

Such a path must fail DISTINCTLY: rigc: not implemented: <what> on stderr and exit status 3 – never exit 1 (the reject convention: “we cannot do this yet” must never be mistakable for “your input is wrong”), never a traceback, and never a silent accept. Exit 2 stays argparse’s own usage-error code, so the full exit vocabulary is 0 accept / 1 rejected input / 2 usage error / 3 not implemented.

Nothing in the frozen corpus reaches it any more. What still does: an unreadable, empty or non-mapping YAML document (loader/documents.py), and cli.py’s unreachable unknown-subcommand branch.

Raised anywhere inside the pipeline, caught ONCE in cli.main().