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        } else {
50            // Hardlink first (free); copy if the FS refuses (other volume).
51            if std::fs::hard_link(&from, &to).is_err() {
52                std::fs::copy(&from, &to).map_err(Error::io(&to))?;
53            }
54        }
55    }
56    Ok(())
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn clones_files_dirs_symlinks_and_exec_bits() {
65        let tmp = tempfile::tempdir().expect("tmp");
66        let src = tmp.path().join("src");
67        std::fs::create_dir_all(src.join("sub")).expect("mkdir");
68        std::fs::write(src.join("a.txt"), b"hello").expect("write");
69        std::fs::write(src.join("sub/tool"), b"#!/bin/sh\n").expect("write");
70        #[cfg(unix)]
71        {
72            use std::os::unix::fs::PermissionsExt as _;
73            std::fs::set_permissions(src.join("sub/tool"), std::fs::Permissions::from_mode(0o755))
74                .expect("chmod");
75            std::os::unix::fs::symlink("a.txt", src.join("link")).expect("ln");
76        }
77
78        let dst = tmp.path().join("dst/pkg");
79        clone_tree(&src, &dst).expect("clone");
80        assert_eq!(std::fs::read(dst.join("a.txt")).expect("read"), b"hello");
81        #[cfg(unix)]
82        {
83            use std::os::unix::fs::PermissionsExt as _;
84            let mode = std::fs::metadata(dst.join("sub/tool"))
85                .expect("meta")
86                .permissions()
87                .mode();
88            assert_eq!(mode & 0o111, 0o111, "executable bit lost on clone");
89            assert!(dst
90                .join("link")
91                .symlink_metadata()
92                .expect("meta")
93                .file_type()
94                .is_symlink());
95        }
96        // Modifying the clone does not touch the source (CoW or hardlink:
97        // we replace the file, we do not edit it in place).
98        std::fs::write(dst.join("a.txt"), b"changed").expect("write");
99        #[cfg(target_os = "macos")]
100        assert_eq!(std::fs::read(src.join("a.txt")).expect("read"), b"hello");
101    }
102}