Skip to content

Commit 7876951

Browse files
branchseerclaude
andcommitted
feat(cache): add output_config to cache key, output_archive to value, and archive module
- Add `output_config` to `CacheEntryKey` so changing output config invalidates cache - Add `output_archive: Option<Str>` to `CacheEntryValue` for the tar.zst filename - Bump DB version from 10 to 11 (resets old databases) - Add `FingerprintMismatch::OutputConfig` variant with display support - Create `archive.rs` module with tar+zstd create/extract operations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4300807 commit 7876951

5 files changed

Lines changed: 100 additions & 13 deletions

File tree

crates/vite_task/docs/task-cache.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ The cache entry key uniquely identifies a command execution context:
9292
```rust
9393
pub struct CacheEntryKey {
9494
pub spawn_fingerprint: SpawnFingerprint,
95-
pub input_config: ResolvedInputConfig,
95+
pub input_config: ResolvedGlobConfig,
9696
}
9797
```
9898

@@ -303,7 +303,7 @@ Cache entries are serialized using `bincode` for efficient storage.
303303
│ ────────────────────── │
304304
│ CacheEntryKey { │
305305
│ spawn_fingerprint: SpawnFingerprint { ... }, │
306-
│ input_config: ResolvedInputConfig { ... }, │
306+
│ input_config: ResolvedGlobConfig { ... }, │
307307
│ } │
308308
│ ExecutionCacheKey::UserTask { │
309309
│ task_name: "build", │
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//! Output archive creation and extraction using tar + zstd compression.
2+
3+
use std::fs::File;
4+
5+
use vite_path::{AbsolutePath, RelativePathBuf};
6+
7+
/// Create a tar.zst archive from workspace-relative output file paths.
8+
///
9+
/// Files that no longer exist are silently skipped (the task may delete
10+
/// temporary files during execution).
11+
///
12+
/// # Errors
13+
///
14+
/// Returns an error if creating the archive file or adding entries fails.
15+
pub fn create_output_archive(
16+
workspace_root: &AbsolutePath,
17+
output_files: &[RelativePathBuf],
18+
archive_path: &AbsolutePath,
19+
) -> anyhow::Result<()> {
20+
let file = File::create(archive_path.as_path())?;
21+
let encoder = zstd::Encoder::new(file, 0)?.auto_finish();
22+
let mut builder = tar::Builder::new(encoder);
23+
24+
for rel_path in output_files {
25+
let abs_path = workspace_root.join(rel_path);
26+
// Skip files that no longer exist (task may delete temp files)
27+
if !abs_path.as_path().exists() {
28+
continue;
29+
}
30+
let metadata = std::fs::metadata(abs_path.as_path())?;
31+
if metadata.is_file() {
32+
let mut file = File::open(abs_path.as_path())?;
33+
let mut header = tar::Header::new_gnu();
34+
header.set_metadata(&metadata);
35+
header.set_cksum();
36+
builder.append_data(&mut header, rel_path.as_str(), &mut file)?;
37+
}
38+
}
39+
40+
builder.finish()?;
41+
Ok(())
42+
}
43+
44+
/// Extract a tar.zst archive, restoring files relative to workspace root.
45+
///
46+
/// Parent directories are created automatically. Existing files are overwritten.
47+
///
48+
/// # Errors
49+
///
50+
/// Returns an error if opening the archive or extracting entries fails.
51+
pub fn extract_output_archive(
52+
workspace_root: &AbsolutePath,
53+
archive_path: &AbsolutePath,
54+
) -> anyhow::Result<()> {
55+
let file = File::open(archive_path.as_path())?;
56+
let decoder = zstd::Decoder::new(file)?;
57+
let mut archive = tar::Archive::new(decoder);
58+
59+
archive.unpack(workspace_root.as_path())?;
60+
Ok(())
61+
}

crates/vite_task/src/session/cache/display.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
174174
}
175175
}
176176
FingerprintMismatch::InputConfig => "input configuration changed",
177+
FingerprintMismatch::OutputConfig => "output configuration changed",
177178
FingerprintMismatch::InputChanged { kind, path } => {
178179
let desc = format_input_change_str(*kind, path.as_str());
179180
return Some(vite_str::format!("○ cache miss: {desc}, executing"));

crates/vite_task/src/session/cache/mod.rs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Execution cache for storing and retrieving cached command results.
22
3+
pub mod archive;
34
pub mod display;
45

56
use std::{collections::BTreeMap, fmt::Display, fs::File, io::Write, sync::Arc, time::Duration};
@@ -15,7 +16,8 @@ use rusqlite::{Connection, OptionalExtension as _, config::DbConfig};
1516
use serde::{Deserialize, Serialize};
1617
use tokio::sync::Mutex;
1718
use vite_path::{AbsolutePath, RelativePathBuf};
18-
use vite_task_graph::config::ResolvedInputConfig;
19+
use vite_str::Str;
20+
use vite_task_graph::config::ResolvedGlobConfig;
1921
use vite_task_plan::cache_metadata::{CacheMetadata, ExecutionCacheKey, SpawnFingerprint};
2022

2123
use super::execute::{fingerprint::PostRunFingerprint, spawn::StdOutput};
@@ -38,14 +40,18 @@ pub struct CacheEntryKey {
3840
pub spawn_fingerprint: SpawnFingerprint,
3941
/// Resolved input configuration that affects cache behavior.
4042
/// Glob patterns are workspace-root-relative.
41-
pub input_config: ResolvedInputConfig,
43+
pub input_config: ResolvedGlobConfig,
44+
/// Resolved output configuration that affects cache restoration.
45+
/// Glob patterns are workspace-root-relative.
46+
pub output_config: ResolvedGlobConfig,
4247
}
4348

4449
impl CacheEntryKey {
4550
fn from_metadata(cache_metadata: &CacheMetadata) -> Self {
4651
Self {
4752
spawn_fingerprint: cache_metadata.spawn_fingerprint.clone(),
4853
input_config: cache_metadata.input_config.clone(),
54+
output_config: cache_metadata.output_config.clone(),
4955
}
5056
}
5157
}
@@ -64,6 +70,9 @@ pub struct CacheEntryValue {
6470
/// Path is relative to workspace root, value is `xxHash3_64` of file content.
6571
/// Stored in the value (not the key) so changes can be detected and reported.
6672
pub globbed_inputs: BTreeMap<RelativePathBuf, u64>,
73+
/// Filename of the output archive (e.g. `{uuid}.tar.zst`) stored alongside
74+
/// `cache.db` in the cache directory. `None` if no output files were produced.
75+
pub output_archive: Option<Str>,
6776
}
6877

6978
#[derive(Debug)]
@@ -105,6 +114,8 @@ pub enum FingerprintMismatch {
105114
},
106115
/// Found a previous cache entry key for the same task, but `input_config` differs.
107116
InputConfig,
117+
/// Found a previous cache entry key for the same task, but `output_config` differs.
118+
OutputConfig,
108119

109120
InputChanged {
110121
kind: InputChangeKind,
@@ -121,6 +132,9 @@ impl Display for FingerprintMismatch {
121132
Self::InputConfig => {
122133
write!(f, "input configuration changed")
123134
}
135+
Self::OutputConfig => {
136+
write!(f, "output configuration changed")
137+
}
124138
Self::InputChanged { kind, path } => {
125139
write!(f, "{}", display::format_input_change_str(*kind, path.as_str()))
126140
}
@@ -168,16 +182,16 @@ impl ExecutionCache {
168182
"CREATE TABLE task_fingerprints (key BLOB PRIMARY KEY, value BLOB);",
169183
(),
170184
)?;
171-
conn.execute("PRAGMA user_version = 10", ())?;
185+
conn.execute("PRAGMA user_version = 11", ())?;
172186
}
173-
1..=9 => {
187+
1..=10 => {
174188
// old internal db version. reset
175189
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)?;
176190
conn.execute("VACUUM", ())?;
177191
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)?;
178192
}
179-
10 => break, // current version
180-
11.. => {
193+
11 => break, // current version
194+
12.. => {
181195
return Err(anyhow::anyhow!("Unrecognized database version: {user_version}"));
182196
}
183197
}
@@ -233,11 +247,20 @@ impl ExecutionCache {
233247
self.get_cache_key_by_execution_key(execution_cache_key).await?
234248
{
235249
// Destructure to ensure we handle all fields when new ones are added
236-
let CacheEntryKey { spawn_fingerprint: old_spawn_fingerprint, input_config: _ } =
237-
old_cache_key;
250+
let CacheEntryKey {
251+
spawn_fingerprint: old_spawn_fingerprint,
252+
input_config: old_input_config,
253+
output_config: old_output_config,
254+
} = old_cache_key;
238255
let mismatch = if old_spawn_fingerprint == *spawn_fingerprint {
239-
// spawn fingerprint is the same but input_config or glob_base changed
240-
FingerprintMismatch::InputConfig
256+
// spawn fingerprint is the same but input_config or output_config changed
257+
if old_input_config != cache_metadata.input_config {
258+
FingerprintMismatch::InputConfig
259+
} else if old_output_config != cache_metadata.output_config {
260+
FingerprintMismatch::OutputConfig
261+
} else {
262+
FingerprintMismatch::InputConfig
263+
}
241264
} else {
242265
FingerprintMismatch::SpawnFingerprint {
243266
old: old_spawn_fingerprint,

crates/vite_task/src/session/reporter/summary.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,9 @@ impl SavedCacheMissReason {
254254
FingerprintMismatch::SpawnFingerprint { old, new } => {
255255
Self::SpawnFingerprintChanged(detect_spawn_fingerprint_changes(old, new))
256256
}
257-
FingerprintMismatch::InputConfig => Self::ConfigChanged,
257+
FingerprintMismatch::InputConfig | FingerprintMismatch::OutputConfig => {
258+
Self::ConfigChanged
259+
}
258260
FingerprintMismatch::InputChanged { kind, path } => {
259261
Self::InputChanged { kind: *kind, path: Str::from(path.as_str()) }
260262
}

0 commit comments

Comments
 (0)