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(target_os = "redox")]
615        use std::os::redox::fs::symlink;
616        #[cfg(unix)]
617        use std::os::unix::fs::symlink;
618        #[cfg(windows)]
619        // FIXME: This should probably panic or have a copy fallback. Symlinks
620        // are not supported in all windows environments. Currently symlinking
621        // is only used for .dSYM directories on macos, but this shouldn't be
622        // accidentally relied upon.
623        use std::os::windows::fs::symlink_dir as symlink;
624
625        let dst_dir = dst.parent().unwrap();
626        let src = if src.starts_with(dst_dir) {
627            src.strip_prefix(dst_dir).unwrap()
628        } else {
629            src
630        };
631        symlink(src, dst)
632    } else {
633        if cfg!(target_os = "macos") {
634            // There seems to be a race condition with APFS when hard-linking
635            // binaries. Gatekeeper does not have signing or hash information
636            // stored in kernel when running the process. Therefore killing it.
637            // This problem does not appear when copying files as kernel has
638            // time to process it. Note that: fs::copy on macos is using
639            // CopyOnWrite (syscall fclonefileat) which should be as fast as
640            // hardlinking. See these issues for the details:
641            //
642            // * https://github.com/rust-lang/cargo/issues/7821
643            // * https://github.com/rust-lang/cargo/issues/10060
644            fs::copy(src, dst).map_or_else(
645                |e| {
646                    if e.raw_os_error()
647                        .map_or(false, |os_err| os_err == 35 /* libc::EAGAIN */)
648                    {
649                        tracing::info!("copy failed {e:?}. falling back to fs::hard_link");
650
651                        // Working around an issue copying too fast with zfs (probably related to
652                        // https://github.com/openzfsonosx/zfs/issues/809)
653                        // See https://github.com/rust-lang/cargo/issues/13838
654                        fs::hard_link(src, dst)
655                    } else {
656                        Err(e)
657                    }
658                },
659                |_| Ok(()),
660            )
661        } else {
662            fs::hard_link(src, dst)
663        }
664    };
665    link_result
666        .or_else(|err| {
667            tracing::debug!("link failed {}. falling back to fs::copy", err);
668            fs::copy(src, dst).map(|_| ())
669        })
670        .with_context(|| {
671            format!(
672                "failed to link or copy `{}` to `{}`",
673                src.display(),
674                dst.display()
675            )
676        })?;
677    Ok(())
678}
679
680/// Copies a file from one location to another.
681///
682/// Equivalent to [`std::fs::copy`] with better error messages.
683pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
684    let from = from.as_ref();
685    let to = to.as_ref();
686    fs::copy(from, to)
687        .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))
688}
689
690/// Changes the filesystem mtime (and atime if possible) for the given file.
691///
692/// This intentionally does not return an error, as this is sometimes not
693/// supported on network filesystems. For the current uses in Cargo, this is a
694/// "best effort" approach, and errors shouldn't be propagated.
695pub fn set_file_time_no_err<P: AsRef<Path>>(path: P, time: FileTime) {
696    let path = path.as_ref();
697    match filetime::set_file_times(path, time, time) {
698        Ok(()) => tracing::debug!("set file mtime {} to {}", path.display(), time),
699        Err(e) => tracing::warn!(
700            "could not set mtime of {} to {}: {:?}",
701            path.display(),
702            time,
703            e
704        ),
705    }
706}
707
708/// Strips `base` from `path`.
709///
710/// This canonicalizes both paths before stripping. This is useful if the
711/// paths are obtained in different ways, and one or the other may or may not
712/// have been normalized in some way.
713pub fn strip_prefix_canonical<P: AsRef<Path>>(
714    path: P,
715    base: P,
716) -> Result<PathBuf, std::path::StripPrefixError> {
717    // Not all filesystems support canonicalize. Just ignore if it doesn't work.
718    let safe_canonicalize = |path: &Path| match path.canonicalize() {
719        Ok(p) => p,
720        Err(e) => {
721            tracing::warn!("cannot canonicalize {:?}: {:?}", path, e);
722            path.to_path_buf()
723        }
724    };
725    let canon_path = safe_canonicalize(path.as_ref());
726    let canon_base = safe_canonicalize(base.as_ref());
727    canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
728}
729
730/// Creates an excluded from cache directory atomically with its parents as needed.
731///
732/// The atomicity only covers creating the leaf directory and exclusion from cache. Any missing
733/// parent directories will not be created in an atomic manner.
734///
735/// This function is idempotent and in addition to that it won't exclude ``p`` from cache if it
736/// already exists.
737pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> Result<()> {
738    let path = p.as_ref();
739    if path.is_dir() {
740        return Ok(());
741    }
742
743    let parent = path.parent().unwrap();
744    let base = path.file_name().unwrap();
745    create_dir_all(parent)?;
746    // We do this in two steps (first create a temporary directory and exclude
747    // it from backups, then rename it to the desired name. If we created the
748    // directory directly where it should be and then excluded it from backups
749    // we would risk a situation where cargo is interrupted right after the directory
750    // creation but before the exclusion the directory would remain non-excluded from
751    // backups because we only perform exclusion right after we created the directory
752    // ourselves.
753    //
754    // We need the tempdir created in parent instead of $TMP, because only then we can be
755    // easily sure that rename() will succeed (the new name needs to be on the same mount
756    // point as the old one).
757    let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
758    exclude_from_backups(tempdir.path());
759    exclude_from_content_indexing(tempdir.path());
760    // Previously std::fs::create_dir_all() (through paths::create_dir_all()) was used
761    // here to create the directory directly and fs::create_dir_all() explicitly treats
762    // the directory being created concurrently by another thread or process as success,
763    // hence the check below to follow the existing behavior. If we get an error at
764    // rename() and suddenly the directory (which didn't exist a moment earlier) exists
765    // we can infer from it's another cargo process doing work.
766    if let Err(e) = fs::rename(tempdir.path(), path) {
767        if !path.exists() {
768            return Err(anyhow::Error::from(e))
769                .with_context(|| format!("failed to create directory `{}`", path.display()));
770        }
771    }
772    Ok(())
773}
774
775/// Mark an existing directory as excluded from backups and indexing.
776///
777/// Errors in marking it are ignored.
778pub fn exclude_from_backups_and_indexing(p: impl AsRef<Path>) {
779    let path = p.as_ref();
780    exclude_from_backups(path);
781    exclude_from_content_indexing(path);
782}
783
784/// Marks the directory as excluded from archives/backups.
785///
786/// This is recommended to prevent derived/temporary files from bloating backups. There are two
787/// mechanisms used to achieve this right now:
788///
789/// * A dedicated resource property excluding from Time Machine backups on macOS
790/// * CACHEDIR.TAG files supported by various tools in a platform-independent way
791fn exclude_from_backups(path: &Path) {
792    exclude_from_time_machine(path);
793    let file = path.join("CACHEDIR.TAG");
794    if !file.exists() {
795        let _ = std::fs::write(
796            file,
797            "Signature: 8a477f597d28d172789f06886806bc55
798# This file is a cache directory tag created by cargo.
799# For information about cache directory tags see https://bford.info/cachedir/
800",
801        );
802        // Similarly to exclude_from_time_machine() we ignore errors here as it's an optional feature.
803    }
804}
805
806/// Marks the directory as excluded from content indexing.
807///
808/// This is recommended to prevent the content of derived/temporary files from being indexed.
809/// This is very important for Windows users, as the live content indexing significantly slows
810/// cargo's I/O operations.
811///
812/// This is currently a no-op on non-Windows platforms.
813fn exclude_from_content_indexing(path: &Path) {
814    #[cfg(windows)]
815    {
816        use std::iter::once;
817        use std::os::windows::prelude::OsStrExt;
818        use windows_sys::Win32::Storage::FileSystem::{
819            GetFileAttributesW, SetFileAttributesW, FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
820        };
821
822        let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
823        unsafe {
824            SetFileAttributesW(
825                path.as_ptr(),
826                GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
827            );
828        }
829    }
830    #[cfg(not(windows))]
831    {
832        let _ = path;
833    }
834}
835
836#[cfg(not(target_os = "macos"))]
837fn exclude_from_time_machine(_: &Path) {}
838
839#[cfg(target_os = "macos")]
840/// Marks files or directories as excluded from Time Machine on macOS
841fn exclude_from_time_machine(path: &Path) {
842    use core_foundation::base::TCFType;
843    use core_foundation::{number, string, url};
844    use std::ptr;
845
846    // For compatibility with 10.7 a string is used instead of global kCFURLIsExcludedFromBackupKey
847    let is_excluded_key: Result<string::CFString, _> = "NSURLIsExcludedFromBackupKey".parse();
848    let path = url::CFURL::from_path(path, false);
849    if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
850        unsafe {
851            url::CFURLSetResourcePropertyForKey(
852                path.as_concrete_TypeRef(),
853                is_excluded_key.as_concrete_TypeRef(),
854                number::kCFBooleanTrue as *const _,
855                ptr::null_mut(),
856            );
857        }
858    }
859    // Errors are ignored, since it's an optional feature and failure
860    // doesn't prevent Cargo from working
861}
862
863#[cfg(test)]
864mod tests {
865    use super::join_paths;
866    use super::normalize_path;
867    use super::write;
868    use super::write_atomic;
869
870    #[test]
871    fn test_normalize_path() {
872        let cases = &[
873            ("", ""),
874            (".", ""),
875            (".////./.", ""),
876            ("/", "/"),
877            ("/..", "/"),
878            ("/foo/bar", "/foo/bar"),
879            ("/foo/bar/", "/foo/bar"),
880            ("/foo/bar/./././///", "/foo/bar"),
881            ("/foo/bar/..", "/foo"),
882            ("/foo/bar/../..", "/"),
883            ("/foo/bar/../../..", "/"),
884            ("foo/bar", "foo/bar"),
885            ("foo/bar/", "foo/bar"),
886            ("foo/bar/./././///", "foo/bar"),
887            ("foo/bar/..", "foo"),
888            ("foo/bar/../..", ""),
889            ("foo/bar/../../..", ".."),
890            ("../../foo/bar", "../../foo/bar"),
891            ("../../foo/bar/", "../../foo/bar"),
892            ("../../foo/bar/./././///", "../../foo/bar"),
893            ("../../foo/bar/..", "../../foo"),
894            ("../../foo/bar/../..", "../.."),
895            ("../../foo/bar/../../..", "../../.."),
896        ];
897        for (input, expected) in cases {
898            let actual = normalize_path(std::path::Path::new(input));
899            assert_eq!(actual, std::path::Path::new(expected), "input: {input}");
900        }
901    }
902
903    #[test]
904    fn write_works() {
905        let original_contents = "[dependencies]\nfoo = 0.1.0";
906
907        let tmpdir = tempfile::tempdir().unwrap();
908        let path = tmpdir.path().join("Cargo.toml");
909        write(&path, original_contents).unwrap();
910        let contents = std::fs::read_to_string(&path).unwrap();
911        assert_eq!(contents, original_contents);
912    }
913    #[test]
914    fn write_atomic_works() {
915        let original_contents = "[dependencies]\nfoo = 0.1.0";
916
917        let tmpdir = tempfile::tempdir().unwrap();
918        let path = tmpdir.path().join("Cargo.toml");
919        write_atomic(&path, original_contents).unwrap();
920        let contents = std::fs::read_to_string(&path).unwrap();
921        assert_eq!(contents, original_contents);
922    }
923
924    #[test]
925    #[cfg(unix)]
926    fn write_atomic_permissions() {
927        use std::os::unix::fs::PermissionsExt;
928
929        let original_perms = std::fs::Permissions::from_mode(u32::from(
930            libc::S_IRWXU | libc::S_IRGRP | libc::S_IWGRP | libc::S_IROTH,
931        ));
932
933        let tmp = tempfile::Builder::new().tempfile().unwrap();
934
935        // need to set the permissions after creating the file to avoid umask
936        tmp.as_file()
937            .set_permissions(original_perms.clone())
938            .unwrap();
939
940        // after this call, the file at `tmp.path()` will not be the same as the file held by `tmp`
941        write_atomic(tmp.path(), "new").unwrap();
942        assert_eq!(std::fs::read_to_string(tmp.path()).unwrap(), "new");
943
944        let new_perms = std::fs::metadata(tmp.path()).unwrap().permissions();
945
946        let mask = u32::from(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO);
947        assert_eq!(original_perms.mode(), new_perms.mode() & mask);
948    }
949
950    #[test]
951    fn join_paths_lists_paths_on_error() {
952        let valid_paths = vec!["/testing/one", "/testing/two"];
953        // does not fail on valid input
954        let _joined = join_paths(&valid_paths, "TESTING1").unwrap();
955
956        #[cfg(unix)]
957        {
958            let invalid_paths = vec!["/testing/one", "/testing/t:wo/three"];
959            let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
960            assert_eq!(
961                err.to_string(),
962                "failed to join paths from `$TESTING2` together\n\n\
963             Check if any of path segments listed below contain an \
964             unterminated quote character or path separator:\
965             \n    \"/testing/one\"\
966             \n    \"/testing/t:wo/three\"\
967             "
968            );
969        }
970        #[cfg(windows)]
971        {
972            let invalid_paths = vec!["/testing/one", "/testing/t\"wo/three"];
973            let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
974            assert_eq!(
975                err.to_string(),
976                "failed to join paths from `$TESTING2` together\n\n\
977             Check if any of path segments listed below contain an \
978             unterminated quote character or path separator:\
979             \n    \"/testing/one\"\
980             \n    \"/testing/t\\\"wo/three\"\
981             "
982            );
983        }
984    }
985
986    #[test]
987    #[cfg(windows)]
988    fn test_remove_symlink_dir() {
989        use super::*;
990        use std::fs;
991        use std::os::windows::fs::symlink_dir;
992
993        let tmpdir = tempfile::tempdir().unwrap();
994        let dir_path = tmpdir.path().join("testdir");
995        let symlink_path = tmpdir.path().join("symlink");
996
997        fs::create_dir(&dir_path).unwrap();
998
999        symlink_dir(&dir_path, &symlink_path).expect("failed to create symlink");
1000
1001        assert!(symlink_path.exists());
1002
1003        assert!(remove_file(symlink_path.clone()).is_ok());
1004
1005        assert!(!symlink_path.exists());
1006        assert!(dir_path.exists());
1007    }
1008
1009    #[test]
1010    #[cfg(windows)]
1011    fn test_remove_symlink_file() {
1012        use super::*;
1013        use std::fs;
1014        use std::os::windows::fs::symlink_file;
1015
1016        let tmpdir = tempfile::tempdir().unwrap();
1017        let file_path = tmpdir.path().join("testfile");
1018        let symlink_path = tmpdir.path().join("symlink");
1019
1020        fs::write(&file_path, b"test").unwrap();
1021
1022        symlink_file(&file_path, &symlink_path).expect("failed to create symlink");
1023
1024        assert!(symlink_path.exists());
1025
1026        assert!(remove_file(symlink_path.clone()).is_ok());
1027
1028        assert!(!symlink_path.exists());
1029        assert!(file_path.exists());
1030    }
1031}