Skip to content

Commit

Permalink
Auto merge of #8378 - jstasiak:backups, r=ehuss
Browse files Browse the repository at this point in the history
Exclude the target directory from backups using CACHEDIR.TAG

This patch follows the lead of #4386 (which excludes target directories
from Time Machine backups) and is motived by the same reasons listen
in #3884. CACHEDIR.TAG is an OS-independent mechanism supported by Borg,
restic, GNU Tar and other backup/archiving solutions.

See https://bford.info/cachedir/ for more information about the
specification. This has been discussed in Rust Internals earlier this
year[1] and it seems like it's an uncontroversial improvement so I went
ahead with the patch.

One thing I'm wondering is whether this should maybe cover the whole main target directory (right now it applies to `target/debug`, `target/release` etc. but not to target root).

[1] https://internals.rust-lang.org/t/pre-rfc-put-cachedir-tag-into-target/12262/11
  • Loading branch information
bors committed Jul 2, 2020
2 parents ea32d80 + 5f2ba2b commit cf3bfc9
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 34 deletions.
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 @@ -5083,3 +5083,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

0 comments on commit cf3bfc9

Please sign in to comment.