What does it mean to set a per-task toolchain in Gradle, and how do you make a single Test task run on a different JDK than the rest of the build?
answer
- javaToolchains service
- launcherFor / compilerFor / javadocToolFor
- javaLauncher / javaCompiler / javadocTool
- lazy Provider — resolved at execution
- overrides project java.toolchain per task
basics
~10 sUse the javaToolchains service to build a launcher for a specific JDK version, then assign it to the task's javaLauncher property: test.javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }.
solid answer
~40 sGradle resolves toolchains lazily through the `javaToolchains` service. A per-task toolchain overrides the project-wide `java.toolchain` for one task only. For a `Test` task you set its `javaLauncher` property — the JDK that launches the test JVM — to a launcher provider obtained from `javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }`. Because `javaLauncher` is a lazy `Property<JavaLauncher>`, the JDK is only located (and possibly auto-provisioned) at execution time, not during configuration. `JavaExec` also uses `javaLauncher`; `JavaCompile` uses `javaCompiler`; `Javadoc` uses `javadocTool`. This lets you, e.g., compile against Java 17 but run a specific integration-test suite on Java 21 to validate forward compatibility, without changing the whole project's toolchain.
code
kotlin · 5 linestasks.named<Test>("test") {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
}
}go deeper
Know the property name javaLauncher and that javaToolchains.launcherFor { languageVersion = ... } produces it; one task can use a different JDK.
Explain the three property/factory pairs (launcher/compiler/javadocTool), that they override the project toolchain per task, and that resolution is lazy.
Discuss laziness as Providers resolved at execution, separation of launch vs compile, and practical multi-JDK validation patterns.
Frame per-task toolchains within an org strategy: when to standardise project toolchains vs. per-task overrides, provisioning/governance, and build-cache implications.
## What a toolchain is A **JVM toolchain** in Gradle is a declared, version-pinned JDK that Gradle locates (or auto-provisions) independently of the JDK running Gradle itself. You normally declare one project-wide toolchain: ```kotlin java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } ``` This makes `JavaCompile`, `Test`, `JavaExec`, and `Javadoc` tasks all use that JDK by default. ## Per-task override Sometimes one task needs a *different* JDK. Each JVM task exposes a lazy tool property: - `JavaCompile.javaCompiler` — `Property<JavaCompiler>` - `Test.javaLauncher` and `JavaExec.javaLauncher` — `Property<JavaLauncher>` - `Javadoc.javadocTool` — `Property<JavadocTool>` You populate these from the **`javaToolchains`** service (type `JavaToolchainService`), which has matching factory methods: `launcherFor { }`, `compilerFor { }`, `javadocToolFor { }`. Each takes a `JavaToolchainSpec` lambda where you set `languageVersion`, and optionally `vendor` / `implementation`. ## Why it is lazy The factory methods return **`Provider<...>`** values, not resolved JDKs. Gradle only resolves (locates on disk, or downloads via auto-provisioning) the JDK when the task actually runs. So referencing a toolchain you don't have installed costs nothing at configuration time and never breaks unrelated tasks. ## Example: one test suite on a newer JDK ```kotlin val java21 = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(21) } tasks.named<Test>("test") { javaLauncher = java21 } ``` Here the production code may still compile/run on 17 (the project toolchain), while `test` is *launched* on a Java 21 JVM. Note `javaLauncher` controls only the **launching** JVM — it does not change the bytecode target of compilation; that is governed by the compile task's `javaCompiler` plus `release`/`sourceCompatibility`. ## Common real uses - Validate forward/backward compatibility by running tests on multiple JDKs. - Run a `JavaExec` tooling task on a JDK that a plugin requires, distinct from the build JDK. - Generate Javadoc with a specific JDK's `javadoc` tool.
- Does setting javaLauncher on Test change which JDK compiled the test classes?No. javaLauncher only chooses the JVM that *launches* the test process. Compilation is controlled by the JavaCompile task's javaCompiler (and release/sourceCompatibility). They are independent and can target different JDKs.
- When is the JDK behind launcherFor actually located on disk?At task execution time. The factory returns a Provider, so configuration is cheap and a missing JDK only matters if/when the task runs (it may then be auto-provisioned).
saying these in an interview costs you the question
- Claiming you must install/point JAVA_HOME at the JDK to run a task on it — toolchains are resolved independently of the Gradle JVM.
- Saying javaLauncher changes the compiled bytecode version — it only changes the launching JVM.