skip to content

You run CI on a JDK 21 agent but ship Android bytecode that must build identically everywhere. How do toolchains let the Gradle daemon stay on JDK 21 while AGP compiles on a fixed JDK?

level: seniorimportance: should knowfreq 30%

answer

  1. daemon JDK ≠ compile JDK
  2. jvmToolchain pins compile JDK
  3. Foojay auto-provisioning on CI
  4. auto-detect / auto-download properties
  5. stable cache keys + reproducible bytecode

basics

~10 s

Declare a fixed toolchain (e.g. jvmToolchain(17)). The daemon keeps running on JDK 21, but Gradle resolves JDK 17 for AGP's JavaCompile and Kotlin compile, so output is identical regardless of the launching JDK.

solid answer

~40 s

The toolchain abstraction is exactly the lever here. The Gradle daemon can launch on whatever JDK the CI agent provides (JDK 21) for speed/compatibility, while every compile task uses the *declared* toolchain JDK. Setting `kotlin { jvmToolchain(17) }` (plus consistent `compileOptions`) pins compilation to JDK 17 across all agents and developer machines, so bytecode is reproducible and independent of the launching JDK. To make this robust on CI: enable auto-provisioning (Foojay `org.gradle.toolchains.foojay-resolver-convention`) so a missing JDK 17 downloads automatically, or pre-install it and let Gradle auto-detect. You can disable detection of `JAVA_HOME` and rely on managed JDKs for hermeticity. Pinning the daemon JDK separately (via `org.gradle.java.home` or the toolchain for the daemon) is optional — the key insight is daemon JDK and compile JDK are independent.

code

kotlin · 7 lines
kotlin
// settings.gradle.kts — auto-provision missing JDKs
plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}

// app/build.gradle.kts — pin compilation regardless of daemon JDK
kotlin { jvmToolchain(17) }

go deeper

for a junior

Know that declaring a toolchain fixes the compile JDK independent of how Gradle is launched.

for a middle

Explain daemon-JDK vs compile-JDK independence and the Foojay resolver for missing JDKs.

for a senior

Tie it to reproducible bytecode, hermetic CI (detection/download properties), and build-cache key stability.

for a principal

Define org-wide toolchain + provisioning policy so all agents and devs produce byte-identical, cacheable Android artifacts.

## The independence that makes this work Gradle distinguishes: - **Daemon JDK** — runs the build logic; chosen by `JAVA_HOME`/`org.gradle.java.home`/Daemon JVM criteria. - **Toolchain JDK** — used by compile/test tasks; chosen by the declared toolchain. Because they're independent, CI can launch the daemon on JDK 21 (faster GC, newer runtime) while AGP compiles on JDK 17. ## Pinning compilation ```kotlin // settings.gradle.kts plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } ``` ```kotlin // app/build.gradle.kts kotlin { jvmToolchain(17) } android { compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } } ``` Now `JavaCompile` and `KotlinCompile` run on a resolved JDK 17 on every agent. ## Making it hermetic on CI 1. **Auto-provisioning:** the Foojay resolver downloads a matching JDK if none is detected, so a fresh agent needs no manual JDK 17. 2. **Detection control:** `org.gradle.java.installations.auto-detect` and `auto-download` properties govern whether Gradle scans the machine and whether it may download. For maximum reproducibility you can disable auto-detect and point at known installations via `org.gradle.java.installations.paths`. 3. **Daemon JVM (newer Gradle):** you may declare daemon JVM criteria so even the daemon JDK is reproducible — but that's separate from compilation. ## Why bytecode becomes reproducible Different `javac`/`kotlinc` versions can emit subtly different class files (method ordering, bridge methods, default desugaring). Pinning the toolchain JDK pins those compilers, so output is byte-stable across machines — important for caching, signing, and diff-based verification. ## Caching synergy With a fixed toolchain, the build cache key for compile tasks is stable across agents, so remote build-cache hits work regardless of the launching JDK. Mismatched compile JDKs would otherwise produce cache misses. ## Summary Daemon JDK ≠ compile JDK. Declare a toolchain to fix the compile JDK; use Foojay/auto-provisioning for portability; control detection for hermeticity. The result: identical Android artifacts whatever JDK starts Gradle.

  • How does a fixed toolchain help the remote build cache?
    Compile-task cache keys include the toolchain JDK. Fixing it makes keys stable across agents, so cache hits work even when daemons launch on different JDKs.
  • What property lets CI download a missing toolchain JDK automatically?
    `org.gradle.java.installations.auto-download` (true by default) together with a resolver like the Foojay convention plugin.
  • Could you also pin the daemon JDK for full reproducibility?
    Yes — via org.gradle.java.home or Daemon JVM criteria — but it's independent of compilation reproducibility, which the toolchain already guarantees.

The daemon JDK is the delivery truck; the toolchain JDK is the factory machine. You can swap trucks freely, but as long as the factory machine is the same model, the parts come out identical.

saying these in an interview costs you the question

  • Claiming you must downgrade the CI agent's JDK to ship JDK 17 bytecode — the toolchain avoids that.
  • Saying the daemon JDK and compile JDK are the same thing.
  • Ignoring that mismatched compile JDKs break build-cache portability.

context