Skip to content

Commit fa9255d

Browse files
wan9chiclaude
andcommitted
feat(cache): integrate runner-aware IPC reports
Replaces the previous PR's placeholder with the real `vite_task_server::serve(...)` call: per-task IPC server, napi addon embedded into the runner binary and materialized to disk on first use, `VP_RUN_NODE_CLIENT_PATH` injected into the child so the JS wrapper can `require()` it. Cache integration: `Reports` collected from the IPC drive `PostRunFingerprint` and the cache-update path — - `ignore_input` reads → excluded from input fingerprint - `ignore_output` writes → excluded from output archive - `tracked_envs` (single name) + `tracked_env_globs` → folded into the post-run fingerprint so a value change misses the cache - `disable_cache` → skips the cache-update path entirely (`ToolRequested`) - IPC server bind/runtime failure → `IpcServerError` cache disable End-to-end coverage via the `ipc_client_test` fixture set: one fixture per API method, each exercising the real Rust ↔ JS path and asserting the right cache behaviour. Adds `vtt` (test-only) helpers `grep_file` and `stat_file` that the fixtures need. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 58b706e commit fa9255d

25 files changed

Lines changed: 733 additions & 25 deletions

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fspy/build.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ fn register_preload_cdylib() -> anyhow::Result<()> {
158158
}
159159

