Skip to content

Commit 8f7910a

Browse files
authored
fix(pipeline): absolutize project_dir in discover_project_includes (#303) (#315)
`fbuild test-emu .build/pio/<env>` was failing every ESP32 QEMU CI run with `fatal error: FastLED.h: No such file or directory` even though the preceding `ci-compile.py` step had just produced a working firmware in the same directory. Root cause: the CLI passes the project path through verbatim. ci-compile.py sends an *absolute* path (workspace-rooted) so include flags end up absolute, get stripped to project-relative by `zccache::path_arg_for_compile_cwd`, and GCC resolves them correctly against `cwd = <project>/`. test-emu passes the raw `.build/pio/<env>` *relative* string, which propagates straight into `discover_project_includes` and produces relative include paths like `-I.build/pio/esp32dev/lib/FastLED`. The zccache normalizer short-circuits on non-absolute inputs and passes them through unchanged. GCC then resolves the relative include against the already-project-rooted compile cwd, doubling the prefix to `<project>/.build/pio/esp32dev/lib/FastLED` — a path that doesn't exist — and FastLED.h is reported missing. Fix: promote `project_dir` to an absolute path at the top of `discover_project_includes` via the existing `absolute_from_cwd` helper (made `pub` for cross-module use). Every emitted include dir is now absolute regardless of how the caller spelled the input, so the normalize→exec chain is stable. Regression tests cover both absolute and relative `project_dir` inputs and assert the absolutization invariant. Full fbuild-build test suite: 546/546 passing. Fixes #303
1 parent 8bfba3b commit 8f7910a

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

crates/fbuild-build/src/compiler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ fn command_hash_path(object: &Path) -> PathBuf {
303303
/// enforced by `clippy.toml` (1.75). Does not canonicalize symlinks or `..`.
304304
/// Falls back to the original path if `current_dir()` fails (e.g. cwd was
305305
/// deleted) — callers should treat that as the path they originally got.
306-
fn absolute_from_cwd(path: &Path) -> PathBuf {
306+
pub fn absolute_from_cwd(path: &Path) -> PathBuf {
307307
if path.is_absolute() {
308308
path.to_path_buf()
309309
} else {

crates/fbuild-build/src/pipeline/project_discovery.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,21 @@ use std::path::{Path, PathBuf};
66
/// Add the project's `include/` directory and `lib/` subdirectories to include paths.
77
///
88
/// PlatformIO automatically adds these — replicate that behavior.
9+
///
10+
/// All emitted paths are absolute. When `project_dir` is relative, it is first
11+
/// resolved against the current working directory (see `absolute_from_cwd`).
12+
/// This is load-bearing for the compiler step: the zccache path normalizer
13+
/// (`zccache::path_arg_for_compile_cwd`) only strips the compile-cwd prefix
14+
/// from *absolute* include paths and passes relative ones through unchanged.
15+
/// Compiles run with `cwd = <project>/`, so a relative include like
16+
/// `.build/pio/esp32dev/lib/FastLED` would be re-resolved against the
17+
/// already-project-rooted cwd and yield a doubled path that GCC then fails to
18+
/// open (`fatal error: FastLED.h: No such file or directory`). Promoting
19+
/// `project_dir` to absolute up front keeps the include paths stable through
20+
/// the normalize→exec chain. See FastLED/fbuild#303.
921
pub fn discover_project_includes(project_dir: &Path, include_dirs: &mut Vec<PathBuf>) {
22+
let project_dir = crate::compiler::absolute_from_cwd(project_dir);
23+
1024
// PlatformIO automatically includes the project's include/ directory
1125
let include_dir = project_dir.join("include");
1226
if include_dir.is_dir() {
@@ -68,3 +82,73 @@ pub fn is_platform_project(
6882
}
6983
false
7084
}
85+
86+
#[cfg(test)]
87+
mod tests {
88+
use super::*;
89+
90+
/// FastLED/fbuild#303: every emitted include dir must be absolute, even
91+
/// when `project_dir` is passed in relative form (which is what
92+
/// `fbuild test-emu .build/pio/<env>` produces). A relative include path
93+
/// survives `zccache::path_arg_for_compile_cwd` unchanged and then gets
94+
/// re-resolved against `cwd = <project>/`, producing a doubled-prefix
95+
/// non-existent path. Promoting to absolute up front breaks the cycle.
96+
#[test]
97+
fn includes_are_absolute_for_absolute_project_dir() {
98+
let tmp = tempfile::tempdir().unwrap();
99+
let project = tmp.path();
100+
std::fs::create_dir_all(project.join("lib").join("FastLED")).unwrap();
101+
std::fs::write(project.join("lib").join("FastLED").join("FastLED.h"), b"").unwrap();
102+
std::fs::create_dir_all(project.join("include")).unwrap();
103+
104+
let mut dirs = Vec::new();
105+
discover_project_includes(project, &mut dirs);
106+
107+
assert!(!dirs.is_empty(), "should discover include + lib paths");
108+
for d in &dirs {
109+
assert!(
110+
d.is_absolute(),
111+
"include dir must be absolute, got: {}",
112+
d.display()
113+
);
114+
}
115+
// The lib's root must be in the list — that's where FastLED.h lives.
116+
assert!(
117+
dirs.iter().any(|d| d.ends_with("FastLED")),
118+
"lib/FastLED root missing from {:?}",
119+
dirs
120+
);
121+
}
122+
123+
/// Same property, but with a relative `project_dir` — mirrors what the
124+
/// daemon receives from `fbuild test-emu .build/pio/esp32dev` (the
125+
/// `PathBuf` is built directly from the request string and never
126+
/// canonicalized in the handler).
127+
#[test]
128+
fn includes_are_absolute_for_relative_project_dir() {
129+
let tmp = tempfile::tempdir().unwrap();
130+
let project = tmp.path();
131+
std::fs::create_dir_all(project.join("lib").join("FastLED")).unwrap();
132+
133+
// Build a relative path against the current process cwd that points
134+
// at our tempdir. If the tempdir isn't under cwd (varies by platform
135+
// and TMPDIR), fall back to the absolute path — the absolutization
136+
// invariant must still hold either way.
137+
let cwd = std::env::current_dir().unwrap();
138+
let relative = match project.strip_prefix(&cwd) {
139+
Ok(rel) => PathBuf::from(".").join(rel),
140+
Err(_) => project.to_path_buf(),
141+
};
142+
143+
let mut dirs = Vec::new();
144+
discover_project_includes(&relative, &mut dirs);
145+
146+
for d in &dirs {
147+
assert!(
148+
d.is_absolute(),
149+
"include dir must be absolute even when project_dir is relative; got: {}",
150+
d.display()
151+
);
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)