How do you configure runtime classpath normalization to ignore a volatile file like build-info.properties, and why is it needed?
answer
- settings.gradle(.kts) normalization block
- runtimeClasspath { ignore(...) }
- pattern matches path INSIDE the jar
- metaInf.ignoreAttribute for manifest
- fixes always-out-of-date / cache misses
basics
~10 sIn settings.gradle(.kts) use normalization { runtimeClasspath { ignore 'build-info.properties' } }. It strips that file from the runtime-classpath fingerprint so a volatile, build-stamped entry doesn't cause needless cache misses or out-of-date tasks.
solid answer
~40 sSome files on the runtime classpath change on every build but don't affect behaviour — a `build-info.properties` with a timestamp/commit hash, a generated manifest, etc. Because `@Classpath` hashes jar **contents**, such a volatile entry flips the fingerprint each build, so cacheable tasks miss and up-to-date checks fail. **Runtime classpath normalization**, configured in `settings.gradle.kts`, lets you exclude those entries from the fingerprint: ```kotlin normalization { runtimeClasspath { ignore("build-info.properties") } } ``` The ignored pattern is matched against the path *inside* each jar/dir on the runtime classpath, so the file's presence and content no longer participate in the hash. This restores up-to-date-ness and build-cache hits without changing what's actually packaged. It's settings-level so it applies consistently to all projects. There's also `metaInf { ignoreAttribute(...) }` and `ignoreManifest()` for volatile manifest attributes.
code
kotlin · 9 lines// settings.gradle.kts
normalization {
runtimeClasspath {
ignore("build-info.properties")
metaInf {
ignoreAttribute("Implementation-Version")
}
}
}go deeper
Recognize the symptom (always out-of-date) and that a normalization ignore rule fixes it.
Write the settings-level block correctly and explain matching is against the in-jar path.
Weigh the correctness trade-off of ignoring and choose metaInf attribute-level ignores over blanket ones.
Set org-wide conventions for which generated stamps are safe to normalize and enforce them across repos.
## The problem: volatile classpath entries Runtime-classpath fingerprinting (via `@Classpath`) hashes the logical contents of every jar and class dir. If one of those entries is regenerated on every build with embedded volatile data — a build timestamp, Git SHA, CI build number in a `build-info.properties`, or a `MANIFEST.MF` with `Build-Date` — then the fingerprint changes every single build. Consequences: - Cacheable tasks that consume the runtime classpath always **miss** the build cache. - Up-to-date checks always report **out-of-date**, so the work re-runs. The data is irrelevant to the task's *behaviour*, so this is pure waste. ## The fix: normalization rules Gradle lets you declare **normalization** rules — instructions that strip volatile parts out of the fingerprint *before* hashing. They live in `settings.gradle(.kts)` so they apply build-wide: ```kotlin normalization { runtimeClasspath { // path is matched relative to the root of each classpath entry (jar/dir) ignore("build-info.properties") ignore("**/*-version.txt") metaInf { ignoreAttribute("Implementation-Version") ignoreProperty("app.build.timestamp") } } } ``` - `ignore(pattern)` — drops matching files entirely from the fingerprint (Ant-style globs). - `metaInf { ignoreAttribute(...) }` — drops a single volatile `MANIFEST.MF` attribute while keeping the rest. - `metaInf { ignoreProperty(...) }` — drops a volatile key from properties files under `META-INF`. - `metaInf { ignoreManifest() }` / `ignoreCompletely()` — broader nukes when needed. ## Scope and matching - It applies to the **runtime** classpath fingerprint only — compile classpaths already ignore most volatile data via ABI extraction. - The pattern matches the path *inside* each entry (e.g. inside the jar), not the on-disk path of the jar itself. - Because it's in `settings`, it's global and consistent across modules and across machines, which is what you want for cache correctness. ## Don't over-ignore Ignoring a file means changes to it will NOT trigger reruns. If that file ever does affect behaviour, you've created a correctness hole — the task can be stale. Scope ignores narrowly to truly volatile, behaviour-irrelevant entries.
- Why is the normalization block placed in settings.gradle rather than a build.gradle?So the rules apply consistently to every project in the build and to all machines sharing the cache. Cache keys must agree across modules, so a global declaration avoids per-module drift.
- What is the risk of ignoring a file in normalization?If that file actually does influence behaviour, the task can become stale — Gradle won't re-run it when the file changes. Only ignore truly volatile, behaviour-irrelevant entries.
- Does ignore() match the on-disk jar path or the path inside the jar?The path inside each classpath entry (e.g. inside the jar), not the absolute path of the jar on disk.
saying these in an interview costs you the question
- Putting the normalization block in build.gradle of a single module and expecting global effect.
- Thinking ignore() removes the file from the packaged artifact — it only removes it from the fingerprint.
- Over-broadly ignoring files that actually affect runtime behaviour.