160160
fn main() -> anyhow::Result<()> {
161-
println!("cargo:rerun-if-changed=build.rs");
162161
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
163162
fetch_macos_binaries(&out_dir).context("Failed to fetch macOS binaries")?;
164163
register_preload_cdylib().context("Failed to register preload cdylib")?;

crates/vite_task/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,28 @@ tokio = { workspace = true, features = [
4343
tokio-util = { workspace = true }
4444
tracing = { workspace = true }
4545
twox-hash = { workspace = true }
46+
materialized_artifact = { workspace = true }
4647
uuid = { workspace = true, features = ["v4"] }
4748
vite_glob = { workspace = true }
4849
vite_path = { workspace = true }
4950
vite_select = { workspace = true }
5051
vite_str = { workspace = true }
5152
vite_task_graph = { workspace = true }
53+
vite_task_ipc_shared = { workspace = true }
5254
vite_task_plan = { workspace = true }
5355
vite_task_server = { workspace = true }
5456
vite_workspace = { workspace = true }
5557
wax = { workspace = true }
5658
zstd = { workspace = true }
5759

60+
# Artifact build-deps must be unconditional: cargo's resolver panics when
61+
# `artifact = "cdylib"` deps live under a `[target.cfg.build-dependencies]`
62+
# block on cross-compile.
63+
[build-dependencies]
64+
anyhow = { workspace = true }
65+
materialized_artifact_build = { workspace = true }
66+
vite_task_client_napi = { workspace = true }
67+
5868
[dev-dependencies]
5969
tempfile = { workspace = true }
6070

crates/vite_task/build.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
#![expect(
2+
clippy::disallowed_types,
3+
clippy::disallowed_macros,
4+
reason = "build.rs interfaces with std::path and cargo's env-var API"
5+
)]
6+
7+
use std::{env, path::Path};
8+
9+
use anyhow::Context;
10+
111
// Why `cfg(fspy)` instead of matching on `target_os` directly at each use site:
212
// "fspy is available" is a single semantic predicate, but the underlying reason
313
// (the `fspy` crate builds on windows/macos/linux) is a three-OS list that
@@ -7,12 +17,18 @@
717
// over OSes. The OS allowlist lives in two spots that must stay in sync: this
818
// file (for the rustc cfg) and the target-scoped dep block in Cargo.toml
919
// (which Cargo resolves before build.rs runs, so it can't reuse this cfg).
10-
fn main() {
20+
fn main() -> anyhow::Result<()> {
1121
println!("cargo::rustc-check-cfg=cfg(fspy)");
1222
println!("cargo::rerun-if-changed=build.rs");
1323

14-
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
24+
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
1525
if matches!(target_os.as_str(), "windows" | "macos" | "linux") {
1626
println!("cargo::rustc-cfg=fspy");
1727
}
28+
29+
let env_name = "CARGO_CDYLIB_FILE_VITE_TASK_CLIENT_NAPI";
30+
println!("cargo:rerun-if-env-changed={env_name}");
31+
let dylib_path = env::var_os(env_name).with_context(|| format!("{env_name} not set"))?;
32+
materialized_artifact_build::register("vite_task_client_napi", Path::new(&dylib_path));
33+
Ok(())
1834
}

crates/vite_task/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod cli;
22
mod collections;
3+
mod napi_client;
34
pub mod session;
45

56
// Public exports for vite_task_bin
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! The `vite_task_client_napi` cdylib is embedded into the `vp` binary and
2+
//! materialized to disk on first use so tools can `require()` it at runtime.
3+
4+
use std::{env, fs, sync::LazyLock};
5+
6+
use materialized_artifact::artifact;
7+
use vite_path::{AbsolutePath, AbsolutePathBuf};
8+
9+
/// Path to the materialized `vite_task_client_napi` `.node` addon.
10+
///
11+
/// The file is written to a process-wide temp directory on first call and
12+
/// reused on every subsequent call (content-addressed filename; no re-writes).
13+
///
14+
/// # Panics
15+
///
16+
/// Panics if the materialization fails on first call — this mirrors fspy's
17+
/// `SPY_IMPL` and the same reasoning applies: if we can't write into the
18+
/// system temp dir, the runner can't run tasks anyway.
19+
#[must_use]
20+
pub fn napi_client_path() -> &'static AbsolutePath {
21+
static PATH: LazyLock<AbsolutePathBuf> = LazyLock::new(|| {
22+
let dir = env::temp_dir().join("vite_task_client_napi");
23+
let _ = fs::create_dir(&dir);
24+
let path = artifact!("vite_task_client_napi")
25+
.materialize()
26+
.suffix(".node")
27+
.at(&dir)
28+
.expect("materialize vite_task_client_napi");
29+
AbsolutePathBuf::new(path).expect("system temp dir yields an absolute path")
30+
});
31+
PATH.as_absolute_path()
32+
}

crates/vite_task/src/session/event.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ pub enum ExecutionError {
4848
/// The runner-aware IPC server failed to bind for this task. Reported
4949
/// instead of silently degrading so that `{ auto: true }` inputs stay
5050
/// observable end-to-end.
51-
#[expect(dead_code, reason = "placeholder; constructed once the real `serve()` lands")]
5251
#[error("Failed to start runner IPC server")]
5352
IpcServerBind(#[source] std::io::Error),
5453
}

crates/vite_task/src/session/execute/mod.rs

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ use tokio::sync::Semaphore;
2424
use tokio_util::sync::CancellationToken;
2525
use vite_path::{AbsolutePath, RelativePathBuf};
2626
use vite_str::Str;
27+
use vite_task_ipc_shared::NODE_CLIENT_PATH_ENV_NAME;
2728
use vite_task_plan::{
2829
ExecutionGraph, ExecutionItemDisplay, ExecutionItemKind, LeafExecutionKind, SpawnExecution,
2930
cache_metadata::CacheMetadata, execution_graph::ExecutionNodeIndex,
3031
};
31-
use vite_task_server::{Recorder, Reports, StopAccepting};
32+
use vite_task_server::{Recorder, Reports, ServerHandle, StopAccepting, serve};
3233
use wax::Program as _;
3334

3435
#[cfg(fspy)]
@@ -499,25 +500,35 @@ pub async fn execute_spawn(
499500
return SpawnOutcome::Failed;
500501
}
501502
};
502-
// PLACEHOLDER: the IPC server isn't wired up yet on this
503-
// branch. We construct an empty `Recorder` whose `Reports`
504-
// never get any IPC traffic, and a `StopAccepting::noop`
505-
// since no real server was bound. A follow-up will swap
506-
// these out for `vite_task_server::serve(...)`.
503+
// fspy + IPC are bundled. If binding the IPC server fails
504+
// we abort the execution — tools that rely on IPC would
505+
// otherwise silently diverge from the cache.
506+
//
507+
// The IPC `getEnv` endpoint serves values from the runner's
508+
// own parent env (not the task's filtered `all_envs`), so a
509+
// tool can ask for vars the user never declared and have
510+
// them fingerprinted via the tool's `tracked: true` flag.
507511
let env_map: FxHashMap<Arc<OsStr>, Arc<OsStr>> = std::env::vars_os()
508512
.map(|(k, v)| {
509513
(Arc::<OsStr>::from(k.as_os_str()), Arc::<OsStr>::from(v.as_os_str()))
510514
})
511515
.collect();
512-
let recorder = Recorder::new(env_map);
513-
let driver: LocalBoxFuture<'static, Result<Recorder, vite_task_server::Error>> =
514-
Box::pin(std::future::ready(Ok(recorder)));
515-
Some(Tracking {
516-
input_negative_globs: negatives,
517-
ipc_envs: Vec::new(),
518-
ipc_server_fut: driver,
519-
stop_accepting: StopAccepting::noop(),
520-
})
516+
match serve(Recorder::new(env_map)) {
517+
Ok((envs, ServerHandle { driver, stop_accepting })) => Some(Tracking {
518+
input_negative_globs: negatives,
519+
ipc_envs: envs.collect(),
520+
ipc_server_fut: driver,
521+
stop_accepting,
522+
}),
523+
Err(err) => {
524+
leaf_reporter.finish(
525+
None,
526+
CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled),
527+
Some(ExecutionError::IpcServerBind(err)),
528+
);
529+
return SpawnOutcome::Failed;
530+
}
531+
}
521532
} else {
522533
None
523534
};
@@ -539,12 +550,17 @@ pub async fn execute_spawn(
539550
ExecutionMode::Uncached { pipe_writers: None } => (SpawnStdio::Inherited, false),
540551
};
541552

