Skip to content

Commit 7bf34a6

Browse files
committed
fix(plan): resolve runner getEnv from spawn env context
Motivation: Runner-served getEnv values need to match the command context that planning and execution use, including command-prefix envs and enclosing nested vp run prefix envs. Serving only the filtered child env can hide undeclared prefix values from tools. Scope: Build on the spawn_envs naming cleanup by storing the unfiltered command env context on CacheMetadata for cached executions, then serve runner getEnv from that context. This keeps SpawnCommand focused on process-spawn data and intentionally does not add cache fingerprinting for getEnv reads. Verification: - UPDATE_SNAPSHOTS=1 cargo test -p vite_task_bin --test e2e_snapshots fetch_env_sees_command_prefix_env -- --ignored - UPDATE_SNAPSHOTS=1 cargo test -p vite_task_bin --test e2e_snapshots fetch_env_sees_intermediate_prefix_envs -- --ignored - cargo test -p vite_task_bin --test e2e_snapshots fetch_env_sees_command_prefix_env -- --ignored - cargo test -p vite_task_bin --test e2e_snapshots fetch_env_sees_intermediate_prefix_envs -- --ignored - cargo check -p vite_task_plan -p vite_task -p vite_task_bin - cargo test -p vite_task_plan envs::tests - cargo test -p vite_task_plan --test plan_snapshots
1 parent 42cd212 commit 7bf34a6

9 files changed

Lines changed: 104 additions & 21 deletions

File tree

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

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ use std::{
1818
};
1919

