skip to content

Your custom task uses @InputFiles for a jar classpath, and the build cache never hits across CI agents. Walk through diagnosing and fixing the input normalization.

level: principalimportance: should knowfreq 22%

answer

  1. caching.debug / --scan to see fingerprints
  2. @InputFiles defaults to ABSOLUTE
  3. @Classpath = path-insensitive + jar-normalized
  4. reproducible archives: no timestamps, stable order
  5. convention plugin + validation to prevent regression

basics

~20 s

Plain @InputFiles defaults to ABSOLUTE path sensitivity and full byte content, so jar timestamps and agent paths bust the fingerprint. Switch the property to @Classpath (or @CompileClasspath for compile inputs) so paths are ignored and jars are normalized, making the fingerprint portable.

solid answer

~40 s

First confirm the symptom with `--scan` or `-Dorg.gradle.caching.debug=true`, which prints why a task missed the cache and what its input fingerprint was. A classpath declared as bare `@InputFiles` defaults to ABSOLUTE path sensitivity and hashes full jar bytes — so per-agent absolute paths and non-reproducible jar metadata (timestamps) make every agent's fingerprint unique. The fix is to declare the input with classpath semantics: `@Classpath` for a runtime classpath (order-significant, jar-normalized, path-insensitive) or `@CompileClasspath` if the input feeds compilation (ABI-only). That alone usually restores cross-agent hits. If non-reproducible jars are also involved, complement it by making upstream archives reproducible (preserveFileTimestamps = false, reproducibleFileOrder = true). At org scale, encode the classpath annotation in a convention plugin and validate it, so no custom task ships a classpath as plain @InputFiles again.

code

kotlin · 9 lines
kotlin
tasks.withType<AbstractArchiveTask>().configureEach {
    isPreserveFileTimestamps = false
    isReproducibleFileOrder = true
}

abstract class LaunchTask : DefaultTask() {
    @get:Classpath
    abstract val classpath: ConfigurableFileCollection
}

go deeper

for a junior

Recognize that a plain @InputFiles classpath caches poorly and @Classpath is the better annotation.

for a middle

Explain ABSOLUTE-default vs @Classpath normalization and use caching.debug to diagnose a miss.

for a senior

Diagnose both wrong-normalization and non-reproducible-jar causes and apply both fixes.

for a principal

Treat it as governance: encode normalization in convention plugins, add validation/CI guards, and monitor shared-cache hit-rate to prevent regressions across the org.

## Step 1 — Reproduce and observe Enable cache diagnostics: run with `--build-cache -Dorg.gradle.caching.debug=true` or publish a build scan with `--scan`. The output shows each task's individual input property fingerprints and the resulting cache key. Compare the same task across two agents: you'll see the classpath property's hash differs even though the dependencies are identical. ## Step 2 — Identify the root cause Two failure modes typically combine: 1. **Wrong normalization.** A property declared `@InputFiles` (without classpath annotation) defaults to `ABSOLUTE` path sensitivity. Agent A resolves jars under `/agentA/caches/...`, agent B under `/agentB/...`; the absolute paths differ, so the fingerprint differs and the cache never matches. 2. **Non-reproducible jar content.** Even with paths normalized, jars built with embedded timestamps or unstable entry order hash differently per build, busting the fingerprint. ## Step 3 — Apply classpath semantics Replace the generic annotation with a classpath-aware one: ```kotlin abstract class LaunchTask : DefaultTask() { // BEFORE: order/path/byte sensitive, defaults to ABSOLUTE // @get:InputFiles abstract val classpath: ConfigurableFileCollection // AFTER: path-insensitive, jar-normalized runtime classpath @get:Classpath abstract val classpath: ConfigurableFileCollection } ``` `@Classpath` ignores the absolute location of jars and normalizes their internal entries, so two agents resolving the same dependencies fingerprint equally. If the classpath feeds a compiler, use `@CompileClasspath` instead to additionally collapse non-ABI changes. ## Step 4 — Make jars reproducible For archives you produce, ensure deterministic content so even content hashing is stable: ```kotlin tasks.withType<AbstractArchiveTask>().configureEach { isPreserveFileTimestamps = false isReproducibleFileOrder = true } ``` ## Step 5 — Prevent regression at scale This is a governance problem, not a one-off. Put the classpath annotation in your **convention plugin's** task types, and rely on Gradle's task validation (which warns when a cacheable task's file input lacks proper normalization) plus a CI check that no custom classpath input is declared as plain `@InputFiles`. Track shared-cache hit rate as a metric; a sudden drop usually means a new task shipped with the wrong normalization. ## Summary The portability of a classpath input is governed by its declared normalization. Generic `@InputFiles` is ABSOLUTE and byte-exact; `@Classpath`/`@CompileClasspath` are path-insensitive and jar/ABI-normalized. Choosing the right annotation — and keeping archives reproducible — is what makes a remote cache actually shared across agents.

  • How do you confirm WHY a task missed the build cache?
    Run with -Dorg.gradle.caching.debug=true (or use a --scan) to print each input property's fingerprint and the cache key. Diff the classpath property's hash across two agents to see the location/byte difference.
  • After switching to @Classpath the cache still misses sometimes — what else?
    Likely non-reproducible jars: embedded timestamps or unstable entry order make the normalized content hash differ. Set preserveFileTimestamps=false and reproducibleFileOrder=true on archive tasks upstream.
  • Why prefer @CompileClasspath if the input feeds a compiler?
    It fingerprints only the ABI, so recompiled-but-unchanged-API upstream jars don't bust the consumer's fingerprint, adding compile avoidance on top of path portability.

saying these in an interview costs you the question

  • Blaming the remote cache infrastructure before checking input normalization.
  • Switching everything to ABSOLUTE 'to be safe' — that guarantees non-portability.
  • Fixing one task by hand without addressing the convention-plugin/validation gap that lets it recur.

context