Skip to content

Commit 7d2f9a8

Browse files
committed
docs(materialized_artifact): explain the atomic-write invariants
Spell out why the temp file lives in the destination directory (single-filesystem requirement for atomic rename), why the Unix mode is set via `Builder::permissions` (no window with wrong bits), and why we use `persist_noclobber` (atomic link-or-fail, race-safe).
1 parent 9eb4b53 commit 7d2f9a8

2 files changed

Lines changed: 12 additions & 4 deletions

File tree

crates/fspy/src/unix/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ impl SpyImpl {
5858
preload_path,
5959
#[cfg(target_os = "macos")]
6060
artifacts: {
61-
let coreutils_path = macos_artifacts::COREUTILS_BINARY.materialize_in(dir, "", true)?;
61+
let coreutils_path =
62+
macos_artifacts::COREUTILS_BINARY.materialize_in(dir, "", true)?;
6263
let bash_path = macos_artifacts::OILS_BINARY.materialize_in(dir, "", true)?;
6364
Artifacts {
6465
bash_path: bash_path.as_path().into(),

crates/materialized_artifact/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,15 @@ impl Artifact {
123123
}
124124

125125
// Slow path: write to a unique temp file in the same directory, then
126-
// rename into place atomically. `NamedTempFile`'s `Drop` removes the
127-
// temp if we bail before `persist_noclobber`, avoiding orphaned files
128-
// on errors.
126+
// rename into place atomically. The temp must live in `dir` (not the
127+
// system temp) so the final rename stays within one filesystem — cross-
128+
// filesystem rename isn't atomic. `NamedTempFile`'s `Drop` removes the
129+
// temp on any early return, so we never leak partial files on error.
129130
#[cfg(unix)]
130131
let mut tmp = {
131132
use std::os::unix::fs::PermissionsExt;
133+
// `Builder::permissions` sets the mode at open(2) time, so there's
134+
// no window where the temp exists with the wrong bits.
132135
tempfile::Builder::new()
133136
.permissions(fs::Permissions::from_mode(want_mode))
134137
.tempfile_in(dir)?
@@ -137,6 +140,10 @@ impl Artifact {
137140
let mut tmp = tempfile::NamedTempFile::new_in(dir)?;
138141
tmp.as_file_mut().write_all(self.content)?;
139142

143+
// `persist_noclobber` (link+unlink on Unix, MoveFileExW without
144+
// REPLACE_EXISTING on Windows) fails atomically if the destination
145+
// already exists — so two racing processes can't clobber each other
146+
// mid-write, and the loser sees the error below.
140147
if let Err(err) = tmp.persist_noclobber(&path) {
141148
// If another process won the race and the destination now exists,
142149
// treat that as success; `err.file` drops here, cleaning up our

0 commit comments

Comments
 (0)