Build contract

SkillAI & models

Builds your changed code across the affected hardware boards and reports pass or fail with the first error.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Build contract skill

About this capability

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.

What this skill tells your AI

The instructions your AI receives, as published by hathach/tinyusb in .claude/skills/build/SKILL.md and read by ahel’s review.

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 in validate, which needs one configured tree for the analyser, not a sweep.

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 --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 ... --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), 3 an uncovered path. -v streams the build to stderr.

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/ci_select.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 (for example examples-membrowse-upload for tools/membrowse_report.py) is -T; a default sweep is no evidence for it.
  • A path nothing builds is one of two kinds, in ci_select'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.
  • 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, except on Espressif boards, where tools/build.py hands defines to cmake only, so the run is refused rather than built without them. 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.

Signals

GitHub stars
7k
Forks
2k
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
build-hathach
Source
github.com/hathach/tinyusb