@@ -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}
0 commit comments