skip to content

Per-Task Toolchains

Overriding the toolchain on a single JavaCompile, Test, or JavaExec task through javaToolchains launchers and compilers. Asked when a project must compile for one JDK but be tested on several.

on this pageshow

questions

5

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?

level: middleimportance: must knowfreq 55%

answer

  1. javaToolchains service
  2. launcherFor / compilerFor / javadocToolFor
  3. javaLauncher / javaCompiler / javadocTool
  4. lazy Provider — resolved at execution
  5. overrides project java.toolchain per task

basics

~10 s

Use 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 s

Gradle 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 lines
kotlin
tasks.named<Test>("test") {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

go deeper

for a junior

Know the property name javaLauncher and that javaToolchains.launcherFor { languageVersion = ... } produces it; one task can use a different JDK.

for a middle

Explain the three property/factory pairs (launcher/compiler/javadocTool), that they override the project toolchain per task, and that resolution is lazy.

for a senior

Discuss laziness as Providers resolved at execution, separation of launch vs compile, and practical multi-JDK validation patterns.

for a principal

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.

context

open as a page

How do you make a custom JavaExec task (or the application run task) execute on a specific JDK without changing the project-wide toolchain?

level: middleimportance: should knowfreq 35%

basics

~10 s

Set the JavaExec task's javaLauncher to a launcher from javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(X) }. The project's java.toolchain stays unchanged.

open as a page

Walk me through the difference between a JavaCompile task's javaCompiler and a Test/JavaExec task's javaLauncher. When would you set each to different JDKs in the same build?

level: seniorimportance: should knowfreq 40%

basics

~20 s

javaCompiler picks the JDK that compiles sources; javaLauncher picks the JDK that runs a process (tests or JavaExec). You set them differently to, e.g., compile bytecode for Java 17 but run tests on Java 21.

open as a page

Why do javaToolchains.launcherFor/compilerFor return Providers, and what practical consequences does that laziness have for a per-task toolchain override?

level: seniorimportance: should knowfreq 30%

basics

~20 s

They return Providers so the JDK is located only when the task runs, not at configuration time. So referencing a JDK you don't have is free unless the task actually executes, and assignment integrates with Gradle's lazy configuration and up-to-date checks.

open as a page

How do you point the Javadoc task at a specific JDK's javadoc tool, and what fields can you set in the toolchain spec passed to javadocToolFor/launcherFor besides languageVersion?

level: middleimportance: nice to knowfreq 22%

basics

~10 s

Set the Javadoc task's javadocTool to javaToolchains.javadocToolFor { languageVersion = ... }. In the spec you can also set vendor (e.g. JvmVendorSpec.ADOPTIUM) and nativeImageCapable/implementation to constrain which JDK matches.

open as a page