Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

- **Changed** Arguments passed after a task name (e.g. `vp run test some-filter`) are now forwarded only to that task. Tasks pulled in via `dependsOn` no longer receive them ([#324](https://github.com/voidzero-dev/vite-task/issues/324))
- **Fixed** Windows file access tracking no longer panics when a task touches malformed paths that cannot be represented as workspace-relative inputs ([#330](https://github.com/voidzero-dev/vite-task/pull/330))
- **Fixed** `vp run --cache` now supports running without a task specifier and opens the interactive task selector, matching bare `vp run` behavior ([#312](https://github.com/voidzero-dev/vite-task/pull/313))
- **Fixed** Ctrl-C now prevents future tasks from being scheduled and prevents caching of in-flight task results ([#309](https://github.com/voidzero-dev/vite-task/pull/309))
Expand Down
39 changes: 32 additions & 7 deletions crates/vite_task_graph/src/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,26 @@ use crate::{IndexedTaskGraph, TaskDependencyType, TaskId, TaskNodeIndex};

/// A task execution graph queried from a `TaskQuery`.
///
/// Nodes are `TaskNodeIndex` values into the full `TaskGraph`.
/// Nodes in `graph` are `TaskNodeIndex` values into the full `TaskGraph`.
/// Edges represent the final dependency relationships between tasks (no weights).
pub type TaskExecutionGraph = DiGraphMap<TaskNodeIndex, ()>;
///
/// `requested` is the subset of nodes the user typed on the CLI — i.e. the
/// nodes added by `map_subgraph_to_tasks` (stage 2), not the ones reached
/// only via `dependsOn` expansion in [`Self::add_dependencies`] (stage 3).
///
/// For example, given `test` with `dependsOn: ["build"]` and the command
/// `vp run test some-filter`:
///
/// - `graph` contains both `test` and `build` with an edge between them.
/// - `requested` contains only `test`.
///
/// The planner uses this distinction to forward `some-filter` to `test`
/// while running `build` with no extra args.
#[derive(Debug, Default, Clone)]
pub struct TaskExecutionGraph {
pub graph: DiGraphMap<TaskNodeIndex, ()>,
pub requested: FxHashSet<TaskNodeIndex>,
}
Comment thread
branchseer marked this conversation as resolved.

/// A query for which tasks to run.
///
Expand Down Expand Up @@ -167,13 +184,17 @@ impl IndexedTaskGraph {
// Map remaining nodes and their edges to task nodes.
// Every node still in `subgraph` is in `pkg_to_task`; the index operator
// panics on a missing key — that would be a bug in the loop above.
//
// All nodes added here are explicitly-requested tasks, so they are
// inserted into both the inner graph and the `requested` set.
for &task_idx in pkg_to_task.values() {
execution_graph.add_node(task_idx);
execution_graph.graph.add_node(task_idx);
execution_graph.requested.insert(task_idx);
}
for (src, dst, ()) in subgraph.all_edges() {
let st = pkg_to_task[&src];
let dt = pkg_to_task[&dst];
execution_graph.add_edge(st, dt, ());
execution_graph.graph.add_edge(st, dt, ());
}
}

Expand All @@ -187,9 +208,13 @@ impl IndexedTaskGraph {
execution_graph: &mut TaskExecutionGraph,
mut filter_edge: impl FnMut(TaskDependencyType) -> bool,
) {
let mut frontier: FxHashSet<TaskNodeIndex> = execution_graph.nodes().collect();
let mut frontier: FxHashSet<TaskNodeIndex> = execution_graph.graph.nodes().collect();

// Continue until no new nodes are added to the frontier.
//
// Nodes added here are dependency-only tasks and must NOT be marked as
// `requested` — the planner uses that distinction to decide whether to
// forward CLI extra args to a task.
while !frontier.is_empty() {
let mut next_frontier = FxHashSet::<TaskNodeIndex>::default();

Expand All @@ -198,8 +223,8 @@ impl IndexedTaskGraph {
let to_node = edge_ref.target();
let dep_type = *edge_ref.weight();
if filter_edge(dep_type) {
let is_new = !execution_graph.contains_node(to_node);
execution_graph.add_edge(from_node, to_node, ());
let is_new = !execution_graph.graph.contains_node(to_node);
execution_graph.graph.add_edge(from_node, to_node, ());
if is_new {
next_frontier.insert(to_node);
}
Expand Down
38 changes: 26 additions & 12 deletions crates/vite_task_plan/src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@ fn plan_spawn_execution(
/// `vp run build` produces a different query than the script's `vp run -r build`,
/// so the skip rule doesn't fire, but the prune rule catches root in the result).
/// Like the skip rule, extra args don't affect this — only the `TaskQuery` matters.
#[expect(clippy::too_many_lines, reason = "sequential planning steps are clearer in one function")]
pub async fn plan_query_request(
query: Arc<TaskQuery>,
plan_options: PlanOptions,
Expand Down Expand Up @@ -696,7 +697,12 @@ pub async fn plan_query_request(

let parallel = plan_options.parallel;

context.set_extra_args(plan_options.extra_args);
// Extra args are applied per-task below, not globally on the context.
// Tasks explicitly requested by the query receive `extra_args`; tasks
// only reached via `dependsOn` expansion receive an empty slice so that
// caller-specific CLI args don't pollute dependency tasks.
// See https://github.com/voidzero-dev/vite-task/issues/324.
let extra_args = plan_options.extra_args;
context.set_parent_query(Arc::clone(&query));

// Query matching tasks from the task graph.
Expand All @@ -715,35 +721,43 @@ pub async fn plan_query_request(
// This handles cases like root `"build": "vp run build"` — the root's build
// task is in the result but expanding it would recurse, so we remove it and
// reconnect its predecessors directly to its successors.
let pruned_task = context.expanding_task().filter(|t| task_node_index_graph.contains_node(*t));
let pruned_task =
context.expanding_task().filter(|t| task_node_index_graph.graph.contains_node(*t));

let mut execution_node_indices_by_task_index =
FxHashMap::<TaskNodeIndex, ExecutionNodeIndex>::with_capacity_and_hasher(
task_node_index_graph.node_count(),
task_node_index_graph.graph.node_count(),
rustc_hash::FxBuildHasher,
);

// Build the inner DiGraph first, then validate acyclicity at the end.
let mut inner_graph = InnerExecutionGraph::with_capacity(
task_node_index_graph.node_count(),
task_node_index_graph.edge_count(),
task_node_index_graph.graph.node_count(),
task_node_index_graph.graph.edge_count(),
);

// Plan each task node as execution nodes, skipping the pruned task
for task_index in task_node_index_graph.nodes() {
for task_index in task_node_index_graph.graph.nodes() {
if Some(task_index) == pruned_task {
continue;
}
let task_execution = plan_task_as_execution_node(task_index, context.duplicate(), true)
.boxed_local()
.await?;
let mut task_context = context.duplicate();
// Only the explicitly requested tasks receive CLI extra args.
// Dep-only tasks (pulled in via `dependsOn`) run with empty extras.
if task_node_index_graph.requested.contains(&task_index) {
task_context.set_extra_args(Arc::clone(&extra_args));
} else {
task_context.set_extra_args(Arc::new([]));
}
Comment thread
branchseer marked this conversation as resolved.
let task_execution =
plan_task_as_execution_node(task_index, task_context, true).boxed_local().await?;
execution_node_indices_by_task_index
.insert(task_index, inner_graph.add_node(task_execution));
}

// Add edges between execution nodes according to task dependencies,
// skipping edges involving the pruned task.
for (from_task_index, to_task_index, ()) in task_node_index_graph.all_edges() {
for (from_task_index, to_task_index, ()) in task_node_index_graph.graph.all_edges() {
if Some(from_task_index) == pruned_task || Some(to_task_index) == pruned_task {
continue;
}
Expand All @@ -757,9 +771,9 @@ pub async fn plan_query_request(
// Reconnect through the pruned node: wire each predecessor directly to each successor.
if let Some(pruned) = pruned_task {
let preds: Vec<_> =
task_node_index_graph.neighbors_directed(pruned, Direction::Incoming).collect();
task_node_index_graph.graph.neighbors_directed(pruned, Direction::Incoming).collect();
let succs: Vec<_> =
task_node_index_graph.neighbors_directed(pruned, Direction::Outgoing).collect();
task_node_index_graph.graph.neighbors_directed(pruned, Direction::Outgoing).collect();
for &pred in &preds {
for &succ in &succs {
if let (Some(&pe), Some(&se)) = (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "@test/extra-args-not-forwarded-to-depends-on"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Tests that extra args (`vt run test some-filter`) are forwarded only to the
# explicitly requested task (`test`), not to `dependsOn` tasks (`build`).
# https://github.com/voidzero-dev/vite-task/issues/324

[[plan]]
name = "extra args only reach requested task"
args = ["run", "test", "some-filter"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
source: crates/vite_task_plan/tests/plan_snapshots/main.rs
expression: "&plan_json"
info:
args:
- run
- test
- some-filter
input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/extra-args-not-forwarded-to-depends-on
---
{
"graph": [
{
"key": [
"<workspace>/",
"build"
],
"node": {
"task_display": {
"package_name": "@test/extra-args-not-forwarded-to-depends-on",
"task_name": "build",
"package_path": "<workspace>/"
},
"items": [
{
"execution_item_display": {
"task_display": {
"package_name": "@test/extra-args-not-forwarded-to-depends-on",
"task_name": "build",
"package_path": "<workspace>/"
},
"command": "vt tool print build",
"and_item_index": null,
"cwd": "<workspace>/"
},
"kind": {
"Leaf": {
"Spawn": {
"cache_metadata": {
"spawn_fingerprint": {
"cwd": "",
"program_fingerprint": {
"OutsideWorkspace": {
"program_name": "vtt"
}
},
"args": [
"print",
"build"
],
"env_fingerprints": {
"fingerprinted_envs": {},
"untracked_env_config": [
"<default untracked envs>"
]
}
},
"execution_cache_key": {
"UserTask": {
"task_name": "build",
"and_item_index": 0,
"extra_args": [],
"package_path": ""
}
},
"input_config": {
"includes_auto": true,
"positive_globs": [],
"negative_globs": []
}
},
"spawn_command": {
"program_path": "<tools>/vtt",
"args": [
"print",
"build"
],
"all_envs": {
"NO_COLOR": "1",
"PATH": "<workspace>/node_modules/.bin:<tools>"
},
"cwd": "<workspace>/"
}
}
}
}
}
]
},
"neighbors": []
},
{
"key": [
"<workspace>/",
"test"
],
"node": {
"task_display": {
"package_name": "@test/extra-args-not-forwarded-to-depends-on",
"task_name": "test",
"package_path": "<workspace>/"
},
"items": [
{
"execution_item_display": {
"task_display": {
"package_name": "@test/extra-args-not-forwarded-to-depends-on",
"task_name": "test",
"package_path": "<workspace>/"
},
"command": "vt tool print test some-filter",
"and_item_index": null,
"cwd": "<workspace>/"
},
"kind": {
"Leaf": {
"Spawn": {
"cache_metadata": {
"spawn_fingerprint": {
"cwd": "",
"program_fingerprint": {
"OutsideWorkspace": {
"program_name": "vtt"
}
},
"args": [
"print",
"test",
"some-filter"
],
"env_fingerprints": {
"fingerprinted_envs": {},
"untracked_env_config": [
"<default untracked envs>"
]
}
},
"execution_cache_key": {
"UserTask": {
"task_name": "test",
"and_item_index": 0,
"extra_args": [
"some-filter"
],
"package_path": ""
}
},
"input_config": {
"includes_auto": true,
"positive_globs": [],
"negative_globs": []
}
},
"spawn_command": {
"program_path": "<tools>/vtt",
"args": [
"print",
"test",
"some-filter"
],
"all_envs": {
"NO_COLOR": "1",
"PATH": "<workspace>/node_modules/.bin:<tools>"
},
"cwd": "<workspace>/"
}
}
}
}
}
]
},
"neighbors": [
[
"<workspace>/",
"build"
]
]
}
],
"concurrency_limit": 4
}
Loading
Loading