blob: b8121d430c8fe109e09afd2bede8e0ba1e024e49 [file] [log] [blame]
use crate::error::{Error, Result};
use crate::gen::fs;
use crate::paths;
use std::path::{Component, Path, PathBuf};
use std::{env, io};
pub(crate) fn write(path: impl AsRef<Path>, content: &[u8]) -> Result<()> {
let path = path.as_ref();
let mut create_dir_error = None;
if fs::exists(path) {
if let Ok(existing) = fs::read(path) {
if existing == content {
// Avoid bumping modified time with unchanged contents.
return Ok(());
}
}
best_effort_remove(path);
} else {
let parent = path.parent().unwrap();
create_dir_error = fs::create_dir_all(parent).err();
}
match fs::write(path, content) {
// As long as write succeeded, ignore any create_dir_all error.
Ok(()) => Ok(()),
// If create_dir_all and write both failed, prefer the first error.
Err(err) => Err(Error::Fs(create_dir_error.unwrap_or(err))),
}
}
pub(crate) fn symlink_file(original: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
let original = best_effort_relativize_symlink(original, link);
let mut create_dir_error = None;
if fs::exists(link) {
best_effort_remove(link);
} else {
let parent = link.parent().unwrap();
create_dir_error = fs::create_dir_all(parent).err();
}
match paths::symlink_or_copy(original, link) {
// As long as symlink_or_copy succeeded, ignore any create_dir_all error.
Ok(()) => Ok(()),
Err(err) => {
if err.kind() == io::ErrorKind::AlreadyExists {
// This is fine, a different simultaneous build script already
// created the same link or copy. The cxx_build target directory
// is laid out such that the same path never refers to two
// different targets during the same multi-crate build, so if
// some other build script already created the same path then we
// know it refers to the identical target that the current build
// script was trying to create.
Ok(())
} else {
// If create_dir_all and symlink_or_copy both failed, prefer the
// first error.
Err(Error::Fs(create_dir_error.unwrap_or(err)))
}
}
}
}
pub(crate) fn symlink_dir(original: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
let original = best_effort_relativize_symlink(original, link);
let mut create_dir_error = None;
if fs::exists(link) {
best_effort_remove(link);
} else {
let parent = link.parent().unwrap();
create_dir_error = fs::create_dir_all(parent).err();
}
match fs::symlink_dir(original, link) {
// As long as symlink_dir succeeded, ignore any create_dir_all error.
Ok(()) => Ok(()),
// If create_dir_all and symlink_dir both failed, prefer the first error.
Err(err) => Err(Error::Fs(create_dir_error.unwrap_or(err))),
}
}
fn best_effort_remove(path: &Path) {
use std::fs;
if cfg!(windows) {
// On Windows, the correct choice of remove_file vs remove_dir needs to
// be used according to what the symlink *points to*. Trying to use
// remove_file to remove a symlink which points to a directory fails
// with "Access is denied".
if let Ok(metadata) = fs::metadata(path) {
if metadata.is_dir() {
let _ = fs::remove_dir_all(path);
} else {
let _ = fs::remove_file(path);
}
} else if fs::symlink_metadata(path).is_ok() {
// The symlink might exist but be dangling, in which case there is
// no standard way to determine what "kind" of symlink it is. Try
// deleting both ways.
if fs::remove_dir_all(path).is_err() {
let _ = fs::remove_file(path);
}
}
} else {
// On non-Windows, we check metadata not following symlinks. All
// symlinks are removed using remove_file.
if let Ok(metadata) = fs::symlink_metadata(path) {
if metadata.is_dir() {
let _ = fs::remove_dir_all(path);
} else {
let _ = fs::remove_file(path);
}
}
}
}
fn best_effort_relativize_symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) -> PathBuf {
let original = original.as_ref();
let link = link.as_ref();
// Relativization only makes sense if there is a semantically meaningful
// base directory shared between the two paths.
//
// For example /Volumes/code/library/src/lib.rs
// and /Volumes/code/library/target/path/to/something.a
// have a meaningful shared base of /Volumes/code/library. The target and
// source directory only likely ever get relocated as one unit.
//
// On the other hand, /Volumes/code/library/src/lib.rs
// and /Volumes/shared_target
// do not, since upon moving library to a different location it should
// continue referring to the original location of that shared Cargo target
// directory.
let likely_no_semantic_prefix = env::var_os("CARGO_TARGET_DIR").is_some();
if likely_no_semantic_prefix
|| original.is_relative()
|| link.is_relative()
|| path_contains_intermediate_components(original)
|| path_contains_intermediate_components(link)
{
return original.to_path_buf();
}
let common_prefix = common_prefix(original, link);
if common_prefix == Path::new("") {
return original.to_path_buf();
}
let relative_original = original.strip_prefix(&common_prefix).expect("unreachable");
let mut link = link
.parent()
.expect("we know that link is an absolute path, so at least one parent exists")
.to_path_buf();
let mut path_to_common_prefix = PathBuf::new();
while link != common_prefix {
path_to_common_prefix.push(Component::ParentDir);
assert!(
link.pop(),
"we know there is a common prefix of nonzero size, so this should never return 'no parent'"
);
}
path_to_common_prefix.join(relative_original)
}
fn path_contains_intermediate_components(path: impl AsRef<Path>) -> bool {
path.as_ref()
.components()
.any(|component| component == Component::ParentDir)
}
fn common_prefix(first: &Path, second: &Path) -> PathBuf {
let mut common_prefix = PathBuf::new();
let mut first = first.components();
let mut second = second.components();
loop {
match (first.next(), second.next()) {
(Some(first_component), Some(second_component))
if first_component == second_component =>
{
common_prefix.push(first_component);
}
_ => return common_prefix,
}
}
}
#[cfg(test)]
mod tests {
use crate::out::best_effort_relativize_symlink;
#[cfg(not(windows))]
#[test]
fn test_relativize_symlink_unix() {
assert_eq!(
best_effort_relativize_symlink("/foo/bar/baz", "/foo/spam/eggs")
.to_str()
.unwrap(),
"../bar/baz"
);
assert_eq!(
best_effort_relativize_symlink("/foo/bar/../baz", "/foo/spam/eggs")
.to_str()
.unwrap(),
"/foo/bar/../baz"
);
assert_eq!(
best_effort_relativize_symlink("/foo/bar/baz", "/foo/spam/./eggs")
.to_str()
.unwrap(),
"../bar/baz"
);
}
#[cfg(windows)]
#[test]
fn test_relativize_symlink_windows() {
use std::path::PathBuf;
let windows_target: PathBuf = ["c:\\", "windows", "foo"].iter().collect();
let windows_link: PathBuf = ["c:\\", "users", "link"].iter().collect();
let windows_different_volume_link: PathBuf = ["d:\\", "users", "link"].iter().collect();
assert_eq!(
best_effort_relativize_symlink(windows_target.clone(), windows_link)
.to_str()
.unwrap(),
"..\\windows\\foo"
);
assert_eq!(
best_effort_relativize_symlink(windows_target.clone(), windows_different_volume_link),
windows_target
);
}
}