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?
answer
- --info → 'Input property X has changed'
- org.gradle.caching.debug=true to see per-entry hashes
- usual culprit: timestamped properties / manifest attr
- fix: normalization ignore / metaInf.ignoreAttribute
- verify @Classpath not @InputFiles
basics
~10 sRun 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 sFirst 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# 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 hashgo deeper
Know to run with --info to see which input changed; recognize a timestamp file as a likely cause.
Use caching debug to find the entry and apply a targeted normalization ignore.
Systematically rule out relocatability (@InputFiles) vs volatile content, and prefer reproducible jars + narrow ignores.
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.