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
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,17 @@ pub(super) async fn build_arduino_mbed_stm32(
let variant_dir = framework.get_variant_dir(&ctx.board.variant);

let scanner = SourceScanner::new(&ctx.src_dir, &ctx.src_build_dir);
let sources = SourceCollection {
sketch_sources: scanner.scan_sketch_sources_filtered_with_include_roots(
let (sketch_sources, ino_preludes) = scanner
.scan_sketch_sources_filtered_with_include_roots_and_preludes(
ctx.source_filter.as_deref(),
&[core_dir.as_path(), variant_dir.as_path()],
)?,
)?;
let sources = SourceCollection {
sketch_sources,
core_sources: framework.get_core_sources(),
variant_sources: framework.get_variant_sources(&ctx.board.variant),
headers: Vec::new(),
ino_preludes,
};

tracing::info!(
Expand Down
143 changes: 137 additions & 6 deletions crates/fbuild-build-engine/src/compile_database/clang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,115 @@ pub fn translate_flags_for_clang(args: &[String], arch: TargetArchitecture) -> V
result
}

/// Deterministic, deduplicated `-isystem <dir>` args for the GCC toolchain's
/// builtin include directories (`stdbool.h`, `stddef.h`, `stdarg.h`, etc. —
/// implicit GCC search paths that never appear in `compile_commands.json`
/// because GCC adds them automatically).
///
/// clangd has no such implicit search path, so without these baked in as
/// `-isystem` it can't find those headers. Previously the only fix was a
/// `--query-driver` clangd argument asking clangd to *run* the real compiler
/// and ask it — but `translate_for_clang` (this same function) already
/// rewrites `arguments[0]` to bare `clang`/`clang++`, which made
/// `--query-driver` resolve to nothing useful (FastLED/fbuild#1076 Phase 0).
/// Baking the dirs in directly is robust on every platform and doesn't
/// require clangd to shell out to anything.
///
/// Empty (no-op) when no toolchain is cached yet — never fails a build.
fn builtin_isystem_args() -> Vec<String> {
isystem_args_from_dirs(fbuild_packages::toolchain::clang::find_gcc_builtin_include_dirs())
}

/// Sort + dedup a list of include dirs and flatten it into `-isystem <dir>`
/// pairs. Pulled out of [`builtin_isystem_args`] so the sort/dedup/flatten
/// logic is unit-testable without depending on (or mutating) the real
/// toolchain cache directory.
pub(super) fn isystem_args_from_dirs(mut dirs: Vec<PathBuf>) -> Vec<String> {
dirs.sort();
dirs.dedup();

let mut args = Vec::with_capacity(dirs.len() * 2);
for dir in dirs {
args.push("-isystem".to_string());
args.push(dir.to_string_lossy().to_string());
}
args
}

impl CompileDatabase {
/// Create a new compile database with GCC flags translated to clang equivalents.
/// Create a new compile database with GCC flags translated to clang
/// equivalents, with the toolchain's GCC builtin include dirs baked in
/// as `-isystem` (see [`builtin_isystem_args`]).
pub fn translate_for_clang(&self, arch: TargetArchitecture) -> CompileDatabase {
let builtin_includes = builtin_isystem_args();
let entries = self
.entries
.iter()
.map(|entry| CompileEntry {
arguments: translate_flags_for_clang(&entry.arguments, arch),
directory: entry.directory.clone(),
file: entry.file.clone(),
output: entry.output.clone(),
.map(|entry| {
let mut arguments = translate_flags_for_clang(&entry.arguments, arch);
arguments.extend(builtin_includes.iter().cloned());
CompileEntry {
arguments,
directory: entry.directory.clone(),
file: entry.file.clone(),
output: entry.output.clone(),
}
})
.collect();
CompileDatabase { entries }
}

/// Swap the generated `<stem>.ino.cpp` compile entry for one raw-`.ino`
/// entry per tab (FastLED/fbuild#1076 Phase 0 — IDE-flavored compile DB;
/// direction update: "use the converter's `.ino.cpp` output for clangd
/// IntelliSense").
///
/// clangd analyzes the live text of whatever file is open in the editor.
/// An entry naming the generated `.ino.cpp` means unsaved edits to the
/// sketch are invisible and diagnostics point at a file nobody is
/// looking at. Each raw `.ino` instead gets the generated entry's
/// (already clang-translated) flags plus `-x c++ -include <prelude>`,
/// with the `file` field and the `-c <file>` / `file` argument swapped to
/// the raw `.ino` path. The generated `.ino.cpp` entry is removed —
/// keeping both would double-index the same code under two translation
/// units and confuse go-to-definition.
///
/// No-op (including: does not remove the generated entry) when
/// `ino_preludes` is empty — i.e. no `.ino` tabs were preprocessed for
/// this build (no `.ino` files, or `main.cpp` skipped preprocessing).
pub fn swap_ino_entries_for_raw(&self, ino_preludes: &[(PathBuf, PathBuf)]) -> CompileDatabase {
if ino_preludes.is_empty() {
return CompileDatabase {
entries: self.entries.clone(),
};
}

let mut template: Option<&CompileEntry> = None;
let mut entries: Vec<CompileEntry> =
Vec::with_capacity(self.entries.len() + ino_preludes.len());
for entry in &self.entries {
if entry.file.ends_with(".ino.cpp") {
template = Some(entry);
continue;
}
entries.push(entry.clone());
}

let Some(template) = template else {
// No generated .ino.cpp entry present — leave the database as-is
// rather than silently dropping something unexpected.
return CompileDatabase {
entries: self.entries.clone(),
};
};

for (raw_ino, prelude) in ino_preludes {
entries.push(raw_ino_entry_from_template(template, raw_ino, prelude));
}

CompileDatabase { entries }
}

/// Prepare compile database for IWYU (include-what-you-use) analysis.
///
/// Transforms the existing (already clang-translated) compile database so that
Expand Down Expand Up @@ -167,3 +260,41 @@ impl CompileDatabase {
CompileDatabase { entries }
}
}

/// Build one raw-`.ino` compile entry from the generated `.ino.cpp` entry's
/// (already clang-translated) flags: insert `-x c++ -include <prelude>`
/// right before `-c`, and swap every argument that names the generated file
/// (the `-c <file>` argument) for the raw `.ino` path.
fn raw_ino_entry_from_template(
template: &CompileEntry,
raw_ino: &Path,
prelude: &Path,
) -> CompileEntry {
let raw_ino_str = raw_ino.to_string_lossy().to_string();
let prelude_str = prelude.to_string_lossy().to_string();

let mut arguments = Vec::with_capacity(template.arguments.len() + 4);
let mut inserted_flavor = false;
for arg in &template.arguments {
if !inserted_flavor && arg == "-c" {
arguments.push("-x".to_string());
arguments.push("c++".to_string());
arguments.push("-include".to_string());
arguments.push(prelude_str.clone());
inserted_flavor = true;
}

if *arg == template.file {
arguments.push(raw_ino_str.clone());
} else {
arguments.push(arg.clone());
}
}

CompileEntry {
arguments,
directory: template.directory.clone(),
file: raw_ino_str,
output: template.output.clone(),
}
}
Comment on lines +263 to +300

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Raw .ino entries share the original .ino.cpp object output path.

Every raw .ino entry produced from a multi-tab sketch keeps the template's -o <path> argument and output field unchanged, so all tabs point at the same generated .ino.cpp.o path. Harmless for clangd (which never executes -c/-o), but incorrect metadata that could cause collisions if this compile DB is ever consumed by tooling that actually invokes the commands (e.g. the prepare_for_iwyu path in this same file).

🛠️ Suggested fix
     let mut arguments = Vec::with_capacity(template.arguments.len() + 4);
     let mut inserted_flavor = false;
     for arg in &template.arguments {
         if !inserted_flavor && arg == "-c" {
             arguments.push("-x".to_string());
             arguments.push("c++".to_string());
             arguments.push("-include".to_string());
             arguments.push(prelude_str.clone());
             inserted_flavor = true;
         }

-        if *arg == template.file {
+        if let Some(out) = &template.output {
+            if arg == out {
+                arguments.push(format!("{raw_ino_str}.o"));
+                continue;
+            }
+        }
+        if *arg == template.file {
             arguments.push(raw_ino_str.clone());
         } else {
             arguments.push(arg.clone());
         }
     }

     CompileEntry {
         arguments,
         directory: template.directory.clone(),
         file: raw_ino_str,
-        output: template.output.clone(),
+        output: template.output.as_ref().map(|_| format!("{raw_ino_str}.o")),
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Build one raw-`.ino` compile entry from the generated `.ino.cpp` entry's
/// (already clang-translated) flags: insert `-x c++ -include <prelude>`
/// right before `-c`, and swap every argument that names the generated file
/// (the `-c <file>` argument) for the raw `.ino` path.
fn raw_ino_entry_from_template(
template: &CompileEntry,
raw_ino: &Path,
prelude: &Path,
) -> CompileEntry {
let raw_ino_str = raw_ino.to_string_lossy().to_string();
let prelude_str = prelude.to_string_lossy().to_string();
let mut arguments = Vec::with_capacity(template.arguments.len() + 4);
let mut inserted_flavor = false;
for arg in &template.arguments {
if !inserted_flavor && arg == "-c" {
arguments.push("-x".to_string());
arguments.push("c++".to_string());
arguments.push("-include".to_string());
arguments.push(prelude_str.clone());
inserted_flavor = true;
}
if *arg == template.file {
arguments.push(raw_ino_str.clone());
} else {
arguments.push(arg.clone());
}
}
CompileEntry {
arguments,
directory: template.directory.clone(),
file: raw_ino_str,
output: template.output.clone(),
}
}
/// Build one raw-`.ino` compile entry from the generated `.ino.cpp` entry's
/// (already clang-translated) flags: insert `-x c++ -include <prelude>`
/// right before `-c`, and swap every argument that names the generated file
/// (the `-c <file>` argument) for the raw `.ino` path.
fn raw_ino_entry_from_template(
template: &CompileEntry,
raw_ino: &Path,
prelude: &Path,
) -> CompileEntry {
let raw_ino_str = raw_ino.to_string_lossy().to_string();
let prelude_str = prelude.to_string_lossy().to_string();
let mut arguments = Vec::with_capacity(template.arguments.len() + 4);
let mut inserted_flavor = false;
for arg in &template.arguments {
if !inserted_flavor && arg == "-c" {
arguments.push("-x".to_string());
arguments.push("c++".to_string());
arguments.push("-include".to_string());
arguments.push(prelude_str.clone());
inserted_flavor = true;
}
if let Some(out) = &template.output {
if arg == out {
arguments.push(format!("{raw_ino_str}.o"));
continue;
}
}
if *arg == template.file {
arguments.push(raw_ino_str.clone());
} else {
arguments.push(arg.clone());
}
}
CompileEntry {
arguments,
directory: template.directory.clone(),
file: raw_ino_str,
output: template.output.as_ref().map(|_| format!("{raw_ino_str}.o")),
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-build-engine/src/compile_database/clang.rs` around lines 263 -
300, Update raw_ino_entry_from_template so each generated raw .ino entry uses a
unique object output path derived from raw_ino instead of retaining
template.output and its -o argument. Replace the template’s output-path argument
consistently in arguments and set CompileEntry.output to the corresponding raw
.ino object path, while preserving the existing flag insertion and
source-argument substitution.

156 changes: 155 additions & 1 deletion crates/fbuild-build-engine/src/compile_database/tests/clang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::path::{Path, PathBuf};

use super::super::clang::should_remove_flag;
use super::super::clang::{isystem_args_from_dirs, should_remove_flag};
use crate::compile_database::{
CompileDatabase, CompileEntry, TargetArchitecture, translate_flags_for_clang,
};
Expand Down Expand Up @@ -225,6 +225,160 @@ fn test_database_translate_for_clang() {
);
}

// =========================================================================
// FastLED/fbuild#1076 Phase 0: builtin include-dir baking + raw-.ino swap
// =========================================================================

#[test]
fn test_isystem_args_from_dirs_sorted_and_deduped() {
let dirs = vec![
PathBuf::from("/tc/lib/gcc/xtensa/14/include"),
PathBuf::from("/tc/lib/gcc/avr/14/include"),
PathBuf::from("/tc/lib/gcc/xtensa/14/include"), // duplicate
];
let args = isystem_args_from_dirs(dirs);
assert_eq!(
args,
vec![
"-isystem".to_string(),
"/tc/lib/gcc/avr/14/include".to_string(),
"-isystem".to_string(),
"/tc/lib/gcc/xtensa/14/include".to_string(),
]
);
}

#[test]
fn test_isystem_args_from_dirs_empty_is_no_op() {
assert!(isystem_args_from_dirs(Vec::new()).is_empty());
}

fn ino_cpp_template_entry() -> CompileEntry {
CompileEntry {
arguments: vec![
"clang++".to_string(),
"--target=xtensa-esp-elf".to_string(),
"-Isrc".to_string(),
"-c".to_string(),
"/project/.fbuild/build/env/src/sketch.ino.cpp".to_string(),
"-o".to_string(),
"/project/.fbuild/build/env/src/sketch.ino.cpp.o".to_string(),
],
directory: "/project".to_string(),
file: "/project/.fbuild/build/env/src/sketch.ino.cpp".to_string(),
output: Some("/project/.fbuild/build/env/src/sketch.ino.cpp.o".to_string()),
}
}

#[test]
fn test_swap_ino_entries_for_raw_replaces_generated_entry() {
let mut db = CompileDatabase::new();
db.add_entry(ino_cpp_template_entry());

let ino_preludes = vec![(
PathBuf::from("/project/src/sketch.ino"),
PathBuf::from("/project/.fbuild/build/env/src/sketch.ino.prelude.h"),
)];
let swapped = db.swap_ino_entries_for_raw(&ino_preludes);

// Generated .ino.cpp entry removed.
assert!(!swapped.entries.iter().any(|e| e.file.ends_with(".ino.cpp")));

// Raw .ino entry present with -x c++ -include <prelude> and the raw
// path swapped in for both `file` and the `-c <file>` argument.
assert_eq!(swapped.entries.len(), 1);
let entry = &swapped.entries[0];
assert_eq!(entry.file, "/project/src/sketch.ino");
assert!(entry.arguments.contains(&"-x".to_string()));
assert!(entry.arguments.contains(&"c++".to_string()));
let include_idx = entry
.arguments
.iter()
.position(|a| a == "-include")
.unwrap();
assert_eq!(
entry.arguments[include_idx + 1],
"/project/.fbuild/build/env/src/sketch.ino.prelude.h"
);
assert!(
entry
.arguments
.contains(&"/project/src/sketch.ino".to_string())
);
assert!(!entry.arguments.iter().any(|a| a.ends_with(".ino.cpp")));
// Flags from the template are preserved.
assert!(entry.arguments.contains(&"-Isrc".to_string()));
assert!(
entry
.arguments
.contains(&"--target=xtensa-esp-elf".to_string())
);
}

#[test]
fn test_swap_ino_entries_for_raw_multi_tab() {
let mut db = CompileDatabase::new();
db.add_entry(ino_cpp_template_entry());

let ino_preludes = vec![
(
PathBuf::from("/project/src/main.ino"),
PathBuf::from("/project/.fbuild/build/env/src/main.ino.prelude.h"),
),
(
PathBuf::from("/project/src/a_tab.ino"),
PathBuf::from("/project/.fbuild/build/env/src/a_tab.ino.prelude.h"),
),
];
let swapped = db.swap_ino_entries_for_raw(&ino_preludes);

assert_eq!(swapped.entries.len(), 2);
assert!(
swapped
.entries
.iter()
.any(|e| e.file == "/project/src/main.ino")
);
assert!(
swapped
.entries
.iter()
.any(|e| e.file == "/project/src/a_tab.ino")
);
assert!(!swapped.entries.iter().any(|e| e.file.ends_with(".ino.cpp")));
}

#[test]
fn test_swap_ino_entries_for_raw_empty_preludes_is_no_op() {
let mut db = CompileDatabase::new();
db.add_entry(ino_cpp_template_entry());

let swapped = db.swap_ino_entries_for_raw(&[]);
// The generated entry survives untouched — this is the main.cpp-present
// / no-.ino-tabs case, where there is nothing to swap.
assert_eq!(swapped.entries.len(), 1);
assert!(swapped.entries[0].file.ends_with(".ino.cpp"));
}

#[test]
fn test_swap_ino_entries_for_raw_no_generated_entry_leaves_db_untouched() {
let mut db = CompileDatabase::new();
db.add_entry(CompileEntry {
arguments: vec!["clang".to_string(), "-c".to_string(), "main.c".to_string()],
directory: "/project".to_string(),
file: "main.c".to_string(),
output: None,
});

let ino_preludes = vec![(
PathBuf::from("/project/src/sketch.ino"),
PathBuf::from("/project/.fbuild/build/env/src/sketch.ino.prelude.h"),
)];
let swapped = db.swap_ino_entries_for_raw(&ino_preludes);
assert_eq!(swapped.entries.len(), 1);
assert_eq!(swapped.entries[0].file, "main.c");
}

#[test]
fn test_translate_does_not_modify_original() {
let mut db = CompileDatabase::new();
Expand Down
9 changes: 9 additions & 0 deletions crates/fbuild-build-engine/src/pipeline/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ pub async fn compile_local_libraries(
}

/// Generate `compile_commands.json` from core/variant and sketch sources.
///
/// IDE-flavored: when `ino_preludes` is non-empty (i.e. the sketch had
/// `.ino` tabs preprocessed into a generated `<stem>.ino.cpp`), the
/// generated file's entry is swapped for one raw-`.ino` entry per tab so
/// clangd gives IntelliSense on the file the user actually edits
/// (FastLED/fbuild#1076 Phase 0). See
/// [`compile_database::CompileDatabase::swap_ino_entries_for_raw`].
#[allow(clippy::too_many_arguments)]
pub fn generate_compile_db(
gcc_path: &Path,
Expand All @@ -127,6 +134,7 @@ pub fn generate_compile_db(
all_src_flags: &LanguageExtraFlags,
core_sources: &[PathBuf],
sketch_sources: &[PathBuf],
ino_preludes: &[(PathBuf, PathBuf)],
core_build_dir: &Path,
src_build_dir: &Path,
build_dir: &Path,
Expand Down Expand Up @@ -157,6 +165,7 @@ pub fn generate_compile_db(
project_dir,
));
let compile_db = compile_db.translate_for_clang(arch);
let compile_db = compile_db.swap_ino_entries_for_raw(ino_preludes);
if compile_db.has_entries() {
Ok(Some(compile_db.write_and_copy(build_dir, project_dir)?))
} else {
Expand Down
Loading
Loading