Gradle Init Scripts & the Tooling API

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 on 2026-10-16 with the 9.8.1 wrapper 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 and the Tooling API clients ran on Oracle JDK 26 (java version "26"), the default JDK of the machine, and no toolchain was involved. Every run used a Gradle User Home created for the page (the GRADLE_USER_HOME environment variable), so that nothing was written to the real ~/.gradle, and a gradle.properties in it that sets org.gradle.jvmargs=-Xmx512m -Duser.language=en -Duser.country=US, so that messages are in English. The scratch copies pointed their wrapper at a local copy of the same distribution archive; the properties file that a real project commits is

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

The scratch directory is shown as <work> and the Gradle User Home of a run as <gradle-user-home>; only that path prefix was replaced. 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". Also not run here: the init.d directory of the Gradle installation (GRADLE_HOME), connecting the Tooling API with useGradleVersion (it downloads a distribution), running a single test class through the TestLauncher, and publishing a Build Scan.

Two features of Gradle sit at the edge of the build and are used by tools more than by build authors. An init script runs before the build proper and can change how every build on a machine, or in a CI job, behaves, without touching any repository. The Tooling API is the library with which IntelliJ IDEA, Eclipse Buildship, Visual Studio Code and Gradle’s own TestKit start builds and ask Gradle what a build looks like. The page covers both, and ends with the older way of feeding an IDE, the idea and eclipse plugins that write project files, most of which Gradle has started to deprecate. For the phases that init scripts hook into see The Build Lifecycle; for the directories they live in see Directories & Logging; and for how IDEs and CI servers use the three build tools side by side see CI and IDE Integration.

What an init script is

An init script is a Groovy or Kotlin script that Gradle runs during the initialization phase, "before the build’s settings.gradle(.kts) and project build scripts are evaluated", in the words of the manual. The receiver of the script is the Gradle object (the whole build invocation), not a Project or a Settings. The error for a misspelt call shows it: Groovy reports "Could not find method printn() for arguments [oops] on build ':' of type org.gradle.invocation.DefaultGradle" (the full text is in the error gallery below). Because Gradle is available before any project exists, init scripts register callbacks (beforeSettings, settingsEvaluated, projectsLoaded, beforeProject, afterProject, allprojects, projectsEvaluated, taskGraph and more) that run when those objects appear; these are the hooks of the lifecycle page.

The manual lists what init scripts are for:

  • enterprise-wide defaults, such as plugin resolution rules and repository mirrors;

  • configuration that varies by environment (a developer’s laptop against CI);

  • user-specific data, such as credentials supplied through providers;

  • machine-specific details, such as the location of a JDK or an SDK;

  • build listeners and custom loggers.

The manual’s warning is that an init script "affects every build" it is attached to, local or CI, so it is to be treated like production code. One limitation is stated too: init scripts cannot use the classes of the buildSrc directory of the build they run against, because buildSrc is compiled after the initialization phase (not run here; use an init plugin from a published JAR, or an included build, for shared code).

Where Gradle finds init scripts

The manual lists four places, and says that Gradle runs all the scripts it finds, in this order:

Order Where Typical use

1

The command line: -I or --init-script followed by a file, which may be repeated

One-off or CI-specific scripts, passed by the pipeline

2

$GRADLE_USER_HOME/init.gradle or init.gradle.kts

A personal script that applies to every build of one user

3

$GRADLE_USER_HOME/init.d/, every *.init.gradle and *.init.gradle.kts file in alphabetical order

Several small scripts; a numeric prefix controls the order

4

$GRADLE_HOME/init.d/ in the Gradle installation, alphabetical order

Scripts shared by everyone who uses that installation (not run here)

The Gradle User Home defaults to ~/.gradle; the GRADLE_USER_HOME environment variable and --gradle-user-home move it (see Directories & Logging). The fourth place is the init.d directory of the unpacked distribution, which is empty in the official download (see Installing Gradle), so it matters mainly for a Gradle installed by hand or baked into a CI image.

The demonstration uses a Gradle User Home that holds one script of each kind, plus a file that is not a script. Each script is a single println.

File Content

scripts/command-line.init.gradle.kts (passed with -I)

println("[init] 0 -I command-line.init.gradle.kts")

<gradle-user-home>/init.gradle.kts

println("[init] 1 $GRADLE_USER_HOME/init.gradle.kts")

<gradle-user-home>/init.d/10-corporate.init.gradle.kts

println("[init] 2 init.d/10-corporate.init.gradle.kts")

<gradle-user-home>/init.d/20-developer.init.gradle.kts

println("[init] 3 init.d/20-developer.init.gradle.kts")

<gradle-user-home>/init.d/30-plain.gradle.kts

println("[init] 4 init.d/30-plain.gradle.kts (no .init in the name)")

<gradle-user-home>/init.d/notes.txt

println("[init] 5 init.d/notes.txt is not run")

Run with the command-line script:

GRADLE_USER_HOME=<gradle-user-home> ./gradlew --console=plain -q -I ../scripts/command-line.init.gradle.kts :tidepool-model:compileJava
[init] 0 -I command-line.init.gradle.kts
[init] 1 $GRADLE_USER_HOME/init.gradle.kts
[init] 2 init.d/10-corporate.init.gradle.kts
[init] 3 init.d/20-developer.init.gradle.kts
[init] 4 init.d/30-plain.gradle.kts (no .init in the name)

The Groovy copy has the same layout with init.gradle, 10-corporate.init.gradle, 20-developer.init.gradle and 30-plain.gradle, and printed the five lines with the .gradle names:

Groovy DSL: output
[init] 0 -I command-line.init.gradle
[init] 1 GRADLE_USER_HOME/init.gradle
[init] 2 init.d/10-corporate.init.gradle
[init] 3 init.d/20-developer.init.gradle
[init] 4 init.d/30-plain.gradle (no .init in the name)

Three things the run shows. The command-line script ran first, ahead of the Gradle User Home, as the manual says. The files of init.d ran in alphabetical order, which is why corporate numbering such as 10-, 20- is used. And a file whose name does not contain .init. (30-plain.gradle.kts) also ran, although the manual describes the pattern as *.init.gradle(.kts); the .txt file did not. Do not rely on that: name scripts the way the manual says.

Kotlin init scripts (*.init.gradle.kts) exist since Gradle 4.6, whose release notes list "initialization scripts support" among the changes of Kotlin DSL 0.15.6; Groovy init scripts are older than any version covered here.

The configuration cache and init scripts

Init scripts are inputs of the configuration cache. The same scripts, run with --configuration-cache three times (the first stores an entry, the second reuses it, the third reuses it again), printed their five lines the first time and nothing on the second and third: when the entry is reused the top-level code of the scripts did not run at all, so a println in an init script is good for debugging and for nothing else. Editing one of the scripts invalidates the entry, and Gradle names the file:

Calculating task graph as configuration cache cannot be reused because init script '<gradle-user-home>/init.d/20-developer.init.gradle.kts' has changed.
[init] 0 -I command-line.init.gradle.kts
[init] 1 $GRADLE_USER_HOME/init.gradle.kts
[init] 2 init.d/10-corporate.init.gradle.kts
[init] 3 init.d/20-developer.init.gradle.kts (edited)
[init] 4 init.d/30-plain.gradle.kts (no .init in the name)
...
Configuration cache entry stored.

(The path in the message is relative to the project directory in the real output; the middle of the output is left out.) Prefer providers (providers.gradleProperty, providers.environmentVariable) to reading System values at the top of a script: they are lazy and explicit about what the build depends on, and the examples below use them.

Writing an init script

The callbacks that matter, with the moment each one runs (the lifecycle page shows them observed in order):

Callback on Gradle When

beforeSettings { }

Before the settings file is evaluated. The Settings object can be configured, for example pluginManagement, before the build’s own settings code runs.

settingsEvaluated { }

After the settings file has run: the place to adjust what the settings declared (repositories, included builds).

projectsLoaded { }

When the project tree is known, before any project is configured.

allprojects { }, beforeProject { }, afterProject { }

Configure every project, or run code around each project’s configuration. allprojects { } is the same call as in a root build file, executed before the project’s own script.

projectsEvaluated { }, taskGraph.whenReady { }

After all projects are configured, and when the task graph is known.

One rule catches people out. Properties from gradle.properties are available on Settings and Project, not on Gradle, so an init script reads them through a provider on the settings object: settings.providers.gradleProperty("name"). The examples below use it.

Configuring projects: allprojects, and the repositories mode

The manual’s first example adds a repository to every project. In the tidepool build, whose settings follow the convention of this section (repositories declared once in settings.gradle.kts, with RepositoriesMode.FAIL_ON_PROJECT_REPOS), that example fails:

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'
Kotlin DSL: extra-repo.init.gradle.kts
allprojects {
    repositories {
        mavenLocal()
    }
}
Groovy DSL: extra-repo.init.gradle
allprojects {
    repositories {
        mavenLocal()
    }
}
./gradlew --console=plain -q -I ../scripts/extra-repo.init.gradle.kts :tidepool-model:compileJava
FAILURE: Build failed with an exception.

* What went wrong:
Build was configured to prefer settings repositories over project repositories but repository 'MavenLocal' was added by initialization script '../scripts/extra-repo.init.gradle.kts'

* Try:
> Run with --stacktrace option to get the stack trace.
...

The Groovy script gave the same message with the .gradle file name, preceded by a Where: line that names the init script and line 3. The message is useful: it names the script that broke the rule. The modern place for repositories is the settings, so an init script that wants to change them should change the settings, which is the next example.

An enterprise mirror from a settings hook

A company that runs a repository manager wants every build on its machines to resolve through it, whatever the repositories of the project say. The init script below replaces the repositories that the settings declared with one mirror, but only when the user has set a mirrorUrl property, which is user-specific data and so lives in the user’s gradle.properties, not in the script. (Here the mirror is a directory in Maven repository layout holding picocli, Commons CSV, their dependencies and the POMs that go with them, copied from the Gradle cache of an earlier build, so that the demonstration needs no server.)

The Gradle User Home’s gradle.properties
org.gradle.jvmargs=-Xmx512m -Duser.language=en -Duser.country=US
mirrorUrl=file://<work>/mirror
Kotlin DSL: mirror.init.gradle.kts
settingsEvaluated {
    val mirrorUrl = providers.gradleProperty("mirrorUrl")
    if (mirrorUrl.isPresent) {
        dependencyResolutionManagement.repositories.apply {
            clear()
            maven {
                name = "corporateMirror"
                url = uri(mirrorUrl.get())
            }
        }
        val names = dependencyResolutionManagement.repositories.map { it.name }
        println("[mirror] dependency repositories: $names")
    }
}
Groovy DSL: mirror.init.gradle
settingsEvaluated { settings ->
    def mirrorUrl = settings.providers.gradleProperty('mirrorUrl')
    if (mirrorUrl.present) {
        settings.dependencyResolutionManagement.repositories {
            clear()
            maven {
                name = 'corporateMirror'
                url = uri(mirrorUrl.get())
            }
        }
        def names = settings.dependencyResolutionManagement.repositories*.name
        println "[mirror] dependency repositories: ${names}"
    }
}

Run in both copies with a fresh Gradle User Home that holds the gradle.properties above, building :tidepool-cli:build with --configuration-cache:

Kotlin DSL and Groovy DSL: both printed
Calculating task graph as no cached configuration is available for tasks: :tidepool-cli:build
[mirror] dependency repositories: [corporateMirror]
> Task :tidepool-model:compileJava
...
BUILD SUCCESSFUL in 14s
Configuration cache entry stored.

(Task lines are trimmed; the Kotlin copy had already been built, so its tasks were up to date.) The dependency report then shows that the mirror served the whole tree:

./gradlew --console=plain -q -I ../scripts/mirror.init.gradle.kts :tidepool-engine:dependencies --configuration runtimeClasspath
[mirror] dependency repositories: [corporateMirror]

------------------------------------------------------------
Project ':tidepool-engine'
------------------------------------------------------------

runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-model'
\--- org.apache.commons:commons-csv:1.14.1
     +--- commons-io:commons-io:2.20.0
     \--- commons-codec:commons-codec:1.19.0

A web-based, searchable dependency report is available by adding the --scan option.

To check that nothing else was consulted, a build of :tidepool-cli:compileJava in an empty Gradle User Home with --debug --no-daemon resolved picocli-4.7.7.jar from the mirror directory and mentioned the mirror in 60 log lines and Maven Central’s hosts (repo.maven.apache.org, repo1.maven.org) in none. This script changes only dependency repositories. A real policy also changes pluginManagement.repositories (the plugin repositories) in the same hook, and Gradle 9.8.0 can read mirrors from a Maven settings.xml instead (org.gradle.mirror.maven.settings, incubating, covered with the repositories in Repositories & Content Filtering).

External dependencies for the script

An init script can have its own classpath, declared in an initscript { } block (a ScriptHandler): repositories and a classpath configuration. Any dependency type except a project dependency can be added. The manual’s example uses Apache Commons Math:

Kotlin DSL: classpath.init.gradle.kts
import org.apache.commons.math.fraction.Fraction

initscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.apache.commons:commons-math:2.0")
    }
}

println("One fifth, twice: " + Fraction.ONE_FIFTH.multiply(2))
Groovy DSL: classpath.init.gradle
import org.apache.commons.math.fraction.Fraction

initscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.apache.commons:commons-math:2.0'
    }
}

println "One fifth, twice: " + Fraction.ONE_FIFTH.multiply(2)

Both printed (./gradlew -q -I classpath.init.gradle.kts :tidepool-model:compileJava, with classpath.init.gradle for Groovy):

One fifth, twice: 2 / 5

