skip to content

How does runtime classpath normalization help cache hits, and how do you configure it to ignore volatile files inside artifacts?

level: seniorimportance: should knowfreq 30%

answer

  1. @Classpath = order-sensitive, metadata-insensitive
  2. volatile build-info.properties busts test
  3. normalization.runtimeClasspath { ignore(...) }
  4. ignoreProperty for specific keys
  5. metaInf ignoreAttribute for manifest

basics

~20 s

Runtime classpath normalization lets you tell Gradle to ignore specific files (like build-stamped property files) when fingerprinting a runtime classpath, so a harmless change inside a jar doesn't bust up-to-date or cache for tasks like test.

solid answer

~40 s

A runtime classpath is fingerprinted order-sensitively (jar order matters) but Gradle already ignores jar timestamps/metadata so two jars with identical entries hash the same. The remaining problem is **volatile content inside artifacts** — e.g. a `build-info.properties` carrying a build timestamp or commit hash. That file changes every build, so the classpath fingerprint changes, and downstream consumers like `test` rerun even though behavior is identical. You fix this with `normalization.runtimeClasspath` in the build, declaring `ignore("build-info.properties")` (and optionally `metaInf { ... }` rules) so those entries are excluded from the fingerprint. You can also normalize property files by ignoring specific keys. The result: stable classpath fingerprints across builds and machines, so test/run tasks stay UP-TO-DATE or hit the cache despite incidental, non-behavioral changes inside dependencies.

code

kotlin · 10 lines
kotlin
normalization {
    runtimeClasspath {
        ignore("build-info.properties")
        properties("META-INF/git.properties") {
            ignoreProperty("git.build.time")
            ignoreProperty("git.commit.id")
        }
        metaInf { ignoreAttribute("Build-Jdk") }
    }
}

go deeper

for a junior

Know that you can tell Gradle to ignore certain files inside jars (like a build timestamp) so cache/up-to-date isn't broken.

for a middle

Configure normalization.runtimeClasspath with ignore() and explain the volatile-file scenario it fixes.

for a senior

Use ignoreProperty/metaInf precisely, distinguish from @PathSensitive and @CompileClasspath ABI normalization, and tie it to remote-cache hit rate.

for a principal

Mandate normalization wherever build metadata is stamped into artifacts as part of an org cache-hit-rate baseline, and review for over-broad ignores that hide real changes.

## What a runtime classpath fingerprint is When a task declares an input annotated `@Classpath` (or `@CompileClasspath`), Gradle treats it as a **classpath**, not a plain file list. For a runtime classpath the fingerprint is **order-sensitive across jars** but **insensitive to each jar's own irrelevant metadata** — Gradle hashes the *contents* of each jar entry, ignoring file timestamps and the jar's central-directory ordering. So rebuilding an upstream jar with the same class bytes yields the same fingerprint even though the jar bytes differ. ## The volatile-content problem That default still breaks when a jar contains a file whose bytes legitimately change every build but don't affect runtime behavior — classic example: a generated `build-info.properties` or `git.properties` carrying a timestamp/commit hash. Because its bytes differ, the entry's hash differs, the jar's content hash differs, the classpath fingerprint differs, and every consumer (notably `test`) reruns. You get cache misses for a non-functional change. ## Runtime classpath normalization Gradle lets you declare normalization rules that are applied **before** fingerprinting any runtime classpath in the build: ```kotlin normalization { runtimeClasspath { // drop this entry from every runtime-classpath fingerprint ignore("build-info.properties") // ignore volatile keys within a properties file rather than the whole file properties("META-INF/build-info.properties") { ignoreProperty("timestamp") ignoreProperty("commit") } // normalize MANIFEST attributes that change per build metaInf { ignoreAttribute("Built-By") ignoreAttribute("Build-Jdk") } } } ``` - `ignore(pattern)` removes matching entries entirely from the fingerprint. - `properties(pattern) { ignoreProperty(...) }` parses matching `.properties` files and excludes only the named keys, so functional keys still count. - `metaInf { ... }` normalizes the manifest (ignore the whole manifest, specific attributes, or the version). These rules apply globally to all runtime-classpath inputs in the project, so a single declaration stabilizes `test`, custom `JavaExec` tasks, packaging, etc. ## Why it matters for cache strategy Without normalization, any team that stamps build metadata into artifacts effectively disables runtime-classpath-based caching for everything downstream. Normalization is the standard remedy and is a prerequisite for a high-hit-rate remote cache. Note it is **distinct from `@PathSensitive`**: path sensitivity adjusts the *path* portion of the fingerprint for arbitrary file inputs; classpath normalization adjusts the *content/entry* portion specifically for classpath-typed inputs. ## Compile vs. runtime `@CompileClasspath` goes even further: Gradle extracts only the **ABI** (public API signatures) of classes, so changing a method body that doesn't alter signatures won't bust compile-classpath consumers. Runtime normalization is the runtime-side analogue you configure manually for non-class volatile files.

  • Why prefer ignoreProperty over ignoring the whole file?
    Because the file may also carry functional keys whose changes should bust the cache; ignoreProperty excludes only the volatile keys (timestamp, commit) while keeping the rest in the fingerprint.
  • Is runtime classpath normalization the same as @PathSensitive?
    No. @PathSensitive adjusts the path portion of any file input's fingerprint; classpath normalization adjusts the content/entry portion of classpath-typed inputs (ignoring entries, properties keys, or manifest attributes).
  • How does @CompileClasspath differ from a runtime classpath here?
    @CompileClasspath fingerprints only the ABI/signatures of classes, so method-body-only changes don't bust compile consumers — a built-in normalization you don't configure manually.

saying these in an interview costs you the question

  • Claiming you must rebuild upstream jars deterministically to fix cache misses, when normalization is the intended tool.
  • Confusing classpath normalization with @PathSensitive path modes.
  • Saying jar timestamps already bust the cache by default — Gradle ignores them; it's the entry contents that matter.

context