Skip to content

Commit fc2d61c

Browse files
zackeesclaude
andcommitted
fix(library-select): honor Arduino library spec for 1.0 flat layout; release v2.2.6
Closes #267 (Fix 2 — the scanner correctness fix). ## The bug `fbuild 2.2.5` fails to build any sketch for `teensy41`: fatal error: ft2build.h: No such file or directory 23 | #include <ft2build.h> compilation terminated. The offending file is `framework-arduinoteensy/libraries/ssd1351/fontconvert/fontconvert.c` — a *host-only* desktop tool that links `-lfreetype`. ssd1351 ships it as a font preprocessor; it was never intended to be ARM-cross-compiled. ## Root cause `collect_library_sources` in `framework_library.rs` walked the entire library directory recursively for 1.0 flat-layout libraries (no `src/`), violating the Arduino library specification. The spec says: - **1.5 recursive layout** (has `src/`): scan `src/**` only. - **1.0 flat layout** (no `src/`): scan root *non-recursively* plus the literal `utility/` subdirectory. Subdirectories like `fontconvert/`, `util/`, `Fonts/`, `examples/`, and `extras/` must be ignored. The ssd1351 author parked `fontconvert.c` in `fontconvert/` (not `utility/`) precisely so Arduino IDE / PlatformIO would skip it. fbuild's recursive walk swept it in. ## The fix Rewrite `collect_library_sources` to dispatch on layout: - 1.5 (has `src/`): recurse into `src/` only — already correct. - 1.0 (no `src/`): `collect_root_level_sources` (non-recursive) + recurse into `utility/` only. This matches `arduino-cli` exactly and protects fbuild from any well-formed library shipping companion tooling in a non-`utility/` subdirectory. ## Tests - `collect_library_sources_flat_layout_arduino_spec` — fixture mimics ssd1351 (root .cpp + utility/ + fontconvert/ + util/ + Fonts/ + examples/). Asserts only root + utility/ are returned; fontconvert/ (with the `#include <ft2build.h>` that breaks the build), util/, Fonts/, and examples/ are dropped. - `collect_library_sources_recursive_layout_arduino_spec` — fixture with `src/SPI.cpp`, `src/sub/bar.cpp`, root-level `baz.cpp`, and `examples/`. Asserts only `src/**` is returned; root `baz.cpp` and examples/ are dropped. Full workspace cargo check / clippy / fmt / test all green. ## Release v2.2.6 Patch release rolling up the #267 scanner-correctness fix. Both teensy41 regressions from earlier today (#261 LTO tmpdir, #263 bundled FastLED dup) carry over from v2.2.5. Cargo.toml + pyproject.toml bumped to 2.2.6. ## Deferred (out of scope for this PR) Fix 1 from #267 — investigate why ssd1351 is reaching the compile set at all when Blink doesn't `#include <ssd1351.h>` — is a separate LDF-leak investigation. With Fix 2 in place, even if ssd1351 IS selected, only `ssd1351.cpp` at the root would be compiled (which should cross-compile fine for ARM). The user-reported build failure is resolved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 48bf377 commit fc2d61c

4 files changed

Lines changed: 171 additions & 38 deletions

File tree

Cargo.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ members = [
1818
]
1919

2020
[workspace.package]
21-
version = "2.2.5"
21+
version = "2.2.6"
2222
edition = "2021"
2323
rust-version = "1.94.1"
2424
license = "MIT OR Apache-2.0"

crates/fbuild-packages/src/library/framework_library.rs

Lines changed: 155 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,26 +77,61 @@ pub fn library_include_dirs(lib_dir: &Path) -> Vec<PathBuf> {
7777
dirs
7878
}
7979

