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 ) ]
3639pub 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
0 commit comments