- name
- build
- description
- The project build contract. Use when a change must be verified by building, when an agent needs the boards a diff affects, or when a workflow asks for "the project's build". Resolves scope to boards, builds in a private dir, reports pass/fail with the first error.
# Build contract
`scripts/check_build.py` is the entry point for verifying a change; every agent and workflow that does so calls it rather than composing cmake lines. Two builds are not verification and stay on `tools/build.py` directly: the debug build below, and the PVS compile-database build for the `pvs-studio` agent, which needs one configured tree for the analyser, not a sweep.
```bash
B=.claude/skills/build/scripts/check_build.py
python3 $B --scope <changed paths or dirs> # boards a change affects, one per family
python3 $B --base master # same, from the branch diff
python3 $B --base master --worktree # same, plus uncommitted and untracked changes
python3 $B --select-only (--scope ... | --base REF [--worktree] | --endpoints A..B) [--config <roster>]... # the selection alone
python3 $B --board stm32f407disco [-e device/cdc_msc] [-T target]
python3 $B --board stm32f407disco -e host/cdc_msc_hid --cflag=-DCFG_TUH_CDC_FTDI_LATENCY=16
python3 $B ... --shared # canonical cmake-build-<board>, the dir HIL flashes from
python3 $B --board <b> --shared --variants <hil config> # one cmake-build-<variant> per roster variant
python3 $B --board <b> --shared --variants <hil config> --receipt .hil-remote/<name>.json # plus the build receipt a delegated HIL run checks (hil skill)
python3 $B ... --fetch-deps # fetch a family's missing deps instead of failing
```
The last stdout line is JSON: `pass`, per-board `status` (`ok`, `failed`, `skipped`, `error`), `built` (elf files this run wrote), `firstError`, `buildDir`, how the boards were resolved, and the scope paths nothing builds, split into `nothingToBuild` and `uncovered`. Exit 0 pass, 1 a board failed, 2 usage or resolution error with the message in `error` (a missing dependency names its remedy there) or a refused receipt with its reason in `receipt.error`, 3 an uncovered path. `-v` streams the build to stderr.
`--select-only` is the selection contract: CI and any agent that needs "what does this change affect" read its manifest rather than calling `tools/change_impact.py` or parsing its log. It prints one JSON line and stops before any dependency check or build, under an isolated stdlib-only interpreter:
- `version` 1. `input`: `mode` (`paths`, `base`, `worktree`, `endpoints`), the ref as given, and the SHAs it resolved to (`base_sha`, `merge_base`, `head`, `deps_base`). Committed modes classify the checked-out tree, so `--endpoints A..B` refuses a `B` that is not HEAD.
- `build`: `full` and `needed` (booleans); `families` maps each selected family to `examples` (`"all"` or a non-empty list), and is empty when `full`; `required_boards` (changed `hw/bsp/<f>/boards/<b>` still in the tree); `required_targets` (`examples-membrowse-upload` for `tools/membrowse_cli.py`); `dropped` (families pruned away, each with its line); `paths`, one record per changed path: `rule`, `effect` (`none` nothing to verify, `gap` firmware no build compiles, `select`, `full`), the `families` and `examples` the rule names (`null` means every one), `port`, `targets`, and the human `reason`. `effect` is what the path contributes to the selection, not a coverage verdict.
- `hil`: `full`, `needed`, `boards`, and the per-roster `args`, `args_flasher` and `hil_examples`.
A selector error is exit 2 with the message: a consumer that falls back to the full matrix decides so itself.
## Judgment
- **Full example set by default.** `-e` narrows to named examples for a quick check; a change in `src/` or a class driver is verified by the full set on every resolved board before it is called green.
- **Scope, not guesswork.** `--scope` takes changed paths, expanding a directory into its tracked files first, and runs `tools/change_impact.py`: one board per affected family; a board whose own `hw/bsp/<family>/boards/<board>/` files changed is built itself, else a rig-roster board of the family, else the first in the family. A full-matrix answer (core or unclassified paths) builds `stm32f407disco` and `raspberry_pi_pico`, plus any board whose own files changed, plus one board per family a port, bsp or mcu path in the scope names: the pair stands in for the matrix on core code, not on a port it does not contain. A changed driver under `src/portable/` adds, per family it maps to, a board whose `hw/bsp/family.json` row lists the driver and, where the host preprocessor can say, selects the USB IPs its guard names: `stm32l412nucleo` for fsdev next to a DWC2 pick on stm32l4, `feather_rp2040_max3421` for `hcd_max3421.c`. A special target the changed path needs (the manifest's `required_targets`: `examples-membrowse-upload` for `tools/membrowse_cli.py`) is `-T`; a default sweep is no evidence for it. `tools/code_size.py` is validated by CI's code-size step, which no local build runs, so it always stays `uncovered` here.
- **A path nothing builds** is one of two kinds, in `change_impact`'s words, and the script sorts them so that a mixed change gets the stricter answer:
- `nothingToBuild` (docs, `.claude/`, `*.md`, unit tests, HIL harness): there is nothing a build could verify, which is not a failure. A scope of only such paths is `pass` true with no boards, exit 0; quote the reason in your notes so nobody mistakes it for a build that ran.
- `uncovered` (a class or typec no example config enables, a lib nothing builds, a port mapping to no board family, a family the selector pruned because nothing in it builds, an example or class no built board wrote an elf for, a port driver no built board of its families kept a line of after preprocessing): firmware this run did not compile, judged against the boards and elfs it actually produced. The port test is the driver's own compile command run with `-E` on every instance in an example this run built, so a board whose configuration guards the driver away (`dcd_nrf5x.c` on NRF54, `hcd_rp2040.c` under MAX3421) is not coverage whatever else went green; the reason names each board's verdict. `-e` or `-T` hands coverage to you: the script then does not report what you chose to leave out, and a `-T` target that writes no elf is not a verified build of anything. Exit 3 and `pass` false, even when every other path in the scope built green — a class driver plus the core file that registers it is the common shape, and the green boards say nothing about the driver. The verdict is false and the `uncovered` reason, quoted verbatim, is the finding: a human adds coverage (the next bullet) or accepts the gap.
- **Private dirs for parallel agents.** Builds land in `cmake-build/cmake-build-agent-<pid>-<board>`; remove yours when done. `--shared` writes `cmake-build/cmake-build-<board>`, the dir `hil_test.py` flashes from by default, and needs exclusive ownership of that board: never while a HIL run or another agent is on it. `--variants <hil config>` builds instead each variant that roster gives a named board, into the `cmake-build-<variant>` `hil_test.py` runs it from, with your own defines and flags followed by the variant's, so a repeated `-D` takes the variant's value; a `--cflag` macro the variant's flags also set is a `-Werror` redefinition, not an override (a board without a variant list as itself); it refuses a board the roster lacks.
- **`hw/bsp/family.json`** is what a board's default configure compiles: its `mcu`, the tested defines, `family_mcus`, the `MAX3421_HOST` option and its drivers under `src/portable/`, one row per cmake board, `null` for the two Make-only pic32mz boards. `tools/build.py` rewrites the row whenever it runs a default configure (cmake, gcc, no `-D`, no `--cflag`, into a dir that did not exist yet, which every private dir is; `--configure-only` does just that), so a change that moves a board's configuration leaves the catalog modified: the run's JSON says so in `familyJsonChanged` and per board in `familyJson`, and the row is committed with the change. The pre-commit hook `family-json` repairs the catalog in place, like codespell: it configures each board without a row once into a fresh private dir, drops rows of boards that are gone, rewrites the file canonical, and fails the commit when it changed anything, so you stage the file and commit again (`python3 tools/family_json.py fix` by hand does the same). A board this machine cannot configure (no toolchain, ESP-IDF not exported) stays reported with tools/build.py's reason.
- **Dependencies.** The family's entries in `tools/get_deps.py` must exist. In a worktree, symlink them from the primary checkout (`CLAUDE.md`, Working Rules); `--fetch-deps` is for a fresh clone. The script never fetches on its own.
- **Code no example enables.** A branch behind a `CFG_*` option no board sets is not compiled by any sweep, so a change there is unverified until you build it with the option on: `--cflag=-D<OPTION>=<value>` (repeatable), narrowed with `-e` to one example that reaches the code. `-D` passes a build-system define (`LOG=2`) the same way. Never hand-roll a cmake line for this.
- **Espressif** boards go through `tools/build.py`'s ESP-IDF path; `. "$IDF_PATH/export.sh"` must be in the environment first, and only ESP-IDF examples build for them.
- **Debug builds**: `-DCMAKE_BUILD_TYPE=Debug -DLOG=2 -DLOGGER=rtt` via `tools/build.py -D` directly; this script builds MinSizeRel. ETM trace builds add `-DTRACE_ETM=1` the same way. `python3 tools/build_utils.py board-info <board>` prints the board's J-Link device name and its reference Ozone project. With several J-Links attached, `-DJLINK_OPTION="-USB <serial>"` pins the `<example>-jlink` flash target. rp2350 at a non-default core clock: pass `SYS_CLK_KHZ` together with `PLL_SYS_VCO_FREQ_HZ`/`PLL_SYS_POSTDIV1`/`PLL_SYS_POSTDIV2` from the SDK's `scripts/vcocalc.py` as compile definitions; 240 MHz also needs `SYS_CLK_VREG_VOLTAGE_AUTO_ADJUST=1` and `SYS_CLK_VREG_VOLTAGE_MIN=VREG_VOLTAGE_1_15`.
- **Printing a command is not verification.** A verdict is the JSON of a run that happened; an agent that cannot run the script reports the build as not run.
GitHubで見る