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

- **Added** Platform support for targets without `input` auto-inference (e.g. Android). Tasks still run; those relying on auto-inference run uncached, with the summary noting that `input` must be configured manually to enable caching ([#352](https://github.com/voidzero-dev/vite-task/pull/352))
- **Fixed** `vp run` no longer aborts with `failed to prepare the command for injection: Invalid argument` when the user environment already has `LD_PRELOAD` (Linux) or `DYLD_INSERT_LIBRARIES` (macOS) set. The tracer shim is now appended to any existing value and placed last, so user preloads keep their symbol-interposition precedence ([#340](https://github.com/voidzero-dev/vite-task/issues/340))
- **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))
Expand Down
36 changes: 12 additions & 24 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ fspy_shared_unix = { path = "crates/fspy_shared_unix" }
futures = "0.3.31"
futures-util = "0.3.31"
jsonc-parser = { version = "0.29.0", features = ["serde"] }
libc = "0.2.172"
libc = "0.2.185"
libtest-mimic = "0.8.2"
memmap2 = "0.9.7"
monostate = "1.0.2"
native_str = { path = "crates/native_str" }
nix = { version = "0.30.1", features = ["dir", "signal"] }
nix = { version = "0.31.2", features = ["dir", "signal"] }
ntapi = "0.4.1"
nucleo-matcher = "0.3.1"
once_cell = "1.19"
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ impl Command {

/// Convert to a `tokio::process::Command` without tracking.
#[must_use]
pub fn into_tokio_command(self) -> TokioCommand {
pub(crate) fn into_tokio_command(self) -> TokioCommand {
let mut tokio_cmd = TokioCommand::new(self.program);
if let Some(cwd) = &self.cwd {
tokio_cmd.current_dir(cwd);
Expand Down
17 changes: 13 additions & 4 deletions crates/vite_task/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "vite_task"
version = "0.0.0"
edition.workspace = true
include = ["/src"]
include = ["/src", "/build.rs"]
license.workspace = true
publish = false
readme = "README.md"
Expand All @@ -17,8 +17,7 @@ async-trait = { workspace = true }
wincode = { workspace = true, features = ["derive"] }
clap = { workspace = true, features = ["derive"] }
ctrlc = { workspace = true }
derive_more = { workspace = true, features = ["from"] }
fspy = { workspace = true }
derive_more = { workspace = true, features = ["debug", "from"] }
futures-util = { workspace = true }
once_cell = { workspace = true }
owo-colors = { workspace = true }
Expand All @@ -30,7 +29,14 @@ rustc-hash = { workspace = true }
serde = { workspace = true, features = ["derive", "rc"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "io-util", "macros", "sync"] }
tokio = { workspace = true, features = [
"rt-multi-thread",
"io-std",
"io-util",
"macros",
"process",
"sync",
] }
tokio-util = { workspace = true }
tracing = { workspace = true }
twox-hash = { workspace = true }
Expand All @@ -45,6 +51,9 @@ wax = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

[target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies]
fspy = { workspace = true }

[target.'cfg(unix)'.dependencies]
nix = { workspace = true }

Expand Down
18 changes: 18 additions & 0 deletions crates/vite_task/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Why `cfg(fspy)` instead of matching on `target_os` directly at each use site:
// "fspy is available" is a single semantic predicate, but the underlying reason
// (the `fspy` crate builds on windows/macos/linux) is a three-OS list that
// would otherwise have to be repeated — as `any(target_os = "windows", "macos",
// "linux")` — everywhere `fspy::*` is touched. Naming it `fspy` keeps the
// source self-documenting: code reads `#[cfg(fspy)]` instead of a disjunction
// over OSes. The OS allowlist lives in two spots that must stay in sync: this
// file (for the rustc cfg) and the target-scoped dep block in Cargo.toml
// (which Cargo resolves before build.rs runs, so it can't reuse this cfg).
fn main() {
println!("cargo::rustc-check-cfg=cfg(fspy)");
println!("cargo::rerun-if-changed=build.rs");

let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
if matches!(target_os.as_str(), "windows" | "macos" | "linux") {
println!("cargo::rustc-cfg=fspy");
}
}
4 changes: 4 additions & 0 deletions crates/vite_task/src/session/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ pub enum CacheNotUpdatedReason {
/// First path that was both read and written during execution.
path: RelativePathBuf,
},
/// fspy isn't compiled in on this build and the task requires fspy
/// (its `input` config includes auto-inference). Task ran but cannot
/// be cached without tracked path accesses.
FspyUnsupported,
}

#[derive(Debug)]
Expand Down
7 changes: 6 additions & 1 deletion crates/vite_task/src/session/execute/fingerprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ use vite_path::{AbsolutePath, RelativePathBuf};
use vite_str::Str;
use wincode::{SchemaRead, SchemaWrite};

use super::tracked_accesses::PathRead;
use crate::{collections::HashMap, session::cache::InputChangeKind};

/// Path read access info
#[derive(Debug, Clone, Copy)]
pub struct PathRead {
pub read_dir_entries: bool,
}

/// Post-run fingerprint capturing file state after execution.
/// Used to validate whether cached outputs are still valid.
#[derive(SchemaWrite, SchemaRead, Debug, Serialize)]
Expand Down
78 changes: 52 additions & 26 deletions crates/vite_task/src/session/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod glob_inputs;
mod hash;
pub mod pipe;
pub mod spawn;
#[cfg(fspy)]
pub mod tracked_accesses;
#[cfg(windows)]
mod win_job;
Expand All @@ -20,12 +21,13 @@ use vite_task_plan::{
cache_metadata::CacheMetadata, execution_graph::ExecutionNodeIndex,
};

#[cfg(fspy)]
use self::tracked_accesses::TrackedPathAccesses;
use self::{
fingerprint::PostRunFingerprint,
fingerprint::{PathRead, PostRunFingerprint},
glob_inputs::compute_globbed_inputs,
pipe::{PipeSinks, StdOutput, pipe_stdio},
spawn::{SpawnStdio, spawn},
tracked_accesses::TrackedPathAccesses,
};
use super::{
cache::{CacheEntryValue, ExecutionCache},
Expand Down Expand Up @@ -291,6 +293,17 @@ struct CacheState<'a> {
fspy_negatives: Option<Vec<wax::Glob<'static>>>,
}

/// Post-execution summary of what fspy observed for a single task. Used in the
/// cache-update step. Fields are cfg-agnostic so the downstream match logic
/// doesn't need `cfg(fspy)` — the value is only ever `Some` when tracking
/// happened (see the `let tracking = ...` fork in `execute_spawn`).
struct TrackingOutcome {
path_reads: HashMap<RelativePathBuf, PathRead>,
/// First path that was both read and written during execution, if any.
/// A non-empty value means caching this task is unsound.
read_write_overlap: Option<RelativePathBuf>,
}

/// Execute a spawned process with cache-aware lifecycle.
///
/// This is a free function (not tied to `ExecutionContext`) so it can be reused
Expand Down Expand Up @@ -539,44 +552,57 @@ pub async fn execute_spawn(
let (cache_update_status, cache_error) = if let ExecutionMode::Cached { state, .. } = mode {
let CacheState { metadata, globbed_inputs, std_outputs, fspy_negatives } = state;

// Normalize fspy accesses. `zip` gives `Some` iff fspy was enabled
// (both outcome.path_accesses and fspy_negatives are Some together).
let path_accesses = outcome
.path_accesses
.as_ref()
.zip(fspy_negatives.as_deref())
.map(|(raw, negs)| TrackedPathAccesses::from_raw(raw, cache_base_path, negs));
// Post-execution summary of what fspy observed. `Some` iff tracking was
// both requested (`fspy_negatives.is_some()`) and compiled in (`cfg(fspy)`).
// On a `cfg(not(fspy))` build this is always `None`, and the match below
// short-circuits to `FspyUnsupported` when tracking was needed.
let tracking: Option<TrackingOutcome> = {
#[cfg(fspy)]
{
outcome.path_accesses.as_ref().zip(fspy_negatives.as_deref()).map(|(raw, negs)| {
let tracked = TrackedPathAccesses::from_raw(raw, cache_base_path, negs);
let read_write_overlap = tracked
.path_reads
.keys()
.find(|p| tracked.path_writes.contains(*p))
.cloned();
TrackingOutcome { path_reads: tracked.path_reads, read_write_overlap }
})
}
#[cfg(not(fspy))]
{
None
}
};

let cancelled = fast_fail_token.is_cancelled() || interrupt_token.is_cancelled();
if cancelled {
// Cancelled (Ctrl-C or sibling failure) — result is untrustworthy
(CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::Cancelled), None)
} else if outcome.exit_status.success() {
// Check for read-write overlap: if the task wrote to any file it also
// read, the inputs were modified during execution — don't cache.
// Note: this only checks fspy-inferred reads, not globbed_inputs keys.
// A task that writes to a glob-matched file without reading it causes
// perpetual cache misses (glob detects the hash change) but not a
// correctness bug, so we don't handle that case here.
if let Some(path) = path_accesses
.as_ref()
.and_then(|pa| pa.path_reads.keys().find(|p| pa.path_writes.contains(*p)))
{
// fspy-inferred read-write overlap: the task wrote to a file it also
// read, so the prerun input hashes are stale and caching is unsound.
// (We only check fspy-inferred reads, not globbed_inputs. A task that
// writes to a glob-matched file without reading it produces perpetual
// cache misses but not a correctness bug.)
if let Some(TrackingOutcome { read_write_overlap: Some(path), .. }) = &tracking {
(
CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::InputModified {
path: path.clone(),
}),
None,
)
} else if tracking.is_none() && fspy_negatives.is_some() {
// Task requested fspy auto-inference but this binary was built
// without `cfg(fspy)`. Task ran, but we can't compute a valid
// cache entry without tracked path accesses.
(CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::FspyUnsupported), None)
} else {
// path_reads is empty when inference is disabled (path_accesses is None)
// Paths already in globbed_inputs are skipped: the overlap check
// above guarantees no input modification, so the prerun hash is
// the correct post-exec hash.
let empty_path_reads = HashMap::default();
let path_reads =
path_accesses.as_ref().map_or(&empty_path_reads, |pa| &pa.path_reads);

// Execution succeeded — attempt to create fingerprint and update cache.
// Paths already in globbed_inputs are skipped: Rule 1 (above) guarantees
// no input modification, so the prerun hash is the correct post-exec hash.
let path_reads = tracking.as_ref().map_or(&empty_path_reads, |t| &t.path_reads);
match PostRunFingerprint::create(path_reads, cache_base_path, &globbed_inputs) {
Ok(post_run_fingerprint) => {
let new_cache_value = CacheEntryValue {
Expand Down
Loading
Loading