542-
// Build the extra envs to inject: IPC connection info + (eventually)
543-
// napi addon path. Empty when tracking is off. The napi path is added
544-
// in a follow-up that wires up the real server.
553+
// Build the extra envs to inject: IPC connection info + napi addon path.
554+
// Empty when tracking is off.
545555
let extra_envs: Vec<(&OsStr, &OsStr)> = match &mode {
546556
ExecutionMode::Cached { state: CacheState { tracking: Some(t), .. }, .. } => {
547-
t.ipc_envs.iter().map(|(k, v)| (*k as &OsStr, v.as_os_str())).collect()
557+
let mut envs: Vec<(&OsStr, &OsStr)> =
558+
t.ipc_envs.iter().map(|(k, v)| (*k as &OsStr, v.as_os_str())).collect();
559+
envs.push((
560+
OsStr::new(NODE_CLIENT_PATH_ENV_NAME),
561+
crate::napi_client::napi_client_path().as_path().as_os_str(),
562+
));
563+
envs
548564
}
549565
_ => Vec::new(),
550566
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
pub fn run(args: &[String]) {
2+
let [path, pattern] = args else {
3+
eprintln!("Usage: vtt grep-file <path> <pattern>");
4+
std::process::exit(2);
5+
};
6+
match std::fs::read_to_string(path) {
7+
Ok(content) => {
8+
if content.contains(pattern.as_str()) {
9+
println!("{path}: found {pattern:?}");
10+
} else {
11+
println!("{path}: missing {pattern:?}");
12+
}
13+
}
14+
Err(_) => println!("{path}: not found"),
15+
}
16+
}

crates/vite_task_bin/src/vtt/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod check_tty;
1111
mod cp;
1212
mod exit;
1313
mod exit_on_ctrlc;
14+
mod grep_file;
1415
mod list_dir;
1516
mod mkdir;
1617
mod pipe_stdin;
@@ -22,6 +23,7 @@ mod print_file;
2223
mod read_stdin;
2324
mod replace_file_content;
2425
mod rm;
26+
mod stat_file;
2527
mod touch_file;
2628
mod write_file;
2729

@@ -30,7 +32,7 @@ fn main() {
3032
if args.len() < 2 {
3133
eprintln!("Usage: vtt <subcommand> [args...]");
3234
eprintln!(
33-
"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"
35+
"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"
3436
);
3537
std::process::exit(1);
3638
}
@@ -44,6 +46,10 @@ fn main() {
4446
"cp" => cp::run(&args[2..]),
4547
"exit" => exit::run(&args[2..]),
4648
"exit-on-ctrlc" => exit_on_ctrlc::run(),
49+
"grep-file" => {
50+
grep_file::run(&args[2..]);
51+
Ok(())
52+
}
4753
"list-dir" => list_dir::run(&args[2..]),
4854
"mkdir" => mkdir::run(&args[2..]),
4955
"pipe-stdin" => pipe_stdin::run(&args[2..]),
@@ -58,6 +64,10 @@ fn main() {
5864
"read-stdin" => read_stdin::run(),
5965
"replace-file-content" => replace_file_content::run(&args[2..]),
6066
"rm" => rm::run(&args[2..]),
67+
"stat-file" => {
68+
stat_file::run(&args[2..]);
69+
Ok(())
70+
}
6171
"touch-file" => touch_file::run(&args[2..]),
6272
"write-file" => write_file::run(&args[2..]),
6373
other => {

0 commit comments

Comments
 (0)