Repository navigation
Description
Summary
Replace the current eager "compile every library under <framework>/libraries/" behavior with a fast LDF-style transitive reachability scan, written in Rust and cached through zccache so the cost is paid at most once per (project, toolchain, library-set) tuple.
This is the proper fix for #204 (teensyLC/30 RAM overflow from unreferenced framework libraries) and gives the right answer for STM32 (#202) and any future platform port.
Motivation
Two competing goals:
- Correctness — only link libraries transitively reachable from the sketch, matching PlatformIO's LDF
chainmode. teensy30/LC: Teensy orchestrator compiles unreferenced framework libraries (FNET, Snooze, RadioHead, mbedtls) — causes .bss / .dmabuffers RAM overflow #204 shows that building the wholelibraries/*tree pins unreferenced statics (FNET / Snooze / RadioHead / mbedtls ~300 TUs for a Blink sketch) in.bss, which blows the 8 KB / 16 KB RAM budget on teensyLC / teensy30. - Speed — PlatformIO's LDF is notoriously slow (spawns the C preprocessor, single-threaded, runs every build). fbuild's selling point is that it's fast; copying PlatformIO's design wholesale would regress build time.
A Rust-native scanner backed by zccache hits both.
Proposal
1. Native Rust header scanner
A parser that reads C/C++ source and extracts #include "…" / #include <…> directives without invoking the preprocessor.
Design notes:
- No full C parse. Just line-oriented tokenization that respects:
- block / line comments,
- string literals (don't match
#includeinside"…"), - the few preprocessing cases that matter (
#if 0/#ifdef __has_includecan be skipped — false positives in include set are fine; false negatives are not).
- Output:
Vec<IncludeRef>per TU, with{ path: String, kind: Quoted | Angled, span: (line, col) }. - Performance target: ≥ 50 MB/s single-thread on a modern host. With rayon fan-out across TUs, a 500-source Teensy core scan should be ≤ 50 ms cold.
- Unit-testable against a table of C/C++ fixtures; no external fixtures needed.
Prior art in the Rust ecosystem: cpp_includes / include_graph on crates.io, plus Bazel's and Meson's internal scanners. None are quite drop-in; a 200–400 LoC purpose-built scanner is simpler than adopting a dependency and lets us cache the output shape we actually want.
2. Transitive reachability walk
Given a seed set of TUs (the sketch + FastLED library sources + core required objects), walk the include graph over the union of project + framework + toolchain header search paths, and produce:
included_files: Set<PathBuf>— all headers transitively reached.required_libraries: Set<LibraryId>— any Teensyduino (or STM32duino, or ESP)libraries/<Name>/src/<Name>.hwhose header appears inincluded_filesmarks the library as "selected".- For a selected library, compile
libraries/<Name>/src/**/*.{c,cpp,S}(notexamples/,extras/,tests/), addlibraries/<Name>/src/as an include root, and link the resulting archive. Then re-seed and walk again until the selection set stabilizes (fixed-point over include closure — typically 2–3 iterations).
This is exactly what PlatformIO LDF chain does semantically, but done in-process in Rust instead of spawning gcc or fork-wrapping a script.
3. zccache-backed memoization
The output of the scanner is a pure function of:
- Source file content hashes (the zccache input-fingerprint already tracks this per TU).
- The ordered search-path list (framework + toolchain + project include dirs).
- The toolchain triple (different toolchains may have different builtin headers that end the walk).
Cache key: blake3(source_content_hashes || search_paths || toolchain_triple || scanner_version).
Cache value: the serialized { included_files, required_libraries } pair. Bincode is fine; size is small (a few KB per project).
Store: put it in the existing zccache store under a new namespace (library-selection/…) so it rides existing actions/cache logic (#149, #151) and survives cross-runner restore.
Expected hit rate: on warm CI runs the scanner should be a pure cache hit. On cold runs it should be ≤ 100 ms for a typical FastLED sketch.
Non-goals
- Not a full preprocessor. We don't evaluate
#if, don't expand macros. LDF doesn't either. - Not a replacement for
compile_commands.json. The scanner complements it. - Not a replacement for the existing per-TU compile cache. It's a new cache for a new, separate question ("which libraries do I need?").
Acceptance criteria
fbuild build teensylc Blinkcompiles ≤ 250 TUs (currently 451). Specifically,FNET,Snooze,RadioHead,mbedtlsare not compiled. Link succeeds with.bss≤ 3 KB.fbuild build teensy30 AnalogOutput— link succeeds,.dmabuffers≤ 1 KB.fbuild build teensy41 Blink— still succeeds; the existing Teensy 4 test coverage continues to pass.fbuild build stm32f103c8 Blink— the SPI dependency is discovered automatically; no manual allowlist needed (closes stm32: Arduino_Core_STM32 framework libraries (SPI, Wire, ...) not discovered — fatal error: SPI.h: No such file or directory #202 too).- Warm build of the FastLED examples matrix adds ≤ 50 ms over current fbuild on the library-selection step (measured in bench/fastled-examples).
- Cold run of library selection on
teensy41project ≤ 200 ms on a typical CI runner.
Suggested crate layout
crates/fbuild-header-scan/— the scanner + include-graph walker. Standalone, no fbuild dependencies; testable on its own.crates/fbuild-library-select/— the per-orchestrator library resolver. Uses the scanner, adds the Teensyduino / STM32duino / Arduino library conventions. Wired into each*/orchestrator.rs.- zccache wiring in
crates/fbuild-cache/— new namespace for library-selection results.
Related
- teensy30/LC: Teensy orchestrator compiles unreferenced framework libraries (FNET, Snooze, RadioHead, mbedtls) — causes .bss / .dmabuffers RAM overflow #204 — teensyLC/teensy30 .bss overflow (bug this fixes)
- stm32: Arduino_Core_STM32 framework libraries (SPI, Wire, ...) not discovered — fatal error: SPI.h: No such file or directory #202 — STM32 SPI discovery (bug this also fixes)
- Teensy 4.x builds miss framework libraries like SPI and OctoWS2811 #163 / Fix Teensy framework library resolution #164 — prior Teensy framework-library resolution (current implementation that needs replacing)
- build: per-TU cache misses across CI runs — fast-path fingerprint bakes mtime, defeats content-based reuse #146 / build: absolute filesystem paths baked into cache keys — breaks cross-runner / cross-workspace reuse #148 / daemon/setup: expose zccache store location so consumer workflows can actions/cache it #149 / bench: add zccache store to bench/fastled-examples actions/cache path list #151 — zccache cross-runner cache strategy (the cache substrate reused here)
- Teensy 3.0 / LC link-time RAM overflow (.dmabuffers / .bss) across all examples FastLED#2406 — consumer-side tracking
Activity
zackees commentedon Apr 24, 2026
TDD Test Plan, Implementation Plan, and Parallel Subagent Directive
Note on the issue text
One small correction before locking in the design: the issue says PlatformIO's LDF is slow because it "spawns the C preprocessor." That's not quite right — PlatformIO LDF is Python (
platformio/builder/tools/piolib.py), and inchain/chain+modes it does its own regex-based#includescan in Python (nocppsubprocess per TU). The slowness is Python + single-threaded walking + SCons node graph overhead, not preprocessor spawning. The Rust-native + rayon + zccache answer is still correct — just for the right reason.Part 1 — TDD Test Plan
Organized Red → Green → Refactor. Every criterion maps to a concrete
#[test], integration harness, or artifact probe. No mocks — usetempfile::TempDirtrees per the project's TDD rule.1.
fbuild-header-scan— Scanner unit testsGoal: pure function
scan(&str) -> Vec<IncludeRef>. No I/O. Fixture strings inline.1.1 Directive recognition
#include <stdio.h>stdio.h#include "foo.h"foo.h#include <a.h>(leading ws)# include <a.h>(ws after#)#include <a/b/c.h>a/b/c.h#include <a.h> // trail#include "a.h" "b.h"1.2 Must-NOT-match cases (false-positive guards)
// #include <evil.h>/* #include <evil.h> *//*\n#include <evil.h>\n*/const char* s = "#include <evil.h>";const char* s = "\"#include <evil.h>\"";R"(#include <evil.h>)"char c = '#'; #include <a.h>//#include <a.h>\n#include <b.h>b.h1.3 Line/column spans
\n\n#include <a.h>#include <a.h>1.4 Permissively over-include (per issue: false positives OK, false negatives not)
#if 0\n#include <a.h>\n#endif#ifdef __has_include\n#include <a.h>\n#endif#if defined(X)\n#include <a.h>\n#else\n#include <b.h>\n#endif1.5 Fuzz property tests (
proptest)scan()never panics.#includelines (interspersed with comments/strings), scanner returns exactly N refs in order.//) monotonically decreases the ref count by 1.1.6 Performance micro-bench (criterion)
cores/teensy4/*.cpp, ~2 MB). Fail CI if < 30 MB/s.2.
fbuild-header-scan— Graph walker unit testsGoal:
walk(seeds, search_paths) -> { reached: Set<PathBuf>, unresolved: Set<String> }. Usetempfile::TempDirtrees — no mocks.2.1 Resolution order
unresolved, walk continues2.2 Graph semantics
a.h→b.h→a.hreachedmain.cpp→a.h,b.h; both →common.hcommon.hscanned exactly once<stdio.h>from toolchain include path marks as reached but does not recurse into libc internalsbits/*.hin reached set2.3 Determinism
reachedreturned as sortedVec<PathBuf>— run twice on same tree, byte-equal output (required for stable cache keys).3.
fbuild-library-select— Resolver unit testsGoal:
resolve(seeds, framework_root, project_root) -> Selection { included_files, required_libraries }.3.1 Fixed-point iteration
<SPI.h>;libraries/SPI/src/SPI.hexistsSPIinrequired_librariesSPI/src/SPI.cppincludes<Wire.h>;libraries/Wire/src/Wire.hexistsSPIandWireselected (2nd iteration)required_librariesempty3.2 Selection filters (per-framework conventions)
libraries/Foo/src/Foo.hreachedFoo/src/**/*.{c,cpp,S}returned as compile setlibraries/Foo/examples/**presentlibraries/Foo/extras/**,tests/**presentlibraries/FNET/but not reached from seedslibraries/SrcWrapper/src/stm32/layoutsrc/, honorslibrary.propertiesinclude root3.3 Framework-specific conventions
library.jsondeclares alternatesrcDir→ resolver honors it.SPI.h, user writes<Spi.h>).4.
fbuild-cache— zccache integration testsUse a real
TempDir-rooted zccache store.4.1 Key stability
scanner_versionconstant4.2 Hit/miss semantics
resolve).5. Per-orchestrator integration tests
Each is a
cargo test -p fbuild-library-select --test <name>using real tiny framework trees incrates/fbuild-test-support/fixtures/.orchestrator.rswired to library-select; Blink sketch → compile set excludesFNET,Snooze,RadioHead,mbedtls.<SPI.h>→ SPI lib auto-selected; no manual allowlist.required_libraries.#includefrom the sketch and rebuilding drops the library from compile set.6. End-to-end board tests (TDD acceptance criteria from issue)
FNET|Snooze|RadioHead|mbedtlsabsent from compile DB7. Post-build artifact inspection
This is the teeth of the acceptance criteria. Don't trust "it compiles" — measure the ELF. Prefer the
objectcrate to parse ELF in-process.7.1 Section-size probes
firmware.elf.bsssizefirmware.elf.dmabufferssection.text + .data + .bss.textSPI symbolsSPIClass::*present7.2 Symbol presence / absence
fnet_*,snooze_*,RadioHead::*,mbedtls_*symbolssetup,loop,digitalWritepresentSPIClass::transferpresent7.3 Compile-DB / TU-count probe
.fbuild/compile_commands.jsonTU count, teensyLC/Blink.ofiles underlibraries/FNET/**required_libraries∪ {core}7.4 Link-line probe
-lFNET,-lSnooze,-lRadioHead,-lmbedtlsabsent from teensyLC link.--start-group ... --end-grouparchives reachable from sketch.8. Performance benchmarks (gate, not just report)
Wire these through
bench/fastled-examplesso regressions fail CI, not just appear in a report.Part 2 — Step-by-Step Red → Green Implementation Plan
Phase 0 — Test-support scaffolding (parallel, independent)
fbuild-test-support::MiniFramework—TempDirbuilder for fake Teensyduino/STM32duino/Arduino trees (libs, sketches, cores)fbuild-test-support::ElfProbe— wrapsobjectcrate:sections(),symbols(),section_size(name)fbuild-test-support::CompileDb— parsescompile_commands.jsonintoHashSet<PathBuf>crates/fbuild-header-scan/(Cargo.toml, lib.rs stub, README)crates/fbuild-library-select/library-selection/namespace to zccache store schemaPhase 1 — Scanner (TDD cycle)
#[test].proptestfor P-01..P-03.criterionbench B-01..B-02.Phase 2 — Graph walker
MiniFramework.walk()— BFS with visited set, quoted-first resolution for#include "…", search-path order for<…>.Phase 3 — Resolver
examples/,extras/,tests/; includesrc/**/*.{c,cpp,S}.library.json/library.propertiesparser, case-folding on Windows.Phase 4 — zccache memoization (parallel with Phase 3 after Phase 2)
blake3(sorted_source_hashes ++ search_paths ++ toolchain_triple ++ SCANNER_VERSION).fbuild-cache::library-selectionnamespace, bincode-serializedSelection.Phase 5 — Per-orchestrator integration (parallel by framework)
All 4 can run in parallel after Phase 3 lands:
crates/fbuild-build/src/teensy/orchestrator.rscrates/fbuild-build/src/stm32/orchestrator.rscrates/fbuild-build/src/avr/orchestrator.rscrates/fbuild-build/src/esp32/orchestrator.rs#includeremovalEach step replaces the eager "compile every library" call with
library_select::resolve(...)and feeds the result into the existing compile pipeline.Phase 6 — End-to-end + artifact gates
ElfProbe.Phase 7 — Performance gates
Phase 8 — Cleanup
docs/architecture/with the new library-selection subsystem.tasks/lessons.md.Part 3 — Acceptance Criteria (flat checklist)
Functional
proptestiterations without panic.examples/,extras/,tests/.Caching
Integration
#includedrops the library from next build (I-05).End-to-end (issue acceptance criteria)
.bss≤ 3 KB..dmabuffers≤ 1 KB.Artifact probes
Performance
Hygiene
RUSTFLAGS="-D warnings").uv run soldr cargo clippy --workspace --all-targets -- -D warningsclean.uv run soldr cargo fmt --allclean.RUSTDOCFLAGS="-D warnings" uv run soldr cargo doc --workspace --no-depsclean.README.md(hook enforces).docs/architecture/updated.tasks/lessons.mdupdated.Part 4 — Parallel Subagent Directive
To any agent/contributor picking up this work: parallelize aggressively. The dependency graph below has several independent branches. Dispatch subagents for each concurrently. Do not serialize work that can be done in parallel.
Dependency DAG
Subagent dispatch rules
Parallel execution target
With 6 subagents running Phase 0 simultaneously, Phase 0 should complete in roughly the time of the slowest single item (~MiniFramework). With 4 subagents in Phase 5, per-orchestrator wiring lands together rather than over 4 serial rounds. Target total wall-clock: measured in days, not weeks.
Part 5 — zccache Coordination Directive
If any part of this work requires a change to zccache itself (new namespace support, serialization helpers, bug fixes in the cache layer that block library-selection memoization, cross-runner restore semantics, etc.), the workflow is:
~/dev/zccache.Cargo.toml,CHANGELOG, tag.cargo search zccache,pip index versions zccache), don't just rely on CI success.Cargo.toml/pyproject.toml), re-resolveCargo.lock.Do not vendor-patch zccache locally in fbuild's
Cargo.toml(nopath = "../zccache", no[patch.crates-io]hacks committed tomain). Path overrides are fine for local iteration, but the landed code must pull a published version — otherwise CI and downstream consumers will break.All updates — zccache changes, fbuild integration PRs, blockers, decisions — must be posted as comments on issue #205. This issue is the single source of truth for the feature's progress. Don't fragment status across a zccache issue, a Slack thread, or a separate fbuild issue. One thread, one timeline.
Part 6 — Build-Breakage Fix Directive
All build breakages encountered along the way must be fixed, regardless of whether they are caused by this work or not.
This includes but is not limited to:
-D warningson touched files.rust-toolchain.toml,Cargo.lock, or hook scripts.mainduring this feature's development.Rationale: a partially green tree masks real regressions in the work at hand. If an unrelated breakage is left unfixed, every future test run is contaminated with a "known red" that hides new breakages introduced by this change. Fix it, commit the fix as a separate PR or preceding commit, then continue.
If a breakage is outside the scope of what's reasonable to fix, the subagent must:
// TODO(#NEW_ISSUE)comment, or (b) block the phase until the separate issue is resolved.#[ignore],#[cfg(not(ci))], or--no-verifypast the breakage.Zero tolerance for red CI on landing. The final commit in this feature must have a fully green workspace — clippy, fmt, doc, tests, board matrix, perf gates.
zackees commentedon Apr 24, 2026
For PR submission the agent should push and then confirm green and then merge or fix until it's merged.
zackees commentedon Apr 24, 2026
Open-Question Resolution (post-research)
Before kicking off subagents, I dug into the current fbuild3 code, the zccache repo, and PlatformIO's LDF source to resolve the open questions. The research turned up four findings that materially change the original plan, then concrete answers to Q1–Q15.
Findings that change the plan
Finding 1 — fbuild3 already has a transitive walker
crates/fbuild-build/src/framework_libs.rs:22-95(resolve_framework_library_sources) is already a BFS worklist over#includeedges. It is NOT the "eager compile everything" approach the issue framing implies — it's a naive version of what #205 proposes.Consequence: #205 is not a green-field implementation. It's:
--gc-sectionsstrip" (esp32/orchestrator.rs:569-760),Two suspect bugs in the current walker worth pinning early in Phase 1:
framework_libs.rs:127matches headers by basename only — collisions are possible (anyArduino.hwins regardless of which lib contains it).framework_libs.rs:126compiles.c, .cpp, .cc, .cxx, .sfor libraries but omits uppercase.S— assembly sources in libs are silently dropped.Finding 2 — zccache has no Rust K/V API today
~/dev/zccacheexposes only a compile-action-shaped store:ArtifactStore::insert(key, &ArtifactIndex)whereArtifactIndexis specifically{ output_names, output_sizes, stdout, stderr, exit_code }(crates/zccache-artifact/src/store.rs:20-39). There is noget_bytes(key) -> Vec<u8>/put_bytes(key, bytes)surface, no namespace concept, and fbuild3 currently consumes zccache only as a CLI subprocess — zero Rust dep in anyCargo.toml.Consequence: Phase 4 (cache wiring) activates the zccache-coordination directive from the previous comment. Concretely:
~/dev/zccachelocally.zccache-kvcrate (or extendzccache-artifact) with namespaced blake3-keyedVec<u8>get/put backed by redb + content-addressed files.No vendor-patch shortcut. This must ship through a real zccache release.
Finding 3 — PlatformIO LDF does path-prefix attribution, not basename
Per
platformio/builder/tools/piolib.py::search_deps_recursive(L428) and__contains__(L158-178): PIO resolves each#includeto an absolute path via a real scanner, then attributes the resolved path to whichever library'sinclude_dirscontains it as a prefix. It does not match by folder name, does not readlibrary.propertiesincludes=(Arduino-IDE-only), and does not pre-glob.h.Consequence: The new resolver should match PIO's path-prefix attribution, not the basename match the current fbuild3 code uses. This is also cleaner — it falls out naturally from the walker's search-path resolution.
Finding 4 — PlatformIO LDF chain mode is 2-pass, not fixed-point
Chain mode is: (1) BFS from project sources, marking libs dependent when reached; (2) one reconciliation pass (
_correct_found_libsL1156) that re-walks each dependent lib's full source set (not just headers) to capture anything missed by the header-only first pass. Unconverged deps are silently dropped (L1164-1167). No iteration cap, no N-pass fixed point.Consequence: The issue's "fixed-point over include closure — typically 2–3 iterations" framing is wrong for matching PIO semantics. Replace with PIO's 2-pass (BFS + reconciliation) design. Simpler and proven.
Answers to Q1–Q15
A. Seed set & scan boundary
Q1 — Seed set: Match PIO exactly. Seeds = user's
src/+include/trees (plus test dir when testing). The sketch.inois part ofsrc/. Core/variant TUs are NOT seeds — they're separately compiled framework code. Project's own LDF mode is always upgraded chain→deep (PIO does the same atpiolib.py:951), so the full project tree is walked regardless of any--ldf-mode chainflag.Q2 — Terminal roots: No explicit terminal-root allowlist. Simply do not add toolchain system include paths to the walker's search paths.
<stdio.h>naturally goes unresolved and is silently skipped. The walker's search paths are: project include dirs + selected framework libs' include dirs. That's it.Q3 — Unresolved headers: Silently skip. Match PIO. Optionally emit a
--verboselog line per unresolved include for debugging, but never warn or error by default. Covered by test W-04.B. Library matching conventions
Q4 — Library discovery: Path-prefix attribution, not basename. For each library, collect its
include_dirs(i.e.,src/if present, else lib root; plusutility/,include/if they exist, plus anylibrary.json build.includeDiroverride). Resolve each#includeto an absolute path via the walker; attribute the resolved path to the lib whoseinclude_dirscontains it as a prefix. Parselibrary.jsonbuild.includeDir/srcDir/srcFilteroverrides (PIO L794-879). Do NOT consultlibrary.propertiesincludes=(PIO ignores it; Arduino-IDE-only).Q5 — Source extensions: Match PIO's Arduino-library set —
c, cpp, cc, cxx, c++, s, S(PIO L31-36, withasm/ASMdropped for Arduino libs per L568-576). Fix the current.Somission atframework_libs.rs:126. Exclude.h/.hppfrom compilation (they're inputs to the scanner only)..inois project-only, never a library source.Q6 — Layout: Auto-detect. If
<lib>/src/exists → src-nested layout (src_dir=lib/src). Else → flat (src_dir=lib). Also expose<lib>/utility/and<lib>/include/as include dirs when present.library.json build.srcDir/includeDirfully override. Current fbuild3 already does this correctly atfbuild-packages/src/library/framework_library.rs:60-78— keep.C. Convergence & re-seeding
Q7 — Re-seed semantics: Two-pass, PIO style:
#includeedges. When a resolved include lands inside any lib's include dirs, mark that libdependent=trueand add the reached header to the walk frontier (not all its sources).dependent=truelib, do a full walk seeded with all its source files, not just reached headers. Any lib newly reached in pass 2 is also marked dependent (one extra pass, not recursive). Unconverged deps dropped silently.Q8 — Convergence cap: No cap on the LDF walk itself (PIO has none). Cycle-safe via the visited-path set — already handled in the BFS. The only cap is preprocessor-directive depth inside a single file, and we don't evaluate
#ifat all, so it's moot.D. Cache & invalidation
Q9 — Cache key composition: Do NOT hash every framework source (too expensive — Teensyduino is ~500 files). Key =
blake3(src/<lib-name>.h(canonical header), pluslibrary.properties/library.jsonmtime-or-hash,SCANNER_VERSIONconst,LDF_MODE_VERSIONconst (bump when 2-pass algorithm changes))This catches content changes without hashing everything. Also catches framework upgrades via the version identifier.
Q10 —
framework_libs.rs(417 lines) fate: Port and delete. Reusable bits → newfbuild-library-selectcrate: the local-header shadowing (L49-52), the exclusion filter (should_scan_framework_entryL190-199), and the#includeregex (L222-240, will be superseded by the new scanner but it's a reference). Rest gets replaced by the new resolver. Delete the file entirely at end of Phase 8. Its only current caller is viaresolve_framework_library_sourcesin orchestrators — update them in Phase 5.E. Scope & interactions
Q11 — FastLED as seed: FastLED is a user library/project, not a framework library. Every
src/**/*.{cpp,c,S}in the FastLED tree is a seed. No special-casing — falls out of Q1. When building a FastLED sketch, the sketch source is the entry point; FastLED'ssrc/is on the project-source seed set exactly like<project>/src/.Q12 — ESP32: Arduino-flavored ESP32 builds do run the new resolver. Currently ESP32 compiles every built-in lib and lets linker
--gc-sectionsstrip — this is the eagerly-compiled case the issue actually refers to, not teensy (see Finding 1). Pure ESP-IDF/CMake component model is out of scope for #205. In Phase 5,esp32/orchestrator.rs:569-760is the integration point — replace the greedy loop with resolver output.Q13 — Introspection CLI: Add
fbuild lib-select --explain <project> -e <env>in Phase 8. Prints the selected library set, each lib's trigger include, pass-1 vs pass-2 origin, and unresolved headers. Low priority but cheap once the resolver exists, and will pay for itself the first time someone debugs an AC#4-style STM32 SPI discovery regression.F. Process
Q14 — PR strategy: Stacked PRs per phase. Each phase is independently reviewable and mergeable:
--explainCLI + framework_libs.rs deletion.Squash-merge when the branch has messy intermediate commits; regular merge when commits are clean (per
.claude/rulesgit-workflow). This stack allows Phases 1/2/3 to land onmaineven if the zccache coordination slips.Q15 — Baseline measurements: No baseline infrastructure exists today. Add a Phase 0.7 "measure baseline" subagent that runs (on a stable
mainSHA, pinned):uv run soldr cargo run -p fbuild-cli -- deploy tests/platform/teensyLC -e teensyLC --dry-run→ capture TU count, compile_commands.json.firmware.elffor teensyLC/Blink, teensy30/AnalogOutput, teensy41/Blink, stm32f103c8/Blink; parse with theobjectcrate for.text/.data/.bss/.dmabuffers.tasks/baseline-205.mdwith SHA + date.This anchors AC#1/#2/#3 thresholds to real data (issue quotes 451 TUs but doesn't cite
.bssin bytes). Without this, thresholds are guesses and agents will argue about them mid-Phase-6.Revised DAG (post-research)
Ready-to-dispatch Phase 0 subagents
Six parallel subagent jobs are now unblocked. No further open questions should stall them:
fbuild-test-support::MiniFramework— TempDir builderfbuild-test-support::ElfProbe—object-crate wrapperfbuild-test-support::CompileDb— parse compile_commands.jsoncrates/fbuild-header-scan/skeleton (Cargo.toml, README, lib.rs stub, bench harness, proptest dev-dep)crates/fbuild-library-select/skeleton (Cargo.toml, README, lib.rs stub)zccache-kvpublic API sketch posted as #205 comment for review before Phase 4.a beginstasks/baseline-205.mdDispatch all seven in one round.
feat(library-selection): foundation + Phase 0 scaffolding for #205
feat(library-selection): foundation + Phase 0 scaffolding for #205 (#207
zackees commentedon Apr 25, 2026
CodeRabbit follow-ups from PR #208
Captured here so they remain visible across PR merges. All 5 are minor; addressed in a follow-up PR.
1.
crates/fbuild-build/tests/teensylc_acceptance.rs:80— overly-permissive setup/loop fallbackhas_symbol_containing("setup")/"loop"matches mangled C++ symbols whose names contain those substrings (e.g._ZN6Stream8setupXxx). Sincesetup/loopareextern "C"from the sketch, drop the substring fallback and usehas_symbol(...)only.2.
crates/fbuild-cli/src/lib_select.rs:25— possibly unusedFrameworkimportCodeRabbit flags
use fbuild_packages::Framework;as unused. Local clippy-D warningswas green so this may be a false positive on the bot's side, but it's worth verifying and removing if truly unused.3.
crates/fbuild-cli/src/lib_select.rs(related) — document the in-process exception incrates/CLAUDE.mdThe CLI's "thin HTTP client" guideline in
crates/CLAUDE.mddoesn't currently call out that diagnostic subcommands (lib-select,clang-tidy,iwyu,clang-query,mcp,lnk) intentionally run in-process. Add a sentence so future readers don't think the newlib-selectviolates the contract.4.
docs/architecture/library-selection.md:5— stale status blockThe status note still lists Phase 6 + Phase 8.a as follow-ups, but #208 ships them. Update to:
5.
docs/architecture/library-selection.md:143— Phase 8 in "Future work" is wrongPhase 8.a (
fbuild lib-select --explainCLI) ships in #208 — it should not be under "Future work". Either narrow the entry to Phase 8.b (framework_libs.rsfinal deletion) or move Phase 8.a to a "Now" / "What's shipped" section.These are all addressed in PR (link to follow-up PR once opened).
feat(library-selection): #205 Phase 6 acceptance gates + Phase 8 CLI/…
docs(library-selection): apply CodeRabbit follow-ups from PR #208 (#209)
feat(library-selection): #205 Phase 7 perf benchmarks (P-02, P-03) (#210
ci(library-selection): #205 acceptance + bench workflows + bench/ stub (
zackees commentedon May 10, 2026
Phase 4 status update — zccache K/V memoization shipped
Phase 4 of the revised DAG is up for review at #212.
What's done
~/dev/zccache, add KV cratezccache-artifact = "1.4.0"Cache-key composition (matches Q9)
blake3(of:(canonical_path, blake3(content))for seeds,(lib_name, blake3(canonical header))for libraries (header content, not every TU — Teensyduino has ~500 files; PIO doesn't hash everything either),SCANNER_VERSION = 1,LDF_MODE_VERSION = 1)Test coverage (16/16 passing)
Resolver (9 tests):
r01_direct_include_selects_library,r02_transitive_library_selection,r03_no_includes_selects_nothing,r13_unrelated_library_not_selected,path_prefix_attribution_distinguishes_same_basename,empty_libraries_yields_empty_selection,missing_library_include_dir_does_not_panic,many_libraries_in_random_order_returns_sorted,required_libraries_returned_sorted_by_name_not_input_order.Cache (7 tests):
c01_cache_miss_then_hit,c02_seed_content_change_invalidates_key,c03_toolchain_change_invalidates_key,c04_framework_version_change_invalidates_key,c05_library_input_order_does_not_affect_key,c06_search_path_order_does_affect_key,c07_corrupt_entry_falls_through_to_recompute.The corrupt-entry test plants garbage bytes under the cache key and asserts that
resolve_cachedrecomputes silently and overwrites — a stale cache must never poison a build.Remaining work on #205
framework_libs.rsonce orchestrators stop calling the legacy BFS. Phase 8.a (fbuild lib-select --explainCLI) shipped in feat(library-selection): #205 Phase 6 acceptance gates + Phase 8 CLI/docs #208.Once #212 lands, Phases 0–6 + 8.a are complete and Phase 4 closes. Phase 7 polish + Phase 8.b are the only follow-ups left to close out #205.
Side fix
Stop-hook plumbing was running
uv run cargoinstead ofsoldr cargo, which on Windows resolves to a different host triple than the rustup toolchainsoldruses — so every Stop fired a target-triple mismatch cascade. Patched in #213 (independent of #205, but blocked the local CI loop while shipping Phase 4).feat(library-selection): #205 Phase 4 zccache K/V memoization
feat(library-selection): #205 Phase 4 zccache K/V memoization (#212)
feat(library-selection): #215 add resolve_warm criterion bench (P-01 …
feat(library-selection): #214 wire resolve_cached into teensy + stm32…
zackees commentedon May 10, 2026
Status update — production wiring + warm bench landed
Two follow-up PRs to Phase 4 (#212) shipped today:
#217 — orchestrator cache wiring (closes #214)
teensy/orchestrator.rsandstm32/orchestrator.rsnow buildCacheKeyInputsfromPackage::get_info()(install path + version) plus stable toolchain triples and route through a newframework_libs::resolve_framework_library_sources_cached(...)helper. The helper falls back to the uncached path on anyKvStorebackend error — same "stale cache must never poison a build" philosophy as the corrupt-entry handling incache.rs.A process-shared
KvStorelives atfbuild_paths::get_cache_root().join("library-selection")viaOnceLock<Option<KvStore>>. Phase 4 is now live in production for both teensy and stm32 builds.5/5 framework_libs tests pass (4 existing + new
cached_resolution_round_trips_through_kvstoreasserting miss → hit). 16/16 library-select tests pass.#216 —
resolve_warmcriterion bench (closes #215)Mirrors
resolve_coldbut pre-populates theKvStoreso the timed loop measures only the cache-hit path. Local numbers (Windows, release, MiniFramework with 30 libs / chain depth 5):resolve_coldresolve_warm~3× speedup on the synthetic fixture; expected to widen on real Teensyduino-class trees where the cold side scales with LDF reconciliation cost. The warm-path floor is bounded by
cache_keyitself — sorted seed/header content hashes — by design (soundness over speed).What's left on #205
bench/fastled-examples/— needs to run resolver cold+warm across a real subset of~/dev/fastled/examples/**per supported board, then anchor AC#5 (≤ 50 ms warm). Distinct from the per-crateresolve_warmbench (which is synthetic). Filed: separate follow-up.ci/measure_baseline_205.pyexists but hasn't been run end-to-end on a host with all toolchains warm. Running now; will post numbers once it completes.framework_libs.rsis now a thin orchestrator-facing delegator (uncached + cached entry points +KvStoreopener + project-seed walker). No dead helpers remain.feat(library-selection): #218 add bench-fastled-examples Phase 7 warm…
feat(library-selection): #218 add bench-fastled-examples Phase 7 warm…
feat(library-selection): #218 add bench-fastled-examples Phase 7 warm…
chore(baseline-205): fix teensylc env name + capture #205 AC baseline (…
chore(baseline-205): tighten excluded-libs scan to file field only
chore(baseline-205): tighten excluded-libs scan to file field only (#221
test(library-selection): #205 add pass-2 reconciliation test, fix tee…
test(library-selection): #205 add pass-2 reconciliation test, fix tee…
test(library-selection): #205 add pass-2 reconciliation test, fix tee…
feat(acceptance): #205 AC#2 teensy30 AnalogOutput .dmabuffers gate (#227
feat(bench): #205 AC#5 enforce <= 50 ms warm threshold in bench-fastl…
feat(acceptance): #205 AC#6 teensy41 cold resolver <= 200 ms gate (#229)
perf(library-select): #236 parallel BFS walker + Pass1/Pass2 scan mem…