Skip to content
Closed
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
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion crates/fspy/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ fn register_preload_cdylib() -> anyhow::Result<()> {
}

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")?;
Expand Down
10 changes: 10 additions & 0 deletions crates/vite_task/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,28 @@ tokio = { workspace = true, features = [
tokio-util = { workspace = true }
tracing = { workspace = true }
twox-hash = { workspace = true }
materialized_artifact = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
vite_glob = { workspace = true }
vite_path = { workspace = true }
vite_select = { workspace = true }
vite_str = { workspace = true }
vite_task_graph = { workspace = true }
vite_task_ipc_shared = { workspace = true }
vite_task_plan = { workspace = true }
vite_task_server = { workspace = true }
vite_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 }
vite_task_client_napi = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }

Expand Down
20 changes: 18 additions & 2 deletions crates/vite_task/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
#![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;

// 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
// (the `fspy` crate builds on windows/macos/linux) is a three-OS list that
Expand All @@ -7,12 +17,18 @@
// 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() {
fn main() -> anyhow::Result<()> {
println!("cargo::rustc-check-cfg=cfg(fspy)");
println!("cargo::rerun-if-changed=build.rs");

let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
if matches!(target_os.as_str(), "windows" | "macos" | "linux") {
println!("cargo::rustc-cfg=fspy");
}

let env_name = "CARGO_CDYLIB_FILE_VITE_TASK_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("vite_task_client_napi", Path::new(&dylib_path));
Ok(())
}
1 change: 1 addition & 0 deletions crates/vite_task/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod cli;
mod collections;
mod napi_client;
pub mod session;

// Public exports for vite_task_bin
Expand Down
32 changes: 32 additions & 0 deletions crates/vite_task/src/napi_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! The `vite_task_client_napi` cdylib is embedded into the `vp` binary and
//! materialized to disk on first use so tools can `require()` it at runtime.

use std::{env, fs, sync::LazyLock};

use materialized_artifact::artifact;
use vite_path::{AbsolutePath, AbsolutePathBuf};

/// Path to the materialized `vite_task_client_napi` `.node` addon.
///
/// The file is written to a process-wide temp directory on first call and
/// reused on every subsequent call (content-addressed filename; no re-writes).
///
/// # Panics
///
/// Panics if the materialization fails on first call — this mirrors fspy's
/// `SPY_IMPL` and the same reasoning applies: if we can't write into the
/// system temp dir, the runner can't run tasks anyway.
#[must_use]
pub fn napi_client_path() -> &'static AbsolutePath {
static PATH: LazyLock<AbsolutePathBuf> = LazyLock::new(|| {
let dir = env::temp_dir().join("vite_task_client_napi");
let _ = fs::create_dir(&dir);
let path = artifact!("vite_task_client_napi")
.materialize()
.suffix(".node")
.at(&dir)
.expect("materialize vite_task_client_napi");
AbsolutePathBuf::new(path).expect("system temp dir yields an absolute path")
});
PATH.as_absolute_path()
}
1 change: 0 additions & 1 deletion crates/vite_task/src/session/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ pub enum ExecutionError {
/// The runner-aware IPC server failed to bind for this task. Reported
/// instead of silently degrading so that `{ auto: true }` inputs stay
/// observable end-to-end.
#[expect(dead_code, reason = "placeholder; constructed once the real `serve()` lands")]
#[error("Failed to start runner IPC server")]
IpcServerBind(#[source] std::io::Error),
}
Expand Down
54 changes: 35 additions & 19 deletions crates/vite_task/src/session/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ use tokio::sync::Semaphore;
use tokio_util::sync::CancellationToken;
use vite_path::{AbsolutePath, RelativePathBuf};
use vite_str::Str;
use vite_task_ipc_shared::NODE_CLIENT_PATH_ENV_NAME;
use vite_task_plan::{
ExecutionGraph, ExecutionItemDisplay, ExecutionItemKind, LeafExecutionKind, SpawnExecution,
cache_metadata::CacheMetadata, execution_graph::ExecutionNodeIndex,
};
use vite_task_server::{Recorder, Reports, StopAccepting};
use vite_task_server::{Recorder, Reports, ServerHandle, StopAccepting, serve};
use wax::Program as _;

#[cfg(fspy)]
Expand Down Expand Up @@ -499,25 +500,35 @@ pub async fn execute_spawn(
return SpawnOutcome::Failed;
}
};
// PLACEHOLDER: the IPC server isn't wired up yet on this
// branch. We construct an empty `Recorder` whose `Reports`
// never get any IPC traffic, and a `StopAccepting::noop`
// since no real server was bound. A follow-up will swap
// these out for `vite_task_server::serve(...)`.
// fspy + IPC are bundled. If binding the IPC server fails
// we abort the execution — tools that rely on IPC would
// otherwise silently diverge from the cache.
//
// The IPC `getEnv` endpoint serves values from the runner's
// own parent env (not the task's filtered `all_envs`), so a
// tool can ask for vars the user never declared and have
// them fingerprinted via the tool's `tracked: true` flag.
let env_map: FxHashMap<Arc<OsStr>, Arc<OsStr>> = std::env::vars_os()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serve IPC env lookups from the spawned environment

