Skip to main content

speccoder_runtime/
seed.rs

1//! A new lane's build folders, cloned from its feature's worktree.
2//!
3//! Where the feature's worktree has `target/` or `node_modules/` for the
4//! same lockfile, they are cloned copy-on-write (APFS `clonefile`, a
5//! reflink on Btrfs or XFS): the lane's own files, nothing linked. Where
6//! the filesystem cannot clone, nothing is copied. Cargo keys a build by
7//! its path, so only the dependencies are reused. `.venv/` is never cloned
8//! (it holds absolute paths).
9
10use std::path::Path;
11use std::process::Command;
12
13use crate::prepare::{self, Toolchain};
14
15/// Clones what `to` (a lane, just made) can take from `from` (its
16/// feature's worktree, idle while lanes are set up): `target/` beside an
17/// equal `Cargo.lock`, and each prepared Node folder whose lockfile is
18/// equal, recorded as prepared so the lane does not install it again.
19/// Returns the folders cloned, project-relative.
20pub 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        // Not recorded: the lane installs as before, over the clone.
43        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
52/// Clones the folder `src` to `dst` copy-on-write: only a directory of
53/// `from_root` reached without a link, only to a `dst` of `to_root` that
54/// does not exist. Links inside are copied as links, never followed. A
55/// clone that fails leaves nothing behind (one a crash cut short goes with
56/// the lane, which is made afresh every time).
57fn 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    // `-c`: clonefile(2), refused where the volume cannot clone.
74    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    // `--reflink=always`: refused where the filesystem cannot reflink.
85    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    /// A feature worktree and a lane beside it, both with `files`.
105    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        // macOS temporary folders are on APFS, which clones.
153        #[cfg(target_os = "macos")]
154        assert!(!done.is_empty(), "nothing was cloned on APFS");
155        if done.is_empty() {
156            // A filesystem that cannot clone: nothing is copied.
157            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        // The lane's own files: a write there leaves the feature's alone.
168        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        // Recorded as prepared for its lockfile; uv's folder is never cloned.
181        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}