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 9609dcde..ec3021b4 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 efeba969..89416984 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 00000000..c7929b36 --- /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 00000000..4e116fff --- /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 00000000..b232849c --- /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 0c3fcba0..f0cb1ec4 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 a9a1ffff..81b6c24e 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 7acabe79..13ff2055 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 70ee101e..54e53f21 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 ba051630..fffddb6d 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 93bef864..df99cbc6 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 ec3021b4..63da70db 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 4e116fff..42c805a0 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 f0cb1ec4..07b0d2d4 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 81b6c24e..b3b48564 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 fffddb6d..6bb216e9 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 df99cbc6..722efa93 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 63da70db..efc45c74 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 89416984..0e29627d 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 07b0d2d4..856454c0 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 00000000..c7929b36 --- /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 00000000..03e9bcbc --- /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 00000000..2db3d048 --- /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 00000000..66097acf --- /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 b3b48564..ea9d392d 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 84987b42..5caf06dd 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 efc45c74..06d5c808 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 66097acf..3272b7e1 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 ea9d392d..8b3bb559 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 5caf06dd..281bd7d2 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 281bd7d2..bc17c4e0 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 06d5c808..6498f188 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 42c805a0..4e116fff 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 856454c0..56ef13b0 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 2db3d048..99a14045 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 3272b7e1..9f565c11 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 8b3bb559..b9d31207 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 bc17c4e0..449ede01 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 54e53f21..d39d2348 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 6bb216e9..4ae22fe2 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 722efa93..4c6b9133 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 9728cd98..42bf9e9c 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 6498f188..9235abed 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 0e29627d..d61e4889 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 4e116fff..f2583c88 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 00000000..aa77c89b --- /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 56ef13b0..884aece3 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 03e9bcbc..d3a2287e 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 00000000..5894c95b --- /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 9f565c11..6a992767 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 b232849c..00000000 --- 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 99a14045..00000000 --- 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 b9d31207..fca3946c 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 449ede01..3f77d1ab 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 d39d2348..fa2f5878 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 4ae22fe2..767ed278 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 4c6b9133..7e0864dd 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 9235abed..04d876ae 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 f2583c88..e4b251b9 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 aa77c89b..f84a63f8 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 884aece3..9884b952 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 fa2f5878..4ca0755d 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 767ed278..b5dee80d 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 7e0864dd..1906d94d 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 d61e4889..b739d454 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 6a992767..bd19aa08 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 fca3946c..7b1e4ebe 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 3f77d1ab..733f6ce4 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 1906d94d..5311aa95 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 04d876ae..8499bc07 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 b739d454..80d414e5 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 e4b251b9..cac239d6 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 d3a2287e..a94367fa 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 7b1e4ebe..aef025a7 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 733f6ce4..8f1fcc2d 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 8499bc07..bf11ea1b 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 80d414e5..85c0f40f 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 f84a63f8..00000000 --- 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 5894c95b..00000000 --- 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 aef025a7..e8443e55 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 8f1fcc2d..b6659f15 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 4ca0755d..287b5c64 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 b5dee80d..4c838e26 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 5311aa95..9f6ab039 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 cac239d6..643c40a1 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 00000000..60b505d3 --- /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 9884b952..128eb7d8 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 a94367fa..c2d5dbd3 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 00000000..7f727fad --- /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 bd19aa08..70ab48a7 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 287b5c64..39a6c810 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 4c838e26..ba7d5977 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 9f6ab039..1d5d6cd0 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 128eb7d8..67c06918 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 70ab48a7..0f76ff4a 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 ba7d5977..d0625ac6 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 67c06918..bc8cd855 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 39a6c810..17b014bd 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 d0625ac6..f01f63b5 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 1d5d6cd0..8081e129 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 bc8cd855..26cabd85 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 0f76ff4a..cc6c4fc7 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 26cabd85..e6822893 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 cc6c4fc7..0f7a60f4 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 e6822893..88b89072 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 0f7a60f4..cc6c4fc7 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 88b89072..7380129e 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) => {