You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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 #include inside "…"),
the few preprocessing cases that matter (#if 0 / #ifdef __has_include can 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>.h whose header appears in included_files marks the library as "selected".
For a selected library, compile libraries/<Name>/src/**/*.{c,cpp,S} (not examples/, extras/, tests/), add libraries/<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 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 Blink compiles ≤ 250 TUs (currently 451). Specifically, FNET, Snooze, RadioHead, mbedtls are 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.
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 teensy41 project ≤ 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.
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:
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.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:
#includeinside"…"),#if 0/#ifdef __has_includecan be skipped — false positives in include set are fine; false negatives are not).Vec<IncludeRef>per TU, with{ path: String, kind: Quoted | Angled, span: (line, col) }.Prior art in the Rust ecosystem:
cpp_includes/include_graphon 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".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
chaindoes 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:
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
#if, don't expand macros. LDF doesn't either.compile_commands.json. The scanner complements it.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).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.crates/fbuild-cache/— new namespace for library-selection results.Related