From 68e44f4a5a96a510de2974af1e5eefe5d4069708 Mon Sep 17 00:00:00 2001 From: ppannuto-claude Date: Fri, 28 Aug 2026 09:12:25 -0700 Subject: [PATCH 1/3] qemu_i486_q35: use [unstable] json-target-spec instead of -Zjson-target-spec qemu_i486_q35's target is a bare JSON filename, not a path, which cargo only resolves as a custom target spec (rather than requiring a builtin triple name) via the unstable json-target-spec mechanism. Previously this was passed as a `-Zjson-target-spec` CLI flag baked into the board's own Makefile (`CARGO = cargo -Zjson-target-spec`), the only board doing so. Set it instead via `[unstable] json-target-spec = true` in the board's own .cargo/config.toml -- the same persistent-config mechanism ../cargo/riscv_flags.toml already uses for the RISC-V boards' JSON specs (those are referenced by relative path instead, so didn't need it spelled out explicitly). That makes a plain `cargo clippy`/`cargo check`/`cargo build` all work with no special-casing in the board Makefile, so the now-redundant CARGO override is dropped. Also removed a stale `[env] RUST_TARGET_PATH` + TODO comment that predated the current include-based config layout. Verified: a full `make -C boards/qemu_i486_q35` build still produces a working kernel binary with the CARGO override removed. Co-Authored-By: Claude Sonnet 5 --- boards/qemu_i486_q35/.cargo/config.toml | 13 ++++++++----- boards/qemu_i486_q35/Makefile | 4 ---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/boards/qemu_i486_q35/.cargo/config.toml b/boards/qemu_i486_q35/.cargo/config.toml index 053c8ea2905..a77c953ce57 100644 --- a/boards/qemu_i486_q35/.cargo/config.toml +++ b/boards/qemu_i486_q35/.cargo/config.toml @@ -7,13 +7,16 @@ include = [ "../../cargo/unstable_flags.toml", ] -[env] -# Relative to crate root, not this file -# TODO: where is the best place to store this config file? -RUST_TARGET_PATH = "." - [build] target = "i486-unknown-none.json" [target.i486-unknown-none] runner = "qemu-system-i386 -cpu 486 -machine q35 -net none -device isa-debug-exit,iobase=0xf4,iosize=0x04 -device virtio-rng-pci,disable-legacy=on -serial stdio -kernel" + +# `target` above is a bare filename, not a path -- cargo only resolves that +# as a custom target spec (rather than requiring a builtin triple name) with +# this enabled. Same mechanism ../../cargo/riscv_flags.toml uses for the +# RISC-V boards' JSON specs, just needed explicitly here since those are +# referenced by relative path instead. +[unstable] +json-target-spec = true diff --git a/boards/qemu_i486_q35/Makefile b/boards/qemu_i486_q35/Makefile index 6ca8ce21842..74fc04fff9a 100644 --- a/boards/qemu_i486_q35/Makefile +++ b/boards/qemu_i486_q35/Makefile @@ -8,10 +8,6 @@ # Skip auto-installing targets with rustup, since we are using a custom target NO_RUSTUP := 1 -# Because we use a custom target with a .json file, we must pass -# `-Zjson-target-spec` to cargo as of roughly January 2026. -CARGO = cargo -Zjson-target-spec - include ../Makefile.common QEMU_CMD := qemu-system-i386 From b12efe4ab2d3c0c5eb71590a2093cdcef318a646 Mon Sep 17 00:00:00 2001 From: ppannuto-claude Date: Fri, 28 Aug 2026 09:12:40 -0700 Subject: [PATCH 2/3] Make ci-job-clippy's board selection cover all archs, dynamically The hand-picked board list (nrf52840dk, raspberry_pi_pico, hifive1, qemu_i486_q35) silently missed arch/* crates with no board maintaining the list ever noticed: cortexm7, cortexm33, and rv64i had no clippy coverage with a real target. tools/build/list_arch_boards.sh picks one board per arch/* crate, so the set self-updates as boards and archs are added or removed instead of drifting stale. It iterates tools/build/list_archs.sh's small, already-existing arch list on the outside and boards on the inside, stopping at the first (alphabetically, since boards are sorted first) match per arch -- rather than scanning every board regardless of whether a match was already found. Every chip that depends on an arch crate does so directly (one hop; verified against every chips/*/Cargo.toml), and a few board crates also depend on an arch crate directly alongside their chip (e.g. hail, the apollo3 boards) -- so for each candidate board, check both its own Cargo.toml and its chip's for an arch/ path dependency. An arch crate with no board using it directly is only an error if no *other* arch crate depends on it either (checked with one more grep over arch/*/Cargo.toml): e.g. cortex-v7m, cortex-m, cortex-m0, and riscv all have zero boards depending on them directly today, but each is a sibling dependency of some other arch crate that a board does use directly (cortex-m4f -> cortex-v7m -> cortex-m, rv32i -> riscv, etc.), so building that board compiles -- and so checks -- them too as a side effect, with no separate pick needed. cortex-m3 is currently the only arch crate genuinely unreachable either way; the script correctly hard-fails on it (verified: `make ci-job-clippy` now fails specifically and only on cortex-m3, everything else still resolves to the same 7 boards as before). This should self-resolve once a board using cortex-m3 lands. Results are collected into a variable and only printed after the full loop succeeds, rather than printed as each arch resolves: a failure partway through then leaves stdout completely empty instead of an incomplete board list, which matters because a `$(shell ...)`-based caller (as opposed to a direct invocation checked with `|| exit 1`) can only observe empty-vs-nonempty output, not the exit status. An earlier version used `cargo metadata` to compute each board's whole transitive dependency graph, which turned out to be solving a problem that doesn't exist here -- the codebase just doesn't have deep chip->chip->arch chains that would need it. A cold-context review caught a real bug in the regex: `path = "..."` requires exact spacing around `=`, but two chips (psoc62xa, rp2040) write `path="..."` with none, so their arch dependency silently didn't match. Harmless today only by coincidence -- both boards using those chips also happen to redeclare the same arch crate directly, so the board-level lookup covered for it -- but a future cleanup removing that "redundant" direct dependency would silently drop cortex-m0p from clippy coverage with nothing to flag it. Loosened the regex to tolerate any whitespace around `=`. Verified: `make prepush` passes except for ci-job-clippy, which fails specifically and only on the cortex-m3 gap described above; output for every other arch is identical to the earlier board-first version's. Co-Authored-By: Claude Sonnet 5 --- Makefile | 17 +++------ tools/build/list_arch_boards.sh | 67 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 11 deletions(-) create mode 100755 tools/build/list_arch_boards.sh diff --git a/Makefile b/Makefile index c9113b29180..53e78cbbebd 100644 --- a/Makefile +++ b/Makefile @@ -405,17 +405,12 @@ ci-job-readme-check: ci-job-clippy: $(call banner,CI-Job: Clippy) @cargo clippy -- -D warnings - # Run `cargo clippy` in select boards so we run clippy with targets that - # actually check the arch-specific functions. - # - # - nrf52840dk: cortex-m4 - # - raspberry_pi_pico: cortex-m0 - # - hifive1: riscv - # - qemu_i486_q35: x86 - @cd boards/nordic/nrf52840dk && cargo clippy -- -D warnings - @cd boards/raspberry_pi_pico && cargo clippy -- -D warnings - @cd boards/hifive1 && cargo clippy -- -D warnings - @cd boards/qemu_i486_q35 && cargo clippy -Zjson-target-spec -- -D warnings + # One board per `arch/*` crate (tools/build/list_arch_boards.sh). + @arch_boards="`./tools/build/list_arch_boards.sh`" || exit 1;\ + for b in $$arch_boards;\ + do echo "$$(tput bold)Clippy $$b$$(tput sgr0)";\ + (cd boards/$$b && cargo clippy -- -D warnings) || exit 1;\ + done diff --git a/tools/build/list_arch_boards.sh b/tools/build/list_arch_boards.sh new file mode 100755 index 00000000000..d3dc6d7844d --- /dev/null +++ b/tools/build/list_arch_boards.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2026. + +# For each arch/* crate, print one representative board: the +# alphabetically-first board that depends on it, directly or via its chip. +# +# Reasonably aware of arch dependencies (i.e., `cortexm` picked up by `cortexm7`), and +# avoids duplicates where unneeded (but, e.g., `cortexm4` also necessarily repeats `cortexm`). +# +# Variants of another board (extra feature/policy configs, tutorial +# copies) are skipped, same as boards/README.md's own tooling, since +# they're not independent ports and would just be redundant picks. +# Sorted once so the loop below can stop at the first (alphabetical) match. +boards=($(./tools/build/list_boards.sh | sort | grep -vE '^(configurations|tutorials)/')) + +# True if $board depends on arch/$arch, directly or via its chip. +board_depends_on_arch() { + local board="$1" arch="$2" + local board_toml="boards/$board/Cargo.toml" + # Matches a `path = "…/arch/$arch"` dependency line; some Cargo.toml + # files omit the whitespace around `=`. + local pattern="path[[:space:]]*=[[:space:]]*\"[^\"]*/arch/$arch\"" + + grep -qE "$pattern" "$board_toml" && return 0 + + local chip_path chip_toml + # Pulls the path out of each `chips/*` dependency the board declares. + for chip_path in $(grep -oE 'path[[:space:]]*=[[:space:]]*"[^"]*/chips/[^"]*"' "$board_toml" | sed -E 's/.*"(.*)"/\1/'); do + chip_toml="boards/$board/$chip_path/Cargo.toml" + [ -f "$chip_toml" ] && grep -qE "$pattern" "$chip_toml" && return 0 + done + return 1 +} + +# Collected rather than printed as we go, so a failure partway through +# (see below) leaves nothing on stdout instead of an incomplete board list +# -- callers using `$(shell ...)` can't see a nonzero exit status, only +# empty-vs-nonempty output. +found_boards="" + +for arch in $(./tools/build/list_archs.sh); do + found="" + for board in "${boards[@]}"; do + if board_depends_on_arch "$board" "$arch"; then + found="$board" + break + fi + done + if [ -n "$found" ]; then + found_boards="$found_boards$found +" + continue + fi + + # No board uses this arch directly -- fine if another arch crate does + # (e.g. cortex-m, picked up by cortex-m4), since it then gets compiled, + # and so checked, as a side effect of building that arch's own board. + grep -qE "path[[:space:]]*=[[:space:]]*\"\.\./$arch\"" arch/*/Cargo.toml 2>/dev/null && continue + + echo "error: no board or arch crate depends on arch/$arch" >&2 + exit 1 +done + +printf '%s' "$found_boards" | sort -u From 59573919f2dace8a229db22b41c000f5fb419910 Mon Sep 17 00:00:00 2001 From: ppannuto-claude Date: Fri, 28 Aug 2026 00:12:05 -0700 Subject: [PATCH 3/3] Run `make prepush`'s checks concurrently `prepush` ran format-check, clippy, syntax-check, and licensecheck back-to-back. Each already builds into an isolated cargo state (a distinct target triple, or for licensecheck an entirely separate workspace/target dir), verified empirically: alternating `cargo clippy` and `cargo check --config deny_warnings.toml` on the same workspace does not invalidate each other's incremental cache on this toolchain, and concurrent cargo invocations against the same target directory complete correctly. So there's no ordering-based fingerprint thrashing to fix, but there's nothing stopping these jobs from running in parallel either. Split ci-job-clippy's per-board `cargo clippy` invocations (workspace plus one board per `arch/*` crate, per the prior commit) into separate phony sub-targets, and have prepush re-invoke make so its scheduler fans all of these (and ci-job-syntax/licensecheck) across available cores, plus `--output-sync=target` so each job's output still prints as one block instead of interleaving. `make ci-job-clippy` and `make prepush` keep their existing behavior for other callers (e.g. ci-runner-github-clippy); this only changes how the work is scheduled. The sub-target set is generated from CLIPPY_ARCH_BOARDS (the previous commit's dynamic board list) via `$(eval)`, so it stays correct as that list changes instead of hand-tracking a fixed board count. `$(shell)` doesn't fail the build on a nonzero exit, so a failure in list_arch_boards.sh here would silently produce an empty CLIPPY_ARCH_BOARDS and generate zero board-specific clippy rules -- guarded against in ci-job-clippy's own recipe, not a top-level `$(error)`: that assignment runs on every `make` invocation regardless of target (Make expands all rule headers before deciding what to build), so a parse-time error there would take down unrelated targets like `make clean` too. ci-job-syntax now calls `cargo check --config ` directly instead of through `$(MAKE) allcheck`: a recursive $(MAKE) inside a synced target's recipe escapes --output-sync's buffering (the child make manages its own output independently), so under a parallel `prepush` the check's compile output could land in the middle of another job's printed block. Reproduced directly; this was the only one of prepush's three parallel jobs using that pattern. The recursive `make` only defaults to `-j$(nproc)` when the outer invocation didn't already set one. GNU Make submake `-j` always overrides (not merges with) a parent's -- verified with a synthetic nested-make test: an unconditional `-j` in the recipe ran all jobs at once even under `make -j2 prepush`, silently ignoring the user's own limit and printing "warning: -jN forced in submake: resetting jobserver mode". A MAKEFLAGS-based guard (`$(filter -j% --jobserver%,$(MAKEFLAGS))`) now skips forcing our own -j whenever the outer invocation already set one, so `make -j2 prepush` really does run at 2-way concurrency; a plain `make prepush` still defaults to parallel. `--output-sync=target` itself requires GNU Make >= 4.0 (2013) -- it's the only 4.0+ feature anywhere in this Makefile, checked directly. An older make (e.g. macOS's bundled 3.81 -- Tock's Getting_Started.md macOS setup never mentions installing a newer one, implying reliance on the system make, and Apple has shipped 3.81 indefinitely since Make moved to GPLv3 at 3.82) doesn't degrade gracefully on an unrecognized flag: it's a hard parse-time error before any recipe runs, breaking `make prepush` entirely. Gated it behind `$(MAKE_VERSION)`'s major version using make-function primitives old enough to be safe on 3.81 itself. One real serialization point does exist: these jobs share one workspace `target/` directory (separate `/` subdirectories, not separate target dirs). `-Zbuild-std` (every board rebuilds `core`/ `compiler_builtins` from source, see boards/cargo/unstable_flags.toml) takes what appears to be an exclusive lock on the whole directory while building an uncached triple's sysroot. Confirmed with timestamped concurrent runs on a cold cache: a board needing build-std was blocked on "file lock on build directory" for the *entire* duration of any other concurrent cargo invocation against the same target dir, resuming within ~0.1s of the other one finishing -- both against the root workspace check/clippy and against another board's clippy. This only costs anything on a cold cache (fresh checkout or after `make clean`); once every triple's build-std output is cached, which is the normal case for repeated `make prepush` runs, the lock is never taken again and all jobs run fully concurrently. Also folded in: an `nproc` -> `sysctl -n hw.ncpu` -> `4` fallback chain (stock macOS has no `nproc`, which was silently degrading `-j$(shell nproc)` to unbounded `-j`), and running format-check synchronously before the parallel batch so a trivial `cargo fmt` miss still fails in ~4s instead of waiting behind the full multi-minute clippy/check batch. Verified via a forced worst-case invalidation (touching kernel/src/lib.rs, which invalidates every variant): the parallel run completes correctly and faster than an equivalent serial run, a deliberately broken build now fails in ~4s via the synchronous format-check step, a cold-cache `make -j2 prepush` run completes cleanly with no submake-override warnings, and a full cold `make prepush` run passes with all of the above in place. Also verified directly: `make list` (and other targets unrelated to clippy) still succeeds when list_arch_boards.sh is broken, while `make ci-job-clippy` correctly fails with the intended error message; and `make -n prepush` does show the real recursive-make work. Co-Authored-By: Claude Sonnet 5 --- Makefile | 67 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 53e78cbbebd..86ed438cbcb 100644 --- a/Makefile +++ b/Makefile @@ -217,12 +217,21 @@ ci-nosetup: # Run the fast jobs. # This is designed for developers, to be run often and before submitting code upstream. +# +# These jobs write to separate target-triple subdirectories, so run them +# concurrently once format-check (cheap, fails fast) passes on its own. +# Only default our own -j if the user didn't pass one -- a submake's own +# -j always overrides a parent's instead of merging with it. --output-sync +# needs make >= 4.0 (e.g. macOS's bundled make is 3.81), so only pass it +# if supported, since an unknown flag is a hard error, not a graceful skip. +NPROC := $(or $(shell nproc 2>/dev/null),$(shell sysctl -n hw.ncpu 2>/dev/null),4) +JOBS := $(if $(filter -j% --jobserver%,$(MAKEFLAGS)),,-j$(NPROC)) +OSYNC := $(if $(filter-out 0 1 2 3,$(firstword $(subst ., ,$(MAKE_VERSION)))),--output-sync=target,) .PHONY: prepush -prepush:\ - format-check\ - ci-job-clippy\ - ci-job-syntax\ - licensecheck +prepush: + @$(MAKE) format-check + @$(MAKE) $(OSYNC) $(JOBS) \ + ci-job-clippy ci-job-syntax licensecheck $(call banner,Pre-Push checks all passed!) # Note: Tock runs additional and more intense CI checks on all PRs. # If one of these error, you can run `make ci-job-NAME` to test locally. @@ -401,16 +410,42 @@ ci-job-readme-check: ### ci-runner-github-clippy jobs: +# +# One board per `arch/*` crate actually in use (tools/build/list_arch_boards.sh), +# so clippy checks every architecture's target-specific code without +# checking every board. +# +# Split into sub-targets so `make -j` can run them concurrently instead of +# back-to-back. They share one workspace `target/` dir, so on a clean +# checkout they still partly serialize on `-Zbuild-std`'s lock; cached +# (normal) runs don't. +# +# $(shell) doesn't fail the build on a nonzero exit, so an empty result +# (the script errored, or a future change left it with nothing to say) +# would otherwise silently generate zero board-specific clippy rules +# below instead of failing loudly. Checked in ci-job-clippy's own recipe, +# not a top-level $(error): this assignment runs on every `make` +# invocation regardless of target (Make expands all rule headers up +# front), so failing here would take down unrelated targets like `make +# clean` whenever this script breaks. +CLIPPY_ARCH_BOARDS := $(shell ./tools/build/list_arch_boards.sh) + .PHONY: ci-job-clippy -ci-job-clippy: - $(call banner,CI-Job: Clippy) +ci-job-clippy: ci-job-clippy-workspace $(foreach b,$(CLIPPY_ARCH_BOARDS),ci-job-clippy-$(subst /,-,$(b))) + @test -n "$(CLIPPY_ARCH_BOARDS)" || (echo "error: list_arch_boards.sh returned no boards -- check it succeeded" >&2; exit 1) + +.PHONY: ci-job-clippy-workspace +ci-job-clippy-workspace: + $(call banner,CI-Job: Clippy (workspace)) @cargo clippy -- -D warnings - # One board per `arch/*` crate (tools/build/list_arch_boards.sh). - @arch_boards="`./tools/build/list_arch_boards.sh`" || exit 1;\ - for b in $$arch_boards;\ - do echo "$$(tput bold)Clippy $$b$$(tput sgr0)";\ - (cd boards/$$b && cargo clippy -- -D warnings) || exit 1;\ - done + +define clippy_arch_board_rule +.PHONY: ci-job-clippy-$(subst /,-,$(1)) +ci-job-clippy-$(subst /,-,$(1)): + $$(call banner,CI-Job: Clippy ($(1))) + @cd boards/$(1) && cargo clippy -- -D warnings +endef +$(foreach b,$(CLIPPY_ARCH_BOARDS),$(eval $(call clippy_arch_board_rule,$(b)))) @@ -422,10 +457,14 @@ ci-job-clippy: # `rustflags` a crate already sets via its own `.cargo/config.toml`. DENY_WARNINGS_CARGO_CONFIG := $(CURDIR)/boards/cargo/deny_warnings.toml +# Calls `cargo check` directly rather than via `$(MAKE) allcheck`: a +# recursive $(MAKE) here bypasses --output-sync=target's buffering (the +# child manages its own output), so under a parallel `prepush` its +# compile output could land in the middle of another job's block. .PHONY: ci-job-syntax ci-job-syntax: $(call banner,CI-Job: Syntax) - @TOCK_CARGO_FLAGS="--config $(DENY_WARNINGS_CARGO_CONFIG)" $(MAKE) allcheck + @cargo check --config $(DENY_WARNINGS_CARGO_CONFIG) .PHONY: ci-job-compilation ci-job-compilation: