An AGP module has no toolchain declared and no compileOptions set. What JDK compiles the code and what bytecode is emitted — and why is that a problem?
answer
- no toolchain => daemon JDK compiles
- AGP default bytecode ~ Java 8
- non-reproducible across machines
- silent under-targeting
- fix: jvmToolchain via convention plugin
basics
~10 sWithout a toolchain, AGP compiles using the JDK that runs Gradle (the daemon JDK), and defaults to a low source/target (historically Java 8). That's a problem because output depends on whoever's machine launched Gradle.
solid answer
~40 sWhen no toolchain is declared, AGP has no independent compile JDK, so it falls back to the **Gradle daemon's** JDK to run `javac`/`kotlinc`. The bytecode level defaults to AGP's built-in `compileOptions` default (historically Java 8 / `VERSION_1_8`), independent of how new the daemon JDK is. The problem is twofold: (1) **non-reproducibility** — the compiling JDK now varies by machine/CI agent, so subtle bytecode differences and 'works on my machine' issues creep in; (2) **silent under-targeting** — you might be running JDK 21 but still emitting Java 8 bytecode and missing language features. The fix is to declare an explicit toolchain (`jvmToolchain(17)`), which both pins the compile JDK and defaults source/target to the chosen language version. A convention plugin can enforce this across all modules so no module silently inherits the daemon JDK.
code
bash · 2 lines# Inspect detected toolchains and which JDK the build uses
./gradlew -q javaToolchainsgo deeper
Know that without a toolchain the daemon JDK compiles and the default bytecode is low (Java 8).
Explain the reproducibility and silent-under-targeting risks and the jvmToolchain fix.
Discuss build-cache divergence, JAVA_HOME coupling, and diagnosing with javaToolchains.
Mandate toolchain declaration via convention plugins so no module relies on the daemon-JDK fallback.
## The fallback behavior A toolchain is *optional*. If you declare none: - **Compile JDK** = the JDK running the Gradle daemon. There is no independent toolchain JDK to resolve, so tasks use the daemon's `java`/`javac`. - **Bytecode level** = AGP's default `sourceCompatibility`/`targetCompatibility`, which historically defaults to Java 8 (`JavaVersion.VERSION_1_8`) unless the module sets otherwise. ## Why this is fragile 1. **Reproducibility loss:** Developer A on JDK 17 and CI on JDK 21 now compile with different compilers, which can emit subtly different class files (desugaring, synthetic methods). Build-cache keys diverge and 'it builds for me' bugs appear. 2. **Silent under-targeting:** Running on a modern JDK doesn't raise the bytecode floor — you can still be stuck at Java 8 output and unable to use newer language features without editing compileOptions. 3. **Hidden coupling to JAVA_HOME:** The build's behavior depends on an environment variable, the antithesis of hermetic builds. ## The fix ```kotlin kotlin { jvmToolchain(17) } ``` This pins the compile JDK to a resolved JDK 17 and defaults source/target to 17. Add a Foojay resolver so missing JDKs download. For consistency, push this into a **convention plugin** applied by every module: ```kotlin // build-logic convention plugin class AndroidConventionPlugin : Plugin<Project> { override fun apply(target: Project) = with(target) { extensions.configure<KotlinAndroidProjectExtension> { jvmToolchain(17) } } } ``` ## Detecting the problem Run `./gradlew javaToolchains` to list detected JDKs and which the build resolves. If a module shows it's compiling on the daemon JDK rather than a declared toolchain, it's relying on the fragile fallback.
- If I run JDK 21 with no toolchain, do I automatically get Java 21 bytecode?No. AGP defaults source/target to a low level (historically Java 8) regardless of the daemon JDK; you must set a toolchain or compileOptions to raise it.
- How do you enforce a toolchain across many Android modules?Apply a shared convention plugin (build-logic) that configures jvmToolchain(n) for every module, so none silently uses the daemon JDK.
saying these in an interview costs you the question
- Assuming a newer daemon JDK automatically raises the bytecode target.
- Believing the daemon JDK and a declared toolchain are interchangeable.
- Leaving compilation implicitly tied to JAVA_HOME in a shared codebase.