The repositories of the initscript block are independent of the build’s: they serve only the script, which is why this works under FAIL_ON_PROJECT_REPOS. They are also what a packaged init plugin needs, next.

Init plugins

An init plugin is a plugin whose target type is Plugin<Gradle>. Where a project plugin (Plugin<Project>) acts when a project is configured and a settings plugin (Plugin<Settings>) when the settings are, an init plugin attaches to the Gradle object during initialization. The manual points out its reach and its limit: it is applied from an init script, so it affects every build on that machine or agent, but not every build everywhere; TestKit builds, for example, use an isolated Gradle User Home that does not see your init.d, and other users and agents have their own. The comparison of the three plugin targets is in Plugins Overview, and the building and publishing of plugins in general in Binary Plugins and Testing & Publishing Plugins.

The manual shows the plugin class inside the init script itself, which is fine for a few lines. For anything shared between machines the plugin is better compiled, tested and published like any other library. The example plugin adds a ciInfo task to every project, so that a team can ask any build "does this run on CI?" without editing the build. It is written in Java and is the same for both DSLs:

TidepoolInitPlugin.java
package io.tidepool.init;

import org.gradle.api.Plugin;
import org.gradle.api.invocation.Gradle;
import org.gradle.api.provider.Provider;

public class TidepoolInitPlugin implements Plugin<Gradle> {
    @Override
    public void apply(Gradle gradle) {
        gradle.allprojects(project -> {
            Provider<String> ci = project.getProviders()
                    .environmentVariable("CI")
                    .orElse("not set");
            String name = project.getName();
            project.getTasks().register("ciInfo", task -> {
                task.setGroup("help");
                task.setDescription("Shows whether the build runs on CI (added by an init plugin).");
                task.doLast(t -> System.out.println(name + ": CI=" + ci.get()));
            });
        });
    }
}

The environment variable is read through a Provider and the task captures a String and the provider, not the Project, so that the task is compatible with the configuration cache. The build file of the plugin project compiles against the Gradle API and publishes to a repository inside the project (a stand-in for the company’s repository):

Kotlin DSL: init-plugin/build.gradle.kts
plugins {
    `java-library`
    `maven-publish`
}

group = "io.tidepool"
version = "0.1.0"

repositories {
    mavenCentral()
}

dependencies {
    compileOnly(gradleApi())
}

publishing {
    publications {
        create<MavenPublication>("initPlugin") {
            from(components["java"])
        }
    }
    repositories {
        maven {
            name = "buildRepo"
            url = uri(layout.buildDirectory.dir("repo"))
        }
    }
}
Groovy DSL: init-plugin/build.gradle
plugins {
    id 'java-library'
    id 'maven-publish'
}

group = 'io.tidepool'
version = '0.1.0'

repositories {
    mavenCentral()
}

dependencies {
    compileOnly gradleApi()
}

publishing {
    publications {
        initPlugin(MavenPublication) {
            from components.java
        }
    }
    repositories {
        maven {
            name = 'buildRepo'
            url = layout.buildDirectory.dir('repo')
        }
    }
}

./gradlew --configuration-cache publish ran the five tasks generatePomFileForInitPluginPublication, compileJava, jar, generateMetadataFileForInitPluginPublication and publishInitPluginPublicationToBuildRepoRepository, in both copies, and the repository received tidepool-init-plugin-0.1.0 as a JAR, a POM and a Gradle Module Metadata file. The init script puts that repository on its classpath and applies the plugin:

Kotlin DSL: tidepool-init-plugin.init.gradle.kts
import io.tidepool.init.TidepoolInitPlugin

initscript {
    repositories {
        maven {
            url = uri("file://<work>/init-plugin-kts/build/repo")
        }
    }
    dependencies {
        classpath("io.tidepool:tidepool-init-plugin:0.1.0")
    }
}

apply<TidepoolInitPlugin>()
Groovy DSL: tidepool-init-plugin.init.gradle
import io.tidepool.init.TidepoolInitPlugin

initscript {
    repositories {
        maven {
            url = 'file://<work>/init-plugin-groovy/build/repo'
        }
    }
    dependencies {
        classpath 'io.tidepool:tidepool-init-plugin:0.1.0'
    }
}

apply plugin: TidepoolInitPlugin

Applied to the unchanged tidepool build (CI=true ./gradlew --console=plain -q -I ../scripts/tidepool-init-plugin.init.gradle.kts ciInfo), the task exists in the root project and in every subproject:

tidepool: CI=true
tidepool-cli: CI=true
tidepool-engine: CI=true
tidepool-model: CI=true

Without the variable the Groovy copy printed tidepool: CI=not set and the same for the three subprojects, and ./gradlew tasks --all lists ciInfo for each project with the description from the plugin. With --configuration-cache the build ran too; the order of the four lines is then not guaranteed, because with the configuration cache the tasks of unrelated projects run in parallel. The init plugin is compatible with the configuration cache.

When to use an init script

An init script is the right tool when the rule belongs to the machine or the CI agent, not to a repository. The other places for shared logic are in the same table, because choosing the wrong one is the common mistake.

Mechanism Use it for Reaches

gradle.properties (user or project)

Plain values: JVM arguments, caching switches, credentials as properties

One user or one project

Settings file (pluginManagement, dependencyResolutionManagement)

Repositories and plugin versions of one build, in version control

Everyone who builds that repository

Convention plugins in an included build build-logic

Shared build logic of one or many repositories (see Convention Plugins & Sharing Build Logic)

Builds that apply the plugin

Init script or init plugin

Policy that must apply to builds you do not edit: mirrors, a Build Scan server, listeners, machine-specific paths

Every build of a user or an agent that has the script

A Build Scan or Develocity setup for a whole CI fleet is the typical case for the last row: the server address and the "publish on failure" policy are properties of the agent, not of any project. Publishing a Build Scan means accepting terms of use and was not run here; see Performance Profiling & Build Scans. Another standard use is the cache retention of the Gradle User Home, shown on Dependency Locking, Verification & Caching.

Because an init script changes builds that did not ask for it, three habits keep it safe: keep it small and in version control; make it do nothing when its input is absent (the mirror script above does nothing without mirrorUrl); and avoid depending on a script that only some developers have, because then "works on my machine" is a property of init.d.

Errors

The messages of a few typical mistakes, copied from runs:

Mistake Real message Cause and fix

-I file that does not exist

The specified initialization script '<work>/kts/missing.init.gradle.kts' does not exist.

Gradle resolves the path against the project directory, so a relative path is relative to where gradlew runs.

Typo in a Kotlin init script

e: file:///<work>/scripts/broken.init.gradle.kts:2:5: Unresolved reference 'printn'. followed by Initialization script '<work>/scripts/broken.init.gradle.kts' line: 2 and Script compilation error:

Fix the call; the Kotlin script is compiled before it runs.

Typo in a Groovy init script