80-
/// Collect every buildable source file from a library.
80+
/// Collect every buildable source file from a library, honoring the
81+
/// [Arduino library specification](https://arduino.github.io/arduino-cli/1.5/library-specification/).
8182
///
82-
/// Skips `examples/`, `tests/`, and `extras/` subtrees to keep user-facing
83-
/// demos out of the build.
83+
/// - **1.5 recursive layout** (library has `src/`): scan `src/**` recursively.
84+
/// The library root is *not* scanned. This is the modern layout used by
85+
/// well-organized libraries.
86+
/// - **1.0 flat layout** (no `src/`): scan the library root *non-recursively*
87+
/// plus the literal `utility/` subdirectory recursively. Every other
88+
/// subdirectory (`fontconvert/`, `util/`, `Fonts/`, `examples/`, `extras/`,
89+
/// etc.) is ignored.
90+
///
91+
/// The flat-layout rule is what protects fbuild from compiling host-only
92+
/// tools that ship inside libraries — e.g. `ssd1351/fontconvert/fontconvert.c`
93+
/// (a libfreetype-linked desktop utility) under
94+
/// `framework-arduinoteensy/libraries/ssd1351/`. Arduino IDE and PlatformIO
95+
/// skip those because the spec says to scan only root + `utility/`; fbuild
96+
/// previously walked the full tree and tried to ARM-cross-compile the host
97+
/// tool, which fails at `#include <ft2build.h>`. See FastLED/fbuild#267.
8498
pub fn collect_library_sources(lib_dir: &Path) -> Vec<PathBuf> {
85-
let search_dir = {
86-
let src = lib_dir.join("src");
87-
if src.is_dir() {
88-
src
89-
} else {
90-
lib_dir.to_path_buf()
91-
}
92-
};
93-
9499
let mut sources = Vec::new();
95-
collect_library_sources_inner(&search_dir, &mut sources);
100+
let src = lib_dir.join("src");
101+
if src.is_dir() {
102+
// 1.5 layout — recursive scan of src/ only.
103+
collect_library_sources_inner(&src, &mut sources);
104+
} else {
105+
// 1.0 flat layout per Arduino spec:
106+
// * root level (non-recursive)
107+
// * utility/ (recursive, literal lowercase per the spec)
108+
collect_root_level_sources(lib_dir, &mut sources);
109+
let utility = lib_dir.join("utility");
110+
if utility.is_dir() {
111+
collect_library_sources_inner(&utility, &mut sources);
112+
}
113+
}
96114
sources.sort();
97115
sources
98116
}
99117

118+
/// Non-recursive scan of a directory for buildable source files.
119+
/// Used only for the root of a 1.0 flat-layout library.
120+
fn collect_root_level_sources(dir: &Path, out: &mut Vec<PathBuf>) {
121+
let Ok(entries) = std::fs::read_dir(dir) else {
122+
return;
123+
};
124+
for entry in entries.flatten() {
125+
let path = entry.path();
126+
if path.is_file() && is_buildable_source(&path) {
127+
out.push(path);
128+
}
129+
}
130+
}
131+
132+
/// Recursive scan used inside `src/` (1.5 layout) and `utility/` (1.0 layout).
133+
/// Still skips `examples/`, `tests/`, and `extras/` defensively in case a
134+
/// library nests them under `src/` or `utility/`.
100135
fn collect_library_sources_inner(dir: &Path, out: &mut Vec<PathBuf>) {
101136
let Ok(entries) = std::fs::read_dir(dir) else {
102137
return;
@@ -117,19 +152,21 @@ fn collect_library_sources_inner(dir: &Path, out: &mut Vec<PathBuf>) {
117152
continue;
118153
}
119154
collect_library_sources_inner(&path, out);
120-
} else {
121-
let ext = path
122-
.extension()
123-
.unwrap_or_default()
124-
.to_string_lossy()
125-
.to_lowercase();
126-
if matches!(ext.as_str(), "c" | "cpp" | "cc" | "cxx" | "s") {
127-
out.push(path);
128-
}
155+
} else if is_buildable_source(&path) {
156+
out.push(path);
129157
}
130158
}
131159
}
132160

