How do you make a TransformAction's results cacheable in the build cache, and what determines whether two invocations are considered identical?
answer
- @CacheableTransform on the class
- @InputArtifact needs @PathSensitive/@Classpath
- key = normalized input + params + impl
- missing normalization -> cross-machine misses
- cheap transforms can skip it
basics
~10 sAnnotate the action class with @CacheableTransform and give the @InputArtifact a @PathSensitive (or @Classpath/@Normalize) annotation. Two invocations match when the input artifact's normalized content and all parameter inputs hash equal.
solid answer
~50 sBy default a transform's result is reused within a build via Gradle's in-memory/workspace caching, but storing it in the **build cache** (shareable across builds and machines) requires opting in: 1. Annotate the action class with `@CacheableTransform`. 2. Declare normalization on the input artifact — e.g. `@get:PathSensitive(PathSensitivity.NONE)` or, for classpath-like inputs, `@get:Classpath` / `@get:CompileClasspath` — on the `@InputArtifact` property. 3. Annotate each parameter property the same way you would task inputs (`@Input`, `@InputFiles` + `@PathSensitive`). The cache key is computed from the **normalized** input artifact content plus all normalized parameter inputs (and the action's implementation classpath). Two invocations are identical iff every one of those hashes matches. Without `@CacheableTransform` and proper path sensitivity, results stay local and may even miss within a session, so cheap transforms are fine but expensive ones (instrumentation, code-gen) should always be made cacheable.
code
kotlin · 11 lines@CacheableTransform
abstract class GenSources : TransformAction<Params> {
@get:InputArtifact
@get:PathSensitive(PathSensitivity.NONE)
abstract val inputArtifact: Provider<FileSystemLocation>
override fun transform(outputs: TransformOutputs) {
val out = outputs.dir("generated")
generate(inputArtifact.get().asFile, out, parameters.flags.get())
}
}go deeper
Aware transforms can be cached but not the exact annotations.
Knows @CacheableTransform plus normalization on @InputArtifact is required.
Explains the full cache key (normalized input + params + impl) and the cross-machine path-sensitivity pitfall.
Sets policy on which transforms warrant build-cache participation and audits normalization for portable, high-hit caching.
## Two layers of reuse 1. **Within a build / workspace:** Gradle already avoids transforming the same artifact twice in a run and keeps results in a per-user workspace. 2. **Build cache (cross-build, cross-machine):** opt-in, lets CI and teammates skip the work entirely. This is what `@CacheableTransform` unlocks. ## Making a transform cacheable ```kotlin @CacheableTransform abstract class Instrument : TransformAction<Params> { @get:InputArtifact @get:Classpath abstract val inputArtifact: Provider<FileSystemLocation> override fun transform(outputs: TransformOutputs) { /* ... */ } } ``` Three requirements: - **`@CacheableTransform` on the class.** Without it, results are never stored in the build cache. - **Normalization on `@InputArtifact`.** You must say how the input's path/content is normalized: `@PathSensitive(PathSensitivity.NONE)` (content only), `@Classpath` (jar/dir treated as a classpath entry, order/ timestamps normalized), or `@CompileClasspath`. Omitting this with `@CacheableTransform` is an error. - **Annotated parameters.** Every parameter property must declare its input semantics so it contributes to the key deterministically. ## What makes two invocations 'the same' Gradle computes a cache key from: 1. The **normalized** input artifact (its content, normalized per your annotation — not its absolute path). 2. The **normalized parameters** (each parameter's annotated value/content). 3. The transform **implementation** (the action class and its classpath) — change the code, change the key. If all hash identically, the stored outputs are replayed; otherwise the action runs and the result is stored. This is exactly analogous to a `@CacheableTask`. ## Why normalization is the crux The single most common cause of "why isn't my transform caching across CI and dev?" is path sensitivity. If the input artifact contributes its absolute path to the key (the default without an explicit `@PathSensitive`/`@Classpath`), then different checkout directories yield different keys and never share cache entries. Declaring `@Classpath` or `@PathSensitive(NONE)` makes the key content-addressed and portable. ## When to bother For a trivial transform (rename, tiny filter) the in-build reuse is enough. For expensive ones — bytecode instrumentation, code generation, native compilation — make them `@CacheableTransform` so the work is paid once across the whole team and CI.
- What error do you hit if you add @CacheableTransform but no path-sensitivity on @InputArtifact?Gradle reports a validation problem — a cacheable transform must declare normalization (e.g. @PathSensitive or @Classpath) on its input artifact.
- Does changing the action's source code invalidate the cache?Yes — the implementation classpath is part of the key, so editing the transform class produces a new key and re-runs it.
- Should every transform be @CacheableTransform?No — cheap transforms gain little and add key-computation overhead; reserve it for expensive work like instrumentation or code generation.
saying these in an interview costs you the question
- Thinking results are shared across machines without @CacheableTransform.
- Adding @CacheableTransform but leaving @InputArtifact without @PathSensitive/@Classpath.
- Believing the action's code changes don't affect the cache key.