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** `output` field for cached tasks: archives matching files after a successful run and restores them on cache hit ([#375](https://github.com/voidzero-dev/vite-task/pull/375))
- **Fixed** Windows cached tasks can now run package shims rewritten through PowerShell; default env passthrough now preserves `PATHEXT` ([#366](https://github.com/voidzero-dev/vite-task/pull/366))
- **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))
Expand Down
43 changes: 43 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ winsafe = { version = "0.0.27", features = ["kernel"] }
xxhash-rust = { version = "0.8.15", features = ["const_xxh3"] }
ntest = "0.9.5"
terminal_size = "0.4"
zstd = "0.13"

[workspace.metadata.cargo-shear]
ignored = [
Expand Down
3 changes: 3 additions & 0 deletions crates/vite_task/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ rustc-hash = { workspace = true }
serde = { workspace = true, features = ["derive", "rc"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tar = { workspace = true }
tokio = { workspace = true, features = [
"rt-multi-thread",
"io-std",
Expand All @@ -40,13 +41,15 @@ tokio = { workspace = true, features = [
tokio-util = { workspace = true }
tracing = { workspace = true }
twox-hash = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
vite_path = { workspace = true }
vite_select = { workspace = true }
vite_str = { workspace = true }
vite_task_graph = { workspace = true }
vite_task_plan = { workspace = true }
vite_workspace = { workspace = true }
wax = { workspace = true }
zstd = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
Expand Down
63 changes: 63 additions & 0 deletions crates/vite_task/src/session/cache/archive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! Output archive creation and extraction using tar + zstd compression.

use std::{fs::File, io};

use vite_path::{AbsolutePath, RelativePathBuf};

/// Create a tar.zst archive from workspace-relative output file paths.
///
/// Files that no longer exist are silently skipped (the task may delete
/// temporary files during execution).
///
/// # Errors
///
/// Returns an error if creating the archive file or adding entries fails.
pub fn create_output_archive(
workspace_root: &AbsolutePath,
output_files: &[RelativePathBuf],
archive_path: &AbsolutePath,
) -> anyhow::Result<()> {
let file = File::create(archive_path.as_path())?;
let encoder = zstd::Encoder::new(file, 0)?.auto_finish();
let mut builder = tar::Builder::new(encoder);

for rel_path in output_files {
let abs_path = workspace_root.join(rel_path);
// Skip files that no longer exist (task may delete temp files between
// glob walk and archiving). Any other error is propagated.
let metadata = match std::fs::metadata(abs_path.as_path()) {
Ok(m) => m,
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(err.into()),
};
if metadata.is_file() {
let mut file = File::open(abs_path.as_path())?;
let mut header = tar::Header::new_gnu();
header.set_metadata(&metadata);
header.set_cksum();
builder.append_data(&mut header, rel_path.as_str(), &mut file)?;
}
}

builder.finish()?;
Ok(())
}

/// Extract a tar.zst archive, restoring files relative to workspace root.
///
/// Parent directories are created automatically. Existing files are overwritten.
///
/// # Errors
///
/// Returns an error if opening the archive or extracting entries fails.
pub fn extract_output_archive(
workspace_root: &AbsolutePath,
archive_path: &AbsolutePath,
) -> anyhow::Result<()> {
let file = File::open(archive_path.as_path())?;
let decoder = zstd::Decoder::new(file)?;
let mut archive = tar::Archive::new(decoder);

archive.unpack(workspace_root.as_path())?;
Ok(())
}
1 change: 1 addition & 0 deletions crates/vite_task/src/session/cache/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
}
}
FingerprintMismatch::InputConfig => "input configuration changed",
FingerprintMismatch::OutputConfig => "output configuration changed",
FingerprintMismatch::InputChanged { kind, path } => {
let desc = format_input_change_str(*kind, path.as_str());
return Some(vite_str::format!("○ cache miss: {desc}, executing"));
Expand Down
65 changes: 52 additions & 13 deletions crates/vite_task/src/session/cache/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Execution cache for storing and retrieving cached command results.

pub mod archive;
pub mod display;

use std::{collections::BTreeMap, fmt::Display, fs::File, io::Write, sync::Arc, time::Duration};
Expand All @@ -14,7 +15,8 @@ use rusqlite::{Connection, OptionalExtension as _, config::DbConfig};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use vite_path::{AbsolutePath, RelativePathBuf};
use vite_task_graph::config::ResolvedInputConfig;
use vite_str::Str;
use vite_task_graph::config::ResolvedGlobConfig;
use vite_task_plan::cache_metadata::{CacheMetadata, ExecutionCacheKey, SpawnFingerprint};
use wincode::{
SchemaRead, SchemaReadOwned, SchemaWrite,
Expand Down Expand Up @@ -43,14 +45,18 @@ pub struct CacheEntryKey {
pub spawn_fingerprint: SpawnFingerprint,
/// Resolved input configuration that affects cache behavior.
/// Glob patterns are workspace-root-relative.
pub input_config: ResolvedInputConfig,
pub input_config: ResolvedGlobConfig,
/// Resolved output configuration that affects cache restoration.
/// Glob patterns are workspace-root-relative.
pub output_config: ResolvedGlobConfig,
}

impl CacheEntryKey {
fn from_metadata(cache_metadata: &CacheMetadata) -> Self {
Self {
spawn_fingerprint: cache_metadata.spawn_fingerprint.clone(),
input_config: cache_metadata.input_config.clone(),
output_config: cache_metadata.output_config.clone(),
}
}
}
Expand Down Expand Up @@ -103,6 +109,9 @@ pub struct CacheEntryValue {
/// Path is relative to workspace root, value is `xxHash3_64` of file content.
/// Stored in the value (not the key) so changes can be detected and reported.
pub globbed_inputs: BTreeMap<RelativePathBuf, u64>,
/// Filename of the output archive (e.g. `{uuid}.tar.zst`) stored alongside
/// `cache.db` in the cache directory. `None` if no output files were produced.
pub output_archive: Option<Str>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -142,6 +151,8 @@ pub enum FingerprintMismatch {
},
/// Found a previous cache entry key for the same task, but `input_config` differs.
InputConfig,
/// Found a previous cache entry key for the same task, but `output_config` differs.
OutputConfig,

InputChanged {
kind: InputChangeKind,
Expand All @@ -158,6 +169,9 @@ impl Display for FingerprintMismatch {
Self::InputConfig => {
write!(f, "input configuration changed")
}
Self::OutputConfig => {
write!(f, "output configuration changed")
}
Self::InputChanged { kind, path } => {
write!(f, "{}", display::format_input_change_str(*kind, path.as_str()))
}
Expand Down Expand Up @@ -201,16 +215,16 @@ impl ExecutionCache {
"CREATE TABLE task_fingerprints (key BLOB PRIMARY KEY, value BLOB);",
(),
)?;
conn.execute("PRAGMA user_version = 11", ())?;
conn.execute("PRAGMA user_version = 12", ())?;
}
1..=10 => {
1..=11 => {
// old internal db version. reset
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)?;
conn.execute("VACUUM", ())?;
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)?;
}
11 => break, // current version
12.. => {
12 => break, // current version
13.. => {
return Err(anyhow::anyhow!(
"Unrecognized database version: {user_version}. \
The cache may have been created by a newer version of Vite Task. \
Expand Down Expand Up @@ -269,17 +283,25 @@ impl ExecutionCache {
if let Some(old_cache_key) =
self.get_cache_key_by_execution_key(execution_cache_key).await?
{
// Destructure to ensure we handle all fields when new ones are added
let CacheEntryKey { spawn_fingerprint: old_spawn_fingerprint, input_config: _ } =
old_cache_key;
let mismatch = if old_spawn_fingerprint == *spawn_fingerprint {
// spawn fingerprint is the same but input_config or glob_base changed
FingerprintMismatch::InputConfig
} else {
// Destructure to ensure we handle all fields when new ones are added.
// `get_by_cache_key` above returned None for the *current* cache key,
// so at least one field on `old_cache_key` must differ from the
// current metadata — checked in priority order (spawn → input → output).
let CacheEntryKey {
spawn_fingerprint: old_spawn_fingerprint,
input_config: old_input_config,
output_config: old_output_config,
} = old_cache_key;
let mismatch = if old_spawn_fingerprint != *spawn_fingerprint {
FingerprintMismatch::SpawnFingerprint {
old: old_spawn_fingerprint,
new: spawn_fingerprint.clone(),
}
} else if old_input_config != cache_metadata.input_config {
FingerprintMismatch::InputConfig
} else {
debug_assert!(old_output_config != cache_metadata.output_config);
FingerprintMismatch::OutputConfig
};
return Ok(Err(CacheMiss::FingerprintMismatch(mismatch)));
}
Expand All @@ -288,16 +310,33 @@ impl ExecutionCache {
}

/// Update cache after successful execution.
///
/// If a previous entry exists for the same cache key with a different
/// `output_archive`, the stale archive file in `cache_dir` is removed
/// (best-effort) so it doesn't accumulate on disk.
#[tracing::instrument(level = "debug", skip_all)]
pub async fn update(
&self,
cache_metadata: &CacheMetadata,
cache_value: CacheEntryValue,
cache_dir: &AbsolutePath,
) -> anyhow::Result<()> {
let execution_cache_key = &cache_metadata.execution_cache_key;

let cache_key = CacheEntryKey::from_metadata(cache_metadata);

// If a previous entry exists with a stale output archive, delete the
// old file so the cache directory doesn't accumulate orphaned archives.
if let Some(old_value) = self.get_by_cache_key(&cache_key).await?
&& let Some(old_archive) = old_value.output_archive
&& cache_value.output_archive.as_ref() != Some(&old_archive)
{
let old_archive_path = cache_dir.join(old_archive.as_str());
// Best-effort cleanup: a missing file (e.g. after a crash or manual
// cache clear) is fine, so we ignore the error.
let _ = std::fs::remove_file(old_archive_path.as_path());
}

Comment thread
branchseer marked this conversation as resolved.
self.upsert_cache_entry(&cache_key, &cache_value).await?;
self.upsert_task_fingerprint(execution_cache_key, &cache_key).await?;
Ok(())
Expand Down
Loading
Loading