Skip to content

fix(stm32): discover Arduino_Core_STM32 framework libraries (fixes #202)#203

Merged
zackees merged 1 commit intomainfrom
fix/issue-202-stm32-framework-libs
Apr 24, 2026
Merged

fix(stm32): discover Arduino_Core_STM32 framework libraries (fixes #202)#203
zackees merged 1 commit intomainfrom
fix/issue-202-stm32-framework-libs

Conversation

@zackees
Copy link
Copy Markdown
Member

@zackees zackees commented Apr 24, 2026

Summary

  • STM32 sketches that #include <SPI.h> (and friends) fail fbuild builds since fbuild became the default backend for STM32 in FastLED CI — the orchestrator installs Arduino_Core_STM32 but never walks libraries/*/ to expose bundled libs to the sketch.
  • Mirrors the Teensy fix from PR Fix Teensy framework library resolution #164: enumerate libraries/*/, resolve which ones the sketch transitively references, inject their include dirs, and compile their sources.
  • Factors out the #include-scanning/resolution logic into a shared framework_libs module so both Teensy and STM32 go through one code path instead of two near-identical copies.

Closes #202.

What changed

  • fbuild-packages: new FrameworkLibrary struct + discover_framework_libraries() walker in library/framework_library.rs. TeensyCores now uses it; Stm32Cores gains get_framework_libraries() / get_framework_library_include_dirs().
  • fbuild-build: new framework_libs.rs module with resolve_framework_library_sources() + include parsing helpers extracted from teensy/orchestrator.rs. Teensy orchestrator calls into it.
  • stm32/orchestrator.rs: calls the shared resolver right after SrcWrapper (so selected library sources land in core_sources) and extends include_dirs with framework.get_framework_library_include_dirs(). This is the user-visible fix for stm32: Arduino_Core_STM32 framework libraries (SPI, Wire, ...) not discovered — fatal error: SPI.h: No such file or directory #202.

Test plan

  • uv run soldr cargo check --workspace --all-targets — clean
  • uv run soldr cargo clippy --workspace --all-targets -- -D warnings — clean
  • uv run soldr cargo test -p fbuild-packages --lib — 407 passed, including new framework_library walker tests
  • uv run soldr cargo test -p fbuild-build --lib — 498 passed, including shared framework_libs resolver tests carried over from the Teensy suite (transitive includes, local-shadow-over-framework)
  • uv run soldr cargo fmt --all --check — clean
  • RUSTDOCFLAGS="-D warnings" uv run soldr cargo doc -p fbuild-packages -p fbuild-build --no-deps — clean
  • End-to-end: kick STM32 FastLED CI (stm32f103c8_bluepill, stm32f411ce_blackpill) and confirm the SPI.h failure no longer reproduces

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Framework libraries are now automatically discovered and included in builds.
    • Enhanced library dependency resolution for STM32 and Teensy platforms with transitive include tracking.
    • Build system now properly resolves required library source files and include paths.
  • Refactor

    • Consolidated framework library discovery mechanism across platforms for improved consistency.

…e, ...)

The STM32 orchestrator installs the STM32duino core but never walked
libraries/*/ to surface bundled libraries to sketches. Any sketch that
transitively included <SPI.h> (e.g. FastLED's STM32 fastspi path) failed
with "fatal error: SPI.h: No such file or directory" despite SPI living
right there in the cache.

Mirror PR #164's Teensy fix:

- Add a shared FrameworkLibrary model + discover_framework_libraries()
  walker in fbuild-packages, used by both TeensyCores and Stm32Cores.
- Extract the #include-scanning / transitive-closure resolver out of the
  Teensy orchestrator into crate::framework_libs so both platforms share
  one code path.
- Wire Stm32Cores::get_framework_libraries() +
  get_framework_library_include_dirs() into the STM32 orchestrator after
  SrcWrapper so the resolver picks up SPI/Wire/EEPROM/... on demand and
  their include dirs reach the sketch compile command.

Fixes #202

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a unified framework library discovery and resolution system for Arduino-style packages. It extracts shared infrastructure to resolve framework libraries by parsing #include directives, maps headers to available libraries, and handles transitive dependencies. The system is integrated into both STM32 and Teensy orchestrators, with Teensy refactored to use the new shared mechanism, eliminating code duplication.

Changes

Cohort / File(s) Summary
Framework Library Discovery & Resolution
crates/fbuild-build/src/framework_libs.rs, crates/fbuild-packages/src/library/framework_library.rs
New shared modules defining FrameworkLibrary struct and discovery/resolution functions. Parses project includes, maps headers to libraries, resolves transitive dependencies, and excludes example/test directories. Includes comprehensive unit tests.
Module Infrastructure & Exports
crates/fbuild-build/src/lib.rs, crates/fbuild-packages/src/library/mod.rs
Public module exports added for framework_libs and unified FrameworkLibrary. Removes TeensyFrameworkLibrary from public API in favor of shared type.
STM32 Core & Orchestrator Integration
crates/fbuild-packages/src/library/stm32_core.rs, crates/fbuild-build/src/stm32/orchestrator.rs
Adds methods to STM32 cores to enumerate framework libraries and include directories. Orchestrator now uses shared resolve_framework_library_sources to extend compilation inputs and include paths with framework libraries transitively required by sketch includes.
Teensy Orchestrator Refactoring
crates/fbuild-build/src/teensy/orchestrator.rs, crates/fbuild-packages/src/library/teensy_core.rs
Replaces custom in-module discovery logic with shared resolver. Removes TeensyFrameworkLibrary type, helper functions for scanning/filtering, and associated unit tests. Signature updated to return FrameworkLibrary instances.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 With includes lost in framework deep,
A rabbit found the scattered heap,
Now unified, both STM32 and Teensy gleam,
Headers dance together—a shared-code dream! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: discovering Arduino_Core_STM32 framework libraries for STM32 builds, directly addressing issue #202.
Linked Issues check ✅ Passed All coding objectives from issue #202 are met: framework libraries are discovered, include directories are added to sketch compilation, library sources are compiled when transitively referenced, and the same selection logic used for Teensy is reused.
Out of Scope Changes check ✅ Passed All changes are within scope of issue #202 requirements. The refactoring of Teensy orchestrator to use shared resolver logic, removal of TeensyFrameworkLibrary in favor of shared FrameworkLibrary, and new public APIs on Stm32Cores are all necessary implementations of the mirrored fix pattern.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-202-stm32-framework-libs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
crates/fbuild-packages/src/library/framework_library.rs (1)

80-131: Source collection: minor filter inconsistency with the build-side scanner.

collect_library_sources_inner skips example(s), test(s), and extras, but the companion scanner in crates/fbuild-build/src/framework_libs.rs (should_scan_framework_entry, Lines 190–199) additionally skips fontconvert. If a framework library ships a fontconvert/ tool tree (e.g. Adafruit-GFX style), its sources would be picked up here and fed to the linker, while its headers would be invisible to the resolver — a recipe for "undefined reference to main" or silent bloat.

Not a problem for the libraries currently shipped by Arduino_Core_STM32 / Teensyduino, so low urgency; worth aligning if you want the two filters to stay in lockstep.

Optional alignment
             if matches!(
                 name.as_str(),
-                "example" | "examples" | "test" | "tests" | "extras"
+                "example" | "examples" | "test" | "tests" | "extras" | "fontconvert"
             ) {
                 continue;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-packages/src/library/framework_library.rs` around lines 80 -
131, collect_library_sources_inner currently filters out "example(s)", "test(s)"
and "extras" but not "fontconvert", causing a mismatch with
should_scan_framework_entry; update collect_library_sources_inner (used by
collect_library_sources) to also skip directories named "fontconvert" (and its
lowercase variants) alongside the existing matches so library tool trees under
fontconvert/ aren't picked up as linkable sources.
crates/fbuild-build/src/stm32/orchestrator.rs (1)

107-121: Core fix for issue #202 looks right — one small dedup opportunity.

The invocation correctly mirrors Teensy: resolve after SrcWrapper sources are in, extend core_sources, and extend include_dirs with bundled library header paths so <SPI.h> resolves. The ordering (framework lib includes appended before CMSIS/sysroot) means bundled library headers can't be shadowed by toolchain-provided headers.

Nit: framework.get_framework_libraries() at Line 112 already walks libraries/*; the later call to framework.get_framework_library_include_dirs() at Line 195 re-walks the same tree (see stm32_core.rs Lines 113–118, which calls get_framework_libraries() again internally). You can skip the second walk by reusing the vector from Line 112:

♻️ Avoid re-walking libraries/
         let framework_libs = framework.get_framework_libraries();
         let framework_library_sources =
             resolve_framework_library_sources(&framework_libs, &params.project_dir, &ctx.src_dir);
         if !framework_library_sources.is_empty() {
             tracing::info!(
                 "STM32 framework library sources added: {}",
                 framework_library_sources.len()
             );
             sources.core_sources.extend(framework_library_sources);
         }
         // Bundled framework library headers (SPI, Wire, EEPROM, ...) so
         // sketches can `#include <SPI.h>` etc.
-        include_dirs.extend(framework.get_framework_library_include_dirs());
+        include_dirs.extend(framework_libs.iter().flat_map(|l| l.include_dirs.iter().cloned()));

Low-impact since discovery is cheap, but saves one full directory walk per build.

Also applies to: 193-195

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-build/src/stm32/orchestrator.rs` around lines 107 - 121, The
code currently calls framework.get_framework_libraries() to compute
framework_libs and later re-invokes
framework.get_framework_library_include_dirs(), which causes a duplicate
directory walk; reuse the previously computed framework_libs when computing
include dirs instead of calling get_framework_library_include_dirs() again: pass
framework_libs into whatever helper or directly compute include dirs from that
vector and extend sources.include_dirs with the result, leaving
resolve_framework_library_sources(&framework_libs, ...) and extending
sources.core_sources unchanged; update references around framework_libs,
framework_library_sources, and sources.include_dirs to use the reused data.
crates/fbuild-build/src/framework_libs.rs (2)

86-91: tracing::info! log level for per-library selection — consider debug!.

Each selected library emits an info-level line at build time. On a sketch that pulls SPI + Wire + EEPROM that's 3 lines, but on a fatter project this could add noise to the normal build log. tracing::debug! would match the "detail" nature of the message while keeping it available under RUST_LOG=debug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-build/src/framework_libs.rs` around lines 86 - 91, The
per-library selection log currently uses tracing::info! inside framework_libs.rs
(around where libraries[idx].name and libraries[idx].source_files.len() are
referenced and where sources.extend(libraries[idx].source_files.iter().cloned())
is called); change that tracing::info! call to tracing::debug! so these routine,
detailed messages are only emitted under debug logging (RUST_LOG=debug) to avoid
noisy normal build logs while keeping the same message and interpolation of
libraries[idx].name and libraries[idx].source_files.len().

38-47: First-library-wins on duplicate headers — document or verify intent.

header_to_library.entry(header).or_insert(idx) means that if two bundled libraries both expose the same header basename, the first one in libraries.iter() order wins silently. Since discover_framework_libraries sorts alphabetically by library name, the winner is determined by alphabetical order, not precedence.

For Arduino_Core_STM32 (and Teensyduino) today, header basenames are unique across libraries/*, so this is a non-issue in practice. Still, worth a one-line doc note or a debug log on collision, since a future LibraryA/SPI.h + LibraryB/SPI.h could route the sketch to the wrong implementation without warning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-build/src/framework_libs.rs` around lines 38 - 47, The current
header_to_library population silently keeps the first library index on duplicate
header basenames, which can mask collisions; update discover_framework_libraries
to detect when header_to_library.entry(header).or_insert(idx) would skip an
existing entry and emit a debug/warn log (including the header basename, the
existing library name from libraries[existing_idx].name and the new library name
from libraries[idx].name) or add a one-line documentation comment about
first-library-wins behavior; locate the logic in header_to_library, use
collect_header_names as-is, and ensure the log clarifies that alphabetical
ordering determines precedence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/fbuild-packages/src/library/framework_library.rs`:
- Around line 60-78: The function library_include_dirs currently adds a
non-standard "include" directory to the returned include paths; remove the block
that constructs and pushes lib_dir.join("include") (the `include` variable and
its is_dir() check) from library_include_dirs so only the standard "src" (and
optional "utility") dirs are exposed, or if non-standard support is required
make it opt-in via a clearly named feature/flag; update any tests/comments that
assume the "include" entry accordingly.

---

Nitpick comments:
In `@crates/fbuild-build/src/framework_libs.rs`:
- Around line 86-91: The per-library selection log currently uses tracing::info!
inside framework_libs.rs (around where libraries[idx].name and
libraries[idx].source_files.len() are referenced and where
sources.extend(libraries[idx].source_files.iter().cloned()) is called); change
that tracing::info! call to tracing::debug! so these routine, detailed messages
are only emitted under debug logging (RUST_LOG=debug) to avoid noisy normal
build logs while keeping the same message and interpolation of
libraries[idx].name and libraries[idx].source_files.len().
- Around line 38-47: The current header_to_library population silently keeps the
first library index on duplicate header basenames, which can mask collisions;
update discover_framework_libraries to detect when
header_to_library.entry(header).or_insert(idx) would skip an existing entry and
emit a debug/warn log (including the header basename, the existing library name
from libraries[existing_idx].name and the new library name from
libraries[idx].name) or add a one-line documentation comment about
first-library-wins behavior; locate the logic in header_to_library, use
collect_header_names as-is, and ensure the log clarifies that alphabetical
ordering determines precedence.

In `@crates/fbuild-build/src/stm32/orchestrator.rs`:
- Around line 107-121: The code currently calls
framework.get_framework_libraries() to compute framework_libs and later
re-invokes framework.get_framework_library_include_dirs(), which causes a
duplicate directory walk; reuse the previously computed framework_libs when
computing include dirs instead of calling get_framework_library_include_dirs()
again: pass framework_libs into whatever helper or directly compute include dirs
from that vector and extend sources.include_dirs with the result, leaving
resolve_framework_library_sources(&framework_libs, ...) and extending
sources.core_sources unchanged; update references around framework_libs,
framework_library_sources, and sources.include_dirs to use the reused data.

In `@crates/fbuild-packages/src/library/framework_library.rs`:
- Around line 80-131: collect_library_sources_inner currently filters out
"example(s)", "test(s)" and "extras" but not "fontconvert", causing a mismatch
with should_scan_framework_entry; update collect_library_sources_inner (used by
collect_library_sources) to also skip directories named "fontconvert" (and its
lowercase variants) alongside the existing matches so library tool trees under
fontconvert/ aren't picked up as linkable sources.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3c38786b-24ea-4828-8e1f-a679dbb888bb

📥 Commits

Reviewing files that changed from the base of the PR and between a5ed36a and 6e9a86c.

📒 Files selected for processing (8)
  • crates/fbuild-build/src/framework_libs.rs
  • crates/fbuild-build/src/lib.rs
  • crates/fbuild-build/src/stm32/orchestrator.rs
  • crates/fbuild-build/src/teensy/orchestrator.rs
  • crates/fbuild-packages/src/library/framework_library.rs
  • crates/fbuild-packages/src/library/mod.rs
  • crates/fbuild-packages/src/library/stm32_core.rs
  • crates/fbuild-packages/src/library/teensy_core.rs

Comment on lines +60 to +78
pub fn library_include_dirs(lib_dir: &Path) -> Vec<PathBuf> {
let mut dirs = Vec::new();
let src = lib_dir.join("src");
if src.is_dir() {
dirs.push(src);
} else {
dirs.push(lib_dir.to_path_buf());
}

let utility = lib_dir.join("utility");
if utility.is_dir() {
dirs.push(utility);
}
let include = lib_dir.join("include");
if include.is_dir() {
dirs.push(include);
}
dirs
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Arduino library specification 1.5 layout src utility include directory

💡 Result:

Arduino library specification (1.5 format) layout rules for src / utility / include: 1) src/ is the root for compiling and for the library’s public headers - For “1.5.x+-only” libraries, all source code is placed under the src/ folder. - The builder compiles and links everything inside src/ and all its subfolders. - Only the src/ folder is added to the compiler’s include search path. 2) Which headers get auto-added for #include by the IDE - When a user imports the library via the IDE’s “Include Library”/web editor include action, the default behavior is to add an #include for each header (.h) that is located directly in src/ (but not in src subfolders). - Therefore, the headers at src/

.h form the de-facto public interface. Internal headers should live in a subfolder of src/. 3) utility/ directory is only part of the legacy (1.0/flat) layout, not the 1.5 recursive layout - For backward compatibility with Arduino IDE 1.0.x, a library author may use the older “flat”/legacy format where sources are placed in the library root and a utility/ folder. - In that case, the builder searches the library root and the utility/ folder for source. - You should not mix the 1.5 format layout (src/) with the legacy/utility layout. Summary mapping to your keywords - “1.5 layout src”: Use src/; recursive compilation and linking happen under src/. - “utility directory”: belongs to the legacy (1.0) format; for 1.5 format, put internal code under src/ subfolders instead. - “include directory”: Only src/ is added to include search paths for 1.5 format libraries; headers in root src/ are the ones the IDE auto-includes.

Citations:


include/ subdirectory is non-standard per Arduino library specification.

The Arduino 1.5+ library spec standardizes only src/ for the compiler's include search path. The include/ directory is not mentioned in the official specification (neither in 1.5 format nor in the legacy 1.0 flat layout), making it speculative and potentially prone to polluting the include path by exposing unintended headers from foreign libraries.

Consider dropping include/ unless you have a concrete library that requires it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-packages/src/library/framework_library.rs` around lines 60 -
78, The function library_include_dirs currently adds a non-standard "include"
directory to the returned include paths; remove the block that constructs and
pushes lib_dir.join("include") (the `include` variable and its is_dir() check)
from library_include_dirs so only the standard "src" (and optional "utility")
dirs are exposed, or if non-standard support is required make it opt-in via a
clearly named feature/flag; update any tests/comments that assume the "include"
entry accordingly.

@zackees zackees merged commit d088072 into main Apr 24, 2026
77 of 78 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stm32: Arduino_Core_STM32 framework libraries (SPI, Wire, ...) not discovered — fatal error: SPI.h: No such file or directory

1 participant