1use 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 let rc = unsafe { libc::clonefile(c_src.as_ptr(), c_dst.as_ptr(), 0) };
26 if rc == 0 {
27 return Ok(());
28 }
29 }
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 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 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}