diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5cfa856..87b438f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +- **Fixed** Uncached tasks executed directly now preserve colors with `vp run --log labeled` and `--log grouped`, including Vitest coverage tables ([#997](https://github.com/voidzero-dev/vite-task/pull/997)). + - **Fixed** On Windows, tasks with automatic file-access tracking can now run Git Bash's `sh` and other MSYS2 or Cygwin programs, which aborted with `fatal error - MapViewOfFileEx 'shared.5'(…) Win32 error 487` ([#830](https://github.com/voidzero-dev/vite-task/issues/830), [#952](https://github.com/voidzero-dev/vite-task/pull/952)). - **Fixed** On Linux, cached tasks no longer fail to spawn child processes in restricted sandboxes, for example rootless bubblewrap that denies the `seccomp` syscall. When the `seccomp` filter for file-access tracking cannot be installed, the process runs untracked instead and the run is reported as not cached. The not-cached message now reads "automatic file tracking couldn't record everything this task did", which fits both this case and a full tracking channel ([#700](https://github.com/voidzero-dev/vite-task/issues/700), [#701](https://github.com/voidzero-dev/vite-task/pull/701)). - **Fixed** `vp run ` in a package that doesn't define the task now reports `Task "" not found` when another package's `vite.config.*` fails to load, instead of failing with that package's error. Showing the tasks to choose from loads every package's config, so the task selector or task list is skipped then ([#987](https://github.com/voidzero-dev/vite-task/pull/987)). diff --git a/crates/vt/src/session/execute/mod.rs b/crates/vt/src/session/execute/mod.rs index 86cc5115e..b4ef58789 100644 --- a/crates/vt/src/session/execute/mod.rs +++ b/crates/vt/src/session/execute/mod.rs @@ -21,10 +21,11 @@ use std::{ use futures_util::future::LocalBoxFuture; use tokio_util::sync::CancellationToken; +use vt_casefold::EnvName; use vt_glob::path::PathGlobSet; use vt_ipc_shared::NODE_CLIENT_PATH_ENV_NAME; use vt_path::{AbsolutePath, RelativePathBuf}; -use vt_plan::{SpawnExecution, cache_metadata::CacheMetadata}; +use vt_plan::{SpawnCommand, SpawnExecution, cache_metadata::CacheMetadata}; use vt_server::{Recorder, Reports, ServerHandle, StopAccepting, serve}; use self::{ @@ -205,9 +206,10 @@ impl<'a> ExecutionMode<'a> { } /// The extra envs to inject into the child: IPC connection info + the - /// napi addon path runner-aware tools `require()`. Empty when execution - /// is uncached. - fn injected_envs(&self) -> Vec<(&OsStr, &OsStr)> { + /// napi addon path runner-aware tools `require()`. Uncached piped tasks + /// also preserve the parent terminal's color support unless the task + /// explicitly controls colors. + fn injected_envs(&self, command: &SpawnCommand) -> Vec<(&OsStr, &OsStr)> { match self { Self::Cached { state: CacheState { tracking: t, .. }, .. } => { let mut envs: Vec<(&OsStr, &OsStr)> = @@ -218,6 +220,15 @@ impl<'a> ExecutionMode<'a> { )); envs } + Self::Uncached { pipe_writers: Some(_) } + if !command.shell_fallback + && super::stdout_supports_color() + && !["FORCE_COLOR", "NO_COLOR", "NODE_DISABLE_COLORS"].iter().any(|name| { + command.spawn_envs.contains_key(EnvName::from_ref(OsStr::new(name))) + }) => + { + vec![(OsStr::new("FORCE_COLOR"), OsStr::new("1"))] + } Self::Uncached { .. } => Vec::new(), } } @@ -498,7 +509,7 @@ async fn run( fspy_enabled, spawn_stdio, fast_fail_token.clone(), - mode.injected_envs(), + mode.injected_envs(&spawn_execution.spawn_command), ) .await .map_err(|err| Report::failed(ExecutionError::Spawn(err)))?; diff --git a/crates/vt/src/session/reporter/mod.rs b/crates/vt/src/session/reporter/mod.rs index 6c03c3d4a..a705e9fc6 100644 --- a/crates/vt/src/session/reporter/mod.rs +++ b/crates/vt/src/session/reporter/mod.rs @@ -526,6 +526,7 @@ pub mod test_fixtures { kind: ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(SpawnExecution { cache_metadata: None, spawn_command: SpawnCommand { + shell_fallback: false, program_path: test_path(), args: Arc::from([]), spawn_envs: Arc::new(BTreeMap::new()), diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/package.json index cf301ba7f..b3a5751e2 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/package.json +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/package.json @@ -1,4 +1,10 @@ { "name": "color-env-handling", - "private": true + "private": true, + "scripts": { + "uncached-color": "vtt print-color green coverage-row", + "uncached-force": "vtt print-env FORCE_COLOR", + "explicit-no-color": "FORCE_COLOR=0 vtt print-env FORCE_COLOR", + "shell-force": "vtt print-env FORCE_COLOR > shell-force.txt" + } } diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots.toml index 85c1436a7..7a07d08b3 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots.toml +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots.toml @@ -45,3 +45,35 @@ argv = ["vt", "run", "print-colored-b"] envs = [["FORCE_COLOR", "1"]] comment = "task B — cache hit replayed; formatted snapshot proves the cached bytes were coloured, even though the cache-miss run (above) showed them as plain text" formatted-snapshot = true + +[[e2e]] +name = "uncached_piped_colors" + +[[e2e.steps]] +argv = ["vt", "run", "--log", "labeled", "uncached-color"] +formatted-snapshot = true + +[[e2e.steps]] +argv = ["vt", "run", "--log", "grouped", "uncached-color"] +formatted-snapshot = true + +[[e2e]] +name = "uncached_explicit_color_controls" +steps = [ + { argv = ["vt", "run", "--log", "labeled", "uncached-force"], envs = [["FORCE_COLOR", "0"]] }, + { argv = ["vt", "run", "--log", "labeled", "uncached-force"], envs = [["FORCE_COLOR", "3"]] }, + { argv = ["vt", "run", "--log", "labeled", "uncached-force"], envs = [["NO_COLOR", "1"]] }, + { argv = ["vt", "run", "--log", "labeled", "uncached-force"], envs = [["NODE_DISABLE_COLORS", "1"]] }, + ["vt", "run", "--log", "labeled", "explicit-no-color"], +] + +[[e2e]] +name = "uncached_stdio_color_detection" +steps = [ + ["vt", "run", "--log", "labeled", "shell-force"], + ["vtt", "print-file", "shell-force.txt"], + ["vt", "run", "--log", "grouped", "shell-force"], + ["vtt", "print-file", "shell-force.txt"], + ["vtt", "pipe-stdout", "--", "vt", "run", "--log", "labeled", "uncached-force"], + ["vt", "run", "--log", "interleaved", "uncached-force"], +] diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/uncached_explicit_color_controls.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/uncached_explicit_color_controls.md new file mode 100644 index 000000000..145ac369a --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/uncached_explicit_color_controls.md @@ -0,0 +1,36 @@ +# uncached_explicit_color_controls + +## `FORCE_COLOR=0 vt run --log labeled uncached-force` + +``` +[color-env-handling#uncached-force] $ vtt print-env FORCE_COLOR ⊘ cache disabled +[color-env-handling#uncached-force] 0 +``` + +## `FORCE_COLOR=3 vt run --log labeled uncached-force` + +``` +[color-env-handling#uncached-force] $ vtt print-env FORCE_COLOR ⊘ cache disabled +[color-env-handling#uncached-force] 3 +``` + +## `NO_COLOR=1 vt run --log labeled uncached-force` + +``` +[color-env-handling#uncached-force] $ vtt print-env FORCE_COLOR ⊘ cache disabled +[color-env-handling#uncached-force] (undefined) +``` + +## `NODE_DISABLE_COLORS=1 vt run --log labeled uncached-force` + +``` +[color-env-handling#uncached-force] $ vtt print-env FORCE_COLOR ⊘ cache disabled +[color-env-handling#uncached-force] (undefined) +``` + +## `vt run --log labeled explicit-no-color` + +``` +[color-env-handling#explicit-no-color] $ FORCE_COLOR=0 vtt print-env FORCE_COLOR ⊘ cache disabled +[color-env-handling#explicit-no-color] 0 +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/uncached_piped_colors.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/uncached_piped_colors.md new file mode 100644 index 000000000..49f0c2098 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/uncached_piped_colors.md @@ -0,0 +1,16 @@ +# uncached_piped_colors + +## `vt run --log labeled uncached-color` + +``` +\x1b[90m[color-env-handling#uncached-color] \x1b[34m$ vtt print-color green coverage-row \x1b[30m\xe2\x8a\x98 \x1b[90mcache disabled +\x1b[90m[color-env-handling#uncached-color] \x1b[32mcoverage-row +``` + +## `vt run --log grouped uncached-color` + +``` +\x1b[90m[color-env-handling#uncached-color] \x1b[34m$ vtt print-color green coverage-row \x1b[30m\xe2\x8a\x98 \x1b[90mcache disabled +\x1b[90m\xe2\x94\x80\xe2\x94\x80 \x1b[90m[color-env-handling#uncached-color] \x1b[90m\xe2\x94\x80\xe2\x94\x80 +\x1b[32mcoverage-row +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/uncached_stdio_color_detection.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/uncached_stdio_color_detection.md new file mode 100644 index 000000000..9118584f1 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/uncached_stdio_color_detection.md @@ -0,0 +1,40 @@ +# uncached_stdio_color_detection + +## `vt run --log labeled shell-force` + +``` +[color-env-handling#shell-force] $ vtt print-env FORCE_COLOR > shell-force.txt ⊘ cache disabled +``` + +## `vtt print-file shell-force.txt` + +``` +(undefined) +``` + +## `vt run --log grouped shell-force` + +``` +[color-env-handling#shell-force] $ vtt print-env FORCE_COLOR > shell-force.txt ⊘ cache disabled +``` + +## `vtt print-file shell-force.txt` + +``` +(undefined) +``` + +## `vtt pipe-stdout -- vt run --log labeled uncached-force` + +``` +--- stdout --- +[color-env-handling#uncached-force] $ vtt print-env FORCE_COLOR ⊘ cache disabled +[color-env-handling#uncached-force] (undefined) +``` + +## `vt run --log interleaved uncached-force` + +``` +$ vtt print-env FORCE_COLOR ⊘ cache disabled +(undefined) +``` diff --git a/crates/vt_plan/src/lib.rs b/crates/vt_plan/src/lib.rs index 6612b7375..baeac66f5 100644 --- a/crates/vt_plan/src/lib.rs +++ b/crates/vt_plan/src/lib.rs @@ -54,6 +54,11 @@ pub struct SpawnCommand { #[serde(serialize_with = "serialize_envs")] pub spawn_envs: Arc>, Arc>>, + /// Whether the runner delegates an unparsed task script to the system shell. + /// Color controls inside the script are not represented in `spawn_envs`. + #[serde(skip)] + pub shell_fallback: bool, + /// Current working directory pub cwd: Arc, } diff --git a/crates/vt_plan/src/plan.rs b/crates/vt_plan/src/plan.rs index 616d830a9..bf7b20169 100644 --- a/crates/vt_plan/src/plan.rs +++ b/crates/vt_plan/src/plan.rs @@ -388,7 +388,7 @@ async fn plan_task_as_execution_node( context.resolved_global_cache(), ), }; - let spawn_execution = plan_spawn_execution( + let mut spawn_execution = plan_spawn_execution( context.workspace_path(), Some(ExecutionCacheKey::UserTask { task_name: task_node.task_display.task_name.clone(), @@ -408,6 +408,7 @@ async fn plan_task_as_execution_node( Arc::clone(&*SHELL_PROGRAM_PATH), SHELL_ARGS.iter().map(|s| Str::from(*s)).chain(std::iter::once(script)).collect(), )?; + spawn_execution.spawn_command.shell_fallback = true; items.push(ExecutionItem { execution_item_display, kind: ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution)), @@ -723,6 +724,7 @@ fn plan_spawn_execution( Ok(SpawnExecution { spawn_command: SpawnCommand { + shell_fallback: false, program_path, args: Arc::clone(&args), cwd,