1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! What:
//! One copy of the built executable per build on the fixtures' filesystem,
//! for fixtures that cannot hard-link the build itself.
//! Why:
//! Cargo's scratch directory moves with `build.build-dir`, which this repository's hosts relocate to another volume
//! than the final artifacts,
//! and a hard link cannot cross volumes.
//! Every fixture then copied the 22 MB debug executable:
//! a wave of 169 tests wrote about 3.7 GB (169 times 21.9 MB) within seconds, on the volume that every other command of
//! those tests syncs to; the failing runs that carried the "no hard link" message hit the shared 5-second bound.
//! The copy is published without a lock:
//! each caller that finds no stored copy writes its own uniquely named partial file and links it to the final name,
//! which fails if another caller published first, so the final name is only ever a complete file and the first
//! publisher wins.
//! Callers that start together may each write a partial copy once per build; every later caller finds the final file.
//! The final name is the content of the build, not its size or time, and a copy is checked against that name before
//! it is published.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! if (!exists(stored)) { await copy(built, partial); try { await link(partial, stored); } catch (AlreadyExists) {} await unlink(partial); }
//! ```
/// Native path and file types.
use std::path::{Path, PathBuf};
/// Distinguishes the partial files of callers in one process.
use std::sync::atomic::{AtomicUsize, Ordering};
/// The name of the build, computed once per process.
use std::sync::OnceLock;
/// Ages.
use std::time::Duration;
/// The directory of a fixture root that holds the stored copies.
pub const STORE_DIRECTORY: &str = "wrapper-store";
/// Prefix of a copy in progress; a finished copy is linked to its final name, so a name with this prefix is never used.
pub const PARTIAL_PREFIX: &str = ".partial-";
/// How long a copy of another build must go unlinked before a publisher removes it. Each fixture's link changes the
/// copy's status-change time, so the age is the time since a fixture last linked it; a run that links a copy does so
/// within seconds of finding it, far inside this.
pub const IDLE_BUILD_AGE: Duration = Duration::from_secs(24 * 3600);
/// How many times a caller asks for the stored copy when the link finds it gone.
const LINK_ATTEMPTS: usize = 3;
/// The number of partial files this process has started, which keeps their names unique.
static PARTIALS_STARTED: AtomicUsize = AtomicUsize::new(0);
/// The stored name of the built executable, read once per process.
static BUILD_NAME: OnceLock<String> = OnceLock::new();
/// How many bytes one read takes, so a build of any size is hashed in bounded memory.
const CHUNK_BYTES: usize = 1 << 20;
/// What:
/// The name that identifies the bytes of a file:
/// a 64-bit hash of them and their length.
/// Why:
/// Length and modification time do not identify a build: two different builds of one length can share a timestamp.
/// The hash is `DefaultHasher` (SipHash), which runs at the speed of the optimized standard library even in a debug
/// test build, and it guards against accidental equality, not against someone who crafts a file; its value is stable
/// only within one toolchain, which is enough, since a different toolchain just stores another copy.
///
/// # Errors
/// When the file cannot be read.
pub fn content_name(path: &Path) -> std::io::Result<String> {
use std::hash::Hasher;
use std::io::Read;
let mut file: std::fs::File = std::fs::File::open(path)?;
let mut hasher: std::hash::DefaultHasher = std::hash::DefaultHasher::new();
let mut chunk: Vec<u8> = vec![0; CHUNK_BYTES];
let mut length: u64 = 0;
loop {
let read: usize = file.read(chunk.as_mut_slice())?;
if read == 0 {
break;
}
hasher.write(&chunk[..read]);
length += u64::try_from(read).map_err(std::io::Error::other)?;
}
return Ok(format!("{:016x}-{length}-cli-git-native", hasher.finish()));
}
/// The name of the build `built`, read once per process.
fn build_name(built: &Path) -> std::io::Result<String> {
if let Some(known) = BUILD_NAME.get() {
return Ok(known.clone());
}
let computed: String = content_name(built)?;
return Ok(BUILD_NAME.get_or_init(|| { return computed; }).clone());
}
/// What:
/// The path of the stored copy of `built` under `root`, made if it does not exist yet.
/// Why:
/// Fixtures on `root`'s filesystem link this file. The partial copy is read back and must have the build's name
/// before it is linked to the stored name, never renamed over it, so a caller that loses the race leaves the
/// winner's file alone and uses it, and a copy made from a build that changed under the caller is never published.
/// Copies of other builds that no fixture has linked for `IDLE_BUILD_AGE` are removed once a new copy stands.
///
/// # Errors
/// When the store cannot be created or written, the copy fails, or the copy does not have the build's name.
pub fn shared_copy(built: &Path, root: &Path) -> std::io::Result<PathBuf> {
let store: PathBuf = root.join(STORE_DIRECTORY);
std::fs::create_dir_all(&store)?;
let name: String = build_name(built)?;
let stored: PathBuf = store.join(&name);
if stored.exists() {
return Ok(stored);
}
let partial: PathBuf = store.join(format!(
"{PARTIAL_PREFIX}{}-{}",
std::process::id(),
PARTIALS_STARTED.fetch_add(1, Ordering::SeqCst)
));
super::support::copy_executable(built, partial.as_path());
if content_name(partial.as_path())? != name {
let _ = std::fs::remove_file(&partial);
return Err(std::io::Error::other(format!(
"{} changed while it was copied: the copy does not have the name {name} read before",
built.display()
)));
}
publish(partial.as_path(), stored.as_path())?;
remove_idle_builds(store.as_path(), stored.as_path(), IDLE_BUILD_AGE);
return Ok(stored);
}
/// What:
/// Make the finished `partial` file the stored copy unless a stored copy already exists, and remove `partial` either way.
/// Why:
/// A hard link fails with `AlreadyExists` when another caller published first, and then that caller's file stays the
/// stored copy: a rename would replace it under the links the other caller's fixtures already hold. Taking the
/// winner's file is correct because both have the build's name.
///
/// # Errors
/// When the link fails for a reason other than the stored copy existing.
pub fn publish(partial: &Path, stored: &Path) -> std::io::Result<()> {
let published: std::io::Result<()> = match std::fs::hard_link(partial, stored) {
Err(error) if error.kind() != std::io::ErrorKind::AlreadyExists => Err(error),
_ => Ok(()),
};
if let Err(error) = std::fs::remove_file(partial) {
eprintln!("wrapper store: could not remove {} ({error})", partial.display());
}
return published;
}
/// What:
/// Hard-link the stored copy of `built` at `to`, asking for the copy again if it is gone before the link.
/// Why:
/// A copy of another build can be removed by a publisher between a caller's getting its path and linking it (it
/// must have gone unlinked for `IDLE_BUILD_AGE`, so this is rare, but not impossible); the caller then makes or finds
/// the copy again instead of falling back to a copy of its own. `after_found` runs between the two steps so a control
/// can remove the copy at exactly that moment.
///
/// # Errors
/// When the stored copy cannot be made, the link fails for another reason than the copy being gone (a link across
/// filesystems included), or the copy is gone on every attempt.
pub fn link_stored(
built: &Path,
root: &Path,
to: &Path,
after_found: &mut dyn FnMut(&Path),
) -> std::io::Result<()> {
for _ in 0..LINK_ATTEMPTS {
let stored: PathBuf = shared_copy(built, root)?;
after_found(stored.as_path());
match std::fs::hard_link(&stored, to) {
Ok(()) => return Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound && !stored.exists() => {}
Err(error) => return Err(error),
}
}
return Err(std::io::Error::other(format!(
"the stored copy of {} was gone {LINK_ATTEMPTS} times before it could be linked",
built.display()
)));
}
/// Remove every finished copy in the store except `keep` that no fixture has linked for `idle`, and partial files
/// that old (a dead caller's); a name that cannot be removed stays. A younger partial file, a live caller's, is
/// never touched.
pub fn remove_idle_builds(store: &Path, keep: &Path, idle: Duration) {
use std::os::unix::fs::MetadataExt;
let Ok(entries) = std::fs::read_dir(store) else {
return;
};
let now: i64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |since| { return i64::try_from(since.as_secs()).unwrap_or(i64::MAX); });
let idle_seconds: i64 = i64::try_from(idle.as_secs()).unwrap_or(i64::MAX);
for entry in entries.flatten() {
let path: PathBuf = entry.path();
let is_partial: bool = entry.file_name().to_string_lossy().starts_with(PARTIAL_PREFIX);
let idle_long_enough: bool = entry
.metadata()
.is_ok_and(|metadata| { return now.saturating_sub(metadata.ctime()) >= idle_seconds; });
if (is_partial || path != keep) && idle_long_enough
&& let Err(error) = std::fs::remove_file(&path)
&& error.kind() != std::io::ErrorKind::NotFound
{
eprintln!("wrapper store: could not remove {} ({error})", path.display());
}
}
}