This captures only the runner process environment, but the actual child environment is spawn_execution.spawn_command.all_envs plus prefix envs and runtime injections. When a task uses a command prefix or other planned env override (for example FOO=prod node ... while the parent has FOO=dev or no FOO), getEnv('FOO') returns the parent value/None instead of what the spawned tool sees, so runner-aware tools can compute outputs from the wrong value and record the wrong post-run fingerprint.

Useful? React with 👍 / 👎.

.map(|(k, v)| {
(Arc::<OsStr>::from(k.as_os_str()), Arc::<OsStr>::from(v.as_os_str()))
})
.collect();
let recorder = Recorder::new(env_map);
let driver: LocalBoxFuture<'static, Result<Recorder, vite_task_server::Error>> =
Box::pin(std::future::ready(Ok(recorder)));
Some(Tracking {
input_negative_globs: negatives,
ipc_envs: Vec::new(),
ipc_server_fut: driver,
stop_accepting: StopAccepting::noop(),
})
match serve(Recorder::new(env_map)) {
Ok((envs, ServerHandle { driver, stop_accepting })) => Some(Tracking {
input_negative_globs: negatives,
ipc_envs: envs.collect(),
ipc_server_fut: driver,
stop_accepting,
}),
Err(err) => {
leaf_reporter.finish(
None,
CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled),
Some(ExecutionError::IpcServerBind(err)),
);
return SpawnOutcome::Failed;
}
}
} else {
None
};
Expand All @@ -539,12 +550,17 @@ pub async fn execute_spawn(
ExecutionMode::Uncached { pipe_writers: None } => (SpawnStdio::Inherited, false),
};

// Build the extra envs to inject: IPC connection info + (eventually)
// napi addon path. Empty when tracking is off. The napi path is added
// in a follow-up that wires up the real server.
// Build the extra envs to inject: IPC connection info + napi addon path.
// Empty when tracking is off.
let extra_envs: Vec<(&OsStr, &OsStr)> = match &mode {
ExecutionMode::Cached { state: CacheState { tracking: Some(t), .. }, .. } => {
t.ipc_envs.iter().map(|(k, v)| (*k as &OsStr, v.as_os_str())).collect()
let mut envs: Vec<(&OsStr, &OsStr)> =
t.ipc_envs.iter().map(|(k, v)| (*k as &OsStr, v.as_os_str())).collect();
envs.push((
OsStr::new(NODE_CLIENT_PATH_ENV_NAME),
crate::napi_client::napi_client_path().as_path().as_os_str(),
));
envs
}
_ => Vec::new(),
};
Expand Down
16 changes: 16 additions & 0 deletions crates/vite_task_bin/src/vtt/grep_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pub fn run(args: &[String]) {
let [path, pattern] = args else {
eprintln!("Usage: vtt grep-file <path> <pattern>");
std::process::exit(2);
};
match std::fs::read_to_string(path) {
Ok(content) => {
if content.contains(pattern.as_str()) {
println!("{path}: found {pattern:?}");
} else {
println!("{path}: missing {pattern:?}");
}
}
Err(_) => println!("{path}: not found"),
}
}
12 changes: 11 additions & 1 deletion crates/vite_task_bin/src/vtt/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod check_tty;
mod cp;
mod exit;
mod exit_on_ctrlc;
mod grep_file;
mod list_dir;
mod mkdir;
mod pipe_stdin;
Expand All @@ -22,6 +23,7 @@ mod print_file;
mod read_stdin;
mod replace_file_content;
mod rm;
mod stat_file;
mod touch_file;
mod write_file;

