Skip to main content

vivacity_core/
clone.rs

1//! Clone of a store tree into vendor/, the hot path of the install.
2//! macOS/APFS: `clonefile(2)` of the whole directory (one syscall, copy-on-write,
3//! measured 8x faster than extraction in M0). Elsewhere, or if clonefile
4//! fails (other FS, different volume): recursive walk with hardlinks (pnpm
5//! model), and a real copy as a last resort. Always towards an ABSENT
6//! destination (the caller removes the previous version first), so no mixed
7//! states.
8
9use crate::error::{Error, Result};
10use std::path::Path;
11
12pub fn clone_tree(src: &Path, dst: &Path) -> Result<()> {
13    if let Some(parent) = dst.parent() {
14        std::fs::create_dir_all(parent).map_err(Error::io(parent))?;
15    }
16
17    #[cfg(target_os = "macos")]
18    {
19        use std::os::unix::ffi::OsStrExt as _;
20        let c_src = std::ffi::CString::new(src.as_os_str().as_bytes());
21        let c_dst = std::ffi::CString::new(dst.as_os_str().as_bytes());
22        if let (Ok(c_src), Ok(c_dst)) = (c_src, c_dst) {
23            // SAFETY: FFI call to clonefile through libc, two valid C strings,
24            // no shared memory.
25            let rc = unsafe { libc::clonefile(c_src.as_ptr(), c_dst.as_ptr(), 0) };
26            if rc == 0 {
27                return Ok(());
28            }
29            // Failure (non-APFS FS, different volumes...): fall through to the next strategies.
30        }
31    }
32
33    link_or_copy_tree(src, dst)
34}
35
36fn link_or_copy_tree(src: &Path, dst: &Path) -> Result<()> {
37    std::fs::create_dir_all(dst).map_err(Error::io(dst))?;
38    for entry in std::fs::read_dir(src).map_err(Error::io(src))? {
39        let entry = entry.map_err(Error::io(src))?;
40        let from = entry.path();
41        let to = dst.join(entry.file_name());
42        let ftype = entry.file_type().map_err(Error::io(&from))?;
43        if ftype.is_dir() {
44            link_or_copy_tree(&from, &to)?;
45        } else if ftype.is_symlink() {
46            let target = std::fs::read_link(&from).map_err(Error::io(&from))?;
47            #[cfg(unix)]
48            std::os::unix::fs::symlink(&target, &to).map_err(Error::io(&to))?;
49            #[cfg(windows)]
50            clone_symlink_windows(&from, &target, &to)?;
51            #[cfg(not(any(unix, windows)))]
52            {
53                let _ = target;
54                std::fs::copy(&from, &to).map_err(Error::io(&to))?;
55            }
56        } else {
57            // Hardlink first (free); copy if the FS refuses (other volume).
58            if std::fs::hard_link(&from, &to).is_err() {
59                std::fs::copy(&from, &to).map_err(Error::io(&to))?;
60            }
61        }
62    }
63    Ok(())
64}
65
66/// Windows: creating symlinks requires a privilege (Developer Mode or
67/// SeCreateSymbolicLinkPrivilege). We try the real symlink, and failing that
68/// copy the RESOLVED content — the resulting vendor/ is functional but no
69/// longer a link (a parity divergence documented in docs/windows.md). A
70/// dangling link fails loudly instead of vanishing silently.
71#[cfg(windows)]
72fn clone_symlink_windows(from: &Path, target: &Path, to: &Path) -> Result<()> {
73    let is_dir = std::fs::metadata(from).map(|m| m.is_dir()).unwrap_or(false);
74    let made = if is_dir {
75        std::os::windows::fs::symlink_dir(target, to)
76    } else {
77        std::os::windows::fs::symlink_file(target, to)
78    };
79    if made.is_ok() {
80        return Ok(());
81    }
82    if is_dir {
83        link_or_copy_tree(from, to) // read_dir follows the link
84    } else {
85        if std::fs::hard_link(from, to).is_err() {
86            std::fs::copy(from, to).map_err(Error::io(to))?;
87        }
88        Ok(())
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn clones_files_dirs_symlinks_and_exec_bits() {
98        let tmp = tempfile::tempdir().expect("tmp");
99        let src = tmp.path().join("src");
100        std::fs::create_dir_all(src.join("sub")).expect("mkdir");
101        std::fs::write(src.join("a.txt"), b"hello").expect("write");
102        std::fs::write(src.join("sub/tool"), b"#!/bin/sh\n").expect("write");
103        #[cfg(unix)]
104        {
105            use std::os::unix::fs::PermissionsExt as _;
106            std::fs::set_permissions(src.join("sub/tool"), std::fs::Permissions::from_mode(0o755))
107                .expect("chmod");
108            std::os::unix::fs::symlink("a.txt", src.join("link")).expect("ln");
109        }
110        // Windows: the fixture's link requires Developer Mode or
111        // SeCreateSymbolicLinkPrivilege — without it we cannot build the
112        // fixture, so that portion is announced and then skipped (the copy
113        // fallback of clone_symlink_windows cannot be forced from here).
114        #[cfg(windows)]
115        let with_link = match std::os::windows::fs::symlink_file("a.txt", src.join("link")) {
116            Ok(()) => true,
117            Err(e) => {
118                eprintln!("symlink refused on this host ({e}) — link portion not exercised");
119                false
120            }
121        };
122
123        let dst = tmp.path().join("dst/pkg");
124        clone_tree(&src, &dst).expect("clone");
125        assert_eq!(std::fs::read(dst.join("a.txt")).expect("read"), b"hello");
126        #[cfg(unix)]
127        {
128            use std::os::unix::fs::PermissionsExt as _;
129            let mode = std::fs::metadata(dst.join("sub/tool"))
130                .expect("meta")
131                .permissions()
132                .mode();
133            assert_eq!(mode & 0o111, 0o111, "executable bit lost on clone");
134            assert!(dst
135                .join("link")
136                .symlink_metadata()
137                .expect("meta")
138                .file_type()
139                .is_symlink());
140        }
141        #[cfg(windows)]
142        if with_link {
143            // Never a lost entry: either a real link (privilege present —
144            // the same one that allowed the fixture), or a copy of the
145            // resolved content; in both cases reading yields the target's
146            // content.
147            let meta = dst.join("link").symlink_metadata().expect("entry lost");
148            assert!(
149                meta.file_type().is_symlink() || meta.file_type().is_file(),
150                "neither link nor file"
151            );
152            assert_eq!(std::fs::read(dst.join("link")).expect("read"), b"hello");
153        }
154        // Modifying the clone does not touch the source (CoW or hardlink:
155        // we replace the file, we do not edit it in place).
156        std::fs::write(dst.join("a.txt"), b"changed").expect("write");
157        #[cfg(target_os = "macos")]
158        assert_eq!(std::fs::read(src.join("a.txt")).expect("read"), b"hello");
159    }
160}