161+
fn is_buildable_source(path: &Path) -> bool {
162+
let ext = path
163+
.extension()
164+
.unwrap_or_default()
165+
.to_string_lossy()
166+
.to_lowercase();
167+
matches!(ext.as_str(), "c" | "cpp" | "cc" | "cxx" | "s")
168+
}
169+
133170
#[cfg(test)]
134171
mod tests {
135172
use super::*;
@@ -164,6 +201,102 @@ mod tests {
164201
assert_eq!(sources, vec![tmp.path().join("OctoWS2811.cpp")]);
165202
}
166203

204+
/// Regression for FastLED/fbuild#267 — 1.0 flat-layout libraries
205+
/// (no `src/`) must scan only the library root non-recursively plus
206+
/// the literal `utility/` subdirectory. Subdirectories like
207+
/// `fontconvert/`, `util/`, `Fonts/`, and `examples/` are ignored
208+
/// per the Arduino library spec.
209+
///
210+
/// The specific failure this guards: `ssd1351/fontconvert/fontconvert.c`
211+
/// is a desktop host tool linking `-lfreetype`; ARM-cross-compiling it
212+
/// fails on `#include <ft2build.h>`. Arduino IDE / PlatformIO skip
213+
/// `fontconvert/` because the spec says scan root + `utility/` only.
214+
#[test]
215+
fn collect_library_sources_flat_layout_arduino_spec() {
216+
let tmp = tempfile::TempDir::new().unwrap();
217+
let lib = tmp.path().join("ssd1351");
218+
std::fs::create_dir_all(&lib).unwrap();
219+
220+
// Root level — compiled.
221+
std::fs::write(lib.join("ssd1351.cpp"), "").unwrap();
222+
std::fs::write(lib.join("ssd1351.h"), "").unwrap(); // header, not source
223+
224+
// `utility/` — compiled (recursive).
225+
std::fs::create_dir_all(lib.join("utility")).unwrap();
226+
std::fs::write(lib.join("utility").join("helpers.cpp"), "").unwrap();
227+
std::fs::create_dir_all(lib.join("utility").join("nested")).unwrap();
228+
std::fs::write(lib.join("utility").join("nested").join("deep.cpp"), "").unwrap();
229+
230+
// Non-standard subdirs — ALL must be skipped.
231+
std::fs::create_dir_all(lib.join("fontconvert")).unwrap();
232+
std::fs::write(
233+
lib.join("fontconvert").join("fontconvert.c"),
234+
"#include <ft2build.h>\n", // would fail to ARM-cross-compile
235+
)
236+
.unwrap();
237+
std::fs::create_dir_all(lib.join("util")).unwrap();
238+
std::fs::write(lib.join("util").join("misc.cpp"), "").unwrap();
239+
std::fs::create_dir_all(lib.join("Fonts")).unwrap();
240+
std::fs::write(lib.join("Fonts").join("Roboto.c"), "").unwrap();
241+
std::fs::create_dir_all(lib.join("examples").join("Demo")).unwrap();
242+
std::fs::write(lib.join("examples").join("Demo").join("Demo.ino"), "").unwrap();
243+
244+
let sources = collect_library_sources(&lib);
245+
246+
let mut expected = vec![
247+
lib.join("ssd1351.cpp"),
248+
lib.join("utility").join("helpers.cpp"),
249+
lib.join("utility").join("nested").join("deep.cpp"),
250+
];
251+
expected.sort();
252+
253+
assert_eq!(
254+
sources, expected,
255+
"1.0 flat layout must yield ONLY root non-recursive + utility/ \
256+
recursive per Arduino spec — see #267. \
257+
Got {sources:?}, expected {expected:?}"
258+
);
259+
}
260+
261+
/// Regression for FastLED/fbuild#267 — 1.5 recursive layout (library
262+
/// has `src/`) must scan only `src/**`. Any root-level source files
263+
/// are intentionally ignored per the Arduino library spec; the
264+
/// library's `library.properties` declares it as 1.5 by virtue of
265+
/// shipping `src/`.
266+
#[test]
267+
fn collect_library_sources_recursive_layout_arduino_spec() {
268+
let tmp = tempfile::TempDir::new().unwrap();
269+
let lib = tmp.path().join("SPI");
270+
std::fs::create_dir_all(lib.join("src")).unwrap();
271+
std::fs::create_dir_all(lib.join("src").join("sub")).unwrap();
272+
273+
// src/ — recursive scan.
274+
std::fs::write(lib.join("src").join("SPI.cpp"), "").unwrap();
275+
std::fs::write(lib.join("src").join("sub").join("bar.cpp"), "").unwrap();
276+
277+
// Root-level — must NOT be compiled (1.5 layout ignores root).
278+
std::fs::write(lib.join("baz.cpp"), "").unwrap();
279+
280+
// examples/, tests/ — always skipped.
281+
std::fs::create_dir_all(lib.join("examples")).unwrap();
282+
std::fs::write(lib.join("examples").join("Demo.cpp"), "").unwrap();
283+
284+
let sources = collect_library_sources(&lib);
285+
286+
let mut expected = vec![
287+
lib.join("src").join("SPI.cpp"),
288+
lib.join("src").join("sub").join("bar.cpp"),
289+
];
290+
expected.sort();
291+
292+
assert_eq!(
293+
sources, expected,
294+
"1.5 recursive layout must yield ONLY src/** per Arduino spec — \
295+
root-level `baz.cpp` must be ignored — see #267. \
296+
Got {sources:?}, expected {expected:?}"
297+
);
298+
}
299+
167300
#[test]
168301
fn discover_framework_libraries_walks_each_subdirectory() {
169302
let tmp = tempfile::TempDir::new().unwrap();

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "fbuild"
3-
version = "2.2.5"
3+
version = "2.2.6"
44
description = "PlatformIO-compatible embedded build tool (Rust implementation)"
55
readme = "README.md"
66
requires-python = ">=3.10"

0 commit comments

Comments
 (0)