Initialization script '<work>/scripts/broken.init.gradle' line: 2, then Could not find method printn() for arguments [oops] on build ':' of type org.gradle.invocation.DefaultGradle.

The receiver is the Gradle object, which explains why project-level names such as repositories are not found at the top level.

Project repository added by a script under FAIL_ON_PROJECT_REPOS

Build was configured to prefer settings repositories over project repositories but repository 'MavenLocal' was added by initialization script '../scripts/extra-repo.init.gradle.kts'

Change the settings repositories from settingsEvaluated, as in the mirror example.

The Tooling API

The Tooling API is a programmatic interface to Gradle: a Java library with which a program starts a build, listens to its events, and asks what the build looks like. The manual names its main audience as authors of IDEs, CI servers and other user interfaces, and says that the API is open to anyone who needs to embed Gradle. IntelliJ IDEA and Eclipse Buildship use it to import a project and run its tasks, and Gradle TestKit uses it for functional tests of plugins (see Testing & Publishing Plugins). It was introduced in Gradle 1.0, released on 12 June 2012, whose release notes list "Embedding Gradle via the Tooling API".

The properties that make it suitable for tools, from the manual:

  • Version independent. One version of the library can work with builds of Gradle versions older and newer than itself. The API is aware of the wrapper and by default uses the version that the build’s wrapper names.

  • It can query the project hierarchy, the project dependencies and external dependencies (including source and Javadoc JARs), the source directories and the tasks of each project.

  • It can run a build and report standard output and error, progress, and events for project configuration, task execution and test execution; run a specific test class or method; cancel a running build; and combine separate builds into a composite.

  • It can download and install the Gradle version needed, like the wrapper.

  • It is lightweight, with few dependencies, and makes no assumptions about the class loaders or the logging of the embedding application.

  • It always uses the Gradle daemon, so repeated requests, model queries or task runs, share one long-lived process (see The Gradle Daemon).

Ant and Maven can be embedded as well, with a different model: Ant through its Project class (see Embedding Ant) and Maven through its embedder; those APIs were not compared with the Tooling API here.

Quickstart: the dependency and the connector

The library is not on Maven Central. It is published in the repository of Gradle, https://repo.gradle.org/gradle/libs-releases, as org.gradle:gradle-tooling-api with the same version number as the Gradle release, and it needs an SLF4J implementation at runtime:

Kotlin DSL: tidepool-tapi/build.gradle.kts
plugins {
    application
}

repositories {
    maven { url = uri("https://repo.gradle.org/gradle/libs-releases") }
    mavenCentral()
}

val toolingApiVersion = "9.8.1"

dependencies {
    implementation("org.gradle:gradle-tooling-api:$toolingApiVersion")
    // The Tooling API needs an SLF4J implementation at runtime
    runtimeOnly("org.slf4j:slf4j-simple:2.0.17")
}

application {
    mainClass = "io.tidepool.tapi.Main"
    applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
}
Groovy DSL: tidepool-tapi/build.gradle
plugins {
    id 'application'
}

repositories {
    maven { url = 'https://repo.gradle.org/gradle/libs-releases' }
    mavenCentral()
}

def toolingApiVersion = '9.8.1'

dependencies {
    implementation "org.gradle:gradle-tooling-api:$toolingApiVersion"
    // The Tooling API needs an SLF4J implementation at runtime
    runtimeOnly 'org.slf4j:slf4j-simple:2.0.17'
}

application {
    mainClass = 'io.tidepool.tapi.Main'
    applicationDefaultJvmArgs = ['--enable-native-access=ALL-UNNAMED']
}

(The build declares org.slf4j:slf4j-api only through the Tooling API’s own dependency; both builds resolved slf4j-api 2.0.17.) The applicationDefaultJvmArgs line is not in the manual. Without it, the client on JDK 26 printed four lines to standard error before its first result, each beginning with the word "WARNING" (they are not shown as a listing here). The first said A restricted method in java.lang.System has been called, the second named java.lang.System::load, called by net.rubygrapefruit.platform.internal.NativeLibraryLoader from the native-platform JAR in the unpacked distribution, and the third said Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module, because the Tooling API loads Gradle’s native library into the client JVM. The warning comes from the JDK, not from Gradle, and it means that a Tooling API client on a recent JDK should start with --enable-native-access=ALL-UNNAMED. The entry point is GradleConnector; forProjectDirectory says which build, and connect() returns a ProjectConnection, which is AutoCloseable and connects to a single Gradle project:

Main.java (the same for both DSLs)
package io.tidepool.tapi;

import java.io.File;
import org.gradle.tooling.GradleConnector;
import org.gradle.tooling.ProjectConnection;

public class Main {
    public static void main(String[] args) throws Exception {
        String command = args[0];
        File projectDir = new File(args[1]);
        File userHome = new File(args[2]);

        GradleConnector connector = GradleConnector.newConnector()
                .forProjectDirectory(projectDir)
                .useGradleUserHomeDir(userHome);
        if (args.length > 3) {
            connector.useInstallation(new File(args[3]));
        }

        try (ProjectConnection connection = connector.connect()) {
            switch (command) {
                case "env" -> Models.environment(connection);
                case "help" -> Models.help(connection);
                case "project" -> Models.projects(connection);
                case "idea" -> Models.idea(connection);
                case "run" -> Builds.run(connection);
                case "cancel" -> Builds.cancel(connector, connection);
                case "fetch" -> Resilient.compare(connection);
                default -> throw new IllegalArgumentException(command);
            }
        }
    }
}

The small program is a command dispatcher so that each example can be run on its own; the class names Models, Builds and Resilient are the three listings below. How the connector picks the Gradle to run is a choice, and the table lists the options of GradleConnector:

Method Effect

forProjectDirectory(File)

The project directory; with the wrapper files present, the connector uses the distribution that gradle-wrapper.properties names (the default behaviour, and what the examples below used, against the local archive named in the scratch copy).

useGradleUserHomeDir(File)

The Gradle User Home for the daemon and the wrapper distribution. The examples used a directory of their own so as not to touch ~/.gradle.

useInstallation(File)

An existing Gradle installation directory. The env command accepts one as an optional fourth argument and printed the same version, 9.8.1, when pointed at the installation that the archive unpacks to.

useDistribution(URI)

A distribution at a URI (not run here).

useBuildDistribution()

The wrapper distribution of the build, the default when the build has a wrapper (not run here as an explicit call).

useGradleVersion(String)

A version number; the connector downloads that distribution from the official distribution server. Not run here: it would download a Gradle distribution.

Compatibility of Java and Gradle versions

