From 34a4934bbb9897c9aed066a19e1afa944569b87c Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 10 Aug 2026 23:36:38 +0800 Subject: [PATCH] perf: declare artifact cdylibs as normal dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the fspy preload libraries and vt_client_napi from `[build-dependencies]` to (target-scoped) `[dependencies]`. Cargo compiles build-dependency artifact deps with the build-override profile — opt-level 0 in release builds, and only a root-manifest override can fix it, which every downstream workspace had to copy (see rust-lang/cargo#16719). Under `[dependencies]` the normal profile applies everywhere with no overrides, and the cdylibs additionally gain `panic = "abort"` from the release profile. `CARGO_CDYLIB_FILE_*` is now provided while the consuming crate compiles instead of to its build script, so hashing moves from `materialized_artifact_build::register` into a new `materialized_artifact_macros::artifact!` proc macro that reads and hashes the file at expansion time. The macro declares its reads via `proc_macro::tracked` so a future expansion cache cannot serve a stale hash next to fresh bytes, and its expansion re-references the file and env var through `include_bytes!(env!(…))` for dep-info tracking. When the env var is unset the expansion branches on `cfg(rust_analyzer)`: a well-typed stub under rust-analyzer, an actionable `compile_error!` under rustc. The macOS downloaded binaries switch to the same macro (their build script now publishes only a path via `cargo:rustc-env`), which leaves `materialized_artifact_build` without consumers; the crate is removed. Scoping the preload deps to the targets that embed them also stops musl and Windows/Unix cross-builds from producing empty cdylibs — resolver panics on cfg-scoped artifact deps only affect `[build-dependencies]` placement. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 8 +- Cargo.toml | 19 ++- crates/fspy/Cargo.toml | 23 +-- crates/fspy/build.rs | 33 ++--- crates/fspy/src/unix/macos_artifacts.rs | 4 +- crates/fspy/src/unix/mod.rs | 3 +- crates/fspy/src/windows/mod.rs | 3 +- crates/materialized_artifact/Cargo.toml | 1 + crates/materialized_artifact/src/lib.rs | 24 +--- crates/materialized_artifact_build/README.md | 4 - crates/materialized_artifact_build/src/lib.rs | 37 ----- .../.clippy.toml | 0 .../Cargo.toml | 14 +- .../materialized_artifact_macros/src/lib.rs | 133 ++++++++++++++++++ crates/vt/Cargo.toml | 14 +- crates/vt/build.rs | 18 +-- crates/vt/src/napi_client.rs | 2 +- 17 files changed, 212 insertions(+), 128 deletions(-) delete mode 100644 crates/materialized_artifact_build/README.md delete mode 100644 crates/materialized_artifact_build/src/lib.rs rename crates/{materialized_artifact_build => materialized_artifact_macros}/.clippy.toml (100%) rename crates/{materialized_artifact_build => materialized_artifact_macros}/Cargo.toml (63%) create mode 100644 crates/materialized_artifact_macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4e616ff5e..f448972df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1238,7 +1238,6 @@ dependencies = [ "futures-util", "libc", "materialized_artifact", - "materialized_artifact_build", "nix 0.31.2", "ntest", "ouroboros", @@ -2006,13 +2005,17 @@ dependencies = [ name = "materialized_artifact" version = "0.0.0" dependencies = [ + "materialized_artifact_macros", "tempfile", ] [[package]] -name = "materialized_artifact_build" +name = "materialized_artifact_macros" version = "0.0.0" dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", "xxhash-rust", ] @@ -4197,7 +4200,6 @@ dependencies = [ "fspy", "futures-util", "materialized_artifact", - "materialized_artifact_build", "nix 0.31.2", "once_cell", "owo-colors", diff --git a/Cargo.toml b/Cargo.toml index 9152581fe..c915cc7cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,15 +71,15 @@ diff-struct = "0.5.3" directories = "6.0.0" elf = { version = "0.8.0", default-features = false } materialized_artifact = { path = "crates/materialized_artifact" } -materialized_artifact_build = { path = "crates/materialized_artifact_build" } +materialized_artifact_macros = { path = "crates/materialized_artifact_macros" } flate2 = "1.0.35" fspy = { path = "crates/fspy" } fspy_benchmark_launcher = { path = "crates/fspy_benchmark_launcher", artifact = "bin" } fspy_benchmark_target = { path = "crates/fspy_benchmark_target", artifact = "bin" } fspy_client_unix = { path = "crates/fspy_client_unix" } fspy_detours_sys = { path = "crates/fspy_detours_sys" } -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_preload_unix = { path = "crates/fspy_preload_unix", artifact = "cdylib" } +fspy_preload_windows = { path = "crates/fspy_preload_windows", artifact = "cdylib" } fspy_seccomp_unotify = { path = "crates/fspy_seccomp_unotify" } fspy_shm = { path = "crates/fspy_shm" } fspy_shared = { path = "crates/fspy_shared" } @@ -113,9 +113,11 @@ petgraph = "0.8.2" phf = { version = "0.13.0", features = ["macros"] } portable-pty = "0.9.0" pretty_assertions = "1.4.1" +proc-macro2 = "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" } +quote = "1" ratatui = "0.30.0" rayon = "1.10.0" ref-cast = "1.0.24" @@ -139,6 +141,7 @@ socket_ipc = { path = "crates/socket_ipc" } stackalloc = "1.2.1" subprocess_test = { path = "crates/subprocess_test" } supports-color = "3.0.1" +syn = "2" syscalls = { version = "0.8.0", default-features = false } tar = "0.4.45" tempfile = "3.14.0" @@ -165,7 +168,7 @@ vt_str = { path = "crates/vt_str" } vt = { path = "crates/vt" } vt_bin = { path = "crates/vt_bin" } vt_client = { path = "crates/vt_client" } -vt_client_napi = { path = "crates/vt_client_napi", artifact = "cdylib", target = "target" } +vt_client_napi = { path = "crates/vt_client_napi", artifact = "cdylib" } vt_graph = { path = "crates/vt_graph" } vt_ipc_shared = { path = "crates/vt_ipc_shared" } vt_plan = { path = "crates/vt_plan" } @@ -208,3 +211,11 @@ codegen-units = 1 strip = "symbols" # set to `false` for debug information debug = false # set to `true` for debug information panic = "abort" # Let it crash and force ourselves to write safe Rust. + +# The workspace's artifact dependencies (the preload libraries and +# `vt_client_napi`) are declared under `[dependencies]`, so they get the +# normal profile — including this release profile — with no per-package +# overrides. Keeping them out of `[build-dependencies]` matters: there Cargo +# would compile them with the build-override profile (opt-level 0) even in +# release builds, in this workspace and in every downstream workspace. See +# https://github.com/rust-lang/cargo/issues/16719. diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index 9413a9025..59ce55cc9 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -31,8 +31,23 @@ tokio = { workspace = true, features = ["bytes"] } fspy_shared_unix = { workspace = true } nix = { workspace = true, features = ["fs", "process", "socket", "feature"] } +# The preload artifact deps live under `[dependencies]` — not +# `[build-dependencies]`, where Cargo would compile them with the +# build-override profile (opt-level 0 in release; see +# https://github.com/rust-lang/cargo/issues/16719) — so they get the normal +# profile, in this workspace and downstream ones, with no per-package profile +# overrides. Cargo provides `CARGO_CDYLIB_FILE_*` while compiling this crate +# for `artifact!` to embed. Each dep is scoped to the targets whose code +# embeds it, so other targets (e.g. musl, which never dynamically links a +# preload) don't build a useless empty cdylib. Scoping artifact deps under +# `[target.cfg…]` is only safe for normal deps: the same shape under +# `[target.cfg….build-dependencies]` panics cargo's resolver on cross-compile. +[target.'cfg(all(unix, not(target_env = "musl")))'.dependencies] +fspy_preload_unix = { workspace = true } + [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 } @@ -54,17 +69,9 @@ 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 } -materialized_artifact_build = { workspace = true } flate2 = { workspace = true } -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 90b7bbcf4..031251828 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -53,9 +53,11 @@ fn sha256_hex(bytes: &[u8]) -> String { } 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. + /// On-disk filename in `OUT_DIR`. name: &'static str, + /// Env var published via `cargo:rustc-env`, through which + /// `materialized_artifact::artifact!` finds and embeds the binary. + env_var: &'static str, /// GitHub release asset URL. url: &'static str, /// Path of the binary within the tarball. @@ -73,6 +75,7 @@ const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ // https://github.com/wan9chi/oils-for-unix-build/releases/tag/oils-for-unix-0.37.0 BinaryDownload { name: "oils_for_unix", + env_var: "FSPY_MACOS_ARTIFACT_OILS_FOR_UNIX", url: "https://github.com/wan9chi/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: "ce4bb80b15f0a0371af08b19b65bfa5ea17d30429ebb911f487de3d2bcc7a07d", @@ -80,6 +83,7 @@ const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ // https://github.com/uutils/coreutils/releases/tag/0.4.0 BinaryDownload { name: "coreutils", + env_var: "FSPY_MACOS_ARTIFACT_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: "8e8f38d9323135a19a73d617336fce85380f3c46fcb83d3ae3e031d1c0372f21", @@ -92,6 +96,7 @@ const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ // https://github.com/wan9chi/oils-for-unix-build/releases/tag/oils-for-unix-0.37.0 BinaryDownload { name: "oils_for_unix", + env_var: "FSPY_MACOS_ARTIFACT_OILS_FOR_UNIX", url: "https://github.com/wan9chi/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: "cf1a95993127770e2a5fff277cd256a2bb28cf97d7f83ae42fdccc172cdb540d", @@ -99,6 +104,7 @@ const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ // https://github.com/uutils/coreutils/releases/tag/0.4.0 BinaryDownload { name: "coreutils", + env_var: "FSPY_MACOS_ARTIFACT_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: "6be8bee6e8b91fc44a465203b9cc30538af00084b6657dc136d9e55837753eb1", @@ -119,8 +125,12 @@ fn fetch_macos_binaries(out_dir: &Path) -> anyhow::Result<()> { .context(format!("Unsupported macOS arch: {target_arch}"))? .1; - for BinaryDownload { name, url, path_in_targz, expected_sha256 } in downloads { + for BinaryDownload { name, env_var, url, path_in_targz, expected_sha256 } in downloads { let dest = out_dir.join(name); + let dest_str = dest.to_str().expect("OUT_DIR path must be valid UTF-8"); + // Emit rerun-if-changed before fetching so cargo still sees it even + // if the download or write below fails. + println!("cargo:rerun-if-changed={dest_str}"); // Cache hit: an already-extracted binary whose contents hash to // `expected_sha256` is known-good and reused without redownloading. let cached = matches!( @@ -138,29 +148,14 @@ fn fetch_macos_binaries(out_dir: &Path) -> anyhow::Result<()> { ); fs::write(&dest, &data).with_context(|| format!("writing {}", dest.display()))?; } - materialized_artifact_build::register(name, &dest); + println!("cargo:rustc-env={env_var}={dest_str}"); } 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"))?; - materialized_artifact_build::register("fspy_preload", Path::new(&dylib_path)); - Ok(()) -} - fn main() -> anyhow::Result<()> { println!("cargo:rerun-if-changed=build.rs"); 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 17b014bd7..a8a388837 100644 --- a/crates/fspy/src/unix/macos_artifacts.rs +++ b/crates/fspy/src/unix/macos_artifacts.rs @@ -1,7 +1,7 @@ use materialized_artifact::{Artifact, artifact}; -pub const COREUTILS_BINARY: Artifact = artifact!("coreutils"); -pub const OILS_BINARY: Artifact = artifact!("oils_for_unix"); +pub const COREUTILS_BINARY: Artifact = artifact!("coreutils", "FSPY_MACOS_ARTIFACT_COREUTILS"); +pub const OILS_BINARY: Artifact = artifact!("oils_for_unix", "FSPY_MACOS_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 f01f63b5d..7c8008dc3 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -47,7 +47,8 @@ impl SpyImpl { let preload_path = { use materialized_artifact::{Artifact, artifact}; - const PRELOAD_CDYLIB: Artifact = artifact!("fspy_preload"); + const PRELOAD_CDYLIB: Artifact = + artifact!("fspy_preload", "CARGO_CDYLIB_FILE_FSPY_PRELOAD_UNIX"); let preload_cdylib_path = PRELOAD_CDYLIB.materialize().suffix(".dylib").at(dir)?; preload_cdylib_path.as_path().into() diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 71a44739d..c468888a6 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -27,7 +27,8 @@ use crate::{ ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}, }; -const INTERPOSE_CDYLIB: Artifact = artifact!("fspy_preload"); +const INTERPOSE_CDYLIB: Artifact = + artifact!("fspy_preload", "CARGO_CDYLIB_FILE_FSPY_PRELOAD_WINDOWS"); pub struct PathAccessIterable { ipc_receiver_lock_guard: OwnedReceiverLockGuard, diff --git a/crates/materialized_artifact/Cargo.toml b/crates/materialized_artifact/Cargo.toml index 643c40a15..919370f18 100644 --- a/crates/materialized_artifact/Cargo.toml +++ b/crates/materialized_artifact/Cargo.toml @@ -7,6 +7,7 @@ publish = false rust-version.workspace = true [dependencies] +materialized_artifact_macros = { workspace = true } tempfile = { workspace = true } [lints] diff --git a/crates/materialized_artifact/src/lib.rs b/crates/materialized_artifact/src/lib.rs index 7380129ed..3bf00a075 100644 --- a/crates/materialized_artifact/src/lib.rs +++ b/crates/materialized_artifact/src/lib.rs @@ -9,9 +9,8 @@ //! 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: +//! directory. The hash (computed at macro-expansion time by [`artifact!`]) +//! gives three properties without any coordination between processes: //! //! - **No repeated writes.** [`Materialize::at`] returns the existing path if //! the file is already there; repeated calls and re-runs skip I/O. @@ -42,22 +41,9 @@ 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) => { - $crate::Artifact::__new( - $name, - ::core::include_bytes!(::core::env!(::core::concat!( - "MATERIALIZED_ARTIFACT_", - $name, - "_PATH" - ))), - ::core::env!(::core::concat!("MATERIALIZED_ARTIFACT_", $name, "_HASH")), - ) - }; -} +/// Construct an [`Artifact`] from an env var holding a file path at compile +/// time — see the macro's own docs for usage and design notes. +pub use materialized_artifact_macros::artifact; impl Artifact { #[doc(hidden)] diff --git a/crates/materialized_artifact_build/README.md b/crates/materialized_artifact_build/README.md deleted file mode 100644 index 7f727fada..000000000 --- a/crates/materialized_artifact_build/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# materialized_artifact_build - -Build-script helper for publishing artifacts consumed by -`materialized_artifact`'s `artifact!` macro. diff --git a/crates/materialized_artifact_build/src/lib.rs b/crates/materialized_artifact_build/src/lib.rs deleted file mode 100644 index cc6c4fc77..000000000 --- a/crates/materialized_artifact_build/src/lib.rs +++ /dev/null @@ -1,37 +0,0 @@ -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)` -/// macro can embed it. -/// -/// Emits three `cargo:…` directives: -/// `rerun-if-changed={path}`, -/// `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 `Materialize::at`), so it must be a valid identifier-like string -/// that matches the one passed to `artifact!`. -/// -/// # 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/materialized_artifact_build/.clippy.toml b/crates/materialized_artifact_macros/.clippy.toml similarity index 100% rename from crates/materialized_artifact_build/.clippy.toml rename to crates/materialized_artifact_macros/.clippy.toml diff --git a/crates/materialized_artifact_build/Cargo.toml b/crates/materialized_artifact_macros/Cargo.toml similarity index 63% rename from crates/materialized_artifact_build/Cargo.toml rename to crates/materialized_artifact_macros/Cargo.toml index c2d5dbd3a..c51d53948 100644 --- a/crates/materialized_artifact_build/Cargo.toml +++ b/crates/materialized_artifact_macros/Cargo.toml @@ -1,17 +1,21 @@ [package] -name = "materialized_artifact_build" +name = "materialized_artifact_macros" version = "0.0.0" edition.workspace = true license.workspace = true publish = false rust-version.workspace = true +[lib] +proc-macro = true +doctest = false +test = false + [dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } xxhash-rust = { workspace = true, features = ["xxh3"] } [lints] workspace = true - -[lib] -doctest = false -test = false diff --git a/crates/materialized_artifact_macros/src/lib.rs b/crates/materialized_artifact_macros/src/lib.rs new file mode 100644 index 000000000..be4ca3ebe --- /dev/null +++ b/crates/materialized_artifact_macros/src/lib.rs @@ -0,0 +1,133 @@ +//! Proc-macro side of `materialized_artifact`: embed a file and its content +//! hash at compile time, given an env var holding the file's path. +//! +//! The env var is either a Cargo artifact dependency's `CARGO__FILE_` +//! (provided only while the consuming crate is compiled — no build script ever +//! sees it) or a path the consuming crate's own build script published via +//! `cargo:rustc-env`. Either way the content hash must be computed here, at +//! macro-expansion time. +//! +//! # Why `proc_macro::tracked` +//! +//! The hash and the embedded bytes are two reads of the same file through +//! different mechanisms: this macro reads it to hash, and the emitted +//! `include_bytes!` reads it to embed. Today both stay in sync because +//! expansion reruns on every compilation, and the emitted +//! `include_bytes!(env!(…))` registers the file and env var in dep-info. But +//! a future compiler that caches proc-macro expansions keyed on input tokens +//! would serve a stale hash next to fresh bytes — a silent mismatch that +//! ships. The [`tracked`] calls declare this macro's reads to the compiler +//! so any such cache invalidates correctly. They are load-bearing, not +//! defensive: do not remove them. +//! +//! [`tracked`]: proc_macro::tracked + +#![feature(proc_macro_tracked_env)] +#![feature(proc_macro_tracked_path)] + +use std::fs; + +use proc_macro::tracked; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + LitStr, Token, + parse::{Parse, ParseStream}, +}; + +/// Construct a `materialized_artifact::Artifact` from an env var holding a +/// file path at compile time. +/// +/// Usage: `artifact!("fspy_preload", "CARGO_CDYLIB_FILE_FSPY_PRELOAD_UNIX")` +/// where the first argument is the artifact name (used in the materialized +/// filename) and the second is the env var holding the file's path: a Cargo +/// artifact dependency's `CARGO__FILE_`, or a var the consuming +/// crate's build script published via `cargo:rustc-env`. +/// +/// Expands to `Artifact::__new(name, include_bytes!(env!(env_var)), "")` +/// with the xxh3-128 hex of the file computed during expansion. The expansion +/// deliberately re-references the env var and file through `env!` + +/// `include_bytes!` — embedding needs them anyway, and they double as +/// dep-info registration alongside the `proc_macro::tracked` calls. +/// +/// When the env var is unset the expansion branches on `cfg(rust_analyzer)`: +/// under rust-analyzer (which never has artifact-dep env vars) it is a +/// well-typed stub with no diagnostics, while under rustc — where an unset +/// var means the artifact dependency is genuinely missing or the build +/// script didn't emit it — a `compile_error!` fails the build with an +/// actionable message. The cfg-gated tokens exist only in this unset-var +/// expansion, never in a successful build, so consumers need no +/// `check-cfg` declaration for `rust_analyzer`. +#[proc_macro] +pub fn artifact(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + // The single place errors become tokens: every failure inside is a + // spanned `syn::Error`. + expand(input).unwrap_or_else(syn::Error::into_compile_error).into() +} + +fn expand(input: proc_macro::TokenStream) -> syn::Result { + let Args { name, env_var } = syn::parse(input)?; + + // `tracked::env_var` both reads the var and declares the read to the + // compiler (see crate docs for why that declaration matters). + let (content, hash, guard) = if let Ok(file_path) = tracked::env_var(env_var.value()) { + // Declare the file read before attempting it: the dependency must + // be registered even if the read fails, so a later fix to the file + // retriggers this macro. + tracked::path(&file_path); + let bytes = fs::read(&file_path).map_err(|err| { + syn::Error::new( + env_var.span(), + format!( + "`{}` points at {file_path}, which could not be read: {err}", + env_var.value() + ), + ) + })?; + let hash = format!("{:x}", xxhash_rust::xxh3::xxh3_128(&bytes)); + (quote!(::core::include_bytes!(::core::env!(#env_var))), hash, TokenStream::new()) + } else { + // Env var unset. Only the compilation context itself can tell + // whether that is fine (rust-analyzer never has artifact-dep env + // vars) or a real error (missing artifact dependency, build script + // not emitting the var), so the guard lets the emitted tokens branch + // on `cfg(rust_analyzer)` instead of guessing here. + let message = format!( + "`{}` is not set at compile time; declare the artifact as a Cargo artifact \ + dependency under `[dependencies]`, or publish the path from a build script via \ + `cargo:rustc-env`", + env_var.value() + ); + let guard = quote! { + #[cfg(not(rust_analyzer))] + ::core::compile_error!(#message); + }; + (quote!(&[]), String::from("rust-analyzer-stub"), guard) + }; + + Ok(quote! { + { + #guard + ::materialized_artifact::Artifact::__new(#name, #content, #hash) + } + }) +} + +/// The two arguments of [`artifact!`]: `"name", "ENV_VAR"`, with an optional +/// trailing comma. +struct Args { + name: LitStr, + env_var: LitStr, +} + +impl Parse for Args { + fn parse(input: ParseStream) -> syn::Result { + let name = input.parse()?; + input.parse::()?; + let env_var = input.parse()?; + if input.peek(Token![,]) { + input.parse::()?; + } + Ok(Self { name, env_var }) + } +} diff --git a/crates/vt/Cargo.toml b/crates/vt/Cargo.toml index 1a59fc291..7800ca95e 100644 --- a/crates/vt/Cargo.toml +++ b/crates/vt/Cargo.toml @@ -49,6 +49,12 @@ vt_glob = { workspace = true } vt_path = { workspace = true } vt_select = { workspace = true } vt_str = { workspace = true } +# Artifact dep under `[dependencies]` — not `[build-dependencies]`, where +# Cargo would compile it with the build-override profile (opt-level 0 in +# release; see https://github.com/rust-lang/cargo/issues/16719). Cargo +# provides `CARGO_CDYLIB_FILE_VT_CLIENT_NAPI` while compiling this crate +# for `artifact!` to embed. +vt_client_napi = { workspace = true } vt_graph = { workspace = true } vt_ipc_shared = { workspace = true } vt_plan = { workspace = true } @@ -57,14 +63,6 @@ vt_workspace = { workspace = true } wax = { workspace = true } zstd = { workspace = true } -# 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. -[build-dependencies] -anyhow = { workspace = true } -materialized_artifact_build = { workspace = true } -vt_client_napi = { workspace = true } - [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/vt/build.rs b/crates/vt/build.rs index 2a1b3657c..2cf96d32c 100644 --- a/crates/vt/build.rs +++ b/crates/vt/build.rs @@ -1,12 +1,4 @@ -#![expect( - clippy::disallowed_types, - clippy::disallowed_macros, - reason = "build.rs interfaces with std::path and cargo's env-var API" -)] - -use std::{env, path::Path}; - -use anyhow::Context; +use std::env; // Why `cfg(fspy)` instead of matching on `target_os` directly at each use site: // "fspy is available" is a single semantic predicate, but the underlying reason @@ -17,7 +9,7 @@ use anyhow::Context; // over OSes. The OS allowlist lives in two spots that must stay in sync: this // file (for the rustc cfg) and the target-scoped dep block in Cargo.toml // (which Cargo resolves before build.rs runs, so it can't reuse this cfg). -fn main() -> anyhow::Result<()> { +fn main() { println!("cargo::rustc-check-cfg=cfg(fspy)"); println!("cargo::rerun-if-changed=build.rs"); @@ -25,10 +17,4 @@ fn main() -> anyhow::Result<()> { if matches!(target_os.as_str(), "windows" | "macos" | "linux") { println!("cargo::rustc-cfg=fspy"); } - - let env_name = "CARGO_CDYLIB_FILE_VT_CLIENT_NAPI"; - println!("cargo:rerun-if-env-changed={env_name}"); - let dylib_path = env::var_os(env_name).with_context(|| format!("{env_name} not set"))?; - materialized_artifact_build::register("vt_client_napi", Path::new(&dylib_path)); - Ok(()) } diff --git a/crates/vt/src/napi_client.rs b/crates/vt/src/napi_client.rs index c2dc631d6..0f300911b 100644 --- a/crates/vt/src/napi_client.rs +++ b/crates/vt/src/napi_client.rs @@ -21,7 +21,7 @@ pub fn napi_client_path() -> &'static AbsolutePath { static PATH: LazyLock = LazyLock::new(|| { let dir = env::temp_dir().join("vt_client_napi"); let _ = fs::create_dir(&dir); - let path = artifact!("vt_client_napi") + let path = artifact!("vt_client_napi", "CARGO_CDYLIB_FILE_VT_CLIENT_NAPI") .materialize() .suffix(".node") .at(&dir)