Build contract
SkillAI & modelsBuilds 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.
No other account needed.
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.
-enarrows to named examples for a quick check; a change insrc/or a class driver is verified by the full set on every resolved board before it is called green. - Scope, not guesswork.
--scopetakes changed paths, expanding a directory into its tracked files first, and runstools/ci_select.py: one board per affected family; a board whose ownhw/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) buildsstm32f407discoandraspberry_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 undersrc/portable/adds, per family it maps to, a board whosehw/bsp/family.jsonrow lists the driver and, where the host preprocessor can say, selects the USB IPs its guard names:stm32l412nucleofor fsdev next to a DWC2 pick on stm32l4,feather_rp2040_max3421forhcd_max3421.c. A special target the changed path needs (for exampleexamples-membrowse-uploadfortools/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 ispasstrue 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-Eon every instance in an example this run built, so a board whose configuration guards the driver away (dcd_nrf5x.con NRF54,hcd_rp2040.cunder MAX3421) is not coverage whatever else went green; the reason names each board's verdict.-eor-Thands coverage to you: the script then does not report what you chose to leave out, and a-Ttarget that writes no elf is not a verified build of anything. Exit 3 andpassfalse, 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 theuncoveredreason, 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.--sharedwritescmake-build/cmake-build-<board>, the dirhil_test.pyflashes from by default, and needs exclusive ownership of that board: never while a HIL run or another agent is on it. hw/bsp/family.jsonis what a board's default configure compiles: itsmcu, the tested defines,family_mcus, theMAX3421_HOSToption and its drivers undersrc/portable/, one row per cmake board,nullfor the two Make-only pic32mz boards.tools/build.pyrewrites 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-onlydoes just that), so a change that moves a board's configuration leaves the catalog modified: the run's JSON says so infamilyJsonChangedand per board infamilyJson, and the row is committed with the change. The pre-commit hookfamily-jsonrepairs 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 fixby 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.pymust exist. In a worktree, symlink them from the primary checkout (CLAUDE.md, Working Rules);--fetch-depsis 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-eto one example that reaches the code.-Dpasses a build-system define (LOG=2) the same way, except on Espressif boards, wheretools/build.pyhands 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=rttviatools/build.py -Ddirectly; this script builds MinSizeRel. ETM trace builds add-DTRACE_ETM=1the 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>-jlinkflash target. rp2350 at a non-default core clock: passSYS_CLK_KHZtogether withPLL_SYS_VCO_FREQ_HZ/PLL_SYS_POSTDIV1/PLL_SYS_POSTDIV2from the SDK'sscripts/vcocalc.pyas compile definitions; 240 MHz also needsSYS_CLK_VREG_VOLTAGE_AUTO_ADJUST=1andSYS_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