diff --git a/Cargo.lock b/Cargo.lock index f82b067d..bf305491 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3247,6 +3247,7 @@ dependencies = [ "fspy", "futures-util", "nix 0.30.1", + "once_cell", "owo-colors", "petgraph", "rayon", diff --git a/Cargo.toml b/Cargo.toml index f48631cb..361911db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,8 +76,10 @@ futures-util = "0.3.31" insta = "1.44.3" libc = "0.2.172" memmap2 = "0.9.7" +monostate = "1.0.2" nix = { version = "0.30.1", features = ["dir"] } ntapi = "0.4.1" +once_cell = "1.19" os_str_bytes = "7.1.1" ouroboros = "0.18.5" owo-colors = "4.1.0" diff --git a/crates/vite_task/Cargo.toml b/crates/vite_task/Cargo.toml index c40708da..e319b82a 100644 --- a/crates/vite_task/Cargo.toml +++ b/crates/vite_task/Cargo.toml @@ -21,6 +21,7 @@ derive_more = { workspace = true, features = ["from"] } diff-struct = { workspace = true } fspy = { workspace = true } futures-util = { workspace = true } +once_cell = { workspace = true } owo-colors = { workspace = true } petgraph = { workspace = true } rayon = { workspace = true } diff --git a/crates/vite_task/src/cli/mod.rs b/crates/vite_task/src/cli/mod.rs index 72b6afaa..e31c7d9a 100644 --- a/crates/vite_task/src/cli/mod.rs +++ b/crates/vite_task/src/cli/mod.rs @@ -6,17 +6,37 @@ use vite_str::Str; use vite_task_graph::{TaskSpecifier, query::TaskQueryKind}; use vite_task_plan::plan_request::{PlanOptions, PlanRequest, QueryPlanRequest}; -/// Represents the CLI arguments handled by vite-task, including both built-in and custom subcommands. +/// Represents the CLI arguments handled by vite-task, including both built-in (like run) and custom subcommands (like lint). #[derive(Debug)] pub struct TaskCLIArgs { pub(crate) original: Arc<[Str]>, pub(crate) parsed: ParsedTaskCLIArgs, } +impl TaskCLIArgs { + /// Inspect the custom subcommand (like lint/install). Returns `None` if it's built-in subcommand + /// The caller should not use this method to actually handle the custom subcommand. Instead, it should + /// private TaskSynthesizer to Session so that vite-task can handle custom subcommands consistently from + /// both direct CLI invocations and invocations in task scripts. + /// + /// This method is provided only to make it possible for the caller to behave differently BEFORE and AFTER the session. + /// For example, vite+ needs this method to skip auto-install when the custom subcommand is already `install`. + pub fn custom_subcommand(&self) -> Option<&CustomSubcommand> { + match &self.parsed { + ParsedTaskCLIArgs::BuiltIn(_) => None, + ParsedTaskCLIArgs::Custom(custom) => Some(custom), + } + } +} + +/// Represents the overall CLI arguments, containing three kinds of subcommands: +/// 1. Built-in subcommands handled by vite-task (like run) +/// 2. Custom subcommands handled by vite-task with the help of TaskSyntheizer (like lint) +/// 3. Custom subcommands not handled by vite-task (like vite+ commands without cache) pub enum CLIArgs { - /// vite-task's own built-in subcommands + /// Subcommands handled by vite task, including built-in (like run) and custom (like lint) Task(TaskCLIArgs), - /// custom subcommands provided by vite+ + /// Custom subcommands not handled by vite task (like vite+ commands without cache) NonTask(NonTaskSubcommand), } diff --git a/crates/vite_task/src/session/cache/mod.rs b/crates/vite_task/src/session/cache/mod.rs index 72df4bd5..d43a01db 100644 --- a/crates/vite_task/src/session/cache/mod.rs +++ b/crates/vite_task/src/session/cache/mod.rs @@ -2,7 +2,7 @@ pub mod display; -use std::{fmt::Display, io::Write, sync::Arc, time::Duration}; +use std::{fmt::Display, fs::File, io::Write, sync::Arc, time::Duration}; use bincode::{Decode, Encode, decode_from_slice, encode_to_vec}; // Re-export display functions for convenience @@ -71,6 +71,11 @@ impl ExecutionCache { tracing::info!("Creating task cache directory at {:?}", path); std::fs::create_dir_all(path)?; + // Use file lock to prevent race conditions when multiple processes initialize the database + let lock_path = path.join("db_open.lock"); + let lock_file = File::create(lock_path.as_path())?; + lock_file.lock()?; + let db_path = path.join("cache.db"); let conn = Connection::open(db_path.as_path())?; conn.execute_batch("PRAGMA journal_mode=WAL;")?; @@ -101,6 +106,7 @@ impl ExecutionCache { } } } + // Lock is released when lock_file is dropped Ok(Self { conn: Mutex::new(conn) }) } diff --git a/crates/vite_task/src/session/execute/mod.rs b/crates/vite_task/src/session/execute/mod.rs index 502c6adc..d002333f 100644 --- a/crates/vite_task/src/session/execute/mod.rs +++ b/crates/vite_task/src/session/execute/mod.rs @@ -4,7 +4,7 @@ pub mod spawn; use std::sync::Arc; use futures_util::FutureExt; -use petgraph::{algo::toposort, graph::DiGraph}; +use petgraph::{algo::toposort, stable_graph::StableGraph}; use vite_path::AbsolutePath; use vite_task_plan::{ ExecutionItemKind, ExecutionPlan, LeafExecutionKind, SpawnExecution, TaskExecution, @@ -46,9 +46,9 @@ impl ExecutionContext<'_> { ) -> Result<(), ExecutionAborted> { match item_kind { ExecutionItemKind::Expanded(graph) => { - // clone for reversing edges and removing nodes - let mut graph: DiGraph<&TaskExecution, (), ExecutionIx> = - graph.map(|_, task_execution| task_execution, |_, ()| ()); + // Use StableGraph to preserve node indices during removal + let mut graph: StableGraph<&TaskExecution, (), _, ExecutionIx> = + graph.map(|_, task_execution| task_execution, |_, ()| ()).into(); // To be consistent with the package graph in vite_package_manager and the dependency graph definition in Wikipedia // https://en.wikipedia.org/wiki/Dependency_graph, we construct the graph with edges from dependents to dependencies @@ -339,10 +339,24 @@ impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> { plan: ExecutionPlan, mut reporter: Box, ) -> Result<(), ExitStatus> { + // Lazily initialize the cache on first execution + let cache = match self.cache() { + Ok(cache) => cache, + Err(err) => { + reporter.handle_event(ExecutionEvent { + execution_id: ExecutionId::zero(), + kind: ExecutionEventKind::Error { + message: format!("Failed to initialize cache: {err}"), + }, + }); + return Err(ExitStatus(1)); + } + }; + let mut execution_context = ExecutionContext { event_handler: &mut *reporter, current_execution_id: ExecutionId::zero(), - cache: &self.cache, + cache, cache_base_path: &self.workspace_path, }; diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index 061b6714..c06ce44e 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -10,6 +10,7 @@ use cache::ExecutionCache; pub use cache::{CacheMiss, FingerprintMismatch}; use clap::{Parser, Subcommand}; pub use event::ExecutionEvent; +use once_cell::sync::OnceCell; pub use reporter::{LabeledReporter, Reporter}; use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_str::Str; @@ -142,7 +143,10 @@ pub struct Session<'a, CustomSubcommand> { plan_request_parser: PlanRequestParser<'a, CustomSubcommand>, - cache: ExecutionCache, + /// Cache is lazily initialized to avoid SQLite race conditions when multiple + /// processes (e.g., parallel `vite lib` commands) start simultaneously. + cache: OnceCell, + cache_path: AbsolutePathBuf, } fn get_cache_path_of_workspace(workspace_root: &AbsolutePath) -> AbsolutePathBuf { @@ -181,13 +185,7 @@ impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> { let workspace_node_modules_bin = workspace_root.path.join("node_modules").join(".bin"); prepend_path_env(&mut envs, &workspace_node_modules_bin)?; - if !cache_path.as_path().exists() - && let Some(cache_dir) = cache_path.as_path().parent() - { - tracing::info!("Creating task cache directory at {}", cache_dir.display()); - std::fs::create_dir_all(cache_dir)?; - } - let cache = ExecutionCache::load_from_path(cache_path)?; + // Cache is lazily initialized on first access to avoid SQLite race conditions Ok(Self { workspace_path: Arc::clone(&workspace_root.path), lazy_task_graph: LazyTaskGraph::Uninitialized { @@ -197,12 +195,16 @@ impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> { envs: Arc::new(envs), cwd, plan_request_parser: PlanRequestParser { task_synthesizer: callbacks.task_synthesizer }, - cache, + cache: OnceCell::new(), + cache_path, }) } - pub fn cache(&self) -> &ExecutionCache { - &self.cache + /// Lazily initializes and returns the execution cache. + /// The cache is only created when first accessed to avoid SQLite race conditions + /// when multiple processes start simultaneously. + pub fn cache(&self) -> anyhow::Result<&ExecutionCache> { + self.cache.get_or_try_init(|| ExecutionCache::load_from_path(self.cache_path.clone())) } pub fn workspace_path(&self) -> Arc { diff --git a/crates/vite_task_bin/Cargo.toml b/crates/vite_task_bin/Cargo.toml index 2e41f42c..e7234327 100644 --- a/crates/vite_task_bin/Cargo.toml +++ b/crates/vite_task_bin/Cargo.toml @@ -14,7 +14,7 @@ path = "src/main.rs" anyhow = { workspace = true } async-trait = { workspace = true } clap = { workspace = true, features = ["derive"] } -monostate = "1.0.2" +monostate = { workspace = true } tokio = { workspace = true, features = ["full"] } vite_path = { workspace = true } vite_str = { workspace = true } diff --git a/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/package.json b/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/package.json new file mode 100644 index 00000000..6b676d35 --- /dev/null +++ b/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "lint": "vite run a#lint" + } +} diff --git a/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/packages/a/package.json b/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/packages/a/package.json new file mode 100644 index 00000000..6a08f0a8 --- /dev/null +++ b/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/packages/a/package.json @@ -0,0 +1,6 @@ +{ + "name": "a", + "scripts": { + "lint": "vite lint" + } +} diff --git a/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/pnpm-workspace.yaml b/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/pnpm-workspace.yaml new file mode 100644 index 00000000..18ec407e --- /dev/null +++ b/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/snapshots.toml b/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/snapshots.toml new file mode 100644 index 00000000..786c3fe6 --- /dev/null +++ b/crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage/snapshots.toml @@ -0,0 +1,3 @@ +[[plan]] +name = "synthetic-in-subpackage" +args = ["run", "lint"] diff --git a/crates/vite_task_bin/tests/test_snapshots/snapshots/test_snapshots__query - synthetic-in-subpackage@synthetic-in-subpackage.snap b/crates/vite_task_bin/tests/test_snapshots/snapshots/test_snapshots__query - synthetic-in-subpackage@synthetic-in-subpackage.snap new file mode 100644 index 00000000..57ca5097 --- /dev/null +++ b/crates/vite_task_bin/tests/test_snapshots/snapshots/test_snapshots__query - synthetic-in-subpackage@synthetic-in-subpackage.snap @@ -0,0 +1,114 @@ +--- +source: crates/vite_task_bin/tests/test_snapshots/main.rs +expression: "&plan_json" +input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage +--- +{ + "root_node": { + "Expanded": [ + { + "key": [ + "/", + "lint" + ], + "node": { + "task_display": { + "package_name": "", + "task_name": "lint", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "", + "task_name": "lint", + "package_path": "/" + }, + "command": "vite run a#lint", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Expanded": [ + { + "key": [ + "/packages/a", + "lint" + ], + "node": { + "task_display": { + "package_name": "a", + "task_name": "lint", + "package_path": "/packages/a" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "a", + "task_name": "lint", + "package_path": "/packages/a" + }, + "command": "vite lint", + "and_item_index": null, + "cwd": "/packages/a" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "packages/a", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "oxlint" + } + }, + "args": [], + "env_fingerprints": { + "fingerprinted_envs": {}, + "pass_through_env_config": [ + "" + ] + }, + "fingerprint_ignores": null + }, + "execution_cache_key": { + "kind": { + "UserTask": { + "task_name": "lint", + "and_item_index": 0, + "extra_args": [] + } + }, + "origin_path": "packages/a" + } + }, + "spawn_command": { + "program_path": "/test_bins/node_modules/.bin/oxlint", + "args": [], + "all_envs": { + "NO_COLOR": "1", + "PATH": "/packages/a/node_modules/.bin:/node_modules/.bin:/test_bins/node_modules/.bin" + }, + "cwd": "/packages/a" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ] + } + } + ] + }, + "neighbors": [] + } + ] + } +} diff --git a/crates/vite_task_bin/tests/test_snapshots/snapshots/test_snapshots__task graph@synthetic-in-subpackage.snap b/crates/vite_task_bin/tests/test_snapshots/snapshots/test_snapshots__task graph@synthetic-in-subpackage.snap new file mode 100644 index 00000000..ac2dba07 --- /dev/null +++ b/crates/vite_task_bin/tests/test_snapshots/snapshots/test_snapshots__task graph@synthetic-in-subpackage.snap @@ -0,0 +1,63 @@ +--- +source: crates/vite_task_bin/tests/test_snapshots/main.rs +expression: task_graph_json +input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage +--- +[ + { + "key": [ + "/", + "lint" + ], + "node": { + "task_display": { + "package_name": "", + "task_name": "lint", + "package_path": "/" + }, + "resolved_config": { + "command": "vite run a#lint", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "pass_through_envs": [ + "" + ] + } + } + } + } + }, + "neighbors": [] + }, + { + "key": [ + "/packages/a", + "lint" + ], + "node": { + "task_display": { + "package_name": "a", + "task_name": "lint", + "package_path": "/packages/a" + }, + "resolved_config": { + "command": "vite lint", + "resolved_options": { + "cwd": "/packages/a", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "pass_through_envs": [ + "" + ] + } + } + } + } + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_graph/Cargo.toml b/crates/vite_task_graph/Cargo.toml index cc1a8933..80ef4955 100644 --- a/crates/vite_task_graph/Cargo.toml +++ b/crates/vite_task_graph/Cargo.toml @@ -10,7 +10,7 @@ rust-version.workspace = true anyhow = { workspace = true } async-trait = { workspace = true } clap = { workspace = true, features = ["derive"] } -monostate = "1.0.2" +monostate = { workspace = true } petgraph = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/crates/vite_task_plan/src/context.rs b/crates/vite_task_plan/src/context.rs index fb4e38e5..4dd51e07 100644 --- a/crates/vite_task_plan/src/context.rs +++ b/crates/vite_task_plan/src/context.rs @@ -95,10 +95,6 @@ impl<'a> PlanContext<'a> { } } - pub fn cwd(&self) -> &Arc { - &self.cwd - } - pub fn envs(&self) -> &HashMap, Arc> { &self.envs } diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index ef79b87d..53be6de3 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -189,7 +189,7 @@ async fn plan_task_as_execution_node( &and_item.envs, synthetic_plan_request, Some(task_execution_cache_key), - context.cwd(), + &cwd, ) .with_plan_context(&context)?; ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution))