Skip to content

Commit dab5a0c

Browse files
authored
build: normalize zccache compile paths (#193)
1 parent ada3b60 commit dab5a0c

4 files changed

Lines changed: 209 additions & 13 deletions

File tree

.github/workflows/template_build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ jobs:
4141
target-cache: true
4242
cache-key-suffix: board-${{ inputs.env-name }}
4343

44+
- name: Reset zccache daemon after cache warm
45+
run: pkill -f '[z]ccache-daemon' || true
46+
4447
- name: Restore fbuild toolchains
4548
uses: actions/cache/restore@v5
4649
with:

crates/fbuild-build/src/compiler.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -563,17 +563,37 @@ pub fn compile_source(
563563
std::fs::create_dir_all(parent)?;
564564
}
565565

566+
let compile_cwd = compiler_cache.and_then(|_| crate::zccache::compile_cwd_from_output(output));
567+
let (source_arg, output_arg) = if let Some(cwd) = compile_cwd.as_deref() {
568+
(
569+
crate::zccache::path_arg_for_compile_cwd(source, cwd),
570+
crate::zccache::path_arg_for_compile_cwd(output, cwd),
571+
)
572+
} else {
573+
(
574+
source.to_string_lossy().to_string(),
575+
output.to_string_lossy().to_string(),
576+
)
577+
};
578+
566579
let mut all_flags: Vec<String> = Vec::new();
567-
all_flags.extend(flags.iter().cloned());
568-
all_flags.extend(extra_pre_flags.iter().cloned());
569-
all_flags.extend(extra_flags.iter().cloned());
580+
if let Some(cwd) = compile_cwd.as_deref() {
581+
all_flags.extend(crate::zccache::normalize_flags_for_compile_cwd(flags, cwd));
582+
all_flags.extend(crate::zccache::normalize_flags_for_compile_cwd(
583+
extra_pre_flags,
584+
cwd,
585+
));
586+
all_flags.extend(crate::zccache::normalize_flags_for_compile_cwd(
587+
extra_flags,
588+
cwd,
589+
));
590+
} else {
591+
all_flags.extend(flags.iter().cloned());
592+
all_flags.extend(extra_pre_flags.iter().cloned());
593+
all_flags.extend(extra_flags.iter().cloned());
594+
}
570595
let rebuild_signature = build_rebuild_signature(compiler, flags, extra_pre_flags, extra_flags);
571-
all_flags.extend([
572-
"-c".to_string(),
573-
source.to_string_lossy().to_string(),
574-
"-o".to_string(),
575-
output.to_string_lossy().to_string(),
576-
]);
596+
all_flags.extend(["-c".to_string(), source_arg, "-o".to_string(), output_arg]);
577597

578598
// On Windows, write all flags to a response file to avoid command-line
579599
// length limits and backslash-quote escaping issues with CreateProcessW.
@@ -596,7 +616,6 @@ pub fn compile_source(
596616
};
597617

598618
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
599-
let compile_cwd = compiler_cache.and_then(|_| crate::zccache::compile_cwd_from_output(output));
600619

601620
if verbose {
602621
tracing::info!("compile: {}", args.join(" "));

crates/fbuild-build/src/zccache.rs

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,116 @@ pub fn compile_cwd_from_output(output: &Path) -> Option<PathBuf> {
191191
.and_then(|name| name.to_str())
192192
.is_some_and(|name| name.eq_ignore_ascii_case(".fbuild"))
193193
{
194-
return dir.parent().map(Path::to_path_buf);
194+
return dir.parent().map(|workspace| {
195+
canonicalize_existing_path(workspace).unwrap_or_else(|| workspace.to_path_buf())
196+
});
195197
}
196198
dir = dir.parent()?;
197199
}
198200
}
199201

202+
/// Return a path argument that is stable relative to the zccache compile CWD.
203+
///
204+
/// macOS can canonicalize `/var/...` working directories to `/private/var/...`
205+
/// inside the child process. Canonicalizing absolute compiler arguments before
206+
/// stripping the compile CWD keeps zccache keys workspace-relative across both
207+
/// path spellings.
208+
pub fn path_arg_for_compile_cwd(path: &Path, cwd: &Path) -> String {
209+
if !path.is_absolute() {
210+
return path.to_string_lossy().to_string();
211+
}
212+
213+
let stable_path = canonicalize_existing_path(path).unwrap_or_else(|| path.to_path_buf());
214+
stable_path
215+
.strip_prefix(cwd)
216+
.unwrap_or(&stable_path)
217+
.to_string_lossy()
218+
.to_string()
219+
}
220+
221+
/// Normalize common path-bearing compiler flags for a zccache CWD.
222+
pub fn normalize_flags_for_compile_cwd(flags: &[String], cwd: &Path) -> Vec<String> {
223+
let mut normalized = Vec::with_capacity(flags.len());
224+
let mut next_is_path = false;
225+
226+
for flag in flags {
227+
if next_is_path {
228+
normalized.push(path_arg_for_compile_cwd(Path::new(flag), cwd));
229+
next_is_path = false;
230+
continue;
231+
}
232+
233+
if flag_takes_path_argument(flag) {
234+
normalized.push(flag.clone());
235+
next_is_path = true;
236+
continue;
237+
}
238+
239+
if let Some(value) = flag.strip_prefix("--sysroot=") {
240+
normalized.push(format!(
241+
"--sysroot={}",
242+
path_arg_for_compile_cwd(Path::new(value), cwd)
243+
));
244+
continue;
245+
}
246+
247+
if let Some((prefix, value)) = split_joined_path_flag(flag) {
248+
normalized.push(format!(
249+
"{}{}",
250+
prefix,
251+
path_arg_for_compile_cwd(Path::new(value), cwd)
252+
));
253+
continue;
254+
}
255+
256+
normalized.push(flag.clone());
257+
}
258+
259+
normalized
260+
}
261+
262+
fn canonicalize_existing_path(path: &Path) -> Option<PathBuf> {
263+
if let Ok(canonical) = path.canonicalize() {
264+
return Some(canonical);
265+
}
266+
267+
let parent = path.parent()?.canonicalize().ok()?;
268+
Some(match path.file_name() {
269+
Some(name) => parent.join(name),
270+
None => parent,
271+
})
272+
}
273+
274+
fn flag_takes_path_argument(flag: &str) -> bool {
275+
matches!(
276+
flag,
277+
"-I" | "-isystem"
278+
| "-iquote"
279+
| "-idirafter"
280+
| "-include"
281+
| "-imacros"
282+
| "-isysroot"
283+
| "--sysroot"
284+
)
285+
}
286+
287+
fn split_joined_path_flag(flag: &str) -> Option<(&'static str, &str)> {
288+
for prefix in [
289+
"-I",
290+
"-isystem",
291+
"-iquote",
292+
"-idirafter",
293+
"-include",
294+
"-imacros",
295+
"-isysroot",
296+
] {
297+
if let Some(value) = flag.strip_prefix(prefix).filter(|value| !value.is_empty()) {
298+
return Some((prefix, value));
299+
}
300+
}
301+
None
302+
}
303+
200304
/// Ask zccache whether the watched root changed since the last successful mark.
201305
///
202306
/// Exit code semantics come from `zccache fp check`:
@@ -287,4 +391,63 @@ mod tests {
287391

288392
assert!(compile_cwd_from_output(output).is_none());
289393
}
394+
395+
#[test]
396+
fn compile_cwd_from_output_canonicalizes_existing_workspace() {
397+
let tmp = tempfile::TempDir::new().unwrap();
398+
let workspace = tmp.path().join("project");
399+
let output = workspace.join(".fbuild/build/main.o");
400+
std::fs::create_dir_all(output.parent().unwrap()).unwrap();
401+
let expected = workspace.canonicalize().unwrap();
402+
403+
assert_eq!(
404+
compile_cwd_from_output(&output).as_deref(),
405+
Some(expected.as_path())
406+
);
407+
}
408+
409+
#[test]
410+
fn path_arg_for_compile_cwd_returns_workspace_relative_path() {
411+
let tmp = tempfile::TempDir::new().unwrap();
412+
let cwd = tmp.path().join("project");
413+
let source = cwd.join("src/main.cpp");
414+
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
415+
std::fs::write(&source, "int main() { return 0; }\n").unwrap();
416+
let cwd = cwd.canonicalize().unwrap();
417+
let expected = Path::new("src")
418+
.join("main.cpp")
419+
.to_string_lossy()
420+
.to_string();
421+
422+
assert_eq!(path_arg_for_compile_cwd(&source, &cwd), expected);
423+
}
424+
425+
#[test]
426+
fn normalize_flags_for_compile_cwd_rewrites_include_paths() {
427+
let tmp = tempfile::TempDir::new().unwrap();
428+
let cwd = tmp.path().join("project");
429+
let include = cwd.join("include");
430+
let vendor = cwd.join("vendor");
431+
let sysroot = cwd.join("sysroot");
432+
std::fs::create_dir_all(&include).unwrap();
433+
std::fs::create_dir_all(&vendor).unwrap();
434+
std::fs::create_dir_all(&sysroot).unwrap();
435+
let cwd = cwd.canonicalize().unwrap();
436+
let flags = vec![
437+
"-I".to_string(),
438+
include.to_string_lossy().to_string(),
439+
format!("-I{}", vendor.display()),
440+
format!("--sysroot={}", sysroot.display()),
441+
];
442+
443+
assert_eq!(
444+
normalize_flags_for_compile_cwd(&flags, &cwd),
445+
vec![
446+
"-I".to_string(),
447+
"include".to_string(),
448+
"-Ivendor".to_string(),
449+
"--sysroot=sysroot".to_string(),
450+
]
451+
);
452+
}
290453
}

crates/fbuild-build/tests/zccache_hit_across_workspace_rename.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ fn zccache_hit_across_workspace_rename() {
217217

218218
create_workspace(&ws_a);
219219
create_workspace(&ws_b);
220+
let expected_ws_a = cwd_display_path(&ws_a);
221+
let expected_ws_b = cwd_display_path(&ws_b);
220222

221223
let _cwd = CurrentDirGuard::set_to(tmp.path());
222224
env::set_var("FBUILD_FAKE_ZCCACHE_CACHE", &cache_dir);
@@ -237,15 +239,24 @@ fn zccache_hit_across_workspace_rename() {
237239
"renamed workspace should reuse the cache entry:\n{log}"
238240
);
239241
assert!(
240-
lines[0].contains(&format!("cwd={}", ws_a.display())),
242+
lines[0].contains(&format!("cwd={expected_ws_a}")),
241243
"first wrapper CWD should be workspace root:\n{log}"
242244
);
243245
assert!(
244-
lines[1].contains(&format!("cwd={}", ws_b.display())),
246+
lines[1].contains(&format!("cwd={expected_ws_b}")),
245247
"second wrapper CWD should be workspace root:\n{log}"
246248
);
247249
}
248250

251+
fn cwd_display_path(path: &Path) -> String {
252+
let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
253+
let display = path.display().to_string();
254+
display
255+
.strip_prefix(r"\\?\")
256+
.unwrap_or(&display)
257+
.to_string()
258+
}
259+
249260
fn compile_fake_zccache(root: &Path) -> PathBuf {
250261
let source = root.join("fake_zccache.rs");
251262
let exe = root.join(format!("fake-zccache{}", env::consts::EXE_SUFFIX));

0 commit comments

Comments
 (0)