Four components are involved: the version of the Tooling API library, the JVM that runs the client (the IDE process), the JVM of the Gradle daemon, and the Gradle version. The manual’s rules:

  • The library is compiled for Java 8 compatibility and is loaded into the client JVM, so a client may run on Java 8 even though Gradle 9 itself needs Java 17 or later to run builds (see Versions, Compatibility & Release History); the upgrade guide to 9.0.0 says that the wrapper and command-line launcher can run with JVM 8 (they still need a newer JVM to start the build) and that the Tooling API and TestKit "remain compatible with JVM 8 and higher".

  • The client JVM and the daemon JVM can differ; classes sent to the build in custom build actions must target the lowest Java version supported by the Gradle in use.

  • A Tooling API version is guaranteed to run builds with all Gradle versions of the last five major releases (the manual’s example: Tooling API 9.0.0 is compatible with Gradle 4.0 and later), and is guaranteed compatible with future releases of the current and the next major (the 8.1 library runs Gradle 9.x builds and may break on Gradle 10).

  • Newer features need newer pairs: the resilient fetch below needs both library and target Gradle at 9.3 or later.

Running the 9.8.1 library against an older Gradle than 9.8.1 was not possible here (no other distribution was downloaded) and is not run here.

Asking for models

A model is a read-only snapshot of some information about a build; connection.getModel(Type.class) asks for it. The ones used most:

Model (package org.gradle.tooling.model) What it describes

build.BuildEnvironment

The Gradle version and Gradle User Home, the JVM and its arguments, and (since 9.4) the exact gradle --version text without starting the daemon (getVersionInfo()).

build.Help

(Since 9.4) The text of gradle --help.

GradleProject

The hierarchy of projects: paths, directories, tasks of each project, children.

gradle.GradleBuild

The build structure only: its projects and included builds. It comes from the settings, so it can be built when a project script fails.

gradle.BuildInvocations, gradle.ProjectPublications

Tasks and selectors that can be run; the publications of a project.

idea.IdeaProject, idea.BasicIdeaProject

The IntelliJ view: modules, content roots (source and test directories), dependencies with their scope, language level.

eclipse.EclipseProject, eclipse.HierarchicalEclipseProject

The Eclipse view: source directories, classpath entries, project dependencies, natures, build commands.

Of these, BuildEnvironment, Help, GradleProject, GradleBuild and IdeaProject were run here; the others were not. Environment and help models first. The env command prints three lines of BuildEnvironment:

Models.java (excerpt, the same for both DSLs)
static void environment(ProjectConnection connection) {
    BuildEnvironment env = connection.getModel(BuildEnvironment.class);
    System.out.println("Gradle version: " + env.getGradle().getGradleVersion());
    System.out.println("Daemon JVM:     " + env.getJava().getJavaHome());
    System.out.println("JVM arguments:  " + env.getJava().getJvmArguments());
}
Gradle version: 9.8.1
Daemon JVM:     /Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home
JVM arguments:  [-Xmx512m, -Dfile.encoding=UTF-8, -Duser.country=US, -Duser.language=en, -Duser.variant]

The daemon’s JVM arguments are those of the project’s org.gradle.jvmargs, which the connector passes on. help printed the first lines of getVersionInfo() and of the Help model (Gradle 9.4 and later; the middle is left out):

------------------------------------------------------------
Gradle 9.8.1
------------------------------------------------------------
...

To see help contextual to the project, use gradle help

To see more detail about a task, run gradle help --task <task>

To see a list of available tasks, run gradle tasks

GradleProject is a tree. The project command walks it and prints the first three tasks of each project:

Models.java (excerpt)
static void projects(ProjectConnection connection) {
    GradleProject root = connection.getModel(GradleProject.class);
    print(root, "");
}

private static void print(GradleProject project, String indent) {
    System.out.println(indent + project.getPath() + " (" + project.getName() + ")");
    project.getTasks().stream().limit(3).forEach(task ->
            System.out.println(indent + "    task " + task.getName() + " - " + task.getDescription()));
    System.out.println(indent + "    ... " + project.getTasks().size() + " tasks in all");
    for (GradleProject child : project.getChildren()) {
        print(child, indent + "  ");
    }
}
: (tidepool)
    task artifactTransforms - Displays the Artifact Transforms that can be executed in root project 'tidepool'.
    task buildEnvironment - Displays all buildscript dependencies declared in root project 'tidepool'.
    task dependencies - Displays all dependencies declared in root project 'tidepool'.
    ... 15 tasks in all
  :tidepool-cli (tidepool-cli)
      task artifactTransforms - Displays the Artifact Transforms that can be executed in project ':tidepool-cli'.
      task assemble - Assembles the outputs of this project.
      task assembleDist - Assembles the main distributions
      ... 33 tasks in all
  :tidepool-engine (tidepool-engine)
      task artifactTransforms - Displays the Artifact Transforms that can be executed in project ':tidepool-engine'.
      task assemble - Assembles the outputs of this project.
      task build - Assembles and tests this project.
      ... 27 tasks in all
  :tidepool-model (tidepool-model)
      task artifactTransforms - Displays the Artifact Transforms that can be executed in project ':tidepool-model'.
      task assemble - Assembles the outputs of this project.
      task build - Assembles and tests this project.
      ... 27 tasks in all

The tool never opened a build file: Gradle configured the build in a daemon and sent back the answer, which is how an IDE can show the task list of a project in a tree. The same, for the IntelliJ model, shows what an IDE learns. The idea command prints modules, their source and test directories, and their dependencies with their scopes (the output for the plain tidepool build):

Models.java (excerpt)
static void idea(ProjectConnection connection) {
    IdeaProject project = connection.getModel(IdeaProject.class);
    System.out.println("IDEA project " + project.getName()
            + ", language level " + project.getJavaLanguageSettings().getLanguageLevel());
    for (IdeaModule module : project.getModules()) {
        System.out.println("module " + module.getName());
        module.getContentRoots().forEach(root -> {
            root.getSourceDirectories().forEach(dir ->
                    System.out.println("    source dir " + root.getRootDirectory().toPath().relativize(dir.getDirectory().toPath())));
            root.getTestDirectories().forEach(dir ->
                    System.out.println("    test dir   " + root.getRootDirectory().toPath().relativize(dir.getDirectory().toPath())));
        });
        for (IdeaDependency dependency : module.getDependencies()) {
            if (dependency instanceof IdeaModuleDependency moduleDependency) {
                System.out.println("    module  " + moduleDependency.getTargetModuleName()
                        + " [" + dependency.getScope().getScope() + "]");
            } else if (dependency instanceof IdeaSingleEntryLibraryDependency library) {
                System.out.println("    library " + library.getFile().getName()
                        + " [" + dependency.getScope().getScope() + "]");
            }
        }
    }
}
IDEA project tidepool, language level 26
module tidepool
module tidepool-cli
    source dir src/main/java
    test dir   src/test/java
    module  tidepool-engine [COMPILE]
    module  tidepool-model [COMPILE]
    library picocli-4.7.7.jar [COMPILE]
    library commons-csv-1.14.1.jar [RUNTIME]
    library commons-io-2.20.0.jar [RUNTIME]
    library commons-codec-1.19.0.jar [RUNTIME]
    library commons-csv-1.14.1.jar [TEST]
    library commons-io-2.20.0.jar [TEST]
    library commons-codec-1.19.0.jar [TEST]
