Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)).
- **Added** `vp run --json` prints every package and task in the workspace, with each task's `dependsOn` resolved to task IDs, as one JSON document for scripts, editors and coding agents. It runs nothing and, like `vp run` without a task, loads every package's config. It prints the same document from any directory in the workspace: indented in a terminal, on one line when captured. Its `$schema` field points to a JSON Schema that describes each field, which `vp run` writes to the system temp directory ([#948](https://github.com/voidzero-dev/vite-task/pull/948)).
- **Fixed** On Linux, where every user shares the temp folder `/tmp`, the helper files `vp run` writes there, such as the library that tracks file accesses, now go in a folder only the current user can write to, like `/tmp/fspy-1000`. Before, they went in a folder such as `/tmp/fspy` that belonged to whoever ran `vp run` first: running a different version as another user failed, and the same version used files that user could replace ([#947](https://github.com/voidzero-dev/vite-task/pull/947)).
- **Fixed** On macOS, task commands that use `sh` or `bash` now behave as they do on Linux in four cases. A `while read` loop with a redirect under `set -e` no longer exits the script. An `EXIT` trap now sees the exit status in `$?`, and a bare `exit` in the trap keeps that status, so a failed task is no longer reported as successful. `$BASH` is now set when the command runs as `bash`. The bundled shell is updated to an Oils build from master that has these fixes ([#939](https://github.com/voidzero-dev/vite-task/pull/939)).
Expand Down
7 changes: 2 additions & 5 deletions crates/fspy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,5 @@ pub struct TrackedChild {
pub process_handle: std::os::windows::io::OwnedHandle,
}

pub(crate) static SPY_IMPL: LazyLock<SpyImpl> = 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<SpyImpl> =
LazyLock::new(|| SpyImpl::init().expect("Failed to initialize global spy"));
14 changes: 8 additions & 6 deletions crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -45,19 +45,21 @@ 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<Self> {
pub fn init() -> io::Result<Self> {
#[cfg(not(target_env = "musl"))]
let preload_path = {
use vt_materialized_artifact::{Artifact, artifact};

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()
};

Expand All @@ -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(),
})
Expand Down
7 changes: 4 additions & 3 deletions crates/fspy/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use std::{
ffi::OsStrExt,
io::{AsRawHandle, BorrowedHandle},
},
path::Path,
sync::Arc,
};

Expand Down Expand Up @@ -56,8 +55,10 @@ pub struct SpyImpl {
}

impl SpyImpl {
pub fn init_in(path: &Path) -> io::Result<Self> {
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<Self> {
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::<Vec<u16>>();
let mut ansi_dll_path =
Expand Down
13 changes: 5 additions & 8 deletions crates/vt/src/napi_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -20,13 +20,10 @@ use vt_path::{AbsolutePath, AbsolutePathBuf};
#[must_use]
pub fn napi_client_path() -> &'static AbsolutePath {
static PATH: LazyLock<AbsolutePathBuf> = 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")
});
Expand Down
17 changes: 9 additions & 8 deletions crates/vt/src/session/task_graph_json/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -14,9 +14,10 @@ pub fn schema_url() -> io::Result<Str> {
}

fn materialize() -> io::Result<AbsolutePathBuf> {
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"))
}
Expand Down Expand Up @@ -101,17 +102,17 @@ 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)]
#[test]
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"),
Expand Down
62 changes: 37 additions & 25 deletions crates/vt_materialized_artifact/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<PathBuf> {
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<PathBuf> {
user_dir_in(&env::temp_dir(), prefix)
}

#[cfg(unix)]
Expand Down Expand Up @@ -86,8 +76,8 @@ fn user_dir_in(temp_dir: &Path, name: &str) -> io::Result<PathBuf> {
/// 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)]
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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<PathBuf> {
self.at(user_temp_dir(prefix)?)
}

/// Materialize the artifact in `dir` under a content-addressed filename,
/// writing it if missing.
///
Expand All @@ -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
///
Expand Down
2 changes: 1 addition & 1 deletion docs/rfcs/dry-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
6 changes: 3 additions & 3 deletions docs/rfcs/task-graph-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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.
Expand Down
Loading