Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude the target directory from backups using CACHEDIR.TAG #8378

Merged
merged 13 commits into from
Jul 2, 2020
38 changes: 5 additions & 33 deletions src/cargo/core/compiler/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,11 @@ impl Layout {
// If the root directory doesn't already exist go ahead and create it
// here. Use this opportunity to exclude it from backups as well if the
// system supports it since this is a freshly created folder.
if !dest.as_path_unlocked().exists() {
dest.create_dir()?;
exclude_from_backups(dest.as_path_unlocked());
}
//
paths::create_dir_all_excluded_from_backups_atomic(root.as_path_unlocked())?;
// Now that the excluded from backups target root is created we can create the
// actual destination (sub)subdirectory.
paths::create_dir_all(dest.as_path_unlocked())?;

// For now we don't do any more finer-grained locking on the artifact
// directory, so just lock the entire thing for the duration of this
Expand Down Expand Up @@ -219,32 +220,3 @@ impl Layout {
&self.build
}
}

#[cfg(not(target_os = "macos"))]
fn exclude_from_backups(_: &Path) {}

#[cfg(target_os = "macos")]
/// Marks files or directories as excluded from Time Machine on macOS
///
/// This is recommended to prevent derived/temporary files from bloating backups.
fn exclude_from_backups(path: &Path) {
use core_foundation::base::TCFType;
use core_foundation::{number, string, url};
use std::ptr;

// For compatibility with 10.7 a string is used instead of global kCFURLIsExcludedFromBackupKey
let is_excluded_key: Result<string::CFString, _> = "NSURLIsExcludedFromBackupKey".parse();
let path = url::CFURL::from_path(path, false);
if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
unsafe {
url::CFURLSetResourcePropertyForKey(
path.as_concrete_TypeRef(),
is_excluded_key.as_concrete_TypeRef(),
number::kCFBooleanTrue as *const _,
ptr::null_mut(),
);
}
}
// Errors are ignored, since it's an optional feature and failure
// doesn't prevent Cargo from working
}
89 changes: 89 additions & 0 deletions src/cargo/util/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::iter;
use std::path::{Component, Path, PathBuf};

use filetime::FileTime;
use tempfile::Builder as TempFileBuilder;

use crate::util::errors::{CargoResult, CargoResultExt};

Expand Down Expand Up @@ -457,3 +458,91 @@ pub fn strip_prefix_canonical<P: AsRef<Path>>(
let canon_base = safe_canonicalize(base.as_ref());
canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
}

/// Creates an excluded from cache directory atomically with its parents as needed.
///
/// The atomicity only covers creating the leaf directory and exclusion from cache. Any missing
/// parent directories will not be created in an atomic manner.
///
/// This function is idempotent and in addition to that it won't exclude ``p`` from cache if it
/// already exists.
pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> CargoResult<()> {
let path = p.as_ref();
if path.is_dir() {
return Ok(());
}

let parent = path.parent().unwrap();
let base = path.file_name().unwrap();
create_dir_all(parent)?;
// We do this in two steps (first create a temporary directory and exlucde
// it from backups, then rename it to the desired name. If we created the
// directory directly where it should be and then excluded it from backups
// we would risk a situation where cargo is interrupted right after the directory
// creation but before the exclusion the the directory would remain non-excluded from
// backups because we only perform exclusion right after we created the directory
// ourselves.
//
// We need the tempdir created in parent instead of $TMP, because only then we can be
// easily sure that rename() will succeed (the new name needs to be on the same mount
// point as the old one).
let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
exclude_from_backups(&tempdir.path());
// Previously std::fs::create_dir_all() (through paths::create_dir_all()) was used
// here to create the directory directly and fs::create_dir_all() explicitly treats
// the directory being created concurrently by another thread or process as success,
// hence the check below to follow the existing behavior. If we get an error at
// rename() and suddently the directory (which didn't exist a moment earlier) exists
// we can infer from it it's another cargo process doing work.
if let Err(e) = fs::rename(tempdir.path(), path) {
if !path.exists() {
return Err(anyhow::Error::from(e));
}
}
Ok(())
}

/// Marks the directory as excluded from archives/backups.
///
/// This is recommended to prevent derived/temporary files from bloating backups. There are two
/// mechanisms used to achieve this right now:
///
/// * A dedicated resource property excluding from Time Machine backups on macOS
/// * CACHEDIR.TAG files supported by various tools in a platform-independent way
fn exclude_from_backups(path: &Path) {
exclude_from_time_machine(path);
let _ = std::fs::write(
path.join("CACHEDIR.TAG"),
"Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/",
);
// Similarly to exclude_from_time_machine() we ignore errors here as it's an optional feature.
}

#[cfg(not(target_os = "macos"))]
fn exclude_from_time_machine(_: &Path) {}

#[cfg(target_os = "macos")]
/// Marks files or directories as excluded from Time Machine on macOS
fn exclude_from_time_machine(path: &Path) {
use core_foundation::base::TCFType;
use core_foundation::{number, string, url};
use std::ptr;

// For compatibility with 10.7 a string is used instead of global kCFURLIsExcludedFromBackupKey
let is_excluded_key: Result<string::CFString, _> = "NSURLIsExcludedFromBackupKey".parse();
let path = url::CFURL::from_path(path, false);
if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
unsafe {
url::CFURLSetResourcePropertyForKey(
path.as_concrete_TypeRef(),
is_excluded_key.as_concrete_TypeRef(),
number::kCFBooleanTrue as *const _,
ptr::null_mut(),
);
}
}
// Errors are ignored, since it's an optional feature and failure
// doesn't prevent Cargo from working
}
20 changes: 20 additions & 0 deletions tests/testsuite/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4989,3 +4989,23 @@ fn reduced_reproduction_8249() {
p.cargo("check").run();
p.cargo("check").run();
}

#[cargo_test]
fn target_directory_backup_exclusion() {
let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.build();

// Newly created target/ should have CACHEDIR.TAG inside...
p.cargo("build").run();
let cachedir_tag = p.build_dir().join("CACHEDIR.TAG");
assert!(cachedir_tag.is_file());
assert!(fs::read_to_string(&cachedir_tag)
.unwrap()
.starts_with("Signature: 8a477f597d28d172789f06886806bc55"));
// ...but if target/ already exists CACHEDIR.TAG should not be created in it.
fs::remove_file(&cachedir_tag).unwrap();
p.cargo("build").run();
assert!(!&cachedir_tag.is_file());
}
4 changes: 3 additions & 1 deletion tests/testsuite/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,9 @@ fn assert_all_clean(build_dir: &Path) {
}) {
let entry = entry.unwrap();
let path = entry.path();
if let ".rustc_info.json" | ".cargo-lock" = path.file_name().unwrap().to_str().unwrap() {
if let ".rustc_info.json" | ".cargo-lock" | "CACHEDIR.TAG" =
path.file_name().unwrap().to_str().unwrap()
{
continue;
}
if path.is_symlink() || path.is_file() {
Expand Down