module tidepool-engine
    source dir src/main/java
    test dir   src/test/java
    module  tidepool-model [COMPILE]
    library commons-csv-1.14.1.jar [COMPILE]
    library commons-io-2.20.0.jar [COMPILE]
    library commons-codec-1.19.0.jar [COMPILE]
module tidepool-model
    source dir src/main/java
    test dir   src/test/java

The listing is the Gradle view of dependency scopes translated into IntelliJ’s: api(project(":tidepool-model")) of the engine is a COMPILE module dependency, while implementation dependencies of a library are RUNTIME for the consumers (the cli module sees commons-csv at runtime only). The Java language level is 26 because no release or toolchain was configured. Both Tooling API clients, with the Kotlin and the Groovy build file, printed byte-identical output for these commands.

Running a build, listening, cancelling

connection.newBuild() returns a launcher. forTasks chooses the tasks, withArguments passes command-line options (here --configuration-cache, so the client’s builds also use it), setStandardOutput redirects the output, and progress listeners receive events filtered by OperationType. The run command builds installDist, prints each task as it finishes with the kind of result, then the captured output:

Builds.java (excerpt, the same for both DSLs)
static void run(ProjectConnection connection) {
    ByteArrayOutputStream output = new ByteArrayOutputStream();
    connection.newBuild()
            .forTasks(":tidepool-cli:installDist")
            .withArguments("--console=plain", "--configuration-cache")
            .setStandardOutput(output)
            .addProgressListener(event -> {
                if (event instanceof TaskFinishEvent finish) {
                    System.out.println("finished " + finish.getDescriptor().getTaskPath()
                            + " -> " + finish.getResult().getClass().getSimpleName()
                            .replace("Default", "").replace("Result", ""));
                }
            }, OperationType.TASK)
            .run();
    System.out.println("--- captured standard output ---");
    System.out.print(output);
}
finished :tidepool-engine:processResources -> TaskSkipped
finished :tidepool-model:processResources -> TaskSkipped
finished :tidepool-cli:processResources -> TaskSkipped
finished :tidepool-model:compileJava -> JavaCompileTaskSuccess
finished :tidepool-model:classes -> TaskSuccess
finished :tidepool-model:jar -> TaskSuccess
finished :tidepool-engine:compileJava -> JavaCompileTaskSuccess
finished :tidepool-engine:classes -> TaskSuccess
finished :tidepool-engine:jar -> TaskSuccess
finished :tidepool-cli:compileJava -> JavaCompileTaskSuccess
finished :tidepool-cli:classes -> TaskSuccess
finished :tidepool-cli:jar -> TaskSuccess
finished :tidepool-cli:startScripts -> TaskSuccess
finished :tidepool-cli:installDist -> TaskSuccess
--- captured standard output ---
Calculating task graph as no cached configuration is available for tasks: :tidepool-cli:installDist
> Task :tidepool-engine:processResources NO-SOURCE
...
> Task :tidepool-cli:installDist

BUILD SUCCESSFUL in 6s
8 actionable tasks: 8 executed
Configuration cache entry stored.

The three TaskSkipped events are the NO-SOURCE tasks of the console output. The events come from the same build the command line would run; an IDE draws its progress bar from them. Cancelling uses a token: the cancel command runs a task slow that sleeps for 60 seconds and cancels the build after five seconds.

Builds.java (excerpt)
static void cancel(GradleConnector connector, ProjectConnection connection) throws Exception {
    CancellationTokenSource cancellation = GradleConnector.newCancellationTokenSource();
    var scheduler = java.util.concurrent.Executors.newSingleThreadScheduledExecutor();
    scheduler.schedule(cancellation::cancel, 5, TimeUnit.SECONDS);
    try {
        connection.newBuild()
                .forTasks("slow")
                .withCancellationToken(cancellation.token())
                .run();
        System.out.println("the build finished");
    } catch (BuildCancelledException e) {
        Throwable cause = e;
        while (cause.getCause() != null) {
            cause = cause.getCause();
        }
        System.out.println("cancelled: " + cause.getMessage());
    } finally {
        scheduler.shutdownNow();
    }
}
cancelled: Build cancelled while executing task ':slow'

The whole command took 5.4 seconds, not 60. (If the build is still compiling its Kotlin build scripts when the token fires, the message names that step instead: for example "Build cancelled while executing Kotlin DSL script compilation".) A TestLauncher (connection.newTestsLauncher()) runs selected test classes or methods in the same way; it was not run here. Since Gradle 9.4 a client can also pass org.gradle.tooling.parallel to decide whether models are built in parallel independently of org.gradle.parallel; relying on the derived default is deprecated since 9.8 (see Build Environment & Properties).

Build actions and resilient sync

A BuildAction runs inside the Gradle process and gets a BuildController with which it asks for several models in one round trip, even for different projects or included builds. The action class is serialised to the daemon, so it must be Serializable and written for the lowest Java version supported. The classic calls are getModel(...), which throws if the model cannot be built (for instance because a build script fails), and findModel(...), which returns null only when the model type is not supported; when either throws, the action fails and everything it had collected is discarded.

Gradle 9.3 added BuildController.fetch(...), which returns a FetchModelResult: the model (or null) together with the failures met while building it, and does not throw. This is what an IDE needs to show a partial project when one script is broken. It is incubating, requires the client and target Gradle to be 9.3 or later, and the daemon-side behaviour (a partial model with the failures) needs 9.4 or later. Using fetch is the only opt-in. Since the 9.7 daemon, a sync that partially failed still fails as a whole: run() throws, so the models must be read from the intermediate handlers of a phased action. The test below shows all of that. The target is a copy of tidepool in which tidepool-engine/build.gradle.kts ends with a line that throws:

throw GradleException("tidepool-engine is broken on purpose")
Resilient.java (excerpt, the same for both DSLs)
static class ClassicAction implements BuildAction<String>, Serializable {
    @Override
    public String execute(BuildController controller) {
        GradleProject root = controller.getModel(GradleProject.class);
        return root.getChildren().size() + " child projects";
    }
}

static class FetchBuildStructure implements BuildAction<String>, Serializable {
    @Override
    public String execute(BuildController controller) {
        FetchModelResult<GradleBuild> result = controller.fetch(GradleBuild.class);
        return result.getModel().getProjects().size() + " projects, "
                + result.getFailures().size() + " failures";
    }
}

static class FetchProjectModel implements BuildAction<String>, Serializable {
    @Override
    public String execute(BuildController controller) {
        FetchModelResult<GradleProject> result = controller.fetch(GradleProject.class);
        String model = result.getModel() == null ? "no model" : "a model";
        StringBuilder text = new StringBuilder(model + ", " + result.getFailures().size() + " failure(s)");
        for (Failure failure : result.getFailures()) {
            text.append("\n    ").append(failure.getMessage());
        }
        return text.toString();
    }
}

