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