2020
use futures_util::future::LocalBoxFuture;
21-
use rustc_hash::FxHashMap;
2221
use tokio_util::sync::CancellationToken;
2322
use vite_path::{AbsolutePath, RelativePathBuf};
2423
use vite_task_ipc_shared::NODE_CLIENT_PATH_ENV_NAME;
@@ -157,7 +156,6 @@ impl<'a> ExecutionMode<'a> {
157156
cache_metadata: Option<&'a CacheMetadata>,
158157
stdio_config: StdioConfig,
159158
globbed_inputs: BTreeMap<RelativePathBuf, u64>,
160-
envs: &BTreeMap<Arc<OsStr>, Arc<OsStr>>,
161159
) -> Result<Self, ExecutionError> {
162160
let Some(metadata) = cache_metadata else {
163161
return Ok(Self::Uncached {
@@ -184,10 +182,9 @@ impl<'a> ExecutionMode<'a> {
184182
// Bind runner IPC for every cached task. The merged cache-control API
185183
// (`disableCache`) must work even when a task uses explicit inputs and
186184
// therefore does not need fspy auto-input inference.
187-
let server_envs: FxHashMap<Arc<OsStr>, Arc<OsStr>> =
188-
envs.iter().map(|(name, value)| (Arc::clone(name), Arc::clone(value))).collect();
189185
let (ipc_envs, ServerHandle { driver, stop_accepting }) =
190-
serve(Recorder::new(Arc::new(server_envs))).map_err(ExecutionError::IpcServerBind)?;
186+
serve(Recorder::new(Arc::clone(&metadata.unfiltered_envs)))
187+
.map_err(ExecutionError::IpcServerBind)?;
191188
let tracking =
192189
Tracking { fspy, ipc_envs: ipc_envs.collect(), ipc_server_fut: driver, stop_accepting };
193190

@@ -409,16 +406,8 @@ async fn run(
409406
};
410407

411408
// 4. Fold the cache/fspy/stdio decisions into the typed mode.
412-
let mut mode = ExecutionMode::build(
413-
cache_metadata,
414-
stdio_config,
415-
globbed_inputs,
416-
// TODO(env-track): A later PR in this stack replaces this child env
417-
// map with the full planned env context so IPC can serve envs even
418-
// when they were not declared in `env` or `untrackedEnv`.
419-
&spawn_execution.spawn_command.spawn_envs,
420-
)
421-
.map_err(Report::failed)?;
409+
let mut mode = ExecutionMode::build(cache_metadata, stdio_config, globbed_inputs)
410+
.map_err(Report::failed)?;
422411

423412
// Measure end-to-end duration here — spawn() doesn't track time.
424413
let start = Instant::now();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { getEnv } from '@voidzero-dev/vite-task-client';
2+
3+
const servedA = getEnv('PREFIXED_A') ?? '(unset)';
4+
const servedB = getEnv('PREFIXED_B') ?? '(unset)';
5+
const ownA = process.env.PREFIXED_A ?? '(unset)';
6+
const ownB = process.env.PREFIXED_B ?? '(unset)';
7+
8+
console.log(`served A=${servedA} B=${servedB}`);
9+
console.log(`process.env A=${ownA} B=${ownB}`);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getEnv } from '@voidzero-dev/vite-task-client';
2+
3+
const served = getEnv('PREFIXED_ENV') ?? '(unset)';
4+
const own = process.env.PREFIXED_ENV ?? '(unset)';
5+
6+
console.log(`served=${served}`);
7+
console.log(`process.env=${own}`);

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,31 @@ steps = [
6565
],
6666
], comment = "runner serves PROBE_ENV from the spawned task env map" },
6767
]
68+
69+
[[e2e]]
70+
name = "fetch_env_sees_command_prefix_env"
71+
comment = """
72+
A command-prefixed env (`PREFIXED_ENV=from-command node ...`) is part of the spawn's full env context. `getEnv('PREFIXED_ENV')` must serve the same value the child process sees in `process.env`.
73+
"""
74+
ignore = true
75+
steps = [
76+
{ argv = [
77+
"vt",
78+
"run",
79+
"fetch-prefixed-env",
80+
], comment = "tool asks the runner for an env the command prefix sets" },
81+
]
82+
83+
[[e2e]]
84+
name = "fetch_env_sees_intermediate_prefix_envs"
85+
comment = """
86+
Prefix envs accumulate through nested runs: `outer-prefixed` is `PREFIXED_A=a vt run inner-prefixed`, and `inner-prefixed` is `PREFIXED_B=b node ...`. The inner tool's `getEnv` must see both, even though only the inner prefix reaches the child process env after cache env filtering.
87+
"""
88+
ignore = true
89+
steps = [
90+
{ argv = [
91+
"vt",
92+
"run",
93+
"outer-prefixed",
94+
], comment = "outer prefix env reaches the inner tool via the runner" },
95+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# fetch_env_sees_command_prefix_env
2+
3+
A command-prefixed env (`PREFIXED_ENV=from-command node ...`) is part of the spawn's full env context. `getEnv('PREFIXED_ENV')` must serve the same value the child process sees in `process.env`.
4+
5+
## `vt run fetch-prefixed-env`
6+
7+
tool asks the runner for an env the command prefix sets
8+
9+
```
10+
$ PREFIXED_ENV=from-command node scripts/fetch_prefixed_env.mjs
11+
served=from-command
12+
process.env=from-command
13+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# fetch_env_sees_intermediate_prefix_envs
2+
3+
Prefix envs accumulate through nested runs: `outer-prefixed` is `PREFIXED_A=a vt run inner-prefixed`, and `inner-prefixed` is `PREFIXED_B=b node ...`. The inner tool's `getEnv` must see both, even though only the inner prefix reaches the child process env after cache env filtering.
4+
5+
## `vt run outer-prefixed`
6+
7+
outer prefix env reaches the inner tool via the runner
8+
9+
```
10+
$ PREFIXED_B=b node scripts/fetch_intermediate_envs.mjs
11+
served A=a B=b
12+
process.env A=(unset) B=b
13+
```

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@
1414
// TODO(env-track): Remove this explicit env declaration down the stack and update the e2e name once runner-tracked env reads cover it.
1515
"env": ["PROBE_ENV"],
1616
"cache": true
17+
},
18+
"fetch-prefixed-env": {
19+
"command": "PREFIXED_ENV=from-command node scripts/fetch_prefixed_env.mjs",
20+
"cache": true
21+
},
22+
"outer-prefixed": {
23+
"command": "PREFIXED_A=a vt run inner-prefixed",
24+
"cache": true
25+
},
26+
"inner-prefixed": {
27+
"command": "PREFIXED_B=b node scripts/fetch_intermediate_envs.mjs",
28+
"cache": true
1729
}
1830
}
1931
}

crates/vite_task_plan/src/cache_metadata.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use std::sync::Arc;
1+
use std::{ffi::OsStr, sync::Arc};
22

3+
use rustc_hash::FxHashMap;
34
use serde::{Deserialize, Serialize};
45
use vite_path::RelativePathBuf;
56
use vite_str::{self, Str};
@@ -52,6 +53,16 @@ pub struct CacheMetadata {
5253
/// Resolved output configuration for cache restoration.
5354
/// Used at execution time to determine what output files to archive.
5455
pub output_config: ResolvedGlobConfig,
56+
57+
/// The unfiltered env context for runner-aware APIs. This is the planning
58+
/// context's envs before spawn-env filtering, including command prefix envs
59+
/// from this command and enclosing nested `vp run` expansions.
60+
///
61+
/// It is not passed to the spawned process; that remains
62+
/// `SpawnCommand::spawn_envs`. It is skipped in serialized plans because it
63+
/// mirrors the ambient environment and would make snapshots noisy.
64+
#[serde(skip)]
65+
pub unfiltered_envs: Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
5566
}
5667

5768
/// Fingerprint for spawn execution that affects caching.

crates/vite_task_plan/src/plan.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,11 @@ async fn plan_task_as_execution_node(
143143

144144
// This command's env context: extend the planning context with
145145
// the command's prefix envs (`FOO=1 tool ...`). Everything that
146-
// interprets the command reads this one context program
147-
// lookup, plan-request callbacks, and nested-run planning. The
148-
// per-and-item duplicate above scopes the extension to this
149-
// command, matching shell semantics (`FOO=1 a && b` does not
150-
// set `FOO` for `b`).
146+
// interprets the command reads this one context: program
147+
// lookup, plan-request callbacks, nested-run planning, and
148+
// cached execution metadata. The per-and-item duplicate
149+
// above scopes the extension to this command, matching shell
150+
// semantics (`FOO=1 a && b` does not set `FOO` for `b`).
151151
context.add_envs(and_item.envs.iter());
152152

153153
let mut args = and_item.args;
@@ -673,6 +673,7 @@ fn plan_spawn_execution(
673673
execution_cache_key,
674674
input_config: cache_config.input_config.clone(),
675675
output_config: cache_config.output_config.clone(),
676+
unfiltered_envs: Arc::clone(envs),
676677
});
677678
}
678679
}

0 commit comments

Comments
 (0)