static void compare(ProjectConnection connection) {
    try {
        System.out.println("single action: " + connection.action(new ClassicAction()).run());
    } catch (GradleConnectionException e) {
        System.out.println("single action failed: " + rootCause(e));
    }
    try {
        connection.action()
                .projectsLoaded(new FetchBuildStructure(), result -> System.out.println("projectsLoaded: " + result))
                .buildFinished(new FetchProjectModel(), result -> System.out.println("buildFinished:  " + result))
                .build()
                .run();
    } catch (GradleConnectionException e) {
        System.out.println("phased action failed as a whole: " + rootCause(e));
    }
}
single action failed: tidepool-engine is broken on purpose
projectsLoaded: 4 projects, 0 failures
buildFinished:  no model, 1 failure(s)
    The build could not be configured; see the reported build failures for the underlying problems.
phased action failed as a whole: tidepool-engine is broken on purpose

Read the output as the three outcomes. The single action with getModel lost everything. The phased action delivered the build structure (four projects, from the settings) to the projectsLoaded handler before any project was configured, and delivered a GradleProject result with no model and one failure to the buildFinished handler, so an IDE can show the four projects and a red mark on the broken one. And run() still threw, as the manual describes for a 9.7 daemon. The rootCause helper in the listing is the usual loop over getCause(). A fourth method, BuildActionExecuter.forTasks(...), makes tasks (for example code generation) run in the same invocation before the action; since Gradle 9.4 the model-producing action runs even if one of those tasks fails. Gradle 9.8.0 adds Failure.getOwnDescription() for walking a failure tree without repeating the causes (it returns null when the target Gradle is older than 9.7.0).

IDE integration

The manual’s page on third-party tools gives the answer for each IDE:

IDE What the manual says

IntelliJ IDEA

Built-in support for importing Gradle projects. The IDEA plugin for Gradle exists "if you need to fine tune the import process".

Android Studio

A variant of IDEA with built-in support; the IDEA plugin can fine-tune the import as well (see Java Projects for the project layout it imports).

Eclipse

Use the Eclipse Buildship plugin to import and run Gradle builds; the Eclipse plugins for Gradle fine-tune the import.

Visual Studio Code

Not listed on the manual’s page; the VS Code Java documentation describes the "Gradle for Java" extension, as used in CI and IDE Integration.

NetBeans

Built-in support in Apache NetBeans.

All of these ask Gradle what the build is through the Tooling API, as in the previous section: IDEA and Buildship import and run through it, with the models above (and some that only IDEs use, such as the Kotlin DSL script model). That is why an IDE sees the same project as the command line only as long as it uses the same Gradle version and JDK: the IDE’s "Gradle JVM" is the JVM of the daemon, set independently of JAVA_HOME (see The Gradle Daemon and Toolchains). The general rule for integrating a tool with Gradle, from the same page of the manual, is that either the Gradle build uses the tool (a plugin) or the tool executes the Gradle build (the Tooling API).

The idea and eclipse plugins

Before IDEs imported Gradle builds, Gradle generated the IDE’s project files and the user opened those. The plugins still exist:

Kotlin DSL: build.gradle.kts
plugins {
    idea
    eclipse
}

subprojects {
    apply(plugin = "idea")
    apply(plugin = "eclipse")
}
Groovy DSL: build.gradle
plugins {
    id 'idea'
    id 'eclipse'
}

subprojects {
    apply plugin: 'idea'
    apply plugin: 'eclipse'
}

With idea, the tasks are idea (all files), ideaModule (the .iml file of each project), and, in the root project only, ideaProject (.ipr) and ideaWorkspace (.iws), plus openIdea (generate and open) and the matching clean tasks. With eclipse: eclipse, eclipseProject (.project), eclipseClasspath (.classpath), eclipseJdt (.settings/org.eclipse.jdt.core.prefs) and the matching clean tasks; eclipse-wtp adds the WTP component and facet files. In Gradle 9.8.1 these tasks are deprecated. Running ./gradlew --warning-mode all idea printed:

> Task :ideaModule
The ideaModule task has been deprecated. This is scheduled to be removed in Gradle 10. Consult the upgrading guide for further information: https://docs.gradle.org/9.8.1/userguide/upgrading_version_9.html#ide_task_deprecation
> Task :ideaProject
The ideaProject task has been deprecated. This is scheduled to be removed in Gradle 10. Consult the upgrading guide for further information: https://docs.gradle.org/9.8.1/userguide/upgrading_version_9.html#ide_task_deprecation
...
> Task :idea
The idea task has been deprecated. This is scheduled to be removed in Gradle 10. Consult the upgrading guide for further information: https://docs.gradle.org/9.8.1/userguide/upgrading_version_9.html#ide_task_deprecation
Generated IDEA project at file:///<work>/ide-kts/tidepool.ipr

and the same message for eclipseProject and the other Eclipse tasks. The deprecation was announced in Gradle 9.6.0 (the section is in the 9.6.0 upgrade guide and not in the 9.5.0 one). Its text: all modern IDEs import Gradle projects directly, so the generated files are no longer needed. The upgrade guide lists what is deprecated: the tasks (and their clean tasks, and openIdea), and the model properties that only affect the generated files: idea.module.iml { }, idea.project.ipr { }, idea.workspace { }, idea.targetVersion, idea.pathVariables, idea.project.projectLibraries, eclipse.wtp.facet { }, the file { } merging hooks of the Eclipse JDT and WTP component, and the withXml { } hooks. Not deprecated: the properties "that affect how IDEs understand your project" (source directories, language levels, dependency scopes), because the IDE’s own import reads them through the Tooling API, and the beforeMerged { } and whenMerged { } hooks of the Eclipse project and classpath models.

That distinction is testable. Setting a hook on the deprecated iml object, as in the manual’s whenMerged example for "export all dependencies of an IDEA module", worked and wrote exported="" into the .iml file; but the build script that wrote it was reported:

> Configure project :tidepool-engine
Using types related to file generation tasks of IDE plugins (org.gradle.plugins.ide.idea.model.IdeaModuleIml). This behavior has been deprecated. This is scheduled to be removed in Gradle 10. Consult the upgrading guide for further information: https://docs.gradle.org/9.8.1/userguide/upgrading_version_9.html#ide_task_deprecation
	at Build_gradle.<init>(build.gradle.kts:19)
	(Run with --stacktrace to get the full stack trace of this deprecation warning.)

(In the Groovy copy the same message was followed by a line pointing at tidepool-engine/build.gradle:18.) The non-deprecated way to tell IDEA about an extra source directory is the idea.module model. The manual’s example makes an integration test source set a test source for the IDE (the scratch module has src/integration/java), and turns off the download of source JARs, a property of the model:

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

dependencies {
    api(project(":tidepool-model"))
    implementation("org.apache.commons:commons-csv:1.14.1")
}

val intTest by sourceSets.creating {
    java.setSrcDirs(listOf("src/integration/java"))
}

