1use std::path::Path;
11use std::process::Command;
12
13use crate::prepare::{self, Toolchain};
14
15pub fn seed(from_root: &Path, from: &Path, to_root: &Path, to: &Path) -> Vec<String> {
21 let mut done = Vec::new();
22 if same_file(&from.join("Cargo.lock"), &to.join("Cargo.lock"))
23 && clone(from_root, &from.join("target"), to_root, &to.join("target"))
24 {
25 done.push("target".to_owned());
26 }
27 let mut record = prepare::Record::default();
28 for e in prepare::load(from_root).roots {
29 let lockfile = to.join(&e.lockfile);
30 if e.toolchain == Toolchain::Uv
31 || prepare::sha256_file(&lockfile).as_deref() != Some(e.lock_sha256.as_str())
32 {
33 continue;
34 }
35 let folder = e.folder();
36 if clone(from_root, &from.join(&folder), to_root, &to.join(&folder)) {
37 done.push(folder);
38 record.roots.push(e);
39 }
40 }
41 if !record.roots.is_empty() && prepare::store(to_root, &record).is_err() {
42 prepare::forget(to_root);
44 }
45 done
46}
47
48fn same_file(a: &Path, b: &Path) -> bool {
49 matches!((prepare::sha256_file(a), prepare::sha256_file(b)), (Some(x), Some(y)) if x == y)
50}
51
52fn clone(from_root: &Path, src: &Path, to_root: &Path, dst: &Path) -> bool {
58 if !prepare::holds(from_root, src)
59 || prepare::confined(to_root, dst).is_err()
60 || std::fs::symlink_metadata(dst).is_ok()
61 {
62 return false;
63 }
64 let cloned = cow_copy(src, dst);
65 if !cloned {
66 let _ = std::fs::remove_dir_all(dst);
67 }
68 cloned
69}
70
71#[cfg(target_os = "macos")]
72fn cow_copy(src: &Path, dst: &Path) -> bool {
73 Command::new("/bin/cp")
75 .arg("-cR")
76 .arg(src)
77 .arg(dst)
78 .output()
79 .is_ok_and(|o| o.status.success())
80}
81
82#[cfg(target_os = "linux")]
83fn cow_copy(src: &Path, dst: &Path) -> bool {
84 Command::new("cp")
86 .args(["-R", "--no-dereference", "--reflink=always"])
87 .arg(src)
88 .arg(dst)
89 .output()
90 .is_ok_and(|o| o.status.success())
91}
92
93#[cfg(not(any(target_os = "macos", target_os = "linux")))]
94fn cow_copy(_: &Path, _: &Path) -> bool {
95 false
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use std::fs;
102 use std::path::PathBuf;
103
104 fn pair(files: &[(&str, &str)]) -> (tempfile::TempDir, PathBuf, PathBuf) {
106 let d = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
107 let (from, to) = (d.path().join("feature"), d.path().join("lane"));
108 for root in [&from, &to] {
109 for (p, t) in files {
110 let f = root.join(p);
111 fs::create_dir_all(f.parent().unwrap_or(root)).ok();
112 fs::write(f, t).ok();
113 }
114 }
115 (d, from, to)
116 }
117
118 fn entry(toolchain: Toolchain, lockfile: &str, text: &str) -> prepare::Entry {
119 prepare::Entry {
120 dir: String::new(),
121 toolchain,
122 lockfile: lockfile.to_owned(),
123 lock_sha256: crate::integrity::sha256_hex(text.as_bytes()),
124 why: None,
125 at: 0,
126 }
127 }
128
129 #[test]
130 fn a_lane_gets_its_own_clone_of_the_build_folders_for_the_same_lockfiles() {
131 let (_d, from, to) = pair(&[
132 ("Cargo.lock", "v1"),
133 ("package-lock.json", "{}"),
134 ("uv.lock", "u"),
135 ]);
136 fs::create_dir_all(from.join("target/debug")).ok();
137 fs::write(from.join("target/debug/dep.rlib"), "built").ok();
138 fs::create_dir_all(from.join("node_modules/.bin")).ok();
139 fs::write(from.join("node_modules/left-pad.js"), "pad").ok();
140 #[cfg(unix)]
141 std::os::unix::fs::symlink("../left-pad.js", from.join("node_modules/.bin/pad")).ok();
142 fs::create_dir_all(from.join(".venv")).ok();
143 let record = prepare::Record {
144 roots: vec![
145 entry(Toolchain::Npm, "package-lock.json", "{}"),
146 entry(Toolchain::Uv, "uv.lock", "u"),
147 ],
148 };
149 prepare::store(&from, &record).unwrap_or_else(|e| panic!("{}", e.message));
150
151 let done = seed(&from, &from, &to, &to);
152 #[cfg(target_os = "macos")]
154 assert!(!done.is_empty(), "nothing was cloned on APFS");
155 if done.is_empty() {
156 assert!(!to.join("target").exists() && !to.join("node_modules").exists());
158 return;
159 }
160 assert_eq!(done, ["target", "node_modules"]);
161 assert_eq!(
162 fs::read_to_string(to.join("target/debug/dep.rlib"))
163 .ok()
164 .as_deref(),
165 Some("built")
166 );
167 fs::write(to.join("target/debug/dep.rlib"), "rebuilt").ok();
169 assert_eq!(
170 fs::read_to_string(from.join("target/debug/dep.rlib"))
171 .ok()
172 .as_deref(),
173 Some("built")
174 );
175 #[cfg(unix)]
176 assert!(
177 fs::symlink_metadata(to.join("node_modules/.bin/pad"))
178 .is_ok_and(|m| m.file_type().is_symlink())
179 );
180 assert!(!to.join(".venv").exists());
182 let lane = prepare::load(&to);
183 assert_eq!(lane.roots, vec![record.roots[0].clone()]);
184 }
185
186 #[test]
187 fn nothing_is_cloned_for_another_lockfile_over_a_folder_or_through_a_link() {
188 let (_d, from, to) = pair(&[("Cargo.lock", "v1")]);
189 fs::write(to.join("Cargo.lock"), "v2").ok();
190 fs::create_dir_all(from.join("target")).ok();
191 assert!(
192 seed(&from, &from, &to, &to).is_empty(),
193 "another Cargo.lock"
194 );
195
196 let (_d, from, to) = pair(&[("Cargo.lock", "v1")]);
197 fs::create_dir_all(from.join("target")).ok();
198 fs::create_dir_all(to.join("target")).ok();
199 fs::write(to.join("target/own"), "x").ok();
200 assert!(
201 seed(&from, &from, &to, &to).is_empty(),
202 "the lane has its own"
203 );
204 assert!(to.join("target/own").exists());
205
206 #[cfg(unix)]
207 {
208 let (d, from, to) = pair(&[("Cargo.lock", "v1")]);
209 fs::create_dir_all(d.path().join("elsewhere")).ok();
210 std::os::unix::fs::symlink(d.path().join("elsewhere"), from.join("target")).ok();
211 assert!(
212 seed(&from, &from, &to, &to).is_empty(),
213 "a link is not the worktree's own"
214 );
215 assert!(!to.join("target").exists());
216 }
217 }
218}