diff --git a/README.md b/README.md index 58022c9e..15501b5c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ [![Documentation](https://github.com/fastled/fbuild/actions/workflows/docs.yml/badge.svg)](https://github.com/fastled/fbuild/actions/workflows/docs.yml) [![Validate Boards](https://github.com/fastled/fbuild/actions/workflows/validate-boards.yml/badge.svg)](https://github.com/fastled/fbuild/actions/workflows/validate-boards.yml) [![Build Native Binaries](https://github.com/fastled/fbuild/actions/workflows/build.yml/badge.svg)](https://github.com/fastled/fbuild/actions/workflows/build.yml) +[![Library Selection Acceptance (#205)](https://github.com/fastled/fbuild/actions/workflows/acceptance-205.yml/badge.svg)](https://github.com/fastled/fbuild/actions/workflows/acceptance-205.yml) +[![Library Selection Perf (#205)](https://github.com/fastled/fbuild/actions/workflows/bench-205.yml/badge.svg)](https://github.com/fastled/fbuild/actions/workflows/bench-205.yml)
Per-platform board build badges (click to expand) diff --git a/crates/fbuild-build/tests/teensylc_acceptance.rs b/crates/fbuild-build/tests/teensylc_acceptance.rs index 3c5bd6d7..50f6a7fa 100644 --- a/crates/fbuild-build/tests/teensylc_acceptance.rs +++ b/crates/fbuild-build/tests/teensylc_acceptance.rs @@ -30,7 +30,10 @@ fn teensylc_blink_meets_205_acceptance_criteria() { let params = BuildParams { project_dir: project_dir.clone(), - env_name: "teensyLC".to_string(), + // WHY: env names are case-sensitive and must match the + // [env:teensylc] key in tests/platform/teensylc/platformio.ini. + // Same root-cause family as #220 / #221 in measure_baseline_205.py. + env_name: "teensylc".to_string(), clean: true, profile: BuildProfile::Release, build_dir: build_dir.path().to_path_buf(), @@ -71,15 +74,29 @@ fn teensylc_blink_meets_205_acceptance_criteria() { #204 regression" ); } - for required in ["setup", "loop"] { + // WHY: setup/loop are extern "C" via Arduino.h's prototype, so + // ideally appear unmangled. But Teensyduino's main calls them via + // the framework's main.cpp and toolchain LTO can leave only the + // mangled C++ symbols (`_Z5setupv` / `_Z4loopv`) when the .ino is + // compiled as C++ without the extern "C" prototype reaching the + // definition. Accept either form — the contract is "the user's + // setup/loop landed in the firmware", not "they kept their C + // linkage". The earlier `has_symbol_containing` was rejected in + // PR #209 review for matching `Stream::setupXxx`-style false + // positives; the explicit-mangled fallback below is targeted and + // doesn't share that problem. + for (required, mangled) in [("setup", "_Z5setupv"), ("loop", "_Z4loopv")] { + let unmangled_present = probe.has_symbol(required).expect("symbol query"); + let mangled_present = probe.has_symbol(mangled).expect("symbol query"); assert!( - probe.has_symbol(required).expect("symbol query"), - "A-11: required symbol '{required}' missing from ELF" + unmangled_present || mangled_present, + "A-11: required symbol '{required}' missing from ELF \ + (also looked for mangled '{mangled}')" ); } // ── compile_commands.json probes (AC#1, A-20..A-22) ───────────────── - let compdb_path = locate_compile_commands(build_dir.path(), "teensyLC") + let compdb_path = locate_compile_commands(build_dir.path(), "teensylc") .expect("compile_commands.json should land in build dir"); let db = CompileDb::from_path(&compdb_path).expect("parse compile_commands.json"); assert!( diff --git a/crates/fbuild-library-select/src/lib.rs b/crates/fbuild-library-select/src/lib.rs index 8e049558..ddd4d53a 100644 --- a/crates/fbuild-library-select/src/lib.rs +++ b/crates/fbuild-library-select/src/lib.rs @@ -248,6 +248,48 @@ mod tests { ); } + #[test] + fn r04_pass2_reconciliation_catches_cpp_only_dependency() { + // The whole reason the LDF resolver is 2-pass instead of single-pass + // BFS: a lib's `.cpp` may pull in a second lib that the first lib's + // `.h` does NOT mention. Pass 1 (BFS from project seeds + reached + // headers) cannot see that edge; pass 2 re-seeds with each selected + // lib's full source set and catches it. + // + // Setup: project includes . SPI.h is silent. SPI.cpp includes + // . Wire is only reachable through SPI.cpp. + // + // Expected: pass 1 selects {SPI}; pass 2 (with SPI.cpp as a seed) + // selects {SPI, Wire}. A regression that drops the second pass would + // produce {SPI} only and silently miss Wire at link time. + let tmp = TempDir::new().unwrap(); + let project_src = tmp.path().join("project").join("src"); + write(&project_src.join("main.cpp"), "#include \n"); + + let mut spi = lib(tmp.path(), "SPI"); + write( + &spi.include_dirs[0].join("SPI.h"), + "// no transitive includes\n", + ); + let spi_cpp = spi.include_dirs[0].join("SPI.cpp"); + write(&spi_cpp, "#include \n"); + spi.source_files.push(spi_cpp); + + let mut wire = lib(tmp.path(), "Wire"); + write(&wire.include_dirs[0].join("Wire.h"), ""); + let wire_cpp = wire.include_dirs[0].join("Wire.cpp"); + write(&wire_cpp, ""); + wire.source_files.push(wire_cpp); + + let seeds = vec![project_src.join("main.cpp")]; + let sel = resolve(&seeds, &[project_src], &[spi, wire]); + assert_eq!( + sel.required_libraries, + vec!["SPI".to_string(), "Wire".to_string()], + "pass 2 reconciliation must catch Wire reached only via SPI.cpp" + ); + } + #[test] fn r03_no_includes_selects_nothing() { let tmp = TempDir::new().unwrap();