cargo/util/
paths.rs

1use std::env;
2use std::ffi::{OsStr, OsString};
3use std::fs::{self, File, OpenOptions};
4use std::io;
5use std::io::prelude::*;
6use std::iter;
7use std::path::{Component, Path, PathBuf};
8
9use filetime::FileTime;
10
11use crate::util::errors::{CargoResult, CargoResultExt};
12
13pub fn join_paths<T: AsRef<OsStr>>(paths: &[T], env: &str) -> CargoResult<OsString> {
14    env::join_paths(paths.iter())
15        .chain_err(|| {
16            let paths = paths.iter().map(Path::new).collect::<Vec<_>>();
17            format!("failed to join path array: {:?}", paths)
18        })
19        .chain_err(|| {
20            format!(
21                "failed to join search paths together\n\
22                     Does ${} have an unterminated quote character?",
23                env
24            )
25        })
26}
27
28pub fn dylib_path_envvar() -> &'static str {
29    if cfg!(windows) {
30        "PATH"
31    } else if cfg!(target_os = "macos") {
32        // When loading and linking a dynamic library or bundle, dlopen
33        // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and
34        // DYLD_FALLBACK_LIBRARY_PATH.
35        // In the Mach-O format, a dynamic library has an "install path."
36        // Clients linking against the library record this path, and the
37        // dynamic linker, dyld, uses it to locate the library.
38        // dyld searches DYLD_LIBRARY_PATH *before* the install path.
39        // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot
40        // find the library in the install path.
41        // Setting DYLD_LIBRARY_PATH can easily have unintended
42        // consequences.
43        //
44        // Also, DYLD_LIBRARY_PATH appears to have significant performance
45        // penalty starting in 10.13. Cargo's testsuite ran more than twice as
46        // slow with it on CI.
47        "DYLD_FALLBACK_LIBRARY_PATH"
48    } else {
49        "LD_LIBRARY_PATH"
50    }
51}
52
53pub fn dylib_path() -> Vec<PathBuf> {
54    match env::var_os(dylib_path_envvar()) {
55        Some(var) => env::split_paths(&var).collect(),
56        None => Vec::new(),
57    }
58}
59
60pub fn normalize_path(path: &Path) -> PathBuf {
61    let mut components = path.components().peekable();
62    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
63        components.next();
64        PathBuf::from(c.as_os_str())
65    } else {
66        PathBuf::new()
67    };
68
69    for component in components {
70        match component {
71            Component::Prefix(..) => unreachable!(),
72            Component::RootDir => {
73                ret.push(component.as_os_str());
74            }
75            Component::CurDir => {}
76            Component::ParentDir => {
77                ret.pop();
78            }
79            Component::Normal(c) => {
80                ret.push(c);
81            }
82        }
83    }
84    ret
85}
86
87pub fn resolve_executable(exec: &Path) -> CargoResult<PathBuf> {
88    if exec.components().count() == 1 {
89        let paths = env::var_os("PATH").ok_or_else(|| anyhow::format_err!("no PATH"))?;
90        let candidates = env::split_paths(&paths).flat_map(|path| {
91            let candidate = path.join(&exec);
92            let with_exe = if env::consts::EXE_EXTENSION == "" {
93                None
94            } else {
95                Some(candidate.with_extension(env::consts::EXE_EXTENSION))
96            };
97            iter::once(candidate).chain(with_exe)
98        });
99        for candidate in candidates {
100            if candidate.is_file() {
101                // PATH may have a component like "." in it, so we still need to
102                // canonicalize.
103                return Ok(candidate.canonicalize()?);
104            }
105        }
106
107        anyhow::bail!("no executable for `{}` found in PATH", exec.display())
108    } else {
109        Ok(exec.canonicalize()?)
110    }
111}
112
113pub fn read(path: &Path) -> CargoResult<String> {
114    match String::from_utf8(read_bytes(path)?) {
115        Ok(s) => Ok(s),
116        Err(_) => anyhow::bail!("path at `{}` was not valid utf-8", path.display()),
117    }
118}
119
120pub fn read_bytes(path: &Path) -> CargoResult<Vec<u8>> {
121    let res = (|| -> CargoResult<_> {
122        let mut ret = Vec::new();
123        let mut f = File::open(path)?;
124        if let Ok(m) = f.metadata() {
125            ret.reserve(m.len() as usize + 1);
126        }
127        f.read_to_end(&mut ret)?;
128        Ok(ret)
129    })()
130    .chain_err(|| format!("failed to read `{}`", path.display()))?;
131    Ok(res)
132}
133
134pub fn write(path: &Path, contents: &[u8]) -> CargoResult<()> {
135    (|| -> CargoResult<()> {
136        let mut f = File::create(path)?;
137        f.write_all(contents)?;
138        Ok(())
139    })()
140    .chain_err(|| format!("failed to write `{}`", path.display()))?;
141    Ok(())
142}
143
144pub fn write_if_changed<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> CargoResult<()> {
145    (|| -> CargoResult<()> {
146        let contents = contents.as_ref();
147        let mut f = OpenOptions::new()
148            .read(true)
149            .write(true)
150            .create(true)
151            .open(&path)?;
152        let mut orig = Vec::new();
153        f.read_to_end(&mut orig)?;
154        if orig != contents {
155            f.set_len(0)?;
156            f.seek(io::SeekFrom::Start(0))?;
157            f.write_all(contents)?;
158        }
159        Ok(())
160    })()
161    .chain_err(|| format!("failed to write `{}`", path.as_ref().display()))?;
162    Ok(())
163}
164
165pub fn append(path: &Path, contents: &[u8]) -> CargoResult<()> {
166    (|| -> CargoResult<()> {
167        let mut f = OpenOptions::new()
168            .write(true)
169            .append(true)
170            .create(true)
171            .open(path)?;
172
173        f.write_all(contents)?;
174        Ok(())
175    })()
176    .chain_err(|| format!("failed to write `{}`", path.display()))?;
177    Ok(())
178}
179
180pub fn mtime(path: &Path) -> CargoResult<FileTime> {
181    let meta = fs::metadata(path).chain_err(|| format!("failed to stat `{}`", path.display()))?;
182    Ok(FileTime::from_last_modification_time(&meta))
183}
184
185/// Record the current time on the filesystem (using the filesystem's clock)
186/// using a file at the given directory. Returns the current time.
187pub fn set_invocation_time(path: &Path) -> CargoResult<FileTime> {
188    // note that if `FileTime::from_system_time(SystemTime::now());` is determined to be sufficient,
189    // then this can be removed.
190    let timestamp = path.join("invoked.timestamp");
191    write(
192        &timestamp,
193        b"This file has an mtime of when this was started.",
194    )?;
195    let ft = mtime(&timestamp)?;
196    log::debug!("invocation time for {:?} is {}", path, ft);
197    Ok(ft)
198}
199
200#[cfg(unix)]
201pub fn path2bytes(path: &Path) -> CargoResult<&[u8]> {
202    use std::os::unix::prelude::*;
203    Ok(path.as_os_str().as_bytes())
204}
205#[cfg(windows)]
206pub fn path2bytes(path: &Path) -> CargoResult<&[u8]> {
207    match path.as_os_str().to_str() {
208        Some(s) => Ok(s.as_bytes()),
209        None => Err(anyhow::format_err!(
210            "invalid non-unicode path: {}",
211            path.display()
212        )),
213    }
214}
215
216#[cfg(unix)]
217pub fn bytes2path(bytes: &[u8]) -> CargoResult<PathBuf> {
218    use std::os::unix::prelude::*;
219    Ok(PathBuf::from(OsStr::from_bytes(bytes)))
220}
221#[cfg(windows)]
222pub fn bytes2path(bytes: &[u8]) -> CargoResult<PathBuf> {
223    use std::str;
224    match str::from_utf8(bytes) {
225        Ok(s) => Ok(PathBuf::from(s)),
226        Err(..) => Err(anyhow::format_err!("invalid non-unicode path")),
227    }
228}
229
230pub fn ancestors(path: &Path) -> PathAncestors<'_> {
231    PathAncestors::new(path)
232}
233
234pub struct PathAncestors<'a> {
235    current: Option<&'a Path>,
236    stop_at: Option<PathBuf>,
237}
238
239impl<'a> PathAncestors<'a> {
240    fn new(path: &Path) -> PathAncestors<'_> {
241        PathAncestors {
242            current: Some(path),
243            //HACK: avoid reading `~/.cargo/config` when testing Cargo itself.
244            stop_at: env::var("__CARGO_TEST_ROOT").ok().map(PathBuf::from),
245        }
246    }
247}
248
249impl<'a> Iterator for PathAncestors<'a> {
250    type Item = &'a Path;
251
252    fn next(&mut self) -> Option<&'a Path> {
253        if let Some(path) = self.current {
254            self.current = path.parent();
255
256            if let Some(ref stop_at) = self.stop_at {
257                if path == stop_at {
258                    self.current = None;
259                }
260            }
261
262            Some(path)
263        } else {
264            None
265        }
266    }
267}
268
269pub fn create_dir_all(p: impl AsRef<Path>) -> CargoResult<()> {
270    _create_dir_all(p.as_ref())
271}
272
273fn _create_dir_all(p: &Path) -> CargoResult<()> {
274    fs::create_dir_all(p).chain_err(|| format!("failed to create directory `{}`", p.display()))?;
275    Ok(())
276}
277
278pub fn remove_dir_all<P: AsRef<Path>>(p: P) -> CargoResult<()> {
279    _remove_dir_all(p.as_ref())
280}
281
282fn _remove_dir_all(p: &Path) -> CargoResult<()> {
283    if p.symlink_metadata()?.file_type().is_symlink() {
284        return remove_file(p);
285    }
286    let entries = p
287        .read_dir()
288        .chain_err(|| format!("failed to read directory `{}`", p.display()))?;
289    for entry in entries {
290        let entry = entry?;
291        let path = entry.path();
292        if entry.file_type()?.is_dir() {
293            remove_dir_all(&path)?;
294        } else {
295            remove_file(&path)?;
296        }
297    }
298    remove_dir(&p)
299}
300
301pub fn remove_dir<P: AsRef<Path>>(p: P) -> CargoResult<()> {
302    _remove_dir(p.as_ref())
303}
304
305fn _remove_dir(p: &Path) -> CargoResult<()> {
306    fs::remove_dir(p).chain_err(|| format!("failed to remove directory `{}`", p.display()))?;
307    Ok(())
308}
309
310pub fn remove_file<P: AsRef<Path>>(p: P) -> CargoResult<()> {
311    _remove_file(p.as_ref())
312}
313
314fn _remove_file(p: &Path) -> CargoResult<()> {
315    let mut err = match fs::remove_file(p) {
316        Ok(()) => return Ok(()),
317        Err(e) => e,
318    };
319
320    if err.kind() == io::ErrorKind::PermissionDenied && set_not_readonly(p).unwrap_or(false) {
321        match fs::remove_file(p) {
322            Ok(()) => return Ok(()),
323            Err(e) => err = e,
324        }
325    }
326
327    Err(err).chain_err(|| format!("failed to remove file `{}`", p.display()))?;
328    Ok(())
329}
330
331fn set_not_readonly(p: &Path) -> io::Result<bool> {
332    let mut perms = p.metadata()?.permissions();
333    if !perms.readonly() {
334        return Ok(false);
335    }
336    perms.set_readonly(false);
337    fs::set_permissions(p, perms)?;
338    Ok(true)
339}
340
341/// Hardlink (file) or symlink (dir) src to dst if possible, otherwise copy it.
342///
343/// If the destination already exists, it is removed before linking.
344pub fn link_or_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> CargoResult<()> {
345    let src = src.as_ref();
346    let dst = dst.as_ref();
347    _link_or_copy(src, dst)
348}
349
350fn _link_or_copy(src: &Path, dst: &Path) -> CargoResult<()> {
351    log::debug!("linking {} to {}", src.display(), dst.display());
352    if same_file::is_same_file(src, dst).unwrap_or(false) {
353        return Ok(());
354    }
355
356    // NB: we can't use dst.exists(), as if dst is a broken symlink,
357    // dst.exists() will return false. This is problematic, as we still need to
358    // unlink dst in this case. symlink_metadata(dst).is_ok() will tell us
359    // whether dst exists *without* following symlinks, which is what we want.
360    if fs::symlink_metadata(dst).is_ok() {
361        remove_file(&dst)?;
362    }
363
364    let link_result = if src.is_dir() {
365        #[cfg(target_os = "redox")]
366        use std::os::redox::fs::symlink;
367        #[cfg(unix)]
368        use std::os::unix::fs::symlink;
369        #[cfg(windows)]
370        // FIXME: This should probably panic or have a copy fallback. Symlinks
371        // are not supported in all windows environments. Currently symlinking
372        // is only used for .dSYM directories on macos, but this shouldn't be
373        // accidentally relied upon.
374        use std::os::windows::fs::symlink_dir as symlink;
375
376        let dst_dir = dst.parent().unwrap();
377        let src = if src.starts_with(dst_dir) {
378            src.strip_prefix(dst_dir).unwrap()
379        } else {
380            src
381        };
382        symlink(src, dst)
383    } else if env::var_os("__CARGO_COPY_DONT_LINK_DO_NOT_USE_THIS").is_some() {
384        // This is a work-around for a bug in macOS 10.15. When running on
385        // APFS, there seems to be a strange race condition with
386        // Gatekeeper where it will forcefully kill a process launched via
387        // `cargo run` with SIGKILL. Copying seems to avoid the problem.
388        // This shouldn't affect anyone except Cargo's test suite because
389        // it is very rare, and only seems to happen under heavy load and
390        // rapidly creating lots of executables and running them.
391        // See https://github.com/rust-lang/cargo/issues/7821 for the
392        // gory details.
393        fs::copy(src, dst).map(|_| ())
394    } else {
395        fs::hard_link(src, dst)
396    };
397    link_result
398        .or_else(|err| {
399            log::debug!("link failed {}. falling back to fs::copy", err);
400            fs::copy(src, dst).map(|_| ())
401        })
402        .chain_err(|| {
403            format!(
404                "failed to link or copy `{}` to `{}`",
405                src.display(),
406                dst.display()
407            )
408        })?;
409    Ok(())
410}