skip to content

A cacheable task that consumes a runtime classpath always reports out-of-date even when nothing meaningful changed. How would you diagnose and fix it?

level: seniorimportance: should knowfreq 35%

answer

  1. --info → 'Input property X has changed'
  2. org.gradle.caching.debug=true to see per-entry hashes
  3. usual culprit: timestamped properties / manifest attr
  4. fix: normalization ignore / metaInf.ignoreAttribute
  5. verify @Classpath not @InputFiles

basics

~10 s

Run with --info or build-cache debug to see which input changed, find the volatile classpath entry (e.g. a timestamped properties file or manifest attribute), then add a runtimeClasspath normalization ignore/metaInf.ignoreAttribute rule in settings.

solid answer

~50 s

First confirm it's an input-fingerprint issue: run the task twice with `--info` and look for the *'Input property ... has changed'* message naming the offending classpath property. To find the exact entry, enable `org.gradle.caching.debug=true` (or use a build scan), which prints the per-entry hashes of the classpath fingerprint between the two runs — the differing entry is the culprit, typically a regenerated jar containing a `build-info.properties` with a timestamp/Git SHA, or a `MANIFEST.MF` with `Build-Date`/`Implementation-Version`. The fix is **runtime classpath normalization** in `settings.gradle.kts`: ```kotlin normalization { runtimeClasspath { ignore("build-info.properties") metaInf { ignoreAttribute("Build-Date") } } } ``` Also verify the producing task isn't embedding `System.currentTimeMillis()` unnecessarily — sometimes the better fix is to stop generating volatile data. Confirm the custom task uses `@Classpath` (not `@InputFiles` with absolute paths) so relocation across machines doesn't itself cause misses.

code

bash · 3 lines
bash
# Pinpoint the volatile classpath entry
./gradlew :app:myTask -Dorg.gradle.caching.debug=true --rerun-tasks --info
# look for 'Input property ... has changed' and the differing entry hash

go deeper

for a junior

Know to run with --info to see which input changed; recognize a timestamp file as a likely cause.

for a middle

Use caching debug to find the entry and apply a targeted normalization ignore.

for a senior

Systematically rule out relocatability (@InputFiles) vs volatile content, and prefer reproducible jars + narrow ignores.

for a principal

Establish team playbooks (build scans, reproducible-build settings, normalization conventions) so this class of cache miss doesn't recur.

## Step 1 — Is it really the inputs? 'Always out-of-date' has a few causes: missing/changing outputs, an input file whose content changes every build, or non-deterministic task configuration. For a classpath-consuming task, the usual suspect is a **volatile classpath entry**. Run: ```bash ./gradlew :app:myTask --info ``` twice and look for lines like *'Input property runtimeJars has changed for task ...'*. That tells you which property's fingerprint flipped. ## Step 2 — Find the offending entry To pinpoint the entry inside the classpath, turn on cache debugging: ```bash ./gradlew :app:myTask -Dorg.gradle.caching.debug=true --rerun-tasks ``` Gradle then logs the individual hashes that make up the classpath fingerprint. Compare two runs (a **build scan** does this nicely via the *Performance → Task inputs* comparison). The entry whose hash changes between identical-source builds is the volatile one — commonly: - a generated `build-info.properties` / `git.properties` carrying a timestamp or commit hash, - a `META-INF/MANIFEST.MF` attribute such as `Build-Date`, `Build-Jdk`, `Implementation-Version`, - a reproducible-build offender like jar entry timestamps (mitigated by `Jar { isReproducibleFileOrder = true; isPreserveFileTimestamps = false }`). ## Step 3 — Fix it Prefer the **least powerful** fix: 1. **Stop generating volatile data** if it isn't needed (don't stamp a timestamp into a properties file consumed downstream). 2. **Normalize it away** when the data must exist but shouldn't affect fingerprints: ```kotlin // settings.gradle.kts normalization { runtimeClasspath { ignore("build-info.properties") metaInf { ignoreAttribute("Build-Date") ignoreAttribute("Build-Jdk") } } } ``` 3. **Make jars reproducible** so identical inputs produce byte-stable jars. ## Step 4 — Verify the annotation For a *custom* task, double-check the property is `@Classpath` / `@CompileClasspath` rather than `@InputFiles`. With `@InputFiles` and default `ABSOLUTE` path sensitivity, simply running on a different machine (different dependency-cache path) changes the fingerprint, producing perpetual cache misses that masquerade as 'always out-of-date'. Switching to `@Classpath` makes the task relocatable. ## Step 5 — Confirm Re-run twice; the second run should report the task `UP-TO-DATE` (or a cache hit). A build scan's task timeline confirms the win.

  • How could the same task be 'always out-of-date' only on CI but fine locally?
    Likely a relocatability problem: a custom @InputFiles property with absolute path sensitivity, so a different workspace/dependency-cache path on CI changes the fingerprint. Switching to @Classpath fixes it.
  • Besides normalization, what makes jars themselves cache-friendly?
    Reproducible jars: set Jar.isPreserveFileTimestamps = false and isReproducibleFileOrder = true so identical class inputs yield byte-identical archives.

saying these in an interview costs you the question

  • Jumping straight to --rerun-tasks as a 'fix' instead of diagnosing the volatile input.
  • Ignoring an entire jar instead of the single volatile file/attribute.
  • Not checking whether the root cause is @InputFiles absolute path sensitivity.

context