From acdd201e7a6deb41fcf21434a834dade29b1e176 Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 17 Apr 2026 21:17:23 +0800 Subject: [PATCH 01/19] refactor: extract artifact.rs into embedded_artifact crate Split the embedded-binary helper out of fspy so other crates (upcoming vite_task dylib embedding) can reuse it. No behavior change; the Artifact struct, write_to logic, and artifact! macro move verbatim. Minor: Artifact::new now carries #[must_use], write_to gets an `# Errors` section for clippy. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 8 +++++++ Cargo.toml | 1 + crates/embedded_artifact/.clippy.toml | 1 + crates/embedded_artifact/Cargo.toml | 16 ++++++++++++++ crates/embedded_artifact/README.md | 3 +++ .../src/lib.rs} | 22 +++++++++++++------ crates/fspy/Cargo.toml | 1 + crates/fspy/src/lib.rs | 5 ----- crates/fspy/src/unix/macos_artifacts.rs | 2 +- crates/fspy/src/unix/mod.rs | 13 +++++------ crates/fspy/src/windows/mod.rs | 2 +- 11 files changed, 53 insertions(+), 21 deletions(-) create mode 120000 crates/embedded_artifact/.clippy.toml create mode 100644 crates/embedded_artifact/Cargo.toml create mode 100644 crates/embedded_artifact/README.md rename crates/{fspy/src/artifact.rs => embedded_artifact/src/lib.rs} (71%) diff --git a/Cargo.lock b/Cargo.lock index 9609dcde9..ec3021b44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -997,6 +997,13 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" +[[package]] +name = "embedded_artifact" +version = "0.0.0" +dependencies = [ + "rand 0.9.2", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -1188,6 +1195,7 @@ dependencies = [ "csv-async", "ctor", "derive_more", + "embedded_artifact", "flate2", "fspy_detours_sys", "fspy_preload_unix", diff --git a/Cargo.toml b/Cargo.toml index efeba969d..894169848 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ derive_more = "2.0.1" diff-struct = "0.5.3" directories = "6.0.0" elf = { version = "0.8.0", default-features = false } +embedded_artifact = { path = "crates/embedded_artifact" } flate2 = "1.0.35" fspy = { path = "crates/fspy" } fspy_detours_sys = { path = "crates/fspy_detours_sys" } diff --git a/crates/embedded_artifact/.clippy.toml b/crates/embedded_artifact/.clippy.toml new file mode 120000 index 000000000..c7929b369 --- /dev/null +++ b/crates/embedded_artifact/.clippy.toml @@ -0,0 +1 @@ +../../.non-vite.clippy.toml \ No newline at end of file diff --git a/crates/embedded_artifact/Cargo.toml b/crates/embedded_artifact/Cargo.toml new file mode 100644 index 000000000..4e116fff2 --- /dev/null +++ b/crates/embedded_artifact/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "embedded_artifact" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[dependencies] +rand = { workspace = true } + +[lints] +workspace = true + +[lib] +doctest = false diff --git a/crates/embedded_artifact/README.md b/crates/embedded_artifact/README.md new file mode 100644 index 000000000..b232849cf --- /dev/null +++ b/crates/embedded_artifact/README.md @@ -0,0 +1,3 @@ +# embedded_artifact + +Binary assets embedded in an executable and extracted to disk on demand, with content-addressed filenames so repeated extractions reuse the same file. diff --git a/crates/fspy/src/artifact.rs b/crates/embedded_artifact/src/lib.rs similarity index 71% rename from crates/fspy/src/artifact.rs rename to crates/embedded_artifact/src/lib.rs index 0c3fcba0e..f0cb1ec42 100644 --- a/crates/fspy/src/artifact.rs +++ b/crates/embedded_artifact/src/lib.rs @@ -11,12 +11,14 @@ pub struct Artifact { pub hash: &'static str, } -#[cfg(target_os = "macos")] -#[doc(hidden)] +/// Declare an [`Artifact`] whose content and hash live in the caller's `OUT_DIR`. +/// +/// Expects the build script to have written two files: +/// `$OUT_DIR/{name}` (the raw bytes) and `$OUT_DIR/{name}.hash` (the hex hash). #[macro_export] macro_rules! artifact { ($name: literal) => { - $crate::artifact::Artifact::new( + $crate::Artifact::new( $name, ::core::include_bytes!(::core::concat!(::core::env!("OUT_DIR"), "/", $name)), ::core::include_str!(::core::concat!(::core::env!("OUT_DIR"), "/", $name, ".hash")), @@ -24,15 +26,21 @@ macro_rules! artifact { }; } -#[cfg(target_os = "macos")] -pub use artifact; - impl Artifact { - #[cfg(not(target_os = "linux"))] + #[must_use] pub const fn new(name: &'static str, content: &'static [u8], hash: &'static str) -> Self { Self { name, content, hash } } + /// Write the artifact's content to `dir` under a content-addressed filename. + /// + /// Returns the final path. If a file with the same hash already exists at + /// the target path, it is reused without rewriting. + /// + /// # Errors + /// + /// Returns an error if the directory can't be read/written, or if the + /// temp-file rename fails and the destination still doesn't exist. pub fn write_to(&self, dir: impl AsRef, suffix: &str) -> io::Result { let dir = dir.as_ref(); let path = dir.join(format!("{}_{}{}", self.name, self.hash, suffix)); diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index a9a1ffffa..81b6c24eb 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -11,6 +11,7 @@ bstr = { workspace = true, default-features = false } bumpalo = { workspace = true } const_format = { workspace = true, features = ["fmt"] } derive_more = { workspace = true, features = ["debug"] } +embedded_artifact = { workspace = true } fspy_shared = { workspace = true } futures-util = { workspace = true } libc = { workspace = true } diff --git a/crates/fspy/src/lib.rs b/crates/fspy/src/lib.rs index 7acabe793..13ff2055b 100644 --- a/crates/fspy/src/lib.rs +++ b/crates/fspy/src/lib.rs @@ -1,11 +1,6 @@ #![cfg_attr(target_os = "windows", feature(windows_process_extensions_main_thread_handle))] #![feature(once_cell_try)] -// Persist the injected DLL/shared library somewhere in the filesystem. -// Not needed on musl (seccomp-only tracking). -#[cfg(not(target_env = "musl"))] -mod artifact; - pub mod error; #[cfg(not(target_env = "musl"))] diff --git a/crates/fspy/src/unix/macos_artifacts.rs b/crates/fspy/src/unix/macos_artifacts.rs index 70ee101eb..54e53f21d 100644 --- a/crates/fspy/src/unix/macos_artifacts.rs +++ b/crates/fspy/src/unix/macos_artifacts.rs @@ -1,4 +1,4 @@ -use crate::artifact::{Artifact, artifact}; +use embedded_artifact::{Artifact, artifact}; pub const COREUTILS_BINARY: Artifact = artifact!("coreutils"); pub const OILS_BINARY: Artifact = artifact!("oils-for-unix"); diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index ba0516301..fffddb6d2 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -49,15 +49,14 @@ impl SpyImpl { #[cfg(not(target_env = "musl"))] let preload_path = { use const_format::formatcp; + use embedded_artifact::Artifact; use xxhash_rust::const_xxh3::xxh3_128; - use crate::artifact::Artifact; - - const PRELOAD_CDYLIB: Artifact = Artifact { - name: "fspy_preload", - content: PRELOAD_CDYLIB_BINARY, - hash: formatcp!("{:x}", xxh3_128(PRELOAD_CDYLIB_BINARY)), - }; + const PRELOAD_CDYLIB: Artifact = Artifact::new( + "fspy_preload", + PRELOAD_CDYLIB_BINARY, + formatcp!("{:x}", xxh3_128(PRELOAD_CDYLIB_BINARY)), + ); let preload_cdylib_path = PRELOAD_CDYLIB.write_to(dir, ".dylib")?; preload_cdylib_path.as_path().into() diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 93bef864c..df99cbc6e 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -7,6 +7,7 @@ use std::{ }; use const_format::formatcp; +use embedded_artifact::Artifact; use fspy_detours_sys::{DetourCopyPayloadToProcess, DetourUpdateProcessWithDll}; use fspy_shared::{ ipc::{PathAccess, channel::channel}, @@ -23,7 +24,6 @@ use xxhash_rust::const_xxh3::xxh3_128; use crate::{ ChildTermination, TrackedChild, - artifact::Artifact, command::Command, error::SpawnError, ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}, From 857646325506bbf94b32c4b96c179cbc61050c3b Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 17 Apr 2026 21:38:59 +0800 Subject: [PATCH 02/19] refactor(embedded_artifact): add artifact_of! macro (hashes inline) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move `formatcp!` + `xxh3_128` into `embedded_artifact` behind two macros: - `artifact_of!(name, bytes)` — new; hashes the bytes at compile time. Used for small embedded artifacts (preload dylibs). - `artifact!(name)` — unchanged; still reads `{name}.hash` from OUT_DIR. Used for large artifacts where the build script has the bytes and can hash them cheaply, avoiding slow const-time hashing at compile time. Fspy's preload-dylib call sites now use `artifact_of!` and drop the direct `const_format` + `xxhash-rust` regular deps. macOS download script keeps writing `.hash` files for `artifact!`. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 3 ++- crates/embedded_artifact/Cargo.toml | 2 ++ crates/embedded_artifact/src/lib.rs | 31 ++++++++++++++++++++++++++--- crates/fspy/Cargo.toml | 2 -- crates/fspy/src/unix/mod.rs | 12 +++-------- crates/fspy/src/windows/mod.rs | 10 ++-------- 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec3021b44..63da70dba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1001,7 +1001,9 @@ checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" name = "embedded_artifact" version = "0.0.0" dependencies = [ + "const_format", "rand 0.9.2", + "xxhash-rust", ] [[package]] @@ -1191,7 +1193,6 @@ dependencies = [ "anyhow", "bstr", "bumpalo", - "const_format", "csv-async", "ctor", "derive_more", diff --git a/crates/embedded_artifact/Cargo.toml b/crates/embedded_artifact/Cargo.toml index 4e116fff2..42c805a08 100644 --- a/crates/embedded_artifact/Cargo.toml +++ b/crates/embedded_artifact/Cargo.toml @@ -7,7 +7,9 @@ publish = false rust-version.workspace = true [dependencies] +const_format = { workspace = true, features = ["fmt"] } rand = { workspace = true } +xxhash-rust = { workspace = true, features = ["const_xxh3"] } [lints] workspace = true diff --git a/crates/embedded_artifact/src/lib.rs b/crates/embedded_artifact/src/lib.rs index f0cb1ec42..07b0d2d43 100644 --- a/crates/embedded_artifact/src/lib.rs +++ b/crates/embedded_artifact/src/lib.rs @@ -11,10 +11,29 @@ pub struct Artifact { pub hash: &'static str, } -/// Declare an [`Artifact`] whose content and hash live in the caller's `OUT_DIR`. +/// Construct an [`Artifact`] from a `&'static [u8]` expression; the hash is +/// computed from the bytes at compile time. /// -/// Expects the build script to have written two files: -/// `$OUT_DIR/{name}` (the raw bytes) and `$OUT_DIR/{name}.hash` (the hex hash). +/// Only use this for small artifacts — const-time hashing of large payloads +/// significantly slows compilation. For large artifacts, pre-compute the hash +/// in a build script and use [`artifact!`]. +#[macro_export] +macro_rules! artifact_of { + ($name: literal, $content: expr) => { + $crate::Artifact::new( + $name, + $content, + $crate::__private::formatcp!("{:x}", $crate::__private::xxh3_128($content)), + ) + }; +} + +/// Construct an [`Artifact`] from a file in the caller's `OUT_DIR`. The build +/// script is expected to have written `$OUT_DIR/{name}` (the raw bytes) and +/// `$OUT_DIR/{name}.hash` (the hex hash). +/// +/// Use this for large artifacts where the build script already has the bytes +/// and can hash them cheaply — avoiding const-time hashing at compile time. #[macro_export] macro_rules! artifact { ($name: literal) => { @@ -26,6 +45,12 @@ macro_rules! artifact { }; } +#[doc(hidden)] +pub mod __private { + pub use const_format::formatcp; + pub use xxhash_rust::const_xxh3::xxh3_128; +} + impl Artifact { #[must_use] pub const fn new(name: &'static str, content: &'static [u8], hash: &'static str) -> Self { diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index 81b6c24eb..b3b485640 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -9,7 +9,6 @@ allocator-api2 = { workspace = true, features = ["alloc"] } wincode = { workspace = true } bstr = { workspace = true, default-features = false } bumpalo = { workspace = true } -const_format = { workspace = true, features = ["fmt"] } derive_more = { workspace = true, features = ["debug"] } embedded_artifact = { workspace = true } fspy_shared = { workspace = true } @@ -23,7 +22,6 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["net", "process", "io-util", "sync", "rt"] } tokio-util = { workspace = true } which = { workspace = true, features = ["tracing"] } -xxhash-rust = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] fspy_seccomp_unotify = { workspace = true, features = ["supervisor"] } diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index fffddb6d2..6bb216e9b 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -48,15 +48,9 @@ impl SpyImpl { pub fn init_in(#[cfg_attr(target_env = "musl", allow(unused))] dir: &Path) -> io::Result { #[cfg(not(target_env = "musl"))] let preload_path = { - use const_format::formatcp; - use embedded_artifact::Artifact; - use xxhash_rust::const_xxh3::xxh3_128; - - const PRELOAD_CDYLIB: Artifact = Artifact::new( - "fspy_preload", - PRELOAD_CDYLIB_BINARY, - formatcp!("{:x}", xxh3_128(PRELOAD_CDYLIB_BINARY)), - ); + use embedded_artifact::{Artifact, artifact_of}; + + const PRELOAD_CDYLIB: Artifact = artifact_of!("fspy_preload", PRELOAD_CDYLIB_BINARY); let preload_cdylib_path = PRELOAD_CDYLIB.write_to(dir, ".dylib")?; preload_cdylib_path.as_path().into() diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index df99cbc6e..722efa93d 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -6,8 +6,7 @@ use std::{ sync::Arc, }; -use const_format::formatcp; -use embedded_artifact::Artifact; +use embedded_artifact::{Artifact, artifact_of}; use fspy_detours_sys::{DetourCopyPayloadToProcess, DetourUpdateProcessWithDll}; use fspy_shared::{ ipc::{PathAccess, channel::channel}, @@ -20,7 +19,6 @@ use winapi::{ um::{processthreadsapi::ResumeThread, winbase::CREATE_SUSPENDED}, }; use winsafe::co::{CP, WC}; -use xxhash_rust::const_xxh3::xxh3_128; use crate::{ ChildTermination, TrackedChild, @@ -30,11 +28,7 @@ use crate::{ }; const PRELOAD_CDYLIB_BINARY: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_FSPY_PRELOAD_WINDOWS")); -const INTERPOSE_CDYLIB: Artifact = Artifact::new( - "fsyp_preload", - PRELOAD_CDYLIB_BINARY, - formatcp!("{:x}", xxh3_128(PRELOAD_CDYLIB_BINARY)), -); +const INTERPOSE_CDYLIB: Artifact = artifact_of!("fsyp_preload", PRELOAD_CDYLIB_BINARY); pub struct PathAccessIterable { ipc_receiver_lock_guard: OwnedReceiverLockGuard, From 669499e5cac71364472884e37971b1279807cb4b Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 17 Apr 2026 22:04:37 +0800 Subject: [PATCH 03/19] feat: add embedded_artifact_build crate for build-script helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `embedded_artifact_build` crate exposes `hash()` and `write_artifact(out_dir, name, bytes)` so callers' build scripts no longer need a direct `xxhash-rust` dep — the hashing logic needed by `embedded_artifact`'s `artifact!` macro is enclosed here. Fspy's build.rs switches to the helper: - [build-dependencies]: drop `xxhash-rust`, add `embedded_artifact_build`. - Use `embedded_artifact_build::hash` for download verification. - Use `embedded_artifact_build::write_artifact` instead of manually writing the bytes + {name}.hash files. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 9 +++++++- Cargo.toml | 1 + crates/embedded_artifact/src/lib.rs | 9 ++++---- crates/embedded_artifact_build/.clippy.toml | 1 + crates/embedded_artifact_build/Cargo.toml | 16 +++++++++++++ crates/embedded_artifact_build/README.md | 3 +++ crates/embedded_artifact_build/src/lib.rs | 23 +++++++++++++++++++ crates/fspy/Cargo.toml | 2 +- crates/fspy/build.rs | 25 +++++++++++---------- 9 files changed, 70 insertions(+), 19 deletions(-) create mode 120000 crates/embedded_artifact_build/.clippy.toml create mode 100644 crates/embedded_artifact_build/Cargo.toml create mode 100644 crates/embedded_artifact_build/README.md create mode 100644 crates/embedded_artifact_build/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 63da70dba..efc45c748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1006,6 +1006,13 @@ dependencies = [ "xxhash-rust", ] +[[package]] +name = "embedded_artifact_build" +version = "0.0.0" +dependencies = [ + "xxhash-rust", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -1197,6 +1204,7 @@ dependencies = [ "ctor", "derive_more", "embedded_artifact", + "embedded_artifact_build", "flate2", "fspy_detours_sys", "fspy_preload_unix", @@ -1222,7 +1230,6 @@ dependencies = [ "winapi", "wincode", "winsafe 0.0.24", - "xxhash-rust", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 894169848..0e29627da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ diff-struct = "0.5.3" directories = "6.0.0" elf = { version = "0.8.0", default-features = false } embedded_artifact = { path = "crates/embedded_artifact" } +embedded_artifact_build = { path = "crates/embedded_artifact_build" } flate2 = "1.0.35" fspy = { path = "crates/fspy" } fspy_detours_sys = { path = "crates/fspy_detours_sys" } diff --git a/crates/embedded_artifact/src/lib.rs b/crates/embedded_artifact/src/lib.rs index 07b0d2d43..856454c0d 100644 --- a/crates/embedded_artifact/src/lib.rs +++ b/crates/embedded_artifact/src/lib.rs @@ -28,12 +28,11 @@ macro_rules! artifact_of { }; } -/// Construct an [`Artifact`] from a file in the caller's `OUT_DIR`. The build -/// script is expected to have written `$OUT_DIR/{name}` (the raw bytes) and -/// `$OUT_DIR/{name}.hash` (the hex hash). +/// Construct an [`Artifact`] from a file in the caller's `OUT_DIR`. /// -/// Use this for large artifacts where the build script already has the bytes -/// and can hash them cheaply — avoiding const-time hashing at compile time. +/// The build script is expected to have written `$OUT_DIR/{name}` (the raw +/// bytes) and `$OUT_DIR/{name}.hash` (the hex hash) — see the +/// `embedded_artifact_build` crate for a helper that does both in one call. #[macro_export] macro_rules! artifact { ($name: literal) => { diff --git a/crates/embedded_artifact_build/.clippy.toml b/crates/embedded_artifact_build/.clippy.toml new file mode 120000 index 000000000..c7929b369 --- /dev/null +++ b/crates/embedded_artifact_build/.clippy.toml @@ -0,0 +1 @@ +../../.non-vite.clippy.toml \ No newline at end of file diff --git a/crates/embedded_artifact_build/Cargo.toml b/crates/embedded_artifact_build/Cargo.toml new file mode 100644 index 000000000..03e9bcbc0 --- /dev/null +++ b/crates/embedded_artifact_build/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "embedded_artifact_build" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[dependencies] +xxhash-rust = { workspace = true, features = ["xxh3"] } + +[lints] +workspace = true + +[lib] +doctest = false diff --git a/crates/embedded_artifact_build/README.md b/crates/embedded_artifact_build/README.md new file mode 100644 index 000000000..2db3d048c --- /dev/null +++ b/crates/embedded_artifact_build/README.md @@ -0,0 +1,3 @@ +# embedded_artifact_build + +Build-script helpers for producing artifacts consumed by the `embedded_artifact` crate's `artifact!` macro. diff --git a/crates/embedded_artifact_build/src/lib.rs b/crates/embedded_artifact_build/src/lib.rs new file mode 100644 index 000000000..66097acf6 --- /dev/null +++ b/crates/embedded_artifact_build/src/lib.rs @@ -0,0 +1,23 @@ +use std::{fs, io, path::Path}; + +/// Compute the `xxh3_128` hash of `bytes`. Useful for verifying a downloaded +/// artifact against an expected hash before calling [`write_artifact`]. +#[must_use] +pub fn hash(bytes: &[u8]) -> u128 { + xxhash_rust::xxh3::xxh3_128(bytes) +} + +/// Write an artifact produced by a build script so `embedded_artifact`'s +/// `artifact!` macro can load it from `OUT_DIR` at compile time. +/// +/// Creates two files in `out_dir`: `{name}` holding `bytes`, and +/// `{name}.hash` holding the hex-formatted `xxh3_128` hash. +/// +/// # Errors +/// +/// Returns the first I/O error from either write. +pub fn write_artifact(out_dir: &Path, name: &str, bytes: &[u8]) -> io::Result<()> { + fs::write(out_dir.join(name), bytes)?; + fs::write(out_dir.join(format!("{name}.hash")), format!("{:x}", hash(bytes)))?; + Ok(()) +} diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index b3b485640..ea9d392d4 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -60,9 +60,9 @@ fspy_test_bin = { path = "../fspy_test_bin", artifact = "bin", target = "x86_64- [build-dependencies] anyhow = { workspace = true } +embedded_artifact_build = { workspace = true } flate2 = { workspace = true } tar = { workspace = true } -xxhash-rust = { workspace = true, features = ["xxh3"] } [lints] workspace = true diff --git a/crates/fspy/build.rs b/crates/fspy/build.rs index 84987b42a..5caf06ddc 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -7,7 +7,6 @@ use std::{ }; use anyhow::{Context, bail}; -use xxhash_rust::xxh3::xxh3_128; fn download(url: &str) -> anyhow::Result> { let curl = Command::new("curl") @@ -103,22 +102,24 @@ fn fetch_macos_binaries() -> anyhow::Result<()> { for (url, path_in_targz, expected_hash) in downloads.iter().copied() { let filename = path_in_targz.split('/').next_back().unwrap(); let download_path = out_dir.join(filename); - let hash_path = out_dir.join(format!("{filename}.hash")); - let file_exists = matches!(fs::read(&download_path), Ok(existing_file_data) if xxh3_128(&existing_file_data) == expected_hash); - if !file_exists { + let cached = matches!( + fs::read(&download_path), + Ok(existing) if embedded_artifact_build::hash(&existing) == expected_hash, + ); + let data = if cached { + fs::read(&download_path)? + } else { let data = download_and_unpack_tar_gz(url, path_in_targz)?; - fs::write(&download_path, &data).context(format!( - "Saving {path_in_targz} in {url} to {}", - download_path.display() - ))?; - let actual_hash = xxh3_128(&data); + let actual_hash = embedded_artifact_build::hash(&data); assert_eq!( actual_hash, expected_hash, - "expected_hash of {path_in_targz} in {url} needs to be updated" + "expected_hash of {path_in_targz} in {url} needs to be updated", ); - } - fs::write(&hash_path, format!("{expected_hash:x}"))?; + data + }; + embedded_artifact_build::write_artifact(&out_dir, filename, &data) + .context(format!("Writing artifact {filename} to {}", out_dir.display()))?; } Ok(()) // let zsh_path = ensure_downloaded(&zsh_url); From 691db8f63cf18ac8a49ac41115514e69c46e3bab Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 17 Apr 2026 22:27:36 +0800 Subject: [PATCH 04/19] refactor(fspy/build.rs): verify downloads with sha256 of tarball MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Privatize `embedded_artifact_build::hash` — only `write_artifact` is a public API now. - Switch fspy's macOS binary download verification from xxh3_128 of the extracted binary to sha256 of the tarball (the natural unit of a GitHub release asset). - Record each expected sha256 next to a comment pointing at the GitHub release page it came from, with a one-liner showing how to regenerate the value after a release bump. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 1 + crates/embedded_artifact_build/src/lib.rs | 14 ++--- crates/fspy/Cargo.toml | 1 + crates/fspy/build.rs | 70 +++++++++++++---------- 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efc45c748..06d5c8088 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1219,6 +1219,7 @@ dependencies = [ "ouroboros", "rand 0.9.2", "rustc-hash", + "sha2", "subprocess_test", "tar", "tempfile", diff --git a/crates/embedded_artifact_build/src/lib.rs b/crates/embedded_artifact_build/src/lib.rs index 66097acf6..3272b7e15 100644 --- a/crates/embedded_artifact_build/src/lib.rs +++ b/crates/embedded_artifact_build/src/lib.rs @@ -1,17 +1,11 @@ use std::{fs, io, path::Path}; -/// Compute the `xxh3_128` hash of `bytes`. Useful for verifying a downloaded -/// artifact against an expected hash before calling [`write_artifact`]. -#[must_use] -pub fn hash(bytes: &[u8]) -> u128 { - xxhash_rust::xxh3::xxh3_128(bytes) -} - /// Write an artifact produced by a build script so `embedded_artifact`'s /// `artifact!` macro can load it from `OUT_DIR` at compile time. /// /// Creates two files in `out_dir`: `{name}` holding `bytes`, and -/// `{name}.hash` holding the hex-formatted `xxh3_128` hash. +/// `{name}.hash` holding the hex-formatted hash used by `artifact!` to +/// content-address the extracted file at runtime. /// /// # Errors /// @@ -21,3 +15,7 @@ pub fn write_artifact(out_dir: &Path, name: &str, bytes: &[u8]) -> io::Result<() fs::write(out_dir.join(format!("{name}.hash")), format!("{:x}", hash(bytes)))?; Ok(()) } + +fn hash(bytes: &[u8]) -> u128 { + xxhash_rust::xxh3::xxh3_128(bytes) +} diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index ea9d392d4..8b3bb5592 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -62,6 +62,7 @@ fspy_test_bin = { path = "../fspy_test_bin", artifact = "bin", target = "x86_64- anyhow = { workspace = true } embedded_artifact_build = { workspace = true } flate2 = { workspace = true } +sha2 = { workspace = true } tar = { workspace = true } [lints] diff --git a/crates/fspy/build.rs b/crates/fspy/build.rs index 5caf06ddc..281bd7d2f 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -1,5 +1,6 @@ use std::{ env::{self, current_dir}, + fmt::Write as _, fs, io::{Cursor, Read}, path::Path, @@ -7,8 +8,9 @@ use std::{ }; use anyhow::{Context, bail}; +use sha2::{Digest, Sha256}; -fn download(url: &str) -> anyhow::Result> { +fn download(url: &str) -> anyhow::Result> { let curl = Command::new("curl") .args([ "-f", // fail on HTTP errors @@ -21,15 +23,14 @@ fn download(url: &str) -> anyhow::Result> { if !output.status.success() { bail!("curl exited with status {} trying to download {}", output.status, url); } - Ok(Cursor::new(output.stdout)) + Ok(output.stdout) } -fn unpack_tar_gz(content: impl Read, path: &str) -> anyhow::Result> { +fn unpack_tar_gz(tarball: impl Read, path: &str) -> anyhow::Result> { use flate2::read::GzDecoder; use tar::Archive; - // let path = path.as_ref(); - let tar = GzDecoder::new(content); + let tar = GzDecoder::new(tarball); let mut archive = Archive::new(tar); for entry in archive.entries()? { let mut entry = entry?; @@ -42,44 +43,57 @@ fn unpack_tar_gz(content: impl Read, path: &str) -> anyhow::Result> { bail!("Path {path} not found in tar gz") } -fn download_and_unpack_tar_gz(url: &str, path: &str) -> anyhow::Result> { - let resp = download(url).context(format!("Failed to get ok response from {url}"))?; - let data = unpack_tar_gz(resp, path) - .context(format!("Failed to download or unpack {path} out of {url}"))?; - Ok(data) +fn sha256_hex(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + let mut s = String::with_capacity(64); + for b in digest { + write!(&mut s, "{b:02x}").unwrap(); + } + s } -/// (url, `path_in_targz`, `expected_hash`) -type BinaryDownload = (&'static str, &'static str, u128); +/// `(url, path_in_targz, expected_sha256_of_tarball)` +/// +/// The SHA-256 verifies the tarball (the file served by the GitHub release +/// asset URL). Neither upstream currently publishes a `*.sha256` file, so +/// these values are the hash of the asset at the time it was pinned. +/// +/// To verify or refresh after a release bump: +/// `curl -sL | shasum -a 256` +type BinaryDownload = (&'static str, &'static str, &'static str); const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ ( "aarch64", &[ + // https://github.com/branchseer/oils-for-unix-build/releases/tag/oils-for-unix-0.37.0 ( "https://github.com/branchseer/oils-for-unix-build/releases/download/oils-for-unix-0.37.0/oils-for-unix-0.37.0-darwin-arm64.tar.gz", "oils-for-unix", - 282_073_174_065_923_237_490_435_663_309_538_399_576, + "3a35f7ae2be85fcd32392cd8171522f5822f20a69125c5e9d8d68b2f5c857098", ), + // https://github.com/uutils/coreutils/releases/tag/0.4.0 ( "https://github.com/uutils/coreutils/releases/download/0.4.0/coreutils-0.4.0-aarch64-apple-darwin.tar.gz", "coreutils-0.4.0-aarch64-apple-darwin/coreutils", - 35_998_406_686_137_668_997_937_014_088_186_935_383, + "a148b660eeaf409af7a4406903f93d0e6713a5eb9adcaf71a1d732f1e3cc3522", ), ], ), ( "x86_64", &[ + // https://github.com/branchseer/oils-for-unix-build/releases/tag/oils-for-unix-0.37.0 ( "https://github.com/branchseer/oils-for-unix-build/releases/download/oils-for-unix-0.37.0/oils-for-unix-0.37.0-darwin-x86_64.tar.gz", "oils-for-unix", - 142_673_558_272_427_867_831_039_361_796_426_010_330, + "aa12258d1bd553020144ad61fdac18e7dfbe3fc3965da32ee458840153169151", ), + // https://github.com/uutils/coreutils/releases/tag/0.4.0 ( "https://github.com/uutils/coreutils/releases/download/0.4.0/coreutils-0.4.0-x86_64-apple-darwin.tar.gz", "coreutils-0.4.0-x86_64-apple-darwin/coreutils", - 120_898_281_113_671_104_995_723_556_995_187_526_689, + "6e4be8429efe86c9a60247ae7a930221ed11770a975fb4b6fd09ff8d39b9a15c", ), ], ), @@ -98,31 +112,27 @@ fn fetch_macos_binaries() -> anyhow::Result<()> { .find(|(arch, _)| *arch == target_arch) .context(format!("Unsupported macOS arch: {target_arch}"))? .1; - // let downloads = [(zsh_url.as_str(), "bin/zsh", zsh_hash)]; - for (url, path_in_targz, expected_hash) in downloads.iter().copied() { + + for (url, path_in_targz, expected_sha256) in downloads.iter().copied() { let filename = path_in_targz.split('/').next_back().unwrap(); let download_path = out_dir.join(filename); - let cached = matches!( - fs::read(&download_path), - Ok(existing) if embedded_artifact_build::hash(&existing) == expected_hash, - ); - let data = if cached { - fs::read(&download_path)? + let data = if let Ok(cached) = fs::read(&download_path) { + cached } else { - let data = download_and_unpack_tar_gz(url, path_in_targz)?; - let actual_hash = embedded_artifact_build::hash(&data); + let tarball = download(url).context(format!("Failed to download {url}"))?; + let actual_sha256 = sha256_hex(&tarball); assert_eq!( - actual_hash, expected_hash, - "expected_hash of {path_in_targz} in {url} needs to be updated", + actual_sha256, expected_sha256, + "sha256 of {url} does not match — update expected value in MACOS_BINARY_DOWNLOADS", ); - data + unpack_tar_gz(Cursor::new(tarball), path_in_targz) + .context(format!("Failed to extract {path_in_targz} from {url}"))? }; embedded_artifact_build::write_artifact(&out_dir, filename, &data) .context(format!("Writing artifact {filename} to {}", out_dir.display()))?; } Ok(()) - // let zsh_path = ensure_downloaded(&zsh_url); } fn main() -> anyhow::Result<()> { From d49aa77c5eb8948181f12850d9f10a13d33956d1 Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 17 Apr 2026 22:30:49 +0800 Subject: [PATCH 05/19] docs(fspy/build.rs): simplify sha256 comment Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/fspy/build.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/fspy/build.rs b/crates/fspy/build.rs index 281bd7d2f..bc17c4e03 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -54,12 +54,8 @@ fn sha256_hex(bytes: &[u8]) -> String { /// `(url, path_in_targz, expected_sha256_of_tarball)` /// -/// The SHA-256 verifies the tarball (the file served by the GitHub release -/// asset URL). Neither upstream currently publishes a `*.sha256` file, so -/// these values are the hash of the asset at the time it was pinned. -/// -/// To verify or refresh after a release bump: -/// `curl -sL | shasum -a 256` +/// The SHA-256 verifies the tarball served by the GitHub release URL. Each +/// value can be obtained from the release download page. type BinaryDownload = (&'static str, &'static str, &'static str); const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ From 530b300c2bcc8b222dac560d27982fbabe39cc25 Mon Sep 17 00:00:00 2001 From: branchseer Date: Sat, 18 Apr 2026 13:18:05 +0800 Subject: [PATCH 06/19] refactor(embedded_artifact): compute hashes in build.rs via rustc-env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework the artifact API so names and hashes are published through `cargo::rustc-env=…` by `embedded_artifact_build::register(name, path)` and consumed at compile time by `artifact!($name)` (no const-eval hashing, no hash sidecar files, no byte copies for cdylibs). - `embedded_artifact_build::register` hashes with xxh3-128 and emits `EMBEDDED_ARTIFACT_{name}_PATH` + `_HASH` plus `rerun-if-changed`. - `artifact!($name)` reads both env vars via `include_bytes!(env!(…))` and `env!(…)`. - Preload cdylibs are artifact deps in `[build-dependencies]`; cfg-gating the section triggers a cargo resolver panic on cross-compile, so both preload crates are listed unconditionally with `target = "target"` and each cfg-gates itself to an empty crate on non-applicable targets. - fspy's build.rs emits `rerun-if-env-changed` for the `CARGO_CDYLIB_FILE_FSPY_PRELOAD_*` env var it consumes. - `BinaryDownload` becomes a named struct. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 18 ---- crates/embedded_artifact/Cargo.toml | 2 - crates/embedded_artifact/src/lib.rs | 52 +++------- crates/embedded_artifact_build/README.md | 2 +- crates/embedded_artifact_build/src/lib.rs | 48 ++++++--- crates/fspy/Cargo.toml | 13 ++- crates/fspy/build.rs | 117 +++++++++++++--------- crates/fspy/src/unix/macos_artifacts.rs | 2 +- crates/fspy/src/unix/mod.rs | 7 +- crates/fspy/src/windows/mod.rs | 5 +- crates/fspy_preload_unix/src/lib.rs | 7 +- 11 files changed, 134 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06d5c8088..6498f1887 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -546,7 +546,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", - "konst", ] [[package]] @@ -1001,9 +1000,7 @@ checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" name = "embedded_artifact" version = "0.0.0" dependencies = [ - "const_format", "rand 0.9.2", - "xxhash-rust", ] [[package]] @@ -1700,21 +1697,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "konst" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330f0e13e6483b8c34885f7e6c9f19b1a7bd449c673fbb948a51c99d66ef74f4" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - [[package]] name = "kqueue" version = "1.1.1" diff --git a/crates/embedded_artifact/Cargo.toml b/crates/embedded_artifact/Cargo.toml index 42c805a08..4e116fff2 100644 --- a/crates/embedded_artifact/Cargo.toml +++ b/crates/embedded_artifact/Cargo.toml @@ -7,9 +7,7 @@ publish = false rust-version.workspace = true [dependencies] -const_format = { workspace = true, features = ["fmt"] } rand = { workspace = true } -xxhash-rust = { workspace = true, features = ["const_xxh3"] } [lints] workspace = true diff --git a/crates/embedded_artifact/src/lib.rs b/crates/embedded_artifact/src/lib.rs index 856454c0d..56ef13b09 100644 --- a/crates/embedded_artifact/src/lib.rs +++ b/crates/embedded_artifact/src/lib.rs @@ -6,53 +6,33 @@ use std::{ /// An artifact (e.g., a DLL or shared library) whose content is embedded and needs to be written to disk. pub struct Artifact { - pub name: &'static str, - pub content: &'static [u8], - pub hash: &'static str, + name: &'static str, + content: &'static [u8], + hash: &'static str, } -/// Construct an [`Artifact`] from a `&'static [u8]` expression; the hash is -/// computed from the bytes at compile time. -/// -/// Only use this for small artifacts — const-time hashing of large payloads -/// significantly slows compilation. For large artifacts, pre-compute the hash -/// in a build script and use [`artifact!`]. -#[macro_export] -macro_rules! artifact_of { - ($name: literal, $content: expr) => { - $crate::Artifact::new( - $name, - $content, - $crate::__private::formatcp!("{:x}", $crate::__private::xxh3_128($content)), - ) - }; -} - -/// Construct an [`Artifact`] from a file in the caller's `OUT_DIR`. -/// -/// The build script is expected to have written `$OUT_DIR/{name}` (the raw -/// bytes) and `$OUT_DIR/{name}.hash` (the hex hash) — see the -/// `embedded_artifact_build` crate for a helper that does both in one call. +/// Construct an [`Artifact`] from the env vars published by a build script +/// via `embedded_artifact_build::register`. Must match the `ENV_PREFIX` +/// constant in `embedded_artifact_build`. #[macro_export] macro_rules! artifact { - ($name: literal) => { - $crate::Artifact::new( + ($name:literal) => { + $crate::Artifact::__new( $name, - ::core::include_bytes!(::core::concat!(::core::env!("OUT_DIR"), "/", $name)), - ::core::include_str!(::core::concat!(::core::env!("OUT_DIR"), "/", $name, ".hash")), + ::core::include_bytes!(::core::env!(::core::concat!( + "EMBEDDED_ARTIFACT_", + $name, + "_PATH" + ))), + ::core::env!(::core::concat!("EMBEDDED_ARTIFACT_", $name, "_HASH")), ) }; } -#[doc(hidden)] -pub mod __private { - pub use const_format::formatcp; - pub use xxhash_rust::const_xxh3::xxh3_128; -} - impl Artifact { + #[doc(hidden)] #[must_use] - pub const fn new(name: &'static str, content: &'static [u8], hash: &'static str) -> Self { + pub const fn __new(name: &'static str, content: &'static [u8], hash: &'static str) -> Self { Self { name, content, hash } } diff --git a/crates/embedded_artifact_build/README.md b/crates/embedded_artifact_build/README.md index 2db3d048c..99a140450 100644 --- a/crates/embedded_artifact_build/README.md +++ b/crates/embedded_artifact_build/README.md @@ -1,3 +1,3 @@ # embedded_artifact_build -Build-script helpers for producing artifacts consumed by the `embedded_artifact` crate's `artifact!` macro. +Build-script helper for publishing artifacts consumed by `embedded_artifact`'s `artifact!` macro. diff --git a/crates/embedded_artifact_build/src/lib.rs b/crates/embedded_artifact_build/src/lib.rs index 3272b7e15..9f565c11c 100644 --- a/crates/embedded_artifact_build/src/lib.rs +++ b/crates/embedded_artifact_build/src/lib.rs @@ -1,21 +1,37 @@ -use std::{fs, io, path::Path}; +use std::{fs, path::Path}; -/// Write an artifact produced by a build script so `embedded_artifact`'s -/// `artifact!` macro can load it from `OUT_DIR` at compile time. +/// Namespace prefix for the env vars set by [`register`] and consumed by +/// `embedded_artifact`'s `artifact!` macro. Exported so both crates agree on +/// the same prefix. +pub const ENV_PREFIX: &str = "EMBEDDED_ARTIFACT_"; + +/// Publish an artifact at `path` so `embedded_artifact`'s `artifact!($name)` +/// macro can embed it. /// -/// Creates two files in `out_dir`: `{name}` holding `bytes`, and -/// `{name}.hash` holding the hex-formatted hash used by `artifact!` to -/// content-address the extracted file at runtime. +/// Emits three `cargo::…` directives: +/// `rerun-if-changed={path}`, +/// `rustc-env=EMBEDDED_ARTIFACT_{name}_PATH={path}`, and +/// `rustc-env=EMBEDDED_ARTIFACT_{name}_HASH={hex}`. The runtime resolves these +/// at compile time via `include_bytes!(env!(…))` and `env!(…)`. /// -/// # Errors +/// `name` is used both as the env-var key and as the on-disk filename prefix +/// (in `Artifact::write_to`), so it must be a valid identifier-like string +/// that matches the one passed to `artifact!`. /// -/// Returns the first I/O error from either write. -pub fn write_artifact(out_dir: &Path, name: &str, bytes: &[u8]) -> io::Result<()> { - fs::write(out_dir.join(name), bytes)?; - fs::write(out_dir.join(format!("{name}.hash")), format!("{:x}", hash(bytes)))?; - Ok(()) -} - -fn hash(bytes: &[u8]) -> u128 { - xxhash_rust::xxh3::xxh3_128(bytes) +/// # Panics +/// +/// Panics if `path` is not valid UTF-8 or cannot be read. +pub fn register(name: &str, path: &Path) { + let path_str = path.to_str().expect("artifact path must be valid UTF-8"); + #[expect(clippy::print_stdout, reason = "cargo build-script directives")] + { + // Emit rerun-if-changed before reading so cargo still sees it even if + // reading the file below panics. + println!("cargo::rerun-if-changed={path_str}"); + let bytes = + fs::read(path).unwrap_or_else(|e| panic!("failed to read artifact at {path_str}: {e}")); + let hash = format!("{:x}", xxhash_rust::xxh3::xxh3_128(&bytes)); + println!("cargo::rustc-env={ENV_PREFIX}{name}_PATH={path_str}"); + println!("cargo::rustc-env={ENV_PREFIX}{name}_HASH={hash}"); + } } diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index 8b3bb5592..b9d312071 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -28,16 +28,12 @@ fspy_seccomp_unotify = { workspace = true, features = ["supervisor"] } nix = { workspace = true, features = ["uio"] } tokio = { workspace = true, features = ["bytes"] } -[target.'cfg(all(unix, not(target_env = "musl")))'.dependencies] -fspy_preload_unix = { workspace = true } - [target.'cfg(unix)'.dependencies] fspy_shared_unix = { workspace = true } nix = { workspace = true, features = ["fs", "process", "socket", "feature"] } [target.'cfg(target_os = "windows")'.dependencies] fspy_detours_sys = { workspace = true } -fspy_preload_windows = { workspace = true } winapi = { workspace = true, features = ["winbase", "securitybaseapi", "handleapi"] } winsafe = { workspace = true } @@ -58,10 +54,17 @@ fspy_test_bin = { path = "../fspy_test_bin", artifact = "bin", target = "aarch64 [target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dev-dependencies] fspy_test_bin = { path = "../fspy_test_bin", artifact = "bin", target = "x86_64-unknown-linux-musl" } +# Artifact build-deps must be unconditional: cargo's resolver panics when +# `artifact = "cdylib"` deps live under a `[target.cfg.build-dependencies]` +# block on cross-compile. Each preload crate's source is cfg-gated to compile +# as an empty cdylib on non-applicable targets, so the unused cross-target +# builds are cheap. [build-dependencies] anyhow = { workspace = true } embedded_artifact_build = { workspace = true } flate2 = { workspace = true } +fspy_preload_unix = { path = "../fspy_preload_unix", artifact = "cdylib", target = "target" } +fspy_preload_windows = { path = "../fspy_preload_windows", artifact = "cdylib", target = "target" } sha2 = { workspace = true } tar = { workspace = true } @@ -72,4 +75,4 @@ workspace = true doctest = false [package.metadata.cargo-shear] -ignored = ["ctor", "fspy_test_bin"] +ignored = ["ctor", "fspy_test_bin", "fspy_preload_unix", "fspy_preload_windows"] diff --git a/crates/fspy/build.rs b/crates/fspy/build.rs index bc17c4e03..449ede016 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -1,9 +1,9 @@ use std::{ - env::{self, current_dir}, + env, fmt::Write as _, fs, io::{Cursor, Read}, - path::Path, + path::{Path, PathBuf}, process::{Command, Stdio}, }; @@ -52,56 +52,65 @@ fn sha256_hex(bytes: &[u8]) -> String { s } -/// `(url, path_in_targz, expected_sha256_of_tarball)` -/// -/// The SHA-256 verifies the tarball served by the GitHub release URL. Each -/// value can be obtained from the release download page. -type BinaryDownload = (&'static str, &'static str, &'static str); +struct BinaryDownload { + /// Identifier used both as the on-disk filename in `OUT_DIR` and as the + /// env-var prefix consumed by `artifact!($name)` at runtime. + name: &'static str, + /// GitHub release asset URL. + url: &'static str, + /// Path of the binary within the tarball. + path_in_targz: &'static str, + /// SHA-256 of the tarball at `url`. Each value can be obtained from the + /// release download page. + expected_sha256: &'static str, +} const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ ( "aarch64", &[ // https://github.com/branchseer/oils-for-unix-build/releases/tag/oils-for-unix-0.37.0 - ( - "https://github.com/branchseer/oils-for-unix-build/releases/download/oils-for-unix-0.37.0/oils-for-unix-0.37.0-darwin-arm64.tar.gz", - "oils-for-unix", - "3a35f7ae2be85fcd32392cd8171522f5822f20a69125c5e9d8d68b2f5c857098", - ), + BinaryDownload { + name: "oils_for_unix", + url: "https://github.com/branchseer/oils-for-unix-build/releases/download/oils-for-unix-0.37.0/oils-for-unix-0.37.0-darwin-arm64.tar.gz", + path_in_targz: "oils-for-unix", + expected_sha256: "3a35f7ae2be85fcd32392cd8171522f5822f20a69125c5e9d8d68b2f5c857098", + }, // https://github.com/uutils/coreutils/releases/tag/0.4.0 - ( - "https://github.com/uutils/coreutils/releases/download/0.4.0/coreutils-0.4.0-aarch64-apple-darwin.tar.gz", - "coreutils-0.4.0-aarch64-apple-darwin/coreutils", - "a148b660eeaf409af7a4406903f93d0e6713a5eb9adcaf71a1d732f1e3cc3522", - ), + BinaryDownload { + name: "coreutils", + url: "https://github.com/uutils/coreutils/releases/download/0.4.0/coreutils-0.4.0-aarch64-apple-darwin.tar.gz", + path_in_targz: "coreutils-0.4.0-aarch64-apple-darwin/coreutils", + expected_sha256: "a148b660eeaf409af7a4406903f93d0e6713a5eb9adcaf71a1d732f1e3cc3522", + }, ], ), ( "x86_64", &[ // https://github.com/branchseer/oils-for-unix-build/releases/tag/oils-for-unix-0.37.0 - ( - "https://github.com/branchseer/oils-for-unix-build/releases/download/oils-for-unix-0.37.0/oils-for-unix-0.37.0-darwin-x86_64.tar.gz", - "oils-for-unix", - "aa12258d1bd553020144ad61fdac18e7dfbe3fc3965da32ee458840153169151", - ), + BinaryDownload { + name: "oils_for_unix", + url: "https://github.com/branchseer/oils-for-unix-build/releases/download/oils-for-unix-0.37.0/oils-for-unix-0.37.0-darwin-x86_64.tar.gz", + path_in_targz: "oils-for-unix", + expected_sha256: "aa12258d1bd553020144ad61fdac18e7dfbe3fc3965da32ee458840153169151", + }, // https://github.com/uutils/coreutils/releases/tag/0.4.0 - ( - "https://github.com/uutils/coreutils/releases/download/0.4.0/coreutils-0.4.0-x86_64-apple-darwin.tar.gz", - "coreutils-0.4.0-x86_64-apple-darwin/coreutils", - "6e4be8429efe86c9a60247ae7a930221ed11770a975fb4b6fd09ff8d39b9a15c", - ), + BinaryDownload { + name: "coreutils", + url: "https://github.com/uutils/coreutils/releases/download/0.4.0/coreutils-0.4.0-x86_64-apple-darwin.tar.gz", + path_in_targz: "coreutils-0.4.0-x86_64-apple-darwin/coreutils", + expected_sha256: "6e4be8429efe86c9a60247ae7a930221ed11770a975fb4b6fd09ff8d39b9a15c", + }, ], ), ]; -fn fetch_macos_binaries() -> anyhow::Result<()> { +fn fetch_macos_binaries(out_dir: &Path) -> anyhow::Result<()> { if env::var("CARGO_CFG_TARGET_OS").unwrap() != "macos" { return Ok(()); } - let out_dir = current_dir().unwrap().join(Path::new(&std::env::var_os("OUT_DIR").unwrap())); - let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); let downloads = MACOS_BINARY_DOWNLOADS .iter() @@ -109,30 +118,40 @@ fn fetch_macos_binaries() -> anyhow::Result<()> { .context(format!("Unsupported macOS arch: {target_arch}"))? .1; - for (url, path_in_targz, expected_sha256) in downloads.iter().copied() { - let filename = path_in_targz.split('/').next_back().unwrap(); - let download_path = out_dir.join(filename); - - let data = if let Ok(cached) = fs::read(&download_path) { - cached - } else { - let tarball = download(url).context(format!("Failed to download {url}"))?; - let actual_sha256 = sha256_hex(&tarball); - assert_eq!( - actual_sha256, expected_sha256, - "sha256 of {url} does not match — update expected value in MACOS_BINARY_DOWNLOADS", - ); - unpack_tar_gz(Cursor::new(tarball), path_in_targz) - .context(format!("Failed to extract {path_in_targz} from {url}"))? - }; - embedded_artifact_build::write_artifact(&out_dir, filename, &data) - .context(format!("Writing artifact {filename} to {}", out_dir.display()))?; + for BinaryDownload { name, url, path_in_targz, expected_sha256 } in downloads { + let dest = out_dir.join(name); + let tarball = download(url).context(format!("Failed to download {url}"))?; + let actual_sha256 = sha256_hex(&tarball); + assert_eq!( + &actual_sha256, expected_sha256, + "sha256 of {url} does not match — update expected value in MACOS_BINARY_DOWNLOADS", + ); + let data = unpack_tar_gz(Cursor::new(tarball), path_in_targz) + .context(format!("Failed to extract {path_in_targz} from {url}"))?; + fs::write(&dest, &data).with_context(|| format!("writing {}", dest.display()))?; + embedded_artifact_build::register(name, &dest); } Ok(()) } +fn register_preload_cdylib() -> anyhow::Result<()> { + let env_name = match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() { + "windows" => "CARGO_CDYLIB_FILE_FSPY_PRELOAD_WINDOWS", + _ if env::var("CARGO_CFG_TARGET_ENV").unwrap() == "musl" => return Ok(()), + _ => "CARGO_CDYLIB_FILE_FSPY_PRELOAD_UNIX", + }; + // The cdylib path is content-addressed by cargo; when its content changes + // the path changes. Track it so we re-publish the hash on update. + println!("cargo::rerun-if-env-changed={env_name}"); + let dylib_path = env::var_os(env_name).with_context(|| format!("{env_name} not set"))?; + embedded_artifact_build::register("fspy_preload", Path::new(&dylib_path)); + Ok(()) +} + fn main() -> anyhow::Result<()> { println!("cargo:rerun-if-changed=build.rs"); - fetch_macos_binaries().context("Failed to fetch macOS binaries")?; + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + fetch_macos_binaries(&out_dir).context("Failed to fetch macOS binaries")?; + register_preload_cdylib().context("Failed to register preload cdylib")?; Ok(()) } diff --git a/crates/fspy/src/unix/macos_artifacts.rs b/crates/fspy/src/unix/macos_artifacts.rs index 54e53f21d..d39d2348b 100644 --- a/crates/fspy/src/unix/macos_artifacts.rs +++ b/crates/fspy/src/unix/macos_artifacts.rs @@ -1,7 +1,7 @@ use embedded_artifact::{Artifact, artifact}; pub const COREUTILS_BINARY: Artifact = artifact!("coreutils"); -pub const OILS_BINARY: Artifact = artifact!("oils-for-unix"); +pub const OILS_BINARY: Artifact = artifact!("oils_for_unix"); #[cfg(test)] mod tests { diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index 6bb216e9b..4ae22fe22 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -37,9 +37,6 @@ pub struct SpyImpl { preload_path: Box, } -#[cfg(not(target_env = "musl"))] -const PRELOAD_CDYLIB_BINARY: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_FSPY_PRELOAD_UNIX")); - impl SpyImpl { /// Initialize the fs access spy by writing the preload library on disk. /// @@ -48,9 +45,9 @@ impl SpyImpl { pub fn init_in(#[cfg_attr(target_env = "musl", allow(unused))] dir: &Path) -> io::Result { #[cfg(not(target_env = "musl"))] let preload_path = { - use embedded_artifact::{Artifact, artifact_of}; + use embedded_artifact::{Artifact, artifact}; - const PRELOAD_CDYLIB: Artifact = artifact_of!("fspy_preload", PRELOAD_CDYLIB_BINARY); + const PRELOAD_CDYLIB: Artifact = artifact!("fspy_preload"); let preload_cdylib_path = PRELOAD_CDYLIB.write_to(dir, ".dylib")?; preload_cdylib_path.as_path().into() diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 722efa93d..4c6b91330 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -6,7 +6,7 @@ use std::{ sync::Arc, }; -use embedded_artifact::{Artifact, artifact_of}; +use embedded_artifact::{Artifact, artifact}; use fspy_detours_sys::{DetourCopyPayloadToProcess, DetourUpdateProcessWithDll}; use fspy_shared::{ ipc::{PathAccess, channel::channel}, @@ -27,8 +27,7 @@ use crate::{ ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}, }; -const PRELOAD_CDYLIB_BINARY: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_FSPY_PRELOAD_WINDOWS")); -const INTERPOSE_CDYLIB: Artifact = artifact_of!("fsyp_preload", PRELOAD_CDYLIB_BINARY); +const INTERPOSE_CDYLIB: Artifact = artifact!("fspy_preload"); pub struct PathAccessIterable { ipc_receiver_lock_guard: OwnedReceiverLockGuard, diff --git a/crates/fspy_preload_unix/src/lib.rs b/crates/fspy_preload_unix/src/lib.rs index 9728cd98f..42bf9e9cb 100644 --- a/crates/fspy_preload_unix/src/lib.rs +++ b/crates/fspy_preload_unix/src/lib.rs @@ -1,6 +1,7 @@ -// On musl targets, fspy_preload_unix is not needed since we can track accesses via seccomp-only. -// Compile as an empty crate to avoid build failures from missing libc symbols. -#![cfg_attr(not(target_env = "musl"), feature(c_variadic))] +// Compile as an empty crate on non-unix targets and on musl (where seccomp +// alone handles access tracking). Guarding the feature gate keeps rustc from +// warning about unused features on those targets. +#![cfg_attr(all(unix, not(target_env = "musl")), feature(c_variadic))] #[cfg(all(unix, not(target_env = "musl")))] mod client; From 8654ba023d36c195bba1b8f33486adddb4567e6a Mon Sep 17 00:00:00 2001 From: branchseer Date: Sat, 18 Apr 2026 13:31:08 +0800 Subject: [PATCH 07/19] refactor(bundled_artifact): rename from embedded_artifact; method write_to -> ensure_in Rename the pair of crates to `bundled_artifact` + `bundled_artifact_build` to cover both the current embed-and-extract mode and a future check-only mode under the same name. Rename `Artifact::write_to` to `ensure_in` since the call skips the write when a file with the same content-addressed name already exists. Update env prefix from `EMBEDDED_ARTIFACT_` to `BUNDLED_ARTIFACT_`. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 32 +++++++++---------- Cargo.toml | 4 +-- .../.clippy.toml | 0 .../Cargo.toml | 2 +- crates/bundled_artifact/README.md | 3 ++ .../src/lib.rs | 13 ++++---- .../.clippy.toml | 0 .../Cargo.toml | 2 +- crates/bundled_artifact_build/README.md | 3 ++ .../src/lib.rs | 12 +++---- crates/embedded_artifact/README.md | 3 -- crates/embedded_artifact_build/README.md | 3 -- crates/fspy/Cargo.toml | 4 +-- crates/fspy/build.rs | 4 +-- crates/fspy/src/unix/macos_artifacts.rs | 4 +-- crates/fspy/src/unix/mod.rs | 8 ++--- crates/fspy/src/windows/mod.rs | 4 +-- 17 files changed, 51 insertions(+), 50 deletions(-) rename crates/{embedded_artifact => bundled_artifact}/.clippy.toml (100%) rename crates/{embedded_artifact => bundled_artifact}/Cargo.toml (88%) create mode 100644 crates/bundled_artifact/README.md rename crates/{embedded_artifact => bundled_artifact}/src/lib.rs (81%) rename crates/{embedded_artifact_build => bundled_artifact_build}/.clippy.toml (100%) rename crates/{embedded_artifact_build => bundled_artifact_build}/Cargo.toml (88%) create mode 100644 crates/bundled_artifact_build/README.md rename crates/{embedded_artifact_build => bundled_artifact_build}/src/lib.rs (74%) delete mode 100644 crates/embedded_artifact/README.md delete mode 100644 crates/embedded_artifact_build/README.md diff --git a/Cargo.lock b/Cargo.lock index 6498f1887..9235abed9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,20 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bundled_artifact" +version = "0.0.0" +dependencies = [ + "rand 0.9.2", +] + +[[package]] +name = "bundled_artifact_build" +version = "0.0.0" +dependencies = [ + "xxhash-rust", +] + [[package]] name = "bytemuck" version = "1.25.0" @@ -996,20 +1010,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" -[[package]] -name = "embedded_artifact" -version = "0.0.0" -dependencies = [ - "rand 0.9.2", -] - -[[package]] -name = "embedded_artifact_build" -version = "0.0.0" -dependencies = [ - "xxhash-rust", -] - [[package]] name = "env_filter" version = "0.1.4" @@ -1197,11 +1197,11 @@ dependencies = [ "anyhow", "bstr", "bumpalo", + "bundled_artifact", + "bundled_artifact_build", "csv-async", "ctor", "derive_more", - "embedded_artifact", - "embedded_artifact_build", "flate2", "fspy_detours_sys", "fspy_preload_unix", diff --git a/Cargo.toml b/Cargo.toml index 0e29627da..d61e4889e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,8 +70,8 @@ derive_more = "2.0.1" diff-struct = "0.5.3" directories = "6.0.0" elf = { version = "0.8.0", default-features = false } -embedded_artifact = { path = "crates/embedded_artifact" } -embedded_artifact_build = { path = "crates/embedded_artifact_build" } +bundled_artifact = { path = "crates/bundled_artifact" } +bundled_artifact_build = { path = "crates/bundled_artifact_build" } flate2 = "1.0.35" fspy = { path = "crates/fspy" } fspy_detours_sys = { path = "crates/fspy_detours_sys" } diff --git a/crates/embedded_artifact/.clippy.toml b/crates/bundled_artifact/.clippy.toml similarity index 100% rename from crates/embedded_artifact/.clippy.toml rename to crates/bundled_artifact/.clippy.toml diff --git a/crates/embedded_artifact/Cargo.toml b/crates/bundled_artifact/Cargo.toml similarity index 88% rename from crates/embedded_artifact/Cargo.toml rename to crates/bundled_artifact/Cargo.toml index 4e116fff2..f2583c881 100644 --- a/crates/embedded_artifact/Cargo.toml +++ b/crates/bundled_artifact/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "embedded_artifact" +name = "bundled_artifact" version = "0.0.0" edition.workspace = true license.workspace = true diff --git a/crates/bundled_artifact/README.md b/crates/bundled_artifact/README.md new file mode 100644 index 000000000..aa77c89ba --- /dev/null +++ b/crates/bundled_artifact/README.md @@ -0,0 +1,3 @@ +# bundled_artifact + +Binary artifacts bundled with an executable and materialized to disk on demand, with content-addressed filenames so repeated materializations reuse the same file. diff --git a/crates/embedded_artifact/src/lib.rs b/crates/bundled_artifact/src/lib.rs similarity index 81% rename from crates/embedded_artifact/src/lib.rs rename to crates/bundled_artifact/src/lib.rs index 56ef13b09..884aece3e 100644 --- a/crates/embedded_artifact/src/lib.rs +++ b/crates/bundled_artifact/src/lib.rs @@ -12,19 +12,19 @@ pub struct Artifact { } /// Construct an [`Artifact`] from the env vars published by a build script -/// via `embedded_artifact_build::register`. Must match the `ENV_PREFIX` -/// constant in `embedded_artifact_build`. +/// via `bundled_artifact_build::register`. Must match the `ENV_PREFIX` +/// constant in `bundled_artifact_build`. #[macro_export] macro_rules! artifact { ($name:literal) => { $crate::Artifact::__new( $name, ::core::include_bytes!(::core::env!(::core::concat!( - "EMBEDDED_ARTIFACT_", + "BUNDLED_ARTIFACT_", $name, "_PATH" ))), - ::core::env!(::core::concat!("EMBEDDED_ARTIFACT_", $name, "_HASH")), + ::core::env!(::core::concat!("BUNDLED_ARTIFACT_", $name, "_HASH")), ) }; } @@ -36,7 +36,8 @@ impl Artifact { Self { name, content, hash } } - /// Write the artifact's content to `dir` under a content-addressed filename. + /// Ensure the artifact is materialized in `dir` under a content-addressed + /// filename, writing it if missing. /// /// Returns the final path. If a file with the same hash already exists at /// the target path, it is reused without rewriting. @@ -45,7 +46,7 @@ impl Artifact { /// /// Returns an error if the directory can't be read/written, or if the /// temp-file rename fails and the destination still doesn't exist. - pub fn write_to(&self, dir: impl AsRef, suffix: &str) -> io::Result { + pub fn ensure_in(&self, dir: impl AsRef, suffix: &str) -> io::Result { let dir = dir.as_ref(); let path = dir.join(format!("{}_{}{}", self.name, self.hash, suffix)); diff --git a/crates/embedded_artifact_build/.clippy.toml b/crates/bundled_artifact_build/.clippy.toml similarity index 100% rename from crates/embedded_artifact_build/.clippy.toml rename to crates/bundled_artifact_build/.clippy.toml diff --git a/crates/embedded_artifact_build/Cargo.toml b/crates/bundled_artifact_build/Cargo.toml similarity index 88% rename from crates/embedded_artifact_build/Cargo.toml rename to crates/bundled_artifact_build/Cargo.toml index 03e9bcbc0..d3a2287e2 100644 --- a/crates/embedded_artifact_build/Cargo.toml +++ b/crates/bundled_artifact_build/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "embedded_artifact_build" +name = "bundled_artifact_build" version = "0.0.0" edition.workspace = true license.workspace = true diff --git a/crates/bundled_artifact_build/README.md b/crates/bundled_artifact_build/README.md new file mode 100644 index 000000000..5894c95b8 --- /dev/null +++ b/crates/bundled_artifact_build/README.md @@ -0,0 +1,3 @@ +# bundled_artifact_build + +Build-script helper for publishing artifacts consumed by `bundled_artifact`'s `artifact!` macro. diff --git a/crates/embedded_artifact_build/src/lib.rs b/crates/bundled_artifact_build/src/lib.rs similarity index 74% rename from crates/embedded_artifact_build/src/lib.rs rename to crates/bundled_artifact_build/src/lib.rs index 9f565c11c..6a992767c 100644 --- a/crates/embedded_artifact_build/src/lib.rs +++ b/crates/bundled_artifact_build/src/lib.rs @@ -1,21 +1,21 @@ use std::{fs, path::Path}; /// Namespace prefix for the env vars set by [`register`] and consumed by -/// `embedded_artifact`'s `artifact!` macro. Exported so both crates agree on +/// `bundled_artifact`'s `artifact!` macro. Exported so both crates agree on /// the same prefix. -pub const ENV_PREFIX: &str = "EMBEDDED_ARTIFACT_"; +pub const ENV_PREFIX: &str = "BUNDLED_ARTIFACT_"; -/// Publish an artifact at `path` so `embedded_artifact`'s `artifact!($name)` +/// Publish an artifact at `path` so `bundled_artifact`'s `artifact!($name)` /// macro can embed it. /// /// Emits three `cargo::…` directives: /// `rerun-if-changed={path}`, -/// `rustc-env=EMBEDDED_ARTIFACT_{name}_PATH={path}`, and -/// `rustc-env=EMBEDDED_ARTIFACT_{name}_HASH={hex}`. The runtime resolves these +/// `rustc-env=BUNDLED_ARTIFACT_{name}_PATH={path}`, and +/// `rustc-env=BUNDLED_ARTIFACT_{name}_HASH={hex}`. The runtime resolves these /// at compile time via `include_bytes!(env!(…))` and `env!(…)`. /// /// `name` is used both as the env-var key and as the on-disk filename prefix -/// (in `Artifact::write_to`), so it must be a valid identifier-like string +/// (in `Artifact::ensure_in`), so it must be a valid identifier-like string /// that matches the one passed to `artifact!`. /// /// # Panics diff --git a/crates/embedded_artifact/README.md b/crates/embedded_artifact/README.md deleted file mode 100644 index b232849cf..000000000 --- a/crates/embedded_artifact/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# embedded_artifact - -Binary assets embedded in an executable and extracted to disk on demand, with content-addressed filenames so repeated extractions reuse the same file. diff --git a/crates/embedded_artifact_build/README.md b/crates/embedded_artifact_build/README.md deleted file mode 100644 index 99a140450..000000000 --- a/crates/embedded_artifact_build/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# embedded_artifact_build - -Build-script helper for publishing artifacts consumed by `embedded_artifact`'s `artifact!` macro. diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index b9d312071..fca3946cc 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -10,7 +10,7 @@ wincode = { workspace = true } bstr = { workspace = true, default-features = false } bumpalo = { workspace = true } derive_more = { workspace = true, features = ["debug"] } -embedded_artifact = { workspace = true } +bundled_artifact = { workspace = true } fspy_shared = { workspace = true } futures-util = { workspace = true } libc = { workspace = true } @@ -61,7 +61,7 @@ fspy_test_bin = { path = "../fspy_test_bin", artifact = "bin", target = "x86_64- # builds are cheap. [build-dependencies] anyhow = { workspace = true } -embedded_artifact_build = { workspace = true } +bundled_artifact_build = { workspace = true } flate2 = { workspace = true } fspy_preload_unix = { path = "../fspy_preload_unix", artifact = "cdylib", target = "target" } fspy_preload_windows = { path = "../fspy_preload_windows", artifact = "cdylib", target = "target" } diff --git a/crates/fspy/build.rs b/crates/fspy/build.rs index 449ede016..3f77d1aba 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -129,7 +129,7 @@ fn fetch_macos_binaries(out_dir: &Path) -> anyhow::Result<()> { let data = unpack_tar_gz(Cursor::new(tarball), path_in_targz) .context(format!("Failed to extract {path_in_targz} from {url}"))?; fs::write(&dest, &data).with_context(|| format!("writing {}", dest.display()))?; - embedded_artifact_build::register(name, &dest); + bundled_artifact_build::register(name, &dest); } Ok(()) } @@ -144,7 +144,7 @@ fn register_preload_cdylib() -> anyhow::Result<()> { // the path changes. Track it so we re-publish the hash on update. println!("cargo::rerun-if-env-changed={env_name}"); let dylib_path = env::var_os(env_name).with_context(|| format!("{env_name} not set"))?; - embedded_artifact_build::register("fspy_preload", Path::new(&dylib_path)); + bundled_artifact_build::register("fspy_preload", Path::new(&dylib_path)); Ok(()) } diff --git a/crates/fspy/src/unix/macos_artifacts.rs b/crates/fspy/src/unix/macos_artifacts.rs index d39d2348b..fa2f58789 100644 --- a/crates/fspy/src/unix/macos_artifacts.rs +++ b/crates/fspy/src/unix/macos_artifacts.rs @@ -1,4 +1,4 @@ -use embedded_artifact::{Artifact, artifact}; +use bundled_artifact::{Artifact, artifact}; pub const COREUTILS_BINARY: Artifact = artifact!("coreutils"); pub const OILS_BINARY: Artifact = artifact!("oils_for_unix"); @@ -14,7 +14,7 @@ mod tests { #[test] fn coreutils_functions() { let tmpdir = tempfile::tempdir().unwrap(); - let coreutils_path = COREUTILS_BINARY.write_to(&tmpdir, "").unwrap(); + let coreutils_path = COREUTILS_BINARY.ensure_in(&tmpdir, "").unwrap(); let output = Command::new(coreutils_path).arg("--list").output().unwrap(); let mut expected_functions: Vec<&str> = output .stdout diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index 4ae22fe22..767ed2788 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -45,11 +45,11 @@ impl SpyImpl { pub fn init_in(#[cfg_attr(target_env = "musl", allow(unused))] dir: &Path) -> io::Result { #[cfg(not(target_env = "musl"))] let preload_path = { - use embedded_artifact::{Artifact, artifact}; + use bundled_artifact::{Artifact, artifact}; const PRELOAD_CDYLIB: Artifact = artifact!("fspy_preload"); - let preload_cdylib_path = PRELOAD_CDYLIB.write_to(dir, ".dylib")?; + let preload_cdylib_path = PRELOAD_CDYLIB.ensure_in(dir, ".dylib")?; preload_cdylib_path.as_path().into() }; @@ -58,8 +58,8 @@ impl SpyImpl { preload_path, #[cfg(target_os = "macos")] artifacts: { - let coreutils_path = macos_artifacts::COREUTILS_BINARY.write_to(dir, "")?; - let bash_path = macos_artifacts::OILS_BINARY.write_to(dir, "")?; + let coreutils_path = macos_artifacts::COREUTILS_BINARY.ensure_in(dir, "")?; + let bash_path = macos_artifacts::OILS_BINARY.ensure_in(dir, "")?; Artifacts { bash_path: bash_path.as_path().into(), coreutils_path: coreutils_path.as_path().into(), diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 4c6b91330..7e0864dd2 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -6,7 +6,7 @@ use std::{ sync::Arc, }; -use embedded_artifact::{Artifact, artifact}; +use bundled_artifact::{Artifact, artifact}; use fspy_detours_sys::{DetourCopyPayloadToProcess, DetourUpdateProcessWithDll}; use fspy_shared::{ ipc::{PathAccess, channel::channel}, @@ -51,7 +51,7 @@ pub struct SpyImpl { impl SpyImpl { pub fn init_in(path: &Path) -> io::Result { - let dll_path = INTERPOSE_CDYLIB.write_to(path, ".dll").unwrap(); + let dll_path = INTERPOSE_CDYLIB.ensure_in(path, ".dll").unwrap(); let wide_dll_path = dll_path.as_os_str().encode_wide().collect::>(); let mut ansi_dll_path = From 77bdc8dc795014397d02bea439310d3cefadc77c Mon Sep 17 00:00:00 2001 From: branchseer Date: Sat, 18 Apr 2026 13:55:34 +0800 Subject: [PATCH 08/19] update --- Cargo.lock | 2 +- crates/bundled_artifact/Cargo.toml | 2 +- crates/bundled_artifact/README.md | 6 +- crates/bundled_artifact/src/lib.rs | 116 ++++++++++++++++++++---- crates/fspy/src/unix/macos_artifacts.rs | 2 +- crates/fspy/src/unix/mod.rs | 6 +- crates/fspy/src/windows/mod.rs | 2 +- 7 files changed, 108 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9235abed9..04d876ae1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,7 +339,7 @@ dependencies = [ name = "bundled_artifact" version = "0.0.0" dependencies = [ - "rand 0.9.2", + "tempfile", ] [[package]] diff --git a/crates/bundled_artifact/Cargo.toml b/crates/bundled_artifact/Cargo.toml index f2583c881..e4b251b9a 100644 --- a/crates/bundled_artifact/Cargo.toml +++ b/crates/bundled_artifact/Cargo.toml @@ -7,7 +7,7 @@ publish = false rust-version.workspace = true [dependencies] -rand = { workspace = true } +tempfile = { workspace = true } [lints] workspace = true diff --git a/crates/bundled_artifact/README.md b/crates/bundled_artifact/README.md index aa77c89ba..f84a63f8d 100644 --- a/crates/bundled_artifact/README.md +++ b/crates/bundled_artifact/README.md @@ -1,3 +1,7 @@ # bundled_artifact -Binary artifacts bundled with an executable and materialized to disk on demand, with content-addressed filenames so repeated materializations reuse the same file. +Bundle a file into the executable and materialize it to disk on demand, for +APIs that need a filesystem path (`LoadLibrary`, `LD_PRELOAD`, helper +binaries). The on-disk filename is content-addressed so repeated calls skip +writing, multiple versions coexist, and stale files are never mistaken for +current ones. See crate-level docs for details. diff --git a/crates/bundled_artifact/src/lib.rs b/crates/bundled_artifact/src/lib.rs index 884aece3e..9884b952d 100644 --- a/crates/bundled_artifact/src/lib.rs +++ b/crates/bundled_artifact/src/lib.rs @@ -1,10 +1,37 @@ +//! Bundle a file into the executable and materialize it to disk on demand. +//! +//! Some APIs need a file on disk — `LoadLibrary` and `LD_PRELOAD` take a +//! path, and helper binaries have to exist as actual files to be spawned — +//! but we want to ship a single executable. `bundled_artifact` embeds the +//! file content as a `&'static [u8]` at compile time via the [`artifact!`] +//! macro, and [`Artifact::ensure_in`] writes it out to disk when first +//! needed. +//! +//! Materialized files are named `{name}_{hash}{suffix}` in the caller-chosen +//! directory. The hash (computed at build time by +//! `bundled_artifact_build::register`) gives three properties without any +//! coordination between processes: +//! +//! - **No repeated writes.** [`Artifact::ensure_in`] returns the existing +//! path if the file is already there; repeated calls and re-runs skip I/O. +//! - **Correctness.** Two binaries with different embedded content produce +//! different filenames, so a stale file from an older build is never +//! mistaken for the current one. +//! - **Coexistence.** Multiple versions of a bundled artifact (e.g. from +//! different builds of the host program on the same machine) share `dir` +//! without overwriting each other. + use std::{ - fs::{self, OpenOptions}, + fs, io::{self, Write}, path::{Path, PathBuf}, }; -/// An artifact (e.g., a DLL or shared library) whose content is embedded and needs to be written to disk. +/// A file bundled into the executable. Construct with [`artifact!`]; +/// materialize to disk with [`Artifact::ensure_in`]. See the [crate docs] +/// for the design rationale. +/// +/// [crate docs]: crate pub struct Artifact { name: &'static str, content: &'static [u8], @@ -37,36 +64,85 @@ impl Artifact { } /// Ensure the artifact is materialized in `dir` under a content-addressed - /// filename, writing it if missing. + /// filename, writing it if missing. `executable` picks the Unix mode + /// (`0o755` vs `0o644`) for newly created files, and reconciles an + /// existing file's mode if it drifted. On non-Unix targets `executable` + /// has no effect. + /// + /// Returns the final path. If the target already exists and its mode + /// already matches `executable`, no I/O beyond the stat is performed. /// - /// Returns the final path. If a file with the same hash already exists at - /// the target path, it is reused without rewriting. + /// # Preconditions + /// + /// `dir` must already exist — this method does not create it. /// /// # Errors /// - /// Returns an error if the directory can't be read/written, or if the - /// temp-file rename fails and the destination still doesn't exist. - pub fn ensure_in(&self, dir: impl AsRef, suffix: &str) -> io::Result { + /// Returns an error if the directory can't be read/written, the stat + /// fails for any reason other than not-found, or the temp-file rename + /// fails and the destination still doesn't exist. + pub fn ensure_in( + &self, + dir: impl AsRef, + suffix: &str, + executable: bool, + ) -> io::Result { let dir = dir.as_ref(); let path = dir.join(format!("{}_{}{}", self.name, self.hash, suffix)); - if fs::exists(&path)? { - return Ok(path); + #[cfg(unix)] + let want_mode: u32 = if executable { 0o755 } else { 0o644 }; + #[cfg(not(unix))] + let _ = executable; // Unix-mode concept; no-op on Windows. + + // Fast path: one stat tells us both whether the file exists and, + // on Unix, what its permission bits are. The content is assumed + // correct because the hash is in the filename, so there is nothing + // else to verify. + match fs::metadata(&path) { + #[cfg(unix)] + Ok(meta) => { + use std::os::unix::fs::PermissionsExt; + // Reconcile a drifted mode (e.g. someone chmod'd it away) + // but skip the syscall when it already matches. + if meta.permissions().mode() & 0o777 != want_mode { + fs::set_permissions(&path, fs::Permissions::from_mode(want_mode))?; + } + return Ok(path); + } + // On non-Unix there is no mode to reconcile; existence alone is + // enough to declare success. + #[cfg(not(unix))] + Ok(_) => return Ok(path), + // Not found: fall through to the create-and-rename path. + Err(err) if err.kind() == io::ErrorKind::NotFound => {} + // Any other stat failure (permission denied, I/O error, etc.) + // propagates — we can't reason about what's on disk. + Err(err) => return Err(err), } - let tmp_path = dir.join(format!("{:x}", rand::random::())); - let mut tmp_file_open_options = OpenOptions::new(); - tmp_file_open_options.write(true).create_new(true); + + // Slow path: write to a unique temp file in the same directory, then + // rename into place atomically. `NamedTempFile`'s `Drop` removes the + // temp if we bail before `persist_noclobber`, avoiding orphaned files + // on errors. #[cfg(unix)] - std::os::unix::fs::OpenOptionsExt::mode(&mut tmp_file_open_options, 0o755); // executable - let mut tmp_file = tmp_file_open_options.open(&tmp_path)?; - tmp_file.write_all(self.content)?; - drop(tmp_file); + let mut tmp = { + use std::os::unix::fs::PermissionsExt; + tempfile::Builder::new() + .permissions(fs::Permissions::from_mode(want_mode)) + .tempfile_in(dir)? + }; + #[cfg(not(unix))] + let mut tmp = tempfile::NamedTempFile::new_in(dir)?; + tmp.as_file_mut().write_all(self.content)?; - if let Err(err) = fs::rename(&tmp_path, &path) { + if let Err(err) = tmp.persist_noclobber(&path) { + // If another process won the race and the destination now exists, + // treat that as success; `err.file` drops here, cleaning up our + // temp. Otherwise propagate the original error. if !fs::exists(&path)? { - return Err(err); + return Err(err.error); } - fs::remove_file(&tmp_path)?; } Ok(path) } diff --git a/crates/fspy/src/unix/macos_artifacts.rs b/crates/fspy/src/unix/macos_artifacts.rs index fa2f58789..4ca0755d4 100644 --- a/crates/fspy/src/unix/macos_artifacts.rs +++ b/crates/fspy/src/unix/macos_artifacts.rs @@ -14,7 +14,7 @@ mod tests { #[test] fn coreutils_functions() { let tmpdir = tempfile::tempdir().unwrap(); - let coreutils_path = COREUTILS_BINARY.ensure_in(&tmpdir, "").unwrap(); + let coreutils_path = COREUTILS_BINARY.ensure_in(&tmpdir, "", true).unwrap(); let output = Command::new(coreutils_path).arg("--list").output().unwrap(); let mut expected_functions: Vec<&str> = output .stdout diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index 767ed2788..b5dee80da 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -49,7 +49,7 @@ impl SpyImpl { const PRELOAD_CDYLIB: Artifact = artifact!("fspy_preload"); - let preload_cdylib_path = PRELOAD_CDYLIB.ensure_in(dir, ".dylib")?; + let preload_cdylib_path = PRELOAD_CDYLIB.ensure_in(dir, ".dylib", false)?; preload_cdylib_path.as_path().into() }; @@ -58,8 +58,8 @@ impl SpyImpl { preload_path, #[cfg(target_os = "macos")] artifacts: { - let coreutils_path = macos_artifacts::COREUTILS_BINARY.ensure_in(dir, "")?; - let bash_path = macos_artifacts::OILS_BINARY.ensure_in(dir, "")?; + let coreutils_path = macos_artifacts::COREUTILS_BINARY.ensure_in(dir, "", true)?; + let bash_path = macos_artifacts::OILS_BINARY.ensure_in(dir, "", true)?; Artifacts { bash_path: bash_path.as_path().into(), coreutils_path: coreutils_path.as_path().into(), diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 7e0864dd2..1906d94de 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -51,7 +51,7 @@ pub struct SpyImpl { impl SpyImpl { pub fn init_in(path: &Path) -> io::Result { - let dll_path = INTERPOSE_CDYLIB.ensure_in(path, ".dll").unwrap(); + let dll_path = INTERPOSE_CDYLIB.ensure_in(path, ".dll", false).unwrap(); let wide_dll_path = dll_path.as_os_str().encode_wide().collect::>(); let mut ansi_dll_path = From 966570da91958d62b35f684a215393423bd64f1e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 07:19:09 +0000 Subject: [PATCH 09/19] fix: address PR #344 review feedback - Reuse extracted macOS binaries in OUT_DIR instead of re-downloading on every build-script rerun (previous regression would break offline/CI). - Switch build-script directives from `cargo::` back to `cargo:` for consistency with the rest of the build scripts. - Propagate I/O errors from `Artifact::ensure_in` instead of unwrapping in `SpyImpl::init_in`. - Inherit `target = "target"` for the fspy_preload cdylib artifact deps through the workspace so `cargo autoinherit` is idempotent. --- Cargo.toml | 4 ++-- crates/bundled_artifact_build/src/lib.rs | 8 ++++---- crates/fspy/Cargo.toml | 4 ++-- crates/fspy/build.rs | 25 ++++++++++++++---------- crates/fspy/src/windows/mod.rs | 2 +- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d61e4889e..b739d4547 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,8 +75,8 @@ bundled_artifact_build = { path = "crates/bundled_artifact_build" } flate2 = "1.0.35" fspy = { path = "crates/fspy" } fspy_detours_sys = { path = "crates/fspy_detours_sys" } -fspy_preload_unix = { path = "crates/fspy_preload_unix", artifact = "cdylib" } -fspy_preload_windows = { path = "crates/fspy_preload_windows", artifact = "cdylib" } +fspy_preload_unix = { path = "crates/fspy_preload_unix", artifact = "cdylib", target = "target" } +fspy_preload_windows = { path = "crates/fspy_preload_windows", artifact = "cdylib", target = "target" } fspy_seccomp_unotify = { path = "crates/fspy_seccomp_unotify" } fspy_shared = { path = "crates/fspy_shared" } fspy_shared_unix = { path = "crates/fspy_shared_unix" } diff --git a/crates/bundled_artifact_build/src/lib.rs b/crates/bundled_artifact_build/src/lib.rs index 6a992767c..bd19aa08b 100644 --- a/crates/bundled_artifact_build/src/lib.rs +++ b/crates/bundled_artifact_build/src/lib.rs @@ -8,7 +8,7 @@ pub const ENV_PREFIX: &str = "BUNDLED_ARTIFACT_"; /// Publish an artifact at `path` so `bundled_artifact`'s `artifact!($name)` /// macro can embed it. /// -/// Emits three `cargo::…` directives: +/// Emits three `cargo:…` directives: /// `rerun-if-changed={path}`, /// `rustc-env=BUNDLED_ARTIFACT_{name}_PATH={path}`, and /// `rustc-env=BUNDLED_ARTIFACT_{name}_HASH={hex}`. The runtime resolves these @@ -27,11 +27,11 @@ pub fn register(name: &str, path: &Path) { { // Emit rerun-if-changed before reading so cargo still sees it even if // reading the file below panics. - println!("cargo::rerun-if-changed={path_str}"); + println!("cargo:rerun-if-changed={path_str}"); let bytes = fs::read(path).unwrap_or_else(|e| panic!("failed to read artifact at {path_str}: {e}")); let hash = format!("{:x}", xxhash_rust::xxh3::xxh3_128(&bytes)); - println!("cargo::rustc-env={ENV_PREFIX}{name}_PATH={path_str}"); - println!("cargo::rustc-env={ENV_PREFIX}{name}_HASH={hash}"); + println!("cargo:rustc-env={ENV_PREFIX}{name}_PATH={path_str}"); + println!("cargo:rustc-env={ENV_PREFIX}{name}_HASH={hash}"); } } diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index fca3946cc..7b1e4ebee 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -63,8 +63,8 @@ fspy_test_bin = { path = "../fspy_test_bin", artifact = "bin", target = "x86_64- anyhow = { workspace = true } bundled_artifact_build = { workspace = true } flate2 = { workspace = true } -fspy_preload_unix = { path = "../fspy_preload_unix", artifact = "cdylib", target = "target" } -fspy_preload_windows = { path = "../fspy_preload_windows", artifact = "cdylib", target = "target" } +fspy_preload_unix = { workspace = true } +fspy_preload_windows = { workspace = true } sha2 = { workspace = true } tar = { workspace = true } diff --git a/crates/fspy/build.rs b/crates/fspy/build.rs index 3f77d1aba..733f6ce4d 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -120,15 +120,20 @@ fn fetch_macos_binaries(out_dir: &Path) -> anyhow::Result<()> { for BinaryDownload { name, url, path_in_targz, expected_sha256 } in downloads { let dest = out_dir.join(name); - let tarball = download(url).context(format!("Failed to download {url}"))?; - let actual_sha256 = sha256_hex(&tarball); - assert_eq!( - &actual_sha256, expected_sha256, - "sha256 of {url} does not match — update expected value in MACOS_BINARY_DOWNLOADS", - ); - let data = unpack_tar_gz(Cursor::new(tarball), path_in_targz) - .context(format!("Failed to extract {path_in_targz} from {url}"))?; - fs::write(&dest, &data).with_context(|| format!("writing {}", dest.display()))?; + // Reuse the extracted binary if it's already in OUT_DIR; the sha256 + // of the tarball was verified on the initial download. This avoids + // hitting the network on incremental build-script reruns. + if !dest.exists() { + let tarball = download(url).context(format!("Failed to download {url}"))?; + let actual_sha256 = sha256_hex(&tarball); + assert_eq!( + &actual_sha256, expected_sha256, + "sha256 of {url} does not match — update expected value in MACOS_BINARY_DOWNLOADS", + ); + let data = unpack_tar_gz(Cursor::new(tarball), path_in_targz) + .context(format!("Failed to extract {path_in_targz} from {url}"))?; + fs::write(&dest, &data).with_context(|| format!("writing {}", dest.display()))?; + } bundled_artifact_build::register(name, &dest); } Ok(()) @@ -142,7 +147,7 @@ fn register_preload_cdylib() -> anyhow::Result<()> { }; // The cdylib path is content-addressed by cargo; when its content changes // the path changes. Track it so we re-publish the hash on update. - println!("cargo::rerun-if-env-changed={env_name}"); + println!("cargo:rerun-if-env-changed={env_name}"); let dylib_path = env::var_os(env_name).with_context(|| format!("{env_name} not set"))?; bundled_artifact_build::register("fspy_preload", Path::new(&dylib_path)); Ok(()) diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 1906d94de..5311aa951 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -51,7 +51,7 @@ pub struct SpyImpl { impl SpyImpl { pub fn init_in(path: &Path) -> io::Result { - let dll_path = INTERPOSE_CDYLIB.ensure_in(path, ".dll", false).unwrap(); + let dll_path = INTERPOSE_CDYLIB.ensure_in(path, ".dll", false)?; let wide_dll_path = dll_path.as_os_str().encode_wide().collect::>(); let mut ansi_dll_path = From 0e6a788eb0e0b06804d1ab63e2dc80aab1aef988 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 07:25:54 +0000 Subject: [PATCH 10/19] chore: remove unused deps flagged by cargo-shear - Drop `rand` from fspy (was used by the extracted `artifact.rs`, replaced by tempfile in the new `bundled_artifact` crate). - Drop workspace-level `rand` and `const_format` (no longer used by any member). - Set `test = false` on the lib targets of `bundled_artifact` and `bundled_artifact_build` (no unit tests yet). --- Cargo.lock | 1 - Cargo.toml | 2 -- crates/bundled_artifact/Cargo.toml | 1 + crates/bundled_artifact_build/Cargo.toml | 1 + crates/fspy/Cargo.toml | 1 - 5 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04d876ae1..8499bc07e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1214,7 +1214,6 @@ dependencies = [ "libc", "nix 0.30.1", "ouroboros", - "rand 0.9.2", "rustc-hash", "sha2", "subprocess_test", diff --git a/Cargo.toml b/Cargo.toml index b739d4547..80d414e5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,6 @@ cc = "1.2.39" clap = "4.5.53" color-eyre = "0.6.5" compact_str = "0.9.0" -const_format = "0.2.34" constcat = "0.6.1" copy_dir = "0.1.3" cow-utils = "0.1.3" @@ -105,7 +104,6 @@ pretty_assertions = "1.4.1" pty_terminal = { path = "crates/pty_terminal" } pty_terminal_test = { path = "crates/pty_terminal_test" } pty_terminal_test_client = { path = "crates/pty_terminal_test_client" } -rand = "0.9.1" ratatui = "0.30.0" rayon = "1.10.0" ref-cast = "1.0.24" diff --git a/crates/bundled_artifact/Cargo.toml b/crates/bundled_artifact/Cargo.toml index e4b251b9a..cac239d64 100644 --- a/crates/bundled_artifact/Cargo.toml +++ b/crates/bundled_artifact/Cargo.toml @@ -14,3 +14,4 @@ workspace = true [lib] doctest = false +test = false diff --git a/crates/bundled_artifact_build/Cargo.toml b/crates/bundled_artifact_build/Cargo.toml index d3a2287e2..a94367fa9 100644 --- a/crates/bundled_artifact_build/Cargo.toml +++ b/crates/bundled_artifact_build/Cargo.toml @@ -14,3 +14,4 @@ workspace = true [lib] doctest = false +test = false diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index 7b1e4ebee..aef025a71 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -15,7 +15,6 @@ fspy_shared = { workspace = true } futures-util = { workspace = true } libc = { workspace = true } ouroboros = { workspace = true } -rand = { workspace = true } rustc-hash = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } From 0d9eb150a96bd3958208dfa26111bd1d8cdf1079 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 07:35:01 +0000 Subject: [PATCH 11/19] =?UTF-8?q?fix(fspy/build):=20match=20main's=20cache?= =?UTF-8?q?=20workflow=20=E2=80=94=20hash=20extracted=20binary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port main's `fetch_macos_binaries` cache logic: hash the cached file on every build, skip download when it matches `expected_sha256`, and verify the freshly-extracted binary against the same value on a cache miss. The hash is now over the extracted binary (not the tarball), which lets the cached file itself vouch for its own integrity without needing the original tarball on disk. --- crates/fspy/build.rs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/crates/fspy/build.rs b/crates/fspy/build.rs index 733f6ce4d..8f1fcc2d0 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -60,8 +60,9 @@ struct BinaryDownload { url: &'static str, /// Path of the binary within the tarball. path_in_targz: &'static str, - /// SHA-256 of the tarball at `url`. Each value can be obtained from the - /// release download page. + /// SHA-256 of the extracted binary. Doubles as the cache key: an + /// already-extracted binary in `OUT_DIR` whose content hashes to this + /// value is reused without hitting the network. expected_sha256: &'static str, } @@ -74,14 +75,14 @@ const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ name: "oils_for_unix", url: "https://github.com/branchseer/oils-for-unix-build/releases/download/oils-for-unix-0.37.0/oils-for-unix-0.37.0-darwin-arm64.tar.gz", path_in_targz: "oils-for-unix", - expected_sha256: "3a35f7ae2be85fcd32392cd8171522f5822f20a69125c5e9d8d68b2f5c857098", + expected_sha256: "ce4bb80b15f0a0371af08b19b65bfa5ea17d30429ebb911f487de3d2bcc7a07d", }, // https://github.com/uutils/coreutils/releases/tag/0.4.0 BinaryDownload { name: "coreutils", url: "https://github.com/uutils/coreutils/releases/download/0.4.0/coreutils-0.4.0-aarch64-apple-darwin.tar.gz", path_in_targz: "coreutils-0.4.0-aarch64-apple-darwin/coreutils", - expected_sha256: "a148b660eeaf409af7a4406903f93d0e6713a5eb9adcaf71a1d732f1e3cc3522", + expected_sha256: "8e8f38d9323135a19a73d617336fce85380f3c46fcb83d3ae3e031d1c0372f21", }, ], ), @@ -93,14 +94,14 @@ const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ name: "oils_for_unix", url: "https://github.com/branchseer/oils-for-unix-build/releases/download/oils-for-unix-0.37.0/oils-for-unix-0.37.0-darwin-x86_64.tar.gz", path_in_targz: "oils-for-unix", - expected_sha256: "aa12258d1bd553020144ad61fdac18e7dfbe3fc3965da32ee458840153169151", + expected_sha256: "cf1a95993127770e2a5fff277cd256a2bb28cf97d7f83ae42fdccc172cdb540d", }, // https://github.com/uutils/coreutils/releases/tag/0.4.0 BinaryDownload { name: "coreutils", url: "https://github.com/uutils/coreutils/releases/download/0.4.0/coreutils-0.4.0-x86_64-apple-darwin.tar.gz", path_in_targz: "coreutils-0.4.0-x86_64-apple-darwin/coreutils", - expected_sha256: "6e4be8429efe86c9a60247ae7a930221ed11770a975fb4b6fd09ff8d39b9a15c", + expected_sha256: "6be8bee6e8b91fc44a465203b9cc30538af00084b6657dc136d9e55837753eb1", }, ], ), @@ -120,18 +121,21 @@ fn fetch_macos_binaries(out_dir: &Path) -> anyhow::Result<()> { for BinaryDownload { name, url, path_in_targz, expected_sha256 } in downloads { let dest = out_dir.join(name); - // Reuse the extracted binary if it's already in OUT_DIR; the sha256 - // of the tarball was verified on the initial download. This avoids - // hitting the network on incremental build-script reruns. - if !dest.exists() { + // Cache hit: an already-extracted binary whose contents hash to + // `expected_sha256` is known-good and reused without redownloading. + let cached = matches!( + fs::read(&dest), + Ok(existing) if sha256_hex(&existing) == *expected_sha256, + ); + if !cached { let tarball = download(url).context(format!("Failed to download {url}"))?; - let actual_sha256 = sha256_hex(&tarball); + let data = unpack_tar_gz(Cursor::new(tarball), path_in_targz) + .context(format!("Failed to extract {path_in_targz} from {url}"))?; + let actual_sha256 = sha256_hex(&data); assert_eq!( &actual_sha256, expected_sha256, - "sha256 of {url} does not match — update expected value in MACOS_BINARY_DOWNLOADS", + "sha256 of {path_in_targz} in {url} does not match — update expected value in MACOS_BINARY_DOWNLOADS", ); - let data = unpack_tar_gz(Cursor::new(tarball), path_in_targz) - .context(format!("Failed to extract {path_in_targz} from {url}"))?; fs::write(&dest, &data).with_context(|| format!("writing {}", dest.display()))?; } bundled_artifact_build::register(name, &dest); From a2d7f8671116c513c7bfe564482c0c2746ef661c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 07:48:58 +0000 Subject: [PATCH 12/19] refactor: rename bundled_artifact -> materialized_artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Bundled" clashes with `include_bytes!`, which also bundles. The distinctive feature of this crate is that it gives you a real path on disk for APIs that need one (`LoadLibrary`, `LD_PRELOAD`, helper binaries) — the value-add over `include_bytes!` is the materialization step. The env-var prefix moves to `MATERIALIZED_ARTIFACT_` accordingly. --- Cargo.lock | 32 +++++++++---------- Cargo.toml | 4 +-- crates/bundled_artifact/README.md | 7 ---- crates/bundled_artifact_build/README.md | 3 -- crates/fspy/Cargo.toml | 4 +-- crates/fspy/build.rs | 4 +-- crates/fspy/src/unix/macos_artifacts.rs | 2 +- crates/fspy/src/unix/mod.rs | 2 +- crates/fspy/src/windows/mod.rs | 2 +- .../.clippy.toml | 0 .../Cargo.toml | 2 +- crates/materialized_artifact/README.md | 8 +++++ .../src/lib.rs | 31 +++++++++--------- .../.clippy.toml | 0 .../Cargo.toml | 2 +- crates/materialized_artifact_build/README.md | 4 +++ .../src/lib.rs | 14 ++++---- 17 files changed, 62 insertions(+), 59 deletions(-) delete mode 100644 crates/bundled_artifact/README.md delete mode 100644 crates/bundled_artifact_build/README.md rename crates/{bundled_artifact => materialized_artifact}/.clippy.toml (100%) rename crates/{bundled_artifact => materialized_artifact}/Cargo.toml (88%) create mode 100644 crates/materialized_artifact/README.md rename crates/{bundled_artifact => materialized_artifact}/src/lib.rs (82%) rename crates/{bundled_artifact_build => materialized_artifact_build}/.clippy.toml (100%) rename crates/{bundled_artifact_build => materialized_artifact_build}/Cargo.toml (87%) create mode 100644 crates/materialized_artifact_build/README.md rename crates/{bundled_artifact_build => materialized_artifact_build}/src/lib.rs (72%) diff --git a/Cargo.lock b/Cargo.lock index 8499bc07e..bf11ea1bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,20 +335,6 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "bundled_artifact" -version = "0.0.0" -dependencies = [ - "tempfile", -] - -[[package]] -name = "bundled_artifact_build" -version = "0.0.0" -dependencies = [ - "xxhash-rust", -] - [[package]] name = "bytemuck" version = "1.25.0" @@ -1197,8 +1183,6 @@ dependencies = [ "anyhow", "bstr", "bumpalo", - "bundled_artifact", - "bundled_artifact_build", "csv-async", "ctor", "derive_more", @@ -1212,6 +1196,8 @@ dependencies = [ "fspy_test_bin", "futures-util", "libc", + "materialized_artifact", + "materialized_artifact_build", "nix 0.30.1", "ouroboros", "rustc-hash", @@ -1858,6 +1844,20 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "materialized_artifact" +version = "0.0.0" +dependencies = [ + "tempfile", +] + +[[package]] +name = "materialized_artifact_build" +version = "0.0.0" +dependencies = [ + "xxhash-rust", +] + [[package]] name = "memchr" version = "2.8.0" diff --git a/Cargo.toml b/Cargo.toml index 80d414e5e..85c0f40f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,8 +69,8 @@ derive_more = "2.0.1" diff-struct = "0.5.3" directories = "6.0.0" elf = { version = "0.8.0", default-features = false } -bundled_artifact = { path = "crates/bundled_artifact" } -bundled_artifact_build = { path = "crates/bundled_artifact_build" } +materialized_artifact = { path = "crates/materialized_artifact" } +materialized_artifact_build = { path = "crates/materialized_artifact_build" } flate2 = "1.0.35" fspy = { path = "crates/fspy" } fspy_detours_sys = { path = "crates/fspy_detours_sys" } diff --git a/crates/bundled_artifact/README.md b/crates/bundled_artifact/README.md deleted file mode 100644 index f84a63f8d..000000000 --- a/crates/bundled_artifact/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# bundled_artifact - -Bundle a file into the executable and materialize it to disk on demand, for -APIs that need a filesystem path (`LoadLibrary`, `LD_PRELOAD`, helper -binaries). The on-disk filename is content-addressed so repeated calls skip -writing, multiple versions coexist, and stale files are never mistaken for -current ones. See crate-level docs for details. diff --git a/crates/bundled_artifact_build/README.md b/crates/bundled_artifact_build/README.md deleted file mode 100644 index 5894c95b8..000000000 --- a/crates/bundled_artifact_build/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# bundled_artifact_build - -Build-script helper for publishing artifacts consumed by `bundled_artifact`'s `artifact!` macro. diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index aef025a71..e8443e55f 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -10,7 +10,7 @@ wincode = { workspace = true } bstr = { workspace = true, default-features = false } bumpalo = { workspace = true } derive_more = { workspace = true, features = ["debug"] } -bundled_artifact = { workspace = true } +materialized_artifact = { workspace = true } fspy_shared = { workspace = true } futures-util = { workspace = true } libc = { workspace = true } @@ -60,7 +60,7 @@ fspy_test_bin = { path = "../fspy_test_bin", artifact = "bin", target = "x86_64- # builds are cheap. [build-dependencies] anyhow = { workspace = true } -bundled_artifact_build = { workspace = true } +materialized_artifact_build = { workspace = true } flate2 = { workspace = true } fspy_preload_unix = { workspace = true } fspy_preload_windows = { workspace = true } diff --git a/crates/fspy/build.rs b/crates/fspy/build.rs index 8f1fcc2d0..b6659f155 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -138,7 +138,7 @@ fn fetch_macos_binaries(out_dir: &Path) -> anyhow::Result<()> { ); fs::write(&dest, &data).with_context(|| format!("writing {}", dest.display()))?; } - bundled_artifact_build::register(name, &dest); + materialized_artifact_build::register(name, &dest); } Ok(()) } @@ -153,7 +153,7 @@ fn register_preload_cdylib() -> anyhow::Result<()> { // the path changes. Track it so we re-publish the hash on update. println!("cargo:rerun-if-env-changed={env_name}"); let dylib_path = env::var_os(env_name).with_context(|| format!("{env_name} not set"))?; - bundled_artifact_build::register("fspy_preload", Path::new(&dylib_path)); + materialized_artifact_build::register("fspy_preload", Path::new(&dylib_path)); Ok(()) } diff --git a/crates/fspy/src/unix/macos_artifacts.rs b/crates/fspy/src/unix/macos_artifacts.rs index 4ca0755d4..287b5c645 100644 --- a/crates/fspy/src/unix/macos_artifacts.rs +++ b/crates/fspy/src/unix/macos_artifacts.rs @@ -1,4 +1,4 @@ -use bundled_artifact::{Artifact, artifact}; +use materialized_artifact::{Artifact, artifact}; pub const COREUTILS_BINARY: Artifact = artifact!("coreutils"); pub const OILS_BINARY: Artifact = artifact!("oils_for_unix"); diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index b5dee80da..4c838e263 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -45,7 +45,7 @@ impl SpyImpl { pub fn init_in(#[cfg_attr(target_env = "musl", allow(unused))] dir: &Path) -> io::Result { #[cfg(not(target_env = "musl"))] let preload_path = { - use bundled_artifact::{Artifact, artifact}; + use materialized_artifact::{Artifact, artifact}; const PRELOAD_CDYLIB: Artifact = artifact!("fspy_preload"); diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 5311aa951..9f6ab0399 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -6,13 +6,13 @@ use std::{ sync::Arc, }; -use bundled_artifact::{Artifact, artifact}; use fspy_detours_sys::{DetourCopyPayloadToProcess, DetourUpdateProcessWithDll}; use fspy_shared::{ ipc::{PathAccess, channel::channel}, windows::{PAYLOAD_ID, Payload}, }; use futures_util::FutureExt; +use materialized_artifact::{Artifact, artifact}; use tokio_util::sync::CancellationToken; use winapi::{ shared::minwindef::TRUE, diff --git a/crates/bundled_artifact/.clippy.toml b/crates/materialized_artifact/.clippy.toml similarity index 100% rename from crates/bundled_artifact/.clippy.toml rename to crates/materialized_artifact/.clippy.toml diff --git a/crates/bundled_artifact/Cargo.toml b/crates/materialized_artifact/Cargo.toml similarity index 88% rename from crates/bundled_artifact/Cargo.toml rename to crates/materialized_artifact/Cargo.toml index cac239d64..643c40a15 100644 --- a/crates/bundled_artifact/Cargo.toml +++ b/crates/materialized_artifact/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bundled_artifact" +name = "materialized_artifact" version = "0.0.0" edition.workspace = true license.workspace = true diff --git a/crates/materialized_artifact/README.md b/crates/materialized_artifact/README.md new file mode 100644 index 000000000..60b505d35 --- /dev/null +++ b/crates/materialized_artifact/README.md @@ -0,0 +1,8 @@ +# materialized_artifact + +Materialize a compile-time–embedded file to disk on demand, for APIs that +need a filesystem path (`LoadLibrary`, `LD_PRELOAD`, helper binaries) rather +than the bytes you'd get from `include_bytes!`. The on-disk filename is +content-addressed so repeated calls skip writing, multiple versions coexist, +and stale files are never mistaken for current ones. See crate-level docs +for details. diff --git a/crates/bundled_artifact/src/lib.rs b/crates/materialized_artifact/src/lib.rs similarity index 82% rename from crates/bundled_artifact/src/lib.rs rename to crates/materialized_artifact/src/lib.rs index 9884b952d..128eb7d89 100644 --- a/crates/bundled_artifact/src/lib.rs +++ b/crates/materialized_artifact/src/lib.rs @@ -1,23 +1,24 @@ -//! Bundle a file into the executable and materialize it to disk on demand. +//! Materialize a compile-time–embedded file to disk on demand. //! //! Some APIs need a file on disk — `LoadLibrary` and `LD_PRELOAD` take a //! path, and helper binaries have to exist as actual files to be spawned — -//! but we want to ship a single executable. `bundled_artifact` embeds the -//! file content as a `&'static [u8]` at compile time via the [`artifact!`] -//! macro, and [`Artifact::ensure_in`] writes it out to disk when first -//! needed. +//! but we want to ship a single executable. `materialized_artifact` embeds +//! the file content as a `&'static [u8]` at compile time via the +//! [`artifact!`] macro (same as `include_bytes!`), and +//! [`Artifact::ensure_in`] writes it out to disk when first needed — that +//! materialization step is the value-add over a bare `include_bytes!`. //! //! Materialized files are named `{name}_{hash}{suffix}` in the caller-chosen //! directory. The hash (computed at build time by -//! `bundled_artifact_build::register`) gives three properties without any -//! coordination between processes: +//! `materialized_artifact_build::register`) gives three properties without +//! any coordination between processes: //! //! - **No repeated writes.** [`Artifact::ensure_in`] returns the existing //! path if the file is already there; repeated calls and re-runs skip I/O. //! - **Correctness.** Two binaries with different embedded content produce //! different filenames, so a stale file from an older build is never //! mistaken for the current one. -//! - **Coexistence.** Multiple versions of a bundled artifact (e.g. from +//! - **Coexistence.** Multiple versions of a materialized artifact (e.g. from //! different builds of the host program on the same machine) share `dir` //! without overwriting each other. @@ -27,9 +28,9 @@ use std::{ path::{Path, PathBuf}, }; -/// A file bundled into the executable. Construct with [`artifact!`]; -/// materialize to disk with [`Artifact::ensure_in`]. See the [crate docs] -/// for the design rationale. +/// A file embedded into the executable at compile time. Construct with +/// [`artifact!`]; materialize to disk with [`Artifact::ensure_in`]. See the +/// [crate docs] for the design rationale. /// /// [crate docs]: crate pub struct Artifact { @@ -39,19 +40,19 @@ pub struct Artifact { } /// Construct an [`Artifact`] from the env vars published by a build script -/// via `bundled_artifact_build::register`. Must match the `ENV_PREFIX` -/// constant in `bundled_artifact_build`. +/// via `materialized_artifact_build::register`. Must match the `ENV_PREFIX` +/// constant in `materialized_artifact_build`. #[macro_export] macro_rules! artifact { ($name:literal) => { $crate::Artifact::__new( $name, ::core::include_bytes!(::core::env!(::core::concat!( - "BUNDLED_ARTIFACT_", + "MATERIALIZED_ARTIFACT_", $name, "_PATH" ))), - ::core::env!(::core::concat!("BUNDLED_ARTIFACT_", $name, "_HASH")), + ::core::env!(::core::concat!("MATERIALIZED_ARTIFACT_", $name, "_HASH")), ) }; } diff --git a/crates/bundled_artifact_build/.clippy.toml b/crates/materialized_artifact_build/.clippy.toml similarity index 100% rename from crates/bundled_artifact_build/.clippy.toml rename to crates/materialized_artifact_build/.clippy.toml diff --git a/crates/bundled_artifact_build/Cargo.toml b/crates/materialized_artifact_build/Cargo.toml similarity index 87% rename from crates/bundled_artifact_build/Cargo.toml rename to crates/materialized_artifact_build/Cargo.toml index a94367fa9..c2d5dbd3a 100644 --- a/crates/bundled_artifact_build/Cargo.toml +++ b/crates/materialized_artifact_build/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bundled_artifact_build" +name = "materialized_artifact_build" version = "0.0.0" edition.workspace = true license.workspace = true diff --git a/crates/materialized_artifact_build/README.md b/crates/materialized_artifact_build/README.md new file mode 100644 index 000000000..7f727fada --- /dev/null +++ b/crates/materialized_artifact_build/README.md @@ -0,0 +1,4 @@ +# materialized_artifact_build + +Build-script helper for publishing artifacts consumed by +`materialized_artifact`'s `artifact!` macro. diff --git a/crates/bundled_artifact_build/src/lib.rs b/crates/materialized_artifact_build/src/lib.rs similarity index 72% rename from crates/bundled_artifact_build/src/lib.rs rename to crates/materialized_artifact_build/src/lib.rs index bd19aa08b..70ab48a7d 100644 --- a/crates/bundled_artifact_build/src/lib.rs +++ b/crates/materialized_artifact_build/src/lib.rs @@ -1,18 +1,18 @@ use std::{fs, path::Path}; /// Namespace prefix for the env vars set by [`register`] and consumed by -/// `bundled_artifact`'s `artifact!` macro. Exported so both crates agree on -/// the same prefix. -pub const ENV_PREFIX: &str = "BUNDLED_ARTIFACT_"; +/// `materialized_artifact`'s `artifact!` macro. Exported so both crates agree +/// on the same prefix. +pub const ENV_PREFIX: &str = "MATERIALIZED_ARTIFACT_"; -/// Publish an artifact at `path` so `bundled_artifact`'s `artifact!($name)` +/// Publish an artifact at `path` so `materialized_artifact`'s `artifact!($name)` /// macro can embed it. /// /// Emits three `cargo:…` directives: /// `rerun-if-changed={path}`, -/// `rustc-env=BUNDLED_ARTIFACT_{name}_PATH={path}`, and -/// `rustc-env=BUNDLED_ARTIFACT_{name}_HASH={hex}`. The runtime resolves these -/// at compile time via `include_bytes!(env!(…))` and `env!(…)`. +/// `rustc-env=MATERIALIZED_ARTIFACT_{name}_PATH={path}`, and +/// `rustc-env=MATERIALIZED_ARTIFACT_{name}_HASH={hex}`. The runtime resolves +/// these at compile time via `include_bytes!(env!(…))` and `env!(…)`. /// /// `name` is used both as the env-var key and as the on-disk filename prefix /// (in `Artifact::ensure_in`), so it must be a valid identifier-like string From 9eb4b53edb260d898638a5eaabbd700348783d9f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 07:50:44 +0000 Subject: [PATCH 13/19] refactor(materialized_artifact): rename ensure_in -> materialize_in The verb mirrors the crate name and makes the value-add over `include_bytes!` obvious at the call site. --- crates/fspy/src/unix/macos_artifacts.rs | 2 +- crates/fspy/src/unix/mod.rs | 6 +++--- crates/fspy/src/windows/mod.rs | 2 +- crates/materialized_artifact/src/lib.rs | 8 ++++---- crates/materialized_artifact_build/src/lib.rs | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/fspy/src/unix/macos_artifacts.rs b/crates/fspy/src/unix/macos_artifacts.rs index 287b5c645..39a6c8103 100644 --- a/crates/fspy/src/unix/macos_artifacts.rs +++ b/crates/fspy/src/unix/macos_artifacts.rs @@ -14,7 +14,7 @@ mod tests { #[test] fn coreutils_functions() { let tmpdir = tempfile::tempdir().unwrap(); - let coreutils_path = COREUTILS_BINARY.ensure_in(&tmpdir, "", true).unwrap(); + let coreutils_path = COREUTILS_BINARY.materialize_in(&tmpdir, "", true).unwrap(); let output = Command::new(coreutils_path).arg("--list").output().unwrap(); let mut expected_functions: Vec<&str> = output .stdout diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index 4c838e263..ba7d59776 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -49,7 +49,7 @@ impl SpyImpl { const PRELOAD_CDYLIB: Artifact = artifact!("fspy_preload"); - let preload_cdylib_path = PRELOAD_CDYLIB.ensure_in(dir, ".dylib", false)?; + let preload_cdylib_path = PRELOAD_CDYLIB.materialize_in(dir, ".dylib", false)?; preload_cdylib_path.as_path().into() }; @@ -58,8 +58,8 @@ impl SpyImpl { preload_path, #[cfg(target_os = "macos")] artifacts: { - let coreutils_path = macos_artifacts::COREUTILS_BINARY.ensure_in(dir, "", true)?; - let bash_path = macos_artifacts::OILS_BINARY.ensure_in(dir, "", true)?; + let coreutils_path = macos_artifacts::COREUTILS_BINARY.materialize_in(dir, "", true)?; + let bash_path = macos_artifacts::OILS_BINARY.materialize_in(dir, "", true)?; Artifacts { bash_path: bash_path.as_path().into(), coreutils_path: coreutils_path.as_path().into(), diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 9f6ab0399..1d5d6cd02 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -51,7 +51,7 @@ pub struct SpyImpl { impl SpyImpl { pub fn init_in(path: &Path) -> io::Result { - let dll_path = INTERPOSE_CDYLIB.ensure_in(path, ".dll", false)?; + let dll_path = INTERPOSE_CDYLIB.materialize_in(path, ".dll", false)?; let wide_dll_path = dll_path.as_os_str().encode_wide().collect::>(); let mut ansi_dll_path = diff --git a/crates/materialized_artifact/src/lib.rs b/crates/materialized_artifact/src/lib.rs index 128eb7d89..67c069181 100644 --- a/crates/materialized_artifact/src/lib.rs +++ b/crates/materialized_artifact/src/lib.rs @@ -5,7 +5,7 @@ //! but we want to ship a single executable. `materialized_artifact` embeds //! the file content as a `&'static [u8]` at compile time via the //! [`artifact!`] macro (same as `include_bytes!`), and -//! [`Artifact::ensure_in`] writes it out to disk when first needed — that +//! [`Artifact::materialize_in`] writes it out to disk when first needed — that //! materialization step is the value-add over a bare `include_bytes!`. //! //! Materialized files are named `{name}_{hash}{suffix}` in the caller-chosen @@ -13,7 +13,7 @@ //! `materialized_artifact_build::register`) gives three properties without //! any coordination between processes: //! -//! - **No repeated writes.** [`Artifact::ensure_in`] returns the existing +//! - **No repeated writes.** [`Artifact::materialize_in`] returns the existing //! path if the file is already there; repeated calls and re-runs skip I/O. //! - **Correctness.** Two binaries with different embedded content produce //! different filenames, so a stale file from an older build is never @@ -29,7 +29,7 @@ use std::{ }; /// A file embedded into the executable at compile time. Construct with -/// [`artifact!`]; materialize to disk with [`Artifact::ensure_in`]. See the +/// [`artifact!`]; materialize to disk with [`Artifact::materialize_in`]. See the /// [crate docs] for the design rationale. /// /// [crate docs]: crate @@ -82,7 +82,7 @@ impl Artifact { /// Returns an error if the directory can't be read/written, the stat /// fails for any reason other than not-found, or the temp-file rename /// fails and the destination still doesn't exist. - pub fn ensure_in( + pub fn materialize_in( &self, dir: impl AsRef, suffix: &str, diff --git a/crates/materialized_artifact_build/src/lib.rs b/crates/materialized_artifact_build/src/lib.rs index 70ab48a7d..0f76ff4a9 100644 --- a/crates/materialized_artifact_build/src/lib.rs +++ b/crates/materialized_artifact_build/src/lib.rs @@ -15,7 +15,7 @@ pub const ENV_PREFIX: &str = "MATERIALIZED_ARTIFACT_"; /// these at compile time via `include_bytes!(env!(…))` and `env!(…)`. /// /// `name` is used both as the env-var key and as the on-disk filename prefix -/// (in `Artifact::ensure_in`), so it must be a valid identifier-like string +/// (in `Artifact::materialize_in`), so it must be a valid identifier-like string /// that matches the one passed to `artifact!`. /// /// # Panics From 7d2f9a86b023968cd2f379ff1b29561a4c3cd5f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 07:52:46 +0000 Subject: [PATCH 14/19] 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). --- crates/fspy/src/unix/mod.rs | 3 ++- crates/materialized_artifact/src/lib.rs | 13 ++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index ba7d59776..d0625ac63 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -58,7 +58,8 @@ impl SpyImpl { preload_path, #[cfg(target_os = "macos")] artifacts: { - let coreutils_path = macos_artifacts::COREUTILS_BINARY.materialize_in(dir, "", true)?; + let coreutils_path = + macos_artifacts::COREUTILS_BINARY.materialize_in(dir, "", true)?; let bash_path = macos_artifacts::OILS_BINARY.materialize_in(dir, "", true)?; Artifacts { bash_path: bash_path.as_path().into(), diff --git a/crates/materialized_artifact/src/lib.rs b/crates/materialized_artifact/src/lib.rs index 67c069181..bc8cd8550 100644 --- a/crates/materialized_artifact/src/lib.rs +++ b/crates/materialized_artifact/src/lib.rs @@ -123,12 +123,15 @@ impl Artifact { } // Slow path: write to a unique temp file in the same directory, then - // rename into place atomically. `NamedTempFile`'s `Drop` removes the - // temp if we bail before `persist_noclobber`, avoiding orphaned files - // on errors. + // rename into place atomically. The temp must live in `dir` (not the + // system temp) so the final rename stays within one filesystem — cross- + // filesystem rename isn't atomic. `NamedTempFile`'s `Drop` removes the + // temp on any early return, so we never leak partial files on error. #[cfg(unix)] let mut tmp = { use std::os::unix::fs::PermissionsExt; + // `Builder::permissions` sets the mode at open(2) time, so there's + // no window where the temp exists with the wrong bits. tempfile::Builder::new() .permissions(fs::Permissions::from_mode(want_mode)) .tempfile_in(dir)? @@ -137,6 +140,10 @@ impl Artifact { let mut tmp = tempfile::NamedTempFile::new_in(dir)?; tmp.as_file_mut().write_all(self.content)?; + // `persist_noclobber` (link+unlink on Unix, MoveFileExW without + // REPLACE_EXISTING on Windows) fails atomically if the destination + // already exists — so two racing processes can't clobber each other + // mid-write, and the loser sees the error below. if let Err(err) = tmp.persist_noclobber(&path) { // If another process won the race and the destination now exists, // treat that as success; `err.file` drops here, cleaning up our From 51245da90f82a78a0f99716c8940bfa58298b6ae Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 08:16:13 +0000 Subject: [PATCH 15/19] refactor(materialized_artifact): fluent builder for materialize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/fspy/src/unix/macos_artifacts.rs | 2 +- crates/fspy/src/unix/mod.rs | 6 +- crates/fspy/src/windows/mod.rs | 2 +- crates/materialized_artifact/src/lib.rs | 88 +++++++++++++------ crates/materialized_artifact_build/src/lib.rs | 2 +- 5 files changed, 69 insertions(+), 31 deletions(-) diff --git a/crates/fspy/src/unix/macos_artifacts.rs b/crates/fspy/src/unix/macos_artifacts.rs index 39a6c8103..17b014bd7 100644 --- a/crates/fspy/src/unix/macos_artifacts.rs +++ b/crates/fspy/src/unix/macos_artifacts.rs @@ -14,7 +14,7 @@ mod tests { #[test] fn coreutils_functions() { let tmpdir = tempfile::tempdir().unwrap(); - let coreutils_path = COREUTILS_BINARY.materialize_in(&tmpdir, "", true).unwrap(); + let coreutils_path = COREUTILS_BINARY.materialize().executable().at(&tmpdir).unwrap(); let output = Command::new(coreutils_path).arg("--list").output().unwrap(); let mut expected_functions: Vec<&str> = output .stdout diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index d0625ac63..f01f63b5d 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -49,7 +49,7 @@ impl SpyImpl { const PRELOAD_CDYLIB: Artifact = artifact!("fspy_preload"); - let preload_cdylib_path = PRELOAD_CDYLIB.materialize_in(dir, ".dylib", false)?; + let preload_cdylib_path = PRELOAD_CDYLIB.materialize().suffix(".dylib").at(dir)?; preload_cdylib_path.as_path().into() }; @@ -59,8 +59,8 @@ impl SpyImpl { #[cfg(target_os = "macos")] artifacts: { let coreutils_path = - macos_artifacts::COREUTILS_BINARY.materialize_in(dir, "", true)?; - let bash_path = macos_artifacts::OILS_BINARY.materialize_in(dir, "", true)?; + macos_artifacts::COREUTILS_BINARY.materialize().executable().at(dir)?; + let bash_path = macos_artifacts::OILS_BINARY.materialize().executable().at(dir)?; Artifacts { bash_path: bash_path.as_path().into(), coreutils_path: coreutils_path.as_path().into(), diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 1d5d6cd02..8081e1298 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -51,7 +51,7 @@ pub struct SpyImpl { impl SpyImpl { pub fn init_in(path: &Path) -> io::Result { - let dll_path = INTERPOSE_CDYLIB.materialize_in(path, ".dll", false)?; + let dll_path = INTERPOSE_CDYLIB.materialize().suffix(".dll").at(path)?; let wide_dll_path = dll_path.as_os_str().encode_wide().collect::>(); let mut ansi_dll_path = diff --git a/crates/materialized_artifact/src/lib.rs b/crates/materialized_artifact/src/lib.rs index bc8cd8550..26cabd858 100644 --- a/crates/materialized_artifact/src/lib.rs +++ b/crates/materialized_artifact/src/lib.rs @@ -4,17 +4,17 @@ //! path, and helper binaries have to exist as actual files to be spawned — //! but we want to ship a single executable. `materialized_artifact` embeds //! the file content as a `&'static [u8]` at compile time via the -//! [`artifact!`] macro (same as `include_bytes!`), and -//! [`Artifact::materialize_in`] writes it out to disk when first needed — that -//! materialization step is the value-add over a bare `include_bytes!`. +//! [`artifact!`] macro (same as `include_bytes!`), and [`Materialize::at`] +//! writes it out to disk when first needed — that materialization step is +//! the value-add over a bare `include_bytes!`. //! //! Materialized files are named `{name}_{hash}{suffix}` in the caller-chosen //! directory. The hash (computed at build time by //! `materialized_artifact_build::register`) gives three properties without //! any coordination between processes: //! -//! - **No repeated writes.** [`Artifact::materialize_in`] returns the existing -//! path if the file is already there; repeated calls and re-runs skip I/O. +//! - **No repeated writes.** [`Materialize::at`] returns the existing path if +//! the file is already there; repeated calls and re-runs skip I/O. //! - **Correctness.** Two binaries with different embedded content produce //! different filenames, so a stale file from an older build is never //! mistaken for the current one. @@ -28,11 +28,14 @@ use std::{ path::{Path, PathBuf}, }; -/// A file embedded into the executable at compile time. Construct with -/// [`artifact!`]; materialize to disk with [`Artifact::materialize_in`]. See the -/// [crate docs] for the design rationale. +/// A file embedded into the executable at compile time. +/// +/// Construct with [`artifact!`]; materialize to disk via +/// [`Artifact::materialize`] + [`Materialize::at`]. See the [crate docs] for +/// the design rationale. /// /// [crate docs]: crate +#[derive(Clone, Copy)] pub struct Artifact { name: &'static str, content: &'static [u8], @@ -64,14 +67,55 @@ impl Artifact { Self { name, content, hash } } - /// Ensure the artifact is materialized in `dir` under a content-addressed - /// filename, writing it if missing. `executable` picks the Unix mode - /// (`0o755` vs `0o644`) for newly created files, and reconciles an - /// existing file's mode if it drifted. On non-Unix targets `executable` - /// has no effect. + /// Start a fluent materialize chain. Supply optional [`Materialize::suffix`] + /// / [`Materialize::executable`] knobs, then terminate with + /// [`Materialize::at`]. + pub const fn materialize(&self) -> Materialize<'static> { + Materialize { + artifact: *self, + suffix: "", + #[cfg(unix)] + executable: false, + } + } +} + +/// Builder returned by [`Artifact::materialize`]. Terminate with +/// [`Materialize::at`] to write the file. +#[derive(Clone, Copy)] +#[must_use = "materialize() only configures — call .at(dir) to write the file"] +pub struct Materialize<'a> { + artifact: Artifact, + suffix: &'a str, + #[cfg(unix)] + executable: bool, +} + +impl<'a> Materialize<'a> { + /// Filename suffix appended after `{name}_{hash}` (e.g. `.dll`, `.dylib`). + /// Defaults to empty. + pub const fn suffix(mut self, suffix: &'a str) -> Self { + self.suffix = suffix; + self + } + + /// Mark the materialized file as executable (`0o755` on Unix; no-op on + /// Windows where the filesystem has no executable bit). + pub const fn executable(mut self) -> Self { + #[cfg(unix)] + { + self.executable = true; + } + self + } + + /// Materialize the artifact in `dir` under a content-addressed filename, + /// writing it if missing. On Unix, newly created files get `0o755` when + /// [`Materialize::executable`] was called and `0o644` otherwise, and an + /// existing file's mode is reconciled if it drifted. /// /// Returns the final path. If the target already exists and its mode - /// already matches `executable`, no I/O beyond the stat is performed. + /// already matches, no I/O beyond the stat is performed. /// /// # Preconditions /// @@ -82,19 +126,13 @@ impl Artifact { /// Returns an error if the directory can't be read/written, the stat /// fails for any reason other than not-found, or the temp-file rename /// fails and the destination still doesn't exist. - pub fn materialize_in( - &self, - dir: impl AsRef, - suffix: &str, - executable: bool, - ) -> io::Result { + pub fn at(self, dir: impl AsRef) -> io::Result { let dir = dir.as_ref(); - let path = dir.join(format!("{}_{}{}", self.name, self.hash, suffix)); + let path = + dir.join(format!("{}_{}{}", self.artifact.name, self.artifact.hash, self.suffix)); #[cfg(unix)] - let want_mode: u32 = if executable { 0o755 } else { 0o644 }; - #[cfg(not(unix))] - let _ = executable; // Unix-mode concept; no-op on Windows. + let want_mode: u32 = if self.executable { 0o755 } else { 0o644 }; // Fast path: one stat tells us both whether the file exists and, // on Unix, what its permission bits are. The content is assumed @@ -138,7 +176,7 @@ impl Artifact { }; #[cfg(not(unix))] let mut tmp = tempfile::NamedTempFile::new_in(dir)?; - tmp.as_file_mut().write_all(self.content)?; + tmp.as_file_mut().write_all(self.artifact.content)?; // `persist_noclobber` (link+unlink on Unix, MoveFileExW without // REPLACE_EXISTING on Windows) fails atomically if the destination diff --git a/crates/materialized_artifact_build/src/lib.rs b/crates/materialized_artifact_build/src/lib.rs index 0f76ff4a9..cc6c4fc77 100644 --- a/crates/materialized_artifact_build/src/lib.rs +++ b/crates/materialized_artifact_build/src/lib.rs @@ -15,7 +15,7 @@ pub const ENV_PREFIX: &str = "MATERIALIZED_ARTIFACT_"; /// these at compile time via `include_bytes!(env!(…))` and `env!(…)`. /// /// `name` is used both as the env-var key and as the on-disk filename prefix -/// (in `Artifact::materialize_in`), so it must be a valid identifier-like string +/// (in `Materialize::at`), so it must be a valid identifier-like string /// that matches the one passed to `artifact!`. /// /// # Panics From 2fb53c058b1583d8e20f0f6c7e34dedf5ebc38bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 08:23:55 +0000 Subject: [PATCH 16/19] fix(materialized_artifact): build on Windows & allow reparameterized suffix lifetime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `.executable()` was `fn(mut self)` with the write to `self.executable` inside `#[cfg(unix)]` — on Windows the `mut` was unused and tripped `-D unused_mut`. Added a `#[cfg_attr(not(unix), expect(unused_mut, ...))]`. - `.suffix(&str)` now re-parameterizes the builder's lifetime to the new suffix's so a `Materialize<'static>` can still accept a short-lived suffix. Lifetimes are elided to keep the signature tidy. --- crates/materialized_artifact/src/lib.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/materialized_artifact/src/lib.rs b/crates/materialized_artifact/src/lib.rs index 26cabd858..e6822893c 100644 --- a/crates/materialized_artifact/src/lib.rs +++ b/crates/materialized_artifact/src/lib.rs @@ -91,16 +91,21 @@ pub struct Materialize<'a> { executable: bool, } -impl<'a> Materialize<'a> { +impl Materialize<'_> { /// Filename suffix appended after `{name}_{hash}` (e.g. `.dll`, `.dylib`). /// Defaults to empty. - pub const fn suffix(mut self, suffix: &'a str) -> Self { - self.suffix = suffix; - self + pub const fn suffix(self, suffix: &str) -> Materialize<'_> { + Materialize { + artifact: self.artifact, + suffix, + #[cfg(unix)] + executable: self.executable, + } } /// Mark the materialized file as executable (`0o755` on Unix; no-op on /// Windows where the filesystem has no executable bit). + #[cfg_attr(not(unix), expect(unused_mut, reason = "executable is Unix-only"))] pub const fn executable(mut self) -> Self { #[cfg(unix)] { From 5145e2068ae7bf926b8b38d5225612dbb7c7cbcf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 09:44:06 +0000 Subject: [PATCH 17/19] docs(materialized_artifact_build): drop ENV_PREFIX doc comment --- crates/materialized_artifact_build/src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/materialized_artifact_build/src/lib.rs b/crates/materialized_artifact_build/src/lib.rs index cc6c4fc77..0f7a60f4b 100644 --- a/crates/materialized_artifact_build/src/lib.rs +++ b/crates/materialized_artifact_build/src/lib.rs @@ -1,8 +1,5 @@ use std::{fs, path::Path}; -/// Namespace prefix for the env vars set by [`register`] and consumed by -/// `materialized_artifact`'s `artifact!` macro. Exported so both crates agree -/// on the same prefix. pub const ENV_PREFIX: &str = "MATERIALIZED_ARTIFACT_"; /// Publish an artifact at `path` so `materialized_artifact`'s `artifact!($name)` From 837bee14a17840a7ebf9bef19829e8e4d8c4526c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 09:47:53 +0000 Subject: [PATCH 18/19] docs(materialized_artifact): drop artifact! macro doc; restore ENV_PREFIX doc --- crates/materialized_artifact/src/lib.rs | 3 --- crates/materialized_artifact_build/src/lib.rs | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/materialized_artifact/src/lib.rs b/crates/materialized_artifact/src/lib.rs index e6822893c..88b89072e 100644 --- a/crates/materialized_artifact/src/lib.rs +++ b/crates/materialized_artifact/src/lib.rs @@ -42,9 +42,6 @@ pub struct Artifact { hash: &'static str, } -/// Construct an [`Artifact`] from the env vars published by a build script -/// via `materialized_artifact_build::register`. Must match the `ENV_PREFIX` -/// constant in `materialized_artifact_build`. #[macro_export] macro_rules! artifact { ($name:literal) => { diff --git a/crates/materialized_artifact_build/src/lib.rs b/crates/materialized_artifact_build/src/lib.rs index 0f7a60f4b..cc6c4fc77 100644 --- a/crates/materialized_artifact_build/src/lib.rs +++ b/crates/materialized_artifact_build/src/lib.rs @@ -1,5 +1,8 @@ use std::{fs, path::Path}; +/// Namespace prefix for the env vars set by [`register`] and consumed by +/// `materialized_artifact`'s `artifact!` macro. Exported so both crates agree +/// on the same prefix. pub const ENV_PREFIX: &str = "MATERIALIZED_ARTIFACT_"; /// Publish an artifact at `path` so `materialized_artifact`'s `artifact!($name)` From 3ba69967f71b3a63289ef3bd2df6dc12420e8bc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 09:48:12 +0000 Subject: [PATCH 19/19] docs(materialized_artifact): keep artifact! macro summary, drop ENV_PREFIX note --- crates/materialized_artifact/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/materialized_artifact/src/lib.rs b/crates/materialized_artifact/src/lib.rs index 88b89072e..7380129ed 100644 --- a/crates/materialized_artifact/src/lib.rs +++ b/crates/materialized_artifact/src/lib.rs @@ -42,6 +42,8 @@ pub struct Artifact { hash: &'static str, } +/// Construct an [`Artifact`] from the env vars published by a build script +/// via `materialized_artifact_build::register`. #[macro_export] macro_rules! artifact { ($name:literal) => {