Settings & Build Scripts
|
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 (the current release on 2026-10-16) with the embedded Kotlin 2.4.10 and Groovy 4.0.33. Every
example was run on JDK 26 through the Gradle wrapper on 2026-10-16, on macOS, with the configuration cache enabled
(org.gradle.configuration-cache=true in gradle.properties) unless the text says a run turned it off, and says whether the example is
configuration cache compatible. The Kotlin DSL is shown first and the Groovy DSL second wherever the syntax differs, and both versions were
run. Paths of the scratch workspace are shortened to <work>. Gradle 8.14 was not available here: what differs in it is described from the
official documentation in the last sections and is labelled "not run here".
A Gradle build is a handful of scripts. The settings script says which projects exist and where plugins and libraries come from; one build
script per project says what that project is and what it does. They are ordinary programs in the Kotlin or Groovy language run against two
objects, Settings and Project, and almost every surprise in a Gradle build comes from forgetting which object a line is talking to, or
when it runs. This page explains both scripts line by line, using the tidepool toolkit of the section (a model library, an engine library
and a command line application) as the running example. The lifecycle that decides when each script runs is on
Build Lifecycle; the vocabulary of builds, projects and tasks is on
Core Concepts; the two languages themselves are on
Kotlin DSL and
Groovy DSL & Migrating to Kotlin.
The scripts of a build
Gradle accepts exactly two languages for its scripts: the Groovy DSL in files ending .gradle and the Kotlin DSL in files ending
.gradle.kts. The file name decides the language and the role (settings, build, init), and a build may mix languages, because each script is compiled on its own: a copy of the example with the Kotlin settings, root and CLI scripts and Groovy
scripts for the two libraries built successfully.
| Script | Files | Object it configures | Runs |
|---|---|---|---|
settings script |
|
one |
once per build, first; its statements run during the initialization phase |
build script |
|
that project’s |
during the configuration phase, top to bottom, parent project before children |
init script |
|
the |
before the settings script (see Init Scripts & the Tooling API) |
script plugin |
any other script file pulled in with |
whatever object applied it (a |
when the |
precompiled script plugin |
|
the object named by the file suffix ( |
as a normal plugin, when it is applied (see Convention Plugins & Sharing Build Logic) |
Both kinds of script contain two kinds of element. Statements are top-level expressions that run immediately when the script is evaluated.
Blocks are closures (Groovy) or lambdas with a receiver (Kotlin) passed to a configuration method, such as repositories { ... }, and apply
to the object that method returns. The distinction matters because a statement at the top of a build script runs at configuration time on
every build that is not served from the configuration cache, which is where most "why is my build slow" and "why did it print twice" stories
begin.
The tidepool example
The section’s running example is one build with three projects. Throughout this page the build is the one below: versions come from a catalog,
repositories are declared once in the settings, every project compiles for Java 21 with the compiler’s release option, and the wrapper pins
Gradle 9.8.1.
tidepool
├── gradle
│ ├── libs.versions.toml
│ └── wrapper
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts (settings.gradle for the Groovy DSL)
├── build.gradle.kts (build.gradle)
├── data
│ └── stations.csv
├── tidepool-model
│ ├── build.gradle.kts
│ └── src/main/java/io/tidepool/model/...
├── tidepool-engine
│ ├── build.gradle.kts
│ └── src/main/java/io/tidepool/engine/...
└── tidepool-cli
├── build.gradle.kts
└── src/main/java/io/tidepool/cli/...
The wrapper properties, as a project created with gradle wrapper --gradle-version 9.8.1 carries them (the checksum is the one published for the
-bin distribution; the runs on this page used a copy of the same distribution on the local disk, with the same checksum, to avoid a second
download; see Gradle Wrapper):
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 settings file:
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
rootProject.name = "tidepool"
include("tidepool-model", "tidepool-engine", "tidepool-cli")
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
rootProject.name = 'tidepool'
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
The root build script, the catalog and the properties file:
plugins {
base
}
allprojects {
group = "io.tidepool"
version = "1.0.0"
}
plugins {
id 'base'
}
allprojects {
group = 'io.tidepool'
version = '1.0.0'
}
[versions]
commons-csv = "1.14.1"
picocli = "4.7.7"
[libraries]
commons-csv = { module = "org.apache.commons:commons-csv", version.ref = "commons-csv" }
picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
org.gradle.configuration-cache=true
The three project build scripts:
plugins {
`java-library`
}
tasks.withType<JavaCompile>().configureEach {
options.release = 21
}
plugins {
id 'java-library'
}
tasks.withType(JavaCompile).configureEach {
options.release = 21
}
plugins {
`java-library`
}
dependencies {
api(project(":tidepool-model"))
implementation(libs.commons.csv)
}
tasks.withType<JavaCompile>().configureEach {
options.release = 21
}
plugins {
id 'java-library'
}
dependencies {
api project(':tidepool-model')
implementation libs.commons.csv
}
tasks.withType(JavaCompile).configureEach {
options.release = 21
}
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation(libs.picocli)
}
application {
mainClass = "io.tidepool.cli.Main"
}
tasks.withType<JavaCompile>().configureEach {
options.release = 21
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation libs.picocli
}
application {
mainClass = 'io.tidepool.cli.Main'
}
tasks.withType(JavaCompile).configureEach {
options.release = 21
}
Running it, with the Kotlin DSL files and then with the Groovy ones:
./gradlew -q projects
Projects:
------------------------------------------------------------
Root project 'tidepool'
------------------------------------------------------------
Location: <work>
Project hierarchy:
Root project 'tidepool'
+--- Project ':tidepool-cli'
+--- Project ':tidepool-engine'
\--- Project ':tidepool-model'
...
The Groovy DSL build printed the same hierarchy. A full build, here the Kotlin DSL one (the first four lines and the last seven lines are shown, and ... marks the omitted ones):
./gradlew build
Calculating task graph as no cached configuration is available for tasks: build
> Task :assemble UP-TO-DATE
> Task :check UP-TO-DATE
> Task :build UP-TO-DATE
...
> Task :tidepool-cli:distZip
> Task :tidepool-cli:assemble
> Task :tidepool-cli:build
BUILD SUCCESSFUL in 728ms
9 actionable tasks: 9 executed
Configuration cache entry stored.
The Groovy DSL project ended with the same BUILD SUCCESSFUL in ... 9 actionable tasks: 9 executed and Configuration cache entry stored.
lines, and a second run of either build reused the cache and found every task up to date (the first line and the last three are shown):
Reusing configuration cache.
...
BUILD SUCCESSFUL in 445ms
9 actionable tasks: 9 up-to-date
Configuration cache entry reused.
Both builds are configuration cache compatible: nothing in them reads a project from a task action or starts a process while configuring. The rest of the page takes the files apart.
The settings script
The settings file is the entry point of every Gradle build. In a build with a single project it is optional (without one, Gradle treats the directory as a single-project build); in a multi-project build it is mandatory because it is where the projects are declared. It is evaluated before any build script, which makes it the right place for everything that is about the whole build: where plugins and libraries come from, which projects exist, and which other builds are included.
Where Gradle finds it
When the command is run from the root, the settings file is in the current directory. When it is run from a subdirectory, Gradle looks for
the settings file in the parent directories, so ../gradlew run inside tidepool-engine configures the whole build and then works on the
project of that directory. Running ./gradlew -q :tidepool-engine:properties there printed these lines among the project’s properties:
name: tidepool-engine
path: :tidepool-engine
projectDir: <work>/tidepool-engine
rootDir: <work>
When no settings file is found at all, Gradle does not fail with a stack trace; it says what is missing. In an empty directory that has only the
wrapper, ./gradlew -q help printed:
Welcome to Gradle 9.8.1.
Directory '<work>' does not contain a Gradle build.
To create a new build in this directory, run gradlew init
If both settings.gradle and settings.gradle.kts exist in the same directory, the Groovy one was the one used in the run below, which put a
settings.gradle next to the Kotlin file of the example, with a different root project name and a single include:
Projects:
------------------------------------------------------------
Root project 'from-the-groovy-file'
------------------------------------------------------------
Location: <work>
Project hierarchy:
Root project 'from-the-groovy-file'
\--- Project ':tidepool-model'
Keep one settings file per build; two files in two languages is a mistake that does not announce itself.
Block order: pluginManagement, buildscript and plugins first
A settings script has a strict order for the blocks that affect how the rest of it is compiled: pluginManagement { } must be the first block,
and plugins { } follows it (after a buildscript { } block, if the file has one); ordinary statements and the other blocks come later. Moving
the plugins block above pluginManagement in the Kotlin DSL file above is rejected before anything runs:
FAILURE: Build failed with an exception.
* What went wrong:
file://<work>/settings.gradle.kts:1:1: Unexpected `plugins` block found. `plugins` cannot appear before `pluginManagement`.
> Unexpected `plugins` block found. `plugins` cannot appear before `pluginManagement`.
Build scripts have a related rule for their plugins { } block, with a difference between the two languages that a later section shows.
pluginManagement
pluginManagement { } configures where plugins are found and which versions they get, for the plugins applied in the build scripts. It must be the
first block of the settings file. Its most used members are:
-
repositories { }: where plugin artifacts are looked up, in order. The default, when the block is absent, is the Gradle Plugin Portal only. The example lists the Portal and Maven Central, because some plugins are published only to Central. -
plugins { id("some.plugin") version "1.2.3" }: a version for a plugin id, so the build scripts can writeid("some.plugin")without one. Keeping versions here, or in a version catalog, keeps them out of dozens of scripts. -
resolutionStrategy { eachPlugin { ... } }: map a plugin id to an artifact when the plugin is not published with plugin markers. -
includeBuild("build-logic"): make the plugins of an included build (usually the convention plugins) available to the projects. This is how a build shares build logic withoutbuildSrc, and it is the subject of Convention Plugins & Sharing Build Logic (not run here).
Settings plugins: the plugins block of the settings file
A plugin can target the Settings object rather than a project. The plugins { } block of the settings script applies those. The example
applies the Foojay Toolchains Resolver Convention plugin (id org.gradle.toolchains.foojay-resolver-convention, version 1.0.0 on the Plugin
Portal on 2026-10-16), which resolves Java runtimes through the Foojay Disco API so that a build that asks for a Java version that is not installed can
fetch it; it is the plugin that gradle init adds to new builds. It is inert here, because the tidepool build compiles with whatever JDK runs
Gradle (JDK 26) and asks for Java 21 only through options.release. The toolchain feature is the subject of the later Toolchains page. Other
settings plugins you will meet: the Develocity plugin (com.gradle.develocity), and plugins that configure the build cache or included builds.
Settings plugins are applied before any project exists, so they cannot add tasks or extensions to a project; they can add settings-level
extensions and register things that projects later use. The settings.gradle.kts of a real build usually has no more than a handful of
lines in plugins { }.
dependencyResolutionManagement and RepositoriesMode
dependencyResolutionManagement { } centralizes the declaration of repositories and version catalogs for all projects, instead of
repeating repositories { mavenCentral() } in every build script. Its repositoriesMode decides what happens to the project scripts that
still declare repositories, with three values:
| Mode | Meaning | Default |
|---|---|---|
|
repositories declared in a project override those of the settings |
yes |
|
repositories of the settings override those declared in a project |
no |
|
declaring a repository in a project is an error |
no |
The example uses FAIL_ON_PROJECT_REPOS, which the Gradle best practices recommend (declare repositories in the settings), and a single list of repositories is the first
supply-chain control (see Supply Chain Security). Adding a
repositories { mavenCentral() } block to tidepool-engine/build.gradle.kts in the example above, with the settings unchanged, fails already
at configuration time, even for help:
FAILURE: Build failed with an exception.
* Where:
Build file '<work>/tidepool-engine/build.gradle.kts' line: 6
* What went wrong:
Build was configured to prefer settings repositories over project repositories but repository 'MavenRepo' was added by build file 'tidepool-engine/build.gradle.kts'
The build script’s repositories { } block is not wrong, it is just not allowed in this mode. The Groovy DSL fails the same way, with the same sentence under A problem occurred evaluating project ':tidepool-engine'. The same
block also holds the entry point to the version catalogs: a gradle/libs.versions.toml file is picked up automatically and becomes the
libs accessor used in the project scripts (libs.commons.csv above); further catalogs are declared with versionCatalogs { create("name") { from(files(...)) } }.
Both topics have their own pages: Repositories & Content Filtering
and Version Catalogs. Since Gradle 9.8 the same properties file can
make Gradle apply the mirror settings of Maven’s settings.xml to every Maven repository, with org.gradle.mirror.maven.settings=true;
that is one of the headline items of the 9.8.0 release notes (not run here).
rootProject.name
rootProject.name is the name of the build’s root project. It is what projects prints, what an IDE shows, and, with the root project’s
group and version, the identity of anything the build publishes. There is exactly one root project per build. If the settings file does not
set it, Gradle uses the name of the directory that holds the settings file, which makes the name depend on how somebody happened to clone the
repository. In a copy of the example with the rootProject.name line removed, in a directory called checkout, ./gradlew -q projects
printed:
Project hierarchy:
Root project 'checkout'
+--- Project ':tidepool-cli'
+--- Project ':tidepool-engine'
+--- Project ':tidepool-model'
Set the name explicitly in every build that is published or built by CI. The name is checked: it must not contain any of / \ : < > " ? * |.
With rootProject.name = "tide:pool":
FAILURE: Build failed with an exception.
* Where:
Settings file '<work>/settings.gradle.kts' line: 19
* What went wrong:
The project name 'tide:pool' must not contain any of the following characters: [/, \, :, <, >, ", ?, *, |]. Set the 'rootProject.name' or adjust the 'include' statement (see https://docs.gradle.org/9.8.1/dsl/org.gradle.api.initialization.Settings.html#org.gradle.api.initialization.Settings:include(java.lang.String[]) for more details).
The same limit applies to the names given to include. Reading the name inside a settings script is a small trap: the user manual’s page
on writing build scripts says that name alone refers to the root project name in a settings script, but in the 9.8.1 runs here
println(name) in the Kotlin file and println name in the Groovy file both failed, so write rootProject.name:
e: file://<work>/settings.gradle.kts:22:9: Unresolved reference 'name'.
* What went wrong:
A problem occurred evaluating settings 'tidepool'.
> Could not get unknown property 'name' for settings 'tidepool' of type org.gradle.initialization.DefaultSettings.
include: declaring the projects
include adds subprojects to the build. It takes one or more project paths and can be called any number of times. A path is relative to the
root and uses : to nest: include("libs:model") declares a project :libs:model in the directory libs/model and, because Gradle creates
the intermediate project, also a project :libs. Starting from the example, with the two libraries moved into a libs directory and the
settings changed to include("libs:model", "libs:engine", "tidepool-cli"), projects printed (and the build succeeded):
Project hierarchy:
Root project 'tidepool'
+--- Project ':libs'
| +--- Project ':libs:engine'
| \--- Project ':libs:model'
\--- Project ':tidepool-cli'
Every included project must have an existing directory. An include("tidepool-docs") with no such directory is an error in Gradle 9:
FAILURE: Build failed with an exception.
* What went wrong:
Configuring project with invalid directory
Configuring project ':tidepool-docs' without an existing directory is not allowed. The configured projectDirectory '<work>/tidepool-docs' does not exist, can't be written to or is not a directory.
The project name defaults to the directory name, but the two can be decoupled when the directory layout is not yours to change, by setting
projectDir on the project descriptor in the settings. This variant of the example names the projects model, engine and cli, keeps the
directories as they are, and refers to the projects by their short path in the dependencies:
include("model", "engine", "cli")
project(":model").projectDir = file("tidepool-model")
project(":engine").projectDir = file("tidepool-engine")
project(":cli").projectDir = file("tidepool-cli")
include 'model', 'engine', 'cli'
project(':model').projectDir = file('tidepool-model')
project(':engine').projectDir = file('tidepool-engine')
project(':cli').projectDir = file('tidepool-cli')
Project hierarchy:
Root project 'tidepool'
+--- Project ':cli'
+--- Project ':engine'
\--- Project ':model'
Project locations:
project ':cli' - /tidepool-cli
(file(...) in a settings script is resolved against the settings directory.) The build succeeded in both DSLs with api(project(":model"))
and implementation(project(":engine")) in the dependent scripts. Use this mapping sparingly: the usual convention is that the directory
and the project name are the same, and the user manual’s naming advice points the same way. Other ways to bring code into a build are
includeBuild for another complete build, covered by Composite Builds, and
includeFlat for sibling directories, a legacy form that is not used in new builds. The whole subject of structuring many projects is on
Multi Project Builds.
Other things a settings script configures
The Settings object has a wider reach than the blocks above, summarized here with the page that covers each:
| Member | Purpose |
|---|---|
|
local and remote build cache (Build Cache) |
|
where JDKs are downloaded from (Toolchains) |
|
other builds that take part in this one |
|
opt in to a documented feature preview (see Versions, Compatibility & Release History) |
|
the |
|
build-wide locations ( |
|
the project tree |
The build script
A build script describes one project. The tidepool scripts show the four things a typical one does: apply plugins, declare dependencies, configure the extensions the plugins added, and configure or register tasks. The user manual’s walk-through of a sample script numbers them the same way.
plugins { }
The plugins { } block is the preferred way to apply a plugin. A plugin is named by id:
-
a core plugin that ships with Gradle has a short name; in the Kotlin DSL the accessors
base,applicationand(back quoted because of the hyphen) stand for the idsjava-librarybase,applicationandjava-library, andid("java-library")is the same; -
a community plugin needs an id and, unless the settings supply it, a version:
id("com.example.plugin") version "1.2.3"; -
version "1.2.3" apply falsemakes a plugin available to the subprojects without applying it to the project that declares it (used in root scripts of multi-project builds); -
a plugin from a version catalog is
alias(libs.plugins.name).
Applying the application plugin implicitly applies java, so tidepool-cli gets the Java tasks without naming them. What the core plugins
add is on Plugins Overview and
Java Projects.
The block is processed before the rest of the script, and the languages differ in how strict they are about it. In the Groovy DSL any other statement
before plugins { } is a compile error. Prepending println 'first' to tidepool-model/build.gradle:
FAILURE: Build failed with an exception.
* Where:
Build file '<work>/tidepool-model/build.gradle' line: 2
...
* What went wrong:
Groovy DSL script compilation problem
Could not compile build file '<work>/tidepool-model/build.gradle'.
Location: <work>/tidepool-model/build.gradle:2
The Kotlin DSL extracts the plugins { } block first, and a script with a statement above it (a println("first"), or a task registration) compiled and
ran in the runs made for this page. Do not rely on that: keep plugins { } at the top in both languages. Only buildscript { },
pluginManagement { } and other plugins { } blocks may precede it.
Applying plugins with the plugins { } block has a second effect in the Kotlin DSL: Gradle generates type-safe accessors for the extensions,
configurations and tasks that the applied plugins contribute, so java { }, application { }, dependencies { implementation(...) } and
tasks.named("jar") are checked at compile time and completed by the IDE. The accessors exist only for plugins applied through the plugins { }
block of the same script (or by a precompiled script plugin). The older form apply(plugin = "java-library") applies the plugin but generates no
accessors, which breaks the very next block. With this tidepool-model/build.gradle.kts:
apply(plugin = "java-library")
java {
withSourcesJar()
}
e: file://<work>/tidepool-model/build.gradle.kts:3:1: Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch:
val PluginDependenciesSpec.java: PluginDependencySpec
e: file://<work>/tidepool-model/build.gradle.kts:4:5: Unresolved reference 'withSourcesJar'.
FAILURE: Build failed with an exception.
The remedies are to use plugins { } or, when the legacy form cannot be avoided, the generic configure<JavaPluginExtension> { withSourcesJar() },
which compiled and added the sourcesJar task in the run. The Groovy DSL needs none of this because its extensions are found dynamically:
apply plugin: 'java-library' followed by java { withSourcesJar() } worked. Gradle can list what accessors the current script has with the
kotlinDslAccessorsReport task.
dependencies { }
The dependencies { } block adds to the configurations the plugins created. The names are the vocabulary of the Java Library plugin:
api (visible to consumers at compile time), implementation (an implementation detail), compileOnly, runtimeOnly, testImplementation and
testRuntimeOnly. A dependency is either a module ("group:artifact:version", or an accessor from a catalog such as libs.commons.csv), another
project of the build (project(":tidepool-model")), or a file collection. The tidepool-engine script declares one of each kind: an api
dependency on the model, so that the CLI that depends on the engine also sees the model’s types, and an implementation dependency on Commons CSV,
which stays an internal detail.
The names compile, runtime, testCompile and testRuntime of old builds were removed in Gradle 7.0. A script that still declares
compile 'org.apache.commons:commons-csv:1.14.1' fails at configuration time:
FAILURE: Build failed with an exception.
* Where:
Build file '<work>/tidepool-model/build.gradle' line: 6
* What went wrong:
A problem occurred evaluating project ':tidepool-model'.
> Could not find method compile() for arguments [org.apache.commons:commons-csv:1.14.1] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.
(The Kotlin DSL version of the mistake does not compile at all, since there is no compile accessor.) The whole subject of configurations,
version declarations, constraints and resolution is on
Declaring Dependencies & Configurations.
repositories { }
A project script can declare repositories { mavenCentral() }. In the tidepool build it must not, because the settings run in
FAIL_ON_PROJECT_REPOS mode (above). Teams that need per-project repositories leave the default PREFER_PROJECT mode and accept that the list
is no longer in one place.
Extensions and convention properties
A plugin adds extensions to the project, objects with their own configuration blocks. application { mainClass = "io.tidepool.cli.Main" } configures
the application extension that the Application plugin registered; java { }, sourceSets { } and publishing { } are other examples. In the
Kotlin DSL the assignment mainClass = "..." is the lazy property assignment of Gradle 8.2 and later (the Groovy DSL accepts = for such properties too); older scripts write mainClass.set("..."), which still works in both.
Tasks
Tasks are registered and configured in the build script. The examples configure every compile task, using the lazy API:
tasks.withType<JavaCompile>().configureEach { ... } (Groovy: tasks.withType(JavaCompile).configureEach { ... }). The block runs when and
only if a JavaCompile task is needed, and applies to all of them, including those created later by plugins. The practices that matter for scripts
are register instead of create, named to find a task and configureEach instead of all, because the eager forms run the configuration of tasks
that the build will never execute. The full treatment is on Tasks.
The command that creates a task in the book-era style does not exist any more. In the Groovy DSL, task hello << { println 'hello' } was removed in
Gradle 5.0 and fails with:
FAILURE: Build failed with an exception.
* Where:
Build file '<work>/tidepool-model/build.gradle' line: 9
* What went wrong:
A problem occurred evaluating project ':tidepool-model'.
> Could not find method leftShift() for arguments [build_4i19zdaxrzgjg0221aun04tk3$_run_closure2@7d86ee84] on task ':tidepool-model:hello' of type org.gradle.api.DefaultTask.
The replacement is tasks.register("hello") { doLast { println("hello") } }.
allprojects and subprojects: cross-project configuration
The root script of the example uses allprojects { group = "io.tidepool"; version = "1.0.0" }, the "configure everything from the root" style of
the first Gradle books. It works and it is compact, but it makes every subproject depend on a script that none of them mention, it forces
configuration of all projects even when one is built, and it is not compatible with the Isolated Projects feature (see
Configuration Cache & Isolated Projects). The recommended
replacement is a convention plugin, a plugin of your own in an included build that each project applies with one line of its plugins { } block, so that
a project script shows which conventions it follows. That is on
Convention Plugins & Sharing Build Logic; this page
only keeps allprojects where it keeps the example short.
Script plugins with apply from
A script can pull in another script file and run it against the same object. This is the "script plugin", the oldest way to share logic:
the shared file is applied with apply(from = ...) (Groovy: apply from: ...) wherever it is needed. Moving the repeated JavaCompile block of
the three project scripts into gradle/release21.gradle.kts (the Kotlin DSL) gives:
println("release21.gradle.kts applied to ${project.path}")
tasks.withType<JavaCompile>().configureEach {
options.release = 21
}
println "release21.gradle applied to ${project.path}"
tasks.withType(JavaCompile).configureEach {
options.release = 21
}
and, at the end of each of the three project scripts:
apply(from = rootProject.file("gradle/release21.gradle.kts"))
apply from: rootProject.file('gradle/release21.gradle')
Calculating task graph as no cached configuration is available for tasks: build
> Configure project :tidepool-cli
release21.gradle.kts applied to :tidepool-cli
> Configure project :tidepool-engine
release21.gradle.kts applied to :tidepool-engine
> Configure project :tidepool-model
release21.gradle.kts applied to :tidepool-model
> Task :assemble UP-TO-DATE
The file ran once per project, against that project (project.path resolved), and it stays configuration cache compatible. The Groovy DSL printed
the equivalent lines. Script plugins are untyped, cannot declare their own plugin dependencies, are compiled again for every project that applies
them, and get no accessors in the Kotlin DSL; they are replaced by precompiled script plugins and binary plugins, which are the subject of
Binary Plugins.
What a script talks to: the implicit receiver
Everything in a script is a call on some object. In a build script it is the Project; in a settings script it is the Settings; in an init script
it is the Gradle object. In the Groovy DSL the script has that object as its delegate: a name that the script does not define is looked up on
the delegate, so name, version, dependencies and tasks are properties and methods of Project. In the Kotlin DSL the script is
compiled as a class that extends a base class delegating to the Project, so name and dependencies { } are members of the script class itself; a
script of the example was compiled to a class called Build_gradle in the run made for this page. Nested blocks have their own receiver:
inside dependencies { } it is the DependencyHandler, inside repositories { } the RepositoryHandler, inside tasks.register("x") { } the new task.
project.dependencies.implementation(...) is the explicit spelling of the same call, and it is what to write when it is not obvious who
the receiver is.
Seeing the receivers is easy. This probe was added to the end of the Kotlin DSL settings script and to each Kotlin DSL build script:
println("settings script: settings = $settings")
println("settings script: rootProject.name = ${rootProject.name}")
println("settings script: rootDir = ${rootDir.name}")
println("build script: project = $project, path = $path")
println "settings script: settings = ${settings}"
println "settings script: rootProject.name = ${rootProject.name}"
println "settings script: rootDir = ${rootDir.name}"
println "build script: project = ${project}, path = ${path}"
./gradlew -q :tidepool-model:clean
settings script: settings = settings 'tidepool'
settings script: rootProject.name = tidepool
settings script: rootDir = demo-kts
build script: project = root project 'tidepool', path = :
build script: project = project ':tidepool-cli', path = :tidepool-cli
build script: project = project ':tidepool-engine', path = :tidepool-engine
build script: project = project ':tidepool-model', path = :tidepool-model
The Groovy DSL printed exactly the same seven lines. They show the order of events: the settings script ran once, then the root project’s build script,
then the subprojects in alphabetical order of their names (not the order of include, which was model, engine, cli). The statements are in the
body of each script, so they ran during configuration. Running the same command again, with the configuration cache enabled, printed nothing:
The scripts were not evaluated at all, because Gradle restored the task graph from the cache; with --no-configuration-cache the seven lines come back. A
println in a script body is a configuration-time side effect, and the configuration cache is the reason not to rely on it for anything but a quick
look. The same holds for code that is meant to run on every build, such as stamping a timestamp: put it in a task, or in a value source.
Source: Gradle User Manual: Delegates and Receivers, Accessing Project Properties in Build Scripts and Default Script Imports
Gradle adds a list of default imports to every script (for instance org.gradle.api.tasks.*), so StopExecutionException can be written without
its package. The properties and methods that the Groovy DSL resolves on the delegate are the reference pages of
the Gradle DSL (Groovy flavour) and
the Kotlin DSL API.
Local variables, extra properties and why to avoid the latter
A script has two kinds of variables. A local variable (val in Kotlin, def in Groovy) is visible in the scope where it is declared and is a
feature of the language. An extra property is a user-defined value attached to an object that supports them, usually the Project: extra["x"] = ...
in Kotlin and ext.x = ... in Groovy. They are the book-era answer to "where do I keep the Spring version", and they have three problems that the
current releases have turned into warnings.
Extra properties are untyped, global-looking and discouraged
The user manual describes extra properties as having a wider scope than local variables, visible wherever the owning object is visible, subprojects reading
their parent project’s included. Gradle 9.6 deprecated that implicit walk up the project tree. With
extra["buildLabel"] = "nightly" added to the root script and a subproject reading it with project.property("buildLabel"):
tasks.register("showLabel") {
val label = project.property("buildLabel").toString()
doLast {
println("label = $label")
}
}
./gradlew --warning-mode all :tidepool-model:showLabel
Calculating task graph as no cached configuration is available for tasks: :tidepool-model:showLabel
Implicit lookup of properties in parent projects has been deprecated. This will fail with an error in Gradle 10. Property 'buildLabel' was not declared in project ':tidepool-model' and was resolved from root project 'tidepool'. This lookup was initiated by 'property()'. Consult the upgrading guide for further information: https://docs.gradle.org/9.8.1/userguide/upgrading_version_9.html#deprecated_implicit_lookup_in_parent_projects
at Build_gradle$$$result$1.execute(build.gradle.kts:10)
(Run with --stacktrace to get the full stack trace of this deprecation warning.)
> Task :tidepool-model:showLabel
label = nightly
The task worked, but the build uses a deprecated lookup that will fail in Gradle 10. The Groovy DSL, where the bare name buildLabel in the child script is
the lookup (def label = buildLabel), printed the same warning, ending with Property 'buildLabel' was not declared in project ':tidepool-model' and was resolved
from root project 'tidepool'. The deprecation covers the bare names in Groovy, property("x"), findProperty("x"), hasProperty("x") and getProperties() on the
project, and the by project property delegate of the Kotlin DSL when they walk to a parent. The reason is the one the upgrade guide gives: a typo can
silently resolve to an ancestor’s value, projects become coupled by name, and the arrangement is incompatible with Isolated Projects.
Reading through the explicit owner is allowed, because the coupling is then visible in the code. The fixed versions:
val buildLabel = rootProject.extra["buildLabel"] as String
tasks.register("showLabel") {
val label = buildLabel
doLast {
println("label = $label")
}
}
tasks.register('showLabel') {
def label = rootProject.ext.buildLabel
doLast {
println "label = ${label}"
}
}
Both ran without a warning. Asking the child’s own extra properties for the key fails fast, in both languages, with
Cannot get property 'buildLabel' on extra properties extension as it does not exist. That fail-fast behavior is the one good thing about the special syntax of
extra properties: a misspelled key is an error and not a silent null. The Kotlin DSL’s delegated form, val buildLabel: String by rootProject.extra,
which the user manual still shows in places, is deprecated since Gradle 9.6 along with every other by delegate of the Kotlin DSL
(by project, by settings, by registering, by getting, …) and printed The 'val name: Type by extra' property delegate syntax has been deprecated when run; the
replacement is the explicit extra[name] as Type form above.
Kotlin script-level values and the configuration cache
A second trap is specific to the Kotlin DSL. A top-level val of a Kotlin script is a field of the script object, and a task action that uses it captures
the script object. The task below worked without the configuration cache and failed with it:
val buildLabel = rootProject.extra["buildLabel"] as String
tasks.register("showLabel") {
doLast {
println("label = $buildLabel")
}
}
FAILURE: Build failed with an exception.
* What went wrong:
Configuration cache problems found in this build.
1 problem was found storing the configuration cache.
- Task `:tidepool-model:showLabel` of type `org.gradle.api.DefaultTask`: cannot serialize Gradle script object references as these are not supported with the configuration cache.
See https://docs.gradle.org/9.8.1/userguide/configuration_cache_requirements.html#config_cache:requirements:disallowed_types
The fix is the one in the previous listing: copy the value into a local val declared inside the task’s configuration block, so the action captures a
string. The Groovy version, where the script-level variable is a local of the script’s run method, did not have the problem in the same test. The pattern, "read at
configuration time into a local, use the local in the action", is the single most useful rule for writing configuration cache compatible scripts.
What to use instead
| You want | Use |
|---|---|
a library version |
the version catalog ( |
a value from the command line or from |
|
a value from the environment |
|
a setting shared by projects |
a convention plugin with an extension, see Convention Plugins & Sharing Build Logic |
a typed value used inside one script |
a local |
the output of a command |
|
The lazy property route is short. This was appended to the CLI project’s script (Kotlin DSL; the Groovy DSL uses def and doLast { println "..." }):
val dataset = providers.gradleProperty("tidepool.dataset").orElse("sample")
tasks.register("showDataset") {
val name = dataset
doLast {
println("dataset = ${name.get()}")
}
}
./gradlew -q :tidepool-cli:showDataset
./gradlew -q :tidepool-cli:showDataset -Ptidepool.dataset=harbour
dataset = sample
dataset = harbour
The Groovy DSL printed the same two lines. With the cache on, changing the -P value on the command line did not invalidate the cached task graph: the value
is read lazily when the task runs, so the next two runs (harbour, then estuary) both printed Reusing configuration cache. and the new value. The order in
which -P, gradle.properties, system properties and environment variables win is on
Build Environment & Properties.
Services and processes in scripts
Scripts often need to copy files, build a file collection or run a program. A Project still offers files(...), fileTree(...), copy { },
delete(...), zipTree(...), objects, layout and providers. What changed over the releases is when you may use them: with the configuration cache,
the Project must not be used from a task action, so copy { } in a doLast of an ad-hoc task fails with the same "script object references" error as above, and
Project.exec/javaexec were removed in Gradle 9.0 (they were deprecated in 8.11). The service-based replacements are FileSystemOperations,
ArchiveOperations and ExecOperations, which a task or a plugin receives by injection.
Gradle 9.8.0’s release notes summarize one of their improvements as the built-in services for files, objects and processes being
"available directly in scripts and task actions". The release notes have no section for it, the user manual does not list script-level names for those services, and the
user manual’s configuration cache page still says that services cannot be injected directly in scripts and that an extra type has to carry the
injection point. To see what a script has in 9.8.1, a Groovy script was probed for the usual names, and the Kotlin script that calls
execOperations.exec { ... } from a task action was compiled:
['execOperations', 'fileSystemOperations', 'archiveOperations', 'objects', 'layout', 'providers'].each { name ->
println "${name}: ${project.hasProperty(name) ? 'visible in the script' : 'not visible in the script'}"
}
execOperations: not visible in the script
fileSystemOperations: not visible in the script
archiveOperations: not visible in the script
objects: visible in the script
layout: visible in the script
providers: visible in the script
e: file://<work>/build.gradle.kts:12:9: Unresolved reference 'execOperations'.
So in 9.8.1 no script-level execOperations, fileSystemOperations or archiveOperations resolved here, whatever the notes' wording covers; check the
9.8 upgrade guide and the release notes of later versions before relying on a shorter spelling. What does work, and is documented, is injection through a small
type:
interface Injected {
@get:Inject val fs: FileSystemOperations
}
val injected = objects.newInstance<Injected>()
tasks.register("copyData") {
val dataDir = layout.projectDirectory.dir("data")
val outDir = layout.buildDirectory.dir("data-copy")
val fs = injected.fs
doLast {
fs.copy {
from(dataDir)
into(outDir)
}
println("copied ${outDir.get().asFile.list().toList()}")
}
}
interface Injected {
@Inject FileSystemOperations getFs()
}
def injected = objects.newInstance(Injected)
tasks.register('copyData') {
def dataDir = layout.projectDirectory.dir('data')
def outDir = layout.buildDirectory.dir('data-copy')
def fs = injected.fs
doLast {
fs.copy {
from dataDir
into outDir
}
println "copied ${outDir.get().asFile.list().toList()}"
}
}
Calculating task graph as no cached configuration is available for tasks: copyData
> Task :copyData
copied [stations.csv]
BUILD SUCCESSFUL in 824ms
1 actionable task: 1 executed
Configuration cache entry stored.
and, on the second run, Reusing configuration cache. and Configuration cache entry reused.. Both DSL versions ran with the cache on and are
configuration cache compatible; injected is created with objects.newInstance, so Gradle supplies the fs service when it builds the object. The longer
treatment, including the abstract task classes where injection is the normal route, is on
Writing Custom Tasks and
Worker API & Build Services.
Running a program: providers.exec, not ProcessBuilder
Starting an external process while a script is evaluated is a deprecated practice in Gradle 9.8 and an error with the configuration cache. The
supported route for "I need the output of a command at configuration time" is the ProviderFactory.exec provider, which Gradle can track as a
build input:
val gitVersion = providers.exec {
commandLine("git", "--version")
}.standardOutput.asText.map { it.trim() }
tasks.register("showGit") {
val version = gitVersion
doLast {
println("tool: ${version.get()}")
}
}
def gitVersion = providers.exec {
commandLine 'git', '--version'
}.standardOutput.asText.map { it.trim() }
tasks.register('showGit') {
def version = gitVersion
doLast {
println "tool: ${version.get()}"
}
}
Calculating task graph as no cached configuration is available for tasks: showGit
> Task :showGit
tool: git version 2.54.0 (Apple Git-157)
BUILD SUCCESSFUL in 341ms
1 actionable task: 1 executed
Configuration cache entry stored.
Both DSLs gave the same line and a second run reused the cache. The mistake it replaces is a ProcessBuilder (Groovy: 'git --version'.execute()) in the script body.
With val raw = ProcessBuilder("git", "--version").start().inputStream.bufferedReader().readText().trim() the build with the cache off gave a deprecation warning
that names Gradle 11:
> Configure project :
Starting an external process at configuration time has been deprecated. This will fail with an error in Gradle 11. Use ProviderFactory.exec/javaexec or a ValueSource to obtain the process output, or move the process execution into a task. Consult the upgrading guide for further information: https://docs.gradle.org/9.8.1/userguide/upgrading_version_9.html#config_time_external_processes
at Build_gradle.<init>(build.gradle.kts:10)
(Run with --stacktrace to get the full stack trace of this deprecation warning.)
config-time: git version 2.54.0 (Apple Git-157)
and with the configuration cache on the same line is an error:
FAILURE: Build failed with an exception.
* What went wrong:
Configuration cache problems found in this build.
1 problem was found storing the configuration cache.
- Build file 'build.gradle.kts': external process started 'git --version'
See https://docs.gradle.org/9.8.1/userguide/configuration_cache_requirements.html#config_cache:requirements:external_processes
See the complete report at file://<work>/build/reports/configuration-cache/7youub29jtl57w6nyfoiz0uqp/7mhtxg59mao3msxb325ezoma1/configuration-cache-report.html
> Starting an external process 'git --version' during configuration time is unsupported.
Classic mistakes
| Symptom | Cause and fix |
|---|---|
|
the blocks of a settings script are out of order; |
|
|
|
a |
|
|
|
the plugin was applied with |
|
a statement above |
|
a Gradle 6 or earlier dependency configuration; use |
|
|
|
a project reads an |
|
a Kotlin script-level |
|
|
output printed on every build but not on the cached ones |
a |
The same ideas in Ant and Maven
| Concern | Gradle | Maven | Ant |
|---|---|---|---|
which projects exist |
|
|
|
build-wide plugin and repository choice |
|
|
|
one file per unit of build |
|
the POM, a declarative model (POM Fundamentals) |
|
how a project inherits from another |
convention plugins; |
|
|
user-level settings that are not in the project |
|
|
|
Maven’s settings.xml is not the counterpart of Gradle’s settings.gradle(.kts): it holds the user’s servers, mirrors and profiles, whereas the Gradle settings
file is part of the project. The nearest analogue of settings.gradle(.kts) in Maven is the aggregator POM’s <modules> element; the nearest analogue
of the user’s settings.xml is the Gradle User Home’s gradle.properties and init scripts. The comparison of the three build models is on
Comparing Ant, Maven & Gradle; moving a build between them is on
Migrating Between Tools.
Gradle 8.14 (not run here)
Gradle 8.14.6 was not run for this page. From the official documentation, the pieces of this page that behave differently there:
-
The embedded languages are older: Gradle 9.0 moved the Groovy DSL to Groovy 4 and the Kotlin DSL to a newer Kotlin language version (the upgrade guide to Gradle 9 lists the Groovy 4 behavior changes, such as the
is-prefixed boolean properties no longer being recognized as Groovy properties). -
Project.execandProject.javaexecexist and work in 8.14 with a deprecation warning (deprecated in 8.11, removed in 9.0). -
None of the Gradle 9.x deprecations shown above exists there: the implicit lookup of properties in parent projects (deprecated in 9.6), the
by extraand other Kotlin DSL property delegates (9.6) and the configuration-time process warning (9.8) are silent in 8.14. They are still the right things to avoid, because the upgrade to 9 and 10 will make them errors. -
The Groovy DSL "space assignment" syntax (
url "https://...",mainClass 'x') is deprecated since 8.12, so an 8.14 build prints warnings for it that 9.8.1 also prints. -
Lazy property assignment with
=in the Kotlin DSL is available since 8.2, so this page’srepositoriesMode = ...andmainClass = ...lines work in 8.14; before 8.2 they were.set(...). -
The configuration cache is stable since 8.1 and off by default in 8.14 as well as in 9.8.1; "preferred mode" arrived in 9.0.
-
task x << { }and thecompileconfigurations are gone in both (5.0 and 7.0).
Where Gradle in Action is outdated
Gradle in Action (Muschko, Manning 2014) was written for Gradle 1.7. Its chapter on the build script’s model, the Project and Task objects and extra properties is still a good description of how a script is built from an object and a delegate, but several statements no longer hold:
| The book | Gradle 9.8.1 |
|---|---|
|
removed in 5.0; |
|
|
a |
repositories once in |
Groovy DSL only |
Kotlin DSL is the default of |
versions kept in |
version catalogs; implicit parent lookups of |
|
convention plugins in an included build |
the daemon as an opt-in |
the daemon is on by default since Gradle 3.0 |
a Project API used freely in task actions |
not with the configuration cache; services are injected |
Sources for these rows are the upgrade guides linked above and
Upgrading to Gradle 5.0: Potential breaking changes
(for the << operator of the old task definitions).
Where to go next
-
Build Lifecycle: when each script runs, and the hooks that watch it.
-
Kotlin DSL and Groovy DSL & Migrating to Kotlin: the two languages, and moving from one to the other.
-
Tasks and Lazy Configuration & Providers: what to put in a build script’s task blocks, and the provider model behind
providers.gradleProperty. -
Multi Project Builds and Convention Plugins & Sharing Build Logic: structure beyond a handful of projects.
-
Build Environment & Properties:
gradle.properties,-P,-Dand theorg.gradle.*properties. -
Kotlin: Build and Tooling and Groovy: Build Tools for the language-side view of Gradle scripts.