idea {
    module {
        isDownloadSources = false
        testSources.from(intTest.java.srcDirs)
    }
}
Groovy DSL: tidepool-engine/build.gradle
plugins {
    id 'java-library'
}

dependencies {
    api project(':tidepool-model')
    implementation 'org.apache.commons:commons-csv:1.14.1'
}

sourceSets {
    intTest {
        java.srcDirs = ['src/integration/java']
    }
}

idea {
    module {
        downloadSources = false
        testSources.from(sourceSets.intTest.java.srcDirs)
    }
}

With these blocks, ./gradlew --warning-mode all idea printed no IdeaModuleIml warning (only the task deprecations above), and the generated module file (.iml) contained the new test directory, in both DSLs; without isDownloadSources = false the library entries had <SOURCES> pointing at downloaded -sources.jar files, with it they were empty (<SOURCES/>):

      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false"/>
      <sourceFolder url="file://$MODULE_DIR$/src/integration/java" isTestSource="true"/>

The point of the exercise is the Tooling API view of the same build, which needs no generated file. The idea command of the client above, run against this build, showed the engine module with the new directory as a test directory:

module tidepool-engine
    source dir src/main/java
    test dir   src/test/java
    test dir   src/integration/java
    module  tidepool-model [COMPILE]
    library commons-csv-1.14.1.jar [COMPILE]
...

So the idea { module { } } DSL does what its documentation says: it changes what the IDE learns through the Tooling API, and .iml, .classpath and .project files are not needed. What eclipse writes (.project with the Java nature and builder, .classpath with the source directory, a project entry /tidepool-model for the project dependency, the JARs with their source attachments, and a JRE_CONTAINER entry for JavaSE-26) is what Buildship now derives from the same model. Use the plugins only when something genuinely reads their files (an old IDE, a script) and expect to remove them before Gradle 10.

The idea and eclipse plugins and their tasks were run in Gradle 9.8.1 only. In Gradle 8.14 the tasks are not deprecated (not run here: the deprecation was announced in 9.6.0).

Gradle 8.14 and Gradle 9

Not run here: the table is taken from the release notes and upgrade guides listed in the sources.

Topic Gradle 8.14 Gradle 9.8.1

Init script locations and order

The same four places

The same four places (run here)

Kotlin init scripts

Available (since 4.6)

Available (run here)

JVM that runs Gradle

Java 8 to 24

Java 17 to 27; the wrapper, the launcher and the Tooling API client can still run on Java 8

Resilient fetch and FetchModelResult

Not available

9.3 (partial models 9.4, whole-operation failure 9.7, getOwnDescription 9.8)

BuildEnvironment.getVersionInfo(), Help model, org.gradle.tooling.parallel

Not available; model-building parallelism follows org.gradle.parallel

9.4; relying on the derived default is deprecated (9.8)

idea and eclipse file generation tasks

Not deprecated

Deprecated since 9.6, removal announced for Gradle 10

The same idea in Ant and Maven

Concern Ant 1.10.18 Maven 3.10.0 Gradle 9.8.1

User- or machine-wide behaviour that does not live in the project

~/.ant/ant.conf, -propertyfile, ANT_ARGS, ~/.ant/lib for task JARs (see Running Ant)

settings.xml for mirrors, servers and profiles (see Settings & Password Encryption) and core extensions in .mvn/extensions.xml (see Maven Extensions)

Init scripts and init plugins

Code that runs before the build and can change it

-listener and -logger classes

Core extensions and lifecycle participants

Init plugins (Plugin<Gradle>) and gradle.beforeSettings

Embedding the tool in another program

The Project API (see Embedding Ant)

The Maven embedder (MavenCli)

The Tooling API, with models and version independence

IDE project files

None generated by Ant itself

Imported from the POM by the IDE

Imported through the Tooling API; the idea and eclipse plugins are deprecated

Where the book is outdated

Gradle in Action (Muschko, Manning 2014, Gradle 1.7) covers init scripts in its chapter 4 and the Tooling API and IDE integration in its chapter 10. The init script mechanism is described on this page with the same command-line option and the same directories; the examples of the book were not re-run here. What has moved around it:

  • Kotlin init scripts (*.init.gradle.kts) arrived in Gradle 4.6; the book knows Groovy only.

  • The Tooling API of the book’s era was young (it was introduced in Gradle 1.0). Today it is version independent for five major releases, the models are richer, and BuildAction, phased actions, cancellation and the resilient fetch exist; IDEs build their import on it.

  • The book presents the eclipse and idea plugins, and the plugins for other IDEs, as the main way to feed an IDE. IDEs now import Gradle builds directly through the Tooling API, and the file generation tasks of the two plugins are deprecated since 9.6 and scheduled for removal in Gradle 10; the model configuration of the plugins remains useful.

  • The book’s Gradle needed JDK 1.5 or later to run. Gradle 9 needs Java 17 or later for the daemon, while the Tooling API client may still run on Java 8.

The book’s page numbers were not checked; the statements follow the research notes behind this section.

Classic mistakes

  • Expecting an init script’s println on every run. With the configuration cache, an unchanged script does not run when the entry is reused.

  • Putting a repositories { } block in allprojects { } under FAIL_ON_PROJECT_REPOS. Change the settings repositories from settingsEvaluated { }.

  • Using the project’s buildSrc classes in an init script. Not available; publish an init plugin or use an included build.

  • Embedding the Tooling API without an SLF4J implementation. The library needs one at runtime, as in the manual’s quickstart.

  • Starting a Tooling API client on a recent JDK without --enable-native-access=ALL-UNNAMED. The warning above, from the JDK.

  • Assuming the IDE uses the same JDK and Gradle as the shell. Both are IDE settings.

  • Committing the generated .iml, .classpath and .project files. They duplicate the build model and the tasks that write them are deprecated.

Summary

  • Init scripts run before settings and projects, with the Gradle object as receiver. They come from -I, the Gradle User Home (init.gradle, init.d) and the installation’s init.d, in that order, and are alphabetical within a directory.

  • Use them for machine- or fleet-level policy: mirrors, listeners, machine-specific paths. Read user data through providers, keep them small and idempotent, and remember that the configuration cache does not run an unchanged script on reuse.

  • Under FAIL_ON_PROJECT_REPOS an init script changes the settings repositories (settingsEvaluated), not project repositories.

  • initscript { } gives a script its own classpath; an init plugin (Plugin<Gradle>) packaged as a JAR and applied from an init script is the way to share logic that does not live in a repository.

  • The Tooling API (org.gradle:gradle-tooling-api, from repo.gradle.org) embeds Gradle: GradleConnector, ProjectConnection, models, builds with events and cancellation, build actions, and since 9.3 the resilient fetch. It always uses the daemon and is version independent.

  • IDEs use it to import builds; the idea and eclipse file generation tasks are deprecated since 9.6 for removal in Gradle 10, while idea { module { } } style model configuration is not.