From cd55770d4f1e3a2fcb169831caa550266d6504b9 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Sun, 8 Nov 2026 00:36:44 +0800 Subject: [PATCH] refactor(artifact): materialize into one vp_run folder in the user's temp dir Every caller of `vt_materialized_artifact` paired `user_temp_dir(name)` with `.at(dir)` and chose its own name, so `vp run` kept `fspy-`, `vt_client_napi-` and `vp-schemas-` side by side in the temp dir. - `Materialize::at_user_temp_dir(prefix)` creates the user's own folder, with the same checks as before, and materializes into it. `user_temp_dir` is private. - fspy, the napi client and the task graph's JSON Schema all use the prefix `vp_run`, so everything `vp run` writes there goes in `{temp}/vp_run-{uid}` on Unix, `{temp}\vp_run` on Windows. fspy's `SpyImpl::init_in(dir)` becomes `init()`. - The RFCs' `$schema` examples use the new folder. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + crates/fspy/src/lib.rs | 7 +-- crates/fspy/src/unix/mod.rs | 14 +++-- crates/fspy/src/windows/mod.rs | 7 ++- crates/vt/src/napi_client.rs | 13 ++-- .../vt/src/session/task_graph_json/schema.rs | 17 ++--- crates/vt_materialized_artifact/src/lib.rs | 62 +++++++++++-------- docs/rfcs/dry-run.md | 2 +- docs/rfcs/task-graph-json.md | 6 +- 9 files changed, 70 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86d9e3289..685934f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Changed** The helper files `vp run` writes to the system temp directory, such as the library that tracks file accesses and the JSON Schema that `vp run --json` points to, now share one folder that only the current user can write to, like `/tmp/vp_run-1000` on Linux, instead of a folder for each kind of file ([#974](https://github.com/voidzero-dev/vite-task/pull/974)). - **Perf** When several tasks become ready at once, `vp run` now checks their cache entries at the same time instead of one after another, so cache hits start sooner. Hashing a task's input files for its cache lookup no longer holds up other tasks ([#982](https://github.com/voidzero-dev/vite-task/pull/982)). - **Fixed** On Windows, the files a task deletes or moves, such as a build cleaning its output folder before writing it again, are no longer among its inputs, as on Linux and macOS. Before, when a later step changed one of those files, for example by stamping a version into the rebuilt output, the next run missed the cache with a reason like `'dist/index.js' modified`, though the task never read the file ([#980](https://github.com/voidzero-dev/vite-task/pull/980)). - **Changed** In `@voidzero-dev/vite-task-client`, a `getEnvs` query that matches every env name, `{ prefix: '' }` or `*`, now returns nothing instead of every environment variable, and no longer adds them to the cache key ([#976](https://github.com/voidzero-dev/vite-task/pull/976)). diff --git a/crates/fspy/src/lib.rs b/crates/fspy/src/lib.rs index a199c7972..6f47bf749 100644 --- a/crates/fspy/src/lib.rs +++ b/crates/fspy/src/lib.rs @@ -57,8 +57,5 @@ pub struct TrackedChild { pub process_handle: std::os::windows::io::OwnedHandle, } -pub(crate) static SPY_IMPL: LazyLock = LazyLock::new(|| { - vt_materialized_artifact::user_temp_dir("fspy") - .and_then(|dir| SpyImpl::init_in(&dir)) - .expect("Failed to initialize global spy") -}); +pub(crate) static SPY_IMPL: LazyLock = + LazyLock::new(|| SpyImpl::init().expect("Failed to initialize global spy")); diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index e5a8f70c5..e0e850037 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -4,9 +4,9 @@ mod syscall_handler; #[cfg(target_os = "macos")] mod macos_artifacts; +use std::io; #[cfg(target_os = "linux")] use std::sync::Arc; -use std::{io, path::Path}; #[cfg(target_os = "linux")] use fspy_seccomp_unotify::supervisor::supervise; @@ -45,11 +45,12 @@ pub struct SpyImpl { } impl SpyImpl { - /// Initialize the fs access spy by writing the preload library on disk. + /// Initialize the fs access spy by writing the preload library on disk, in + /// the current user's own `vp_run` folder in the system temp dir. /// /// On musl targets, we don't build a preload library — /// only seccomp-based tracking is used. - pub fn init_in(#[cfg_attr(target_env = "musl", allow(unused))] dir: &Path) -> io::Result { + pub fn init() -> io::Result { #[cfg(not(target_env = "musl"))] let preload_path = { use vt_materialized_artifact::{Artifact, artifact}; @@ -57,7 +58,8 @@ impl SpyImpl { const PRELOAD_CDYLIB: Artifact = artifact!("fspy_preload", "CARGO_CDYLIB_FILE_FSPY_PRELOAD_UNIX"); - let preload_cdylib_path = PRELOAD_CDYLIB.materialize().suffix(".dylib").at(dir)?; + let preload_cdylib_path = + PRELOAD_CDYLIB.materialize().suffix(".dylib").at_user_temp_dir("vp_run")?; preload_cdylib_path.as_path().into() }; @@ -68,14 +70,14 @@ impl SpyImpl { bash_path: macos_artifacts::OILS_BINARY .materialize() .executable() - .at(dir)? + .at_user_temp_dir("vp_run")? .as_path() .into(), #[cfg(target_os = "macos")] coreutils_path: macos_artifacts::COREUTILS_BINARY .materialize() .executable() - .at(dir)? + .at_user_temp_dir("vp_run")? .as_path() .into(), }) diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 15533b7bb..1f4dd14f0 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -5,7 +5,6 @@ use std::{ ffi::OsStrExt, io::{AsRawHandle, BorrowedHandle}, }, - path::Path, sync::Arc, }; @@ -56,8 +55,10 @@ pub struct SpyImpl { } impl SpyImpl { - pub fn init_in(path: &Path) -> io::Result { - let dll_path = INTERPOSE_CDYLIB.materialize().suffix(".dll").at(path)?; + /// Initialize the fs access spy by writing the interpose DLL on disk, in the + /// current user's own `vp_run` folder in the system temp dir. + pub fn init() -> io::Result { + let dll_path = INTERPOSE_CDYLIB.materialize().suffix(".dll").at_user_temp_dir("vp_run")?; let wide_dll_path = dll_path.as_os_str().encode_wide().collect::>(); let mut ansi_dll_path = diff --git a/crates/vt/src/napi_client.rs b/crates/vt/src/napi_client.rs index 9592e318d..fbd9960d8 100644 --- a/crates/vt/src/napi_client.rs +++ b/crates/vt/src/napi_client.rs @@ -3,7 +3,7 @@ use std::sync::LazyLock; -use vt_materialized_artifact::{artifact, user_temp_dir}; +use vt_materialized_artifact::artifact; use vt_path::{AbsolutePath, AbsolutePathBuf}; /// Path to the materialized `vt_client_napi` `.node` addon. @@ -20,13 +20,10 @@ use vt_path::{AbsolutePath, AbsolutePathBuf}; #[must_use] pub fn napi_client_path() -> &'static AbsolutePath { static PATH: LazyLock = LazyLock::new(|| { - let path = user_temp_dir("vt_client_napi") - .and_then(|dir| { - artifact!("vt_client_napi", "CARGO_CDYLIB_FILE_VT_CLIENT_NAPI") - .materialize() - .suffix(".node") - .at(&dir) - }) + let path = artifact!("vt_client_napi", "CARGO_CDYLIB_FILE_VT_CLIENT_NAPI") + .materialize() + .suffix(".node") + .at_user_temp_dir("vp_run") .expect("materialize vt_client_napi"); AbsolutePathBuf::new(path).expect("system temp dir yields an absolute path") }); diff --git a/crates/vt/src/session/task_graph_json/schema.rs b/crates/vt/src/session/task_graph_json/schema.rs index 0bad6ba53..221747370 100644 --- a/crates/vt/src/session/task_graph_json/schema.rs +++ b/crates/vt/src/session/task_graph_json/schema.rs @@ -2,7 +2,7 @@ use std::io; -use vt_materialized_artifact::{artifact, user_temp_dir}; +use vt_materialized_artifact::artifact; use vt_path::{AbsolutePath, AbsolutePathBuf}; use vt_str::Str; @@ -14,9 +14,10 @@ pub fn schema_url() -> io::Result { } fn materialize() -> io::Result { - let dir = user_temp_dir("vp-schemas")?; - let path = - artifact!("task-graph", "VT_TASK_GRAPH_SCHEMA").materialize().suffix(".json").at(&dir)?; + let path = artifact!("task-graph", "VT_TASK_GRAPH_SCHEMA") + .materialize() + .suffix(".json") + .at_user_temp_dir("vp_run")?; AbsolutePathBuf::new(path) .ok_or_else(|| io::Error::other("the system temp directory isn't an absolute path")) } @@ -101,8 +102,8 @@ mod tests { #[cfg(not(windows))] #[test] fn file_urls() { - let path = AbsolutePath::new("/var/my tmp/vp-schemas-501/task-graph_0123.json").unwrap(); - assert_eq!(file_url(path), "file:///var/my tmp/vp-schemas-501/task-graph_0123.json"); + let path = AbsolutePath::new("/var/my tmp/vp_run-501/task-graph_0123.json").unwrap(); + assert_eq!(file_url(path), "file:///var/my tmp/vp_run-501/task-graph_0123.json"); } #[cfg(windows)] @@ -110,8 +111,8 @@ mod tests { fn file_urls() { let url = |path: &str| file_url(AbsolutePath::new(path).unwrap()); assert_eq!( - url(r"C:\Users\me\App Data\Temp\vp-schemas\task-graph_0123.json"), - "file:///C:/Users/me/App Data/Temp/vp-schemas/task-graph_0123.json" + url(r"C:\Users\me\App Data\Temp\vp_run\task-graph_0123.json"), + "file:///C:/Users/me/App Data/Temp/vp_run/task-graph_0123.json" ); assert_eq!( url(r"\\?\C:\Temp\task-graph_0123.json"), diff --git a/crates/vt_materialized_artifact/src/lib.rs b/crates/vt_materialized_artifact/src/lib.rs index f08cb4676..0a4000cd2 100644 --- a/crates/vt_materialized_artifact/src/lib.rs +++ b/crates/vt_materialized_artifact/src/lib.rs @@ -9,9 +9,10 @@ //! the value-add over a bare `include_bytes!`. //! //! Materialized files are named `{name}_{hash}{suffix}` in the caller-chosen -//! directory, normally one from [`user_temp_dir`], which no other user can -//! write to. The hash (computed at macro-expansion time by [`artifact!`]) -//! gives three properties without any coordination between processes: +//! directory, normally the current user's own folder in the system temp dir, +//! which no other user can write to (see [`Materialize::at_user_temp_dir`]). +//! 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. @@ -28,22 +29,11 @@ use std::{ path::{Path, PathBuf}, }; -/// A folder named after `name` in the system temp dir that only the current -/// user can write to, for materializing artifacts. It's created if missing. -/// -/// - On Unix, it's `{temp}/{name}-{uid}`, created with mode `0o700`, since the -/// temp dir can be shared by every user (`/tmp` on Linux). An existing path -/// must be a directory, not a symlink, owned by the current user, and not -/// writable by anyone else; otherwise this returns an error, so another user -/// can't plant files there. -/// - On Windows, it's `{temp}\{name}`: the temp dir is already the user's own. -/// -/// # Errors -/// -/// Returns an error if the folder can't be created, or, on Unix, if the -/// existing path isn't a directory only the current user can write to. -pub fn user_temp_dir(name: &str) -> io::Result { - user_dir_in(&env::temp_dir(), name) +/// The folder named after `prefix` in the system temp dir that only the +/// current user can write to, created if missing: see +/// [`Materialize::at_user_temp_dir`]. +fn user_temp_dir(prefix: &str) -> io::Result { + user_dir_in(&env::temp_dir(), prefix) } #[cfg(unix)] @@ -86,8 +76,8 @@ fn user_dir_in(temp_dir: &Path, name: &str) -> io::Result { /// 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. +/// [`Artifact::materialize`] + [`Materialize::at_user_temp_dir`] or +/// [`Materialize::at`]. See the [crate docs] for the design rationale. /// /// [crate docs]: crate #[derive(Clone, Copy)] @@ -110,7 +100,7 @@ impl Artifact { /// Start a fluent materialize chain. Supply optional [`Materialize::suffix`] /// / [`Materialize::executable`] knobs, then terminate with - /// [`Materialize::at`]. + /// [`Materialize::at_user_temp_dir`] or [`Materialize::at`]. pub const fn materialize(&self) -> Materialize<'static> { Materialize { artifact: *self, @@ -122,9 +112,9 @@ impl Artifact { } /// Builder returned by [`Artifact::materialize`]. Terminate with -/// [`Materialize::at`] to write the file. +/// [`Materialize::at_user_temp_dir`] or [`Materialize::at`] to write the file. #[derive(Clone, Copy)] -#[must_use = "materialize() only configures — call .at(dir) to write the file"] +#[must_use = "materialize() only configures — call .at_user_temp_dir(prefix) or .at(dir) to write the file"] pub struct Materialize<'a> { artifact: Artifact, suffix: &'a str, @@ -156,6 +146,27 @@ impl Materialize<'_> { self } + /// Materialize the artifact, as [`Self::at`] does, in the folder named + /// after `prefix` in the system temp dir that only the current user can + /// write to. The folder is created if missing. + /// + /// - On Unix, it's `{temp}/{prefix}-{uid}`, created with mode `0o700`, + /// since the temp dir can be shared by every user (`/tmp` on Linux). An + /// existing path must be a directory, not a symlink, owned by the + /// current user, and not writable by anyone else; otherwise this returns + /// an error, so another user can't plant files there. + /// - On Windows, it's `{temp}\{prefix}`: the temp dir is already the + /// user's own. + /// + /// # Errors + /// + /// Returns an error if the folder can't be created, or, on Unix, if the + /// existing path isn't a directory only the current user can write to, + /// and otherwise as [`Self::at`] does. + pub fn at_user_temp_dir(self, prefix: &str) -> io::Result { + self.at(user_temp_dir(prefix)?) + } + /// Materialize the artifact in `dir` under a content-addressed filename, /// writing it if missing. /// @@ -166,7 +177,8 @@ impl Materialize<'_> { /// # Preconditions /// /// `dir` must already exist and be writable only by the current user, as - /// [`user_temp_dir`] ensures — this method does not create it. + /// [`Self::at_user_temp_dir`]'s folder is — this method does not create + /// it. /// /// # Errors /// diff --git a/docs/rfcs/dry-run.md b/docs/rfcs/dry-run.md index bf6e2e963..a6411a87e 100644 --- a/docs/rfcs/dry-run.md +++ b/docs/rfcs/dry-run.md @@ -140,7 +140,7 @@ vp run --dry-run --json -r build ```json { - "$schema": "file:///tmp/vp-schemas-1000/run-plan_3b5d5c3712955042212316173ccf37be.json", + "$schema": "file:///tmp/vp_run-1000/run-plan_3b5d5c3712955042212316173ccf37be.json", "version": 1, "workspaceRoot": "/home/me/acme", "plan": { diff --git a/docs/rfcs/task-graph-json.md b/docs/rfcs/task-graph-json.md index c3b5308e8..5c96174b4 100644 --- a/docs/rfcs/task-graph-json.md +++ b/docs/rfcs/task-graph-json.md @@ -65,7 +65,7 @@ export default defineConfig({ ```json { - "$schema": "file:///tmp/vp-schemas-1000/task-graph_80cde1ad99307ef3eea7a658d7c95f88.json", + "$schema": "file:///tmp/vp_run-1000/task-graph_80cde1ad99307ef3eea7a658d7c95f88.json", "version": 1, "workspaceRoot": "/home/me/acme", "packages": { @@ -189,11 +189,11 @@ A path inside the workspace is relative to `workspaceRoot`, with `/` on every pl ### `$schema` -`$schema` points to the document's JSON Schema (draft-07), so an agent can read what each field means, and an editor shows the descriptions and checks the document when it is opened as JSON. Each time it prints a document, vp writes the schema to a folder in the system temp directory that only the current user can write to, such as `/tmp/vp-schemas-1000`, unless it is already there. The file is named by a hash of its content, such as `task-graph_80cde1ad99307ef3eea7a658d7c95f88.json`, so documents printed by different vp versions point to their own schemas. +`$schema` points to the document's JSON Schema (draft-07), so an agent can read what each field means, and an editor shows the descriptions and checks the document when it is opened as JSON. Each time it prints a document, vp writes the schema to a folder in the system temp directory that only the current user can write to, such as `/tmp/vp_run-1000`, unless it is already there. The file is named by a hash of its content, such as `task-graph_80cde1ad99307ef3eea7a658d7c95f88.json`, so documents printed by different vp versions point to their own schemas. Its value uses a form that VS Code and JetBrains IDEs both read on every platform: -- `file://`, then the absolute path with `/` separators, with a `/` before a Windows drive: `file:///tmp/vp-schemas-1000/task-graph_80cde1ad99307ef3eea7a658d7c95f88.json`, `file:///C:/Users/me/AppData/Local/Temp/vp-schemas/task-graph_80cde1ad99307ef3eea7a658d7c95f88.json`. +- `file://`, then the absolute path with `/` separators, with a `/` before a Windows drive: `file:///tmp/vp_run-1000/task-graph_80cde1ad99307ef3eea7a658d7c95f88.json`, `file:///C:/Users/me/AppData/Local/Temp/vp_run/task-graph_80cde1ad99307ef3eea7a658d7c95f88.json`. - Not percent-encoded. JetBrains IDEs look the path up as written, so they wouldn't find a space written as `%20`; VS Code reads both. A plain path wouldn't work on Windows: neither editor reads `C:\Users\…` as a schema location.