diff --git a/src/cargo/core/compiler/layout.rs b/src/cargo/core/compiler/layout.rs index 53c615fbe1d..29163122ea1 100644 --- a/src/cargo/core/compiler/layout.rs +++ b/src/cargo/core/compiler/layout.rs @@ -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 @@ -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 = "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 -} diff --git a/src/cargo/util/paths.rs b/src/cargo/util/paths.rs index c5a09fdae05..fc52cc14e58 100644 --- a/src/cargo/util/paths.rs +++ b/src/cargo/util/paths.rs @@ -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}; @@ -457,3 +458,91 @@ pub fn strip_prefix_canonical>( 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) -> 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 = "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 +} diff --git a/tests/testsuite/build.rs b/tests/testsuite/build.rs index a63bc7a569d..567a30e773e 100644 --- a/tests/testsuite/build.rs +++ b/tests/testsuite/build.rs @@ -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()); +} diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index 673dc4969a1..69b14bd17c7 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -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() {