Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 `IndexedTaskGraph::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
41 changes: 29 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,15 @@ 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;
// Allocated once and shared across every dep-only task's context below,
// instead of calling `Arc::new([])` inside the per-task loop.
let empty_extra_args: Arc<[Str]> = Arc::from([]);
context.set_parent_query(Arc::clone(&query));

// Query matching tasks from the task graph.
Expand All @@ -715,35 +724,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::clone(&empty_extra_args));
}
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 +774,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