diff --git a/crates/fbuild-build/src/teensy/orchestrator.rs b/crates/fbuild-build/src/teensy/orchestrator.rs index 28689614..92dc8f0e 100644 --- a/crates/fbuild-build/src/teensy/orchestrator.rs +++ b/crates/fbuild-build/src/teensy/orchestrator.rs @@ -117,6 +117,11 @@ fn resolve_teensy_framework_library_sources_from_libraries( } } + let mut local_headers = HashSet::new(); + for root in roots { + collect_header_names(root, &mut local_headers); + } + let mut pending = HashSet::new(); for root in roots { collect_included_headers(root, &mut pending); @@ -125,6 +130,9 @@ fn resolve_teensy_framework_library_sources_from_libraries( let mut selected = HashSet::new(); let mut queue: Vec = pending.iter().cloned().collect(); while let Some(header) = queue.pop() { + if local_headers.contains(&header) { + continue; + } let Some(&library_idx) = header_to_library.get(&header) else { continue; }; @@ -737,4 +745,55 @@ mod tests { vec![wrapper_dir.join("NeedsSpi.cpp"), spi_dir.join("SPI.cpp")] ); } + + #[test] + fn test_resolve_teensy_framework_libraries_prefers_local_fastled_over_framework() { + let tmp = tempfile::TempDir::new().unwrap(); + let project_src = tmp.path().join("project").join("src"); + let project_lib = tmp + .path() + .join("project") + .join("lib") + .join("FastLED") + .join("src"); + std::fs::create_dir_all(&project_src).unwrap(); + std::fs::create_dir_all(&project_lib).unwrap(); + std::fs::write(project_src.join("main.cpp"), "#include \n").unwrap(); + std::fs::write(project_lib.join("FastLED.h"), "#include \n").unwrap(); + std::fs::write(project_lib.join("FastLED.cpp"), "").unwrap(); + + let framework_fastled_dir = tmp + .path() + .join("framework") + .join("libraries") + .join("FastLED"); + std::fs::create_dir_all(&framework_fastled_dir).unwrap(); + std::fs::write(framework_fastled_dir.join("FastLED.h"), "").unwrap(); + std::fs::write(framework_fastled_dir.join("FastLED.cpp"), "").unwrap(); + + let spi_dir = tmp.path().join("framework").join("libraries").join("SPI"); + std::fs::create_dir_all(&spi_dir).unwrap(); + std::fs::write(spi_dir.join("SPI.h"), "").unwrap(); + std::fs::write(spi_dir.join("SPI.cpp"), "").unwrap(); + + let libraries = vec![ + TeensyFrameworkLibrary { + name: "FastLED".to_string(), + dir: framework_fastled_dir.clone(), + include_dirs: vec![framework_fastled_dir.clone()], + source_files: vec![framework_fastled_dir.join("FastLED.cpp")], + }, + TeensyFrameworkLibrary { + name: "SPI".to_string(), + dir: spi_dir.clone(), + include_dirs: vec![spi_dir.clone()], + source_files: vec![spi_dir.join("SPI.cpp")], + }, + ]; + + let roots = vec![project_src, project_lib]; + let sources = resolve_teensy_framework_library_sources_from_libraries(&libraries, &roots); + + assert_eq!(sources, vec![spi_dir.join("SPI.cpp")]); + } } diff --git a/crates/fbuild-build/tests/teensy_build.rs b/crates/fbuild-build/tests/teensy_build.rs index 51766176..a22ab43e 100644 --- a/crates/fbuild-build/tests/teensy_build.rs +++ b/crates/fbuild-build/tests/teensy_build.rs @@ -6,11 +6,25 @@ //! Run with: `uv run soldr cargo test -p fbuild-build --test teensy_build -- --ignored` use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use fbuild_build::{BuildOrchestrator, BuildParams}; use fbuild_core::BuildProfile; +fn copy_dir_recursive(src: &Path, dst: &Path) { + fs::create_dir_all(dst).unwrap(); + for entry in fs::read_dir(src).unwrap() { + let entry = entry.unwrap(); + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + if src_path.is_dir() { + copy_dir_recursive(&src_path, &dst_path); + } else { + fs::copy(&src_path, &dst_path).unwrap(); + } + } +} + /// Build a self-contained Teensy 4.1 blink sketch. /// /// This test requires Internet access (first run only, then cached). @@ -212,3 +226,128 @@ fn build_teensy41_fixture() { eprintln!("Build succeeded in {:.1}s", result.build_time_secs); } + +/// Build a Teensy 3.0 fixture where a project-local lib/FastLED shadows the bundled framework. +#[test] +#[ignore] +fn build_teensy30_fixture_prefers_local_fastled() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let fixture_dir = manifest_dir + .parent() + .unwrap() + .parent() + .unwrap() + .join("tests/platform/teensy30"); + + if !fixture_dir.exists() { + eprintln!("SKIP: {} does not exist", fixture_dir.display()); + return; + } + + let tmp = tempfile::TempDir::new().unwrap(); + let project_dir = tmp.path().join("project"); + copy_dir_recursive(&fixture_dir, &project_dir); + + fs::create_dir_all(project_dir.join("lib/FastLED/src")).unwrap(); + fs::write( + project_dir.join("lib/FastLED/src/FastLED.h"), + "\ +#pragma once +#include + +namespace fastled_fixture { +void begin(); +} +", + ) + .unwrap(); + fs::write( + project_dir.join("lib/FastLED/src/FastLED.cpp"), + "\ +#include + +namespace fastled_fixture { +void begin() { + pinMode(LED_BUILTIN, OUTPUT); +} +} +", + ) + .unwrap(); + fs::write( + project_dir.join("src/main.ino"), + "\ +#include + +void setup() { + fastled_fixture::begin(); +} + +void loop() { + digitalWrite(LED_BUILTIN, HIGH); + delay(500); + digitalWrite(LED_BUILTIN, LOW); + delay(500); +} +", + ) + .unwrap(); + + let params = BuildParams { + project_dir: project_dir.clone(), + env_name: "teensy30".to_string(), + clean: true, + profile: BuildProfile::Release, + build_dir: tmp.path().join(".fbuild/build"), + verbose: true, + jobs: None, + generate_compiledb: false, + compiledb_only: false, + log_sender: None, + symbol_analysis: false, + symbol_analysis_path: None, + no_timestamp: false, + src_dir: None, + pio_env: Default::default(), + extra_build_flags: Vec::new(), + watch_set_cache: None, + }; + + let orchestrator = fbuild_build::teensy::orchestrator::TeensyOrchestrator; + let result = orchestrator + .build(¶ms) + .expect("Teensy 3.0 local FastLED shadow build should succeed"); + + assert!(result.success); + let firmware_path = result.firmware_path.expect("should produce hex"); + assert!(firmware_path.exists()); + let build_dir = result + .elf_path + .as_ref() + .and_then(|path| path.parent()) + .expect("elf path should live in the build output directory") + .to_path_buf(); + + let local_fastled_objects: Vec<_> = fs::read_dir(build_dir.join("lib").join("FastLED")) + .unwrap() + .filter_map(|entry| entry.ok()) + .map(|entry| entry.file_name().to_string_lossy().to_string()) + .filter(|name| name.starts_with("FastLED_") && name.ends_with(".cpp.o")) + .collect(); + assert!( + !local_fastled_objects.is_empty(), + "expected local lib/FastLED to compile" + ); + + let framework_fastled_objects: Vec<_> = fs::read_dir(build_dir.join("core")) + .unwrap() + .filter_map(|entry| entry.ok()) + .map(|entry| entry.file_name().to_string_lossy().to_string()) + .filter(|name| name.starts_with("FastLED_") && name.ends_with(".cpp.o")) + .collect(); + assert!( + framework_fastled_objects.is_empty(), + "bundled Teensy framework FastLED should be shadowed by lib/FastLED, found {:?}", + framework_fastled_objects + ); +}