create_output_dir/lib.rs
1//! This crate provides only one function, `create_output_dir` which creates an excluded from cache
2//! directory atomically with its parents as needed.
3//!
4//! The source code of this crate has been almost verbatim copy-pasted from
5//! [`cargo_util::paths::create_dir_all_excluded_from_backups_atomic`][cargo-util-fn].
6//!
7//! [cargo-util-fn]: https://docs.rs/cargo-util/latest/cargo_util/paths/fn.create_dir_all_excluded_from_backups_atomic.html
8
9use std::ffi::OsStr;
10use std::path::Path;
11use std::{env, fs};
12
13use anyhow::{Context, Result};
14
15/// Creates an excluded from cache directory atomically with its parents as needed.
16///
17/// The atomicity only covers creating the leaf directory and exclusion from cache. Any missing
18/// parent directories will not be created in an atomic manner.
19///
20/// This function is idempotent and in addition to that it won't exclude `path` from cache if it
21/// already exists.
22pub fn create_output_dir(path: &Path) -> Result<()> {
23 if path.is_dir() {
24 return Ok(());
25 }
26
27 let parent = path.parent().unwrap();
28 let base = path.file_name().unwrap();
29 fs::create_dir_all(parent)
30 .with_context(|| format!("failed to create directory `{}`", parent.display()))?;
31
32 // We do this in two steps: first create a temporary directory and exclude it from backups,
33 // then rename it to the desired name.
34 // If we created the directory directly where it should be and then excluded it from backups
35 // we would risk a situation where the application is interrupted right after the directory
36 // creation, but before the exclusion, and the directory would remain non-excluded from backups.
37 //
38 // We need a temporary directory created in `parent` instead of `$TMP`, because only then we
39 // can be easily sure that `fs::rename()` will succeed (the new name needs to be on the same
40 // mount point as the old one).
41 let tempdir = tempfile::Builder::new().prefix(base).tempdir_in(parent)?;
42 exclude_from_backups(tempdir.path());
43 exclude_from_content_indexing(tempdir.path());
44
45 // Previously `fs::create_dir_all()` was used here to create the directory directly and
46 // `fs::create_dir_all()` explicitly treats the directory being created concurrently by another
47 // thread or process as success, hence the check below to follow the existing behavior.
48 // If we get an error at `fs::rename()` and suddenly the directory (which didn't exist a moment
49 // earlier) exists we can infer from it that another application process is doing work here.
50 if let Err(e) = fs::rename(tempdir.path(), path) {
51 if !path.exists() {
52 return Err(e.into());
53 }
54 }
55
56 Ok(())
57}
58
59/// Marks the directory as excluded from archives/backups.
60///
61/// This is recommended to prevent derived/temporary files from bloating backups.
62/// There are two mechanisms used to achieve this right now:
63/// * A dedicated resource property excluding from Time Machine backups on macOS.
64/// * `CACHEDIR.TAG` files supported by various tools in a platform-independent way.
65fn exclude_from_backups(path: &Path) {
66 exclude_from_time_machine(path);
67 let _ = fs::write(
68 path.join("CACHEDIR.TAG"),
69 format!(
70 "Signature: 8a477f597d28d172789f06886806bc55
71# This file is a cache directory tag{}.
72# For information about cache directory tags see https://bford.info/cachedir/
73",
74 match guess_application_name() {
75 None => String::new(),
76 Some(name) => format!(" created by {}", name),
77 }
78 ),
79 );
80 // Similarly to exclude_from_time_machine() we ignore errors here as it's an optional feature.
81}
82
83/// Marks the directory as excluded from content indexing.
84///
85/// This is recommended to prevent the content of derived/temporary files from being indexed.
86/// This is very important for Windows users, as the live content indexing may significantly slow
87/// I/O operations or compilers etc.
88///
89/// This is currently a no-op on non-Windows platforms.
90fn exclude_from_content_indexing(path: &Path) {
91 #[cfg(windows)]
92 {
93 use std::iter::once;
94 use std::os::windows::prelude::OsStrExt;
95 use winapi::um::fileapi::{GetFileAttributesW, SetFileAttributesW};
96 use winapi::um::winnt::FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
97
98 let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
99 unsafe {
100 SetFileAttributesW(
101 path.as_ptr(),
102 GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
103 );
104 }
105 }
106 #[cfg(not(windows))]
107 {
108 let _ = path;
109 }
110}
111
112#[cfg(not(target_os = "macos"))]
113fn exclude_from_time_machine(_: &Path) {}
114
115/// Marks files or directories as excluded from Time Machine on macOS.
116#[cfg(target_os = "macos")]
117fn exclude_from_time_machine(path: &Path) {
118 use core_foundation::base::TCFType;
119 use core_foundation::{number, string, url};
120 use std::ptr;
121
122 // For compatibility with 10.7 a string is used instead of global kCFURLIsExcludedFromBackupKey
123 let is_excluded_key: std::result::Result<string::CFString, _> =
124 "NSURLIsExcludedFromBackupKey".parse();
125 let path = url::CFURL::from_path(path, false);
126 if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
127 unsafe {
128 url::CFURLSetResourcePropertyForKey(
129 path.as_concrete_TypeRef(),
130 is_excluded_key.as_concrete_TypeRef(),
131 number::kCFBooleanTrue as *const _,
132 ptr::null_mut(),
133 );
134 }
135 }
136 // Errors are ignored, since it's an optional feature and failure
137 // shouldn't prevent applications from working.
138}
139
140fn guess_application_name() -> Option<String> {
141 let exe = env::current_exe().ok()?;
142 let file_name = if exe.extension() == Some(OsStr::new(env::consts::EXE_EXTENSION)) {
143 exe.file_stem()
144 } else {
145 exe.file_name()
146 }?;
147 Some(file_name.to_string_lossy().into_owned())
148}