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