Skip to content

Commit 947aaa2

Browse files
wan9chiclaude
andcommitted
fix(cache): resolve and validate tracked envs from the session env snapshot
The IPC Recorder's env map and PostRunFingerprint::validate both read the live process env (std::env::vars_os / var_os). Tasks' envs come from the plan; std::env::vars_os is only meant to bootstrap the session snapshot (Session.envs) at init. Thread that snapshot through ExecutionContext / execute_spawn into Recorder::new (now Arc-shared) and through ExecutionCache::try_hit into validate, so getEnv/getEnvs and the tracked-env lookup validation resolve against the same map the plan was built from — and tests can inject envs via Session::init_with. Document the rule in CLAUDE.md (Code Constraints → Environment Variables). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent dadf8d5 commit 947aaa2

5 files changed

Lines changed: 53 additions & 24 deletions

File tree

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:

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

Lines changed: 11 additions & 2 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};
@@ -329,12 +332,16 @@ impl ExecutionCache {
329332

330333
/// Try to hit cache by looking up the cache entry key and validating inputs.
331334
/// Returns `Ok(Ok(cache_value))` on cache hit, `Ok(Err(cache_miss))` on miss.
335+
///
336+
/// `envs` is the session env snapshot the run's plan was bootstrapped from;
337+
/// tracked-env validation resolves against it, never the live process env.
332338
#[tracing::instrument(level = "debug", skip_all)]
333339
pub async fn try_hit(
334340
&self,
335341
cache_metadata: &CacheMetadata,
336342
globbed_inputs: &BTreeMap<RelativePathBuf, u64>,
337343
workspace_root: &AbsolutePath,
344+
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
338345
) -> anyhow::Result<Result<CacheEntryValue, CacheMiss>> {
339346
let spawn_fingerprint = &cache_metadata.spawn_fingerprint;
340347
let execution_cache_key = &cache_metadata.execution_cache_key;
@@ -351,7 +358,9 @@ impl ExecutionCache {
351358
}
352359

353360
// Validate post-run fingerprint (inferred inputs + tracked envs)
354-
if let Some(mismatch) = cache_value.post_run_fingerprint.validate(workspace_root)? {
361+
if let Some(mismatch) =
362+
cache_value.post_run_fingerprint.validate(workspace_root, envs)?
363+
{
355364
return Ok(Err(CacheMiss::FingerprintMismatch(mismatch.into())));
356365
}
357366
// Associate the execution key to the cache entry key if not already,

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

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
66
use std::{
77
collections::BTreeMap,
8+
ffi::OsStr,
89
fs::File,
910
io::{self, BufRead},
1011
sync::Arc,
1112
};
1213

1314
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
15+
use rustc_hash::FxHashMap;
1416
use serde::{Deserialize, Serialize};
1517
use vite_path::{AbsolutePath, RelativePathBuf};
1618
use vite_str::Str;
@@ -38,13 +40,13 @@ pub struct PostRunFingerprint {
3840
/// Env vars observed via runner-aware IPC `getEnv` with `tracked: true`.
3941
/// Key is the env name; value is the env value at execution time (or
4042
/// `None` if unset). Validated at cache lookup by comparing against the
41-
/// current parent env.
43+
/// session env snapshot.
4244
pub tracked_envs: BTreeMap<Str, Option<Str>>,
4345

4446
/// Glob-pattern env queries (`getEnvs`) made with `tracked: true`.
4547
/// Outer key is the glob pattern, inner map is the match-set at
4648
/// execution time (name → value). Validated at cache lookup by
47-
/// re-matching against the current parent env and comparing the
49+
/// re-matching against the session env snapshot and comparing the
4850
/// resulting set.
4951
pub tracked_env_globs: BTreeMap<Str, BTreeMap<Str, Str>>,
5052
}
@@ -125,10 +127,19 @@ impl PostRunFingerprint {
125127
Ok(Self { inferred_inputs, tracked_envs, tracked_env_globs })
126128
}
127129

128-
/// Validates the fingerprint against current filesystem and env state.
129-
/// Returns `Some(mismatch)` on the first divergence, `None` if all valid.
130+
/// Validates the fingerprint against the current filesystem state and the
131+
/// session env snapshot. Returns `Some(mismatch)` on the first divergence,
132+
/// `None` if all valid.
133+
///
134+
/// `envs` must be the same env map the run's plan was bootstrapped from
135+
/// (`Session.envs`) — the map that served `getEnv`/`getEnvs` at record
136+
/// time — never a fresh read of the process env.
130137
#[tracing::instrument(level = "debug", skip_all, name = "validate_post_run_fingerprint")]
131-
pub fn validate(&self, base_dir: &AbsolutePath) -> anyhow::Result<Option<PostRunMismatch>> {
138+
pub fn validate(
139+
&self,
140+
base_dir: &AbsolutePath,
141+
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
142+
) -> anyhow::Result<Option<PostRunMismatch>> {
132143
let input_mismatch = self.inferred_inputs.par_iter().find_map_any(
133144
|(input_relative_path, path_fingerprint)| {
134145
let input_full_path = Arc::<AbsolutePath>::from(base_dir.join(input_relative_path));
@@ -162,22 +173,22 @@ impl PostRunFingerprint {
162173
return result.map(Some);
163174
}
164175

165-
// Validate tracked envs against the current parent env.
176+
// Validate tracked envs against the session env snapshot.
166177
for (name, stored_value) in &self.tracked_envs {
167178
let current_value =
168-
std::env::var_os(name.as_str()).and_then(|v| v.to_str().map(Str::from));
179+
envs.get(OsStr::new(name.as_str())).and_then(|v| v.to_str().map(Str::from));
169180
if let Some(mismatch) =
170181
EnvMismatch::compare(name, stored_value.as_ref(), current_value.as_ref())
171182
{
172183
return Ok(Some(PostRunMismatch::TrackedEnvChanged(mismatch)));
173184
}
174185
}
175186

176-
// Validate tracked env globs: re-enumerate the parent env for each
177-
// pattern and report the first entry that diverges from the stored
178-
// match-set.
187+
// Validate tracked env globs: re-expand each pattern over the session
188+
// env snapshot and report the first entry that diverges from the
189+
// stored match-set.
179190
for (pattern, stored_matches) in &self.tracked_env_globs {
180-
let current_matches = match_env_glob(pattern.as_str())?;
191+
let current_matches = match_env_glob(pattern.as_str(), envs)?;
181192
if let Some(mismatch) = first_env_glob_mismatch(stored_matches, &current_matches) {
182193
return Ok(Some(PostRunMismatch::TrackedEnvGlobChanged {
183194
pattern: pattern.clone(),
@@ -190,18 +201,22 @@ impl PostRunFingerprint {
190201
}
191202
}
192203

193-
/// Build the current match-set for `pattern` by enumerating
194-
/// `std::env::vars_os()` and keeping UTF-8 names whose representation matches
195-
/// the glob. Mirrors the server-side match (see
196-
/// `vite_task_server::Recorder::get_envs`).
197-
fn match_env_glob(pattern: &str) -> anyhow::Result<BTreeMap<Str, Str>> {
204+
/// Build the current match-set for `pattern` by enumerating the given env
205+
/// snapshot and keeping UTF-8 names whose representation matches the glob.
206+
/// Mirrors the server-side match (see `vite_task_server::Recorder::get_envs`),
207+
/// which resolved against the same map at record time.
208+
fn match_env_glob(
209+
pattern: &str,
210+
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
211+
) -> anyhow::Result<BTreeMap<Str, Str>> {
198212
let glob = vite_glob::env::EnvGlob::new(pattern)?;
199-
Ok(std::env::vars_os()
213+
Ok(envs
214+
.iter()
200215
.filter_map(|(name, value)| {
201-
let name_str = name.to_str()?.to_owned();
202-
let value_str = value.to_str()?.to_owned();
203-
if glob.is_match(name_str.as_str()) {
204-
Some((Str::from(name_str.as_str()), Str::from(value_str.as_str())))
216+
let name_str = name.to_str()?;
217+
let value_str = value.to_str()?;
218+
if glob.is_match(name_str) {
219+
Some((Str::from(name_str), Str::from(value_str)))
205220
} else {
206221
None
207222
}

crates/vite_task/src/session/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ impl<'a> Session<'a> {
726726
&spawn_execution,
727727
cache,
728728
&self.workspace_path,
729+
&self.envs,
729730
&self.cache_path,
730731
self.program_name.as_str(),
731732
tokio_util::sync::CancellationToken::new(),

docs/runner-task-ipc/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Report information from the tools to the runner, to help runner cache results wi
2020
Workflow:
2121

2222
1. For each spawn execution, `vite_task` starts an IPC server via `vite_task_server::serve` and passes the server's connection info plus the path to a materialized node addon into the child's env.
23-
2. The task process loads the addon through `@voidzero-dev/vite-task-client` and reports back over IPC: which reads/writes to ignore, which envs it needs (returned by the runner from the spawn's resolved env map), and whether to disable caching.
23+
2. The task process loads the addon through `@voidzero-dev/vite-task-client` and reports back over IPC: which reads/writes to ignore, which envs it needs (resolved by the runner from the session env snapshot its plan was bootstrapped from), and whether to disable caching.
2424
3. When the task exits, the server drains and hands its collected reports back to `vite_task`, which feeds them into the cache layer.
2525

2626
Crate / package responsibilities:

0 commit comments

Comments
 (0)