diff --git a/crates/fbuild-build/src/esp32/orchestrator/framework_libs.rs b/crates/fbuild-build/src/esp32/orchestrator/framework_libs.rs index b358680a..be2885be 100644 --- a/crates/fbuild-build/src/esp32/orchestrator/framework_libs.rs +++ b/crates/fbuild-build/src/esp32/orchestrator/framework_libs.rs @@ -50,6 +50,14 @@ pub(super) async fn compile_framework_builtin_libs( let fw_libs_build_dir = build_dir.join("fw_libs"); std::fs::create_dir_all(&fw_libs_build_dir)?; + // Compile framework libs workspace-relative so their zccache keys are + // stable across project directories and hit the warm cache instead of + // recompiling ~150s on every fresh project (FastLED/fbuild#952). The + // object dir lives under `/.fbuild/...`, so this resolves to + // the project workspace root (canonicalized). + let fw_compile_cwd = + fbuild_core::path::compile_cwd_from_output(&fw_libs_build_dir.join("obj").join("_probe.o")); + // Build set of already-compiled library names let already_compiled: std::collections::HashSet = library_archives .iter() @@ -174,6 +182,7 @@ pub(super) async fn compile_framework_builtin_libs( params.verbose, fw_jobs, compiler_cache, + fw_compile_cwd.clone(), ) .await { diff --git a/crates/fbuild-build/src/esp32/orchestrator/local_libs.rs b/crates/fbuild-build/src/esp32/orchestrator/local_libs.rs index 7011998e..b8a6e833 100644 --- a/crates/fbuild-build/src/esp32/orchestrator/local_libs.rs +++ b/crates/fbuild-build/src/esp32/orchestrator/local_libs.rs @@ -83,6 +83,7 @@ pub(super) async fn compile_local_libraries( verbose, jobs, compiler_cache, + None, ) .await { diff --git a/crates/fbuild-build/src/pipeline/library.rs b/crates/fbuild-build/src/pipeline/library.rs index 592ce9ac..3da10545 100644 --- a/crates/fbuild-build/src/pipeline/library.rs +++ b/crates/fbuild-build/src/pipeline/library.rs @@ -174,6 +174,7 @@ pub async fn compile_extra_libraries( env.verbose, env.jobs, env.compiler_cache, + None, ) .await { @@ -278,6 +279,7 @@ pub async fn compile_project_as_library( env.verbose, env.jobs, env.compiler_cache, + None, ) .await { diff --git a/crates/fbuild-build/src/zccache.rs b/crates/fbuild-build/src/zccache.rs index 88e2d688..0aef3bb0 100644 --- a/crates/fbuild-build/src/zccache.rs +++ b/crates/fbuild-build/src/zccache.rs @@ -24,11 +24,18 @@ //! — still used by `compile_source` to keep cache keys //! workspace-relative. -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -use fbuild_core::path::NormalizedPath; use fbuild_core::{FbuildError, Result}; +// Workspace-relativization helpers were moved to `fbuild_core::path` so +// `fbuild-packages` can share them for library compiles (FastLED/fbuild#952). +// Re-exported here so existing `crate::zccache::…` call sites keep working +// unchanged. +pub use fbuild_core::path::{ + compile_cwd_from_output, normalize_flags_for_compile_cwd, path_arg_for_compile_cwd, +}; + /// A persistent zccache fingerprint watch. #[derive(Debug, Clone)] pub struct FingerprintWatch { @@ -79,326 +86,3 @@ pub fn mark_fingerprint_success(watch: &FingerprintWatch) -> Result<()> { )) }) } - -/// Return the workspace root to use as the CWD for zccache compiles. -/// -/// Upstream zccache normalizes cache-key paths relative to the -/// compile CWD. fbuild object files live under -/// `/.fbuild/...`, so running the compile from -/// `` lets identical renamed workspaces share per-TU -/// cache keys even when compiler args contain absolute paths. -pub fn compile_cwd_from_output(output: &Path) -> Option { - let mut dir = output.parent()?; - loop { - if dir - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.eq_ignore_ascii_case(".fbuild")) - { - return dir.parent().map(|workspace| { - canonicalize_existing_path(workspace).unwrap_or_else(|| workspace.to_path_buf()) - }); - } - dir = dir.parent()?; - } -} - -/// Return a path argument that is stable relative to the zccache compile CWD. -/// -/// macOS can canonicalize `/var/...` working directories to `/private/var/...` -/// inside the child process. Canonicalizing absolute compiler arguments before -/// stripping the compile CWD keeps zccache keys workspace-relative across both -/// path spellings. -/// -/// The final `NormalizedPath::display_slash()` call is the load-bearing -/// step: `NormalizedPath` owns the Windows `\` → `/` rewrite that GCC's -/// internal spec-file pass requires (see FastLED/fbuild#875, #885, #890, -/// #912). Do NOT hand-roll the rewrite here — the `ban_manual_slash_normalize` -/// dylint flags that anti-pattern. Route every path arg through -/// `NormalizedPath::display_slash()` at exactly one place, which is here. -pub fn path_arg_for_compile_cwd(path: &Path, cwd: &Path) -> String { - let relative: PathBuf = if !path.is_absolute() { - path.to_path_buf() - } else { - // Both ends must be in the same normal form (stripped of any `\\?\` - // Windows extended-length prefix) for `strip_prefix` to match, since - // `canonicalize_existing_path` now strips that prefix from `path`. - let stable_path = canonicalize_existing_path(path).unwrap_or_else(|| path.to_path_buf()); - let stable_cwd = strip_unc_prefix(cwd.to_path_buf()); - stable_path - .strip_prefix(&stable_cwd) - .map(|tail| tail.to_path_buf()) - .unwrap_or(stable_path) - }; - // GCC's internal spec-file pass treats `\` as an escape character, so - // `src\main.cpp` reaches cc1plus as `srcmain.cpp` and fails to open. - // `NormalizedPath::display_slash()` owns the `\` → `/` rewrite (plus - // the Windows UNC prefix strip) for the whole workspace — see - // fbuild_core::path::NormalizedPath. POSIX hosts pass through unchanged. - let arg = NormalizedPath::from(relative).display_slash(); - // Preserve the "empty relative → '.'" invariant: when the source file - // *is* the compile CWD (or the caller passed a bare empty path), GCC - // requires a positional argument that resolves to the current directory. - if arg.is_empty() { - ".".to_string() - } else { - arg - } -} - -/// Normalize common path-bearing compiler flags for a zccache CWD. -pub fn normalize_flags_for_compile_cwd(flags: &[String], cwd: &Path) -> Vec { - let mut normalized = Vec::with_capacity(flags.len()); - let mut next_is_path = false; - - for flag in flags { - if next_is_path { - normalized.push(path_arg_for_compile_cwd(Path::new(flag), cwd)); - next_is_path = false; - continue; - } - - if flag_takes_path_argument(flag) { - normalized.push(flag.clone()); - next_is_path = true; - continue; - } - - if let Some(value) = flag.strip_prefix("--sysroot=") { - normalized.push(format!( - "--sysroot={}", - path_arg_for_compile_cwd(Path::new(value), cwd) - )); - continue; - } - - if let Some((prefix, value)) = split_joined_path_flag(flag) { - normalized.push(format!( - "{}{}", - prefix, - path_arg_for_compile_cwd(Path::new(value), cwd) - )); - continue; - } - - normalized.push(flag.clone()); - } - - normalized -} - -fn canonicalize_existing_path(path: &Path) -> Option { - if let Ok(canonical) = path.canonicalize() { - return Some(strip_unc_prefix(canonical)); - } - - let parent = path.parent()?.canonicalize().ok()?; - let joined = match path.file_name() { - Some(name) => parent.join(name), - None => parent, - }; - Some(strip_unc_prefix(joined)) -} - -/// On Windows, `Path::canonicalize` returns paths prefixed with the -/// extended-length namespace marker `\\?\`. That prefix only works with -/// backslash path separators, but the response-file writer rewrites all -/// backslashes to forward slashes (so GCC doesn't interpret them as escape -/// sequences). The result is `//?/C:/…` which neither GCC nor mingw's path -/// search code understands as a valid drive-letter path, so `#include` -/// lookups against canonicalized include dirs silently fail (see FastLED -/// issue #2507 — `soc/soc_caps.h` not found on esp32p4). -/// -/// Stripping the prefix here turns `\\?\C:\…` into `C:\…`, which survives -/// the backslash→slash rewrite as the standard `C:/…` form GCC accepts. -/// The trade-off is that long-path support (>260 chars) is lost for the -/// stripped paths, but the cache root is `/.fbuild` which is well -/// under the limit in practice. -fn strip_unc_prefix(path: PathBuf) -> PathBuf { - if !cfg!(windows) { - return path; - } - let s = path.to_string_lossy(); - // Handle both `\\?\C:\…` and (defensively) the already-rewritten `//?/C:/…`. - if let Some(rest) = s.strip_prefix(r"\\?\").or_else(|| s.strip_prefix("//?/")) { - return PathBuf::from(rest); - } - path -} - -fn flag_takes_path_argument(flag: &str) -> bool { - matches!( - flag, - "-I" | "-isystem" - | "-iquote" - | "-idirafter" - | "-include" - | "-imacros" - | "-isysroot" - | "--sysroot" - ) -} - -fn split_joined_path_flag(flag: &str) -> Option<(&'static str, &str)> { - for prefix in [ - "-I", - "-isystem", - "-iquote", - "-idirafter", - "-include", - "-imacros", - "-isysroot", - ] { - if let Some(value) = flag.strip_prefix(prefix).filter(|value| !value.is_empty()) { - return Some((prefix, value)); - } - } - None -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn compile_cwd_from_output_uses_workspace_before_fbuild() { - let output = Path::new("/work/project/.fbuild/build/env/release/src/main.o"); - - assert_eq!( - compile_cwd_from_output(output).as_deref(), - Some(Path::new("/work/project")) - ); - } - - #[test] - fn compile_cwd_from_output_returns_none_without_fbuild_component() { - let output = Path::new("/work/project/build/env/main.o"); - - assert!(compile_cwd_from_output(output).is_none()); - } - - #[test] - fn compile_cwd_from_output_canonicalizes_existing_workspace() { - let tmp = tempfile::TempDir::new().unwrap(); - let workspace = tmp.path().join("project"); - let output = workspace.join(".fbuild/build/main.o"); - std::fs::create_dir_all(output.parent().unwrap()).unwrap(); - // On Windows, canonicalize() yields a `\\?\` extended-length prefix - // that the response-file writer cannot use (forward-slash rewrite - // produces `//?/` which gcc rejects). The helper now strips that - // prefix on Windows, so the expected value must match. - let expected = strip_unc_prefix(workspace.canonicalize().unwrap()); - - assert_eq!( - compile_cwd_from_output(&output).as_deref(), - Some(expected.as_path()) - ); - } - - #[test] - #[cfg(windows)] - fn strip_unc_prefix_removes_extended_length_marker() { - let raw = std::path::PathBuf::from(r"\\?\C:\Users\test\.fbuild\cache"); - let stripped = strip_unc_prefix(raw); - assert_eq!( - stripped, - std::path::PathBuf::from(r"C:\Users\test\.fbuild\cache") - ); - } - - #[test] - #[cfg(windows)] - fn strip_unc_prefix_is_idempotent_for_already_normal_paths() { - let raw = std::path::PathBuf::from(r"C:\Users\test\include"); - let stripped = strip_unc_prefix(raw.clone()); - assert_eq!(stripped, raw); - } - - #[test] - fn path_arg_for_compile_cwd_returns_workspace_relative_path() { - let tmp = tempfile::TempDir::new().unwrap(); - let cwd = tmp.path().join("project"); - let source = cwd.join("src/main.cpp"); - std::fs::create_dir_all(source.parent().unwrap()).unwrap(); - std::fs::write(&source, "int main() { return 0; }\n").unwrap(); - let cwd = cwd.canonicalize().unwrap(); - - // Forward-slash spelling is the invariant on every platform — GCC's - // spec-file pass treats `\` as an escape character on Windows. - // `NormalizedPath::display_slash()` owns that rewrite. - assert_eq!(path_arg_for_compile_cwd(&source, &cwd), "src/main.cpp"); - } - - #[test] - #[cfg(windows)] - fn path_arg_for_compile_cwd_forces_forward_slashes_on_windows() { - // FastLED/fbuild#911 regression test — the #890 / #912 bug class: - // any hand-rolled path formatting on Windows that leaves backslashes - // intact reaches cc1plus with `\` interpreted as an escape, so - // `src\main.cpp` becomes `srcmain.cpp` and fails to open. This test - // walks the absolute-path arm end-to-end on Windows and asserts no - // `\` character survives — locking the invariant that - // `NormalizedPath::display_slash()` is doing its job for the - // compile pipeline. - let tmp = tempfile::TempDir::new().unwrap(); - let cwd = tmp.path().join("project"); - let nested = cwd.join("src").join("sketch"); - let source = nested.join("main.cpp"); - std::fs::create_dir_all(&nested).unwrap(); - std::fs::write(&source, "int main() { return 0; }\n").unwrap(); - let cwd = cwd.canonicalize().unwrap(); - - let arg = path_arg_for_compile_cwd(&source, &cwd); - - assert!( - !arg.contains('\\'), - "compile arg must not contain backslashes: {arg}" - ); - assert_eq!(arg, "src/sketch/main.cpp"); - } - - #[test] - fn path_arg_for_compile_cwd_returns_dot_for_workspace_root() { - let tmp = tempfile::TempDir::new().unwrap(); - let cwd = tmp.path().join("project"); - std::fs::create_dir_all(&cwd).unwrap(); - let cwd = cwd.canonicalize().unwrap(); - - assert_eq!(path_arg_for_compile_cwd(&cwd, &cwd), "."); - } - - #[test] - fn normalize_flags_for_compile_cwd_rewrites_include_paths() { - let tmp = tempfile::TempDir::new().unwrap(); - let cwd = tmp.path().join("project"); - let include = cwd.join("include"); - let vendor = cwd.join("vendor"); - let sysroot = cwd.join("sysroot"); - std::fs::create_dir_all(&include).unwrap(); - std::fs::create_dir_all(&vendor).unwrap(); - std::fs::create_dir_all(&sysroot).unwrap(); - let cwd = cwd.canonicalize().unwrap(); - let flags = vec![ - "-I".to_string(), - include.to_string_lossy().to_string(), - "-I".to_string(), - cwd.to_string_lossy().to_string(), - format!("-I{}", vendor.display()), - format!("-I{}", cwd.display()), - format!("--sysroot={}", sysroot.display()), - ]; - - assert_eq!( - normalize_flags_for_compile_cwd(&flags, &cwd), - vec![ - "-I".to_string(), - "include".to_string(), - "-I".to_string(), - ".".to_string(), - "-Ivendor".to_string(), - "-I.".to_string(), - "--sysroot=sysroot".to_string(), - ] - ); - } -} diff --git a/crates/fbuild-core/src/path.rs b/crates/fbuild-core/src/path.rs index 6a5f8af1..53051410 100644 --- a/crates/fbuild-core/src/path.rs +++ b/crates/fbuild-core/src/path.rs @@ -340,6 +340,151 @@ pub fn normalize_for_key(path: &Path) -> String { } } +// --------------------------------------------------------------------------- +// Compile-CWD workspace relativization (FastLED/fbuild#952). +// +// These live here (rather than in fbuild-build) so both `fbuild-build` +// (sketch/core compiles) and `fbuild-packages` (library compiles) can +// relativize compile args to the workspace root. Keeping compile args +// workspace-relative is what lets zccache keys stay stable across +// different project directories — see agents/docs/path-conventions.md. +// --------------------------------------------------------------------------- + +/// Return the workspace root to use as the CWD for zccache compiles. +/// +/// fbuild object files live under `/.fbuild/...`, so running +/// the compile from `` (and relativizing args to it) lets +/// identically-shaped workspaces at different absolute paths share per-TU +/// cache keys even when the raw args contain absolute paths. +#[must_use] +pub fn compile_cwd_from_output(output: &Path) -> Option { + let mut dir = output.parent()?; + loop { + if dir + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.eq_ignore_ascii_case(".fbuild")) + { + return dir.parent().map(|workspace| { + canonicalize_lexical(workspace).unwrap_or_else(|| workspace.to_path_buf()) + }); + } + dir = dir.parent()?; + } +} + +/// Return a path argument that is stable relative to the zccache compile CWD. +/// +/// Absolute args under the compile CWD are stripped to a workspace-relative +/// tail; everything is finally routed through +/// [`NormalizedPath::display_slash`], which owns the Windows `\` → `/` +/// rewrite GCC's spec-file pass requires (FastLED/fbuild#875, #885, #890, +/// #912). Do NOT hand-roll the slash rewrite — `ban_manual_slash_normalize` +/// flags that. +#[must_use] +pub fn path_arg_for_compile_cwd(path: &Path, cwd: &Path) -> String { + let relative: PathBuf = if !path.is_absolute() { + path.to_path_buf() + } else { + let stable_path = canonicalize_lexical(path).unwrap_or_else(|| path.to_path_buf()); + let stable_cwd = strip_unc_prefix(cwd); + stable_path + .strip_prefix(&stable_cwd) + .map(|tail| tail.to_path_buf()) + .unwrap_or(stable_path) + }; + let arg = NormalizedPath::from(relative).display_slash(); + if arg.is_empty() { + ".".to_string() + } else { + arg + } +} + +/// Normalize common path-bearing compiler flags (`-I`, `-isystem`, +/// `--sysroot`, ...) for a zccache compile CWD. +#[must_use] +pub fn normalize_flags_for_compile_cwd(flags: &[String], cwd: &Path) -> Vec { + let mut normalized = Vec::with_capacity(flags.len()); + let mut next_is_path = false; + + for flag in flags { + if next_is_path { + normalized.push(path_arg_for_compile_cwd(Path::new(flag), cwd)); + next_is_path = false; + continue; + } + if flag_takes_path_argument(flag) { + normalized.push(flag.clone()); + next_is_path = true; + continue; + } + if let Some(value) = flag.strip_prefix("--sysroot=") { + normalized.push(format!( + "--sysroot={}", + path_arg_for_compile_cwd(Path::new(value), cwd) + )); + continue; + } + if let Some((prefix, value)) = split_joined_path_flag(flag) { + normalized.push(format!( + "{}{}", + prefix, + path_arg_for_compile_cwd(Path::new(value), cwd) + )); + continue; + } + normalized.push(flag.clone()); + } + + normalized +} + +/// Canonicalize an existing path (stripping the Windows `\\?\` prefix), +/// falling back to canonicalizing the parent + rejoining the file name when +/// the full path does not yet exist. Returns `None` if neither resolves. +fn canonicalize_lexical(path: &Path) -> Option { + if let Ok(canonical) = path.canonicalize() { + return Some(strip_unc_prefix(&canonical)); + } + let parent = path.parent()?.canonicalize().ok()?; + let joined = match path.file_name() { + Some(name) => parent.join(name), + None => parent, + }; + Some(strip_unc_prefix(&joined)) +} + +fn flag_takes_path_argument(flag: &str) -> bool { + matches!( + flag, + "-I" | "-isystem" + | "-iquote" + | "-idirafter" + | "-include" + | "-imacros" + | "-isysroot" + | "--sysroot" + ) +} + +fn split_joined_path_flag(flag: &str) -> Option<(&'static str, &str)> { + for prefix in [ + "-I", + "-isystem", + "-iquote", + "-idirafter", + "-include", + "-imacros", + "-isysroot", + ] { + if let Some(value) = flag.strip_prefix(prefix).filter(|value| !value.is_empty()) { + return Some((prefix, value)); + } + } + None +} + #[cfg(test)] mod tests { use super::*; @@ -599,4 +744,107 @@ mod tests { .unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::NotFound); } + + // --- compile-CWD relativization (moved from zccache.rs, #952) --- + + #[test] + fn compile_cwd_from_output_uses_workspace_before_fbuild() { + let output = Path::new("/work/project/.fbuild/build/env/release/src/main.o"); + assert_eq!( + compile_cwd_from_output(output).as_deref(), + Some(Path::new("/work/project")) + ); + } + + #[test] + fn compile_cwd_from_output_returns_none_without_fbuild_component() { + let output = Path::new("/work/project/build/env/main.o"); + assert!(compile_cwd_from_output(output).is_none()); + } + + #[test] + fn compile_cwd_from_output_canonicalizes_existing_workspace() { + let tmp = tempfile::TempDir::new().unwrap(); + let workspace = tmp.path().join("project"); + let output = workspace.join(".fbuild/build/main.o"); + std::fs::create_dir_all(output.parent().unwrap()).unwrap(); + let expected = strip_unc_prefix(&workspace.canonicalize().unwrap()); + assert_eq!( + compile_cwd_from_output(&output).as_deref(), + Some(expected.as_path()) + ); + } + + #[test] + fn path_arg_for_compile_cwd_returns_workspace_relative_path() { + let tmp = tempfile::TempDir::new().unwrap(); + let cwd = tmp.path().join("project"); + let source = cwd.join("src/main.cpp"); + std::fs::create_dir_all(source.parent().unwrap()).unwrap(); + std::fs::write(&source, "int main() { return 0; }\n").unwrap(); + let cwd = cwd.canonicalize().unwrap(); + // Forward-slash spelling is the invariant on every platform. + assert_eq!(path_arg_for_compile_cwd(&source, &cwd), "src/main.cpp"); + } + + #[test] + #[cfg(windows)] + fn path_arg_for_compile_cwd_forces_forward_slashes_on_windows() { + let tmp = tempfile::TempDir::new().unwrap(); + let cwd = tmp.path().join("project"); + let nested = cwd.join("src").join("sketch"); + let source = nested.join("main.cpp"); + std::fs::create_dir_all(&nested).unwrap(); + std::fs::write(&source, "int main() { return 0; }\n").unwrap(); + let cwd = cwd.canonicalize().unwrap(); + let arg = path_arg_for_compile_cwd(&source, &cwd); + assert!( + !arg.contains('\\'), + "compile arg must not contain backslashes: {arg}" + ); + assert_eq!(arg, "src/sketch/main.cpp"); + } + + #[test] + fn path_arg_for_compile_cwd_returns_dot_for_workspace_root() { + let tmp = tempfile::TempDir::new().unwrap(); + let cwd = tmp.path().join("project"); + std::fs::create_dir_all(&cwd).unwrap(); + let cwd = cwd.canonicalize().unwrap(); + assert_eq!(path_arg_for_compile_cwd(&cwd, &cwd), "."); + } + + #[test] + fn normalize_flags_for_compile_cwd_rewrites_include_paths() { + let tmp = tempfile::TempDir::new().unwrap(); + let cwd = tmp.path().join("project"); + let include = cwd.join("include"); + let vendor = cwd.join("vendor"); + let sysroot = cwd.join("sysroot"); + std::fs::create_dir_all(&include).unwrap(); + std::fs::create_dir_all(&vendor).unwrap(); + std::fs::create_dir_all(&sysroot).unwrap(); + let cwd = cwd.canonicalize().unwrap(); + let flags = vec![ + "-I".to_string(), + include.to_string_lossy().to_string(), + "-I".to_string(), + cwd.to_string_lossy().to_string(), + format!("-I{}", vendor.display()), + format!("-I{}", cwd.display()), + format!("--sysroot={}", sysroot.display()), + ]; + assert_eq!( + normalize_flags_for_compile_cwd(&flags, &cwd), + vec![ + "-I".to_string(), + "include".to_string(), + "-I".to_string(), + ".".to_string(), + "-Ivendor".to_string(), + "-I.".to_string(), + "--sysroot=sysroot".to_string(), + ] + ); + } } diff --git a/crates/fbuild-packages/src/library/library_compiler.rs b/crates/fbuild-packages/src/library/library_compiler.rs index 20f34324..812acffa 100644 --- a/crates/fbuild-packages/src/library/library_compiler.rs +++ b/crates/fbuild-packages/src/library/library_compiler.rs @@ -76,6 +76,7 @@ pub async fn compile_library( verbose, 1, compiler_cache, + None, ) .await } @@ -95,6 +96,12 @@ pub async fn compile_library_with_jobs( verbose: bool, jobs: usize, compiler_cache: Option<&Path>, + // When `Some(workspace_root)`, compiles run workspace-relative (source + // / `-o` / project `-I` relativized, cwd = workspace) so their zccache + // keys are stable across project directories and hit the warm cache + // (FastLED/fbuild#952). `None` keeps the historical absolute-path, + // ambient-cwd invocation for every caller that hasn't opted in. + compile_cwd: Option, ) -> Result> { if source_files.is_empty() { tracing::debug!("library {} is header-only, skipping compile", name); @@ -123,6 +130,20 @@ pub async fn compile_library_with_jobs( .collect(); let cpp_flags = cpp_flags.to_vec(); + + // When opted in, relativize project-rooted path flags (`-I`, `--sysroot`, + // ...) to the workspace once. Paths outside the workspace (toolchain, + // SDK) are already project-independent and pass through unchanged. This + // keeps the per-TU rebuild signature and the zccache key project-stable + // (FastLED/fbuild#952). + let (include_flags, c_safe_flags, cpp_flags) = match compile_cwd.as_deref() { + Some(cwd) => ( + fbuild_core::path::normalize_flags_for_compile_cwd(&include_flags, cwd), + fbuild_core::path::normalize_flags_for_compile_cwd(&c_safe_flags, cwd), + fbuild_core::path::normalize_flags_for_compile_cwd(&cpp_flags, cwd), + ), + None => (include_flags, c_safe_flags, cpp_flags), + }; let all_objects: Vec = source_files .iter() .map(|source| object_path(source, &obj_dir)) @@ -176,6 +197,7 @@ pub async fn compile_library_with_jobs( name, verbose, compiler_cache, + compile_cwd.as_deref(), ) .await?; } @@ -213,6 +235,7 @@ pub async fn compile_library_with_jobs( let include_flags_arc = std::sync::Arc::new(include_flags.clone()); let name_owned = name.to_string(); let compiler_cache_owned = compiler_cache.map(|p| p.to_path_buf()); + let compile_cwd_owned = compile_cwd.clone(); for source in stale_sources.clone() { let sem = sem.clone(); @@ -224,6 +247,7 @@ pub async fn compile_library_with_jobs( let inc_t = include_flags_arc.clone(); let lib_name_t = name_owned.clone(); let cache_t = compiler_cache_owned.clone(); + let cwd_t = compile_cwd_owned.clone(); let counter = compiled_count.clone(); tasks.spawn(async move { let _permit = sem @@ -242,6 +266,7 @@ pub async fn compile_library_with_jobs( &lib_name_t, verbose, cache_ref, + cwd_t.as_deref(), ) .await?; let count = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; @@ -313,6 +338,12 @@ async fn compile_one_source( lib_name: &str, verbose: bool, compiler_cache: Option<&Path>, + // When `Some`, run the compiler from this workspace root and pass + // source / `-o` as workspace-relative args, so the zccache key is + // stable across project directories (FastLED/fbuild#952). When `None` + // (every non-opted-in caller) the invocation is byte-identical to + // before: absolute paths, ambient cwd. + compile_cwd: Option<&Path>, ) -> Result { let obj = object_path(source, obj_dir); let rsp_dir = obj_dir.parent().unwrap_or(obj_dir).join("tmp"); @@ -334,16 +365,24 @@ async fn compile_one_source( ); } + // Source and object args: workspace-relative when opted in, else the + // historical absolute strings. + let (source_arg, obj_arg) = match compile_cwd { + Some(cwd) => ( + fbuild_core::path::path_arg_for_compile_cwd(source, cwd), + fbuild_core::path::path_arg_for_compile_cwd(&obj, cwd), + ), + None => ( + source.to_string_lossy().to_string(), + obj.to_string_lossy().to_string(), + ), + }; + // Collect all flags that follow the compiler executable let mut all_flags: Vec = Vec::new(); all_flags.extend_from_slice(flags); all_flags.extend_from_slice(include_flags); - all_flags.extend([ - "-c".to_string(), - source.to_string_lossy().to_string(), - "-o".to_string(), - obj.to_string_lossy().to_string(), - ]); + all_flags.extend(["-c".to_string(), source_arg, "-o".to_string(), obj_arg]); // On Windows, put ALL flags in a response file to avoid command-line // length limits (OS error 206). The command becomes: @@ -376,7 +415,7 @@ async fn compile_one_source( // anything beyond that is wedged toolchain rather than a slow build. let result = run_command( &args_ref, - None, + compile_cwd, None, Some(fbuild_core::time::REAL_BUILD_TIMEOUT), ) diff --git a/crates/fbuild-packages/src/library/library_manager.rs b/crates/fbuild-packages/src/library/library_manager.rs index 691d3233..2d077080 100644 --- a/crates/fbuild-packages/src/library/library_manager.rs +++ b/crates/fbuild-packages/src/library/library_manager.rs @@ -154,6 +154,7 @@ pub async fn ensure_libraries( verbose, jobs, compiler_cache, + None, ) .await? {