Skip to content

Commit cf3bfc9

Browse files
committed
Auto merge of #8378 - jstasiak:backups, r=ehuss
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
2 parents ea32d80 + 5f2ba2b commit cf3bfc9

File tree

4 files changed

+117
-34
lines changed

4 files changed

+117
-34
lines changed

src/cargo/core/compiler/layout.rs

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,11 @@ impl Layout {
150150
// If the root directory doesn't already exist go ahead and create it
151151
// here. Use this opportunity to exclude it from backups as well if the
152152
// system supports it since this is a freshly created folder.
153-
if !dest.as_path_unlocked().exists() {
154-
dest.create_dir()?;
155-
exclude_from_backups(dest.as_path_unlocked());
156-
}
153+
//
154+
paths::create_dir_all_excluded_from_backups_atomic(root.as_path_unlocked())?;
155+
// Now that the excluded from backups target root is created we can create the
156+
// actual destination (sub)subdirectory.
157+
paths::create_dir_all(dest.as_path_unlocked())?;
157158

158159
// For now we don't do any more finer-grained locking on the artifact
159160
// directory, so just lock the entire thing for the duration of this
@@ -219,32 +220,3 @@ impl Layout {
219220
&self.build
220221
}
221222
}
222-
223-
#[cfg(not(target_os = "macos"))]
224-
fn exclude_from_backups(_: &Path) {}
225-
226-
#[cfg(target_os = "macos")]
227-
/// Marks files or directories as excluded from Time Machine on macOS
228-
///
229-
/// This is recommended to prevent derived/temporary files from bloating backups.
230-
fn exclude_from_backups(path: &Path) {
231-
use core_foundation::base::TCFType;
232-
use core_foundation::{number, string, url};
233-
use std::ptr;
234-
235-
// For compatibility with 10.7 a string is used instead of global kCFURLIsExcludedFromBackupKey
236-
let is_excluded_key: Result<string::CFString, _> = "NSURLIsExcludedFromBackupKey".parse();
237-
let path = url::CFURL::from_path(path, false);
238-
if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
239-
unsafe {
240-
url::CFURLSetResourcePropertyForKey(
241-
path.as_concrete_TypeRef(),
242-
is_excluded_key.as_concrete_TypeRef(),
243-
number::kCFBooleanTrue as *const _,
244-
ptr::null_mut(),
245-
);
246-
}
247-
}
248-
// Errors are ignored, since it's an optional feature and failure
249-
// doesn't prevent Cargo from working
250-
}

src/cargo/util/paths.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::iter;
77
use std::path::{Component, Path, PathBuf};
88

99
use filetime::FileTime;
10+
use tempfile::Builder as TempFileBuilder;
1011

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

