Skip to content

Commit 5dfba0f

Browse files
committed
fix(plan): extend the command's env context with prefix envs before interpreting it
1 parent b01df20 commit 5dfba0f

11 files changed

Lines changed: 201 additions & 6 deletions

File tree

crates/vite_task_plan/src/plan.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,15 @@ async fn plan_task_as_execution_node(
141141
let mut context = context.duplicate();
142142
context.push_stack_frame(task_node_index, add_item_span.clone());
143143

144+
// This command's env context: extend the planning context with
145+
// 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`).
151+
context.add_envs(and_item.envs.iter());
152+
144153
let mut args = and_item.args;
145154
let extra_args = if is_last_command && and_item_index == and_item_count - 1 {
146155
// For the last and_item of the last command, append extra args from the plan context
@@ -247,8 +256,6 @@ async fn plan_task_as_execution_node(
247256

248257
// Save task name before consuming the request
249258
let task_name = query_plan_request.query.task_name.clone();
250-
// Add prefix envs to the context
251-
context.add_envs(and_item.envs.iter());
252259
let QueryPlanRequest { query, plan_options } = query_plan_request;
253260
let query = Arc::new(query);
254261
let nested_plan =
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"scripts": {
3+
"check": "PATH=__PLAN_SNAPSHOT_PREFIX_BIN__ sometool --from-prefix"
4+
}
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[[plan]]
2+
name = "prefix_path_resolves_program"
3+
args = ["run", "check"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// run check
2+
{
3+
"graph": [
4+
{
5+
"key": [
6+
"<workspace>/",
7+
"check"
8+
],
9+
"node": {
10+
"task_display": {
11+
"package_name": "",
12+
"task_name": "check",
13+
"package_path": "<workspace>/"
14+
},
15+
"items": [
16+
{
17+
"execution_item_display": {
18+
"task_display": {
19+
"package_name": "",
20+
"task_name": "check",
21+
"package_path": "<workspace>/"
22+
},
23+
"command": "PATH=<prefix-bin> sometool --from-prefix",
24+
"cwd": "<workspace>/"
25+
},
26+
"kind": {
27+
"Leaf": {
28+
"Spawn": {
29+
"cache_metadata": {
30+
"spawn_fingerprint": {
31+
"cwd": "",
32+
"program_fingerprint": {
33+
"OutsideWorkspace": {
34+
"program_name": "sometool"
35+
}
36+
},
37+
"args": [
38+
"--from-prefix"
39+
],
40+
"env_fingerprints": {
41+
"fingerprinted_envs": {
42+
"PATH": "<prefix-bin>"
43+
},
44+
"untracked_env_config": [
45+
"<default untracked envs>"
46+
]
47+
}
48+
},
49+
"execution_cache_key": {
50+
"UserTask": {
51+
"task_name": "check",
52+
"command_item_index": 0,
53+
"and_item_index": 0,
54+
"extra_args": [],
55+
"package_path": ""
56+
}
57+
},
58+
"input_config": {
59+
"includes_auto": true,
60+
"positive_globs": [],
61+
"negative_globs": []
62+
},
63+
"output_config": {
64+
"includes_auto": false,
65+
"positive_globs": [],
66+
"negative_globs": []
67+
}
68+
},
69+
"spawn_command": {
70+
"program_path": "<prefix-bin>/sometool",
71+
"args": [
72+
"--from-prefix"
73+
],
74+
"all_envs": {
75+
"FORCE_COLOR": "1",
76+
"PATH": "<prefix-bin>"
77+
},
78+
"cwd": "<workspace>/"
79+
}
80+
}
81+
}
82+
}
83+
}
84+
]
85+
},
86+
"neighbors": []
87+
}
88+
],
89+
"concurrency_limit": 4
90+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// task graph
2+
[
3+
{
4+
"key": [
5+
"<workspace>/",
6+
"check"
7+
],
8+
"node": {
9+
"task_display": {
10+
"package_name": "",
11+
"task_name": "check",
12+
"package_path": "<workspace>/"
13+
},
14+
"resolved_config": {
15+
"commands": [
16+
"PATH=<prefix-bin> sometool --from-prefix"
17+
],
18+
"resolved_options": {
19+
"cwd": "<workspace>/",
20+
"cache_config": {
21+
"env_config": {
22+
"fingerprinted_envs": [],
23+
"untracked_env": [
24+
"<default untracked envs>"
25+
]
26+
},
27+
"input_config": {
28+
"includes_auto": true,
29+
"positive_globs": [],
30+
"negative_globs": []
31+
},
32+
"output_config": {
33+
"includes_auto": false,
34+
"positive_globs": [],
35+
"negative_globs": []
36+
}
37+
}
38+
}
39+
},
40+
"source": "PackageJsonScript"
41+
},
42+
"neighbors": []
43+
}
44+
]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"cache": true
3+
}

crates/vite_task_plan/tests/plan_snapshots/main.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use vite_task_graph::display::TaskDisplay;
2020
use vite_task_plan::{ExecutionGraph, ExecutionItemKind};
2121
use vite_workspace::find_workspace_root;
2222

23+
const PREFIX_BIN_PLACEHOLDER: &str = "__PLAN_SNAPSHOT_PREFIX_BIN__";
24+
2325
/// Local parser wrapper for `BuiltInCommand`
2426
#[derive(Parser)]
2527
#[command(name = "vt")]
@@ -141,6 +143,26 @@ fn assert_identifier_like(kind: &str, value: &str) {
141143
);
142144
}
143145

146+
#[expect(
147+
clippy::disallowed_types,
148+
reason = "Path required for fixture file replacement; String required by std::fs APIs"
149+
)]
150+
fn replace_prefix_bin_placeholder(stage_path: &AbsolutePath, prefix_bin_dir: &std::path::Path) {
151+
let package_json = stage_path.join("package.json");
152+
let contents = match std::fs::read_to_string(package_json.as_path()) {
153+
Ok(contents) => contents,
154+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return,
155+
Err(err) => panic!("failed to read {}: {err}", package_json.as_path().display()),
156+
};
157+
if !contents.contains(PREFIX_BIN_PLACEHOLDER) {
158+
return;
159+
}
160+
161+
let prefix_bin = prefix_bin_dir.to_str().unwrap().cow_replace('\\', "/");
162+
let updated = contents.as_str().cow_replace(PREFIX_BIN_PLACEHOLDER, prefix_bin.as_ref());
163+
std::fs::write(package_json.as_path(), updated.as_ref()).unwrap();
164+
}
165+
144166
#[expect(
145167
clippy::disallowed_types,
146168
reason = "Path required for fixture handling; String required by std::fs::read and toml::from_slice"
@@ -186,15 +208,18 @@ fn run_case_inner(
186208
let stage_path = tmpdir.join(fixture_name);
187209
copy_dir(fixture_path, &stage_path).unwrap();
188210

211+
let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap());
212+
let prefix_bin_dir = manifest_dir.join("tests/plan_snapshots/prefix-bin");
213+
replace_prefix_bin_placeholder(&stage_path, &prefix_bin_dir);
214+
189215
let (workspace_root, _cwd) = find_workspace_root(&stage_path).unwrap();
190216

191217
assert_eq!(
192218
&stage_path, &*workspace_root.path,
193219
"folder '{fixture_name}' should be a workspace root"
194220
);
195221

196-
let fake_bin_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap())
197-
.join("tests/plan_snapshots/fake-bin");
222+
let fake_bin_dir = manifest_dir.join("tests/plan_snapshots/fake-bin");
198223
let combined_path =
199224
Arc::<OsStr>::from(std::ffi::OsString::from(fake_bin_dir.to_str().unwrap()));
200225

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# prefix-bin
2+
3+
Stub executables used by the prefix-env lookup snapshot fixture.
4+
5+
The fixture overwrites `PATH` with this directory from a command prefix env,
6+
so resolving these tools proves that prefix envs participate in executable lookup.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/sh
2+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@echo off

0 commit comments

Comments
 (0)