skip to content

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

level: middleimportance: must knowfreq 50%

answer

  1. ABI = public signatures only
  2. ignores method bodies, privates, resources
  3. JavaCompile.classpath is @CompileClasspath
  4. impl-only change → consumer stays UP-TO-DATE
  5. constant value change IS an ABI change

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.

solid answer

~50 s

Both annotations ignore jar names and paths and respect order. The difference is *what content* they hash. `@Classpath` hashes the full logical contents of each jar entry. `@CompileClasspath` hashes only the **ABI (Application Binary Interface)** — the parts of a class that affect compilation of *consumers*: public/protected type, method and field signatures, constant values, annotations. It deliberately ignores method **bodies**, **private** members, debug info and non-class **resources**. The payoff is huge for incremental compilation: if you change the implementation of a method in a library module, its ABI is unchanged, so downstream `JavaCompile` tasks stay **UP-TO-DATE** and skip. Only an API-affecting change (new method, changed signature, new constant) triggers recompilation. Gradle's built-in `JavaCompile.classpath` uses `@CompileClasspath` precisely for this. You'd choose `@Classpath` when the *actual* runtime bytes matter (packaging, runtime execution), and `@CompileClasspath` for anything that only compiles against the API.

code

kotlin · 7 lines
kotlin
abstract class ApiStubGenerator : DefaultTask() {
    @get:CompileClasspath
    abstract val libraries: ConfigurableFileCollection

    @get:OutputDirectory
    abstract val stubs: DirectoryProperty
}

go deeper

for a junior

Know it exists and fingerprints only the public API, helping skip recompilation on implementation-only changes.

for a middle

Define ABI precisely and explain the incremental-compilation win with a multi-module example.

for a senior

Discuss edge cases — inlined constants, annotation processors — where ABI sensitivity surprises people.

for a principal

Reason about build-time scaling across large module graphs and when ABI insensitivity risks correctness.

## ABI vs full content The **ABI** (Application Binary Interface) of a compiled class is the surface that *other* code compiles against: - public/protected/package class, method, field declarations and their signatures - inheritance and implemented interfaces - constant (`static final`) values that get inlined - annotations visible at compile time It **excludes**: - method **bodies** / implementation - `private` members - local variables and debug line tables - non-`.class` **resources** packaged in the jar ## Why this matters for incremental compilation When module A depends on module B, A's `JavaCompile` only needs B's ABI to compile. If you edit a method body in B, B is recompiled and repackaged, but B's **ABI fingerprint is identical**. Because `JavaCompile.classpath` is annotated `@CompileClasspath`, A's compile-task fingerprint is unchanged, so A's compilation is skipped entirely. This is the engine behind Gradle's fast multi-module incremental builds and is also why a trivial change deep in a dependency graph doesn't cascade into a full rebuild. Contrast with `@Classpath`: it hashes the full jar entries, so *any* byte change (including a method body) changes the fingerprint and invalidates the consumer — correct for runtime/packaging tasks, wasteful for compilation. ## Picking the right one | Input use | Annotation | |---|---| | Compiling against a library's API | `@CompileClasspath` | | Running, testing, or packaging with the real bytes | `@Classpath` | ## Code ```kotlin abstract class ApiStubGenerator : DefaultTask() { // We only care about the public surface of upstream jars @get:CompileClasspath abstract val libraries: ConfigurableFileCollection @get:OutputDirectory abstract val stubs: DirectoryProperty } ``` ## Limits ABI insensitivity has edge cases. `static final` **constants** are inlined by `javac` into consumers, so changing a constant's *value* IS an ABI change and does invalidate consumers. Annotation processors and `-parameters`/debug expectations can also widen what counts as API. When in doubt for correctness-critical runtime behaviour, prefer `@Classpath`.

  • If module B changes a `public static final int VERSION = 2` to `3`, will module A recompile under @CompileClasspath?
    Yes. Compile-time constants are inlined into consumers, so their value is part of the ABI; changing it changes B's compile fingerprint and invalidates A.
  • When should you NOT use @CompileClasspath?
    For tasks that consume the actual runtime bytes — packaging, running, fat-jar assembly, integration tests — where method bodies and resources matter. Use @Classpath there.

saying these in an interview costs you the question

  • Claiming @CompileClasspath ignores ALL upstream changes — constant inlining and signature changes still invalidate.
  • Using @CompileClasspath for packaging/runtime tasks where real bytes matter.
  • Confusing 'ignores method bodies' with 'ignores the whole class'.

context