@@ -457,3 +458,91 @@ pub fn strip_prefix_canonical<P: AsRef<Path>>(
457458
let canon_base = safe_canonicalize(base.as_ref());
458459
canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
459460
}
461+
462+
/// Creates an excluded from cache directory atomically with its parents as needed.
463+
///
464+
/// The atomicity only covers creating the leaf directory and exclusion from cache. Any missing
465+
/// parent directories will not be created in an atomic manner.
466+
///
467+
/// This function is idempotent and in addition to that it won't exclude ``p`` from cache if it
468+
/// already exists.
469+
pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> CargoResult<()> {
470+
let path = p.as_ref();
471+
if path.is_dir() {
472+
return Ok(());
473+
}
474+
475+
let parent = path.parent().unwrap();
476+
let base = path.file_name().unwrap();
477+
create_dir_all(parent)?;
478+
// We do this in two steps (first create a temporary directory and exlucde
479+
// it from backups, then rename it to the desired name. If we created the
480+
// directory directly where it should be and then excluded it from backups
481+
// we would risk a situation where cargo is interrupted right after the directory
482+
// creation but before the exclusion the the directory would remain non-excluded from
483+
// backups because we only perform exclusion right after we created the directory
484+
// ourselves.
485+
//
486+
// We need the tempdir created in parent instead of $TMP, because only then we can be
487+
// easily sure that rename() will succeed (the new name needs to be on the same mount
488+
// point as the old one).
489+
let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
490+
exclude_from_backups(&tempdir.path());
491+
// Previously std::fs::create_dir_all() (through paths::create_dir_all()) was used
492+
// here to create the directory directly and fs::create_dir_all() explicitly treats
493+
// the directory being created concurrently by another thread or process as success,
494+
// hence the check below to follow the existing behavior. If we get an error at
495+
// rename() and suddently the directory (which didn't exist a moment earlier) exists
496+
// we can infer from it it's another cargo process doing work.
497+
if let Err(e) = fs::rename(tempdir.path(), path) {
498+
if !path.exists() {
499+
return Err(anyhow::Error::from(e));
500+
}
501+
}
502+
Ok(())
503+
}
504+
505+
/// Marks the directory as excluded from archives/backups.
506+
///
507+
/// This is recommended to prevent derived/temporary files from bloating backups. There are two
508+
/// mechanisms used to achieve this right now:
509+
///
510+
/// * A dedicated resource property excluding from Time Machine backups on macOS
511+
/// * CACHEDIR.TAG files supported by various tools in a platform-independent way
512+
fn exclude_from_backups(path: &Path) {
513+
exclude_from_time_machine(path);
514+
let _ = std::fs::write(
515+
path.join("CACHEDIR.TAG"),
516+
"Signature: 8a477f597d28d172789f06886806bc55
517+
# This file is a cache directory tag created by cargo.
518+
# For information about cache directory tags see https://bford.info/cachedir/",
519+
);
520+
// Similarly to exclude_from_time_machine() we ignore errors here as it's an optional feature.
521+
}
522+
523+
#[cfg(not(target_os = "macos"))]
524+
fn exclude_from_time_machine(_: &Path) {}
525+
526+
#[cfg(target_os = "macos")]
527+
/// Marks files or directories as excluded from Time Machine on macOS
528+
fn exclude_from_time_machine(path: &Path) {
529+
use core_foundation::base::TCFType;
530+
use core_foundation::{number, string, url};
531+
use std::ptr;
532+
533+
// For compatibility with 10.7 a string is used instead of global kCFURLIsExcludedFromBackupKey
534+
let is_excluded_key: Result<string::CFString, _> = "NSURLIsExcludedFromBackupKey".parse();
535+
let path = url::CFURL::from_path(path, false);
536+
if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
537+
unsafe {
538+
url::CFURLSetResourcePropertyForKey(
539+
path.as_concrete_TypeRef(),
540+
is_excluded_key.as_concrete_TypeRef(),
541+
number::kCFBooleanTrue as *const _,
542+
ptr::null_mut(),
543+
);
544+
}
545+
}
546+
// Errors are ignored, since it's an optional feature and failure
547+
// doesn't prevent Cargo from working
548+
}

tests/testsuite/build.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5083,3 +5083,23 @@ fn reduced_reproduction_8249() {
50835083
p.cargo("check").run();
50845084
p.cargo("check").run();
50855085
}
5086+
5087+
#[cargo_test]
5088+
fn target_directory_backup_exclusion() {
5089+
let p = project()
5090+
.file("Cargo.toml", &basic_bin_manifest("foo"))
5091+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
5092+
.build();
5093+
5094+
// Newly created target/ should have CACHEDIR.TAG inside...
5095+
p.cargo("build").run();
5096+
let cachedir_tag = p.build_dir().join("CACHEDIR.TAG");
5097+
assert!(cachedir_tag.is_file());
5098+
assert!(fs::read_to_string(&cachedir_tag)
5099+
.unwrap()
5100+
.starts_with("Signature: 8a477f597d28d172789f06886806bc55"));
5101+
// ...but if target/ already exists CACHEDIR.TAG should not be created in it.
5102+
fs::remove_file(&cachedir_tag).unwrap();
5103+
p.cargo("build").run();
5104+
assert!(!&cachedir_tag.is_file());
5105+
}

tests/testsuite/clean.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,9 @@ fn assert_all_clean(build_dir: &Path) {
432432
}) {
433433
let entry = entry.unwrap();
434434
let path = entry.path();
435-
if let ".rustc_info.json" | ".cargo-lock" = path.file_name().unwrap().to_str().unwrap() {
435+
if let ".rustc_info.json" | ".cargo-lock" | "CACHEDIR.TAG" =
436+
path.file_name().unwrap().to_str().unwrap()
437+
{
436438
continue;
437439
}
438440
if path.is_symlink() || path.is_file() {

0 commit comments

Comments
 (0)