skip to content

Classpath Normalization

@Classpath and @CompileClasspath for jar inputs, plus runtime classpath normalization that ignores volatile files. Interviewers ask because a timestamped properties file inside a jar can invalidate every downstream task.

on this pageshow

questions

5

What does the @Classpath input annotation do, and why would you use it instead of @InputFiles for a collection of jars?

level: juniorimportance: must knowfreq 55%

answer

  1. order significant, name/path ignored
  2. content hash of jar entries
  3. @InputFiles uses absolute path by default
  4. relocatable + cacheable
  5. @CompileClasspath = ABI only

basics

~10 s

@Classpath marks a task input as a classpath: Gradle hashes the file contents in order but ignores file names and paths. @InputFiles treats each file's absolute path as significant, so it over-invalidates.

solid answer

~40 s

`@Classpath` tells Gradle that a property is a *classpath* — an ordered list of jars or class directories. For up-to-date checks and the build cache, Gradle hashes the **content** of each entry, respects their **order** (classpath order is semantically meaningful), but **ignores the names and paths** of the jar files. `@InputFiles` (or `@InputFiles @PathSensitive`) instead fingerprints individual files where name/path can matter, so a renamed or relocated jar — common with dependency caches on different machines — needlessly invalidates the task. Using `@Classpath` makes the task cacheable and relocatable across machines, which is exactly what you want for `compile`/`runtime` classpaths. There's also `@CompileClasspath` for the narrower case where only the API (public signatures) matters.

code

kotlin · 7 lines
kotlin
abstract class BundleTask : DefaultTask() {
    @get:Classpath
    abstract val runtimeJars: ConfigurableFileCollection

    @get:OutputFile
    abstract val bundle: RegularFileProperty
}

go deeper

for a junior

Know that @Classpath ignores file names/paths but keeps order, so tasks stay up-to-date when only the path changes.

for a middle

Explain content-hashing of jar entries and why this makes tasks relocatable and cacheable across machines.

for a senior

Contrast @Classpath vs @CompileClasspath, and explain how default path sensitivity on @InputFiles causes cache misses.

for a principal

Frame it as a relocatability/cache-correctness concern across a fleet of CI agents and developer machines.

## What a classpath input is A *classpath* is an **ordered** sequence of jars and class directories handed to the JVM or to `javac`. Two properties make it special compared with an arbitrary file collection: 1. **Order matters** — the first class found on the classpath wins, so reordering can change behaviour. 2. **Names/paths do not matter** — `guava-32.jar` resolved into `~/.gradle/caches/...` on one machine and a different absolute path on a CI agent is the *same* dependency. The file name and directory are accidents of where the dependency cache lives. ## How Gradle fingerprints inputs Gradle computes a **fingerprint** of every task input to decide up-to-date status and to form the build-cache key. By default `@InputFiles` uses `ABSOLUTE` path sensitivity unless told otherwise, meaning the absolute path of each file is part of the hash. For a classpath that's wrong: it makes the task non-relocatable (cache misses between machines) and over-sensitive to harmless renames. ## @Classpath Annotating a property with `@Classpath`: - Hashes the **content** of each entry. - Preserves **order** of the entries. - **Ignores** the file names and the paths. - For jar files it hashes the *contents of the jar* (the entries inside), not the jar bytes, so a rebuilt jar with a different timestamp but identical classes produces the same fingerprint. This is the correct annotation for a *runtime* classpath input. ## @CompileClasspath `@CompileClasspath` is a stricter sibling for *compile* classpaths: it ignores everything that can't affect compilation — method bodies, private members, resources inside jars, debug info — and fingerprints only the **ABI** (public type signatures). Changing a method body of an upstream library therefore does **not** invalidate downstream compilation, only the recompiled jar's own consumers' runtime tasks. ## Code ```kotlin abstract class BundleTask : DefaultTask() { @get:Classpath abstract val runtimeJars: ConfigurableFileCollection @get:CompileClasspath abstract val apiJars: ConfigurableFileCollection @get:OutputFile abstract val bundle: RegularFileProperty @TaskAction fun bundle() { /* ... */ } } ``` Built-in tasks already do this: `JavaCompile.classpath` is `@CompileClasspath`, and runtime-oriented tasks use `@Classpath`. You only reach for these annotations on **custom** tasks.

  • Why does @Classpath hash the contents of a jar rather than the jar file's bytes?
    Because jar files embed timestamps and ordering that change on every rebuild even when the compiled classes are identical. Hashing the logical entries inside makes the fingerprint stable across rebuilds.
  • Does @Classpath ignore the order of the entries?
    No — order is preserved and significant, since classpath order determines which class wins when names collide. Only names and paths are ignored.

Think of a classpath like a stack of numbered transparencies: the order you stack them changes the picture, but it doesn't matter what's scribbled on the back (the file name) or which drawer they came from (the path).

saying these in an interview costs you the question

  • Saying @Classpath ignores ordering — it does not; order is significant.
  • Claiming @Classpath and @InputFiles are interchangeable for jars.
  • Thinking @Classpath hashes the raw jar bytes (it hashes the entries inside).

context

open as a page

How does @CompileClasspath differ from @Classpath, and how does ABI-based fingerprinting help avoid recompilation?

level: middleimportance: must knowfreq 50%

basics

~10 s

@CompileClasspath fingerprints only the public API (ABI) of jars — class/method/field signatures — ignoring method bodies, private members and resources. So changing only an implementation detail upstream doesn't invalidate downstream compilation.

open as a page

How do you configure runtime classpath normalization to ignore a volatile file like build-info.properties, and why is it needed?

level: middleimportance: must knowfreq 40%

basics

~10 s

In 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.

open as a page

You're authoring a custom task that takes a set of jars. How do you decide between @Classpath, @CompileClasspath, and @InputFiles with @PathSensitive for that property?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Use @CompileClasspath if you only need the jars' public API, @Classpath if you need real runtime bytes but order matters and names/paths don't, and @InputFiles with an explicit @PathSensitive (RELATIVE/NAME_ONLY/NONE) for non-classpath file inputs where order is irrelevant.

open as a page

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%

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.

open as a page