@@ -2865,7 +2865,7 @@ fn compile_source_in(cache_dir: &Path, src: &str) -> Result<EmittedModule, Strin
28652865 if !so_path. exists ( ) {
28662866 // Identical sources hash to the same `so_path`, so a `cc -o so_path`
28672867 // from one thread can be dlopened half-written by another. Compile to a
2868- // unique temp, then `rename` (atomic within the dir) to publish.
2868+ // unique temp, then `rename`/`mv` (atomic within the dir) to publish.
28692869 use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
28702870 static TMP_CTR : AtomicU64 = AtomicU64 :: new ( 0 ) ;
28712871 let uniq = format ! (
@@ -2878,14 +2878,54 @@ fn compile_source_in(cache_dir: &Path, src: &str) -> Result<EmittedModule, Strin
28782878 let tmp_so = cache_dir. join ( format ! ( "veryl_aot_{hash}.{uniq}.so" ) ) ;
28792879 std:: fs:: write ( & tmp_c, src) . map_err ( |e| format ! ( "write {}: {}" , tmp_c. display( ) , e) ) ?;
28802880
2881- let mut cmd = Command :: new ( & cc_name) ;
2882- cmd. args ( & flags) . arg ( "-o" ) . arg ( & tmp_so) . arg ( & tmp_c) ;
2883-
2884- let out = cmd
2885- . output ( )
2886- . map_err ( |e| format ! ( "spawn cc: {e} (set VERYL_AOT_CC to override)" ) ) ?;
2881+ // The compile AND the publish run through one shell so the cache
2882+ // entry lands even when this process exits first: a short run
2883+ // finishes before cc does and the pool worker dies with it, so a
2884+ // rename on the Rust side would discard the orphaned cc's output
2885+ // and leave every rerun on the JIT path. Orphan reparenting keeps
2886+ // the shell running. Positional parameters keep the paths out of
2887+ // shell-quoting territory.
2888+ #[ cfg( unix) ]
2889+ let out = {
2890+ let mut cmd = Command :: new ( "/bin/sh" ) ;
2891+ cmd. arg ( "-c" )
2892+ . arg (
2893+ r#"cc="$1"; tso="$2"; tc="$3"; pc="$4"; pso="$5"; shift 5; "$cc" "$@" -o "$tso" "$tc" || { rm -f "$tso"; exit 1; }; mv -f "$tc" "$pc"; mv -f "$tso" "$pso""# ,
2894+ )
2895+ . arg ( "sh" )
2896+ . arg ( & cc_name)
2897+ . arg ( & tmp_so)
2898+ . arg ( & tmp_c)
2899+ . arg ( & c_path)
2900+ . arg ( & so_path)
2901+ . args ( & flags) ;
2902+ // Own process group: a group-delivered signal (Ctrl-C on the
2903+ // run, a harness killing its group) must not take the publish
2904+ // down with it.
2905+ {
2906+ use std:: os:: unix:: process:: CommandExt ;
2907+ cmd. process_group ( 0 ) ;
2908+ }
2909+ cmd. output ( )
2910+ . map_err ( |e| format ! ( "spawn sh/cc: {e} (set VERYL_AOT_CC to override)" ) ) ?
2911+ } ;
2912+ #[ cfg( not( unix) ) ]
2913+ let out = {
2914+ let mut cmd = Command :: new ( & cc_name) ;
2915+ cmd. args ( & flags) . arg ( "-o" ) . arg ( & tmp_so) . arg ( & tmp_c) ;
2916+ let out = cmd
2917+ . output ( )
2918+ . map_err ( |e| format ! ( "spawn cc: {e} (set VERYL_AOT_CC to override)" ) ) ?;
2919+ if out. status . success ( ) {
2920+ // A racing peer publishes an equally valid file (same
2921+ // source), so an overwrite either way is fine.
2922+ let _ = std:: fs:: rename ( & tmp_c, & c_path) ;
2923+ std:: fs:: rename ( & tmp_so, & so_path)
2924+ . map_err ( |e| format ! ( "rename {}: {}" , tmp_so. display( ) , e) ) ?;
2925+ }
2926+ out
2927+ } ;
28872928 if !out. status . success ( ) {
2888- let _ = std:: fs:: remove_file ( & tmp_so) ;
28892929 // Leave the temp .c for inspection.
28902930 return Err ( format ! (
28912931 "cc {} failed: {}\n {}" ,
@@ -2894,11 +2934,6 @@ fn compile_source_in(cache_dir: &Path, src: &str) -> Result<EmittedModule, Strin
28942934 String :: from_utf8_lossy( & out. stderr) ,
28952935 ) ) ;
28962936 }
2897- // A racing peer publishes an equally valid file (same source), so an
2898- // overwrite either way is fine.
2899- let _ = std:: fs:: rename ( & tmp_c, & c_path) ;
2900- std:: fs:: rename ( & tmp_so, & so_path)
2901- . map_err ( |e| format ! ( "rename {}: {}" , tmp_so. display( ) , e) ) ?;
29022937 }
29032938
29042939 // SAFETY: the .so was just compiled by us (or previously cached) and
@@ -7637,6 +7672,66 @@ mod tests {
76377672 /// Compile `src` end-to-end; return `None` when the built `.so`
76387673 /// can't load on this host (e.g. cross-arch `cc` on Windows-on-ARM).
76397674 /// Genuine compile failures still panic.
7675+ #[ test]
7676+ #[ cfg( unix) ]
7677+ fn aot_cache_publish_survives_process_exit ( ) {
7678+ // A short run exits while cc is still going, so the publish must not
7679+ // depend on this process surviving to rename the temp files. The
7680+ // test re-executes itself as a child that starts one compile and
7681+ // exits; the parent then waits for the artifact to appear.
7682+ const CHILD_DIR : & str = "VERYL_TEST_AOT_PUBLISH_DIR" ;
7683+ if let Ok ( dir) = std:: env:: var ( CHILD_DIR ) {
7684+ let dir = PathBuf :: from ( dir) ;
7685+ std:: thread:: spawn ( move || {
7686+ let _ = compile_source_in ( & dir, "// AOT-C publish probe\n " ) ;
7687+ } ) ;
7688+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 200 ) ) ;
7689+ std:: process:: exit ( 0 ) ;
7690+ }
7691+
7692+ let tmp = std:: env:: temp_dir ( ) . join ( format ! ( "veryl_aot_pub_{}" , std:: process:: id( ) ) ) ;
7693+ let _ = std:: fs:: remove_dir_all ( & tmp) ;
7694+ std:: fs:: create_dir_all ( & tmp) . unwrap ( ) ;
7695+ // A cc that outlives the child before writing its output.
7696+ let slow_cc = tmp. join ( "slow_cc.sh" ) ;
7697+ std:: fs:: write (
7698+ & slow_cc,
7699+ "#!/bin/sh\n sleep 2\n while [ $# -gt 0 ]; do [ \" $1\" = -o ] && out=$2; shift; done\n : > \" $out\" \n " ,
7700+ )
7701+ . unwrap ( ) ;
7702+ use std:: os:: unix:: fs:: PermissionsExt ;
7703+ std:: fs:: set_permissions ( & slow_cc, std:: fs:: Permissions :: from_mode ( 0o755 ) ) . unwrap ( ) ;
7704+
7705+ let status = Command :: new ( std:: env:: current_exe ( ) . unwrap ( ) )
7706+ . arg ( "aot_cache_publish_survives_process_exit" )
7707+ . env ( CHILD_DIR , & tmp)
7708+ . env ( "VERYL_AOT_CC" , & slow_cc)
7709+ . stdout ( std:: process:: Stdio :: null ( ) )
7710+ . stderr ( std:: process:: Stdio :: null ( ) )
7711+ . status ( )
7712+ . unwrap ( ) ;
7713+ assert ! ( status. success( ) , "child run failed" ) ;
7714+
7715+ // Published artifacts are `veryl_aot_<hash>.so`; the temps carry an
7716+ // extra `.<pid>.<n>` and must not count.
7717+ let published = || {
7718+ std:: fs:: read_dir ( & tmp)
7719+ . into_iter ( )
7720+ . flatten ( )
7721+ . flatten ( )
7722+ . any ( |e| {
7723+ let n = e. file_name ( ) . to_string_lossy ( ) . into_owned ( ) ;
7724+ n. starts_with ( "veryl_aot_" ) && n. ends_with ( ".so" ) && n. matches ( '.' ) . count ( ) == 1
7725+ } )
7726+ } ;
7727+ let deadline = std:: time:: Instant :: now ( ) + std:: time:: Duration :: from_secs ( 30 ) ;
7728+ while !published ( ) && std:: time:: Instant :: now ( ) < deadline {
7729+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
7730+ }
7731+ assert ! ( published( ) , "the .so must publish after the run exits" ) ;
7732+ let _ = std:: fs:: remove_dir_all ( & tmp) ;
7733+ }
7734+
76407735 fn compile_for_test ( cache_dir : & Path , src : & str , what : & str ) -> Option < EmittedModule > {
76417736 match compile_source_in ( cache_dir, src) {
76427737 Ok ( m) => Some ( m) ,
0 commit comments