Gradle Toolchains

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Gradle 9.8.1 (released on 7 October 2026), with Gradle 8.14.6, the last 8.x release, mentioned where it differs. Every example marked as run was executed with the 9.8.1 wrapper on 2026-10-16, on macOS (Apple silicon), in scratch copies of the tidepool build (a root project with the subprojects tidepool-model, tidepool-engine and tidepool-cli), once with *.gradle.kts files and once with *.gradle files. The Gradle daemon ran on Oracle JDK 26 (java version "26"), which is the default JDK of the machine; the toolchains that the examples select are two JetBrains Runtime (JBR) JDKs that are also installed, 21.0.10 and 17.0.14. Every run used a Gradle User Home created for the page (the GRADLE_USER_HOME environment variable), so that no daemon or cache of the real ~/.gradle was touched, and a gradle.properties that contains org.gradle.java.installations.auto-download=false, so that no JDK could be downloaded; the same file sets org.gradle.jvmargs=-Xmx512m -Duser.language=en -Duser.country=US, so that the JVM messages are in English instead of the machine’s Spanish. The examples are compatible with the configuration cache (they ran with --configuration-cache, and a second run reused the entry) except where an error is shown on purpose, which is said each time. Long lines are shortened with ... where it is said so; the scratch directory is shown as <work>, and the home directory as ~. Gradle 8.14.x was not available here, so what is said about it comes from the official release notes and upgrade guides and is labelled "not run here". Three more things were not run, and are labelled the same way: downloading a JDK (auto-provisioning), a toolchain on Java 8 or on a GraalVM that can build native images, and the Kotlin Gradle plugin’s jvmToolchain.

A Gradle build runs on one JVM and compiles, tests and documents your code with others. The first is the Java that the Gradle daemon was started with; the others are the toolchains: JDKs that the build asks for by language version (and optionally vendor), that Gradle finds on the machine or, if you allow it, downloads, and that it uses for the compiler, the test and application launchers and Javadoc. Without them a build compiles with "whatever Java is on this machine", which is why a project compiles on one laptop and not on the CI agent. This page covers declaring a toolchain, how Gradle finds and chooses a JDK, how to give a single task another JDK, and how toolchains relate to --release, JAVA_HOME, the IDE and the JVM of the daemon itself. The JVM of the daemon has its own page, The Gradle Daemon; the java and java-library plugins that consume the toolchain are in Java Projects; and the properties org.gradle.java.* are listed with all other properties in Build Environment & Properties. For the other two tools, Maven’s equivalent is described in Maven Compiling & Toolchains and Ant has no such concept (see Ant: Compiling Java); a table at the end of this page puts the three side by side.

The problem toolchains solve

Three different Java versions take part in a build, and mixing them up is the cause of most "Java version" problems.

Which Java What it is used for How it is chosen

Launcher JVM (the client)

Runs the gradle or gradlew script; starts or finds a daemon. The wrapper and the client can still run on Java 8, though they then need a newer Java for the daemon (see Gradle Wrapper).

JAVA_HOME, or the java on the path.

Daemon JVM

Runs Gradle itself: build scripts, plugins, the task graph. It must be Java 17 to 27 for Gradle 9.8.1.

org.gradle.java.home, or the Daemon JVM criteria in gradle/gradle-daemon-jvm.properties, or the launcher JVM (see The Gradle Daemon).

Toolchain JVMs

javac for JavaCompile, java for Test and JavaExec, javadoc for Javadoc, and the forked workers of compilers and other tools.

java { toolchain { ... } } or the per-task properties described below; by default, the JVM of the daemon.

By default the toolchain is the daemon’s JVM: the official page says Gradle "uses the same Java toolchain for running Gradle itself and building JVM projects". That default is the root of the problems. A project that must compile with Java 21 breaks when a developer’s JAVA_HOME is 26 or 17; a build that wants Java 8 bytecode cannot run on Gradle 9 at all, which needs Java 17 or newer. Toolchains separate the two questions, so that Gradle runs on whichever recent Java it likes while the project says what it needs, in the build, where the team can see it and review it. The feature appeared in Gradle 6.7 (2020-10-14) and became stable in 7.0; the release history is at the end of the page.

Five ways to choose the Java of a project

The manual lists five mechanisms that tell Gradle which Java a JVM project uses. They are not exclusive, and the page recommends toolchains for most builds.

Mechanism What it does Selects the JDK? Downloads a JDK? Blocks newer APIs?

java.toolchain

Declares the language version (and optionally vendor); every compile, test and Javadoc task uses a matching JDK.

yes

yes, if configured

no

options.release

Passes --release N to javac: language level and the API of Java N, whatever JDK compiles.

no

no

yes

sourceCompatibility and targetCompatibility

Pass -source and -target: language level and class file version, but the compiler still sees the API of the JDK it is.

no

no

no

JAVA_HOME

Chooses the JVM of the launcher (and, unless overridden, of the daemon and the build) for every project on the machine.

globally only

no

no

IDE settings

The IntelliJ IDEA or Eclipse "Gradle JVM": the JVM that Gradle runs on inside the IDE.

inside the IDE only

no

no

The --release flag is the only one that stops you from calling an API that does not exist in the target version, so the table’s last column is why the recommendation for strict cross-compilation is release, and for "build with the JDK the project says" is a toolchain; the two combine, which is the subject of the section on combining them below. The compatibility properties are legacy: the manual says to use them "only in cases where you need backward compatibility but cannot use toolchains". Maven has the same split: maven.compiler.release is its --release, and toolchains.xml is its toolchain (see Maven Compiling & Toolchains).

Declaring a toolchain

A toolchain is declared in the java extension, which the java, java-library and application plugins (and the Groovy, Scala and Kotlin JVM plugins, which apply the Java plugin) provide. The only required property is the language version. In the tidepool build, every module asks for Java 21:

Kotlin DSL: tidepool-model/build.gradle.kts
plugins {
    `java-library`
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}
Groovy DSL: tidepool-model/build.gradle
plugins {
    id 'java-library'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

The same three lines go into tidepool-engine and tidepool-cli. (In a real multi-project build they belong in one convention plugin, which is the subject of Convention Plugins & Sharing Build Logic; they are repeated here so that each file is complete.) The root settings.gradle.kts declares the repository once and includes the three modules; the whole build ran from these files:

Kotlin DSL: settings.gradle.kts
rootProject.name = "tidepool"

dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        mavenCentral()
    }
}

include("tidepool-model", "tidepool-engine", "tidepool-cli")
Groovy DSL: settings.gradle
rootProject.name = 'tidepool'

dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        mavenCentral()
    }
}

include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'

When the build runs, the official page lists what Gradle does for you: it configures every compile, test and Javadoc task of the project to use the toolchain, it detects the locally installed JDKs, it picks one that matches (any Java 21 here), and, if there is none and a download repository is configured, it downloads one. In 9.8.1 the compile task reports which JDK it used at the --info level:

./gradlew --configuration-cache --rerun-tasks :tidepool-model:compileJava --info | grep "Compiling with toolchain"
javap -v tidepool-model/build/classes/java/main/io/tidepool/model/Station.class | grep major
Compiling with toolchain '~/Library/Java/JavaVirtualMachines/jbr-21.0.10/Contents/Home'.
  major version: 65

The daemon ran on JDK 26, yet the class file has major version 65, which is Java 21: the compiler was the javac of the JBR 21 installation (javap is only reading the file; any JDK can). The task also records the toolchain as an input. Changing the language version of the module to 17 makes the next build recompile, and --info says why:

Task ':tidepool-model:compileJava' is not up-to-date because:
  Value of input property 'javaCompiler.metadata.languageVersion' has changed for task ':tidepool-model:compileJava'
  Value of input property 'javaCompiler.metadata.taskInputs.languageVersion' has changed for task ':tidepool-model:compileJava'
  Value of input property 'javaVersion' has changed for task ':tidepool-model:compileJava'

(The rest of the --info output is left out.) The manual adds that the Java major version, the vendor and the implementation, if given, "will be tracked as an input for compilation and test execution", which is why a toolchain change does not leave stale class files behind and why it also matters for the build cache (see Build Cache).

Which tasks use the toolchain? JavaCompile, Javadoc and Test, as before. Since Gradle 9.0.0 also JavaExec: the 9.0 upgrade guide says that, when the java-base plugin is applied, JavaExec "will instead default to the Java toolchain configured in the java extension", where earlier versions used the JVM of Gradle itself. A JavaExec task of the cli module that sets nothing shows it:

Kotlin DSL: tidepool-cli/build.gradle.kts (added)
tasks.register<JavaExec>("runDefault") {
    description = "Runs JvmInfo with the launcher that the java toolchain gives by default."
    group = "application"
    classpath = sourceSets.main.get().runtimeClasspath
    mainClass = "io.tidepool.cli.JvmInfo"
}
Groovy DSL: tidepool-cli/build.gradle (added)
tasks.register('runDefault', JavaExec) {
    description = 'Runs JvmInfo with the launcher that the java toolchain gives by default.'
    group = 'application'
    classpath = sourceSets.main.runtimeClasspath
    mainClass = 'io.tidepool.cli.JvmInfo'
}

JvmInfo is a ten-line class in the cli module whose main prints "running on Java " + Runtime.version() + " (" + System.getProperty("java.vendor") + ")". Both DSLs printed the same (command ./gradlew --configuration-cache -q :tidepool-cli:runDefault):

running on Java 21.0.10+7-b1163.110 (JetBrains s.r.o.)

The daemon is on Java 26 (./gradlew --version shows Launcher JVM: 26 and the daemon on the same JDK); the program ran on 21. On Gradle 8.14 the same task would have run on Java 26: the toolchain default for JavaExec did not exist (not run here).

Selecting by vendor, implementation and capability

The language version is the only required property. A toolchain specification (JavaToolchainSpec) may add more, and each narrows the set of JDKs that match.

Property Meaning Since

languageVersion

The Java feature release, as JavaLanguageVersion.of(21).

6.7

vendor

A JvmVendorSpec: one of the 14 constants ADOPTIUM, ADOPTOPENJDK, AMAZON, APPLE, AZUL, BELLSOFT, GRAAL_VM, HEWLETT_PACKARD, IBM, JETBRAINS, MICROSOFT, ORACLE, SAP and TENCENT, or JvmVendorSpec.matching("text"), which tests the java.vendor system property of each JDK for the text, ignoring case. The constants exist because the java.vendor string of one vendor differs between JDK versions.

6.8

implementation

JvmImplementation.VENDOR_SPECIFIC (any implementation of the vendor, the default) or JvmImplementation.J9 (only OpenJ9 / IBM J9).

6.8

nativeImageCapable

true selects only JDKs that can build GraalVM native images; unset or false does not exclude them.

8.14

Kotlin DSL
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
        vendor = JvmVendorSpec.JETBRAINS      // or JvmVendorSpec.matching("jetbrains")
        // implementation = JvmImplementation.J9
        // nativeImageCapable = true
    }
}
Groovy DSL
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
        vendor = JvmVendorSpec.JETBRAINS      // or JvmVendorSpec.matching('jetbrains')
        // implementation = JvmImplementation.J9
        // nativeImageCapable = true
    }
}

The JetBrains vendor and the matching("jetbrains") form both found the JBR 21 on this machine, in both DSLs. The other two lines are commented because nothing here could match them: there is no OpenJ9 JDK, and no GraalVM-capable JDK. With nativeImageCapable = true the build stops, and the message names every property of the specification, which is the quickest way to see what Gradle was looking for (the other messages are in the section on errors below):

      > Cannot find a Java installation on your machine (Mac OS X 27.0.1 aarch64) matching: {languageVersion=21, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=true}. Toolchain auto-provisioning is not enabled.

A specification is valid in two cases: it is empty, or it sets languageVersion and optionally other properties. A vendor without a language version is rejected with an error (since Gradle 8.0, according to the manual, which also says that an unconfigured specification "selects the toolchain of the current build"). Setting only vendor = JvmVendorSpec.JETBRAINS produced:

> Task :tidepool-model:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':tidepool-model:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Error while evaluating property 'javaCompiler' of task ':tidepool-model:compileJava'.
   > Failed to calculate the value of task ':tidepool-model:compileJava' property 'javaCompiler'.
      > Using toolchain specifications without setting a language version is not supported. Consider configuring the language version. Consult the upgrading guide for further information: https://docs.gradle.org/9.8.1/userguide/upgrading_version_7.html#invalid_toolchain_specification_deprecation
...

The vendor and implementation, when given, are tracked as inputs like the language version. Which JDK a specification resolves to is decided by the rules in the sections that follow; the practical advice is to set the language version and nothing else unless the build really depends on a vendor (for example because only one has the compiler flags or the jlink modules the project needs), because every extra property reduces the number of machines on which the build works without a download.

Auto-detection: which JDKs does Gradle know about?

Before it can choose, Gradle has to find the installed JDKs. This is auto-detection, on by default, and the manual lists what it understands: operating-system specific locations (Linux, macOS, Windows), the conventional environment variable JAVA_HOME, the package managers Asdf-vm, Jabba and SDKMAN!, Maven toolchain specifications (~/.m2/toolchains.xml), and IntelliJ IDEA installations. Installations that do not exist or have no bin/java are ignored with a warning, not an error. The javaToolchains report of the project shows what was found and how:

./gradlew -q javaToolchains
 + Options
     | Auto-detection:     Enabled
     | Auto-download:      Disabled

 + JetBrains JDK 17 (17.0.14+1-b1367.22)
     | Location:           ~/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home
     | Language Version:   17
     | Vendor:             JetBrains
     | Architecture:       aarch64
     | Is JDK:             true
     | Detected by:        IntelliJ

 + JetBrains JDK 21 (21.0.10+7-b1163.110)
     | Location:           ~/Library/Java/JavaVirtualMachines/jbr-21.0.10/Contents/Home
     | Language Version:   21
     | Vendor:             JetBrains
     | Architecture:       aarch64
     | Is JDK:             true
     | Detected by:        IntelliJ

 + Oracle JDK 26 (26+35-2893)
     | Location:           /Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home
     | Language Version:   26
     | Vendor:             Oracle
     | Architecture:       aarch64
     | Is JDK:             true
     | Detected by:        Current JVM

