Skip to content

Commit 51245da

Browse files
committed
refactor(materialized_artifact): fluent builder for materialize
Replace `Artifact::materialize_in(dir, suffix, executable)` with a chain that reads like prose and labels the boolean at the call site: INTERPOSE_CDYLIB.materialize().suffix(".dll").at(path)? COREUTILS_BINARY.materialize().executable().at(dir)? `Artifact` becomes `Copy` so the builder can own it by value — no `&'a Artifact` in the type. The `executable` field is `#[cfg(unix)]` so it doesn't take up space on Windows; the method stays unconditional so cross-platform call sites don't need cfg'd guards.
1 parent 7d2f9a8 commit 51245da

5 files changed

Lines changed: 69 additions & 31 deletions

File tree

crates/fspy/src/unix/macos_artifacts.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ mod tests {
1414
#[test]
1515
fn coreutils_functions() {
1616
let tmpdir = tempfile::tempdir().unwrap();
17-
let coreutils_path = COREUTILS_BINARY.materialize_in(&tmpdir, "", true).unwrap();
17+
let coreutils_path = COREUTILS_BINARY.materialize().executable().at(&tmpdir).unwrap();
1818
let output = Command::new(coreutils_path).arg("--list").output().unwrap();
1919
let mut expected_functions: Vec<&str> = output
2020
.stdout

crates/fspy/src/unix/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ impl SpyImpl {
4949

5050
const PRELOAD_CDYLIB: Artifact = artifact!("fspy_preload");
5151

52-
let preload_cdylib_path = PRELOAD_CDYLIB.materialize_in(dir, ".dylib", false)?;
52+
let preload_cdylib_path = PRELOAD_CDYLIB.materialize().suffix(".dylib").at(dir)?;
5353
preload_cdylib_path.as_path().into()
5454
};
5555

@@ -59,8 +59,8 @@ impl SpyImpl {
5959
#[cfg(target_os = "macos")]
6060
artifacts: {
6161
let coreutils_path =
62-
macos_artifacts::COREUTILS_BINARY.materialize_in(dir, "", true)?;
63-
let bash_path = macos_artifacts::OILS_BINARY.materialize_in(dir, "", true)?;
62+
macos_artifacts::COREUTILS_BINARY.materialize().executable().at(dir)?;
63+
let bash_path = macos_artifacts::OILS_BINARY.materialize().executable().at(dir)?;
6464
Artifacts {
6565
bash_path: bash_path.as_path().into(),
6666
coreutils_path: coreutils_path.as_path().into(),

crates/fspy/src/windows/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub struct SpyImpl {
5151

5252
impl SpyImpl {
5353
pub fn init_in(path: &Path) -> io::Result<Self> {
54-
let dll_path = INTERPOSE_CDYLIB.materialize_in(path, ".dll", false)?;
54+
let dll_path = INTERPOSE_CDYLIB.materialize().suffix(".dll").at(path)?;
5555

5656
let wide_dll_path = dll_path.as_os_str().encode_wide().collect::<Vec<u16>>();
5757
let mut ansi_dll_path =

crates/materialized_artifact/src/lib.rs

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44
//! path, and helper binaries have to exist as actual files to be spawned —
55
//! but we want to ship a single executable. `materialized_artifact` embeds
66
//! the file content as a `&'static [u8]` at compile time via the
7-
//! [`artifact!`] macro (same as `include_bytes!`), and
8-
//! [`Artifact::materialize_in`] writes it out to disk when first needed — that
9-
//! materialization step is the value-add over a bare `include_bytes!`.
7+
//! [`artifact!`] macro (same as `include_bytes!`), and [`Materialize::at`]
8+
//! writes it out to disk when first needed — that materialization step is
9+
//! the value-add over a bare `include_bytes!`.
1010
//!
1111
//! Materialized files are named `{name}_{hash}{suffix}` in the caller-chosen
1212
//! directory. The hash (computed at build time by
1313
//! `materialized_artifact_build::register`) gives three properties without
1414
//! any coordination between processes:
1515
//!
16-
//! - **No repeated writes.** [`Artifact::materialize_in`] returns the existing
17-
//! path if the file is already there; repeated calls and re-runs skip I/O.
16+
//! - **No repeated writes.** [`Materialize::at`] returns the existing path if
17+
//! the file is already there; repeated calls and re-runs skip I/O.
1818
//! - **Correctness.** Two binaries with different embedded content produce
1919
//! different filenames, so a stale file from an older build is never
2020
//! mistaken for the current one.
@@ -28,11 +28,14 @@ use std::{
2828
path::{Path, PathBuf},
2929
};
3030

31-
/// A file embedded into the executable at compile time. Construct with
32-
/// [`artifact!`]; materialize to disk with [`Artifact::materialize_in`]. See the
33-
/// [crate docs] for the design rationale.
31+
/// A file embedded into the executable at compile time.
32+
///
33+
/// Construct with [`artifact!`]; materialize to disk via
34+
/// [`Artifact::materialize`] + [`Materialize::at`]. See the [crate docs] for
35+
/// the design rationale.
3436
///
3537
/// [crate docs]: crate
38+
#[derive(Clone, Copy)]
3639
pub struct Artifact {
3740
name: &'static str,
3841
content: &'static [u8],
@@ -64,14 +67,55 @@ impl Artifact {
6467
Self { name, content, hash }
6568
}
6669

67-
/// Ensure the artifact is materialized in `dir` under a content-addressed
68-
/// filename, writing it if missing. `executable` picks the Unix mode
69-
/// (`0o755` vs `0o644`) for newly created files, and reconciles an
70-
/// existing file's mode if it drifted. On non-Unix targets `executable`
71-
/// has no effect.
70+
/// Start a fluent materialize chain. Supply optional [`Materialize::suffix`]
71+
/// / [`Materialize::executable`] knobs, then terminate with
72+
/// [`Materialize::at`].
73+
pub const fn materialize(&self) -> Materialize<'static> {
74+
Materialize {
75+
artifact: *self,
76+
suffix: "",
77+
#[cfg(unix)]
78+
executable: false,
79+
}
80+
}
81+
}
82+
83+
/// Builder returned by [`Artifact::materialize`]. Terminate with
84+
/// [`Materialize::at`] to write the file.
85+
#[derive(Clone, Copy)]
86+
#[must_use = "materialize() only configures — call .at(dir) to write the file"]
87+
pub struct Materialize<'a> {
88+
artifact: Artifact,
89+
suffix: &'a str,
90+
#[cfg(unix)]
91+
executable: bool,
92+
}
93+
94+
impl<'a> Materialize<'a> {
95+
/// Filename suffix appended after `{name}_{hash}` (e.g. `.dll`, `.dylib`).
96+
/// Defaults to empty.
97+
pub const fn suffix(mut self, suffix: &'a str) -> Self {
98+
self.suffix = suffix;
99+
self
100+
}
101+
102+
/// Mark the materialized file as executable (`0o755` on Unix; no-op on
103+
/// Windows where the filesystem has no executable bit).
104+
pub const fn executable(mut self) -> Self {
105+
#[cfg(unix)]
106+
{
107+
self.executable = true;
108+
}
109+
self
110+
}
111+
112+
/// Materialize the artifact in `dir` under a content-addressed filename,
113+
/// writing it if missing. On Unix, newly created files get `0o755` when
114+
/// [`Materialize::executable`] was called and `0o644` otherwise, and an
115+
/// existing file's mode is reconciled if it drifted.
72116
///
73117
/// Returns the final path. If the target already exists and its mode
74-
/// already matches `executable`, no I/O beyond the stat is performed.
118+
/// already matches, no I/O beyond the stat is performed.
75119
///
76120
/// # Preconditions
77121
///
@@ -82,19 +126,13 @@ impl Artifact {
82126
/// Returns an error if the directory can't be read/written, the stat
83127
/// fails for any reason other than not-found, or the temp-file rename
84128
/// fails and the destination still doesn't exist.
85-
pub fn materialize_in(
86-
&self,
87-
dir: impl AsRef<Path>,
88-
suffix: &str,
89-
executable: bool,
90-
) -> io::Result<PathBuf> {
129+
pub fn at(self, dir: impl AsRef<Path>) -> io::Result<PathBuf> {
91130
let dir = dir.as_ref();
92-
let path = dir.join(format!("{}_{}{}", self.name, self.hash, suffix));
131+
let path =
132+
dir.join(format!("{}_{}{}", self.artifact.name, self.artifact.hash, self.suffix));
93133

94134
#[cfg(unix)]
95-
let want_mode: u32 = if executable { 0o755 } else { 0o644 };
96-
#[cfg(not(unix))]
97-
let _ = executable; // Unix-mode concept; no-op on Windows.
135+
let want_mode: u32 = if self.executable { 0o755 } else { 0o644 };
98136

99137
// Fast path: one stat tells us both whether the file exists and,
100138
// on Unix, what its permission bits are. The content is assumed
@@ -138,7 +176,7 @@ impl Artifact {
138176
};
139177
#[cfg(not(unix))]
140178
let mut tmp = tempfile::NamedTempFile::new_in(dir)?;
141-
tmp.as_file_mut().write_all(self.content)?;
179+
tmp.as_file_mut().write_all(self.artifact.content)?;
142180

143181
// `persist_noclobber` (link+unlink on Unix, MoveFileExW without
144182
// REPLACE_EXISTING on Windows) fails atomically if the destination

crates/materialized_artifact_build/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub const ENV_PREFIX: &str = "MATERIALIZED_ARTIFACT_";
1515
/// these at compile time via `include_bytes!(env!(…))` and `env!(…)`.
1616
///
1717
/// `name` is used both as the env-var key and as the on-disk filename prefix
18-
/// (in `Artifact::materialize_in`), so it must be a valid identifier-like string
18+
/// (in `Materialize::at`), so it must be a valid identifier-like string
1919
/// that matches the one passed to `artifact!`.
2020
///
2121
/// # Panics

0 commit comments

Comments
 (0)