Skip to content

Commit 04f458c

Browse files
wan9chiclaude
andcommitted
feat(cache): runner-aware env tracking via getEnv/getEnvs
Makes the addon's getEnv/getEnvs no-ops real — protocol verbs, sync client round-trips, and server-side serving — and fingerprints the served values so changing them invalidates the cache: - getEnv/getEnvs are served from the spawn's full env context saved in the plan: the same env map the task is spawned with, including command-prefix and nested-run prefix envs. std::env::vars_os is read exactly once to bootstrap the session env snapshot (enforced by clippy). - Served values and glob match-sets are recorded in the post-run fingerprint and validated on the next lookup; prerun and postrun env mismatches share one EnvMismatch enum, and misses name the env var inline (cache miss: env 'NODE_ENV' changed). - E2E: direct-client fixtures cover value changes, glob match-set changes, and prefix-env visibility; real vite build re-runs on NODE_ENV/prefix-env changes via the served values. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent af66fd9 commit 04f458c

60 files changed

Lines changed: 2307 additions & 233 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22

3+
- **Added** Runner-aware env tracking: tools can ask the runner for env values via `getEnv`/`getEnvs`, and the served values (and glob match-sets) become part of the cache fingerprint, so changing them invalidates the cache with the env var named in the miss message ([#430](https://github.com/voidzero-dev/vite-task/pull/430))
34
- **Added** Runner-aware tools can now opt the current task run out of caching through the new IPC channel; Vite dev server integration uses this automatically ([#441](https://github.com/voidzero-dev/vite-task/pull/441))
45
- **Fixed** Prefix environment assignments like `PATH=... command` now affect executable lookup during task planning, so tools provided only by the prefixed `PATH` can be resolved correctly ([#440](https://github.com/voidzero-dev/vite-task/pull/440))
56
- **Changed** Cache misses caused by a tracked env var now name the env var inline, for example `cache miss: env 'NODE_ENV' changed`, instead of the generic `envs changed` message ([#438](https://github.com/voidzero-dev/vite-task/pull/438))

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ Enforced by `.clippy.toml`:
140140
- Only convert to std paths when interfacing with std library functions
141141
- Add necessary methods in `vite_path` instead of falling back to std path types
142142

143+
### Environment Variables
144+
145+
`std::env::vars_os` is read exactly once — in `Session::init` — to bootstrap the session env snapshot (`Session.envs`). Everything downstream (planning, spawn env resolution, IPC `getEnv`/`getEnvs`, cache fingerprint validation) must use that snapshot or the plan's resolved env maps, never re-read the live process env. This keeps a run's behavior consistent with its plan and lets tests inject envs via `Session::init_with`.
146+
143147
### Cross-Platform Requirements
144148

145149
All code must work on both Unix and Windows without platform skipping:

Cargo.lock

Lines changed: 8 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/native_str/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use wincode::{
3737
/// **Not portable across platforms.** The binary representation is platform-specific.
3838
/// Deserializing a `NativeStr` serialized on a different platform leads to unspecified
3939
/// behavior (garbage data), but is not unsafe. Designed for same-platform IPC only.
40-
#[derive(TransparentWrapper, PartialEq, Eq)]
40+
#[derive(TransparentWrapper, PartialEq, Eq, Hash)]
4141
#[repr(transparent)]
4242
pub struct NativeStr {
4343
// On unix, this is the raw bytes of the OsStr.

crates/vite_task/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ tracing = { workspace = true }
4545
twox-hash = { workspace = true }
4646
materialized_artifact = { workspace = true }
4747
uuid = { workspace = true, features = ["v4"] }
48+
vite_glob = { workspace = true }
4849
vite_path = { workspace = true }
4950
vite_select = { workspace = true }
5051
vite_str = { workspace = true }

crates/vite_task/docs/task-cache.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ The cache entry key uniquely identifies a command execution context:
9292
```rust
9393
pub struct CacheEntryKey {
9494
pub spawn_fingerprint: SpawnFingerprint,
95-
pub input_config: ResolvedInputConfig,
95+
pub input_config: ResolvedGlobConfig,
9696
}
9797
```
9898

@@ -303,7 +303,7 @@ Cache entries are serialized using `bincode` for efficient storage.
303303
│ ────────────────────── │
304304
│ CacheEntryKey { │
305305
│ spawn_fingerprint: SpawnFingerprint { ... }, │
306-
│ input_config: ResolvedInputConfig { ... }, │
306+
│ input_config: ResolvedGlobConfig { ... }, │
307307
│ } │
308308
│ ExecutionCacheKey::UserTask { │
309309
│ task_name: "build", │

crates/vite_task/src/session/cache/display.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ pub fn detect_spawn_fingerprint_changes(
120120
}
121121

122122
/// Names of the env vars involved in a set of spawn-fingerprint changes, in the
123-
/// order detected. Only env changes are collected; untracked-env and non-env
124-
/// changes are skipped.
123+
/// order detected. Only the tracked-env kinds are collected; untracked-env and
124+
/// non-env changes are skipped.
125125
fn env_change_names(changes: &[SpawnFingerprintChange]) -> Vec<&Str> {
126126
changes
127127
.iter()
@@ -195,6 +195,13 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
195195
FingerprintMismatch::InputChanged { kind, path } => {
196196
format_input_change_str(*kind, path.as_str())
197197
}
198+
// Env changes reported by a runner-aware tool render the same as
199+
// changes detected from a manual `env` config — both name the
200+
// env var that changed.
201+
FingerprintMismatch::TrackedEnvChanged(mismatch)
202+
| FingerprintMismatch::TrackedEnvGlobChanged { mismatch, .. } => {
203+
format_env_changed_inline(&[mismatch.name()])
204+
}
198205
};
199206
Some(vite_str::format!("○ cache miss: {reason}, executing"))
200207
}

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

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
pub mod archive;
44
pub mod display;
55

6-
use std::{collections::BTreeMap, fmt::Display, fs::File, io::Write, sync::Arc, time::Duration};
6+
use std::{
7+
collections::BTreeMap, ffi::OsStr, fmt::Display, fs::File, io::Write, sync::Arc, time::Duration,
8+
};
79

810
// Re-export display functions for convenience
911
pub use display::format_cache_status_inline;
@@ -12,6 +14,7 @@ pub use display::{
1214
format_spawn_change,
1315
};
1416
use rusqlite::{Connection, OptionalExtension as _};
17+
use rustc_hash::FxHashMap;
1518
use serde::{Deserialize, Serialize};
1619
use tokio::sync::Mutex;
1720
use vite_path::{AbsolutePath, RelativePathBuf};
@@ -142,9 +145,10 @@ pub enum InputChangeKind {
142145
/// A single env var difference between a stored fingerprint and the current
143146
/// environment.
144147
///
145-
/// The canonical shape for an env change wherever one is detected and
146-
/// reported. The [`Display`] impl is the single source of the user-facing
147-
/// wording.
148+
/// Shared by the prerun (spawn fingerprint) and post-run (tool-tracked env /
149+
/// env glob) mismatch paths, so every env change is reported and rendered the
150+
/// same way regardless of where it was detected. The [`Display`] impl is the
151+
/// single source of the user-facing wording.
148152
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149153
pub enum EnvMismatch {
150154
/// Set now, but absent from the stored fingerprint.
@@ -165,6 +169,23 @@ impl EnvMismatch {
165169
}
166170
}
167171
}
172+
173+
/// Compare a stored env value against the current one, returning the
174+
/// mismatch if they differ. `None` on either side means the env is unset
175+
/// there; two unset (or two equal) values are not a mismatch.
176+
#[must_use]
177+
pub fn compare(name: &Str, stored: Option<&Str>, current: Option<&Str>) -> Option<Self> {
178+
match (stored, current) {
179+
(None, Some(value)) => Some(Self::Added { name: name.clone(), value: value.clone() }),
180+
(Some(value), None) => Some(Self::Removed { name: name.clone(), value: value.clone() }),
181+
(Some(old_value), Some(new_value)) if old_value != new_value => Some(Self::Changed {
182+
name: name.clone(),
183+
old_value: old_value.clone(),
184+
new_value: new_value.clone(),
185+
}),
186+
_ => None,
187+
}
188+
}
168189
}
169190

170191
impl Display for EnvMismatch {
@@ -198,22 +219,24 @@ pub enum FingerprintMismatch {
198219
kind: InputChangeKind,
199220
path: RelativePathBuf,
200221
},
222+
/// A tool-tracked env var changed between runs.
223+
TrackedEnvChanged(EnvMismatch),
224+
/// A tool-tracked env glob's match-set changed between runs. Carries the
225+
/// first differing entry.
226+
TrackedEnvGlobChanged {
227+
pattern: Str,
228+
mismatch: EnvMismatch,
229+
},
201230
}
202231

203-
impl Display for FingerprintMismatch {
204-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205-
match self {
206-
Self::SpawnFingerprint { old, new } => {
207-
write!(f, "Spawn fingerprint changed: old={old:?}, new={new:?}")
208-
}
209-
Self::InputConfig => {
210-
write!(f, "input configuration changed")
211-
}
212-
Self::OutputConfig => {
213-
write!(f, "output configuration changed")
214-
}
215-
Self::InputChanged { kind, path } => {
216-
write!(f, "{}", display::format_input_change_str(*kind, path.as_str()))
232+
impl From<crate::session::execute::fingerprint::PostRunMismatch> for FingerprintMismatch {
233+
fn from(mismatch: crate::session::execute::fingerprint::PostRunMismatch) -> Self {
234+
use crate::session::execute::fingerprint::PostRunMismatch;
235+
match mismatch {
236+
PostRunMismatch::InputChanged { kind, path } => Self::InputChanged { kind, path },
237+
PostRunMismatch::TrackedEnvChanged(mismatch) => Self::TrackedEnvChanged(mismatch),
238+
PostRunMismatch::TrackedEnvGlobChanged { pattern, mismatch } => {
239+
Self::TrackedEnvGlobChanged { pattern, mismatch }
217240
}
218241
}
219242
}
@@ -240,7 +263,7 @@ pub fn split_path(path: &str) -> (Option<&str>, &str) {
240263
/// its own cache warm across branch switches, and a cache from a different
241264
/// version is simply ignored (it lives in a directory this build never looks
242265
/// at) rather than aborting the run. Bumping the version starts a fresh cache.
243-
const CACHE_SCHEMA_VERSION: u32 = 13;
266+
const CACHE_SCHEMA_VERSION: u32 = 14;
244267

245268
/// Name of the per-version subdirectory (e.g. `v13`) under the task-cache
246269
/// directory that holds the database and output archives for the current
@@ -286,12 +309,16 @@ impl ExecutionCache {
286309

287310
/// Try to hit cache by looking up the cache entry key and validating inputs.
288311
/// Returns `Ok(Ok(cache_value))` on cache hit, `Ok(Err(cache_miss))` on miss.
312+
///
313+
/// `envs` is the session env snapshot the run's plan was bootstrapped from;
314+
/// tracked-env validation resolves against it, never the live process env.
289315
#[tracing::instrument(level = "debug", skip_all)]
290316
pub async fn try_hit(
291317
&self,
292318
cache_metadata: &CacheMetadata,
293319
globbed_inputs: &BTreeMap<RelativePathBuf, u64>,
294320
workspace_root: &AbsolutePath,
321+
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
295322
) -> anyhow::Result<Result<CacheEntryValue, CacheMiss>> {
296323
let spawn_fingerprint = &cache_metadata.spawn_fingerprint;
297324
let execution_cache_key = &cache_metadata.execution_cache_key;
@@ -307,11 +334,11 @@ impl ExecutionCache {
307334
return Ok(Err(CacheMiss::FingerprintMismatch(mismatch)));
308335
}
309336

310-
// Validate post-run fingerprint (inferred inputs from fspy)
311-
if let Some((kind, path)) = cache_value.post_run_fingerprint.validate(workspace_root)? {
312-
return Ok(Err(CacheMiss::FingerprintMismatch(
313-
FingerprintMismatch::InputChanged { kind, path },
314-
)));
337+
// Validate post-run fingerprint (inferred inputs + tracked envs)
338+
if let Some(mismatch) =
339+
cache_value.post_run_fingerprint.validate(workspace_root, envs)?
340+
{
341+
return Ok(Err(CacheMiss::FingerprintMismatch(mismatch.into())));
315342
}
316343
// Associate the execution key to the cache entry key if not already,
317344
// so that next time we can find it and report what changed

0 commit comments

Comments
 (0)