@@ -7,6 +7,7 @@ use std::iter;
7
7
use std:: path:: { Component , Path , PathBuf } ;
8
8
9
9
use filetime:: FileTime ;
10
+ use tempfile:: Builder as TempFileBuilder ;
10
11
11
12
use crate :: util:: errors:: { CargoResult , CargoResultExt } ;
12
13
@@ -457,3 +458,91 @@ pub fn strip_prefix_canonical<P: AsRef<Path>>(
457
458
let canon_base = safe_canonicalize ( base. as_ref ( ) ) ;
458
459
canon_path. strip_prefix ( canon_base) . map ( |p| p. to_path_buf ( ) )
459
460
}
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
+ }
0 commit comments