From 2dc2a37312ac68fdcd74002ce57a2c5564ce4e9c Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 19 Jun 2026 12:37:15 -0700 Subject: [PATCH] feat: conditionally inject Arduino.h for ino preprocessing Closes #646 --- crates/fbuild-build/src/source_scanner.rs | 104 ++++++++++++++++-- .../src/stm32/orchestrator/arduino_mbed.rs | 5 +- 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/crates/fbuild-build/src/source_scanner.rs b/crates/fbuild-build/src/source_scanner.rs index 5ea73ce6..a45f84b0 100644 --- a/crates/fbuild-build/src/source_scanner.rs +++ b/crates/fbuild-build/src/source_scanner.rs @@ -1,7 +1,8 @@ //! Source file scanning and .ino preprocessing. //! //! Finds .cpp, .c, .S, .ino files in project source directories. -//! Preprocesses .ino files into valid .cpp with Arduino.h include and function prototypes. +//! Preprocesses .ino files into valid .cpp with function prototypes and an +//! Arduino.h include when the active include roots provide that header. use regex::Regex; use std::collections::HashSet; @@ -92,6 +93,15 @@ impl SourceScanner { pub fn scan_sketch_sources_filtered( &self, filter_spec: Option<&str>, + ) -> fbuild_core::Result> { + self.scan_sketch_sources_filtered_with_include_roots(filter_spec, &[]) + } + + /// Scan sketch sources with known include roots for conditional .ino preprocessing. + pub fn scan_sketch_sources_filtered_with_include_roots( + &self, + filter_spec: Option<&str>, + include_roots: &[&Path], ) -> fbuild_core::Result> { if !self.src_dir.exists() { return Ok(Vec::new()); @@ -128,7 +138,8 @@ impl SourceScanner { // the .ino content is already compiled via #include in main.cpp. if !ino_files.is_empty() && !has_main_cpp { ino_files.sort(); - let preprocessed = self.preprocess_ino_files(&ino_files)?; + let preprocessed = + self.preprocess_ino_files(&ino_files, arduino_header_available(include_roots))?; sources.insert(0, preprocessed); } @@ -205,7 +216,9 @@ impl SourceScanner { variant_dir: Option<&Path>, filter_spec: Option<&str>, ) -> fbuild_core::Result { - let sketch_sources = self.scan_sketch_sources_filtered(filter_spec)?; + let include_roots: Vec<&Path> = [core_dir, variant_dir].into_iter().flatten().collect(); + let sketch_sources = + self.scan_sketch_sources_filtered_with_include_roots(filter_spec, &include_roots)?; let core_sources = core_dir .map(|d| self.scan_core_sources(d)) .unwrap_or_default(); @@ -232,11 +245,15 @@ impl SourceScanner { /// Preprocess .ino files into a single .cpp file. /// /// 1. Concatenate .ino files (alphabetically sorted) - /// 2. Add `#include ` at top + /// 2. Add `#include ` at top when available /// 3. Extract function prototypes /// 4. Add prototypes before first function definition /// 5. Add `#line` directives for debugging - fn preprocess_ino_files(&self, ino_files: &[PathBuf]) -> fbuild_core::Result { + fn preprocess_ino_files( + &self, + ino_files: &[PathBuf], + include_arduino_h: bool, + ) -> fbuild_core::Result { let mut combined = String::new(); let mut line_offsets: Vec<(usize, &Path)> = Vec::new(); let mut current_line = 1; @@ -263,8 +280,9 @@ impl SourceScanner { // Build output let mut output = String::new(); - // Arduino.h include - output.push_str("#include \n"); + if include_arduino_h { + output.push_str("#include \n"); + } // Function prototypes if !prototypes.is_empty() { @@ -374,6 +392,12 @@ impl SourceFilter { } } +fn arduino_header_available(include_roots: &[&Path]) -> bool { + include_roots + .iter() + .any(|root| root.join("Arduino.h").is_file()) +} + fn compile_source_filter_pattern(pattern: &str) -> fbuild_core::Result { let normalized = pattern.replace('\\', "/"); let regex_body = if normalized == "*" { @@ -601,9 +625,9 @@ mod tests { assert_eq!(sources.len(), 1); assert!(sources[0].to_string_lossy().contains(".ino.cpp")); - // Check preprocessed content + // Direct sketch scans do not know framework include roots. let content = fs::read_to_string(&sources[0]).unwrap(); - assert!(content.contains("#include ")); + assert!(!content.contains("#include ")); } #[test] @@ -688,11 +712,36 @@ mod tests { let sources = scanner.scan_sketch_sources().unwrap(); let content = fs::read_to_string(&sources[0]).unwrap(); - assert!(content.contains("#include ")); + assert!(!content.contains("#include ")); assert!(content.contains("void setup()")); assert!(content.contains("void loop()")); } + #[test] + fn test_preprocess_includes_arduino_h_when_header_available() { + let tmp = TempDir::new().unwrap(); + let src_dir = tmp.path().join("src"); + let build_dir = tmp.path().join("build"); + let core_dir = tmp.path().join("core"); + fs::create_dir_all(&src_dir).unwrap(); + fs::create_dir_all(&core_dir).unwrap(); + fs::write( + src_dir.join("sketch.ino"), + "void setup() {}\nvoid loop() {}\n", + ) + .unwrap(); + fs::write(core_dir.join("Arduino.h"), "#pragma once\n").unwrap(); + + let scanner = SourceScanner::new(&src_dir, &build_dir); + let sources = scanner + .scan_all(Some(&core_dir), None) + .unwrap() + .sketch_sources; + let content = fs::read_to_string(&sources[0]).unwrap(); + + assert!(content.contains("#include ")); + } + #[test] fn test_preprocess_with_custom_functions() { let (_tmp, src_dir, build_dir) = setup_project(&[( @@ -754,6 +803,41 @@ mod tests { assert_eq!(first_mtime, second_mtime); } + #[test] + fn test_preprocess_with_arduino_h_does_not_rewrite_unchanged_output() { + let tmp = TempDir::new().unwrap(); + let src_dir = tmp.path().join("src"); + let build_dir = tmp.path().join("build"); + let core_dir = tmp.path().join("core"); + fs::create_dir_all(&src_dir).unwrap(); + fs::create_dir_all(&core_dir).unwrap(); + fs::write( + src_dir.join("sketch.ino"), + "void setup() {}\nvoid loop() {}\n", + ) + .unwrap(); + fs::write(core_dir.join("Arduino.h"), "#pragma once\n").unwrap(); + let scanner = SourceScanner::new(&src_dir, &build_dir); + + let first = scanner + .scan_all(Some(&core_dir), None) + .unwrap() + .sketch_sources; + let output = first[0].clone(); + let first_mtime = fs::metadata(&output).unwrap().modified().unwrap(); + + std::thread::sleep(std::time::Duration::from_millis(20)); + + let second = scanner + .scan_all(Some(&core_dir), None) + .unwrap() + .sketch_sources; + assert_eq!(second[0], output); + let second_mtime = fs::metadata(&output).unwrap().modified().unwrap(); + + assert_eq!(first_mtime, second_mtime); + } + #[test] fn test_source_collection_all_sources() { let tmp = TempDir::new().unwrap(); diff --git a/crates/fbuild-build/src/stm32/orchestrator/arduino_mbed.rs b/crates/fbuild-build/src/stm32/orchestrator/arduino_mbed.rs index fd027d58..bd047769 100644 --- a/crates/fbuild-build/src/stm32/orchestrator/arduino_mbed.rs +++ b/crates/fbuild-build/src/stm32/orchestrator/arduino_mbed.rs @@ -47,7 +47,10 @@ pub(super) fn build_arduino_mbed_stm32( let scanner = SourceScanner::new(&ctx.src_dir, &ctx.src_build_dir); let sources = SourceCollection { - sketch_sources: scanner.scan_sketch_sources_filtered(ctx.source_filter.as_deref())?, + sketch_sources: scanner.scan_sketch_sources_filtered_with_include_roots( + ctx.source_filter.as_deref(), + &[core_dir.as_path(), variant_dir.as_path()], + )?, core_sources: framework.get_core_sources(), variant_sources: framework.get_variant_sources(&ctx.board.variant), headers: Vec::new(),