From c1b4f2b400aec1188b3ff930953cc8732346fd19 Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 19 Jun 2026 13:22:50 -0700 Subject: [PATCH] fix: stabilize generated ino cpp output Closes #649 --- crates/fbuild-build/src/source_scanner.rs | 71 +++++++++++++++++++++- docs/reference/platformio-compatibility.md | 14 +++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/crates/fbuild-build/src/source_scanner.rs b/crates/fbuild-build/src/source_scanner.rs index c3ecf460..76b7613a 100644 --- a/crates/fbuild-build/src/source_scanner.rs +++ b/crates/fbuild-build/src/source_scanner.rs @@ -261,7 +261,7 @@ impl SourceScanner { let mut current_line = 1; for ino in ino_files { - let content = std::fs::read_to_string(ino)?; + let content = normalize_generated_source_line_endings(&std::fs::read_to_string(ino)?); line_offsets.push((current_line, ino.as_path())); current_line += content.lines().count(); if !combined.is_empty() { @@ -300,7 +300,7 @@ impl SourceScanner { if let Some((_, first_file)) = line_offsets.first() { output.push_str(&format!( "#line 1 \"{}\"\n", - first_file.display().to_string().replace('\\', "/") + self.line_directive_path(first_file) )); } @@ -319,6 +319,12 @@ impl SourceScanner { Ok(output_path) } + + fn line_directive_path(&self, path: &Path) -> String { + let project_root = self.src_dir.parent().unwrap_or(&self.src_dir); + let display_path = path.strip_prefix(project_root).unwrap_or(path); + normalize_generated_source_path(display_path) + } } impl SourceFilter { @@ -400,6 +406,28 @@ fn arduino_header_available(include_roots: &[&Path]) -> bool { .any(|root| root.join("Arduino.h").is_file()) } +fn normalize_generated_source_path(path: &Path) -> String { + normalize_generated_source_path_text(&path.display().to_string()) +} + +fn normalize_generated_source_path_text(path: &str) -> String { + let mut normalized = path.replace('\\', "/"); + let bytes = normalized.as_bytes(); + if bytes.len() >= 3 + && bytes[1] == b':' + && bytes[2] == b'/' + && bytes[0].is_ascii_alphabetic() + && bytes[0].is_ascii_uppercase() + { + normalized.replace_range(0..1, &normalized[0..1].to_ascii_lowercase()); + } + normalized +} + +fn normalize_generated_source_line_endings(source: &str) -> String { + source.replace("\r\n", "\n").replace('\r', "\n") +} + fn order_ino_files(src_dir: &Path, mut ino_files: Vec) -> Vec { ino_files.sort_by(|a, b| compare_ino_paths(a, b)); @@ -1194,6 +1222,45 @@ void Controller::external_tick() {} assert!(content.contains("#line 1")); } + #[test] + fn test_line_directive_path_is_project_relative_and_slash_normalized() { + let (_tmp, src_dir, build_dir) = + setup_project(&[("sketch.ino", "void setup() {}\nvoid loop() {}\n")]); + let scanner = SourceScanner::new(&src_dir, &build_dir); + let sources = scanner.scan_sketch_sources().unwrap(); + let content = fs::read_to_string(&sources[0]).unwrap(); + + assert!(content.contains("#line 1 \"src/sketch.ino\"")); + assert!(!content.contains('\\')); + } + + #[test] + fn test_generated_ino_cpp_uses_lf_line_endings() { + let (_tmp, src_dir, build_dir) = + setup_project(&[("sketch.ino", "void setup() {}\r\nvoid loop() {}\r\n")]); + let scanner = SourceScanner::new(&src_dir, &build_dir); + let sources = scanner.scan_sketch_sources().unwrap(); + let content = fs::read_to_string(&sources[0]).unwrap(); + + assert!(!content.contains("\r\n")); + } + + #[test] + fn test_windows_style_generated_path_text_is_stable() { + assert_eq!( + normalize_generated_source_path_text(r"C:\Users\dev\project\src\main.ino"), + "c:/Users/dev/project/src/main.ino" + ); + assert_eq!( + normalize_generated_source_path_text(r"C:\Users\dev\project/src\main.ino"), + "c:/Users/dev/project/src/main.ino" + ); + assert_eq!( + normalize_generated_source_path_text("src\\main.ino"), + "src/main.ino" + ); + } + #[test] fn test_preprocess_does_not_rewrite_unchanged_output() { let (_tmp, src_dir, build_dir) = diff --git a/docs/reference/platformio-compatibility.md b/docs/reference/platformio-compatibility.md index 1760ef26..f6b91bc6 100644 --- a/docs/reference/platformio-compatibility.md +++ b/docs/reference/platformio-compatibility.md @@ -105,3 +105,17 @@ namespace-local definitions. It also handles common C++ signature syntax such as templates, attributes, references, and default arguments. No clang or libclang runtime install is required for this preprocessing path. + +## Generated `.ino.cpp` Stability + +fbuild writes generated `.ino.cpp` files with LF line endings and avoids +rewriting the file when the generated bytes are unchanged. This preserves the +file mtime for unchanged sketches so compiler caches do not see a fresh input +only because preprocessing ran again. + +Generated `#line` paths are normalized before writing: + +- Paths under the project root are emitted project-relative, for example + `src/main.ino`. +- Path separators are emitted as `/` on all hosts, including Windows. +- Windows drive letters are lowercased when an absolute fallback path is needed.