(The output was printed in full except that the home directory is shown as ~.) Three things stand out. The two JBRs were found by "IntelliJ", because they live in ~/Library/Java/JavaVirtualMachines, one of the places that IntelliJ IDEA uses for the JDKs it manages; the JDK 26 is "Current JVM" because it is the JVM that runs the daemon, whichever way it was installed; and the header shows the state of the two switches, Auto-detection: Enabled and Auto-download: Disabled, the latter because of the gradle.properties of the examples. The task javaToolchains was added in Gradle 6.8. It is the first thing to run when a build says that no JDK matches.

Auto-detection can be turned off or extended with Gradle properties (set in gradle.properties or with -D on the command line; they belong to the daemon’s JVM, so changing them starts a new daemon, which the daemon page explains):

Property Value Effect

org.gradle.java.installations.auto-detect

true (default) or false

Turns the search of the operating system, the package managers and the IDE off.

org.gradle.java.installations.paths

comma-separated directories

JDK homes to consider. Gradle does not look into nested directories.

org.gradle.java.installations.fromEnv

comma-separated variable names

Environment variables that point to JDK homes, for example JDK8,JRE17.

org.gradle.java.installations.auto-download

true (default) or false

Allows or forbids downloading a JDK when none matches (next section but one).

The custom locations extend the set; the manual says Gradle "does not prioritize custom toolchains over auto-detected toolchains". Four runs with different settings show what each one changes (-q javaToolchains, JDK paths shortened, the Options block and the fields other than the name, location and Detected by left out):

$ ./gradlew -q -Dorg.gradle.java.installations.auto-detect=false javaToolchains
 + Options
     | Auto-detection:     Disabled
 + Oracle JDK 26 (26+35-2893)
     | Detected by:        Current JVM

$ ./gradlew -q -Dorg.gradle.java.installations.auto-detect=false \
      -Dorg.gradle.java.installations.paths=~/Library/Java/JavaVirtualMachines/jbr-21.0.10/Contents/Home javaToolchains
 + JetBrains JDK 21 (21.0.10+7-b1163.110)
     | Detected by:        Gradle property 'org.gradle.java.installations.paths'
 + Oracle JDK 26 (26+35-2893)
     | Detected by:        Current JVM

$ MY_JDK17=~/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home ./gradlew -q \
      -Dorg.gradle.java.installations.auto-detect=false -Dorg.gradle.java.installations.fromEnv=MY_JDK17 javaToolchains
 + JetBrains JDK 17 (17.0.14+1-b1367.22)
     | Detected by:        environment variable 'MY_JDK17'
 + Oracle JDK 26 (26+35-2893)
     | Detected by:        Current JVM

$ JAVA_HOME=~/Library/Java/JavaVirtualMachines/jbr-21.0.10/Contents/Home ./gradlew -q \
      -Dorg.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home \
      -Dorg.gradle.java.installations.auto-detect=false javaToolchains
 + JetBrains JDK 21 (21.0.10+7-b1163.110)
     | Detected by:        environment variable 'JAVA_HOME'
 + Oracle JDK 26 (26+35-2893)
     | Detected by:        Current JVM

(The commands are written with ~ for the home directory for readability; the runs used full paths.) The JDK running the daemon is always in the list, even with auto-detection off, which is why a toolchain request for the daemon’s own Java version never needs a search. The last run used a daemon on JDK 26 (through org.gradle.java.home) and a client started with JAVA_HOME pointing at the JBR 21, and shows the JAVA_HOME source: it is detected even with auto-detection off, as the manual’s description of "conventional environment variable" implies. Using JAVA_HOME as a source is new in Gradle 9.0; the release notes say it makes the command line consistent with the IDEs, which did not look at it. Under 8.14 the same variable was not a detection source (not run here).

Which JDK wins: the precedence rules

Several installations can match a specification, for example when a Java 17 request meets two vendors and a JRE. Gradle sorts all matching installations with a fixed sequence of criteria and takes the first:

  1. the installation that is running Gradle is preferred over any other;

  2. JDKs are preferred over JREs;

  3. vendors in this order, highest priority first: ADOPTIUM, ADOPTOPENJDK, AMAZON, APPLE, AZUL, BELLSOFT, GRAAL_VM, HEWLETT_PACKARD, IBM, JETBRAINS, MICROSOFT, ORACLE, SAP, TENCENT, then everything else;

  4. higher major versions before lower ones;

  5. higher minor versions before lower ones;

  6. as a last resort, the lexicographic order of the installation path, so that the choice is deterministic.

The manual’s worked example has five Java 17 installations (Oracle JRE 17.0.1, Oracle JDK 17.0.0, Microsoft JDK 17.0.0, Microsoft JRE 17.0.1 and Microsoft JDK 17.0.1) and, with Gradle running on another Java, Gradle picks Microsoft JDK 17.0.1: the JDKs first, then Microsoft before Oracle, then the higher patch level. This machine has one JDK per version, so the order could not be shown with real output; the first rule is visible in the runs above (a request for 26 resolves to "Current JVM") and the sorting itself is taken from the manual and is therefore "not run here". Two consequences are worth remembering. A patch update of a JDK changes which JDK is chosen, silently. And two developers with different vendors installed build with different JDKs unless the vendor is pinned in the specification.

flowchart TD A["java toolchain: languageVersion 21"] --> B["Auto-detection: OS locations, JAVA_HOME, SDKMAN!, IntelliJ, Maven toolchains.xml, paths, fromEnv"] B --> C{"Installations that match the specification?"} C -- "yes" --> D["Sort: current JVM, JDK over JRE, vendor order, version, path"] D --> E["Use the first one"] C -- "no" --> F{"auto-download enabled and a toolchain repository configured?"} F -- "yes" --> G["Ask the repositories in order, download the first match into the jdks directory of the Gradle User Home"] G --> E F -- "no" --> H["Build fails: Cannot find a Java installation on your machine"]

When no JDK matches: the real errors

Auto-detection can fail in three ways, and each has its own message. All of them were produced in the tidepool build by changing the toolchain of the model module and running ./gradlew :tidepool-model:compileJava. (They were run without --configuration-cache: with it on, the same text appears but prefixed with a line that says that the configuration cache state could not be stored, because Gradle resolves the compiler while it stores the entry. The first message is shown that way below.) The first asks for a vendor that is not installed:

Kotlin DSL
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
        vendor = JvmVendorSpec.ORACLE
    }
}
Groovy DSL
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
        vendor = JvmVendorSpec.ORACLE
    }
}
> Task :tidepool-model:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':tidepool-model:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Error while evaluating property 'javaCompiler' of task ':tidepool-model:compileJava'.
   > Failed to calculate the value of task ':tidepool-model:compileJava' property 'javaCompiler'.
      > Cannot find a Java installation on your machine (Mac OS X 27.0.1 aarch64) matching: {languageVersion=21, vendor=Oracle, implementation=vendor-specific, nativeImageCapable=false}. Toolchain auto-provisioning is not enabled.
...

Auto-provisioning was off, so the message ends with "Toolchain auto-provisioning is not enabled." A request for Java 11, which is not installed, gave the same text with languageVersion=11, vendor=any vendor in the specification. The second failure appears when auto-download is on (the default) but nothing says where to download from. This was run with -Dorg.gradle.java.installations.auto-download=true and Java 11; Gradle stopped before trying any download:

> Task :tidepool-model:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':tidepool-model:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Error while evaluating property 'javaCompiler' of task ':tidepool-model:compileJava'.
   > Failed to calculate the value of task ':tidepool-model:compileJava' property 'javaCompiler'.
      > Cannot find a Java installation on your machine (Mac OS X 27.0.1 aarch64) matching: {languageVersion=11, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain download repositories have not been configured.

* Try:
> Learn more about toolchain auto-detection and auto-provisioning at https://docs.gradle.org/9.8.1/userguide/toolchains.html#sec:auto_detection.
> Learn more about toolchain repositories at https://docs.gradle.org/9.8.1/userguide/toolchains.html#sub:download_repositories.
...

The sentence "Toolchain download repositories have not been configured." is the one to recognise: since Gradle 8.0 there is no default provisioner, so a build that worked in 7.x because it silently downloaded from Adoptium needs a repository declaration now (next section but one). The third failure is the configuration-cache variant of the first, shown for completeness (Java 11 again, with --configuration-cache):

Calculating task graph as configuration cache cannot be reused because file 'settings.gradle.kts' has changed.

1 problem was found storing the configuration cache.
- Task `:tidepool-model:compileJava` of type `org.gradle.api.tasks.compile.JavaCompile`: error writing value of type 'org.gradle.api.internal.provider.DefaultProperty'

See the complete report at file://<work>/build/reports/configuration-cache/6a65qnj3ucuwx2oz8drv38bbo/pgp729s9j0ebr2ezan04f89b/configuration-cache-report.html

[Incubating] Problems report is available at: file://<work>/build/reports/problems/problems-report.html

FAILURE: Build failed with an exception.

* What went wrong:
Configuration cache state could not be cached: field `__javaCompiler__` of task `:tidepool-model:compileJava` of type `org.gradle.api.tasks.compile.JavaCompile`: error writing value of type 'org.gradle.api.internal.provider.DefaultProperty'
> Cannot find a Java installation on your machine (Mac OS X 27.0.1 aarch64) matching: {languageVersion=11, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain auto-provisioning is not enabled.
...

The last line is the same cause. The cache report says that the compiler property could not be stored because it could not be resolved; the fix is the same, not a cache setting. The message about invalid specifications was shown above.

Auto-provisioning and toolchain download repositories

When no installed JDK matches, Gradle can download one. This is auto-provisioning, and it needs two things: org.gradle.java.installations.auto-download not set to false (the default is true), and at least one toolchain download repository declared in settings.gradle(.kts). The rules of the manual are short: the JDK goes to the jdks directory of the Gradle User Home, where it becomes one of the JDKs that auto-detection finds for every later build; Gradle only downloads GA releases (no early-access builds); and provisioning only fills gaps, it never updates a JDK it has already installed, even if a newer minor version exists. To remove one, delete its directory from jdks and stop the daemon, because the daemon caches the toolchain paths (the ./gradlew --stop advice of the manual).

Since Gradle 8.0 there is no default repository. Gradle 7.6 (2022-11-25) introduced toolchain resolver plugins, which map a specification to a download URL, and the 8.0 release notes say that "there is no longer a default toolchain provisioner": you declare at least one, usually through the Foojay Toolchains plugin, which uses the Foojay Disco API to find JDK distributions. There are two plugins on the Plugin Portal, both at version 1.0.0 on 2026-10-16. The convention plugin does all the configuration by being applied:

Kotlin DSL: settings.gradle.kts
plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}

rootProject.name = "tidepool"
Groovy DSL: settings.gradle
plugins {
    id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}

rootProject.name = 'tidepool'

The plain plugin only provides the resolver; you register it in a toolchainManagement block, which is what you need when several repositories are configured, or when their order, or credentials, matter:

Kotlin DSL: settings.gradle.kts
plugins {
    id("org.gradle.toolchains.foojay-resolver") version "1.0.0"
}

toolchainManagement {
    jvm {
        javaRepositories {
            repository("foojay") {
                resolverClass = org.gradle.toolchains.foojay.FoojayToolchainResolver::class.java
            }
        }
    }
}

rootProject.name = "tidepool"
Groovy DSL: settings.gradle
plugins {
    id 'org.gradle.toolchains.foojay-resolver' version '1.0.0'
}

toolchainManagement {
    jvm {
        javaRepositories {
            repository('foojay') {
                resolverClass = org.gradle.toolchains.foojay.FoojayToolchainResolver
            }
        }
    }
}

rootProject.name = 'tidepool'

Both forms were applied to the tidepool build in both DSLs and run with the JDK 21 toolchain. They only matter when a JDK is missing, so the build behaved as before (BUILD SUCCESSFUL, the JBR 21 was used, nothing was downloaded; the same file also configures dependencyResolutionManagement and include as above, omitted here). With auto-download switched off in gradle.properties, a request for Java 11 still stopped with "Toolchain auto-provisioning is not enabled.", exactly as in the section on errors: the repositories are consulted only when the property allows it. That is the setting that keeps CI builds honest. The actual download of a JDK was not run here: it would fetch a JDK from the network into the Gradle User Home, which these examples avoid on purpose.

In the toolchainManagement block, the jvm block holds the Java configuration, javaRepositories defines named repositories, and the resolverClass links each to the plugin that provides it. The order of the declarations matters: Gradle asks the repositories from the first to the last and downloads from the first that has a match. Repositories can carry the same credentials and authentication as dependency repositories (the manual’s example uses a fictitious MadeUpResolver with user name, password and digest authentication; that example was not run here, because it needs a resolver plugin that does not exist). Since Gradle 8.8, an error thrown by one resolver no longer stops the search: Gradle tries the next one.

Two cautions come with downloading JDKs. The Foojay API is a third-party service, and the JDK it points to is run on every build machine, so it is a link in the supply chain (compare Supply-Chain Security); teams that cannot accept that install the JDKs in the CI image and set org.gradle.java.installations.auto-download=false, or configure a resolver of their own for an internal mirror. And a download makes the first build on a fresh machine slow and dependent on the network, which is also why the examples on this page forbid it.

Writing a resolver plugin

A toolchain resolver plugin has two parts. A class that implements JavaToolchainResolver (it is a build service, hence abstract) maps a JavaToolchainRequest to an Optional<JavaToolchainDownload>; the download response contains a URL, and only secure protocols such as https are accepted, so that nobody can tamper with the download in flight. And a settings plugin registers it through the injected JavaToolchainResolverRegistry, after applying the jvm-toolchain-management base plugin, which adds the jvm block to toolchainManagement. The skeleton of the official page (Java, not run here, because a real resolver needs a download service to talk to) is:

public abstract class JavaToolchainResolverImplementation implements JavaToolchainResolver {
    public Optional<JavaToolchainDownload> resolve(JavaToolchainRequest request) {
        return Optional.empty(); // custom mapping logic goes here instead
    }
}

public abstract class JavaToolchainResolverPlugin implements Plugin<Settings> {
    @Inject
    protected abstract JavaToolchainResolverRegistry getToolchainResolverRegistry();

    public void apply(Settings settings) {
        settings.getPluginManager().apply("jvm-toolchain-management");
        JavaToolchainResolverRegistry registry = getToolchainResolverRegistry();
        registry.register(JavaToolchainResolverImplementation.class);
    }
}

Resolver plugins are plugins like any other: how to build, test and publish them is the subject of Binary Plugins and Testing & Publishing Plugins.

Combining a toolchain with --release

A toolchain chooses the compiler; options.release chooses the language level and the API that the compiler lets the code see. Using both is the recommended way to compile with a recent JDK and still produce bytecode for an older Java: the manual’s example compiles with Java 17 and targets 11. In the tidepool build the toolchain is 21 and the root build file sets the release to 17 for every module:

Kotlin DSL: build.gradle.kts
subprojects {
    tasks.withType<JavaCompile>().configureEach {
        options.release = 17
    }
}
Groovy DSL: build.gradle
subprojects {
    tasks.withType(JavaCompile).configureEach {
        options.release = 17
    }
}

(Configuring subprojects from the root is a shortcut for a small example, not a recommendation; the build logic belongs in a convention plugin.) With it, javap reports major version: 61, which is Java 17, for classes that the JBR 21 compiled. The reason this combination matters shows in what happens without it. The next tasks run the unit test and a small program on a Java 17 launcher (their definition is in the section on tasks below), but the classes had been compiled by JDK 21 with the default target, 21. Both failed with the exception that every toolchain user eventually meets, run with ./gradlew --configuration-cache :tidepool-cli:runOn17 and :tidepool-model:testsOn17:

Error: LinkageError occurred while loading main class io.tidepool.cli.JvmInfo
	java.lang.UnsupportedClassVersionError: io/tidepool/cli/JvmInfo has been compiled by a more recent version of the Java Runtime (class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 61.0

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':tidepool-cli:runOn17' (registered in build file 'tidepool-cli/build.gradle.kts').
> Process 'command '~/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home/bin/java'' finished with non-zero exit value 1
...
Calculating task graph as no cached configuration is available for tasks: :tidepool-model:testsOn17
> Task :tidepool-model:processResources NO-SOURCE
> Task :tidepool-model:processTestResources NO-SOURCE
> Task :tidepool-model:compileJava UP-TO-DATE
> Task :tidepool-model:classes UP-TO-DATE
> Task :tidepool-model:compileTestJava UP-TO-DATE
> Task :tidepool-model:testClasses UP-TO-DATE

> Task :tidepool-model:testsOn17 FAILED

Gradle Test Executor 1 FAILED
    org.gradle.api.internal.tasks.testing.TestSuiteExecutionException at SuiteTestDefinitionProcessor.java:55
        Caused by: java.lang.UnsupportedClassVersionError at ClassLoader.java:-2

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':tidepool-model:testsOn17' (registered in build file 'tidepool-model/build.gradle.kts').
> Test process encountered an unexpected problem.
   > Could not execute test class 'io.tidepool.model.JavaVersionTest'.
      > io/tidepool/model/JavaVersionTest has been compiled by a more recent version of the Java Runtime (class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 61.0
...

(Both outputs are trimmed; the second ends before the usual list of hints.) The compiler of the toolchain 21 wrote class files of version 65, and the launcher 17 reads up to 61. Compiling with options.release = 17 fixes it, because the class files are now version 61, and it also makes the compiler refuse APIs newer than Java 17. The same build with the release raised above the toolchain fails at compile time:

> Task :tidepool-model:compileJava FAILED

[Incubating] Problems report is available at: file://<work>/build/reports/problems/problems-report.html

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':tidepool-model:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Java compilation initialization error
    error: release version 21 not supported
...

This one ran with the model’s toolchain at 17 and release = 21. The message comes from javac of the toolchain, which knows nothing about Java 21 (the report line of the Gradle problems API is left out). Setting sourceCompatibility and targetCompatibility instead of the release would not have stopped a call to a Java 21 method when compiling with JDK 21, which is the point of the table above.

Toolchains for individual tasks

The project toolchain is a default. Each task that runs a Java tool has a property for it, and the javaToolchains extension, a JavaToolchainService, creates the value from a specification. Three tools exist:

Provider Tool Used by

javaToolchains.compilerFor { ... }

JavaCompiler

JavaCompile (property javaCompiler)

javaToolchains.launcherFor { ... }

JavaLauncher

Test and JavaExec (property javaLauncher)

javaToolchains.javadocToolFor { ... }

JavadocTool

Javadoc (property javadocTool)

Depending on the task a JRE may be enough (a launcher) or a JDK is required (the compiler); Gradle prefers an installed JDK over a JRE when both satisfy the requirements. The properties are lazy Property values (see Lazy Configuration & Providers), so nothing is resolved, and nothing is provisioned, until the task needs it.

The most common use is to run tests on a different Java than the one the code is compiled for. The model module has a test that checks the Java it runs on (it prints the version and asserts that it matches a system property), and one Test task for each launcher:

Kotlin DSL: tidepool-model/build.gradle.kts
dependencies {
    testImplementation(platform("org.junit:junit-bom:6.1.3"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
    useJUnitPlatform()
    systemProperty("expected.java.feature", "21")
    testLogging {
        showStandardStreams = true
    }
}

tasks.register<Test>("testsOn17") {
    description = "Runs the unit tests on a Java 17 launcher."
    group = "verification"
    testClassesDirs = sourceSets.test.get().output.classesDirs
    classpath = sourceSets.test.get().runtimeClasspath
    useJUnitPlatform()
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
    systemProperty("expected.java.feature", "17")
    testLogging {
        showStandardStreams = true
    }
}
Groovy DSL: tidepool-model/build.gradle
dependencies {
    testImplementation platform('org.junit:junit-bom:6.1.3')
    testImplementation 'org.junit.jupiter:junit-jupiter'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test', Test) {
    useJUnitPlatform()
    systemProperty 'expected.java.feature', '21'
    testLogging {
        showStandardStreams = true
    }
}

tasks.register('testsOn17', Test) {
    description = 'Runs the unit tests on a Java 17 launcher.'
    group = 'verification'
    testClassesDirs = sourceSets.test.output.classesDirs
    classpath = sourceSets.test.runtimeClasspath
    useJUnitPlatform()
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
    systemProperty 'expected.java.feature', '17'
    testLogging {
        showStandardStreams = true
    }
}

The cli module has the same for an application, a JavaExec task that overrides the default launcher (the manual’s example is called runOn17 too):

Kotlin DSL: tidepool-cli/build.gradle.kts (added)
tasks.register<JavaExec>("runOn17") {
    description = "Runs JvmInfo on a Java 17 launcher."
    group = "application"
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
    classpath = sourceSets.main.get().runtimeClasspath
    mainClass = "io.tidepool.cli.JvmInfo"
}
Groovy DSL: tidepool-cli/build.gradle (added)
tasks.register('runOn17', JavaExec) {
    description = 'Runs JvmInfo on a Java 17 launcher.'
    group = 'application'
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
    classpath = sourceSets.main.runtimeClasspath
    mainClass = 'io.tidepool.cli.JvmInfo'
}

With options.release = 17 in place, one run of ./gradlew --configuration-cache :tidepool-model:test :tidepool-model:testsOn17 :tidepool-cli:runOn17 (identical in both DSLs, output shortened to the interesting tasks) printed:

Calculating task graph as configuration cache cannot be reused because file 'build.gradle.kts' has changed.
> Task :tidepool-model:processResources NO-SOURCE
> Task :tidepool-cli:processResources NO-SOURCE
> Task :tidepool-engine:processResources NO-SOURCE
> Task :tidepool-model:processTestResources NO-SOURCE
> Task :tidepool-model:compileJava
> Task :tidepool-model:classes
> Task :tidepool-model:jar
> Task :tidepool-engine:compileJava
> Task :tidepool-engine:classes
> Task :tidepool-engine:jar
> Task :tidepool-cli:compileJava
> Task :tidepool-cli:classes

> Task :tidepool-cli:runOn17
running on Java 17.0.14+1-b1367.22 (JetBrains s.r.o.)

> Task :tidepool-model:compileTestJava
> Task :tidepool-model:testClasses

> Task :tidepool-model:test

JavaVersionTest > runsOnTheExpectedJava() STANDARD_OUT
    tests run on Java 21.0.10+7-b1163.110

> Task :tidepool-model:testsOn17

JavaVersionTest > runsOnTheExpectedJava() STANDARD_OUT
    tests run on Java 17.0.14+1-b1367.22

BUILD SUCCESSFUL in 5s
9 actionable tasks: 9 executed
Configuration cache entry updated for no projects, no projects up-to-date.

The default test task ran on the JBR 21 and testsOn17 on the JBR 17, in a build whose daemon runs on 26; the program ran on 17. A second run reused the configuration cache entry and reported the task as up to date. (testLogging { showStandardStreams = true } is only there to print the version; the test also asserts it. The tests, the JUnit platform and the test reports are the subject of Testing JVM Projects.) The same mechanism lets a module compile with an older JDK than the rest, or build its Javadoc with a newer one. In the engine module, the compile task was given Java 17 and the Javadoc task Java 21 (the project toolchain stays 21):

Kotlin DSL: tidepool-engine/build.gradle.kts (added)
tasks.compileJava {
    javaCompiler = javaToolchains.compilerFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

tasks.javadoc {
    javadocTool = javaToolchains.javadocToolFor {
        languageVersion = JavaLanguageVersion.of(21)
    }
}
Groovy DSL: tidepool-engine/build.gradle (added)
tasks.named('compileJava') {
    javaCompiler = javaToolchains.compilerFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

tasks.named('javadoc') {
    javadocTool = javaToolchains.javadocToolFor {
        languageVersion = JavaLanguageVersion.of(21)
    }
}
> Task :tidepool-engine:processResources NO-SOURCE
> Task :tidepool-model:compileJava
Compiling with toolchain '~/Library/Java/JavaVirtualMachines/jbr-21.0.10/Contents/Home'.
> Task :tidepool-engine:compileJava
Compiling with toolchain '~/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home'.
> Task :tidepool-engine:classes
> Task :tidepool-engine:javadoc
Starting process 'command '~/Library/Java/JavaVirtualMachines/jbr-21.0.10/Contents/Home/bin/javadoc''. ...
BUILD SUCCESSFUL in 2s

(./gradlew --configuration-cache :tidepool-engine:compileJava :tidepool-engine:javadoc --info, filtered with grep for the lines shown; the last Starting process line is cut after the name of the executable, and javap on the engine’s class file said major version: 61, since the release flag is still 17.) The engine compiled with the 17 compiler while its dependency, the model module, compiled with 21, and Javadoc was run by the javadoc executable of the JBR 21. The Groovy DSL form of the same two tasks gave the same two Compiling with toolchain lines.

Tools that take a path

Some tasks, including tasks of third-party plugins, want a path to a java executable or a Java home instead of a launcher property. They can still use toolchains: the launcher or compiler carries metadata with its executablePath, and metadata.installationPath is the Java home. The manual’s advice is to map them lazily, for example javaExecutable = launcher.map { it.executablePath }, because calling launcher.get() while configuring "may realize (and provision) a toolchain eagerly". The next section has a runnable version of that wiring.

Toolchains for plugin authors

A plugin or task that runs a Java tool should follow the same convention as the core plugins: default to the project’s toolchain, and let the user override it per task. The manual’s pattern is a JavaLauncher property marked @Nested (so that the task responds to a toolchain change), whose convention is the launcher of the project’s java extension. The following task prints what it was given. It is declared in the cli module, with two registrations, one with the default and one with an override, and extends the pattern with the path-based properties of the previous section, wired lazily:

Kotlin DSL: tidepool-cli/build.gradle.kts (added)
abstract class ShowToolchain : DefaultTask() {
    @get:Nested
    abstract val launcher: Property<JavaLauncher>

    @get:Internal
    abstract val javaExecutable: RegularFileProperty

    @get:Internal
    abstract val javaHome: DirectoryProperty

    @get:Inject
    protected abstract val javaToolchainService: JavaToolchainService

    init {
        val toolchain = project.extensions.getByType<JavaPluginExtension>().toolchain
        launcher.convention(javaToolchainService.launcherFor(toolchain))
        javaExecutable.convention(launcher.map { it.executablePath })
        javaHome.convention(launcher.map { it.metadata.installationPath })
    }

    @TaskAction
    fun show() {
        val metadata = launcher.get().metadata
        println("Java ${metadata.languageVersion}, ${metadata.vendor}, ${metadata.jvmVersion}")
        println("executable: ${javaExecutable.get().asFile}")
        println("java home:  ${javaHome.get().asFile}")
    }
}

tasks.register<ShowToolchain>("showDefaultToolchain")

tasks.register<ShowToolchain>("showCustomToolchain") {
    launcher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
Groovy DSL: tidepool-cli/build.gradle (added)
abstract class ShowToolchain extends DefaultTask {
    @Nested
    abstract Property<JavaLauncher> getLauncher()

    @Internal
    abstract RegularFileProperty getJavaExecutable()

    @Internal
    abstract DirectoryProperty getJavaHome()

    @Inject
    protected abstract JavaToolchainService getJavaToolchainService()

    ShowToolchain() {
        def toolchain = project.extensions.getByType(JavaPluginExtension).toolchain
        launcher.convention(javaToolchainService.launcherFor(toolchain))
        javaExecutable.convention(launcher.map { it.executablePath })
        javaHome.convention(launcher.map { it.metadata.installationPath })
    }

    @TaskAction
    void show() {
        def metadata = launcher.get().metadata
        println "Java ${metadata.languageVersion}, ${metadata.vendor}, ${metadata.jvmVersion}"
        println "executable: ${javaExecutable.get().asFile}"
        println "java home:  ${javaHome.get().asFile}"
    }
}

tasks.register('showDefaultToolchain', ShowToolchain)

tasks.register('showCustomToolchain', ShowToolchain) {
    launcher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
Calculating task graph as no cached configuration is available for tasks: :tidepool-cli:showDefaultToolchain :tidepool-cli:showCustomToolchain

> Task :tidepool-cli:showDefaultToolchain
Java 21, JetBrains, 21.0.10+7-b1163.110
executable: ~/Library/Java/JavaVirtualMachines/jbr-21.0.10/Contents/Home/bin/java
java home:  ~/Library/Java/JavaVirtualMachines/jbr-21.0.10/Contents/Home

> Task :tidepool-cli:showCustomToolchain
Java 17, JetBrains, 17.0.14+1-b1367.22
executable: ~/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home/bin/java
java home:  ~/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home

BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
Configuration cache entry stored.

(./gradlew --configuration-cache :tidepool-cli:showDefaultToolchain :tidepool-cli:showCustomToolchain, both DSLs; the second run reused the configuration cache entry, so the task type is configuration-cache compatible. The project reference in init is a configuration-time use, which is allowed.) In short: the launcher property is @Nested "to make sure the task is responsive to toolchain changes", as the manual says; the user needs to configure the toolchain once, at the project level, and individual tasks may override it; and the convention comes from JavaToolchainService.launcherFor(toolchain), which receives the specification of the java extension. When a task needs a toolchain without the java plugin applied, the service can be used directly. If an unconfigured specification is given to it, it always returns the tool of the toolchain that is running Gradle (for example javaToolchainService.launcherFor({})). The java plugin is applied automatically by the core Groovy and Scala plugins and by the Kotlin plugin, so for JVM projects it is safe to assume that it is there. A plugin that works with toolchains also has to be built for it: since 8.0, precompiled script plugins use the project’s toolchain (Java 8 if none), and the 9.0 upgrade guide says that the ValidatePlugins task fails unless the jvm-toolchains plugin is applied, which the Java Library plugin and the other JVM plugins do. How to write tasks and plugins is in Writing Custom Tasks and Binary Plugins.

Toolchains and the JVM of the daemon

The toolchain of the project and the JVM of the daemon are independent, and the daemon has its own, declarative file. ./gradlew updateDaemonJvm writes gradle/gradle-daemon-jvm.properties with the language version, optionally the vendor, and download URLs; Gradle then starts a daemon on a matching JDK, which it finds with the same auto-detection and, if necessary, downloads with the same auto-provisioning described above (the org.gradle.java.installations.* properties apply to the daemon too). The criteria take precedence over org.gradle.java.home and JAVA_HOME. The feature appeared in Gradle 8.8 as an incubating preview (called "Gradle daemon JVM configurable via toolchain" in the release notes), became stable in 9.2.0 (2025-10-29), and 8.14 added native-image capability to it. All the runs, the file format, the vendors and the messages are on The Gradle Daemon and are not repeated here. In a build that uses both, a typical split is: the daemon on the newest LTS the team likes (so that Gradle itself is fast and supported), and the toolchain on the Java version of the product. The ./gradlew --version lines Launcher JVM and Daemon JVM, together with javaToolchains, tell which of the three is which.

Other languages and plugins

Toolchains work through the java extension, so every JVM language plugin that applies the Java plugin shares them. The manual’s statement: "Toolchain support is available in the Java plugins and for the tasks they define."

  • Java: compilation, tests, Javadoc and, since 9.0, JavaExec.

  • Groovy: compilation is supported, Groovydoc generation is not (Groovy plugin support came with 6.8).

  • Scala: compilation and Scaladoc.

  • Kotlin: the Kotlin Gradle plugin supports toolchains for Kotlin/JVM compilation. Per the Kotlin documentation, the toolchain sets -jdk-home, sets jvmTarget to the toolchain’s version unless the user set it, and is used by Java compile, test and Javadoc tasks and by the kapt workers; the Kotlin compiler itself always runs on the daemon’s JDK. It can be set through the Java extension or with kotlin { jvmToolchain(21) } (not run here: it needs the Kotlin plugin; see Kotlin DSL for the difference between the Kotlin DSL, which is Gradle’s build-script language, and Kotlin as a language of the project).

  • PMD: since 8.0 it runs in a worker process that uses the JVM toolchain.

  • Android: the Android Gradle plugin supports Gradle’s toolchains from 7.4.0, with the caveat in the Kotlin documentation about targetCompatibility before 8.1.0-alpha09; the Android page of this site covers the rest.

Which Java versions are supported

The compatibility matrix of the user manual lists, for each Java version, the first Gradle release that supports it as a toolchain (to compile, test and run code) and the first that can run Gradle on it. The rows that matter today:

Java Toolchain support from Running Gradle from

8

N/A in the matrix

2.0 to 8.14.x

11

N/A in the matrix

5.0 to 8.14.x

15

6.7

6.7 to 8.14.x

17

7.3

7.3 and after

21

8.4

8.5 and after

24

8.14

8.14 and after

25

9.1.0

9.1.0 and after

26

9.4.0

9.4.0 and after

27

9.8.0

9.8.0 and after

Gradle 9.8.1 needs Java 17 to 27 to run; Java 28 and later is not yet supported. Toolchains are what makes the first column useful beyond the second: the official page says that any fully supported Java can be used for compilation or testing, that Java 6 and later can be used for compilation, Java 8 and later for running tests, and that "the latest Java version may only be supported for compilation or testing, not for running Gradle". A project that compiled with Java 8 or 11 on Gradle 8.x keeps doing so on Gradle 9 through a toolchain, as the 9.0 upgrade guide says. A Java 8 toolchain was not run here: only JDKs 17, 21 and 26 are installed on the machine and nothing may be downloaded.

In the IDE, in CI and in containers

IDE. IntelliJ IDEA (Settings, Build, Execution, Deployment, Gradle, "Gradle JVM") and Eclipse (Preferences, Gradle, "Gradle JDK") each have a setting for the JVM that runs Gradle. It affects the daemon, "not how your code is compiled", unless the build declares no toolchain, in which case the fallback may differ between team members. The manual’s advice is to align the IDE’s Gradle JVM with the command line (JAVA_HOME or the default Java), and to declare a toolchain so that compilation does not depend on the IDE at all. IntelliJ-downloaded JDKs are themselves a detection source (the "IntelliJ" entries in the report above). The IDE settings were not run here. See CI & IDE Integration.

CI. Make the JDKs part of the image, and set org.gradle.java.installations.auto-download=false so that a missing JDK is an immediate, explained failure (the first error above) instead of a download in the middle of a build, or give the build a Foojay-style repository that points at an internal mirror. In both cases keep the javaToolchains report in the build log when debugging. A cached provisioned JDK lives in jdks/ of the Gradle User Home, so the directory belongs with the other things to cache (see Directories & Logging).

Containers. The manual lists one limitation: Gradle may detect toolchains incorrectly when it runs in a JVM compiled against musl, the C library of most Alpine Linux distributions, because such a JVM can override LD_LIBRARY_PATH, which then leaks into the forked Java processes. Using several toolchains is therefore discouraged on musl; a single toolchain (the JVM running Gradle) is fine. The advice is to use another distribution, such as Ubuntu, for the build image. This was not run here.

The same in Ant and Maven

Concern Ant Maven Gradle

Language level

release (or source and target) attribute of <javac>

maven.compiler.release

options.release

Which JDK compiles

The JVM running Ant, or fork="true" and executable of <javac>; jvm of <java>

The JVM running Maven, or toolchains.xml with the Toolchains Plugin (or fork and executable)

java.toolchain, found by auto-detection

Where JDKs are described

nowhere; JAVA_HOME, PATH or a property

~/.m2/toolchains.xml, per machine (Gradle also reads it as a detection source)

auto-detected, or org.gradle.java.installations.* properties

Missing JDK

build fails

build fails ("misconfigured toolchains fail" in Maven 3.10)

build fails, or the JDK is downloaded from a configured repository

Different JDK for tests

jvm attribute of the forked <junit> task

jvm or jdkToolchain parameter of the Surefire plugin

javaLauncher of the Test task

The links for the first two columns: Ant: Compiling Java and Ant: Running Programs; Maven Compiling & Toolchains, and for Surefire Maven: Testing with Surefire. The shared comparison is in Comparing Ant, Maven and Gradle, and what carries over when a Maven build moves to Gradle in Migrating from Maven. The main difference stays the one the Maven page states: in Gradle the requirement is in the build file and a missing JDK can be downloaded; in Maven the JDK has to be installed and described on each machine.

Release history

Release Change

6.7 (2020-10-14)

Java toolchains introduced, selected by language version.

6.8 (2021-01-08)

Vendor and implementation (J9) selection; the javaToolchains task; toolchain support for Groovy compilation.

7.0 (2021-04-09)

Toolchains stable; Java 16.

7.6 (2022-11-25)

Toolchain resolver plugins and toolchainManagement: downloads from repositories of your choice.

8.0 (2023-02-13)

No default toolchain provisioner (a repository must be declared); invalid toolchain specifications are errors; precompiled script plugins use the project toolchain.

8.8 (2024-05-31)

Daemon JVM configured from a toolchain (incubating); errors of a resolver no longer stop the search.

8.14 (2025-04-25)

nativeImageCapable for toolchains and the daemon toolchain; Java 24.

9.0.0 (2025-07-31)

Gradle needs Java 17 to run (toolchains for older targets); JAVA_HOME is a detection source; JavaExec defaults to the project toolchain.

9.1.0 (2025-09-18) and 9.4.0 (2026-03-04) and 9.8.0 (2026-09-24)

Java 25, 26 and 27 as toolchains and for the daemon.

9.2.0 (2025-10-29)

Daemon toolchain stable.

Source: the release notes 6.7, 6.8, 7.0, 7.6, 8.0, 8.8, 8.14, 9.0.0, 9.1.0, 9.2.0, 9.4.0 and 9.8.0

What differs in Gradle 8.14

Gradle 8.14.6 was not available here, so this list comes from the release notes and upgrade guides and is not run here.

  • The core feature set is the same: java.toolchain, vendors, implementations, javaToolchains, auto-detection with its properties, repositories through toolchainManagement, the per-task providers, and (from 8.14) nativeImageCapable.

  • Gradle itself runs on Java 8 to 24 (the matrix above); the 8.x daemon is not limited to 17+, so there the "daemon on a new Java, code on an old one" split is less necessary. 8.14 cannot use Java 25, 26 or 27 as a toolchain.

  • JavaExec runs on the JVM of Gradle itself unless javaLauncher is set; it does not default to the project toolchain.

  • JAVA_HOME is not a toolchain detection source.

  • The Daemon JVM criteria are incubating and print an incubation warning.

  • A legacy build that relied on the implicit Adoptium download of 7.x already needed a repository in 8.0.

  • Gradle 8.9 changed the marker file of provisioned toolchains: existing ones are provisioned again the first time they are used.

Source: Upgrading from Gradle 8.14 to 9.0, Upgrading to Gradle 9.0.0 and the release notes above

Where the books are outdated

Gradle in Action (Muschko, Manning 2014, Gradle 1.7) sets the Java level of a build with sourceCompatibility and targetCompatibility in the Java plugin chapters and takes the compiler from whichever JDK is on the path or in JAVA_HOME; the IDE plugins' jdkName setting plays the same role for project files. None of the toolchain features existed: toolchains (6.7), vendor selection (6.8), automatic downloads (7.6) and the javaToolchains report (6.8) are all later, and the manual now recommends toolchains and, for strict cross-compilation, --release, and calls the compatibility properties a fallback. The book’s requirement of "JDK 1.5 or later" to run Gradle is also long gone: Gradle 9 needs Java 17 or later to run, and older Java versions are reachable only through toolchains. The book’s page numbers were not checked; the statement follows the research notes behind this section.

Summary

  • A toolchain is a JDK that the build asks for in java { toolchain { languageVersion = ... } }; Gradle uses it for compile, test, Javadoc and (since 9.0) JavaExec tasks, while the daemon keeps running on its own Java 17 to 27.

  • Optional vendor, implementation and nativeImageCapable narrow the choice; a specification without a language version is an error.

  • Gradle finds JDKs by auto-detection (operating system, JAVA_HOME, package managers, IntelliJ, Maven toolchains, plus org.gradle.java.installations.paths and fromEnv); ./gradlew javaToolchains shows what it found and why.

  • Several matches are sorted: the running JVM, JDKs over JREs, vendor order, then versions and path.

  • A missing JDK is downloaded only if auto-download allows it and a repository such as the Foojay plugin is declared (no default since 8.0); otherwise the build fails with "Cannot find a Java installation on your machine …​ Toolchain auto-provisioning is not enabled." or "…​ Toolchain download repositories have not been configured."

  • A toolchain does not stop use of newer APIs: combine it with options.release, or classes compiled for a newer Java fail on an older launcher with UnsupportedClassVersionError.

  • Single tasks get their own tool with javaCompiler, javaLauncher and javadocTool; plugin authors default to the project toolchain through JavaToolchainService and a @Nested launcher.