Skip to main content

cargo_util/
paths.rs

1//! Various utilities for working with files and paths.
2
3use anyhow::{Context, Result};
4use filetime::FileTime;
5use std::env;
6use std::ffi::{OsStr, OsString};
7use std::fs::{self, File, Metadata, OpenOptions};
8use std::io;
9use std::io::prelude::*;
10use std::iter;
11use std::path::{Component, Path, PathBuf};
12use tempfile::Builder as TempFileBuilder;
13
14/// Joins paths into a string suitable for the `PATH` environment variable.
15///
16/// This is equivalent to [`std::env::join_paths`], but includes a more
17/// detailed error message. The given `env` argument is the name of the
18/// environment variable this is will be used for, which is included in the
19/// error message.
20pub fn join_paths<T: AsRef<OsStr>>(paths: &[T], env: &str) -> Result<OsString> {
21    env::join_paths(paths.iter()).with_context(|| {
22        let mut message = format!(
23            "failed to join paths from `${env}` together\n\n\
24             Check if any of path segments listed below contain an \
25             unterminated quote character or path separator:"
26        );
27        for path in paths {
28            use std::fmt::Write;
29            write!(&mut message, "\n    {:?}", Path::new(path)).unwrap();
30        }
31
32        message
33    })
34}
35
36/// Returns the name of the environment variable used for searching for
37/// dynamic libraries.
38pub fn dylib_path_envvar() -> &'static str {
39    if cfg!(windows) {
40        "PATH"
41    } else if cfg!(target_os = "macos") {
42        // When loading and linking a dynamic library or bundle, dlopen
43        // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and
44        // DYLD_FALLBACK_LIBRARY_PATH.
45        // In the Mach-O format, a dynamic library has an "install path."
46        // Clients linking against the library record this path, and the
47        // dynamic linker, dyld, uses it to locate the library.
48        // dyld searches DYLD_LIBRARY_PATH *before* the install path.
49        // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot
50        // find the library in the install path.
51        // Setting DYLD_LIBRARY_PATH can easily have unintended
52        // consequences.
53        //
54        // Also, DYLD_LIBRARY_PATH appears to have significant performance
55        // penalty starting in 10.13. Cargo's testsuite ran more than twice as
56        // slow with it on CI.
57        "DYLD_FALLBACK_LIBRARY_PATH"
58    } else if cfg!(target_os = "aix") {
59        "LIBPATH"
60    } else if cfg!(target_os = "haiku") {
61        "LIBRARY_PATH"
62    } else {
63        "LD_LIBRARY_PATH"
64    }
65}
66
67/// Returns a list of directories that are searched for dynamic libraries.
68///
69/// Note that some operating systems will have defaults if this is empty that
70/// will need to be dealt with.
71pub fn dylib_path() -> Vec<PathBuf> {
72    match env::var_os(dylib_path_envvar()) {
73        Some(var) => env::split_paths(&var).collect(),
74        None => Vec::new(),
75    }
76}
77
78/// Normalize a path, removing things like `.` and `..`.
79///
80/// CAUTION: This does not resolve symlinks (unlike
81/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
82/// behavior at times. This should be used carefully. Unfortunately,
83/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
84/// fail, or on Windows returns annoying device paths. This is a problem Cargo
85/// needs to improve on.
86pub fn normalize_path(path: &Path) -> PathBuf {
87    let mut components = path.components().peekable();
88    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
89        components.next();
90        PathBuf::from(c.as_os_str())
91    } else {
92        PathBuf::new()
93    };
94
95    for component in components {
96        match component {
97            Component::Prefix(..) => unreachable!(),
98            Component::RootDir => {
99                ret.push(Component::RootDir);
100            }
101            Component::CurDir => {}
102            Component::ParentDir => {
103                if ret.ends_with(Component::ParentDir) {
104                    ret.push(Component::ParentDir);
105                } else {
106                    let popped = ret.pop();
107                    if !popped && !ret.has_root() {
108                        ret.push(Component::ParentDir);
109                    }
110                }
111            }
112            Component::Normal(c) => {
113                ret.push(c);
114            }
115        }
116    }
117    ret
118}
119
120/// Returns the absolute path of where the given executable is located based
121/// on searching the `PATH` environment variable.
122///
123/// Returns an error if it cannot be found.
124pub fn resolve_executable(exec: &Path) -> Result<PathBuf> {
125    if exec.components().count() == 1 {
126        let paths = env::var_os("PATH").ok_or_else(|| anyhow::format_err!("no PATH"))?;
127        let candidates = env::split_paths(&paths).flat_map(|path| {
128            let candidate = path.join(&exec);
129            let with_exe = if env::consts::EXE_EXTENSION.is_empty() {
130                None
131            } else {
132                Some(candidate.with_extension(env::consts::EXE_EXTENSION))
133            };
134            iter::once(candidate).chain(with_exe)
135        });
136        for candidate in candidates {
137            if candidate.is_file() {
138                return Ok(candidate);
139            }
140        }
141
142        anyhow::bail!("no executable for `{}` found in PATH", exec.display())
143    } else {
144        Ok(exec.into())
145    }
146}
147
148/// Returns metadata for a file (follows symlinks).
149///
150/// Equivalent to [`std::fs::metadata`] with better error messages.
151pub fn metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
152    let path = path.as_ref();
153    std::fs::metadata(path)
154        .with_context(|| format!("failed to load metadata for path `{}`", path.display()))
155}
156
157/// Returns metadata for a file without following symlinks.
158///
159/// Equivalent to [`std::fs::metadata`] with better error messages.
160pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
161    let path = path.as_ref();
162    std::fs::symlink_metadata(path)
163        .with_context(|| format!("failed to load metadata for path `{}`", path.display()))
164}
165
166/// Reads a file to a string.
167///
168/// Equivalent to [`std::fs::read_to_string`] with better error messages.
169pub fn read(path: &Path) -> Result<String> {
170    match String::from_utf8(read_bytes(path)?) {
171        Ok(s) => Ok(s),
172        Err(_) => anyhow::bail!("path at `{}` was not valid utf-8", path.display()),
173    }
174}
175
176/// Reads a file into a bytes vector.
177///
178/// Equivalent to [`std::fs::read`] with better error messages.
179pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
180    fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))
181}
182
183/// Writes a file to disk.
184///
185/// Equivalent to [`std::fs::write`] with better error messages.
186pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
187    let path = path.as_ref();
188    fs::write(path, contents.as_ref())
189        .with_context(|| format!("failed to write `{}`", path.display()))
190}
191
192/// Writes a file to disk atomically.
193///
194/// This uses `tempfile::persist` to accomplish atomic writes.
195/// If the path is a symlink, it will follow the symlink and write to the actual target.
196pub fn write_atomic<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
197    let path = path.as_ref();
198
199    // Check if the path is a symlink and follow it if it is
200    let resolved_path;
201    let path = if path.is_symlink() {
202        let target = fs::read_link(path)
203            .with_context(|| format!("failed to read symlink at `{}`", path.display()))?;
204        resolved_path = path.parent().unwrap().join(target);
205        &resolved_path
206    } else {
207        path
208    };
209
210    // On unix platforms, get the permissions of the original file. Copy only the user/group/other
211    // read/write/execute permission bits. The tempfile lib defaults to an initial mode of 0o600,
212    // and we'll set the proper permissions after creating the file.
213    #[cfg(unix)]
214    let perms = path.metadata().ok().map(|meta| {
215        use std::os::unix::fs::PermissionsExt;
216
217        // these constants are u16 on macOS and i32 on Redox
218        let mask = (libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO) as u32;
219        let mode = meta.permissions().mode() & mask;
220
221        std::fs::Permissions::from_mode(mode)
222    });
223
224    let mut tmp = TempFileBuilder::new()
225        .prefix(path.file_name().unwrap())
226        .tempfile_in(path.parent().unwrap())?;
227    tmp.write_all(contents.as_ref())?;
228
229    // On unix platforms, set the permissions on the newly created file. We can use fchmod (called
230    // by the std lib; subject to change) which ignores the umask so that the new file has the same
231    // permissions as the old file.
232    #[cfg(unix)]
233    if let Some(perms) = perms {
234        tmp.as_file().set_permissions(perms)?;
235    }
236
237    tmp.persist(path)?;
238    Ok(())
239}
240
241/// Equivalent to [`write()`], but does not write anything if the file contents
242/// are identical to the given contents.
243pub fn write_if_changed<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
244    (|| -> Result<()> {
245        let contents = contents.as_ref();
246        let mut f = OpenOptions::new()
247            .read(true)
248            .write(true)
249            .create(true)
250            .open(&path)?;
251        let mut orig = Vec::new();
252        f.read_to_end(&mut orig)?;
253        if orig != contents {
254            f.set_len(0)?;
255            f.seek(io::SeekFrom::Start(0))?;
256            f.write_all(contents)?;
257        }
258        Ok(())
259    })()
260    .with_context(|| format!("failed to write `{}`", path.as_ref().display()))?;
261    Ok(())
262}
263
264/// Equivalent to [`write()`], but appends to the end instead of replacing the
265/// contents.
266pub fn append(path: &Path, contents: &[u8]) -> Result<()> {
267    (|| -> Result<()> {
268        let mut f = OpenOptions::new()
269            .write(true)
270            .append(true)
271            .create(true)
272            .open(path)?;
273
274        f.write_all(contents)?;
275        Ok(())
276    })()
277    .with_context(|| format!("failed to write `{}`", path.display()))?;
278    Ok(())
279}
280
281/// Creates a new file.
282pub fn create<P: AsRef<Path>>(path: P) -> Result<File> {
283    let path = path.as_ref();
284    File::create(path).with_context(|| format!("failed to create file `{}`", path.display()))
285}
286
287/// Opens an existing file.
288pub fn open<P: AsRef<Path>>(path: P) -> Result<File> {
289    let path = path.as_ref();
290    File::open(path).with_context(|| format!("failed to open file `{}`", path.display()))
291}
292
293/// Returns the last modification time of a file.
294pub fn mtime(path: &Path) -> Result<FileTime> {
295    let meta = metadata(path)?;
296    Ok(FileTime::from_last_modification_time(&meta))
297}
298
299/// Returns the maximum mtime of the given path, recursing into
300/// subdirectories, and following symlinks.
301pub fn mtime_recursive(path: &Path) -> Result<FileTime> {
302    let meta = metadata(path)?;
303    if !meta.is_dir() {
304        return Ok(FileTime::from_last_modification_time(&meta));
305    }
306    let max_meta = walkdir::WalkDir::new(path)
307        .follow_links(true)
308        .into_iter()
309        .filter_map(|e| match e {
310            Ok(e) => Some(e),
311            Err(e) => {
312                // Ignore errors while walking. If Cargo can't access it, the
313                // build script probably can't access it, either.
314                tracing::debug!("failed to determine mtime while walking directory: {}", e);
315                None
316            }
317        })
318        .filter_map(|e| {
319            if e.path_is_symlink() {
320                // Use the mtime of both the symlink and its target, to
321                // handle the case where the symlink is modified to a
322                // different target.
323                let sym_meta = match std::fs::symlink_metadata(e.path()) {
324                    Ok(m) => m,
325                    Err(err) => {
326                        // I'm not sure when this is really possible (maybe a
327                        // race with unlinking?). Regardless, if Cargo can't
328                        // read it, the build script probably can't either.
329                        tracing::debug!(
330                            "failed to determine mtime while fetching symlink metadata of {}: {}",
331                            e.path().display(),
332                            err
333                        );
334                        return None;
335                    }
336                };
337                let sym_mtime = FileTime::from_last_modification_time(&sym_meta);
338                // Walkdir follows symlinks.
339                match e.metadata() {
340                    Ok(target_meta) => {
341                        let target_mtime = FileTime::from_last_modification_time(&target_meta);
342                        Some(sym_mtime.max(target_mtime))
343                    }
344                    Err(err) => {
345                        // Can't access the symlink target. If Cargo can't
346                        // access it, the build script probably can't access
347                        // it either.
348                        tracing::debug!(
349                            "failed to determine mtime of symlink target for {}: {}",
350                            e.path().display(),
351                            err
352                        );
353                        Some(sym_mtime)
354                    }
355                }
356            } else {
357                let meta = match e.metadata() {
358                    Ok(m) => m,
359                    Err(err) => {
360                        // I'm not sure when this is really possible (maybe a
361                        // race with unlinking?). Regardless, if Cargo can't
362                        // read it, the build script probably can't either.
363                        tracing::debug!(
364                            "failed to determine mtime while fetching metadata of {}: {}",
365                            e.path().display(),
366                            err
367                        );
368                        return None;
369                    }
370                };
371                Some(FileTime::from_last_modification_time(&meta))
372            }
373        })
374        .max()
375        // or_else handles the case where there are no files in the directory.
376        .unwrap_or_else(|| FileTime::from_last_modification_time(&meta));
377    Ok(max_meta)
378}
379
380/// Record the current time on the filesystem (using the filesystem's clock)
381/// using a file at the given directory. Returns the current time.
382pub fn set_invocation_time(path: &Path) -> Result<FileTime> {
383    // note that if `FileTime::from_system_time(SystemTime::now());` is determined to be sufficient,
384    // then this can be removed.
385    let timestamp = path.join("invoked.timestamp");
386    write(
387        &timestamp,
388        "This file has an mtime of when this was started.",
389    )?;
390    let ft = mtime(&timestamp)?;
391    tracing::debug!("invocation time for {:?} is {}", path, ft);
392    Ok(ft)
393}
394
395/// Converts a path to UTF-8 bytes.
396pub fn path2bytes(path: &Path) -> Result<&[u8]> {
397    #[cfg(unix)]
398    {
399        use std::os::unix::prelude::*;
400        Ok(path.as_os_str().as_bytes())
401    }
402    #[cfg(windows)]
403    {
404        match path.as_os_str().to_str() {
405            Some(s) => Ok(s.as_bytes()),
406            None => Err(anyhow::format_err!(
407                "invalid non-unicode path: {}",
408                path.display()
409            )),
410        }
411    }
412}
413
414/// Converts UTF-8 bytes to a path.
415pub fn bytes2path(bytes: &[u8]) -> Result<PathBuf> {
416    #[cfg(unix)]
417    {
418        use std::os::unix::prelude::*;
419        Ok(PathBuf::from(OsStr::from_bytes(bytes)))
420    }
421    #[cfg(windows)]
422    {
423        use std::str;
424        match str::from_utf8(bytes) {
425            Ok(s) => Ok(PathBuf::from(s)),
426            Err(..) => Err(anyhow::format_err!("invalid non-unicode path")),
427        }
428    }
429}
430
431/// Returns an iterator that walks up the directory hierarchy towards the root.
432///
433/// Each item is a [`Path`]. It will start with the given path, finishing at
434/// the root. If the `stop_root_at` parameter is given, it will stop at the
435/// given path (which will be the last item).
436pub fn ancestors<'a>(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
437    PathAncestors::new(path, stop_root_at)
438}
439
440pub struct PathAncestors<'a> {
441    current: Option<&'a Path>,
442    stop_at: Option<PathBuf>,
443}
444
445impl<'a> PathAncestors<'a> {
446    fn new(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
447        let stop_at = env::var("__CARGO_TEST_ROOT")
448            .ok()
449            .map(PathBuf::from)
450            .or_else(|| stop_root_at.map(|p| p.to_path_buf()));
451        PathAncestors {
452            current: Some(path),
453            //HACK: avoid reading `~/.cargo/config` when testing Cargo itself.
454            stop_at,
455        }
456    }
457}
458
459impl<'a> Iterator for PathAncestors<'a> {
460    type Item = &'a Path;
461
462    fn next(&mut self) -> Option<&'a Path> {
463        if let Some(path) = self.current {
464            self.current = path.parent();
465
466            if let Some(ref stop_at) = self.stop_at {
467                if path == stop_at {
468                    self.current = None;
469                }
470            }
471
472            Some(path)
473        } else {
474            None
475        }
476    }
477}
478
479/// Equivalent to [`std::fs::create_dir_all`] with better error messages.
480pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
481    _create_dir_all(p.as_ref())
482}
483
484fn _create_dir_all(p: &Path) -> Result<()> {
485    fs::create_dir_all(p)
486        .with_context(|| format!("failed to create directory `{}`", p.display()))?;
487    Ok(())
488}
489
490/// Equivalent to [`std::fs::remove_dir_all`] with better error messages.
491///
492/// This does *not* follow symlinks.
493pub fn remove_dir_all<P: AsRef<Path>>(p: P) -> Result<()> {
494    _remove_dir_all(p.as_ref()).or_else(|prev_err| {
495        // `std::fs::remove_dir_all` is highly specialized for different platforms
496        // and may be more reliable than a simple walk. We try the walk first in
497        // order to report more detailed errors.
498        fs::remove_dir_all(p.as_ref()).with_context(|| {
499            format!(
500                "{:?}\n\nError: failed to remove directory `{}`",
501                prev_err,
502                p.as_ref().display(),
503            )
504        })
505    })
506}
507
508fn _remove_dir_all(p: &Path) -> Result<()> {
509    if symlink_metadata(p)?.is_symlink() {
510        return remove_file(p);
511    }
512    let entries = p
513        .read_dir()
514        .with_context(|| format!("failed to read directory `{}`", p.display()))?;
515    for entry in entries {
516        let entry = entry?;
517        let path = entry.path();
518        if entry.file_type()?.is_dir() {
519            remove_dir_all(&path)?;
520        } else {
521            remove_file(&path)?;
522        }
523    }
524    remove_dir(&p)
525}
526
527/// Equivalent to [`std::fs::remove_dir`] with better error messages.
528pub fn remove_dir<P: AsRef<Path>>(p: P) -> Result<()> {
529    _remove_dir(p.as_ref())
530}
531
532fn _remove_dir(p: &Path) -> Result<()> {
533    fs::remove_dir(p).with_context(|| format!("failed to remove directory `{}`", p.display()))?;
534    Ok(())
535}
536
537/// Equivalent to [`std::fs::remove_file`] with better error messages.
538///
539/// If the file is readonly, this will attempt to change the permissions to
540/// force the file to be deleted.
541/// On Windows, if the file is a symlink to a directory, this will attempt to remove
542/// the symlink itself.
543pub fn remove_file<P: AsRef<Path>>(p: P) -> Result<()> {
544    _remove_file(p.as_ref())
545}
546
547fn _remove_file(p: &Path) -> Result<()> {
548    // For Windows, we need to check if the file is a symlink to a directory
549    // and remove the symlink itself by calling `remove_dir` instead of
550    // `remove_file`.
551    #[cfg(target_os = "windows")]
552    {
553        use std::os::windows::fs::FileTypeExt;
554        let metadata = symlink_metadata(p)?;
555        let file_type = metadata.file_type();
556        if file_type.is_symlink_dir() {
557            return remove_symlink_dir_with_permission_check(p);
558        }
559    }
560
561    remove_file_with_permission_check(p)
562}
563
564#[cfg(target_os = "windows")]
565fn remove_symlink_dir_with_permission_check(p: &Path) -> Result<()> {
566    remove_with_permission_check(fs::remove_dir, p)
567        .with_context(|| format!("failed to remove symlink dir `{}`", p.display()))
568}
569
570fn remove_file_with_permission_check(p: &Path) -> Result<()> {
571    remove_with_permission_check(fs::remove_file, p)
572        .with_context(|| format!("failed to remove file `{}`", p.display()))
573}
574
575fn remove_with_permission_check<F, P>(remove_func: F, p: P) -> io::Result<()>
576where
577    F: Fn(P) -> io::Result<()>,
578    P: AsRef<Path> + Clone,
579{
580    match remove_func(p.clone()) {
581        Ok(()) => Ok(()),
582        Err(e) => {
583            if e.kind() == io::ErrorKind::PermissionDenied
584                && set_not_readonly(p.as_ref()).unwrap_or(false)
585            {
586                remove_func(p)
587            } else {
588                Err(e)
589            }
590        }
591    }
592}
593
594fn set_not_readonly(p: &Path) -> io::Result<bool> {
595    let mut perms = p.metadata()?.permissions();
596    if !perms.readonly() {
597        return Ok(false);
598    }
599    perms.set_readonly(false);
600    fs::set_permissions(p, perms)?;
601    Ok(true)
602}
603
604/// Hardlink (file) or symlink (dir) src to dst if possible, otherwise copy it.
605///
606/// If the destination already exists, it is removed before linking.
607pub fn link_or_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
608    let src = src.as_ref();
609    let dst = dst.as_ref();
610    _link_or_copy(src, dst)
611}
612
613fn _link_or_copy(src: &Path, dst: &Path) -> Result<()> {
614    tracing::debug!("linking {} to {}", src.display(), dst.display());
615    if same_file::is_same_file(src, dst).unwrap_or(false) {
616        return Ok(());
617    }
618
619    // NB: we can't use dst.exists(), as if dst is a broken symlink,
620    // dst.exists() will return false. This is problematic, as we still need to
621    // unlink dst in this case. symlink_metadata(dst).is_ok() will tell us
622    // whether dst exists *without* following symlinks, which is what we want.
623    if fs::symlink_metadata(dst).is_ok() {
624        remove_file(&dst)?;
625    }
626
627    let link_result = if src.is_dir() {
628        #[cfg(unix)]
629        use std::os::unix::fs::symlink;
630        #[cfg(windows)]
631        // FIXME: This should probably panic or have a copy fallback. Symlinks
632        // are not supported in all windows environments. Currently symlinking
633        // is only used for .dSYM directories on macos, but this shouldn't be
634        // accidentally relied upon.
635        use std::os::windows::fs::symlink_dir as symlink;
636
637        let dst_dir = dst.parent().unwrap();
638        let src = if src.starts_with(dst_dir) {
639            src.strip_prefix(dst_dir).unwrap()
640        } else {
641            src
642        };
643        symlink(src, dst)
644    } else {
645        if cfg!(target_os = "macos") {
646            // There seems to be a race condition with APFS when hard-linking
647            // binaries. Gatekeeper does not have signing or hash information
648            // stored in kernel when running the process. Therefore killing it.
649            // This problem does not appear when copying files as kernel has
650            // time to process it. Note that: fs::copy on macos is using
651            // CopyOnWrite (syscall fclonefileat) which should be as fast as
652            // hardlinking. See these issues for the details:
653            //
654            // * https://github.com/rust-lang/cargo/issues/7821
655            // * https://github.com/rust-lang/cargo/issues/10060
656            fs::copy(src, dst).map_or_else(
657                |e| {
658                    if e.raw_os_error()
659                        .map_or(false, |os_err| os_err == 35 /* libc::EAGAIN */)
660                    {
661                        tracing::info!("copy failed {e:?}. falling back to fs::hard_link");
662
663                        // Working around an issue copying too fast with zfs (probably related to
664                        // https://github.com/openzfsonosx/zfs/issues/809)
665                        // See https://github.com/rust-lang/cargo/issues/13838
666                        fs::hard_link(src, dst)
667                    } else {
668                        Err(e)
669                    }
670                },
671                |_| Ok(()),
672            )
673        } else {
674            fs::hard_link(src, dst)
675        }
676    };
677    link_result
678        .or_else(|err| {
679            tracing::debug!("link failed {}. falling back to fs::copy", err);
680            fs::copy(src, dst).map(|_| ())
681        })
682        .with_context(|| {
683            format!(
684                "failed to link or copy `{}` to `{}`",
685                src.display(),
686                dst.display()
687            )
688        })?;
689    Ok(())
690}
691
692/// Copies a file from one location to another.
693///
694/// Equivalent to [`std::fs::copy`] with better error messages.
695pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
696    let from = from.as_ref();
697    let to = to.as_ref();
698    fs::copy(from, to)
699        .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))
700}
701
702/// Changes the filesystem mtime (and atime if possible) for the given file.
703///
704/// This intentionally does not return an error, as this is sometimes not
705/// supported on network filesystems. For the current uses in Cargo, this is a
706/// "best effort" approach, and errors shouldn't be propagated.
707pub fn set_file_time_no_err<P: AsRef<Path>>(path: P, time: FileTime) {
708    let path = path.as_ref();
709    match filetime::set_file_times(path, time, time) {
710        Ok(()) => tracing::debug!("set file mtime {} to {}", path.display(), time),
711        Err(e) => tracing::warn!(
712            "could not set mtime of {} to {}: {:?}",
713            path.display(),
714            time,
715            e
716        ),
717    }
718}
719
720/// Strips `base` from `path`.
721///
722/// This canonicalizes both paths before stripping. This is useful if the
723/// paths are obtained in different ways, and one or the other may or may not
724/// have been normalized in some way.
725pub fn strip_prefix_canonical(
726    path: impl AsRef<Path>,
727    base: impl AsRef<Path>,
728) -> Result<PathBuf, std::path::StripPrefixError> {
729    // Not all filesystems support canonicalize. Just ignore if it doesn't work.
730    let safe_canonicalize = |path: &Path| match path.canonicalize() {
731        Ok(p) => p,
732        Err(e) => {
733            tracing::warn!("cannot canonicalize {:?}: {:?}", path, e);
734            path.to_path_buf()
735        }
736    };
737    let canon_path = safe_canonicalize(path.as_ref());
738    let canon_base = safe_canonicalize(base.as_ref());
739    canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
740}
741
742/// Creates an excluded from cache directory atomically with its parents as needed.
743///
744/// The atomicity only covers creating the leaf directory and exclusion from cache. Any missing
745/// parent directories will not be created in an atomic manner.
746///
747/// This function is idempotent and in addition to that it won't exclude ``p`` from cache if it
748/// already exists.
749pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> Result<()> {
750    let path = p.as_ref();
751    if path.is_dir() {
752        return Ok(());
753    }
754
755    let parent = path.parent().unwrap();
756    let base = path.file_name().unwrap();
757    create_dir_all(parent)?;
758    // We do this in two steps (first create a temporary directory and exclude
759    // it from backups, then rename it to the desired name. If we created the
760    // directory directly where it should be and then excluded it from backups
761    // we would risk a situation where cargo is interrupted right after the directory
762    // creation but before the exclusion the directory would remain non-excluded from
763    // backups because we only perform exclusion right after we created the directory
764    // ourselves.
765    //
766    // We need the tempdir created in parent instead of $TMP, because only then we can be
767    // easily sure that rename() will succeed (the new name needs to be on the same mount
768    // point as the old one).
769    let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
770    exclude_from_backups(tempdir.path());
771    exclude_from_content_indexing(tempdir.path());
772    // Previously std::fs::create_dir_all() (through paths::create_dir_all()) was used
773    // here to create the directory directly and fs::create_dir_all() explicitly treats
774    // the directory being created concurrently by another thread or process as success,
775    // hence the check below to follow the existing behavior. If we get an error at
776    // rename() and suddenly the directory (which didn't exist a moment earlier) exists
777    // we can infer from it's another cargo process doing work.
778    if let Err(e) = fs::rename(tempdir.path(), path) {
779        if !path.exists() {
780            return Err(anyhow::Error::from(e))
781                .with_context(|| format!("failed to create directory `{}`", path.display()));
782        }
783    }
784    Ok(())
785}
786
787/// Mark an existing directory as excluded from backups and indexing.
788///
789/// Errors in marking it are ignored.
790pub fn exclude_from_backups_and_indexing(p: impl AsRef<Path>) {
791    let path = p.as_ref();
792    exclude_from_backups(path);
793    exclude_from_content_indexing(path);
794}
795
796/// Marks the directory as excluded from archives/backups.
797///
798/// This is recommended to prevent derived/temporary files from bloating backups. There are two
799/// mechanisms used to achieve this right now:
800///
801/// * A dedicated resource property excluding from Time Machine backups on macOS
802/// * CACHEDIR.TAG files supported by various tools in a platform-independent way
803fn exclude_from_backups(path: &Path) {
804    exclude_from_time_machine_and_cloud_sync(path);
805    let file = path.join("CACHEDIR.TAG");
806    if !file.exists() {
807        let _ = std::fs::write(
808            file,
809            "Signature: 8a477f597d28d172789f06886806bc55
810# This file is a cache directory tag created by cargo.
811# For information about cache directory tags see https://bford.info/cachedir/
812",
813        );
814        // Similarly to exclude_from_time_machine_and_cloud_sync() we ignore errors here as it's an optional feature.
815    }
816}
817
818/// Marks the directory as excluded from content indexing.
819///
820/// This is recommended to prevent the content of derived/temporary files from being indexed.
821/// This is very important for Windows users, as the live content indexing significantly slows
822/// cargo's I/O operations.
823///
824/// This is currently a no-op on non-Windows platforms.
825fn exclude_from_content_indexing(path: &Path) {
826    #[cfg(windows)]
827    {
828        use std::iter::once;
829        use std::os::windows::prelude::OsStrExt;
830        use windows_sys::Win32::Storage::FileSystem::{
831            FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, GetFileAttributesW, SetFileAttributesW,
832        };
833
834        let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
835        unsafe {
836            SetFileAttributesW(
837                path.as_ptr(),
838                GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
839            );
840        }
841    }
842    #[cfg(not(windows))]
843    {
844        let _ = path;
845    }
846}
847
848#[cfg(not(target_os = "macos"))]
849fn exclude_from_time_machine_and_cloud_sync(_: &Path) {}
850
851#[cfg(target_os = "macos")]
852/// Marks files or directories as excluded from Time Machine and iCloud Drive on macOS
853fn exclude_from_time_machine_and_cloud_sync(path: &Path) {
854    use core_foundation::base::TCFType;
855    use core_foundation::{number, string, url};
856    use std::ptr;
857
858    let path = match url::CFURL::from_path(path, false) {
859        Some(url) => url,
860        None => return,
861    };
862
863    // For compatibility with old systems strings are used instead of global symbols
864    const KEY_NAMES: [&str; 2] = [
865        "NSURLIsExcludedFromBackupKey", // kCFURLIsExcludedFromBackupKey
866        "NSURLUbiquitousItemIsExcludedFromSyncKey", // kCFURLUbiquitousItemIsExcludedFromSyncKey
867    ];
868
869    for key_name in KEY_NAMES {
870        let is_excluded_key = match key_name.parse::<string::CFString>() {
871            Ok(key) => key,
872            Err(_) => continue,
873        };
874        unsafe {
875            url::CFURLSetResourcePropertyForKey(
876                path.as_concrete_TypeRef(),
877                is_excluded_key.as_concrete_TypeRef(),
878                number::kCFBooleanTrue as *const _,
879                ptr::null_mut(),
880            );
881        }
882    }
883    // Errors are ignored, since it's an optional feature and failure
884    // doesn't prevent Cargo from working
885}
886
887#[cfg(test)]
888mod tests {
889    use super::join_paths;
890    use super::normalize_path;
891    use super::write;
892    use super::write_atomic;
893
894    #[test]
895    fn test_normalize_path() {
896        let cases = &[
897            ("", ""),
898            (".", ""),
899            (".////./.", ""),
900            ("/", "/"),
901            ("/..", "/"),
902            ("/foo/bar", "/foo/bar"),
903            ("/foo/bar/", "/foo/bar"),
904            ("/foo/bar/./././///", "/foo/bar"),
905            ("/foo/bar/..", "/foo"),
906            ("/foo/bar/../..", "/"),
907            ("/foo/bar/../../..", "/"),
908            ("foo/bar", "foo/bar"),
909            ("foo/bar/", "foo/bar"),
910            ("foo/bar/./././///", "foo/bar"),
911            ("foo/bar/..", "foo"),
912            ("foo/bar/../..", ""),
913            ("foo/bar/../../..", ".."),
914            ("../../foo/bar", "../../foo/bar"),
915            ("../../foo/bar/", "../../foo/bar"),
916            ("../../foo/bar/./././///", "../../foo/bar"),
917            ("../../foo/bar/..", "../../foo"),
918            ("../../foo/bar/../..", "../.."),
919            ("../../foo/bar/../../..", "../../.."),
920        ];
921        for (input, expected) in cases {
922            let actual = normalize_path(std::path::Path::new(input));
923            assert_eq!(actual, std::path::Path::new(expected), "input: {input}");
924        }
925    }
926
927    #[test]
928    fn write_works() {
929        let original_contents = "[dependencies]\nfoo = 0.1.0";
930
931        let tmpdir = tempfile::tempdir().unwrap();
932        let path = tmpdir.path().join("Cargo.toml");
933        write(&path, original_contents).unwrap();
934        let contents = std::fs::read_to_string(&path).unwrap();
935        assert_eq!(contents, original_contents);
936    }
937    #[test]
938    fn write_atomic_works() {
939        let original_contents = "[dependencies]\nfoo = 0.1.0";
940
941        let tmpdir = tempfile::tempdir().unwrap();
942        let path = tmpdir.path().join("Cargo.toml");
943        write_atomic(&path, original_contents).unwrap();
944        let contents = std::fs::read_to_string(&path).unwrap();
945        assert_eq!(contents, original_contents);
946    }
947
948    #[test]
949    #[cfg(unix)]
950    fn write_atomic_permissions() {
951        use std::os::unix::fs::PermissionsExt;
952
953        let original_perms = std::fs::Permissions::from_mode(
954            (libc::S_IRWXU | libc::S_IRGRP | libc::S_IWGRP | libc::S_IROTH) as u32,
955        );
956
957        let tmp = tempfile::Builder::new().tempfile().unwrap();
958
959        // need to set the permissions after creating the file to avoid umask
960        tmp.as_file()
961            .set_permissions(original_perms.clone())
962            .unwrap();
963
964        // after this call, the file at `tmp.path()` will not be the same as the file held by `tmp`
965        write_atomic(tmp.path(), "new").unwrap();
966        assert_eq!(std::fs::read_to_string(tmp.path()).unwrap(), "new");
967
968        let new_perms = std::fs::metadata(tmp.path()).unwrap().permissions();
969
970        let mask = (libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO) as u32;
971        assert_eq!(original_perms.mode(), new_perms.mode() & mask);
972    }
973
974    #[test]
975    fn join_paths_lists_paths_on_error() {
976        let valid_paths = vec!["/testing/one", "/testing/two"];
977        // does not fail on valid input
978        let _joined = join_paths(&valid_paths, "TESTING1").unwrap();
979
980        #[cfg(unix)]
981        {
982            let invalid_paths = vec!["/testing/one", "/testing/t:wo/three"];
983            let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
984            assert_eq!(
985                err.to_string(),
986                "failed to join paths from `$TESTING2` together\n\n\
987             Check if any of path segments listed below contain an \
988             unterminated quote character or path separator:\
989             \n    \"/testing/one\"\
990             \n    \"/testing/t:wo/three\"\
991             "
992            );
993        }
994        #[cfg(windows)]
995        {
996            let invalid_paths = vec!["/testing/one", "/testing/t\"wo/three"];
997            let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
998            assert_eq!(
999                err.to_string(),
1000                "failed to join paths from `$TESTING2` together\n\n\
1001             Check if any of path segments listed below contain an \
1002             unterminated quote character or path separator:\
1003             \n    \"/testing/one\"\
1004             \n    \"/testing/t\\\"wo/three\"\
1005             "
1006            );
1007        }
1008    }
1009
1010    #[test]
1011    fn write_atomic_symlink() {
1012        let tmpdir = tempfile::tempdir().unwrap();
1013        let target_path = tmpdir.path().join("target.txt");
1014        let symlink_path = tmpdir.path().join("symlink.txt");
1015
1016        // Create initial file
1017        write(&target_path, "initial").unwrap();
1018
1019        // Create symlink
1020        #[cfg(unix)]
1021        std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
1022        #[cfg(windows)]
1023        std::os::windows::fs::symlink_file(&target_path, &symlink_path).unwrap();
1024
1025        // Write through symlink
1026        write_atomic(&symlink_path, "updated").unwrap();
1027
1028        // Verify both paths show the updated content
1029        assert_eq!(std::fs::read_to_string(&target_path).unwrap(), "updated");
1030        assert_eq!(std::fs::read_to_string(&symlink_path).unwrap(), "updated");
1031
1032        // Verify symlink still exists and points to the same target
1033        assert!(symlink_path.is_symlink());
1034        assert_eq!(std::fs::read_link(&symlink_path).unwrap(), target_path);
1035    }
1036
1037    #[test]
1038    fn write_atomic_relative_symlink() {
1039        let tmpdir = tempfile::tempdir().unwrap();
1040        let link_dir = tmpdir.path().join("project");
1041        let target_dir = link_dir.join("generated");
1042        let target_path = target_dir.join("target.txt");
1043        let symlink_path = link_dir.join("symlink.txt");
1044        let relative_target = std::path::Path::new("generated/target.txt");
1045
1046        std::fs::create_dir_all(&target_dir).unwrap();
1047        write(&target_path, "initial").unwrap();
1048
1049        #[cfg(unix)]
1050        std::os::unix::fs::symlink(relative_target, &symlink_path).unwrap();
1051        #[cfg(windows)]
1052        std::os::windows::fs::symlink_file(relative_target, &symlink_path).unwrap();
1053
1054        write_atomic(&symlink_path, "updated").unwrap();
1055
1056        assert_eq!(std::fs::read_to_string(&target_path).unwrap(), "updated");
1057        assert!(symlink_path.is_symlink());
1058        assert_eq!(std::fs::read_link(&symlink_path).unwrap(), relative_target);
1059    }
1060
1061    #[test]
1062    #[cfg(windows)]
1063    fn test_remove_symlink_dir() {
1064        use super::*;
1065        use std::fs;
1066        use std::os::windows::fs::symlink_dir;
1067
1068        let tmpdir = tempfile::tempdir().unwrap();
1069        let dir_path = tmpdir.path().join("testdir");
1070        let symlink_path = tmpdir.path().join("symlink");
1071
1072        fs::create_dir(&dir_path).unwrap();
1073
1074        symlink_dir(&dir_path, &symlink_path).expect("failed to create symlink");
1075
1076        assert!(symlink_path.exists());
1077
1078        assert!(remove_file(symlink_path.clone()).is_ok());
1079
1080        assert!(!symlink_path.exists());
1081        assert!(dir_path.exists());
1082    }
1083
1084    #[test]
1085    #[cfg(windows)]
1086    fn test_remove_symlink_file() {
1087        use super::*;
1088        use std::fs;
1089        use std::os::windows::fs::symlink_file;
1090
1091        let tmpdir = tempfile::tempdir().unwrap();
1092        let file_path = tmpdir.path().join("testfile");
1093        let symlink_path = tmpdir.path().join("symlink");
1094
1095        fs::write(&file_path, b"test").unwrap();
1096
1097        symlink_file(&file_path, &symlink_path).expect("failed to create symlink");
1098
1099        assert!(symlink_path.exists());
1100
1101        assert!(remove_file(symlink_path.clone()).is_ok());
1102
1103        assert!(!symlink_path.exists());
1104        assert!(file_path.exists());
1105    }
1106}