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

settings.gradle.kts or settings.gradle, in the root directory

one Settings, which creates the Project objects

once per build, first; its statements run during the initialization phase

build script

build.gradle.kts or build.gradle, one per project, in that project’s directory

that project’s Project

during the configuration phase, top to bottom, parent project before children

init script

init.gradle(.kts) and *.init.gradle(.kts) files in init.d of the Gradle User Home, or -I script on the command line

the Gradle object of the build

before the settings script (see Init Scripts & the Tooling API)

script plugin

any other script file pulled in with apply(from = ...)

whatever object applied it (a Project, a Settings or a Gradle)

when the apply call executes, in the middle of the applying script

precompiled script plugin

*.gradle.kts files under src/main/kotlin of a plugin project such as build-logic

the object named by the file suffix (.settings.gradle.kts, .init.gradle.kts, otherwise a project)

as a normal plugin, when it is applied (see Convention Plugins & Sharing Build Logic)

flowchart TD init["init scripts: Gradle object (optional)"] --> settings["settings.gradle(.kts): Settings object"] settings --> pm["pluginManagement and plugins blocks"] settings --> drm["dependencyResolutionManagement"] settings --> inc["rootProject.name and include()"] inc --> root["root build.gradle(.kts): Project ':'"] inc --> sub["subproject build.gradle(.kts): Project ':tidepool-model' and the others"] root --> body["plugins, dependencies, extensions, task registration"] sub --> body body --> tg["task graph, then execution phase"]

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:

Kotlin DSL: settings.gradle.kts
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")
Groovy DSL: settings.gradle
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:

Kotlin DSL: build.gradle.kts
plugins {
    base
}

allprojects {
    group = "io.tidepool"
    version = "1.0.0"
}
Groovy DSL: build.gradle
plugins {
    id 'base'
}

allprojects {
    group = 'io.tidepool'
    version = '1.0.0'
}
gradle/libs.versions.toml (the same for both DSLs; highlight.js has no TOML grammar, so it is shown as plain text)
[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" }
gradle.properties
org.gradle.configuration-cache=true

The three project build scripts:

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

tasks.withType<JavaCompile>().configureEach {
    options.release = 21
}
Groovy DSL: tidepool-model/build.gradle
plugins {
    id 'java-library'
}

tasks.withType(JavaCompile).configureEach {
    options.release = 21
}
Kotlin DSL: tidepool-engine/build.gradle.kts
plugins {
    `java-library`
}

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

tasks.withType<JavaCompile>().configureEach {
    options.release = 21
}
Groovy DSL: tidepool-engine/build.gradle
plugins {
    id 'java-library'
}

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

tasks.withType(JavaCompile).configureEach {
    options.release = 21
}
Kotlin DSL: tidepool-cli/build.gradle.kts
plugins {
    application
}

dependencies {
    implementation(project(":tidepool-engine"))
    implementation(libs.picocli)
}

application {
    mainClass = "io.tidepool.cli.Main"
}

tasks.withType<JavaCompile>().configureEach {
    options.release = 21
}
Groovy DSL: tidepool-cli/build.gradle
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 write id("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 without buildSrc, 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

PREFER_PROJECT

repositories declared in a project override those of the settings

yes

PREFER_SETTINGS

repositories of the settings override those declared in a project

no

FAIL_ON_PROJECT_REPOS

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:

Kotlin DSL: settings.gradle.kts
include("model", "engine", "cli")
project(":model").projectDir = file("tidepool-model")
project(":engine").projectDir = file("tidepool-engine")
project(":cli").projectDir = file("tidepool-cli")
Groovy DSL: settings.gradle
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

buildCache { }

local and remote build cache (Build Cache)

toolchainManagement { }

where JDKs are downloaded from (Toolchains)

includeBuild(...)

other builds that take part in this one

enableFeaturePreview(...)

opt in to a documented feature preview (see Versions, Compatibility & Release History)

gradle

the Gradle object of this build, for the lifecycle hooks of Build Lifecycle

layout.rootDirectory, layout.settingsDirectory, rootDir

build-wide locations (layout is the incubating BuildLayout service, computed lazily; rootDir is the older eager form)

rootProject, include, project(path)

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, application and java-library (back quoted because of the hyphen) stand for the ids base, application and java-library, and id("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 false makes 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:

Kotlin DSL: gradle/release21.gradle.kts
println("release21.gradle.kts applied to ${project.path}")

tasks.withType<JavaCompile>().configureEach {
    options.release = 21
}
Groovy DSL: gradle/release21.gradle
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:

Kotlin DSL
apply(from = rootProject.file("gradle/release21.gradle.kts"))
Groovy DSL
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:

Kotlin DSL: appended to settings.gradle.kts
println("settings script: settings = $settings")
println("settings script: rootProject.name = ${rootProject.name}")
println("settings script: rootDir = ${rootDir.name}")
Kotlin DSL: appended to every build.gradle.kts
println("build script: project = $project, path = $path")
Groovy DSL: appended to settings.gradle
println "settings script: settings = ${settings}"
println "settings script: rootProject.name = ${rootProject.name}"
println "settings script: rootDir = ${rootDir.name}"
Groovy DSL: appended to every build.gradle
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.

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"):

Kotlin DSL: appended to tidepool-model/build.gradle.kts
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:

Kotlin DSL
val buildLabel = rootProject.extra["buildLabel"] as String

tasks.register("showLabel") {
    val label = buildLabel
    doLast {
        println("label = $label")
    }
}
Groovy DSL
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 (libs.versions.toml), see Version Catalogs

a value from the command line or from gradle.properties

providers.gradleProperty("name"), a lazy Provider

a value from the environment

providers.environmentVariable("NAME")

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 val/def

the output of a command

providers.exec { ... }, see below

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:

Kotlin DSL
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()}")
    }
}
Groovy DSL
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:

Kotlin DSL
val gitVersion = providers.exec {
    commandLine("git", "--version")
}.standardOutput.asText.map { it.trim() }

tasks.register("showGit") {
    val version = gitVersion
    doLast {
        println("tool: ${version.get()}")
    }
}
Groovy DSL
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

plugins cannot appear before pluginManagement

the blocks of a settings script are out of order; pluginManagement, then buildscript, then plugins

Build was configured to prefer settings repositories over project repositories

repositories { } in a project script under FAIL_ON_PROJECT_REPOS; remove it and use the settings

The project name '...' must not contain any of the following characters

a : or / in rootProject.name or in an include name

Configuring project ':x' without an existing directory is not allowed

include("x") with no directory; create it or map projectDir

Unresolved reference for java { } or application { } in a .kts file

the plugin was applied with apply(plugin = ...); use plugins { } or configure<...>

only buildscript {}, pluginManagement {} and other plugins {} script blocks are allowed before plugins {} blocks

a statement above plugins { } in a Groovy script

Could not find method compile()

a Gradle 6 or earlier dependency configuration; use implementation or api

Could not find method leftShift()

task x << { }, removed in 5.0; use tasks.register and doLast

Implicit lookup of properties in parent projects has been deprecated

a project reads an ext/extra property of an ancestor by bare name or property(...); read it through rootProject or use providers.gradleProperty

cannot serialize Gradle script object references

a Kotlin script-level val (or a Project call) used inside a task action; copy it to a local first

Starting an external process ... during configuration time

ProcessBuilder or execute() in a script body; use providers.exec or move it into a task

output printed on every build but not on the cached ones

a println in a script body runs only when the configuration is recomputed

The same ideas in Ant and Maven

Concern Gradle Maven Ant

which projects exist

include(...) in settings.gradle(.kts)

<modules> in the aggregator POM (Multi-Module Builds & the Reactor)

<subant> or <ant> calls; no project tree (Multi-File Builds)

build-wide plugin and repository choice

pluginManagement, dependencyResolutionManagement

<pluginRepositories>, <repositories>, and the user’s settings.xml mirrors (settings.xml & Password Encryption)

ivysettings.xml resolvers, <taskdef> classpaths

one file per unit of build

build.gradle(.kts), a program

the POM, a declarative model (POM Fundamentals)

build.xml, a script (Build File Structure)

how a project inherits from another

convention plugins; allprojects/subprojects from the root

<parent> and inheritance (Inheritance & the Effective POM)

<import>

user-level settings that are not in the project

~/.gradle/gradle.properties and init scripts

~/.m2/settings.xml

ant.properties, ~/.ant

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.exec and Project.javaexec exist 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 extra and 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’s repositoriesMode = ... and mainClass = ... 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 the compile configurations 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

task hello << { ... }

removed in 5.0; tasks.register("hello") { doLast { ... } }

apply plugin: 'java' as the only way, with compile and testCompile dependencies

plugins { } block with implementation, api, testImplementation; compile and the maven plugin removed in 7.0

a repositories { mavenCentral() } block in every project

repositories once in dependencyResolutionManagement, enforced with FAIL_ON_PROJECT_REPOS

Groovy DSL only

Kotlin DSL is the default of gradle init (since 8.2) and what this page shows first

versions kept in ext properties of the root project

version catalogs; implicit parent lookups of ext are deprecated in 9.6

allprojects { } and subprojects { } to configure everything from the root

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