Expand All @@ -30,7 +32,7 @@ fn main() {
if args.len() < 2 {
eprintln!("Usage: vtt <subcommand> [args...]");
eprintln!(
"Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, touch-file, write-file"
"Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, grep-file, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, stat-file, touch-file, write-file"
);
std::process::exit(1);
}
Expand All @@ -44,6 +46,10 @@ fn main() {
"cp" => cp::run(&args[2..]),
"exit" => exit::run(&args[2..]),
"exit-on-ctrlc" => exit_on_ctrlc::run(),
"grep-file" => {
grep_file::run(&args[2..]);
Ok(())
}
"list-dir" => list_dir::run(&args[2..]),
"mkdir" => mkdir::run(&args[2..]),
"pipe-stdin" => pipe_stdin::run(&args[2..]),
Expand All @@ -58,6 +64,10 @@ fn main() {
"read-stdin" => read_stdin::run(),
"replace-file-content" => replace_file_content::run(&args[2..]),
"rm" => rm::run(&args[2..]),
"stat-file" => {
stat_file::run(&args[2..]);
Ok(())
}
"touch-file" => touch_file::run(&args[2..]),
"write-file" => write_file::run(&args[2..]),
other => {
Expand Down
9 changes: 9 additions & 0 deletions crates/vite_task_bin/src/vtt/stat_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pub fn run(args: &[String]) {
for file in args {
if std::fs::metadata(file).is_ok() {
println!("{file}: exists");
} else {
println!("{file}: missing");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "ipc-client-test",
"private": true,
"type": "module"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { disableCache } from '@voidzero-dev/vite-task-client';
import { writeFileSync, mkdirSync } from 'node:fs';

// Produce an output, then ask the runner not to cache this execution — the
// next `vt run` should re-execute the task.
mkdirSync('dist', { recursive: true });
writeFileSync('dist/out.txt', 'ok\n');
disableCache();
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { getEnv } from '@voidzero-dev/vite-task-client';
import { writeFileSync, mkdirSync } from 'node:fs';

// getEnv returns the env value from the runner and — with tracked: true —
// adds the env to the post-run fingerprint, so a change between runs
// invalidates the cache.
const value = getEnv('PROBE_ENV', { tracked: true }) ?? '(unset)';

mkdirSync('dist', { recursive: true });
writeFileSync('dist/out.txt', 'PROBE_ENV=' + value + '\n');
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getEnvs } from '@voidzero-dev/vite-task-client';
import { writeFileSync, mkdirSync } from 'node:fs';

// getEnvs asks the runner for every env matching the glob. The glob (plus
// its match-set) becomes part of the post-run fingerprint, so adding,
// removing, or changing any matching env invalidates the cache on the next
// run. The non-matching UNRELATED envs set by some test steps must not
// contribute.
const matches = getEnvs('PROBE_*', { tracked: true });

mkdirSync('dist', { recursive: true });
const sorted = Object.entries(matches).sort(([a], [b]) => a.localeCompare(b));
const body = sorted.map(([k, v]) => `${k}=${v}`).join('\n');
writeFileSync('dist/out.txt', body + '\n');
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ignoreInput } from '@voidzero-dev/vite-task-client';
import { writeFileSync, readFileSync } from 'node:fs';
import { mkdirSync } from 'node:fs';

// The task reads from `cache_like/` (which we want the runner to IGNORE as
// an input), and writes to `dist/`. Without the ignore, the auto-input
// fingerprint would fluctuate with cache_like/ contents even though they're
// not semantic inputs.
mkdirSync('cache_like', { recursive: true });
writeFileSync('cache_like/stale.txt', 'stale-' + Date.now() + '\n');
ignoreInput('cache_like');
readFileSync('cache_like/stale.txt', 'utf8');

mkdirSync('dist', { recursive: true });
writeFileSync('dist/out.txt', 'ok\n');
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ignoreOutput } from '@voidzero-dev/vite-task-client';
import { writeFileSync, readFileSync, mkdirSync } from 'node:fs';

// The task both reads and writes `sidecar/tmp.txt`. If the runner didn't
// treat `sidecar/` as an ignored output, the read-write overlap check would
// refuse to cache the task. `dist/out.txt` is the real output.
mkdirSync('sidecar', { recursive: true });
writeFileSync('sidecar/tmp.txt', 'initial\n');
readFileSync('sidecar/tmp.txt', 'utf8');
writeFileSync('sidecar/tmp.txt', 'final\n');
ignoreOutput('sidecar');

mkdirSync('dist', { recursive: true });
writeFileSync('dist/out.txt', 'ok\n');
Loading
Loading