@@ -1127,6 +1127,26 @@ impl FileSystem for DiskFileSystem {
11271127 PathBuf :: from ( unix_to_sys ( target) . as_ref ( ) )
11281128 } ;
11291129 let full_path = full_path. into_owned ( ) ;
1130+
1131+ if old_content. is_some ( ) {
1132+ // Remove existing symlink before creating a new one. At least on Unix,
1133+ // symlink(2) fails with EEXIST if the link already exists instead of
1134+ // overwriting it
1135+ retry_blocking ( full_path. clone ( ) , |path| std:: fs:: remove_file ( path) )
1136+ . concurrency_limited ( & inner. write_semaphore )
1137+ . await
1138+ . or_else ( |err| {
1139+ if err. kind ( ) == ErrorKind :: NotFound {
1140+ Ok ( ( ) )
1141+ } else {
1142+ Err ( err)
1143+ }
1144+ } )
1145+ . with_context ( || {
1146+ anyhow ! ( "removing existing symlink {} failed" , full_path. display( ) )
1147+ } ) ?;
1148+ }
1149+
11301150 retry_blocking ( target_path, move |target_path| {
11311151 let _span = tracing:: info_span!(
11321152 "write symlink" ,
@@ -2915,7 +2935,114 @@ mod tests {
29152935 . unwrap ( ) ;
29162936 }
29172937
2918- // Test helpers for denied_path tests
2938+ #[ cfg( test) ]
2939+ mod symlink_tests {
2940+ use std:: {
2941+ fs:: { File , create_dir_all, read_to_string} ,
2942+ io:: Write ,
2943+ } ;
2944+
2945+ use turbo_rcstr:: { RcStr , rcstr} ;
2946+ use turbo_tasks:: { ResolvedVc , apply_effects} ;
2947+ use turbo_tasks_backend:: { BackendOptions , TurboTasksBackend , noop_backing_storage} ;
2948+
2949+ use crate :: { DiskFileSystem , FileSystem , FileSystemPath , LinkContent , LinkType } ;
2950+
2951+ #[ turbo_tasks:: function( operation) ]
2952+ async fn test_write_link_effect (
2953+ fs : ResolvedVc < DiskFileSystem > ,
2954+ path : FileSystemPath ,
2955+ target : RcStr ,
2956+ ) -> anyhow:: Result < ( ) > {
2957+ let write_file = |f| {
2958+ fs. write_link (
2959+ f,
2960+ LinkContent :: Link {
2961+ target : format ! ( "{target}/data.txt" ) . into ( ) ,
2962+ link_type : LinkType :: empty ( ) ,
2963+ }
2964+ . cell ( ) ,
2965+ )
2966+ } ;
2967+ // Write it twice (same content)
2968+ write_file ( path. join ( "symlink-file" ) ?) . await ?;
2969+ write_file ( path. join ( "symlink-file" ) ?) . await ?;
2970+
2971+ let write_dir = |f| {
2972+ fs. write_link (
2973+ f,
2974+ LinkContent :: Link {
2975+ target : target. clone ( ) ,
2976+ link_type : LinkType :: DIRECTORY ,
2977+ }
2978+ . cell ( ) ,
2979+ )
2980+ } ;
2981+ // Write it twice (same content)
2982+ write_dir ( path. join ( "symlink-dir" ) ?) . await ?;
2983+ write_dir ( path. join ( "symlink-dir" ) ?) . await ?;
2984+
2985+ Ok ( ( ) )
2986+ }
2987+
2988+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
2989+ async fn test_write_link ( ) {
2990+ let scratch = tempfile:: tempdir ( ) . unwrap ( ) ;
2991+ let path = scratch. path ( ) . to_owned ( ) ;
2992+
2993+ create_dir_all ( path. join ( "subdir-a" ) ) . unwrap ( ) ;
2994+ File :: create_new ( path. join ( "subdir-a/data.txt" ) )
2995+ . unwrap ( )
2996+ . write_all ( b"foo" )
2997+ . unwrap ( ) ;
2998+ create_dir_all ( path. join ( "subdir-b" ) ) . unwrap ( ) ;
2999+ File :: create_new ( path. join ( "subdir-b/data.txt" ) )
3000+ . unwrap ( )
3001+ . write_all ( b"bar" )
3002+ . unwrap ( ) ;
3003+ let root = path. to_str ( ) . unwrap ( ) . into ( ) ;
3004+
3005+ let tt = turbo_tasks:: TurboTasks :: new ( TurboTasksBackend :: new (
3006+ BackendOptions :: default ( ) ,
3007+ noop_backing_storage ( ) ,
3008+ ) ) ;
3009+
3010+ tt. run_once ( async move {
3011+ let fs = DiskFileSystem :: new ( rcstr ! ( "test" ) , root)
3012+ . to_resolved ( )
3013+ . await ?;
3014+ let root_path = fs. root ( ) . owned ( ) . await ?;
3015+
3016+ let write_result =
3017+ test_write_link_effect ( fs, root_path. clone ( ) , rcstr ! ( "subdir-a" ) ) ;
3018+ write_result. read_strongly_consistent ( ) . await ?;
3019+ apply_effects ( write_result) . await ?;
3020+
3021+ assert_eq ! ( read_to_string( path. join( "symlink-file" ) ) . unwrap( ) , "foo" ) ;
3022+ assert_eq ! (
3023+ read_to_string( path. join( "symlink-dir/data.txt" ) ) . unwrap( ) ,
3024+ "foo"
3025+ ) ;
3026+
3027+ // Write the same links again but with different targets
3028+ let write_result = test_write_link_effect ( fs, root_path, rcstr ! ( "subdir-b" ) ) ;
3029+ write_result. read_strongly_consistent ( ) . await ?;
3030+ apply_effects ( write_result) . await ?;
3031+
3032+ assert_eq ! ( read_to_string( path. join( "symlink-file" ) ) . unwrap( ) , "bar" ) ;
3033+ assert_eq ! (
3034+ read_to_string( path. join( "symlink-dir/data.txt" ) ) . unwrap( ) ,
3035+ "bar"
3036+ ) ;
3037+
3038+ anyhow:: Ok ( ( ) )
3039+ } )
3040+ . await
3041+ . unwrap ( ) ;
3042+ }
3043+ }
3044+
3045+ // Tests helpers for denied_path tests
29193046 #[ cfg( test) ]
29203047 mod denied_path_tests {
29213048 use std:: {
0 commit comments