Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions crates/fbuild-build/src/source_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
));
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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<PathBuf>) -> Vec<PathBuf> {
ino_files.sort_by(|a, b| compare_ino_paths(a, b));

Expand Down Expand Up @@ -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) =
Expand Down
14 changes: 14 additions & 0 deletions docs/reference/platformio-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading