Skip to content

Commit 6198f6b

Browse files
branchseerclaude
andcommitted
feat(cache): auto-detect output writes via fspy
Adds `{ auto: true }` support to the `output` field, plus the implicit default: when `output` is omitted, automatically tracks files the task writes (via fspy) and archives them. Explicit globs and `auto` can be mixed in the same array. Also includes: - `read_write_overlap` check: if a task writes to a file it also read (auto-inferred), the cache update is skipped (`InputModified`). Prerun input hashes would otherwise be stale. - Input negatives apply to reads only, not writes — keeps `input: ["!dist/**"]` from accidentally dropping writes to `dist/**` during archiving. - Input-auto gating: when `input_config.includes_auto` is false, fspy reads do not contribute to the post-run fingerprint, even when fspy is enabled solely for output tracking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6d03bc0 commit 6198f6b

93 files changed

Lines changed: 1211 additions & 464 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- **Added** `output` field for cached tasks: archives output files matching the configured globs after a successful run and restores them on cache hit. Patterns are relative to the package directory; supports negative patterns (e.g. `"!dist/cache/**"`) and `{pattern, base}` form for explicit base. ([#321](https://github.com/voidzero-dev/vite-task/pull/321))
44
- **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))
55
- **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))
6+
- **Added** `output` field for cached tasks: archives output files after a successful run and restores them on cache hit. Defaults to automatically tracking files the task writes; accepts globs (e.g. `"dist/**"`), `{ "auto": true }`, and negative patterns (`"!dist/cache/**"`) ([#321](https://github.com/voidzero-dev/vite-task/pull/321))
67
- **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))
78
- **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))
89
- **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))

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", │

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

Lines changed: 201 additions & 133 deletions
Large diffs are not rendered by default.

crates/vite_task/src/session/execute/tracked_accesses.rs

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
//! Normalize raw fspy path accesses into workspace-relative, filtered form.
2+
//!
3+
//! User-configured negative globs are NOT applied here. They are applied later,
4+
//! separately for reads (input config) and writes (output config), since those
5+
//! two configs are independent.
26
#![cfg(fspy)]
37

48
use std::collections::hash_map::Entry;
@@ -21,22 +25,19 @@ pub struct TrackedPathAccesses {
2125
}
2226

2327
impl TrackedPathAccesses {
24-
/// Build from fspy's raw iterable by stripping the workspace prefix,
25-
/// normalizing `..` components, and filtering against the negative globs.
26-
pub fn from_raw(
27-
raw: &PathAccessIterable,
28-
workspace_root: &AbsolutePath,
29-
resolved_negatives: &[wax::Glob<'static>],
30-
) -> Self {
28+
/// Build from fspy's raw iterable by stripping the workspace prefix and
29+
/// normalizing `..` components. `.git/*` paths are skipped. User-configured
30+
/// negatives are applied by the caller (see module docs).
31+
pub fn from_raw(raw: &PathAccessIterable, workspace_root: &AbsolutePath) -> Self {
3132
let mut accesses = Self::default();
3233
for access in raw.iter() {
33-
// Strip workspace root, clean `..` components, and filter in one pass.
34+
// Strip workspace root and clean `..` components in one pass.
3435
// fspy may report paths like `packages/sub-pkg/../shared/dist/output.js`.
3536
let relative_path = access.path.strip_path_prefix(workspace_root, |strip_result| {
3637
let Ok(stripped_path) = strip_result else {
3738
return None;
3839
};
39-
normalize_tracked_workspace_path(stripped_path, resolved_negatives)
40+
normalize_tracked_workspace_path(stripped_path)
4041
});
4142

4243
let Some(relative_path) = relative_path else {
@@ -71,10 +72,7 @@ impl TrackedPathAccesses {
7172
clippy::disallowed_types,
7273
reason = "fspy strip_path_prefix exposes std::path::Path; convert to RelativePathBuf immediately"
7374
)]
74-
fn normalize_tracked_workspace_path(
75-
stripped_path: &std::path::Path,
76-
resolved_negatives: &[wax::Glob<'static>],
77-
) -> Option<RelativePathBuf> {
75+
fn normalize_tracked_workspace_path(stripped_path: &std::path::Path) -> Option<RelativePathBuf> {
7876
// On Windows, paths are possible to be still absolute after stripping the workspace root.
7977
// For example: c:\workspace\subdir\c:\workspace\subdir
8078
// Just ignore those accesses.
@@ -90,12 +88,6 @@ fn normalize_tracked_workspace_path(
9088
return None;
9189
}
9290

93-
if !resolved_negatives.is_empty()
94-
&& resolved_negatives.iter().any(|neg| wax::Program::is_match(neg, relative.as_str()))
95-
{
96-
return None;
97-
}
98-
9991
Some(relative)
10092
}
10193

@@ -111,8 +103,7 @@ mod tests {
111103
clippy::disallowed_types,
112104
reason = "normalize_tracked_workspace_path requires std::path::Path for fspy strip_path_prefix output"
113105
)]
114-
let relative_path =
115-
normalize_tracked_workspace_path(std::path::Path::new(r"foo\C:\bar"), &[]);
106+
let relative_path = normalize_tracked_workspace_path(std::path::Path::new(r"foo\C:\bar"));
116107
assert!(relative_path.is_none());
117108
}
118109
}

crates/vite_task_bin/tests/e2e_snapshots/fixtures/input_cache_test/snapshots/fspy_env___not_set_when_auto_inference_disabled.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ should not see `FSPY` set.
77

88
```
99
$ vtt print-env FSPY
10-
(undefined)
10+
1
1111
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v1
Lines changed: 215 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,226 @@
11
[[e2e]]
2-
name = "output_globs___files_restored_on_cache_hit"
2+
name = "auto_output___files_restored_on_cache_hit"
33
comment = """
4-
With explicit output globs (`dist/**`), the first run writes a file to
5-
`dist/`. After deleting `dist/`, a second run with no input changes is a
6-
cache hit and the archived output file is restored.
4+
Auto output detection (default): output files written by the task are
5+
restored on a cache hit.
76
"""
87
steps = [
9-
# First run - cache miss, writes dist/output.txt
10-
["vt", "run", "build"],
11-
# Verify file was written
12-
["vtt", "print-file", "dist/output.txt"],
13-
# Delete dist/ to prove restoration is real
14-
["vtt", "rm", "-rf", "dist"],
15-
# Second run - cache hit, restores dist/output.txt from archive
16-
["vt", "run", "build"],
17-
# File should be restored
18-
["vtt", "print-file", "dist/output.txt"],
8+
{ argv = [
9+
"vt",
10+
"run",
11+
"auto-output",
12+
], comment = "first run writes dist/out.txt" },
13+
{ argv = [
14+
"vtt",
15+
"print-file",
16+
"dist/out.txt",
17+
], comment = "output exists after the build" },
18+
{ argv = [
19+
"vtt",
20+
"rm",
21+
"dist/out.txt",
22+
], comment = "delete only the file (keep dir to avoid an fspy inferred-input miss on Windows)" },
23+
{ argv = [
24+
"vt",
25+
"run",
26+
"auto-output",
27+
], comment = "cache hit, should restore dist/out.txt" },
28+
{ argv = [
29+
"vtt",
30+
"print-file",
31+
"dist/out.txt",
32+
], comment = "output was restored from the archive" },
1933
]
2034

2135
[[e2e]]
22-
name = "output_globs___negative_excludes_files_from_archive"
36+
name = "glob_output___only_matched_files_restored"
2337
comment = """
24-
A file matched by a negative output glob is not archived, so it is not
25-
restored on cache hit.
38+
Glob output: only files matching the configured output globs are restored
39+
on a cache hit; files produced outside those globs are left alone.
2640
"""
2741
steps = [
28-
# First run - writes both dist/keep.txt and dist/skip.txt
29-
["vt", "run", "build-with-negative"],
30-
# Both files exist after the run
31-
["vtt", "print-file", "dist/keep.txt"],
32-
["vtt", "print-file", "dist/skip.txt"],
33-
# Delete dist/ to prove restoration is real
34-
["vtt", "rm", "-rf", "dist"],
35-
# Second run - cache hit, only dist/keep.txt is restored
36-
["vt", "run", "build-with-negative"],
37-
["vtt", "print-file", "dist/keep.txt"],
42+
{ argv = [
43+
"vt",
44+
"run",
45+
"glob-output",
46+
], comment = "first run writes dist/out.txt and tmp/temp.txt" },
47+
{ argv = [
48+
"vtt",
49+
"rm",
50+
"dist/out.txt",
51+
], comment = "delete the glob-matched output" },
52+
{ argv = [
53+
"vtt",
54+
"rm",
55+
"tmp/temp.txt",
56+
], comment = "delete the non-matched output" },
57+
{ argv = [
58+
"vt",
59+
"run",
60+
"glob-output",
61+
], comment = "cache hit: should restore dist/out.txt but not tmp/temp.txt" },
62+
{ argv = [
63+
"vtt",
64+
"print-file",
65+
"dist/out.txt",
66+
], comment = "dist/out.txt was restored" },
67+
{ argv = [
68+
"vtt",
69+
"print-file",
70+
"tmp/temp.txt",
71+
], comment = "should fail - tmp not in output globs" },
72+
]
73+
74+
[[e2e]]
75+
name = "auto_output_with_non_auto_input"
76+
comment = """
77+
Auto output works even when input tracking is explicit
78+
(`input: [\"src/**\"]`). Output auto-detection and input auto-detection
79+
are independent.
80+
"""
81+
steps = [
82+
{ argv = [
83+
"vt",
84+
"run",
85+
"auto-output-no-auto-input",
86+
], comment = "first run: input src/** (no auto), output default (auto)" },
87+
{ argv = [
88+
"vtt",
89+
"rm",
90+
"dist/out.txt",
91+
], comment = "delete the output" },
92+
{ argv = [
93+
"vt",
94+
"run",
95+
"auto-output-no-auto-input",
96+
], comment = "cache hit - output files should still be restored" },
97+
{ argv = [
98+
"vtt",
99+
"print-file",
100+
"dist/out.txt",
101+
], comment = "output was restored" },
102+
]
103+
104+
[[e2e]]
105+
name = "negative_output___excluded_files_not_restored"
106+
comment = """
107+
Negative output globs exclude matching files from the cache archive, so
108+
they are not restored on a cache hit even though the task produced them.
109+
"""
110+
steps = [
111+
{ argv = [
112+
"vt",
113+
"run",
114+
"negative-output",
115+
], comment = "first run writes dist/out.txt and dist/cache/tmp.txt" },
116+
{ argv = [
117+
"vtt",
118+
"rm",
119+
"dist/out.txt",
120+
], comment = "delete the archived output" },
121+
{ argv = [
122+
"vtt",
123+
"rm",
124+
"dist/cache/tmp.txt",
125+
], comment = "delete the excluded output" },
126+
{ argv = [
127+
"vt",
128+
"run",
129+
"negative-output",
130+
], comment = "cache hit: should restore dist/out.txt but NOT dist/cache/tmp.txt" },
131+
{ argv = [
132+
"vtt",
133+
"print-file",
134+
"dist/out.txt",
135+
], comment = "dist/out.txt was restored" },
136+
{ argv = [
137+
"vtt",
138+
"print-file",
139+
"dist/cache/tmp.txt",
140+
], comment = "should fail - excluded by !dist/cache/**" },
141+
]
142+
143+
[[e2e]]
144+
name = "output_config_change_invalidates_cache"
145+
comment = """
146+
Changing a task's output configuration invalidates the cache entry —
147+
the next run is a miss, not a hit of the stale archive.
148+
"""
149+
steps = [
150+
{ argv = [
151+
"vt",
152+
"run",
153+
"output-config-change",
154+
], comment = "first run populates the cache" },
155+
{ argv = [
156+
"vtt",
157+
"replace-file-content",
158+
"vite-task.json",
159+
"REPLACE_ME",
160+
"dist/**",
161+
], comment = "change the output globs in the task config" },
162+
{ argv = [
163+
"vt",
164+
"run",
165+
"output-config-change",
166+
], comment = "cache miss: output config changed" },
167+
]
168+
169+
[[e2e]]
170+
name = "input_negative_does_not_drop_output_writes"
171+
comment = """
172+
Input negative globs must not drop matching writes from the output
173+
archive. Here the user excludes `dist/**` from inferred inputs (so
174+
rewriting dist/ won't mark inputs as modified), but default auto output
175+
should still capture dist writes and restore them on a cache hit.
176+
"""
177+
steps = [
178+
{ argv = [
179+
"vt",
180+
"run",
181+
"input-neg-dist-auto-output",
182+
], comment = "first run writes dist/out.txt" },
183+
{ argv = [
184+
"vtt",
185+
"rm",
186+
"dist/out.txt",
187+
], comment = "delete the output" },
188+
{ argv = [
189+
"vt",
190+
"run",
191+
"input-neg-dist-auto-output",
192+
], comment = "cache hit should restore dist/out.txt" },
193+
{ argv = [
194+
"vtt",
195+
"print-file",
196+
"dist/out.txt",
197+
], comment = "output was restored despite dist/** being a negative input glob" },
198+
]
199+
200+
[[e2e]]
201+
name = "explicit_input_ignores_fspy_reads"
202+
comment = """
203+
When input auto is disabled (explicit globs only), unrelated reads
204+
tracked by fspy must NOT become inferred inputs. Default auto output
205+
still needs fspy for write tracking, but reads outside `input: [\"src/**\"]`
206+
should be ignored.
207+
"""
208+
steps = [
209+
{ argv = [
210+
"vt",
211+
"run",
212+
"explicit-input-auto-output",
213+
], comment = "first run reads README.md (not in input globs) and captures the output" },
214+
{ argv = [
215+
"vtt",
216+
"replace-file-content",
217+
"README.md",
218+
"v1",
219+
"v2",
220+
], comment = "modify README.md — not an input, so it must not invalidate the cache" },
221+
{ argv = [
222+
"vt",
223+
"run",
224+
"explicit-input-auto-output",
225+
], comment = "cache hit: README.md not in input globs" },
38226
]

0 commit comments

Comments
 (0)