Skip to content
Open
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** Explicit `cache.untrackedEnv` patterns now override environment dependencies reported by tools. To keep those variables tracked, declare them in `cache.env` or negate their `untrackedEnv` patterns with `!`. Built-in passthrough defaults do not cancel tool-reported tracking ([#856](https://github.com/voidzero-dev/vite-task/pull/856)).
- **Fixed** On Linux, concurrent system calls in tracked programs, such as `pnpm dlx`, no longer cause a stack overflow ([#854](https://github.com/voidzero-dev/vite-task/pull/854)).
- **Fixed** Cached tasks can now run scripts whose shebang names an interpreter path longer than 126 bytes. Before, the path was cut short and the script failed to start or ran the wrong program. On macOS, text after a `#` in the shebang line is now ignored, as it is when the script runs on its own ([#788](https://github.com/voidzero-dev/vite-task/pull/788)).
- **Fixed** On macOS, tasks with automatic file-access tracking can now run Node.js single executable applications, such as pnpm 11's standalone `pnpm` binary, which previously aborted with `Assertion failed: (magic) == (kMagic)`, and `deno compile` outputs, which previously failed with `Did not find magic bytes` ([#840](https://github.com/voidzero-dev/vite-task/pull/840)).
Expand Down
8 changes: 7 additions & 1 deletion crates/vt/src/session/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ impl ExecutionCache {
if let Some(cache_value) = self.get_by_cache_key(cache_key).await? {
if let Some(mismatch) = cache_value.validate(
&cache_metadata.unfiltered_envs,
&cache_metadata.explicit_untracked_env,
globbed_inputs,
workspace_root,
)? {
Expand Down Expand Up @@ -445,7 +446,12 @@ impl ExecutionCache {
.fetch(remote_config, cache_key, &cache_metadata.execution_cache_key, cancel_token)
.await;
let validate = |cache_value: &CacheEntryValue| {
cache_value.validate(&cache_metadata.unfiltered_envs, globbed_inputs, workspace_root)
cache_value.validate(
&cache_metadata.unfiltered_envs,
&cache_metadata.explicit_untracked_env,
globbed_inputs,
workspace_root,
)
};
let Restore { value: cache_value, blob_id } =
match remote::resolve(fetched, cache_key, validate) {
Expand Down
2 changes: 1 addition & 1 deletion crates/vt/src/session/cache/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ mod tests {
) -> impl FnOnce(&CacheEntryValue) -> anyhow::Result<Option<FingerprintMismatch>> {
move |value| {
let workspace_root = vt_path::current_dir().unwrap();
value.validate(&FxHashMap::default(), &globbed_inputs, &workspace_root)
value.validate(&FxHashMap::default(), &[], &globbed_inputs, &workspace_root)
}
}

Expand Down
3 changes: 2 additions & 1 deletion crates/vt/src/session/cache/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ impl CacheEntryValue {
pub(crate) fn validate(
&self,
unfiltered_envs: &FxHashMap<EnvName<Arc<OsStr>>, Arc<OsStr>>,
explicit_untracked_env: &[vt_str::Str],
globbed_inputs: &BTreeMap<RelativePathBuf, u64>,
workspace_root: &AbsolutePath,
) -> anyhow::Result<Option<FingerprintMismatch>> {
Expand All @@ -28,7 +29,7 @@ impl CacheEntryValue {
}

self.post_run_fingerprint
.validate(workspace_root, unfiltered_envs)
.validate(workspace_root, unfiltered_envs, explicit_untracked_env)
.map(|mismatch| mismatch.map(FingerprintMismatch::from))
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/vt/src/session/execute/cache_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ fn collect_tracked_envs(
}

/// Select tool-reported bulk env query records to embed in the post-run
/// fingerprint. The full match-set is stored as value hashes.
/// fingerprint. Keep complete observations so removing an `untrackedEnv`
/// override can revalidate previously ignored dependencies.
fn collect_tracked_env_queries(reports: &Reports) -> anyhow::Result<TrackedEnvQueryValues> {
let mut tracked_env_queries = BTreeMap::new();

Expand Down
106 changes: 92 additions & 14 deletions crates/vt/src/session/execute/fingerprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use vt_casefold::EnvName;
use vt_glob::env::EnvGlobSet;
use vt_path::{AbsolutePath, RelativePathBuf};
use vt_plan::cache_metadata::EnvValueHash;
use vt_str::Str;
Expand Down Expand Up @@ -142,6 +143,8 @@ impl PostRunFingerprint {
/// unfiltered env context used by runner-aware IPC. `unfiltered_envs` must
/// be the same plan env context that served the original `getEnv` request,
/// not the filtered env passed to the spawned process.
/// User-declared `untrackedEnv` patterns override both single-name and
/// bulk dependencies. Built-in passthrough defaults do not.
///
/// Returns `Some(mismatch)` if anything changed, `None` if all valid.
/// Returns an error if a tracked env is currently present but cannot be
Expand All @@ -152,6 +155,7 @@ impl PostRunFingerprint {
&self,
base_dir: &AbsolutePath,
unfiltered_envs: &FxHashMap<EnvName<Arc<OsStr>>, Arc<OsStr>>,
explicit_untracked_env: &[Str],
) -> anyhow::Result<Option<PostRunMismatch>> {
let input_mismatch = self.inferred_inputs.par_iter().find_map_any(
|(input_relative_path, path_fingerprint)| {
Expand Down Expand Up @@ -186,6 +190,11 @@ impl PostRunFingerprint {
return result.map(Some);
}

let has_env_reads = !self.tracked_envs.is_empty() || !self.tracked_env_queries.is_empty();
let untracked_env = (has_env_reads && !explicit_untracked_env.is_empty())
.then(|| EnvGlobSet::new(explicit_untracked_env))
.transpose()?;

for (name, stored_value) in &self.tracked_envs {
let current_value = unfiltered_envs
.get(EnvName::from_ref(OsStr::new(name.as_str())))
Expand All @@ -196,6 +205,9 @@ impl PostRunFingerprint {
Ok::<_, anyhow::Error>(EnvValueHash::new(value_str))
})
.transpose()?;
if untracked_env.as_ref().is_some_and(|patterns| patterns.is_match(name.as_str())) {
continue;
}
if let Some(mismatch) =
EnvMismatch::compare(name, stored_value.as_ref(), current_value.as_ref())
{
Expand All @@ -213,7 +225,9 @@ impl PostRunFingerprint {
}));
}
};
if let Some(mismatch) = first_env_glob_mismatch(stored_matches, &current_matches) {
if let Some(mismatch) =
first_env_glob_mismatch(stored_matches, &current_matches, untracked_env.as_ref())
{
return Ok(Some(PostRunMismatch::TrackedEnvQuery {
query: query.clone(),
mismatch,
Expand Down Expand Up @@ -275,9 +289,12 @@ enum EnvQueryValidation {
fn first_env_glob_mismatch(
stored: &BTreeMap<Str, EnvValueHash>,
current: &BTreeMap<Str, EnvValueHash>,
untracked_env: Option<&EnvGlobSet>,
) -> Option<EnvMismatch> {
let mut stored_iter = stored.iter();
let mut current_iter = current.iter();
let is_tracked =
|name: &Str| !untracked_env.is_some_and(|patterns| patterns.is_match(name.as_str()));
let mut stored_iter = stored.iter().filter(|(name, _)| is_tracked(name));
let mut current_iter = current.iter().filter(|(name, _)| is_tracked(name));
let mut s = stored_iter.next();
let mut c = current_iter.next();

Expand Down Expand Up @@ -516,6 +533,62 @@ mod tests {
OsString::from_wide(&[0xD800])
}

#[test]
fn validate_applies_current_overrides_to_complete_bulk_query_entries() {
for query in [TrackedEnvQuery::Glob(Str::from("*")), TrackedEnvQuery::Prefix(Str::from(""))]
{
let stored_matches = BTreeMap::from([
(Str::from("VITE_TITLE"), EnvValueHash::new("title")),
(Str::from("ACTIONS_ORCHESTRATION_ID"), EnvValueHash::new("old-run")),
]);
let fingerprint = PostRunFingerprint {
tracked_env_queries: BTreeMap::from([(query, stored_matches)]),
..PostRunFingerprint::default()
};
let unfiltered_envs = FxHashMap::from_iter([
(
EnvName::new(Arc::<OsStr>::from(OsStr::new("VITE_TITLE"))),
Arc::<OsStr>::from(OsStr::new("title")),
),
(
EnvName::new(Arc::<OsStr>::from(OsStr::new("ACTIONS_ORCHESTRATION_ID"))),
Arc::<OsStr>::from(OsStr::new("new-run")),
),
]);
let untracked = [Str::from("ACTIONS_*")];
let workspace_root = vt_path::current_dir().expect("cwd");
assert!(
fingerprint
.validate(&workspace_root, &unfiltered_envs, &untracked)
.expect("validation succeeds")
.is_none()
);
}
}

#[test]
fn validate_rejects_non_utf8_bulk_values_even_when_untracked() {
let fingerprint = PostRunFingerprint {
tracked_env_queries: BTreeMap::from([(
TrackedEnvQuery::Prefix(Str::from("")),
BTreeMap::new(),
)]),
..PostRunFingerprint::default()
};
let unfiltered_envs = FxHashMap::from_iter([(
EnvName::new(Arc::<OsStr>::from(OsStr::new("PROBE_NOISE"))),
Arc::<OsStr>::from(non_utf8_os_string()),
)]);
let untracked = [Str::from("PROBE_*")];
let workspace_root = vt_path::current_dir().expect("cwd");
assert!(matches!(
fingerprint
.validate(&workspace_root, &unfiltered_envs, &untracked)
.expect("validation succeeds"),
Some(PostRunMismatch::TrackedEnvQuery { mismatch: EnvMismatch::Changed { .. }, .. })
));
}

#[test]
fn validate_errors_on_current_non_utf8_tracked_env_value() {
let mut tracked_envs = BTreeMap::new();
Expand All @@ -530,7 +603,7 @@ mod tests {

let workspace_root = vt_path::current_dir().expect("cwd");
let err = fingerprint
.validate(&workspace_root, &unfiltered_envs)
.validate(&workspace_root, &unfiltered_envs, &[])
.expect_err("non-UTF-8 tracked env values must error");

assert!(err.to_string().contains("tracked env value for PROBE_ENV is not valid UTF-8"));
Expand All @@ -550,8 +623,9 @@ mod tests {
);

let workspace_root = vt_path::current_dir().expect("cwd");
let mismatch =
fingerprint.validate(&workspace_root, &unfiltered_envs).expect("validation succeeds");
let mismatch = fingerprint
.validate(&workspace_root, &unfiltered_envs, &[])
.expect("validation succeeds");

match mismatch {
Some(PostRunMismatch::TrackedEnvQuery {
Expand Down Expand Up @@ -585,8 +659,9 @@ mod tests {
);

let workspace_root = vt_path::current_dir().expect("cwd");
let mismatch =
fingerprint.validate(&workspace_root, &unfiltered_envs).expect("validation succeeds");
let mismatch = fingerprint
.validate(&workspace_root, &unfiltered_envs, &[])
.expect("validation succeeds");

assert!(mismatch.is_none());
}
Expand All @@ -605,8 +680,9 @@ mod tests {
);

let workspace_root = vt_path::current_dir().expect("cwd");
let mismatch =
fingerprint.validate(&workspace_root, &unfiltered_envs).expect("validation succeeds");
let mismatch = fingerprint
.validate(&workspace_root, &unfiltered_envs, &[])
.expect("validation succeeds");

assert!(mismatch.is_none());
}
Expand All @@ -624,8 +700,9 @@ mod tests {
);

let workspace_root = vt_path::current_dir().expect("cwd");
let mismatch =
fingerprint.validate(&workspace_root, &unfiltered_envs).expect("validation succeeds");
let mismatch = fingerprint
.validate(&workspace_root, &unfiltered_envs, &[])
.expect("validation succeeds");

assert_eq!(mismatch.is_none(), cfg!(windows));
}
Expand All @@ -646,8 +723,9 @@ mod tests {
);

let workspace_root = vt_path::current_dir().expect("cwd");
let mismatch =
fingerprint.validate(&workspace_root, &unfiltered_envs).expect("validation succeeds");
let mismatch = fingerprint
.validate(&workspace_root, &unfiltered_envs, &[])
.expect("validation succeeds");

assert_eq!(mismatch.is_none(), cfg!(windows));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import { getEnvs } from '@voidzero-dev/vite-task-client';
const args = process.argv.slice(2);
const tracked = !args.includes('--untracked');
const prefixIndex = args.indexOf('--prefix');
const query = prefixIndex === -1 ? 'PROBE_*' : { prefix: args[prefixIndex + 1] ?? 'PROBE_' };
const globIndex = args.indexOf('--glob');
const glob = globIndex === -1 ? 'PROBE_*' : (args[globIndex + 1] ?? 'PROBE_*');
const query = prefixIndex === -1 ? glob : { prefix: args[prefixIndex + 1] ?? 'PROBE_' };
const matches = getEnvs(query, { tracked });
const sorted = Object.entries(matches).sort(([a], [b]) => a.localeCompare(b));
const printPrefixIndex = args.indexOf('--print-prefix');
const printPrefix = printPrefixIndex === -1 ? '' : (args[printPrefixIndex + 1] ?? '');
const sorted = Object.entries(matches)
.filter(([name]) => name.startsWith(printPrefix))
.sort(([a], [b]) => a.localeCompare(b));

for (const [key, value] of sorted) {
console.log(`${key}=${value}`);
Expand Down
Loading