@@ -30,7 +30,9 @@ use socket_patch_core::api::client::{get_api_client_with_overrides, ApiClient};
3030use socket_patch_core:: constants:: SOCKET_DIR ;
3131use socket_patch_core:: manifest:: schema:: { PatchManifest , PatchRecord } ;
3232use socket_patch_core:: patch:: copy_tree:: remove_tree;
33- use socket_patch_core:: utils:: fs:: read_regular_to_string;
33+ use socket_patch_core:: utils:: fs:: {
34+ atomic_write_bytes_preserving_mode, read_regular_to_bytes, read_regular_to_string,
35+ } ;
3436use socket_patch_core:: utils:: purl:: normalize_purl;
3537use socket_patch_core:: vendor:: {
3638 self , artifact_is_file_shaped, check_vendored_artifact, load_state, parse_vendor_path,
@@ -800,9 +802,11 @@ impl VendoredBackend<'_> {
800802 }
801803 }
802804 if !retry. is_empty ( ) {
803- scratch
804- . events
805- . retain ( |e| !retry. iter ( ) . any ( |c| e. purl . as_deref ( ) == Some ( c. purl . as_str ( ) ) ) ) ;
805+ scratch. events . retain ( |e| {
806+ !retry
807+ . iter ( )
808+ . any ( |c| e. purl . as_deref ( ) == Some ( c. purl . as_str ( ) ) )
809+ } ) ;
806810 no_source |= run_engine (
807811 & engine_common,
808812 None ,
@@ -1046,9 +1050,18 @@ async fn run_engine(
10461050 no_source
10471051}
10481052
1049- /// The bytes of each candidate's recorded wiring files — what a re-vendor
1050- /// may rewrite for it. `None` records a file that did not exist.
1051- async fn snapshot_wiring ( cwd : & Path , candidates : & [ Candidate ] ) -> Vec < ( PathBuf , Option < Vec < u8 > > ) > {
1053+ /// One wiring file as it was before the re-vendor: its bytes (`None`: it
1054+ /// did not exist) and, for a symlinked lockfile, the link text — lockfile
1055+ /// writers rename over the path, which replaces a link with a file.
1056+ struct WiringSnapshot {
1057+ path : PathBuf ,
1058+ link : Option < PathBuf > ,
1059+ bytes : Option < Vec < u8 > > ,
1060+ }
1061+
1062+ /// Snapshot each candidate's recorded wiring files — what a re-vendor may
1063+ /// rewrite for it. Reads are FIFO-safe; a non-regular file is skipped.
1064+ async fn snapshot_wiring ( cwd : & Path , candidates : & [ Candidate ] ) -> Vec < WiringSnapshot > {
10521065 let mut rels: Vec < String > = Vec :: new ( ) ;
10531066 for c in candidates {
10541067 rels. extend ( c. entry . wiring . iter ( ) . map ( |w| w. file . clone ( ) ) ) ;
@@ -1058,15 +1071,13 @@ async fn snapshot_wiring(cwd: &Path, candidates: &[Candidate]) -> Vec<(PathBuf,
10581071 let mut out = Vec :: with_capacity ( rels. len ( ) ) ;
10591072 for rel in rels {
10601073 let path = cwd. join ( & rel) ;
1061- match tokio:: fs:: symlink_metadata ( & path) . await {
1062- Ok ( meta) if meta. is_file ( ) => {
1063- if let Ok ( bytes) = tokio:: fs:: read ( & path) . await {
1064- out. push ( ( path, Some ( bytes) ) ) ;
1065- }
1066- }
1067- Ok ( _) => { }
1068- Err ( _) => out. push ( ( path, None ) ) ,
1069- }
1074+ let bytes = match read_regular_to_bytes ( & path) . await {
1075+ Ok ( bytes) => Some ( bytes) ,
1076+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => None ,
1077+ Err ( _) => continue ,
1078+ } ;
1079+ let link = tokio:: fs:: read_link ( & path) . await . ok ( ) ;
1080+ out. push ( WiringSnapshot { path, link, bytes } ) ;
10701081 }
10711082 out
10721083}
@@ -1083,20 +1094,34 @@ fn keeps_identity(health: &ArtifactHealth) -> bool {
10831094}
10841095
10851096/// Undo what a re-vendor may have written for `c`: its recorded wiring
1086- /// files go back to their snapshotted bytes and its ledger entry to the
1097+ /// files go back to their snapshotted bytes (a replaced symlink is
1098+ /// re-linked and its target rewritten) and its ledger entry to the
10871099/// original. Other candidates' files are left alone.
1088- async fn undo_candidate ( cwd : & Path , c : & Candidate , snapshot : & [ ( PathBuf , Option < Vec < u8 > > ) ] ) {
1100+ async fn undo_candidate ( cwd : & Path , c : & Candidate , snapshot : & [ WiringSnapshot ] ) {
10891101 let wired: HashSet < PathBuf > = c. entry . wiring . iter ( ) . map ( |w| cwd. join ( & w. file ) ) . collect ( ) ;
1090- for ( path, bytes) in snapshot. iter ( ) . filter ( |( p, _) | wired. contains ( p) ) {
1091- if tokio:: fs:: read ( path) . await . ok ( ) . as_ref ( ) == bytes. as_ref ( ) {
1102+ for snap in snapshot. iter ( ) . filter ( |s| wired. contains ( & s. path ) ) {
1103+ if let Some ( link) = & snap. link {
1104+ if tokio:: fs:: read_link ( & snap. path ) . await . ok ( ) . as_ref ( ) != Some ( link) {
1105+ let _ = tokio:: fs:: remove_file ( & snap. path ) . await ;
1106+ if relink ( link, & snap. path ) . await . is_err ( ) {
1107+ continue ;
1108+ }
1109+ }
1110+ }
1111+ if read_regular_to_bytes ( & snap. path ) . await . ok ( ) == snap. bytes {
10921112 continue ;
10931113 }
1094- match bytes {
1114+ match & snap. bytes {
1115+ // Staged, fsynced and renamed over the link's target (or the
1116+ // file itself), keeping its mode.
10951117 Some ( bytes) => {
1096- let _ = tokio:: fs:: write ( path, bytes) . await ;
1118+ let target = tokio:: fs:: canonicalize ( & snap. path )
1119+ . await
1120+ . unwrap_or_else ( |_| snap. path . clone ( ) ) ;
1121+ let _ = atomic_write_bytes_preserving_mode ( & target, bytes) . await ;
10971122 }
10981123 None => {
1099- let _ = tokio:: fs:: remove_file ( path) . await ;
1124+ let _ = tokio:: fs:: remove_file ( & snap . path ) . await ;
11001125 }
11011126 }
11021127 }
@@ -1108,6 +1133,16 @@ async fn undo_candidate(cwd: &Path, c: &Candidate, snapshot: &[(PathBuf, Option<
11081133 }
11091134}
11101135
1136+ #[ cfg( unix) ]
1137+ async fn relink ( link : & Path , at : & Path ) -> std:: io:: Result < ( ) > {
1138+ tokio:: fs:: symlink ( link, at) . await
1139+ }
1140+
1141+ #[ cfg( windows) ]
1142+ async fn relink ( link : & Path , at : & Path ) -> std:: io:: Result < ( ) > {
1143+ tokio:: fs:: symlink_file ( link, at) . await
1144+ }
1145+
11111146/// One candidate's outcome in the engine's scratch envelope.
11121147struct EngineOutcome {
11131148 /// The engine vendored it (`applied`).
0 commit comments