Table of Contents

OVERVIEW

Gradle User Manual

Gradle Build Tool

gradle Gradle Build Tool is a fast, dependable, and adaptable open-source build automation tool with an elegant and extensible declarative build language.

In this User Manual, Gradle Build Tool is abbreviated Gradle.

Supported Languages and Frameworks

Gradle supports Android, Java, Kotlin Multiplatform, Groovy, Scala, Javascript, and C/C++.

userguide languages

Compatible IDEs

All major IDEs support Gradle, including Android Studio, IntelliJ IDEA, Visual Studio Code, Eclipse, and NetBeans.

userguide ides

You can also invoke Gradle via its command-line interface (CLI) in your terminal or through your continuous integration (CI) server.

Releases

Information on Gradle releases is found on the Release page.

Installing Gradle

Most projects will start with an existing Gradle build which does not require the installation of Gradle. However, if you are starting a project from scratch, and you need to install Gradle, check out the installation guide.

DPE University

Want to get up and running with Gradle quickly? Take our free, self-paced Gradle Build Tool courses at DPE University.

For Software Engineers and Developers

For software developers that need to build, test, and publish their app, or add dependencies to their build, get started here:

1. Learn how to run Gradle Builds
Description: Learn how to invoke tasks and add dependencies.
Training level: Beginner
Reading time: 25 minutes
→ Read Beginner Concepts
Description: Initialize a Gradle build for a basic Java App.
Training level: Beginner
Training time: 45 minutes
→ Start the Tutorial

For Build Engineers

Build engineers that are ready to configure custom build logic should start here:

1. Learn how to write Gradle scripts
Description: Learn to configure builds, create tasks, and apply plugins.
Training level: Intermediate
Reading time: 35 minutes
→ Read Intermediate Concepts
Description: Initialize a Gradle project and create a convention plugin.
Training level: Intermediate
Training time: 55 minutes
→ Start the Tutorial

For Plugin Developers

Plugin authors that are ready to develop and publish their own plugins should start here:

1. Learn how to develop Gradle Plugins
Description: Learn to write and publish a plugin.
Training level: Advanced
Reading time: 35 minutes
→ Read Advanced Concepts
Description: Initialize a Gradle project, create a binary plugin, and publish it locally.
Training level: Advanced
Training time: 55 minutes
→ Start the Tutorial

Support

  • Forum — The fastest way to get help is through the Gradle Forum.

  • Slack — Community members and core contributors answer questions directly on our Slack Channel.

Licenses

Gradle Build Tool source code is open and licensed under the Apache License 2.0. Gradle user manual and DSL reference manual are licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

© 2025 Gradle, Inc. Gradle®, Develocity®, Build Scan®, and the Gradlephant logo are registered trademarks of Gradle, Inc. On this resource, "Gradle" typically means "Gradle Build Tool" and does not reference Gradle, Inc. and/or its subsidiaries.

For inquiries related to commercial use or licensing, contact Gradle, Inc. directly.

Getting Started

Gradle for Software Engineers

Everyone has to start somewhere, and if you’re new to Gradle, this is where to begin.

1. Learn how to run Gradle Builds

This section goes through the Gradle core concepts so that you can quickly understand how to invoke tasks, turn on features, apply plugins, add dependencies to your project, and more.

Training level: Beginner
Reading time: 25 minutes

This section covers:

Part 1. Core Concepts
Part 2. Wrapper Basics
Part 3. Command Line Interface Basics
Part 4. Settings File Basics
Part 5. Build Files Basics
Part 6. Dependencies and Dependency Management Basics
Part 7. Tasks Basics
Part 8. Incremental Builds and Build Caching Basics
Part 9. Plugins Basics
Part 10. Build Scan

2. Beginner Gradle Tutorial

The tutorial will take you from Gradle initialization all the way through to utilizing Gradle’s task caching for your basic Java App. No previous experience is necessary but a basic knowledge of Java and Kotlin is nice to have.

If you need to install Gradle before the tutorial, you can do so in the installation section.

Training level: Beginner
Training time: 55 minutes

The tutorial covers:

Part 1. Initializing the Project
Part 2. Running Tasks
Part 3. Understanding Dependencies
Part 4. Applying Plugins
Part 5. Exploring Incremental Builds
Part 6. Enabling the Cache

Gradle for Build Engineers

Build engineers that are ready to configure and organize custom build logic should start here.

1. Learn how to write Gradle scripts

This section goes through some Gradle authoring basics so that you can quickly understand how to configure builds, create tasks, and organize logic.

Training level: Intermediate
Reading time: 35 minutes

This section covers:

Part 1. Anatomy of a Gradle Build
Part 2. Structuring Multi-Project Builds
Part 3. Gradle Build Lifecycle
Part 4. Writing Build Scripts
Part 5. Gradle Managed Types
Part 6. Declaring and Managing Dependencies
Part 7. Creating and Registering Tasks
Part 8. Working With Plugins

2. Intermediate Gradle Tutorial

The tutorial will take you from Gradle initialization all the way through registering tasks and the basics of plugins.

Training level: Intermediate
Training time: 65 minutes

The tutorial covers:

Part 1. Initializing the Project
Part 2. Understanding the Build Lifecycle
Part 3. Multi-Project Builds
Part 6. Writing the Settings File
Part 5. Writing Build Scripts
Part 6. Writing Tasks
Part 7. Writing Plugins

Gradle for Plugin Developers

Plugin authors that are ready to write their own plugins and publish it should start here.

1. Learn how to develop Gradle Plugins

This section goes through developing tasks, writing, and publishing plugins.

Training level: Advanced
Reading time: 35 minutes

This section covers:

Part 1. Plugin Introduction
Part 2. Pre-Compiled Script Plugins
Part 3. Binary Plugins
Part 4. Binary Plugin Development
Part 5. Binary Plugin Publishing

2. Advanced Gradle Tutorial

The tutorial will take you from Gradle initialization all the way through creating and publishing a binary plugin.

Training level: Advanced
Training time: 65 minutes

The tutorial covers:

Part 1. Initializing the Project
Part 2. Adding an Extension
Part 3. Creating a Custom Task
Part 6. Writing a Unit Test
Part 5. Adding a DataFlow Action
Part 6. Writing a Functional Test
Part 7. Using a Consumer Project
Part 8. Publishing the Plugin

RELEASES

Installing Gradle

If all you want to do is run an existing Gradle project, then you don’t need to install Gradle if the build uses the Gradle Wrapper.

Gradle Installation

The Gradle Wrapper is identifiable by the presence of the gradlew or gradlew.bat files in the root of the project:

.   // (1)
├── gradle
│   └── wrapper // (2)
├── gradlew         // (3)
├── gradlew.bat     // (3)
└── ⋮
  1. Project root directory.

  2. Gradle Wrapper.

  3. Scripts for executing Gradle builds.

If the gradlew or gradlew.bat files are already present in your project, you do not need to install Gradle. But you need to make sure your system satisfies Gradle’s prerequisites.

You can follow the steps in the Upgrading Gradle section if you want to update the Gradle version for your project. Please use the Gradle Wrapper to upgrade Gradle.

Android Studio comes with a working installation of Gradle, so you don’t need to install Gradle separately when only working within that IDE.

If you do not meet the criteria above and decide to install Gradle on your machine, first check if Gradle is already installed by running gradle -v in your terminal. If the command does not return anything, then Gradle is not installed, and you can follow the instructions below.

You can install Gradle Build Tool on Linux, macOS, or Windows. The installation can be done manually or using a package manager like SDKMAN! or Homebrew.

You can find all Gradle releases and their checksums on the releases page.

Prerequisites

Gradle runs on all major operating systems. It requires Java Development Kit (JDK) version 17 or higher to run. You can check the compatibility matrix for more information.

To check, run java -version:

$ java -version
openjdk version "17.0.6" 2023-01-17
OpenJDK Runtime Environment Temurin-17.0.6+10 (build 17.0.6+10)
OpenJDK 64-Bit Server VM Temurin-17.0.6+10 (build 17.0.6+10, mixed mode)

Gradle uses the JDK it finds in your path, the JDK used by your IDE, or the JDK specified in your project.

In this example, the $PATH points to JDK17:

$ echo $PATH
/opt/homebrew/opt/openjdk@17/bin

You can also set the JAVA_HOME environment variable to point to a specific JDK installation directory. This is especially useful when multiple JDKs are installed:

$ echo %JAVA_HOME%
C:\Program Files\Java\jdk17.0_6
$ echo $JAVA_HOME
/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home

Gradle supports Kotlin and Groovy as the main build languages. Gradle ships with its own Kotlin and Groovy libraries, therefore they do not need to be installed. Existing installations are ignored by Gradle.

Linux installation

Installing with a package manager

SDKMAN! is a tool for managing parallel versions of multiple Software Development Kits on most Unix-like systems (macOS, Linux, Cygwin, Solaris and FreeBSD). Gradle is deployed and maintained by SDKMAN!:

$ sdk install gradle

Other package managers are available, but the version of Gradle distributed by them is not controlled by Gradle, Inc. Linux package managers may distribute a modified version of Gradle that is incompatible or incomplete when compared to the official version.

Installing manually

Step 1 - Download the latest Gradle distribution

The distribution ZIP file comes in two flavors:

  • Binary-only (bin)

  • Complete (all) with docs and sources

We recommend downloading the bin file; it is a smaller file that is quick to download (and the latest documentation is available online).

Step 2 - Unpack the distribution

Unzip the distribution zip file in the directory of your choosing, e.g.:

$ mkdir /opt/gradle
$ unzip -d /opt/gradle gradle-9.7.0-bin.zip
$ ls /opt/gradle/gradle-9.7.0
LICENSE  NOTICE  bin  README  init.d  lib  media

Step 3 - Configure your system environment

To install Gradle, the path to the unpacked files needs to be in your Path. Configure your PATH environment variable to include the bin directory of the unzipped distribution, e.g.:

$ export PATH=$PATH:/opt/gradle/gradle-9.7.0/bin

Alternatively, you could also add the environment variable GRADLE_HOME and point this to the unzipped distribution. Instead of adding a specific version of Gradle to your PATH, you can add $GRADLE_HOME/bin to your PATH. When upgrading to a different version of Gradle, simply change the GRADLE_HOME environment variable.

$ export GRADLE_HOME=/opt/gradle/gradle-9.7.0
$ export PATH=${GRADLE_HOME}/bin:${PATH}

macOS installation

Installing with a package manager

SDKMAN! is a tool for managing parallel versions of multiple Software Development Kits on most Unix-like systems (macOS, Linux, Cygwin, Solaris and FreeBSD). Gradle is deployed and maintained by SDKMAN!:

$ sdk install gradle

Using Homebrew:

$ brew install gradle

Using MacPorts:

$ sudo port install gradle

Other package managers are available, but the version of Gradle distributed by them is not controlled by Gradle, Inc.

Installing manually

Step 1 - Download the latest Gradle distribution

The distribution ZIP file comes in two flavors:

  • Binary-only (bin)

  • Complete (all) with docs and sources

We recommend downloading the bin file; it is a smaller file that is quick to download (and the latest documentation is available online).

Step 2 - Unpack the distribution

Unzip the distribution zip file in the directory of your choosing, e.g.:

$ mkdir /usr/local/gradle
$ unzip gradle-9.7.0-bin.zip -d /usr/local/gradle
$ ls /usr/local/gradle/gradle-9.7.0
LICENSE	NOTICE	README	bin	init.d	lib

Step 3 - Configure your system environment

To install Gradle, the path to the unpacked files needs to be in your Path. Configure your PATH environment variable to include the bin directory of the unzipped distribution, e.g.:

$ export PATH=$PATH:/usr/local/gradle/gradle-9.7.0/bin

Alternatively, you could also add the environment variable GRADLE_HOME and point this to the unzipped distribution. Instead of adding a specific version of Gradle to your PATH, you can add $GRADLE_HOME/bin to your PATH. When upgrading to a different version of Gradle, simply change the GRADLE_HOME environment variable.

It’s a good idea to edit .bash_profile in your home directory to add GRADLE_HOME variable:

$ export GRADLE_HOME=/usr/local/gradle/gradle-9.7.0
$ export PATH=$GRADLE_HOME/bin:$PATH

Windows installation

Installing manually

Step 1 - Download the latest Gradle distribution

The distribution ZIP file comes in two flavors:

  • Binary-only (bin)

  • Complete (all) with docs and sources

We recommend downloading the bin file.

Step 2 - Unpack the distribution

Create a new directory C:\Gradle with File Explorer.

Open a second File Explorer window and go to the directory where the Gradle distribution was downloaded. Double-click the ZIP archive to expose the content. Drag the content folder gradle-9.7.0 to your newly created C:\Gradle folder.

Alternatively, you can unpack the Gradle distribution ZIP into C:\Gradle using the archiver tool of your choice.

Step 3 - Configure your system environment

To install Gradle, the path to the unpacked files needs to be in your Path.

In File Explorer right-click on the This PC (or Computer) icon, then click PropertiesAdvanced System SettingsEnvironmental Variables.

Under System Variables select Path, then click Edit. Add an entry for C:\Gradle\gradle-9.7.0\bin. Click OK to save.

Alternatively, you can add the environment variable GRADLE_HOME and point this to the unzipped distribution. Instead of adding a specific version of Gradle to your Path, you can add %GRADLE_HOME%\bin to your Path. When upgrading to a different version of Gradle, just change the GRADLE_HOME environment variable.

Verify the installation

Open a console (or a Windows command prompt) and run gradle -v to run gradle and display the version, e.g.:

$ gradle -v
------------------------------------------------------------
Gradle 9.0.0
------------------------------------------------------------

Build time:    2025-07-17 12:48:00 UTC
Revision:      2db9560bb68c367a265b10516c856c840f9bed8d

Kotlin:        2.2.0
Groovy:        4.0.28
Ant:           Apache Ant(TM) version 1.10.15 compiled on August 25 2024
Launcher JVM:  17.0.11 (Amazon.com Inc. 17.0.11+9-LTS)
Daemon JVM:    Compatible with Java 17, any vendor, nativeImageCapable=false (from gradle/gradle-daemon-jvm.properties)
OS:            Mac OS X 14.7.4 aarch64

You can verify the integrity of the Gradle distribution by downloading the SHA-256 file (available from the releases page) and following these verification instructions.

Upgrading within Gradle 9.x.y

This chapter provides the information you need to migrate your Gradle 9.x.y builds to the latest. For migrating to Gradle 9.0.0, see the older migration guide first.

We recommend the following steps for all users:

  1. Try running gradle help --scan and view the deprecations view of the generated Build Scan.

    Deprecations View of a Gradle Build Scan

    This lets you see any deprecation warnings that apply to your build.

    Alternatively, you can run gradle help --warning-mode=all to see the deprecations in the console, though it may not report as much detailed information.

  2. Update your plugins.

    Some plugins will break with this new version of Gradle because they use internal APIs that have been removed or changed. The previous step will help you identify potential problems by issuing deprecation warnings when a plugin tries to use a deprecated part of the API.

  3. Run gradle :wrapper --gradle-version 9.7.0 to update the project to 9.7.0.

  4. Try to run the project and debug any errors using the Troubleshooting Guide.

Upgrading from 9.6.0 and earlier

Potential breaking changes
Checksum files are no longer published for signature files

When publishing to Maven or Ivy repositories, Gradle no longer publishes checksum files (such as .sha1 and .md5) for signature files like .asc and .sig.

A signature is itself an integrity artifact, so a checksum of a signature provides no additional protection, and the standard Maven publishing tooling does not publish these files either. The signature files themselves continue to be published unchanged; only their checksum files are no longer produced.

The project cache directory is no longer watched

Gradle no longer watches the project cache directory (the .gradle directory in the root project, or a custom location set with --project-cache-dir). Because Gradle is the only writer to this directory, it keeps the virtual file system up to date directly, without relying on OS file system events.

As a result, a custom project cache directory no longer disables file system watching. Previously, --project-cache-dir (or org.gradle.projectcachedir) was incompatible with watching: passing --watch-fs alongside it failed the build, and leaving watching at its default silently disabled it. File system watching now follows the usual rules regardless of where the project cache directory is located.

This applies to the project cache directory itself. Source-dependency check-outs, which live under the project cache directory but whose contents can change externally, remain watched as regular build directories.

Upgrade to Kotlin 2.4.0

The embedded Kotlin has been upgraded from 2.3.21 to Kotlin 2.4.0.

Starting with Kotlin 2.4.0, the compiler no longer supports -language-version=1.9, which means the K1 compiler is gone entirely. 2.0 is the new minimum compile target (and is itself deprecated, pointing at 2.2).

From the 2.4.0 release notes:

  • KGP requires a minimum Gradle version of 7.6.3, fully tested through 9.5.0.

  • Support for Java 26 has been added.

Oldest supported kotlin-dsl plugin version is now 6.2.0, which can be used to build precompiled-script plugins targeting a language level as old as 1.8. Oldest supported KGP version is 1.9.22 which can be used to write Kotlin plugins targeting language levels as old as 1.4 and be used in Gradle versions as old as 7.2.

Kotlin DSL scripts can no longer reference relocated internal dependencies

Kotlin DSL build scripts (.gradle.kts) are now compiled against a prebuilt public API JAR from the Gradle distribution instead of the dynamically generated Gradle API JAR.

As a result, build scripts that import classes from org.gradle.internal.impldep.* (Gradle’s relocated internal dependencies) will no longer compile. These classes were never part of Gradle’s public API.

If your build scripts reference org.gradle.internal.impldep.* types, replace them with the original library coordinates added as dependencies to your build logic:

build.gradle.kts
// Before (will no longer compile):
// import org.gradle.internal.impldep.com.google.gson.Gson

// After: add the dependency to buildscript or buildSrc
buildscript {
    dependencies {
        classpath("com.google.gson:gson:2.11.0")
    }
}
// Then import directly:
// import com.google.gson.Gson
Note
This change does not affect plugin projects that have gradleApi() or gradleKotlinDsl() among their dependencies. Those continue to use the full generated Gradle API JAR as before.
Behavior and contract changes for *Parameters.None

The marker types used to declare a parameterless action — WorkParameters.None, TransformParameters.None, BuildServiceParameters.None, ValueSourceParameters.None, FlowParameters.None and JvmTestToolchainParameters.None — have been standardized.

  1. The configuration Action is now invoked even when the action declares None as its parameters type. Calls like WorkQueue.submit(MyAction.class, p → …​), FlowActionSpec.parameters { …​ }, ValueSourceSpec.parameters { …​ }, BuildServiceSpec.parameters { …​ } and TransformSpec.parameters { …​ } now always run the configuration block. When the parameters type is None, the block receives the None singleton instead of being skipped. Existing blocks that just configured properties on a real parameters type are unaffected; any code that relied on the block being silently skipped for None will now run.

  2. getParameters() returns the None singleton instead of null for parameterless actions. Inside a WorkAction, TransformAction, BuildService, ValueSource or FlowAction that declares None as its parameters type, getParameters() now returns the None singleton. Previously it could return null. Null-checks against the result are no longer required.

No source change is needed in typical builds. If you have custom code that branched on getParameters() == null to detect a parameterless action, replace it with getParameters() instanceof WorkParameters.None (or the equivalent None type).

Feature previews
Opt into improved dependency resolution ordering

A new feature preview, ENHANCED_GRAPH_ORDERING, is available to opt into new dependency resolution ordering behavior that will become the default in Gradle 10.

With this feature preview enabled, Gradle will use a standard breadth-first ordering when using the DEFAULT sort order. Gradle may optionally be configured to return artifacts sorted topologically, using the CONSUMER_FIRST sort option, which sorts artifacts earlier than those they depend on. As usual, the DEPENDENCY_FIRST ordering is the reverse of the CONSUMER_FIRST ordering.

Notably, unlike the existing sorting methods, all sorting options in Gradle 10 will ignore constraint edges when traversing the resolved graph.

You can enable this feature preview in your settings.gradle.kts:

// settings.gradle.kts
enableFeaturePreview("ENHANCED_GRAPH_ORDERING")
Deprecations
Deprecation of the software model

The Gradle software model — including the model { } DSL, RuleSource plugins, and the native and JVM component types built on top of them — is deprecated and will be removed in Gradle 10.

This affects the following groups of public API types:

  • The rule-authoring API in org.gradle.model: RuleSource, ModelMap, ModelSet, ModelElement, and the @Model, @Mutate, @Defaults, @Finalize, @Validate, @Each, @Path, @RuleInput, @RuleTarget, @Rules, @Managed, and @Unmanaged annotations.

  • The component-model base API in org.gradle.platform.base and org.gradle.language.base: ComponentSpec, BinarySpec, LibrarySpec, ApplicationSpec, LanguageSourceSet, and related container, builder, and annotation types.

  • The native component plugins: c, cpp, objective-c, objective-cpp, assembler, windows-resources, and native-component, along with their source-set, binary-spec, build-type, flavor, and prebuilt-library types in org.gradle.nativeplatform and org.gradle.language.*.

  • The native test plugins cunit, cunit-test-suite, google-test, and google-test-test-suite, along with the rule-based test-suite-base infrastructure in org.gradle.testing.base.

  • The componentReport and dependentComponents diagnostic tasks.

Replace software-model-based builds with the current component model:

The new plugins configure components through ordinary Gradle extensions (application { }, library { }, unitTest { }) without the model { } block, integrate with the configuration cache, and support Gradle’s standard dependency-management features.

Deprecation of org.gradle.unsafe.isolated-projects* property names

The Isolated Projects feature can now be enabled with stable property names that drop the .unsafe. segment, alongside a new incubating CLI option --isolated-projects / --no-isolated-projects:

Legacy name New name

org.gradle.unsafe.isolated-projects

org.gradle.isolated-projects

org.gradle.unsafe.isolated-projects.diagnostics

org.gradle.isolated-projects.diagnostics

org.gradle.unsafe.isolated-projects.dangerously-ignore-problems

org.gradle.isolated-projects.dangerously-ignore-problems

The legacy org.gradle.unsafe.* names are now deprecated and will be removed in a future release. They continue to be accepted as aliases for now and currently emit no warning. Update your gradle.properties and command-line invocations to the new names.

When both the legacy and the new name of a property are set — for example, the new name in gradle.properties and the legacy name injected by a plugin or CI script on the command line — an explicit false from either one disables the feature. This guarantees that tooling which still opts out via a legacy property name keeps working after a build migrates to the new names.

See the Enabling Isolated Projects section in the Gradle User Manual for the recommended setup.

Deprecation of serializing custom collection and map types with the configuration cache

The configuration cache serializes a fixed set of standard collection, set, map, and queue types (such as ArrayList, LinkedHashSet, LinkedHashMap, and ArrayDeque) directly. A custom subtype of one of these types — for example, class MyList extends ArrayList<String> {} — cannot be restored as its own type and is instead restored as the nearest standard type, losing any state or behavior the custom type adds.

Referencing such a custom subtype from a task field or task action is now deprecated and will become an error in Gradle 10.

To fix it, use a standard collection or map type instead of a custom subtype, and store any extra data in separate fields.

Upgrading from 9.5.0 and earlier

Potential breaking changes
Plugins relying on removed Problems API internals are no longer compatible with Gradle 9.6

Several internal classes of the Problems API, such as org.gradle.api.problems.internal.InternalProblems, have been removed in Gradle 9.6. Plugins that bound to these internal classes directly fail at build time with an error mentioning the removed type.

The most common case is Android Gradle Plugin (AGP) 8.x, which relied on these internal classes. To resolve this, upgrade to AGP 9.x, which only uses the public Gradle API and fully supports Gradle 9.6. If the failure comes from another plugin, update it to a version that no longer uses Gradle internal APIs, or stay on Gradle 9.5 until such a version is available.

Concurrency primitives can no longer be serialized by the Configuration Cache

Configuration Cache serialization now reports a clear problem when attempting to serialize most of Java’s standard concurrency primitives. This includes classes and interfaces from the java.util.concurrent and java.util.concurrent.locks packages, such as ReentrantLock, CountDownLatch, and SynchronousQueue. Because the Configuration Cache enforces isolation between tasks, these primitives cannot be used correctly for cross-task synchronization. Allowing serialization would silently give each task its own independent instance (for example, its own lock), so cross-task coordination would never actually occur — that is the bug this error prevents.

Previously, attempts to serialize these types could fail with exceptions such as java.lang.reflect.InaccessibleObjectException, without clearly indicating the underlying issue. If your build requires shared, synchronized state, use shared build services, which are explicitly designed for safe coordination between tasks.

For a complete list of types that are incompatible with the Configuration Cache, see the documentation.

Upgrade to Kotlin 2.3.21

The embedded Kotlin has been upgraded from 2.3.20 to Kotlin 2.3.21.

Upgrade to PMD 7.24.0

The default version of PMD has been updated from 7.13.0 to 7.24.0.

Upgrade to CodeNarc 3.7.0

The default version of CodeNarc has been updated from 3.6.0 to 3.7.0.

Upgrade to Ant 1.10.17

Ant has been updated to 1.10.17. Since the previous version was 1.10.15, the 1.10.16 changes are also included.

Upgrade to Groovy 4.0.32

Groovy has been updated to 4.0.32. Since the previous version was 4.0.29, the 4.0.30 and 4.0.31 changes are also included.

Deprecations
Deprecation of implicit lookup of properties and methods in parent projects

In Gradle’s Groovy and Kotlin DSLs, when a child project’s build script references a property or method that isn’t defined locally, the resolution mechanism walks up the parent projects looking for a match:

build.gradle (root project)
ext.foo = "hello"
child/build.gradle
println(foo) // Resolved through hierarchy — now deprecated
build.gradle.kts (root project)
extra["foo"] = "hello"
child/build.gradle.kts
val foo: String? by project
println(foo) // Resolved through hierarchy — now deprecated

This implicit inheritance is deprecated and will be removed in Gradle 10.

Implicit hierarchy lookup creates hidden coupling between projects, makes builds harder to reason about (a typo silently resolves to an ancestor’s definition instead of failing), and is fundamentally incompatible with Isolated Projects.

Here is a full list of affected APIs and DSL constructs:

  • Dynamic references such as a bare foo or bar() in Groovy DSL

  • Kotlin DSL property delegates such as val foo: String by project

  • Explicit API calls on the Project instance or in a build script:

    • findProperty("foo")

    • property("foo")

    • hasProperty("foo")

    • getProperties()

To migrate, choose the approach that best fits your use case:

  • Build-wide configuration values — declare them in gradle.properties at the root, and read them in subprojects via providers.gradleProperty("name"). They become typed Provider<String> values and are available everywhere without any walking.

  • Plugin defaults / shared logic — extract into a convention plugin applied to each subproject. Each subproject gets its own configuration; nothing crosses project boundaries.

  • Explicit reference to an ancestor’s value — write rootProject.ext.foo (or parent.ext.foo) in the child build script. This is a transitional fix: it preserves the cross-project coupling and is therefore not Isolated Projects compatible, but it removes the deprecation warning and makes the dependency explicit.

If you need to keep the original walking semantics, the following helper can be added to a build script or convention plugin:

child/build.gradle.kts
tailrec fun Project.findExtraInHierarchy(name: String): Any? {
    if (extra.has(name)) return extra[name]
    val ancestor = parent ?: return null
    return ancestor.findExtraInHierarchy(name)
}
child/build.gradle
def findExtraInHierarchy(Project project, String name) {
    if (project == null) return null
    if (project.ext.has(name)) return project.ext.get(name)
    return findExtraInHierarchy(project.parent, name)
}
Opt into Gradle 10 behavior by disabling implicit lookup in parent projects

Once you have addressed all related deprecations, you can use a feature preview to adopt the Gradle 10 behavior early. This prevents new accidental implicit lookups from parent projects.

// settings.gradle.kts
enableFeaturePreview("NO_IMPLICIT_LOOKUP_IN_PARENT_PROJECTS")

This will affect the behavior of APIs and DSL constructs mentioned above. It will also affect the output of the properties task when invoked for subprojects.

Deprecation of Develocity plugin versions before 4.0

Earlier versions of the Develocity plugin rely on the implicit property lookup in parent projects, which is deprecated and will be removed in Gradle 10.

Upgrade to the latest Develocity plugin from the Gradle Plugin Portal. See the Develocity plugin compatibility matrix for details.

Deprecation of getProperties()

The following usages of getProperties() have been deprecated and will be removed in Gradle 10:

  • Project.getProperties() — calling getProperties() or accessing the properties property on a Project instance.

  • Calling getProperties() (or accessing the properties property) on a build script, settings script, or init script. This is a separate API on the script base class, but has the same problems.

This method, which historically existed for convenience, makes build logic harder to understand and navigate due to the untyped nature of the properties and the fact that they are sourced from many different locations and containers. Using this method also hurts performance of builds by eagerly resolving properties from the environment, reducing the potential for more frequent Configuration Cache hits. The hierarchical look-up of properties through the parent projects chain creates implicit coupling of the mutable state between projects. This impedes the potential of scalability in the future model, where projects are isolated from each other.

To access project properties, prefer providers.gradleProperty("name") which returns a type-safe Provider and is compatible with lazy configuration. Note that providers.gradleProperty() resolves properties at the build level — it does not include properties from gradle.properties files in subproject directories, nor extra properties or other properties set dynamically on individual projects. If you need to access extra properties, use ext properties directly.

The following table shows common migration scenarios for project properties:

Before (deprecated) After (recommended)

Accessing a single project property:

val value = project.properties["myProp"]
def value = project.properties.myProp

Wire the provider as a task property for better Configuration Cache support:

val myProp: Provider<String> =
    providers.gradleProperty("myProp")

If you need the value immediately:

def value = providers.gradleProperty("myProp")
    .orNull

Accessing a property with a default value (Kotlin, Java, or Groovy):

val value = project.properties
    .getOrDefault("myProp", "default")

Accessing a property with a default value (Groovy, idiomatic):

def value = project.properties["myProp"] ?: "default"

Use providers.gradleProperty() with getOrElse():

val value = providers
    .gradleProperty("myProp")
    .getOrElse("default")
def value = providers.gradleProperty("myProp")
    .getOrElse("default")

Kotlin property delegation:

val myProp: String by project.properties

Get the project property directly:

val myProp = project.property("myProp") as String

Forwarding properties as system properties:

test {
    systemProperties(
        project.properties.filter {
            it.key.startsWith("test.")
        }
    )
}

Use providers.gradlePropertiesPrefixedBy():

test {
    systemProperties(
        providers
            .gradlePropertiesPrefixedBy("test.")
            .get()
    )
}

Token replacement in resource files (common in Minecraft mods, Spring Boot, etc.):

processResources {
    filesMatching("plugin.yml") {
        expand project.properties
    }
}

Build an explicit map of only the properties you need:

processResources {
    filesMatching("plugin.yml") {
        expand(
            "version": project.version,
            "name": project.name
        )
    }
}
Deprecation of Kotlin DSL delegated properties

All Kotlin DSL property delegate extensions have been deprecated and will be removed in Gradle 10. This includes container delegates (by registering, by creating, by existing, by getting), property delegates (by project, by settings, by extra), and value delegates (Property<T> and ConfigurableFileCollection getValue/setValue operators).

Use the explicit API instead.

The container delegates (registering, creating, existing, getting) are defined on NamedDomainObjectContainer and NamedDomainObjectCollection. They apply to any container, not just tasks or configurations.

The examples below use a generic container placeholder; replace it with tasks, configurations, sourceSets, or whichever container you use.

Container delegates:

Deprecated delegate Replacement

val x by container.registering

val x = container.register("x")

val x by container.registering { }

val x = container.register("x") { }

val x by container.registering(Type::class)

val x = container.register<Type>("x")

val x by container.registering(Type::class) { }

val x = container.register<Type>("x") { }

val x by container.creating

val x = container.create("x")

val x by container.creating { }

val x = container.create("x") { }

val x by container.creating(Type::class)

val x = container.create<Type>("x")

val x by container.creating(Type::class) { }

val x = container.create<Type>("x") { }

val x by container.existing

val x = container.named("x")

val x by container.existing { }

val x = container.named("x") { }

val x by container.existing(Type::class)

val x = container.named<Type>("x")

val x by container.existing(Type::class) { }

val x = container.named<Type>("x") { }

val x by container.getting

val x = container.getByName("x")

val x by container.getting { }

val x = container.getByName("x") { }

val x by container.getting(Type::class)

val x = container.getByName<Type>("x")

val x by container.getting(Type::class) { }

val x = container.getByName<Type>("x") { }

val x: Type by container (lookup)

val x = container.getByName("x")

val x: Type by provider (NamedDomainObjectProvider<T> unwrap)

val x = provider.get()

When a container holds exactly one element of a given type, you can look it up by type instead of by name. This is useful for plugin-contributed tasks whose names are not part of a public contract:

// Eagerly resolves the single task of type MyTaskType, regardless of its name.
val myTask = rootProject.tasks.withType<MyTaskType>().single()
Note
withType(…​).single() is eager. The task must already exist when this line runs. For lazy configuration, use tasks.withType<MyTaskType>().configureEach { …​ } instead.

Property, extra, and value delegates:

Deprecated delegate Replacement

val p: String by project

val p = project.property("p")

val p: String? by project

val p = project.findProperty("p")

val p: String by settings (Gradle property)

val p = providers.gradleProperty("p").get()

val p: String? by settings (Gradle property, nullable)

val p = providers.gradleProperty("p").orNull

val p: String by settings (extra property)

val p = extra["p"] as String

val p: String? by settings (extra property, nullable)

val p = extra["p"] as String?

val v by extra("value") or val v by extra { value }

extra.set("v", value)

val v: String by extra

val v = extra["v"] as String

val v: String? by extra

val v = extra["v"] as String?

val x: String by myProperty (Property<T> read)

val x = myProperty.get()

x = "new" via var x: String by myProperty (Property<T> write)

myProperty.set("new")

val files by myFileCollection (ConfigurableFileCollection read)

val files = myFileCollection.getFiles()

files = …​ via var files by myFileCollection (ConfigurableFileCollection write)

myFileCollection.setFrom(…​)

val ext: Type by extensions

val ext = extensions.getByType<Type>()

Delegated properties are being removed because they create silent correctness bugs (e.g. no-op delegates, wrong extra property scope), couple entity names to variable names, diverge between build scripts and class-based plugins, and duplicate functionality already provided by explicit APIs or other DSL sugar.

Deprecation of accessing task dependency relationships from a task action

Calling the following methods from a task action at execution time is now deprecated and will become an error in Gradle 10:

These methods can still be used during configuration time.

The deprecation is only issued if the Configuration Cache is not enabled. When the Configuration Cache is enabled, calls to these methods are reported as Configuration Cache problems instead. This is another step towards moving users away from idioms that are incompatible with the Configuration Cache, which will become the only mode supported by Gradle in a future release. For example:

// Deprecated: inspecting dependencies from within the task action
tasks.register("report") {
    dependsOn("compileJava")
    doLast {
        taskDependencies.getDependencies(this).forEach { println(it.name) }
    }
}

// Recommended: capture what you need at configuration time
tasks.register("report") {
    dependsOn("compileJava")
    val dependencyNames = dependsOn.filterIsInstance<Task>().map { it.name }
    doLast {
        dependencyNames.forEach { println(it) }
    }
}

Please refer to the Configuration Cache documentation for alternatives that are compatible with the Configuration Cache.

Deprecation of accessing extensions from a task action

Calling Task.getExtensions() from a task action at execution time is now deprecated and will become an error in Gradle 10.

This method can still be used during configuration time.

The deprecation is only issued if the Configuration Cache is not enabled. When the Configuration Cache is enabled, calls to this method are reported as Configuration Cache problems instead. This is another step towards moving users away from idioms that are incompatible with the Configuration Cache, which will become the only mode supported by Gradle in a future release.

For example:

// Deprecated: reading the extension from within the task action
tasks.register("greet") {
    doLast {
        val ext = extensions.getByType<GreetingExtension>()
        println(ext.message.get())
    }
}

// Recommended: capture the value at configuration time
tasks.register("greet") {
    val message = extensions.getByType<GreetingExtension>().message
    doLast {
        println(message.get())
    }
}

Please refer to the Configuration Cache documentation for alternatives that are compatible with the configuration cache.

Deprecation of accessing injected Project or Gradle services from a task action

Reading an injected service of type Project or Gradle from a task action at execution time is now deprecated and will become an error in Gradle 10.

These services can still be read during configuration time.

The deprecation is only issued if the Configuration Cache is not enabled. When the Configuration Cache is enabled, such accesses are reported as Configuration Cache problems instead. This is another step towards moving users away from idioms that are incompatible with the Configuration Cache, which will become the only mode supported by Gradle in a future release.

For example:

// Deprecated: reading from the injected Project at execution time
abstract class PrintVersionTask : DefaultTask() {
    @get:Inject
    abstract val project: Project

    @TaskAction
    fun run() {
        println("Building ${project.name} version ${project.version}")
    }
}

// Recommended: declare the values you need as task inputs
abstract class PrintVersionTask : DefaultTask() {
    @get:Input
    abstract val projectName: Property<String>

    @get:Input
    abstract val projectVersion: Property<String>

    @TaskAction
    fun run() {
        println("Building ${projectName.get()} version ${projectVersion.get()}")
    }
}

tasks.register<PrintVersionTask>("printVersion") {
    projectName.set(project.name)
    projectVersion.set(project.version.toString())
}

Please refer to the Configuration Cache documentation for alternatives that are compatible with the Configuration Cache.

Deprecation of accessing Task in dependsOn closures

Using the Task argument provided to closures passed to the Task.dependsOn method has been deprecated. Starting in Gradle 10, closures passed to Task.dependsOn will no longer receive a Task argument.

The following code will not be permitted starting in Gradle 10:

def foo = tasks.register("foo")

// The below usages of dependsOn access the Task object provided as a parameter.
// This is deprecated behavior.
tasks.register("bar") {
    dependsOn { task ->
        task.getName()
        foo
    }
}
tasks.register("baz") {
    dependsOn {
        it.getName()
        foo
    }
}

// Build logic may continue to pass closures to dependsOn without accessing the provided Task.
tasks.register("okay") {
    dependsOn {
        foo
    }
}
Deprecation of undeclared Artifact Transform execution by tasks

Invoking an Artifact Transform from a task that does not declare the resolution as a task input is now deprecated and will become an error in Gradle 10.

With the Configuration Cache, if an Artifact Transform is not properly declared as an input, and requires a producer project that no other scheduled task requires, the producer may not be scheduled and may be unreachable. This can manifest as project <name> not found errors.

Now, a deprecation is emitted for each task that executes an Artifact Transform that is not declared as an input. This provides an early warning for build scenarios that were only working "by accident".

To fix this warning, declare the resolution triggering the Artifact Transform as a task input. You can do this by exposing a configurations' files collection through a typed @InputFiles property on a custom task type. If you must use an ad-hoc task, supply myCustomConfiguration.files to inputs.files(…​), or, when consuming an ArtifactCollection, supply ArtifactCollection.getArtifactFiles(). In the common case, both of these methods wire the transform steps into the execution plan so the task is recognized as a declarer of the transform.

Declaring a transformed FileCollection view as a task input:

val view = configurations["implementation"].incoming.artifactView {
    attributes.attribute(color, "green")
}.files

// Deprecated: the doLast reads view.files without declaring view as a task input.
tasks.register("consume") {
    doLast {
        println("result = ${view.files.map { it.name }}")
    }
}

// Recommended: expose the FileCollection through a typed @InputFiles property on a custom task type.
abstract class Consume : DefaultTask() {
    @get:InputFiles
    abstract val colored: ConfigurableFileCollection

    @TaskAction
    fun run() {
        println("result = ${colored.files.map { it.name }}")
    }
}

tasks.register<Consume>("consume") {
    colored.from(view)
}

Declaring a transformed ArtifactCollection view as a task input — note that declaring the raw Configuration (inputs.files(configurations["compile"])) is NOT enough, because it only schedules the source artifacts, not the transforms:

val artifacts = configurations["compile"].incoming.artifactView {
    attributes { attribute(usage, "transformed") }
}.artifacts

// Deprecated: declaring the raw configuration doesn't wire the transform.
tasks.register("show") {
    inputs.files(configurations["compile"])
    doLast {
        println("files: ${artifacts.map { it.file.name }}")
    }
}

// Recommended: declare the transformed artifact view's artifactFiles as the input.
tasks.register("show") {
    inputs.files(artifacts.artifactFiles)
    doLast {
        println("files: ${artifacts.map { it.file.name }}")
    }
}
Note

How declaration is detected

Gradle determines whether a task has declared a transform output as an input by walking the task’s transitive dependency subgraph in the execution plan after the work graph is fully built. Any transform step reachable from the task through dependency edges, without crossing into another task, is considered declared by that task. This covers chained transforms, @InputArtifactDependencies feeds across components, and componentFilter-restricted artifact views uniformly.

If you write inputs.files(view), inputs.files(artifacts.artifactFiles), or expose the collection through a typed @InputFiles property, Gradle’s input wiring places the corresponding transform nodes into the task’s dependency successors, and the post-graph walk will recognise the task as a declarer. Declaring through dependsOn(taskA) on another task that itself declares the input does NOT count: only direct declaration by the task that performs the query counts.

Deprecation of using Project objects as dependency notation

Passing a Project object directly as a dependency notation has been deprecated and will become an error in Gradle 10.

Previously, you could declare a project dependency by passing a Project instance directly:

build.gradle
def someProject = project(":some-project")
dependencies {
    implementation(someProject)  // Deprecated
}

Instead, use the project() method on DependencyHandler or the createProjectDependency() method on DependencyFactory:

build.gradle
dependencies {
    implementation(project(":some-project"))
}

Or, when using the DependencyFactory directly (e.g., from a plugin):

Configuration implementation = project.getConfigurations().getByName("implementation");
ProjectDependency dependency = project.getDependencyFactory().createProjectDependency(":some-project");
implementation.getDependencies().add(dependency);
Deprecation of artifactUrls on Maven repositories

The artifactUrls family of methods on MavenArtifactRepository is deprecated and will be removed in Gradle 10.

This includes:

  • MavenArtifactRepository.getArtifactUrls()

  • MavenArtifactRepository.artifactUrls(Object…​)

  • MavenArtifactRepository.setArtifactUrls(Set)

  • MavenArtifactRepository.setArtifactUrls(Iterable)

The same deprecation applies to passing artifactUrls as a key in the map argument to RepositoryHandler.mavenCentral(Map), since that path delegates to the methods above.

These methods let a single Maven repository declaration look for POMs at the base URL while looking for artifacts (such as JARs) at one or more additional URLs. This is a Gradle-specific extension with no equivalent in Maven, and there is no direct replacement.

Deprecation of RepositoryHandler.flatDir(Map) and RepositoryHandler.mavenCentral(Map)

The map-argument overloads RepositoryHandler.flatDir(Map) and RepositoryHandler.mavenCentral(Map) are deprecated and will be removed in Gradle 10. Use the action-based overloads (flatDir(Action), mavenCentral(Action)) instead — they are typed, work in both the Groovy and Kotlin DSLs, and are the canonical way to configure a repository.

For flatDir:

build.gradle
repositories {
    flatDir {
        name = "libs"
        dirs "libs1", "libs2"
    }
}
build.gradle.kts
repositories {
    flatDir {
        name = "libs"
        dirs("libs1", "libs2")
    }
}

For mavenCentral:

build.gradle
repositories {
    mavenCentral {
        name = "nonDefaultName"
    }
}
build.gradle.kts
repositories {
    mavenCentral {
        name = "nonDefaultName"
    }
}
Deprecation of buildNeeded and buildDependents tasks

The buildNeeded and buildDependents tasks from the Java plugins are deprecated and will be removed in Gradle 10.

buildNeeded builds the current project along with all projects it depends on. buildDependents does the opposite: it builds the current project and all projects that depend on it. These tasks were introduced back in Gradle 0.8. Their implementation is incompatible with Configure on Demand and Isolated Projects, and the tasks themselves are too rigid to cover the range of use cases users actually need. Existing Gradle functionality covers these use cases, so rather than rework the tasks, we are removing them.

buildNeeded depends on the current project’s build task and on the buildNeeded task of each project that appears as a direct dependency in the testRuntimeClasspath configuration. Because those buildNeeded tasks depend on each other, the result is that the entire project dependency closure is built.

Depending on what you were using buildNeeded for, one of the following may replace it:

  • If you only need to build or test a specific project, run that task directly (e.g., ./gradlew :proj:build) and let Gradle resolve its task dependencies automatically.

  • For running tests across all projects the current project depends on, use test report aggregation.

  • For plugin authors and advanced build logic, use Artifact Views to select artifacts from projects in a dependency graph and wire their producer tasks as dependencies.

There is no direct replacement for buildDependents. For most workflows, running all tasks with a given name (e.g., ./gradlew test) combined with up-to-date checks and build caching will skip work for unaffected projects, at the cost of configuring all projects.

Deprecation of Configuration.getTaskDependencyFromProjectDependency()

The Configuration.getTaskDependencyFromProjectDependency(boolean, String) method has been deprecated and will be removed in Gradle 10.

It exists to support the buildNeeded and buildDependents tasks deprecated above, and has no other intended use. Its implementation is fundamentally incompatible with Isolated Projects and Configure on Demand.

There is no direct replacement for this method. If you were using it to reach tasks across project boundaries, Artifact Views let you select artifacts from dependent projects and let Gradle wire the producer tasks through normal dependency resolution.

Deprecation of file generation tasks of IDE plugins

The idea and eclipse plugins contribute tasks for generating IDE specific files on disk. These tasks were originally the primary way to set up a project for an IDE. However, all modern IDEs now have built-in Gradle integration (IntelliJ IDEA’s Gradle import, Eclipse Buildship, etc.) and can import Gradle projects directly without needing these generated files.

We are deprecating these file generation tasks:

  • idea / ideaProject / ideaModule / ideaWorkspace (generate .ipr, .iml, .iws files)

  • eclipse / eclipseProject / eclipseClasspath / eclipseJdt / eclipseWtpComponent / eclipseWtpFacet (generate .project, .classpath, .settings/ files)

  • openIdea (generates files and opens IDEA)

  • associated clean* tasks

In addition to the tasks, certain model properties that only affect the generated files are also deprecated. These include idea.module.iml { …​ }, idea.project.ipr { …​ }, idea.workspace { …​ }, idea.targetVersion, idea.pathVariables, idea.module.pathVariables, idea.project.projectLibraries, eclipse.wtp.facet { …​ }, the file { …​ } merging hooks on EclipseJdt and eclipse.wtp.component, and the withXml { …​ } hook on all file { …​ } blocks. The beforeMerged { …​ } and whenMerged { …​ } hooks on the eclipse project and classpath models are not deprecated, as they also affect the model consumed by the IDE.

The rest of eclipse.wtp { …​ } is not deprecated: the eclipse.wtp.component { …​ } configuration feeds the WTP classpath attributes surfaced via the Tooling API, which Eclipse Buildship consumes.

The model properties that affect how IDEs understand your project (source directories, language levels, dependency scopes, etc.) are not deprecated. When you apply the plugins and configure the idea { …​ } or eclipse { …​ } blocks in your build scripts, you can customize how your IDE understands your project. These customizations are picked up automatically by the Gradle integration built into IDEs. This functionality will continue to work unchanged.

Note
Not all the functionality of the plugins is being deprecated, only the tasks and some model properties strictly related to them.
Deprecation of targetJdk on the PMD plugin

The targetJdk property on Pmd and PmdExtension, along with the TargetJdk enum, are deprecated and will be removed in Gradle 10.

This includes:

  • Pmd.getTargetJdk() / Pmd.setTargetJdk(TargetJdk)

  • PmdExtension.getTargetJdk() / PmdExtension.setTargetJdk(TargetJdk) / PmdExtension.setTargetJdk(Object)

  • The TargetJdk enum and its toVersion(Object) static method

  • PmdPlugin.getDefaultTargetJdk(JavaVersion)

These were used by PMD versions older than 5.0 to select the target Java language level via Ant’s targetjdk attribute. PMD 5.0 and later infer the language version from the configured rule sets, so this property has been a no-op for all supported PMD versions. Gradle does not support PMD versions earlier than PMD 5.1.0.

There is no replacement. Remove any targetJdk configuration from your build.

Deprecation of ProblemSpec.severity()

Setting problem severity explicitly when creating a new instance is now deprecated. Severity is instead determined by the reporting method: ProblemReporter.report() produces warnings and ProblemReporter.throwing() produces errors. Calling .severity() on ProblemSpec is now a no-op and will be removed in Gradle 10.

Upgrading from 9.4.0 and earlier

Potential breaking changes
Upgrade to Kotlin 2.3.20

The embedded Kotlin has been upgraded from 2.3.10 to Kotlin 2.3.20.

Plugins requested by precompiled settings plugins are now validated at compile time

Gradle now validates plugin requests in precompiled settings plugins (*.settings.gradle.kts) during compilation, failing the build if a requested plugin cannot be resolved. This matches the existing behavior for precompiled script plugins targeting Project.

Previously, if a precompiled settings plugin was never applied, invalid plugin requests in its plugins {} block went undetected until the plugin was used by a consuming build, at which point the build would fail unexpectedly.

buildSrc/src/main/kotlin/my-precompiled-plugin.settings.gradle.kts
plugins {
    id("com.example.invalid.plugin") // now fails at compile time if unresolvable
}

If you have precompiled settings plugins that are declared but not currently applied, check their plugins {} blocks and remove or fix any plugin requests that cannot be resolved from the dependencies available in the build being compiled.

The Windows start script has been reworked to improve usability

The default Windows start script template (used for gradlew.bat and application start scripts) has been reworked to improve usability and consistency with the Unix shell script.

If you use a custom start script template or invoke gradlew.bat from another batch file, review the changes below, as some may affect your build.

The new script includes the following changes:

  • The OS variable check has been removed, since Gradle no longer supports non-NT-based versions of Windows.

  • endlocal is now called before invoking the application, so environment variable changes made inside the script do not leak into the invoked process. Any environment variables needed by the application must be set before calling gradlew.bat.

  • & CALL is now used after invoking the application, which suppresses the "Terminate batch job (Y/N)?" prompt. If your scripts rely on this prompt for flow control, they will need to be updated.

  • exit /b has been replaced with "%COMSPEC%" /c exit, enabling the use of && and || operators when calling the script from another batch file.

Upgrading the wrapper to the new script may cause a one-time error if done before Gradle 8.14, because cmd.exe re-reads the script after it changes. To avoid this, first upgrade to Gradle 8.14 or 9.0.0, then upgrade to 9.5.0.

Calling .values() on environment variables or system properties is now tracked as Configuration Cache input

Starting with Gradle 9.5.0, calling .values() on the maps returned from System.getenv() or System.getProperties() now records all environment variables or system properties as Configuration Cache inputs. This is consistent with how .forEach() calls are already tracked.

As a result, if your build configuration uses System.getenv().values() or System.getProperties().values(), any change to any environment variable or system property will invalidate the Configuration Cache entry, not just the ones your build actually uses. If you experience unexpected cache invalidations after upgrading, check whether your build or any plugins call .values() on these maps, and consider switching to targeted property lookups instead.

Dependency lockfiles are generated with a platform-independent line ending

In the context of Dependency Locking, lockfiles now use Unix line endings (LF, \n) instead of the system default. Existing lockfiles using Windows line endings (CRLF, \r\n) can still be read, but newly generated lockfiles will always use LF.

When running a lock update with Gradle 9.5.0 or above, if your lockfiles were previously generated on Windows, all line endings will be updated to LF. This will appear as a large diff in version control even if no dependencies have changed. This is expected and only happens once.

Dependency verification armored key rings now render non-ASCII characters correctly

In the context of Dependency Verification, when a key has metadata that contains non-ASCII characters, it is now properly rendered in the key header.

When running a dependency verification key export with Gradle 9.5.0 or above, if your keyring already contained keys with non-ASCII metadata, the exported armored keyring will be updated to render those characters correctly. This is expected to happen only once.

The outgoingVariants and resolvableConfigurations reports now hide variants without attributes by default

The outgoingVariants and resolvableConfigurations report tasks now only show variants and configurations that have attributes defined, since only these can participate in variant-aware dependency resolution.

Previously, variants and configurations without attributes were included in the default output. They are now hidden unless the --all flag is passed.

When shown (via --all), attributeless entries are marked with (n) in the report to indicate they are not selectable via variant-aware resolution.

If you rely on seeing attributeless variants or configurations in these reports, add --all to your invocation:

./gradlew outgoingVariants --all
./gradlew resolvableConfigurations --all
Deprecations
Deprecation of setting an exit environment variable

CreateStartScripts.getExitEnvironmentVar() and CreateStartScripts.setExitEnvironmentVar(String) have been deprecated and will be removed in Gradle 10.0.0.

As of this release, these methods are no-ops and have no effect, even with custom start script templates. This is a result of the Windows start script template no longer using an exit environment variable.

If you are using a custom start script template that references the exit environment variable, update your template to remove this dependency. There is no replacement API, the exit environment variable concept has been removed entirely.

Upgrading from 9.3.1 and earlier

Potential breaking changes
ProjectBuilder now enforces consistent build-scoped locations

ProjectBuilder allows you to configure a specific project directory for tests. While project.projectDir and project.rootDir have always respected this setting, project.layout.settingsDirectory previously did not. This discrepancy could cause file resolution to inadvertently escape the project directory.

Gradle now anchors the settings search directly to the configured project directory. This ensures that layout.settingsDirectory, projectDir, and rootDir all point to the same consistent location. Projects created via ProjectBuilder are now better isolated from the host build environment.

If your tests specifically relied on layout.settingsDirectory pointing to an external location, they will need to be adjusted. Even if you do not use settingsDirectory directly, you may still observe changes in file resolution. Previously, dependency management files could be "leaked" into the test from the host build environment; this is no longer the case.

For more details see the related issue.

The java-gradle-plugin plugin now adds the gradleApi() dependency to the compileOnlyApi scope

The gradleApi() dependency is now added to the compileOnlyApi scope instead of the api scope. This prevents the gradleApi() from leaking into the runtime classpath of other dependents of the plugin project.

This might break projects that were implicitly relying on the gradleApi() being on the compilation or runtime classpath. If this is the case, add a gradleApi() dependency to the appropriate scope to restore the previous behavior.

For most plugin projects, this change should be transparent as:

  • at compilation time, the plugin project will get the gradleApi()

  • the default test source set will also automatically get the gradleApi() dependency on the compilation and runtime classpaths

If any additional test source set is used (e.g., integration tests), the plugin extension offers the plugins.testSourceSet method to register the source set for automatic management. If this cannot be done, a regular declaration of the gradleApi() dependency on the test source can be used as well.

Stricter validation for published plugins

For plugin builds that apply any of the com.gradle.plugin-publish, ivy-publish, or maven-publish plugins, Gradle now automatically enables stricter validation of plugin code.

In order not to break your builds, this does not apply to local plugins (in buildSrc or included builds containing build logic). However, we encourage you to always enable stricter validation:

build.gradle.kts
tasks.validatePlugins {
    enableStricterValidation = true
}
CodeNarc compilation classpath is set by default

The CodeNarc plugin now automatically populates the compilationClasspath of a CodeNarc task with the compile classpath of its associated source set.

CodeNarc offers Enhanced Classpath Rules (like UnusedImport or DuplicateImport) that require the project’s compiled classes and dependencies to be analyzed for full accuracy. Previously, you had to wire this up manually. Now, it works out of the box.

This change introduces a task dependency. The CodeNarc task must now wait for the compile task to finish so it can access the compiled classes.

If you do not use enhanced rules and want to restore parallel execution, you can manually empty the compilationClasspath:

build.gradle.kts
plugins {
    id("groovy")
    id("codenarc")
}

tasks.withType<CodeNarc>().configureEach {
    // Override the default compilation classpath
    compilationClasspath = files()
}

If your build relies on a custom configuration for the compilationClasspath of a CodeNarc task, you will need to continue explicitly setting it to override the new default behavior.

System property priority for Wrapper execution has changed

The priority of system properties passed to the Gradle Wrapper now correctly follows the documented order of precedence. In previous versions, properties defined in gradle.properties files could unexpectedly override those passed via the command line.

If a property is defined in multiple locations, Gradle now strictly honors the following hierarchy (from highest to lowest):

Priority Source Example

1 (Highest)

Command Line Option

./gradlew build -Dproperty.name=value

2

Gradle User Home

~/.gradle/gradle.properties

3 (Lowest)

Project Directory

[project-root]/gradle.properties

For more details see the related issue.

Upgrade to Kotlin 2.3.0

The embedded Kotlin has been upgraded from 2.2.21 to Kotlin 2.3.0.

Upgrade to Zinc 1.12.0

Zinc has been updated to 1.12.0.

Deprecations
Deprecation of DomainObjectCollection.findAll(Closure)

The findAll(Closure) method on Gradle collections is now deprecated and scheduled for removal in Gradle 10.0.0.

This method relies specifically on Groovy types and eagerly evaluates the contents of the container.

To fix this, use the similar DomainObjectCollection.matching(Spec). While not a direct replacement for findAll, matching is lazy, it returns a new collection that only filters elements as they are actually needed by the build:

// Deprecated:
def checkTasks = tasks.findAll { it.name.startsWith("check") }

// Recommended:
def checkTasks = tasks.matching { it.name.startsWith("check") }
Deprecation of methods taking Closure on Test tasks

The following APIs are deprecated and will be removed in Gradle 10.0.0:

  • AbstractTestTask.onOutput(Closure) can be replaced with AbstractTestTask.addTestOutputListener(TestOutputListener)

  • AbstractTestTask.beforeTest(Closure) can be replaced with AbstractTestTask.addTestListener(TestListener)

  • AbstractTestTask.afterTest(Closure) can be replaced with AbstractTestTask.addTestListener(TestListener)

  • AbstractTestTask.beforeSuite(Closure) can be replaced with AbstractTestTask.addTestListener(TestListener)

  • AbstractTestTask.beforeSuite(Closure) can be replaced with AbstractTestTask.addTestListener(TestListener)

  • Test.testFramework(Closure) can be replaced with Test.options(Action)

Deprecation of apply false in precompiled script plugins

The use of apply false within precompiled script plugins is now deprecated and will result in an error in Gradle 10.0.0.

In a precompiled script, the plugins {} block behaves differently than in a standard build script. Currently, if you write apply false, Gradle applies the plugin anyway. This creates confusion because the syntax suggests you are merely adding a plugin to the classpath without activating it, which is not what actually happens.

The fix depends on whether you actually want the plugin to be active in your precompiled script:

  • If you want to use the plugin: Remove the apply false statement.

  • If you do NOT want to use the plugin: Delete the line entirely.

my-plugin.gradle.kts
plugins {
    // Deprecated (and misleading, as it is still applied)
    id("org.gradle.test-retry") apply false
    // The plugin will still be on the classpath,
    // but it will not be applied as part of this precompiled script plugin.

    // Recommended: Either remove 'apply false' to keep using it,
    // or delete the line to stop using it.
    id("org.gradle.test-retry")
}
Deprecation of version in precompiled Settings script plugins

The use of version within precompiled Settings script plugins is now deprecated and will become an error in Gradle 10.0.0.

In precompiled scripts, version has no effect. The plugin version is already fixed by the script’s own build file; declaring it again inside the script is ignored and causes confusion about which version is actually in use.

To fix this, remove the .version() or version "…​" call from the plugins {} block:

my-plugin.settings.gradle.kts
plugins {
    // Deprecated:
    id("org.gradle.test-retry") version "x.y.z"
    // Recommended:
    id("org.gradle.test-retry")
}
Deprecation of Dependencies.getProject() method

The getProject() method on Dependencies has been deprecated and will be removed in Gradle 10.0.0.

Dependencies is used to configure dependencies in test suites. Previously, getProject() was used by Gradle internally to reference the current project. While never intended for public use, it could be used unintentionally in build scripts.

For example, the following configuration accidentally invokes the deprecated method because project.path resolves against the Dependencies object rather than the top-level Project object:

testing {
    suites {
        val integrationTest by registering(JvmTestSuite::class) {
            dependencies {
                implementation(project(project.path)) // `project.path` accesses `Dependencies.getProject()`
            }
        }
    }
}

The configuration above may now fail to compile with an error in 9.5.0 if -Werror is set.

To depend on the current project, use the project() method without arguments. This syntax is the idiomatic way to reference the project in both test suites and standard dependencies {} blocks:

testing {
    suites {
        val integrationTest by registering(JvmTestSuite::class) {
            dependencies {
                implementation(project())
            }
        }
    }
}

Upgrading from 9.2.0 and earlier

Potential breaking changes
Referential equality is not guaranteed for Project instances

Instances of Project type representing the same logical Gradle project do not provide any guarantee of being equal by reference (== in Java, === in Kotlin). However, historically it was possible to observe that such Project instances were exactly the same.

In Gradle 9.3.0, Project instances (for the same logical project) can be different with respect to the referential equality, even if obtained within the same context, e.g., in the same build script. This change is necessary to facilitate future performance improvements of Gradle. The Project.equals() equality behavior remains unchanged.

Avoid referential equality in Kotlin:

build.gradle.kts
// DON'T do this
project.rootProject === project.parent

Avoid referential equality in Groovy:

build.gradle
// DON'T do this
project.rootProject.is(project.parent)

Avoid referential equality in Java:

MyPlugin.java

// DON'T do this
project.getRootProject() == project.getParent();

In general, it is better to check project equality via Project.getPath() or Project.getBuildTreePath() for composite-build support. The paths are also better suited to be keys in data structures, like maps.

TestNG output may change when using versions before 6.9.13.3

As part of the AbstractTestTask refactoring, Gradle’s integration with TestNG has been updated. Gradle now relies on a correctly functioning IClassListener to report the hierarchy of test classes and methods.

When using TestNG versions earlier than 6.9.13.3, this can lead to different or degraded output:

  • With older versions, class information may be lost. For example, output that previously looked like: org.gradle > TestClass > ok may now be reported simply as: ok.

  • For TestNG versions from 6.9.10 up to (but not including) 6.9.13.3, the IClassListener API exists but is broken. This can result in even worse output, such as empty or missing names.

To get correct and stable output, we recommend upgrading to TestNG 6.9.13.3 or newer.

Upgrade to Kotlin 2.2.21

The embedded Kotlin has been upgraded from 2.2.20 to 2.2.21.

Upgrade to Jansi 2.4.2

Jansi was upgraded from 1.18 to 2.4.2 to pick up support for Windows ARM64.

Upgrade to ASM 9.9

ASM was upgraded from 9.8 to 9.9 to ensure earlier compatibility for Java 26.

Upgrade to Groovy 4.0.29

Groovy has been updated to 4.0.29.

Upgrade to JaCoCo 0.8.14

JaCoCo has been updated to 0.8.14.

Deprecations
Deprecation of the Wrapper.getAvailableDistributionTypes() method

The method on the Wrapper task has been deprecated and will be removed in Gradle 10.

Use Wrapper.DistributionType.values() to obtain the available distribution types instead.

Deprecation of publishing dependencies on unpublished projects

When publishing a project, Gradle resolves project dependencies to the coordinates of the target project’s publication. If the target project has no publication, Gradle currently resolves the dependency silently using that project’s group, name, and version.

Starting with Gradle 10, this behavior is deprecated. Gradle will no longer silently ignore the absence of a publication. Publishing a project that depends on another project without a publication will be forbidden and will cause the build to fail. This change prevents publishing broken metadata with dependency coordinates that cannot be resolved.

The example below demonstrates a build that triggers the deprecated behavior:

build.gradle.kts
plugins {
    id("java-library")
    id("maven-publish")
}

group = "com.example"
version = "1.0.0"

dependencies {
    api(project(":other"))
}

publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["java"])
        }
    }
}
other/build.gradle.kts
plugins {
    id("java-library")
}

group = "com.example"
version = "1.0.0"

To avoid this deprecation, ensure that all project dependencies of published projects are also published. In the example above, applying the maven-publish plugin and configuring a publication in the :other project resolves the issue:

other/build.gradle.kts
plugins {
    id("java-library")
    id("maven-publish")
}

group = "com.example"
version = "1.0.0"

publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["java"])
        }
    }
}
Deprecation of legacy Usage attribute values

Since Gradle 5.6, the Usage attribute has been split into an additional LibraryElements attribute. In the JVM ecosystem, Usage indicates whether a variant is intended for compilation or runtime, while LibraryElements specifies the format of the artifact (for example, a JAR file or a classes directory).

To ease migration, Gradle has automatically mapped legacy Usage values to their corresponding Usage and LibraryElements pairs:

Legacy Usage

Replaced Usage

Replaced LibraryElements

java-api-jars

java-api

jar

java-api-classes

java-api

classes

java-runtime-jars

java-runtime

jar

java-runtime-classes

java-runtime

classes

java-runtime-resources

java-runtime

resources

Starting with Gradle 10, this automatic mapping will no longer occur when legacy Usage values are added directly to an AttributeContainer in build logic. To maintain backward compatibility for already published modules, Gradle will continue translating legacy Usage values found in published Gradle Module Metadata.

Deprecation of using module coordinates to depend on the current project

Starting with Gradle 10, declaring a dependency on the current project using module coordinates (group, name, version) will no longer resolve to that project. Instead, Gradle will attempt to resolve that dependency from a repository.

The example below demonstrates the change in behavior:

my-project/build.gradle.kts
group = "com.example"
version = "1.0.0"

val deps = configurations.dependencyScope("deps")
val classpath = configurations.resolvable("classpath") {
    extendsFrom(deps.get())
    attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
}

val elements = configurations.consumable("elements") {
    attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
}

dependencies {
    // In Gradle 9.x, this dependency resolves to the `elements` configuration.
    // In Gradle 10, this dependency will attempt to resolve from a repository.
    deps("com.example:my-project:1.0.0")
}

To continue depending on the current project, use a project dependency:

my-project/build.gradle.kts
dependencies {
    // Declare a dependency on the current project to continue resolving
    // to the current project.
    deps(project)
}
Deprecation of ModuleVersionSelector to ModuleComponentSelector conversion

The conversion of ModuleVersionSelector to ModuleComponentSelector (used in ResolutionStrategy.force(…​), DependencyResolveDetails.useTarget(Object), and PluginResolveDetails.useModule(Object) ) has been deprecated and will be removed in Gradle 10.0.0.

Typically, ModuleVersionSelector instances are DependencyContstraint objects.

Note that this deprecation does not apply to ExternalDependency objects, despite them implementing ModuleVersionSelector.

To fix this deprecation, pass one of the supported notations (for example, a String in the group:name:version format).

Upgrading from 9.1.0 and earlier

Potential breaking changes
Upgrade to Kotlin 2.2.20

The embedded Kotlin has been upgraded from 2.2.0 to Kotlin 2.2.20.

Removed incubating ObjectFactory#dependencyCollector() method

The incubating ObjectFactory#dependencyCollector() method has been removed. You can still create DependencyCollectors within Gradle managed types.

Consumable configurations in bundled plugins are now initialized lazily

Consumable configurations created by bundled Gradle plugins are now initialized only when needed. Configure actions on these configurations no longer run at configuration time by default. They only execute if the configuration is published, consumed as a variant, or otherwise realized by build logic.

For example:

build.gradle.kts
plugins {
    id("java-library")
}

configurations.named("apiElements").configure {
    println("Configuring apiElements")
}

With this change, the Configuring apiElements line is no longer printed during configuration time unless apiElements is actually realized.

See Declaring Configurations for more guidance.

ValidatePlugins now has stricter Java version requirements

The ValidatePlugins task must now run on a Java version that is supported by the Gradle daemon. This change was made because the task depends on several core Gradle services, which may now be compiled to the same bytecode version supported by the daemon.

By default, the task’s convention has been updated:

  • If your project’s toolchain is compatible, ValidatePlugins will use it.

  • Otherwise, it will fall back to the Java version used to run Gradle.

If you explicitly set a toolchain like this:

build.gradle.kts
tasks.withType<ValidatePlugins>().configureEach {
    javaLauncher.set(
        project.javaToolchains.launcherFor {
            languageVersion.set(JavaLanguageVersion.of(17))
        }
    )
}
build.gradle
tasks.withType(ValidatePlugins).configureEach {
    javaLauncher.set(
        project.javaToolchains.launcherFor {
            languageVersion.set(JavaLanguageVersion.of(17))
        }
    )
}

If the specified Java version is not compatible with the Gradle daemon, you must update it to a compatible version.

Deprecations
Deprecation of Project.container(…​) methods

The Project.container(…​) methods are deprecated and will be removed in Gradle 10. These methods manually create named domain object containers.

Use a managed property to let Gradle instantiate containers automatically. If a managed property isn’t possible, use ObjectFactory.domainObjectContainer(…​) (available since Gradle 5.5). Unlike Project.container(Class), the ObjectFactory version decorates container elements and makes them extension aware.

Deprecation of ruleSource-based dependency management APIs

The RuleSource-based dependency management APIs have been deprecated and will be removed in Gradle 10.0.0.

Deprecated APIs include:

Use the alternative methods that accept a ComponentMetadataRule class or an Action.

Deprecation of calling registerFeature without applying the Java plugin

Creating a JVM feature with JavaPluginExtension#registerFeature before applying the Java plugin has been deprecated and will become an error in Gradle 10.0.0.

Ensure the Java plugin is applied before invoking registerFeature. The following bundled plugins apply the Java plugin automatically:

  • java-library

  • application

  • groovy

  • scala

  • war

Upgrading from 9.0.0 and earlier

Potential breaking changes
Upgrade to ASM 9.8

ASM was upgraded from 9.7.1 to 9.8 to ensure earlier compatibility for Java 25.

Upgrade to Groovy 4.0.28

Groovy has been updated to Groovy 4.0.28.

Deprecations
Deprecation of multi-string dependency notation

In an effort to simplify and standardize the Gradle API, the multi-string dependency notation used in dependency management has been deprecated and will no longer be permitted in Gradle 10. Gradle will primarily accept dependency declarations in the form of a single string, with each dependency coordinate separated by a colon.

Below are examples of the deprecated multi-string notation:

build.gradle.kts
dependencies {
    implementation(group = "org", name = "foo", version = "1.0")
    implementation(group = "org", name = "foo", version = "1.0", configuration = "conf")
    implementation(group = "org", name = "foo", version = "1.0", classifier = "classifier")
    implementation(group = "org", name = "foo", version = "1.0", ext = "ext")
}

testing.suites.named<JvmTestSuite>("test") {
    dependencies {
        implementation(module(group = "org", name = "foo", version = "1.0"))
    }
}
build.gradle
dependencies {
    implementation(group: 'org', name: 'foo', version: '1.0')
    implementation(group: 'org', name: 'foo', version: '1.0', configuration: 'conf')
    implementation(group: 'org', name: 'foo', version: '1.0', classifier: 'classifier')
    implementation(group: 'org', name: 'foo', version: '1.0', ext: 'ext')
}

testing.suites.test {
    dependencies {
        implementation(module(group: 'org', name: 'foo', version: '1.0'))
    }
}

These declarations should be replaced with the single-string notation:

build.gradle.kts
dependencies {
    implementation("org:foo:1.0")
    implementation("org:foo:1.0") {
        targetConfiguration = "conf"
    }
    implementation("org:foo:1.0:classifier")
    implementation("org:foo:1.0@ext")
}

testing.suites.named<JvmTestSuite>("test") {
    dependencies {
        implementation("org:foo:1.0")
    }
}
build.gradle
dependencies {
    implementation("org:foo:1.0")
    implementation("org:foo:1.0") {
        targetConfiguration = "conf"
    }
    implementation("org:foo:1.0:classifier")
    implementation("org:foo:1.0@ext")
}

testing.suites.test {
    dependencies {
        implementation("org:foo:1.0")
    }
}

In some cases, a complete single-string notation may not be known up front. Instead of concatenating the coordinates into a new string, it is possible to use a DependencyFactory to create Dependency instances directly from the individual components:

build.gradle.kts
val group = "org"
val artifactId = "foo"
val version = "1.0"

configurations.dependencyScope("implementation") {
    dependencies.add(project.dependencyFactory.create(group, artifactId, version))
}
build.gradle
def group = "org"
def artifactId = "foo"
def version = "1.0"

configurations.dependencyScope("implementation") {
    dependencies.add(project.dependencyFactory.create(group, artifactId, version))
}
Deprecation of ReportingExtension.file(String)

The file() method on ReportingExtension has been deprecated and will be removed in Gradle 10.0.0.

Instead, use ReportingExtension.getBaseDirectory() with file(String) or dir(String).

Deprecation of ReportingExtension.getApiDocTitle()

The getApiDocTitle() method on ReportingExtension has been deprecated and will be removed in Gradle 10.0.0.

There is no direct replacement for this method.

Deprecation of JavaForkOptions.setAllJvmArgs()

The setAllJvmArgs() method on JavaForkOptions and, by inheritance, on JavaExecSpec has been deprecated and will be removed in Gradle 10.0.0.

Instead, to overwrite existing JVM arguments, use:

  • JavaForkOptions.jvmArgs()

  • JavaForkOptions.setJvmArgs()

  • Provide a CommandLineArgumentProvider to add arguments via JavaForkOptions.getJvmArgumentProviders()

Note that setAllJvmArgs() method on JavaForkOptions cleared all fork options before setting jvmArgs. The properties cleared included:

  • System properties configured via JavaForkOptions.systemProperties

  • JVM argument providers configured via JavaForkOptions.jvmArgumentProviders

  • Argument providers configured via JavaExecSpec.argumentProviders

  • Memory settings configured via JavaForkOptions.minHeapSize and JavaForkOptions.maxHeapSize

  • All other JVM arguments configured via JavaForkOptions.jvmArgs

  • The assertion and debug flags configured via JavaForkOptions.enableAssertions and JavaForkOptions.debug

If the arguments you provide to setJvmArgs() or jvmArgs() depend on any of the above properties being cleared, you will need to manually clear them.

Consider the following snippets for examples of how to implement this change:

build.gradle.kts
plugins {
    id("java")
}

tasks.register<JavaExec>("myRunTask") {
    jvmArgumentProviders.clear() // Clear existing JVM argument providers
    maxHeapSize = null // Clear max heap size
    jvmArgs = listOf("-Dfoo", "-Dbar") // Set new JVM arguments
}
build.gradle
plugins {
    id("java")
}

tasks.named('myRunTask', JavaExec) {
    jvmArgumentProviders.clear() // Clear existing JVM argument providers
    maxHeapSize = null // Clear max heap size
    jvmArgs = ["-Dfoo", "-Dbar"] // Set new JVM arguments
}
Deprecation of archives configuration

The archives configuration added by the base plugin has been deprecated and will be removed in Gradle 10.0.0. Adding artifacts to the archives configuration will now result in a deprecation warning.

If you want the artifact to be built when running the assemble task, add the artifact (or the task that produces it) as a dependency on assemble:

build.gradle.kts
val specialJar = tasks.register<Jar>("specialJar") {
    archiveBaseName.set("special")
    from("build/special")
}

tasks.named("assemble") {
    dependsOn(specialJar)
}
Deprecation of the Configuration.visible property

Prior to Gradle 9.0.0, any configuration with isVisible() returning true would implicitly trigger artifact creation when running the assemble task. This behavior was removed in Gradle 9.0.0, and the Configuration.visible property no longer has any effect. The property is now deprecated and will be removed in Gradle 10.0.0. You can safely remove any usage of visible.

If you want the artifacts of a configuration to be built when running the assemble task, add an explicit task dependency on assemble:

build.gradle.kts
val specialJar = tasks.register<Jar>("specialJar") {
    archiveBaseName.set("special")
    from("build/special")
}

configurations {
    consumable("special") {
        outgoing.artifact(specialJar)
    }
}

tasks.named("assemble") {
    dependsOn(specialJar)
}
Deprecation of non-string projectProperties in GradleBuild task

The GradleBuild task now deprecates using non-String values in startParameter.projectProperties. While the type is declared as Map<String, String>, there was no strict enforcement, allowing non-String values to be set. This deprecated behavior will be removed in Gradle 10.0.0.

If you are using non-String values in project properties, convert them to String representation:

build.gradle.kts
val myIntProp = 42

tasks.register<GradleBuild>("nestedBuild") {
    startParameter.projectProperties.put("myIntProp", "$myIntProp") // Convert int to String
}
build.gradle
def myIntProp = 42

tasks.register('nestedBuild', GradleBuild) {
    startParameter.projectProperties.put('myIntProp', "$myIntProp") // Convert int to String
}
Deprecation of project properties for toolchain configuration

In previous versions of Gradle, you could configure toolchains using project properties on the command line with the -P flag. For example, to disable toolchain auto-detection, you could use -Porg.gradle.java.installations.auto-detect=false. This behavior is deprecated and will be removed in Gradle 10.0.0. Instead, you should specify these settings as Gradle properties using the -D flag:

-Dorg.gradle.java.installations.auto-detect=false

Upgrading to Gradle 9.0.0

This chapter provides the information you need to migrate your Gradle 8.14.5 builds to Gradle 9.0.0. For migrating within Gradle 8.x, see the older migration guide first.

We recommend the following steps for all users:

  1. Try running gradle help --scan and view the deprecations view of the generated Build Scan.

    Deprecations View in a Build Scan

    This lets you see any deprecation warnings that apply to your build.

    Alternatively, you can run gradle help --warning-mode=all to see the deprecations in the console, though it may not report as much detailed information.

  2. Update your plugins.

    Some plugins may break with a new major version of Gradle, as these releases can remove public APIs. Using the latest version of a plugin increases the likelihood that it is already compatible with the new major Gradle version.

  3. Run gradle :wrapper --gradle-version 9.0.0 to update the project to 9.0.0.

  4. Try to run the project and debug any errors using the Troubleshooting Guide.

Runtime requirements and DSL changes

Java Virtual Machine (JVM) 17 or higher is required

Gradle 9.0.0 requires a Java Virtual Machine (JVM) version 17 or higher to run the Gradle Daemon. This is a breaking change from previous versions, which supported JVM 8 and higher.

Your build can still target lower JVM versions using Toolchains for compilation, testing and other workers (Checkstyle, Javadoc, etc).

The Gradle wrapper and command-line launcher can run with JVM 8, but it still requires a newer JVM to start the build. For more, see the Running Gradle on older JVMs section below.

Gradle Tooling API and TestKit remain compatible with JVM 8 and higher.

Upgrade to Kotlin 2.2.0

Gradle now embeds Kotlin 2.2.0, upgrading from the previously embedded version 2.0.21.

For full details and potential breaking changes, consult the Kotlin release notes.

Kotlin DSL and plugins use the Kotlin language version 2.2

The Kotlin DSL has been upgraded to the latest stable Kotlin 2.2.x runtime and uses Kotlin language version 2.2 across the entire toolchain. This marks a shift from Gradle 8.x, which embedded Kotlin 2.0 starting in 8.11 but continued using Kotlin language version 1.8 for compatibility.

This change impacts not only Kotlin DSL scripts (.gradle.kts) but also build logic and plugins written in Kotlin (both classic and convention plugins). Users should review their code for compatibility with Kotlin 2.2, as both Kotlin 2.0 and Kotlin 2.1 introduced several breaking language changes.

Due to changes in how the Kotlin 2 compiler handles script compilation, you can no longer refer to the script instance using labels like this@Build_gradle, this@Settings_gradle, or this@Init_gradle. If they are used to reference the DSL script target object, then use project, settings, or gradle instead. If they are used to reference a top-level symbol that happens to have the same name as a nested symbol, then use a different name, possibly by adding an intermediary variable.

This upgrade also includes an important change for build-logic and plugin authors: support for Kotlin language versions from 1.4 to 1.7 has been removed.

Read the JSpecify section below to learn about potential breaking changes in Kotlin build logic code related to nullability.

Upgrade to Groovy 4.0.27

Groovy has been upgraded from version 3.0.24 to 4.0.27. The update to Groovy 4 comes with many breaking changes, such as the removal of legacy packages, changes in the module structure, a parser rewrite, and bytecode output changes. For a complete overview of Groovy language changes from 3.x to 4.x, see the Groovy 4.0 release notes. These changes may affect those who use Groovy directly or indirectly in Gradle, and in rare cases, users relying on transitive dependencies.

Tip
Popular Groovy classes that used to be in packages like groovy.util are now in different packages to account for the JPMS "split package requirement". See this specific entry.
is-prefixed Boolean properties no longer recognized by Groovy

Groovy 4 no longer treats getters with an is prefix and a Boolean return type as properties. Gradle still recognizes these as properties for now, but this behavior will change in Gradle 10 to align with Groovy 4. See the deprecation notice for more details.

DELEGATE_FIRST closures may now prefer the delegate in some cases

Groovy 4 has changed the behavior of closures using the DELEGATE_FIRST strategy. Dynamic lookups for properties and methods will now prefer the delegate over the owner. This can result in different behavior when using closures in Gradle scripts, such as certain methods not being found or (e.g. with .with { }) some dynamic properties taking precedence over outer properties or methods. Generally, this should not affect Gradle scripts, as Gradle does not use DELEGATE_FIRST closures with dynamic properties in its API.

Workarounds include using @CompileStatic to avoid dynamic lookups, or explicitly qualifying calls with owner., this., or super. as needed.

For full clarity, in Groovy 3, the lookup order was:

  1. Delegate’s invokeMethod, which chooses known methods and does no dynamic lookup.

  2. Owner’s invokeMethod, which chooses known methods and does no dynamic lookup.

  3. Delegate’s invokeMissingMethod, which does dynamic lookup including via properties.

  4. Owner’s invokeMissingMethod, which does dynamic lookup including via properties.

In Groovy 4, the lookup order is:

  1. Delegate’s invokeMethod, which chooses known methods and does no dynamic lookup.

  2. Delegate’s invokeMissingMethod, which does dynamic lookup including via properties.

  3. Owner’s invokeMethod, which chooses known methods and does no dynamic lookup.

  4. Owner’s invokeMissingMethod, which does dynamic lookup including via properties.

Private properties and methods may be inaccessible in closures

Closures defined in a parent class that reference its private properties or methods may no longer have access to them in subclasses. This is due to a Groovy bug that will not be resolved until Groovy 5. This applies to both buildscripts and plugins written in Groovy.

As a workaround, apply @CompileStatic to the class to remove the dynamic lookup.

Super methods may be inaccessible from Groovy 3.x code

Groovy 4 changed how super method calls are resolved at runtime. As a result, code compiled with Groovy 3.x may be unable to access super methods, as the code does not contain the appropriate runtime code to locate them. This only applies to plugins written in Groovy 3.x, from Gradle 8.x and earlier.

Plugin changes

Plugins written with the Kotlin DSL require Gradle >= 8.11

When building and publishing plugins using the Kotlin DSL on Gradle 9.x.x, those plugins will only be usable on Gradle 8.11 or newer. This is because Gradle 8.11 is the first release that embeds Kotlin 2.0 or higher, which is required to interpret Kotlin metadata version 2.

If you want your plugin to remain compatible with older Gradle versions, you must explicitly compile it against an earlier Kotlin version (1.x).

For example, to support Gradle 6.8 and newer, configure your plugin to target Kotlin 1.7 like this:

build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    `kotlin-dsl`
}

tasks.withType<KotlinCompile>().configureEach {
    compilerOptions {
        languageVersion = KotlinVersion.KOTLIN_1_7
        apiVersion = KotlinVersion.KOTLIN_1_7
    }
}

Refer to Gradle’s compatibility matrix for details on which Kotlin version is embedded in each Gradle release.

Note
Plugins written using the Kotlin DSL and published with Gradle 7.x or 8.x remain compatible with Gradle 6.8 and newer.
Plugins written with the Groovy DSL require Gradle >= 7.0

Plugins authored using the Groovy DSL and built with Gradle 9.x.x require Gradle 7.0 or newer to run. This is because Gradle 7.0 introduced Groovy 3.0 support, and Gradle 9 embeds Groovy 4.0.

Since Gradle 9.0.0 uses Groovy 4.0 internally, plugins built with it may not behave as expected when run on older Gradle versions. For best compatibility, such plugins should be used with Gradle 9.0.0 or later.

Note
Plugins written with the Groovy DSL and published using Gradle 7.x or 8.x remain compatible with Gradle 5.0 and above.
Lowest supported Kotlin Gradle Plugin version change

Starting with Gradle 9.0.0, the minimum supported Kotlin Gradle Plugin version is 2.0.0. Earlier versions are no longer supported as they rely on Gradle APIs that have been removed.

For Gradle 8.x, the minimum supported version was 1.6.10.

Lowest supported Android Gradle Plugin version change

Starting with Gradle 9.0.0, the minimum supported Android Gradle Plugin version is 8.4.0. Earlier versions are no longer supported as they rely on Gradle APIs that have been removed.

For Gradle 8.x, the minimum supported version was 7.3.0.

Lowest supported Gradle Enterprise Plugin version change

Starting with Gradle 9.0.0, the minimum supported Gradle Enterprise Plugin version is 3.13.1. Earlier versions are no longer supported as they rely on Gradle APIs that have been removed.

Consider upgrading to the latest version of the Gradle Enterprise Plugin, or better yet, upgrade to the latest version of the Develocity Plugin.

For Gradle 8.x, the minimum supported version was 3.0.

C++ and Swift plugins no longer depend on software model based plugins

C++ Application Plugin, C++ Library Plugin, Swift Application Plugin, and Swift Library Plugin have been updated and no longer rely on the software model plugin infrastructure.

As a result, toolChains should be configured directly at the top-level of your build script instead of within a model { } block.

For example, instead of:

build.gradle
plugins {
    id("cpp-library")
}

model {
    toolChains {
        clang(Clang) {
            eachPlatform {
                cCompiler.executable = "clang"
                cppCompiler.executable = "clang++"
            }
        }
    }
}

This should become:

build.gradle
plugins {
    id("cpp-library")
}

toolChains {
    clang(Clang) {
        eachPlatform {
            cCompiler.executable = "clang"
            cppCompiler.executable = "clang++"
        }
    }
}

This change needs to be made in builds using the new C++ or Swift plugins or the existing software model based plugins.

Scala plugins no longer create unresolvable configurations

Previously, the Scala plugins used configurations named incrementalScalaAnalysisFor to resolve incremental analysis information between projects. However, these configurations were unresolvable and could lead to errors in the dependencies report.

As of Gradle 9.0.0, these configurations are no longer created or used by the Scala plugins.

Settings file changes

Project directories must exist and be writeable

Gradle will fail if a project is included that does not correspond to an existing directory on the file system, or if the directory exists but is read-only.

For example, you may see an error like:

* What went wrong:
Configuring project ':subproject1' without an existing directory is not allowed. The configured projectDirectory '.../subproject1' does not exist, can't be written to or is not a directory.

* Try:
> Make sure the project directory exists and is writable.

Task changes

ValidatePlugins task now requires Java Toolchains

In Gradle 9.0.0, using the ValidatePlugins task without applying the Java Toolchains plugin will result in an error.

To fix this, explicitly apply the jvm-toolchains plugin:

build.gradle.kts
plugins {
    id("jvm-toolchains")
}
build.gradle
plugins {
    id 'jvm-toolchains'
}
Tip
The jvm-toolchains plugin is automatically applied by the Java Library Plugin and other JVM-related plugins. If you are already applying one of those, no further action is needed.
Archive tasks (Jar, Ear, War, Zip, AbstractArchiveTask) produce reproducible archives by default
Note
This change may affect existing builds that relied on the previous behavior of archive tasks, where file order was not deterministic, and file timestamps and permissions were taken from the file system.

In Gradle 9.0.0, the default behavior of archive tasks (such as Jar, Ear, War, Zip, and AbstractArchiveTask) has changed to produce reproducible archives by default. That means that:

  • File order in the archive is now deterministic.

  • Files have fixed timestamps (timestamps depends on the archive type).

  • All directories have fixed permissions set to 0755.

  • All files have fixed permissions set to 0644.

This change improves the reproducibility of builds and ensures that the generated archives are consistent across different environments.

You can restore the previous behaviour for one or more properties for your task with the following configuration:

build.gradle.kts
tasks.withType<AbstractArchiveTask>().configureEach {
    // Make file order based on the file system
    isReproducibleFileOrder = false
    // Use file timestamps from the file system
    isPreserveFileTimestamps = true
    // Use permissions from the file system
    useFileSystemPermissions()
}
build.gradle
tasks.withType(AbstractArchiveTask).configureEach {
    // Makes file order non deterministic
    reproducibleFileOrder = false
    // Use file timestamps from the file system
    preserveFileTimestamps = true
    // Use permissions from the file system
    useFileSystemPermissions()
}

You can also preserve the file system permissions across archives tasks by configuring a property:

gradle.properties
org.gradle.archives.use-file-system-permissions=true
Test tasks may no longer execute expected tests

In previous releases, it was possible to create Test tasks without any additional configuration. By convention, Gradle used the classpath and test classes from the test source set.

build.gradle.kts
plugins {
    id("java-library")
}

// configure test dependencies
// ...

tasks.register<Test>("otherTest")
build.gradle
plugins {
    id 'java-library'
}

// configure test dependencies
// ...

tasks.register("otherTest", Test)

In this example, otherTest relied on the deprecated convention. This scenario started to emit a deprecation warning in 8.1. Gradle would execute the same tests with otherTest and the built-in test task because Gradle used the classpath and test classes from the test source set.

The convention has been removed, but builds will not fail if they were relying on this behavior. Builds that emitted the deprecation warning will silently stop executing tests. In the example above, otherTest will be skipped and execute no tests because it has no test classes configured.

To return to the previous behavior, you must explicitly configure the Test task:

build.gradle.kts
val test by testing.suites.existing(JvmTestSuite::class)
tasks.register<Test>("otherTest") {
    testClassesDirs = files(test.map { it.sources.output.classesDirs })
    classpath = files(test.map { it.sources.runtimeClasspath })
}
build.gradle
tasks.register("otherTest", Test) {
    testClassesDirs = testing.suites.test.sources.output.classesDirs
    classpath = testing.suites.test.sources.runtimeClasspath
}

or add the extra Test task through test suites:

build.gradle.kts
testing {
    suites {
        named<JvmTestSuite>("test") {
            targets {
                register("otherTest")
            }
        }
    }
}
build.gradle
testing {
    suites {
        test {
            targets {
                otherTest
            }
        }
    }
}
test task fails when no tests are discovered

When test sources are present and no filters are applied, the test task will now fail with an error if it runs but doesn’t discover any tests. This is to help prevent misconfigurations where the tests are written for one test framework but the test task is mistakenly configured to use another test framework. If filters are applied, the outcome depends on the failOnNoMatchingTests property.

This behavior can be disabled by setting the failOnNoDiscoveredTests property to false in the test task configuration:

build.gradle.kts
tasks.withType<AbstractTestTask>().configureEach {
    failOnNoDiscoveredTests = false
}
build.gradle
tasks.withType(AbstractTestTask).configureEach {
    failOnNoDiscoveredTests = false
}
Stale outputs outside the build directory are no longer deleted

In previous versions of Gradle, class files located outside the build directory were deleted when considered stale. This was a special case for class files registered as outputs of a source set.

Because this setup is uncommon and forced Gradle to eagerly realize all compile related tasks in every build, the behavior has been removed in Gradle 9.0.0.

Gradle will continue to clean up stale outputs inside the build directory as needed.

model and component tasks are no longer automatically added

The model and component tasks report on the structure of legacy software model objects configured for the project. Previously, these tasks were automatically added to the project for every build. These tasks are now only added to a project when a rule-based plugin is applied (such as those provided by Gradle’s support for building native software).

API changes

Gradle API now uses JSpecify nullability annotations

Gradle has supported null safety in its public API since Gradle 5.0, allowing early detection of nullability issues when writing Kotlin build scripts or plugin code in Java or Kotlin.

Previously, Gradle used annotations from the now-dormant JSR-305 to indicate nullability. While useful, JSR-305 had limitations and is no longer actively maintained.

Starting with Gradle 9.0.0, the Gradle API now uses JSpecify annotations. JSpecify provides a modern, standardized set of annotations and semantics for nullability in Java APIs, improving support in IDEs and during compilation.

Because JSpecify’s semantics differ slightly from JSR-305, you might see new warnings or errors in your Kotlin or Java plugin code. These typically indicate places where you need to clarify or adjust null handling, and modern compilers and IDEs should provide helpful messages to guide you.

Kotlin 2.1, when combined with JSpecify annotations in the Gradle API, introduces stricter nullability handling. Some formerly-valid code may now fail to compile due to more precise type checking.

Common breaking changes:

  • Unbounded generics for types that have generic bounds will now fail to compile.

    For example if you have a Kotlin extension function on Provider<T> whose signature is fun <T> Provider<T>.some() you must qualify <T> as <T : Any> because T isn’t nullable on Provider<T>.

  • The nullability of generic bounds is now handled strictly.

    For example, you can’t use Property<String?> anymore because the T in Property<T> is not nullable.

    Another example is using a function from the Gradle API that takes a Map<String, *> parameter ; you could pass a map with nullable values before, you can’t do that anymore.

Note
Plugins that use javax.annotation (JSR-305) annotations will continue to work in Gradle 9.0.0 as they did before.
Methods on public API types made final

The methods AndSpec.and and GenerateBuildDashboard.aggregate have been declared final to support the use of the @SafeVarargs annotation.

These types were not intended to be subclassed. However, if your build logic or a plugin attempts to override these methods, it will now result in a runtime failure.

Injection getters are now abstract

All Gradle-provided classes that have @Inject annotated getters now have those getters declared as abstract. This will require all classes that extend Gradle-provided classes to be abstract.

ConfigurationVariant.getDescription is now a Property<String>

This method was added in Gradle 7.5 and was previously a Optional<String>. This property was not configurable by public APIs.

By making the description a Property<String>, secondary variants have a user configurable description that appears in the outgoingVariants report.

New subtypes of ComponentIdentifier introduced

Gradle 9.0.0 introduces RootComponentIdentifier, a new subtype of ComponentIdentifier.

APIs which return instances of ComponentIdentifier may now return identifier instances of this new type. For example, the ComponentResult, ResolvedVariantResult, and ArtifactView APIs, among others, are affected.

In future Gradle versions, additional subtypes of ComponentIdentifier may be introduced. Build logic should remain resilient to unknown ComponentIdentifier subtypes returned by Gradle APIs.

Packaging and artifact behavior changes

Artifact Signing now matches OpenPGP Key Version

Starting with Gradle 9.0.0, the signing plugin produces OpenPGP signatures that match the version of the key used. This change ensures compliance with RFC 9580 and introduces support for OpenPGP version 6 keys. Previously, Gradle always generated OpenPGP version 4 signatures, regardless of the key version.

Ear and War plugins build all artifacts with assemble

Prior to Gradle 9.0.0, applying multiple packaging plugins (e.g., ear, war, java) to the same project resulted in special behavior where only one artifact type was built during assemble. For example:

  • Applying the ear plugin would skip building war and jar artifacts.

  • Applying the war plugin would skip building the jar.

This special handling has been removed in Gradle 9.0.0. Now, if multiple packaging plugins are applied, all corresponding artifacts will be built when running the assemble task. For example, a project applying the ear, war, and java plugins will now produce .ear, .war, and .jar files during assemble.

Ear and War plugins contribute all artifacts to the archives configuration

In previous versions of Gradle, applying multiple packaging plugins (ear, war, java) resulted in selective behavior for the archives configuration. For example:

  • Applying the ear plugin excluded jar and war artifacts from archives.

  • Applying the war plugin excluded the jar artifact from archives.

This behavior has been removed in Gradle 9.0.0. Now, when multiple packaging plugins are applied, all related artifacts—EAR, WAR, and JAR—are included in the archives configuration.

Gradle no longer implicitly builds certain artifacts during assemble

In previous versions of Gradle, the assemble task would implicitly build artifacts from any configuration where the visible flag was not set to false. This behavior has been removed in Gradle 9.0.0.

If you have a custom configuration and want its artifact to be built as part of assemble, you now need to explicitly declare the dependency between the artifact and the assemble task:

build.gradle.kts
val specialJar = tasks.register<Jar>("specialJar") {
    from("foo")
}

val special = configurations.create("special") {
    // In previous versions, this would have been enough to build the specialJar
    // artifact when running assemble
    outgoing.artifact(specialJar)
}

// In Gradle 9.0.0, you need to add a dependency from the artifact to the assemble task
tasks.named("assemble") {
    dependsOn(special.artifacts)
}
build.gradle
def specialJar = tasks.register("specialJar". Jar) {
    from("foo")
}

def special = configurations.create("special") {
    // In previous versions, this would have been enough to build the specialJar
    // artifact when running assemble
    outgoing.artifact(specialJar)
}

// In Gradle 9.0.0, you need to add a dependency from the artifact to the assemble task
tasks.named("assemble") {
    dependsOn(special.artifacts)
}
Gradle no longer implicitly adds certain artifacts to the archives configuration

In previous versions of Gradle, the archives configuration would automatically include artifacts from any configuration where the visible flag was not set to false. This implicit behavior has been removed in Gradle 9.0.0.

To include a custom artifact in the archives configuration, you must now add it explicitly:

build.gradle.kts
val specialJar = tasks.register<Jar>("specialJar") {
    from("foo")
}

configurations {
    create("special") {
        // In previous versions, this would have been enough to add the specialJar
        // artifact to the archives configuration
        outgoing.artifact(specialJar)
    }
    // In Gradle 9.0.0, you need to explicitly add the artifact to the archives
    // configuration
    named("archives") {
        outgoing.artifact(specialJar)
    }
}
build.gradle
def specialJar = tasks.register("specialJar". Jar) {
    from("foo")
}

configurations {
    create("special") {
        // In previous versions, this would have been enough to add the specialJar
        // artifact to the archives configuration
        outgoing.artifact(specialJar)
    }
    // In Gradle 9.0.0, you need to explicitly add the artifact to the archives
    // configuration
    named("archives") {
        outgoing.artifact(specialJar)
    }
}
Gradle Module Metadata can no longer be modified after an eagerly created publication is created from the same component

This behavior previously caused a warning: Gradle Module Metadata is modified after an eagerly populated publication.

It will now fail with an error, suggesting a review of the relevant documentation.

Configuration Cache changes

Unsupported build event listeners are now configuration cache problems

The build event listener registration method BuildEventsListenerRegistry.onTaskCompletion accepts arbitrary providers of any OperationCompletionListener implementations. However, only providers returned from BuildServiceRegistry.registerIfAbsent or BuildServiceRegistration.getService are currently supported when the Configuration Cache is enabled.

Previously, unsupported providers were silently discarded and never received events when the configuration cache was used. Starting with Gradle 9.0.0, registering such providers now causes a configuration cache problem and fails the build.

If your build was previously working with the configuration cache (e.g., the listeners were nonessential), you can temporarily revert to the old behavior by setting:

org.gradle.configuration-cache.unsafe.ignore.unsupported-build-events-listeners=true

This property will be removed in Gradle 10.

Configuration Cache entry is now always discarded for incompatible tasks in warning mode

Configuration Cache enables gradual migration by allowing to explicitly mark tasks as incompatible. When incompatible tasks are scheduled for execution, cache entry is not stored and tasks do not run in parallel to ensure correctness. Configuration Cache can also run in the warning mode by enabling org.gradle.configuration-cache.problems=warn, which hides problems and allows storing and loading of the cache entry, running tasks in parallel with potential issues. The warning mode exists as a migration and troubleshooting aid and is not intended as a persistent way of ignoring incompatibilities.

Starting with Gradle 9.0.0, the presence of incompatible tasks in the work graph results in the cache entry being discarded regardless of the warning mode to ensure correctness. If you relied on the warning mode previously, consider marking the offending tasks as incompatible instead. However, at this stage of Configuration Cache maturity and ecosystem adoption, we recommend resolving the incompatibilities instead to ensure performance benefits this feature brings.

Updated versions

Upgraded default versions of code quality tools

The default version of Checkstyle is 10.24.0.

The default version of CodeNarc is 3.6.0.

The default version of Pmd is 7.13.0

Upgraded default versions of testing frameworks

When using test suites, the version of several testing frameworks has changed.

The default version of JUnit Jupiter is 5.12.2.

The default version of TestNG is 7.11.0.

The default version of Spock is 2.3.

Upgraded version of Eclipse JGit

Eclipse JGit has been updated from 5.13.3 to 7.2.1.

This update reworks how Gradle configures JGit for SSH operations and introduces support for using the SSH Agent, leveraging the new capabilities available in JGit’s SSH agent integration.

Removed APIs and features

Removal of deprecated jcenter()

The jcenter() repository API has been removed in Gradle 9.0.0. The API was deprecated in Gradle 7.0.

The JCenter repository was redirected to Maven Central in August 2024.

RepositoryHandler.mavenCentral() is the closest direct replacement.

Removal of deprecated JvmVendorSpec.IBM_SEMERU

The deprecated JvmVendorSpec.IBM_SEMERU constant has been removed. Its usage should be replaced by JvmVendorSpec.IBM.

Removal of GroovySourceSet and ScalaSourceSet interfaces

The following source set interfaces have been removed in Gradle 9.0.0:

  • org.gradle.api.tasks.GroovySourceSet

  • org.gradle.api.tasks.ScalaSourceSet

To configure Groovy or Scala sources, use the plugin-specific Source Directory Sets instead:

For example, to configure Groovy sources in a plugin:

GroovySourceDirectorySet groovySources = sourceSet.getExtensions().getByType(GroovySourceDirectorySet.class);
groovySources.setSrcDirs(Arrays.asList("sources/groovy"));
Removal of custom build layout options

The ability to specify custom locations for key build files from the command line has been removed in Gradle 9.0.0. The following options, deprecated in Gradle 8.x, are no longer supported:

  • -c, --settings-file — Specify a custom location for the settings file

  • -b, --build-file — Specify a custom location for the build file

In addition, the buildFile property on the GradleBuild task has been removed. This means it is no longer possible to set a custom build file path via the GradleBuild task.

Removal of conventions

The "convention" concept—represented by the org.gradle.api.plugins.Convention type—has been deprecated since Gradle 8.2 and is now fully removed in Gradle 9.0.0.

Core Gradle plugins that previously registered deprecated conventions have been updated accordingly.

This implies removal of the Conventions API. These have been removed:

  • org.gradle.api.Task.getConvention()

  • org.gradle.api.Project.getConvention()

  • org.gradle.api.plugins.Convention

  • org.gradle.api.internal.HasConvention

Existing plugins that use these APIs will fail with Gradle 9.0.0+ and should be updated to use the Extensions API instead.

The table below shows which conventions have been removed and how to migrate:

Plugin Access Type Solution

war

project.war

WarPluginConvention

Configure the war task directly instead.

base

project.distDirName, project.libsDirName, project.archivesBaseName

BasePluginConvention

Replaced by project.base extension of type BasePluginExtension.

project-report

project.projectReports

ProjectReportPluginConvention

Configure the report task (TaskReportTask, PropertyReportTask, DependencyReportTask, HtmlDependencyReportTask) directly.

ear

project.ear

EarPluginConvention

Configure the ear task directly instead.

Removal of org.gradle.cache.cleanup

The org.gradle.cache.cleanup property, which previously allowed users to disable automatic cache cleanup, has been removed in Gradle 9.0.0.

This property no longer has any effect. To control cache cleanup behavior in Gradle 9.0.0 and later, use an init script instead.

Removal of buildCache.local.removeUnusedEntriesAfterDays

In Gradle 9.0.0, the property buildCache.local.removeUnusedEntriesAfterDays has been removed.

This property was previously used to configure the retention period for the local build cache.

To configure retention for unused entries in the local build cache, use the Gradle User Home cache cleanup settings instead.

Removal of deprecated org.gradle.util members

The following members of the org.gradle.util package have been removed:

  • CollectionUtils

  • ConfigureUtil, ClosureBackedAction

    These classes used to provide utilities related to groovy.lang.Closure. Plugins should avoid relying on Groovy specifics, such as Closure, in their APIs. Instead, plugins should create methods that use Action:

    abstract class MyExtension {
        // ...
        public void options(Action<? extends MyOptions>  action) {
            action.execute(options)
        }
    }

    Gradle automatically generates a Closure-taking method at runtime for each method with an Action as a single argument as long as the object is created with ObjectFactory#newInstance.

    As a last resort, to apply some configuration represented by a Groovy Closure, a plugin can use Project#configure.

Removal of deprecated testSourceDirs and testResourceDirs from IdeaModule

The deprecated testSourceDirs and testResourceDirs properties have been removed from org.gradle.plugins.ide.idea.model.IdeaModule. This change does not affect the org.gradle.tooling.model.idea.IdeaModule type used in the Tooling API. Use the testSources and testResources properties instead.

Removal of Unix mode based file permissions

Gradle 9.0.0 removes legacy APIs for specifying file permissions using raw Unix mode integers.

A new and more expressive API for configuring file permissions was introduced in Gradle 8.3 and promoted to stable in Gradle 8.8. See:

The following older methods, deprecated in Gradle 8.8, have now been removed:

  • org.gradle.api.file.CopyProcessingSpec.getFileMode()

  • org.gradle.api.file.CopyProcessingSpec.setFileMode(Integer)

  • org.gradle.api.file.CopyProcessingSpec.getDirMode()

  • org.gradle.api.file.CopyProcessingSpec.setDirMode(Integer)

  • org.gradle.api.file.FileTreeElement.getMode()

  • org.gradle.api.file.FileCopyDetails.setMode(int)

Removal of select Groovy modules from the Gradle distribution

Gradle 9.0.0 removes certain Groovy modules from its bundled distribution. They will no longer be available on the classpath or be available via localGroovy:

  • groovy-test

  • groovy-console

  • groovy-sql

Removal of kotlinDslPluginOptions.jvmTarget

In Gradle 9.0.0, the kotlinDslPluginOptions.jvmTarget property has been removed.

This property was previously used to configure the JVM target version for code compiled with the kotlin-dsl plugin.

To set the target JVM version, you should now configure a Java Toolchain instead.

Removal of the gradle-enterprise plugin block extension in Kotlin DSL

In Kotlin DSL based settings.gradle.kts files, you could previously use the gradle-enterprise plugin block extension to apply the Gradle Enterprise plugin using the same version bundled with gradle --scan:

plugins {
    `gradle-enterprise`
}

This shorthand had no equivalent in the Groovy DSL (settings.gradle) and has now been removed.

Gradle Enterprise has been renamed to Develocity, and the plugin ID has changed from com.gradle.enterprise to com.gradle.develocity. As a result, you must now apply the plugin explicitly using its full ID and version:

plugins {
    id("com.gradle.develocity") version "4.0.2"
}

If you’re still using the legacy name, you may apply the deprecated plugin ID to ease the transition:

plugins {
    id("com.gradle.enterprise") version "3.19.2"
}

We strongly encourage users to adopt the latest released version of the Develocity plugin, even when using it with older versions of Gradle.

Removal of eager artifact configuration accessors in Kotlin DSL

In Gradle 5.0, the type of configuration accessors changed from Configuration to NamedDomainObjectProvider<Configuration> to support lazy configuration. To maintain compatibility with plugins compiled against older Gradle versions, the Kotlin DSL provided eager accessor extensions such as:

configurations.compileClasspath.files // equivalent to configurations.compileClasspath.get().files
configurations.compileClasspath.singleFile // equivalent to configurations.compileClasspath.get().singleFile

These eager accessors were deprecated and removed from the public API in Gradle 8.0 but remained available for plugins compiled against older Gradle versions.

In Gradle 9.0.0, these legacy methods have now been fully removed.

Removal of libraries and bundles from version catalogs in the plugins {} block in Kotlin DSL

In Gradle 8.1, accessing libraries or bundles from dependency version catalogs within the plugins {} block of a Kotlin DSL script was deprecated.

In Gradle 9.0.0, this support has been fully removed. Attempting to reference libraries or bundles in the plugins {} block will now result in a build failure.

Removal of "name"() task reference syntax in Kotlin DSL

In Gradle 9.0.0, referencing tasks or other domain objects using the "name"() syntax in Kotlin DSL has been removed.

Instead of using "name"() to reference a task or domain object, use named("name") or one of the other supported notations.

Removal of outputFile in WriteProperties task

The outputFile property in the WriteProperties task has been removed in Gradle 9.0.0.

This property was deprecated in Gradle 8.0 and was replaced with the destinationFile property.

Removal of Project#exec, Project#javaexec, and script-level counterparts

The following helper methods for launching external processes were deprecated in Gradle 8.11 and have now been removed in Gradle 9.0.0:

  • org.gradle.api.Project#exec(Closure)

  • org.gradle.api.Project#exec(Action)

  • org.gradle.api.Project#javaexec(Closure)

  • org.gradle.api.Project#javaexec(Action)

  • org.gradle.api.Script#exec(Closure)

  • org.gradle.api.Script#exec(Action)

  • org.gradle.api.Script#javaexec(Closure)

  • org.gradle.api.Script#javaexec(Action)

  • org.gradle.kotlin.dsl.InitScriptApi#exec(Action)

  • org.gradle.kotlin.dsl.InitScriptApi#javaexec(Action)

  • org.gradle.kotlin.dsl.KotlinScript#exec(Action)

  • org.gradle.kotlin.dsl.KotlinScript#javaexec(Action)

  • org.gradle.kotlin.dsl.SettingsScriptApi#exec(Action)

  • org.gradle.kotlin.dsl.SettingsScriptApi#javaexec(Action)

Removal of unused public APIs

The following types have been removed in Gradle 9.0.0. These types are not used in Gradle’s public API and as such are not useful.

  • org.gradle.api.artifacts.ArtifactIdentifier

  • org.gradle.api.publish.ivy.IvyDependency

  • org.gradle.api.publish.maven.MavenDependency

Upgrading within Gradle 8.x

This chapter provides the information you need to migrate your Gradle 8.x builds to Gradle 8.14.5. For migrating from Gradle 7.x, see the older migration guide first.

We recommend the following steps for all users:

  1. Try running gradle help --scan and view the deprecations view of the generated Build Scan.

    Deprecations View in a Build Scan

    This lets you see any deprecation warnings that apply to your build.

    Alternatively, you can run gradle help --warning-mode=all to see the deprecations in the console, though it may not report as much detailed information.

  2. Update your plugins.

    Some plugins will break with this new version of Gradle because they use internal APIs that have been removed or changed. The previous step will help you identify potential problems by issuing deprecation warnings when a plugin tries to use a deprecated part of the API.

  3. Run gradle :wrapper --gradle-version 8.14.5 to update the project to 8.14.5.

  4. Try to run the project and debug any errors using the Troubleshooting Guide.

Upgrading from 8.13 and earlier

Potential breaking changes
The Gradle Wrapper is now an executable JAR

The Gradle Wrapper JAR has been converted into an executable JAR. This means it now includes a Main-Class attribute, allowing it to be launched using the -jar option instead of specifying a classpath and main class manually.

When you update the wrapper scripts using the gradle :wrapper or ./gradlew :wrapper command, the wrapper JAR will be updated automatically to reflect this change.

Changes to Settings defaults

The incubating Settings.getDefaults() method, introduced in Gradle 8.10, has been removed. Use the Settings.defaults(Action<SharedModelDefaults>) method instead, which accepts a lambda.

This change allows default values to be interpreted in the context of individual projects rather than at the Settings level.

Upgrade to Guava 33.4.6

Guava has been updated from version 32.1.2 to 33.4.6. This release deprecates several core features, including Charsets. For full details, see the Guava release notes.

EclipseClasspath.baseSourceOutputDir is now a DirectoryProperty

The incubating EclipseClasspath.baseSourceOutputDir was previously declared as a Property<File>. It has now been correctly updated to a DirectoryProperty to reflect the intended type.

Upgrade to Groovy 3.0.24

Groovy has been updated to Groovy 3.0.24.

Since the previous version was 3.0.22, this includes changes for Groovy 3.0.23 as well.

Upgrade to JaCoCo 0.8.13

JaCoCo has been updated to 0.8.13.

JavaExec now uses the toolchain from the java extension by default

Previously, the JavaExec task used the same Java version as the Gradle process itself. Starting in Gradle 9.0.0, when the java-base plugin is applied, JavaExec will instead default to the Java toolchain configured in the java extension. You can override the toolchain explicitly in the JavaExec task configuration if needed.

Upgrade to SLF4J 2.0.17

SLF4J has been updated from 1.7.36 to 2.0.17.

Deprecations
Looking up attributes using null keys is deprecated

Passing null to getAttribute(Attribute) is now explicitly deprecated.

Previously, this would silently return null. Now, a deprecation warning is emitted. There should be no need to perform lookups with null keys in an AttributeContainer.

Groovy string-to-enum coercion for Property types is deprecated

Groovy supports string-to-enum coercion. Assigning a String to a Property<T> where T is an enum is now deprecated. This will become an error in Gradle 10.

This deprecation only affects plugins written in Groovy using the Groovy DSL.

Groovydoc.getAntGroovydoc() and org.gradle.api.internal.tasks.AntGroovydoc have been deprecated

These internal APIs were inadvertently exposed and are now deprecated. They will be removed in Gradle 9.0.0.

Deprecated methods in GradlePluginDevelopmentExtension

The constructor for GradlePluginDevelopmentExtension and its pluginSourceSet method are now deprecated.

These methods should not be used directly, they are intended to be configured solely by the Gradle Plugin Development plugin. Only the main source set is supported for plugin development.

These methods will be removed in Gradle 9.0.0.

Deprecated collections in IdeaModule now emit warnings

The testResourcesDirs and testSourcesDirs properties in org.gradle.plugins.ide.idea.model.IdeaModule were marked @Deprecated in Gradle 7.6, but no warnings were emitted until now.

Gradle now emits deprecation warnings when these properties are used. They will be removed in Gradle 9.0.0.

The ForkOptions.getJavaHome() and ForkOptions.setJavaHome() methods are no longer deprecated

These methods were deprecated in Gradle 8.11, but are no longer deprecated, as they do not yet have stable replacements.

Deprecated StartParameter.isConfigurationCacheRequested now emits warnings

The isConfigurationCacheRequested property in StartParameter was marked @Deprecated in Gradle 8.5, but no warnings were emitted until now.

Gradle now emits deprecation warnings when this property is used. It will be removed in Gradle 10.

Since Gradle 8.5, the same information can be obtained via the BuildFeatures service using configurationCache.requested property.

Deprecated configuration usages are no longer deprecated

Starting in 8.0, adding an artifact to a configuration that is neither resolvable nor consumable was deprecated. This deprecation was overly broad and also captured certain valid usages. It has been removed in Gradle 8.14.

Upgrading from 8.12 and earlier

Potential breaking changes
Changes to JvmTestSuite

The testType property was removed from JvmTestSuite and both the TestSuiteTargetName and TestSuiteType attributes have been removed. Test reports and JaCoCo reports can now be aggregated between projects by specifying the name of the test suite in the target project to aggregate.

See below for additional details.

Changes to Test Report Aggregation and Jacoco Aggregation

Several changes have been made to the incubating Test Report Aggregation and JaCoCo Report Aggregation plugins.

The plugins now create a single test results variant for each test suite, containing all test results for the entire suite, instead of one variant for each test target. This change allows the aggregation plugins to aggregate test suites with multiple targets, where previously this would result in an ambiguous variant selection error.

In the future, as we continue to develop these plugins, we plan to once again create one results variant per test suite target, allowing test results from certain targets to be explicitly aggregated.

The testType property on JacocoCoverageReport and AggregateTestReport has been removed and replaced with a new testSuiteName property:

Previously:

reporting {
    reports {
        val testCodeCoverageReport by creating(JacocoCoverageReport::class) {
            testType = TestSuiteType.UNIT_TEST
        }
    }
}

Now:

reporting {
    reports {
        val testCodeCoverageReport by creating(JacocoCoverageReport::class) {
            testSuiteName = "test"
        }
    }
}
Changed behavior when calling BuildLauncher.addJvmArguments

Issue (#31426) was fixed, that caused BuildLauncher.addJvmArguments to override flags coming from the org.gradle.jvmargs system property. Please ensure that you are not relying on this behavior when upgrading to Gradle 8.13. If system properties needs to be overridden, BuildLauncher.setJvmArguments should be used instead.

val buildLauncher: BuildLauncher = connector.connect().newBuild()
buildLauncher.setJvmArguments("-Xmx2048m", "-Dmy.custom.property=value")
Upgrade to ASM 9.7.1

ASM was upgraded from 9.6 to 9.7.1 to ensure earlier compatibility for Java 24.

Source level deprecation of Project.task methods

Eager task creation methods on the Project interface have been marked @Deprecated and will generate compiler and IDE warnings when used in build scripts or plugin code. There is not yet a Gradle deprecation warning emitted for their use.

However, if the build is configured to fail on warnings during Kotlin script or plugin code compilation, this change may cause the build to fail.

A standard Gradle deprecation warning will be printed upon use when these methods are fully deprecated in a future version.

Deprecations
Recursively querying AttributeContainer in lazy provider

In Gradle 9.0.0, querying the contents of an AttributeContainer from within an attribute value provider of the same container will become an error.

The following example showcases the forbidden behavior:

AttributeContainer container = getAttributeContainer();
Attribute<String> firstAttribute = Attribute.of("first", String.class);
Attribute<String> secondAttribute = Attribute.of("second", String.class);
container.attributeProvider(firstAttribute, project.getProviders().provider(() -> {
    // Querying the contents of the container within an attribute value provider
    // will become an error.
    container.getAttribute(secondAttribute);
    return "first";
}));
Deprecated org.gradle.api.artifacts.transform.VariantTransformConfigurationException

There is no good public use case for this exception, and it is not intended to be thrown by users. It will be replaced by org.gradle.api.internal.artifacts.transform.VariantTransformConfigurationException for internal use only in Gradle 9.0.0.

Deprecated properties in the incubating UpdateDaemonJvm

The following properties of UpdateDaemonJvm are now deprecated:

  • jvmVersion

  • jvmVendor

They are replaced by languageVersion and vendor respectively. This allows the configuration of a Java toolchain spec and the UpdateDaemonJvm task to be interchangeable.

Note that due to the change of type for the vendor property, executing updateDaemonJvm with the jvmVendor property will result in the task failing. See the documentation for the new configuration option.

Declaring boolean properties with is-prefix and Boolean types

Gradle property names are derived by following the Java Bean specification with one exception. Gradle recognizes methods with a Boolean return type and a is-prefix as a boolean property. This is behavior inherited from Groovy originally. Groovy 4 more closely follows the Java Bean specification and no longer supports this exception.

Gradle will emit a deprecation warning when it detects that a boolean property is derived from a method with a Boolean return type and is-prefix. In Gradle 9.0.0, Groovy 4 will no longer recognize this as a property in build scripts and Groovy source files. Gradle’s property-based behavior will not change. Gradle will still consider these properties for up-to-date checks. In Gradle 10, these methods will no longer be treated as defining a Gradle property. This may cause tasks to behave differently when a Boolean property is used as an input.

There are two options to fix this:

  1. Introduce a new method that starts with get instead of is which has the same behavior. The old method does not need to be removed (in order to preserve binary compatibility), but may need adjustments as indicated below.

    • It is recommended to deprecate the is- method, and then remove it in a future major version.

  2. Change the type of the property (both get and set) to boolean. This is a breaking change.

For task input properties using the first option, you should also annotate the old is- method with @Deprecated and @ReplacedBy to ensure it is not used by Gradle. For example, this code:

class MyValue {
    private final Boolean property = Boolean.TRUE;

    @Input
    Boolean isProperty() { return property; }
}

Should be replaced with the following:

class MyValue {
    private final Boolean property = Boolean.TRUE;

    @Deprecated
    @ReplacedBy("getProperty")
    Boolean isProperty() { return property; }

    @Input
    Boolean getProperty() { return property; }
}

Upgrading from 8.11 and earlier

Potential breaking changes
Upgrade to Kotlin 2.0.21

The embedded Kotlin has been updated from 2.0.20 to Kotlin 2.0.21.

Upgrade to Ant 1.10.15

Ant has been updated to Ant 1.10.15.

Upgrade to Zinc 1.10.4

Zinc has been updated to 1.10.4.

Swift SDK discovery

To determine the location of the Mac OS X SDK for Swift, Gradle now passes the --sdk macosx arguments to xcrun. This is necessary because the SDK could be discovered inconsistently without this argument across different environments.

Source level deprecation of TaskContainer.create methods

Eager task creation methods on the TaskContainer interface have been marked @Deprecated and will generate compiler and IDE warnings when used in build scripts or plugin code. There is not yet a Gradle deprecation warning emitted for their use.

However, if the build is configured to fail on warnings during Kotlin script or plugin code compilation, this behavior may cause the build to fail.

A standard Gradle deprecation warning will be printed upon use when these methods are fully deprecated in a future version.

Deprecations
Deprecated Ambiguous Transformation Chains

Previously, when at least two equal-length chains of artifact transforms were available that would produce compatible variants that would each satisfy a resolution request, Gradle would arbitrarily, and silently, pick one.

Now, Gradle emits a deprecation warning that explains this situation:

There are multiple distinct artifact transformation chains of the same length that would satisfy this request. This behavior has been deprecated. This will fail with an error in Gradle 9.0.0.
Found multiple transformation chains that produce a variant of 'root project :' with requested attributes:
  - color 'red'
  - texture 'smooth'
Found the following transformation chains:
  - From configuration ':squareBlueSmoothElements':
      - With source attributes:
          - artifactType 'txt'
          - color 'blue'
          - shape 'square'
          - texture 'smooth'
      - Candidate transformation chains:
          - Transformation chain: 'ColorTransform':
              - 'BrokenColorTransform':
                  - Converts from attributes:
                      - color 'blue'
                      - texture 'smooth'
                  - To attributes:
                      - color 'red'
          - Transformation chain: 'ColorTransform2':
              - 'BrokenColorTransform2':
                  - Converts from attributes:
                      - color 'blue'
                      - texture 'smooth'
                  - To attributes:
                      - color 'red'
 Remove one or more registered transforms, or add additional attributes to them to ensure only a single valid transformation chain exists.

In such a scenario, Gradle has no way to know which of the two (or more) possible transformation chains should be used. Picking an arbitrary chain can lead to inefficient performance or unexpected behavior changes when seemingly unrelated parts of the build are modified. This is potentially a very complex situation and the message now fully explains the situation by printing all the registered transforms in order, along with their source (input) variants for each candidate chain.

When encountering this type of failure, build authors should either:

  1. Add additional, distinguishing attributes when registering transforms present in the chain, to ensure that only a single chain will be selectable to satisfy the request

  2. Request additional attributes to disambiguate which chain is selected (if they result in non-identical final attributes)

  3. Remove unnecessary registered transforms from the build

This will become an error in Gradle 9.0.0.

init must run alone

The init task must run by itself. This task should not be combined with other tasks in a single Gradle invocation.

Running init in the same invocation as other tasks will become an error in Gradle 9.0.0.

For instance, this wil not be allowed:

> gradlew init tasks
Calling Task.getProject() from a task action

Calling Task.getProject() from a task action at execution time is now deprecated and will be made an error in Gradle 10. This method can still be used during configuration time.

The deprecation is only issued if the configuration cache is not enabled. When the configuration cache is enabled, calls to Task.getProject() are reported as configuration cache problems instead.

This deprecation was originally introduced in Gradle 7.4 but was only issued when the STABLE_CONFIGURATION_CACHE feature flag was enabled. That feature flag no longer controls this deprecation. This is another step towards moving users away from idioms that are incompatible with the configuration cache, which will become the only mode supported by Gradle in a future release.

Please refer to the configuration cache documentation for alternatives to invoking Task.getProject() at execution time that are compatible with the configuration cache.

Groovy "space assignment" syntax

Currently, there are multiple ways to set a property with Groovy DSL syntax:

propertyName = value
setPropertyName(value)
setPropertyName value
propertyName(value)
propertyName value

The latter one, "space-assignment", is a Gradle-specific feature that is not part of the Groovy language. In regular Groovy, this is just a method call: propertyName(value), and Gradle generates propertyName method in the runtime if this method hasn’t been present already. This feature may be a source of confusion (especially for new users) and adds an extra layer of complexity for users and the Gradle codebase without providing any significant value. Sometimes, classes declare methods with the same name, and these may even have semantics that are different from a plain assignment.

These generated methods are now deprecated and will be removed in Gradle 10, and both propertyName value and propertyName(value) will stop working unless the explicit method propertyName is defined. Use explicit assignment propertyName = value instead.

For explicit methods, consider using the propertyName(value) syntax instead of propertyName value for clarity. For example, jvmArgs "some", "arg" can be replaced with jvmArgs("some", "arg") or with jvmArgs = ["some", "arg"] for Test tasks.

If you have a big project, to replace occurrences of space-assignment syntax you can use, for example, the following sed command:

find . -name 'build.gradle' -type f -exec sed -i.bak -E 's/([^A-Za-z]|^)(replaceme)[ \t]*([^= \t{])/\1\2 = \3/g' {} +

You should replace replaceme with one or more property names you want to replace, separated by |, e.g. (url|group).

DependencyInsightReportTask.getDependencySpec

The method was deprecated because it was not intended for public use in build scripts.

ReportingExtension.baseDir

ReportingExtension.getBaseDir(), ReportingExtension.setBaseDir(File), and ReportingExtension.setBaseDir(Object) were deprecated. They should be replaced with ReportingExtension.getBaseDirectory() property.

Upgrading from 8.10 and earlier

Potential breaking changes
Upgrade to Kotlin 2.0.20

The embedded Kotlin has been updated from 1.9.24 to Kotlin 2.0.20. Also see the Kotlin 2.0.10 and Kotlin 2.0.0 release notes.

The default kotlin-test version in JVM test suites has been upgraded to 2.0.20 as well.

Kotlin DSL scripts are still compiled with Kotlin language version set to 1.8 for backward compatibility.

Gradle daemon JVM configuration via toolchain

The type of the property UpdateDaemonJvm.jvmVersion is now Property<JavaLanguageVersion>.

If you configured the task in a build script, you will need to replace:

jvmVersion = JavaVersion.VERSION_17

With:

jvmVersion = JavaLanguageVersion.of(17)

Using the CLI options to configure which JVM version to use for the Gradle Daemon has no impact.

Name matching changes

The name-matching logic has been updated to treat numbers as word boundaries for camelCase names. Previously, a request like unique would match both uniqueA and unique1. Such a request will now fail due to ambiguity. To avoid issues, use the exact name instead of a shortened version.

This change impacts:

  • Task selection

  • Project selection

  • Configuration selection in dependency report tasks

Deprecations
Deprecated javaHome property of ForkOptions

The javaHome property of the ForkOptions type has been deprecated and will be removed in Gradle 9.0.0.

Use JVM Toolchains, or the executable property instead.

Note
This deprecation was later removed, and for Gradle versions starting with 8.14, these methods will no longer throw deprecation warnings.
Deprecated mutating buildscript configurations

Starting in Gradle 9.0.0, mutating configurations in a script’s buildscript block will result in an error. This applies to project, settings, init, and standalone scripts.

The buildscript configurations block is only intended to control buildscript classpath resolution.

Consider the following script that creates a new buildscript configuration in a Settings script and resolves it:

buildscript {
    configurations {
        create("myConfig")
    }
    dependencies {
        "myConfig"("org:foo:1.0")
    }
}

val files = buildscript.configurations["myConfig"].files

This pattern is sometimes used to resolve dependencies in Settings, where there is no other way to obtain a Configuration. Resolving dependencies in this context is not recommended. Using a detached configuration is a possible but discouraged alternative.

The above example can be modified to use a detached configuration:

val myConfig = buildscript.configurations.detachedConfiguration(
    buildscript.dependencies.create("org:foo:1.0")
)

val files = myConfig.files
Selecting Maven variants by configuration name

Starting in Gradle 9.0.0, selecting variants by name from non-Ivy external components will be forbidden.

Selecting variants by name from local components will still be permitted; however, this pattern is discouraged. Variant aware dependency resolution should be preferred over selecting variants by name for local components.

The following dependencies will fail to resolve when targeting a non-Ivy external component:

dependencies {
    implementation(group: "com.example", name: "example", version: "1.0", configuration: "conf")
    implementation("com.example:example:1.0") {
        targetConfiguration = "conf"
    }
}
Deprecated manually adding to configuration container

Starting in Gradle 9.0.0, manually adding configuration instances to a configuration container will result in an error. Configurations should only be added to the container through the eager or lazy factory methods. Detached configurations and copied configurations should not be added to the container.

Calling the following methods on ConfigurationContainer will be forbidden: - add(Configuration) - addAll(Collection) - addLater(Provider) - addAllLater(Provider)

Deprecated ProjectDependency#getDependencyProject()

The ProjectDependency#getDependencyProject() method has been deprecated and will be removed in Gradle 9.0.0.

Accessing the mutable project instance of other projects should be avoided.

To discover details about all projects that were included in a resolution, inspect the full ResolutionResult. Project dependencies are exposed in the DependencyResult. See the user guide section on programmatic dependency resolution for more details on this API. This is the only reliable way to find all projects that are used in a resolution. Inspecting only the declared ProjectDependencys may miss transitive or substituted project dependencies.

To get the identity of the target project, use the new Isolated Projects safe project path method: ProjectDependency#getPath().

To access or configure the target project, consider this direct replacement:

val projectDependency: ProjectDependency = getSomeProjectDependency()

// Old way:
val someProject = projectDependency.dependencyProject

// New way:
val someProject = project.project(projectDependency.path)

This approach will not fetch project instances from different builds.

Deprecated ResolvedConfiguration.getFiles() and LenientConfiguration.getFiles()

The ResolvedConfiguration.getFiles() and LenientConfiguration.getFiles() methods have been deprecated and will be removed in Gradle 9.0.0.

These deprecated methods do not track task dependencies, unlike their replacements.

val deprecated: Set<File> = conf.resolvedConfiguration.files
val replacement: FileCollection = conf.incoming.files

val lenientDeprecated: Set<File> = conf.resolvedConfiguration.lenientConfiguration.files
val lenientReplacement: FileCollection = conf.incoming.artifactView {
    isLenient = true
}.files
Deprecated AbstractOptions

The AbstractOptions class has been deprecated and will be removed in Gradle 9.0.0. All classes extending AbstractOptions will no longer extend it.

As a result, the AbstractOptions#define(Map) method will no longer be present. This method exposes a non-type-safe API and unnecessarily relies on reflection. It can be replaced by directly setting the properties specified in the map.

Additionally, CompileOptions#fork(Map), CompileOptions#debug(Map), and GroovyCompileOptions#fork(Map), which depend on define, are also deprecated for removal in Gradle 9.0.0.

Consider the following example of the deprecated behavior and its replacement:

tasks.withType(JavaCompile) {
    // Deprecated behavior
    options.define(encoding: 'UTF-8')
    options.fork(memoryMaximumSize: '1G')
    options.debug(debugLevel: 'lines')

    // Can be replaced by
    options.encoding = 'UTF-8'

    options.fork = true
    options.forkOptions.memoryMaximumSize = '1G'

    options.debug = true
    options.debugOptions.debugLevel = 'lines'
}
Deprecated Dependency#contentEquals(Dependency)

The Dependency#contentEquals(Dependency) method has been deprecated and will be removed in Gradle 9.0.0.

The method was originally intended to compare dependencies based on their actual target component, regardless of whether they were of different dependency type. The existing method does not behave as specified by its Javadoc, and we do not plan to introduce a replacement that does.

Potential migrations include using Object.equals(Object) directly, or comparing the fields of dependencies manually.

Deprecated Project#exec and Project#javaexec

The Project#exec(Closure), Project#exec(Action), Project#javaexec(Closure), Project#javaexec(Action) methods have been deprecated and will be removed in Gradle 9.0.0.

These methods are scheduled for removal as part of the ongoing effort to make writing configuration-cache-compatible code easier. There is no way to use these methods without breaking configuration cache requirements so it is recommended to migrate to a compatible alternative. The appropriate replacement for your use case depends on the context in which the method was previously called.

At execution time, for example in @TaskAction or doFirst/doLast callbacks, the use of Project instance is not allowed when the configuration cache is enabled. To run external processes, tasks should use an injected ExecOperation service, which has the same API and can act as a drop-in replacement. The standard Java/Groovy/Kotlin process APIs, like java.lang.ProcessBuilder can be used as well.

At configuration time, only special Provider-based APIs must be used to run external processes when the configuration cache is enabled. You can use ProviderFactory.exec and ProviderFactory.javaexec to obtain the output of the process. A custom ValueSource implementation can be used for more sophisticated scenarios. The configuration cache guide has a more elaborate example of using these APIs.

Detached Configurations should not use extendsFrom

Detached configurations should not extend other configurations using extendsFrom.

This behavior has been deprecated and will become an error in Gradle 9.0.0.

To create extension relationships between configurations, you should change to using non-detached configurations created via the other factory methods present in the project’s ConfigurationContainer.

Deprecated customized Gradle logging

The Gradle#useLogger(Object) method has been deprecated and will be removed in Gradle 9.0.0.

This method was originally intended to customize logs printed by Gradle. However, it only allows intercepting a subset of the logs and cannot work with the configuration cache. We do not plan to introduce a replacement for this feature.

Unnecessary options on compile options and doc tasks have been deprecated

Gradle’s API allowed some properties that represented nested groups of properties to be replaced wholesale with a setter method. This was awkward and unusual to do and would sometimes require the use of internal APIs. The setters for these properties will be removed in Gradle 9.0.0 to simplify the API and ensure consistent behavior. Instead of using the setter method, these properties should be configured by calling the getter and configuring the object directly or using the convenient configuration method. For example, in CompileOptions, instead of calling the setForkOptions setter, you can call getForkOptions() or forkOptions(Action).

The affected properties are:

Deprecated Javadoc.isVerbose() and Javadoc.setVerbose(boolean)

These methods on Javadoc have been deprecated and will be removed in Gradle 9.0.0.

Upgrading from 8.9 and earlier

Potential breaking changes
JavaCompile tasks may fail when using a JRE even if compilation is not necessary

The JavaCompile tasks may sometimes fail when using a JRE instead of a JDK. This is due to changes in the toolchain resolution code, which enforces the presence of a compiler when one is requested. The java-base plugin uses the JavaCompile tasks it creates to determine the default source and target compatibility when sourceCompatibility/targetCompatibility or release are not set. With the new enforcement, the absence of a compiler causes this to fail when only a JRE is provided, even if no compilation is needed (e.g., in projects with no sources).

This can be fixed by setting the sourceCompatibility/targetCompatibility explicitly in the java extension, or by setting sourceCompatibility/targetCompatibility or release in the relevant task(s).

Upgrade to Kotlin 1.9.24

The embedded Kotlin has been updated from 1.9.23 to Kotlin 1.9.24.

Upgrade to Ant 1.10.14

Ant has been updated to Ant 1.10.14.

Upgrade to JaCoCo 0.8.12

JaCoCo has been updated to 0.8.12.

Upgrade to Groovy 3.0.22

Groovy has been updated to Groovy 3.0.22.

Deprecations
Running Gradle on older JVMs

Starting in Gradle 9.0.0, Gradle will require JVM 17 or later to run. Most Gradle APIs will be compiled to target JVM 17 bytecode.

Gradle will still support compiling Java code to target JVM version 6 or later. The target JVM version of the compiled code can be configured separately from the JVM version used to run Gradle.

All Gradle clients (wrapper, launcher, Tooling API and TestKit) will remain compatible with JVM 8 and will be compiled to target JVM 8 bytecode. Only the Gradle daemon will require JVM 17 or later. These clients can be configured to run Gradle builds with a different JVM version than the one used to run the client:

Alternatively, the JAVA_HOME environment variable can be set to a JVM 17 or newer, which will run both the client and daemon with the same version of the JVM.

Running Gradle builds with --no-daemon or using ProjectBuilder in tests will require JVM version 17 or later. The worker API will remain compatible with JVM 8, and running JVM tests will require JVM 8.

We decided to upgrade the minimum version of the Java runtime for a number of reasons:

  • Dependencies are beginning to drop support for older versions and may not release security patches.

  • Significant language improvements between Java 8 and Java 17 cannot be used without upgrading.

  • Some of the most popular plugins already require JVM 17 or later.

  • Download metrics for Gradle distributions show that JVM 17 is widely used.

Deprecated consuming non-consumable configurations from Ivy

In prior versions of Gradle, it was possible to consume non-consumable configurations of a project using published Ivy metadata. An Ivy dependency may sometimes be substituted for a project dependency, either explicitly through the DependencySubstitutions API or through included builds. When this happens, configurations in the substituted project could be selected that were marked as non-consumable.

Consuming non-consumable configurations in this manner is deprecated and will result in an error in Gradle 9.0.0.

Deprecated extending configurations in the same project

In prior versions of Gradle, it was possible to extend a configuration in a different project.

The hierarchy of a Project’s configurations should not be influenced by configurations in other projects. Cross-project hierarchies can lead to unexpected behavior when configurations are extended in a way that is not intended by the configuration’s owner.

Projects should also never access the mutable state of another project. Since Configurations are mutable, extending configurations across project boundaries restricts the parallelism that Gradle can apply.

Extending configurations in different projects is deprecated and will result in an error in Gradle 9.0.0.

Upgrading from 8.8 and earlier

Potential breaking changes
Change to toolchain provisioning

In previous versions of Gradle, toolchain provisioning could leave a partially provisioned toolchain in place with a marker file indicating that the toolchain was fully provisioned. This could lead to strange behavior with the toolchain. In Gradle 8.9, the toolchain is fully provisioned before the marker file is written. However, to not detect potentially broken toolchains, a different marker file (.ready) is used. This means all your existing toolchains will be re-provisioned the first time you use them with Gradle 8.9. Gradle 8.9 also writes the old marker file (provisioned.ok) to indicate that the toolchain was fully provisioned. This means that if you return to an older version of Gradle, an 8.9-provisioned toolchain will not be re-provisioned.

Upgrade to Kotlin 1.9.23

The embedded Kotlin has been updated from 1.9.22 to Kotlin 1.9.23.

Change the encoding of daemon log files

In previous versions of Gradle, the daemon log file, located at $GRADLE_USER_HOME/daemon/9.7.0/, was encoded with the default JVM encoding. This file is now always encoded with UTF-8 to prevent clients who may use different default encodings from reading data incorrectly. This change may affect third-party tools trying to read this file.

Compiling against Gradle implementation classpath

In previous versions of Gradle, Java projects that had no declared dependencies could implicitly compile against Gradle’s runtime classes. This means that some projects were able to compile without any declared dependencies even though they referenced Gradle runtime classes. This situation is unlikely to arise in projects since IDE integration and test execution would be compromised. However, if you need to utilize the Gradle API, declare a gradleApi dependency or apply the java-gradle-plugin plugin.

Configuration cache implementation packages now under org.gradle.internal

References to Gradle types not part of the public API should be avoided, as their direct use is unsupported. Gradle internal implementation classes may suffer breaking changes (or be renamed or removed) from one version to another without warning.

Users need to distinguish between the API and internal parts of the Gradle codebase. This is typically achieved by including internal in the implementation package names. However, before this release, the configuration cache subsystem did not follow this pattern.

To address this issue, all code initially under the org.gradle.configurationcache* packages has been moved to new internal packages (org.gradle.internal.*).

File-system watching on macOS 11 (Big Sur) and earlier is disabled

Since Gradle 8.8, file-system watching has only been supported on macOS 12 (Monterey) and later. We added a check to automatically disable file-system watching on macOS 11 (Big Sur) and earlier versions.

Possible change to JDK8-based compiler output when annotation processors are used

The Java compilation infrastructure has been updated to use the Problems API. This change will supply the Tooling API clients with structured, rich information about compilation issues.

The feature should not have any visible impact on the usual build output, with JDK8 being an exception. When annotation processors are used in the compiler, the output message differs slightly from the previous ones.

The change mainly manifests itself in typename printed. For example, Java standard types like java.lang.String will be reported as java.lang.String instead of String.

Upgrading from 8.7 and earlier

Deprecations
Deprecate mutating configuration after observation

To ensure the accuracy of dependency resolution, Gradle checks that Configurations are not mutated after they have been used as part of a dependency graph.

  • Resolvable configurations should not have their resolution strategy, dependencies, hierarchy, etc., modified after they have been resolved.

  • Consumable configurations should not have their dependencies, hierarchy, attributes, etc. modified after they have been published or consumed as a variant.

  • Dependency scope configurations should not have their dependencies, constraints, etc., modified after a configuration that extends from them is observed.

In prior versions of Gradle, many of these circumstances were detected and handled by failing the build. However, some cases went undetected or did not trigger build failures. In Gradle 9.0.0, all changes to a configuration, once observed, will become an error. After a configuration of any type has been observed, it should be considered immutable. This validation covers the following properties of a configuration:

  • Resolution Strategy

  • Dependencies

  • Constraints

  • Exclude Rules

  • Artifacts

  • Role (consumable, resolvable, dependency scope)

  • Hierarchy (extendsFrom)

  • Others (Transitive, Visible)

Starting in Gradle 8.8, a deprecation warning will be emitted in cases that were not already an error. Usually, this deprecation is caused by mutating a configuration in a beforeResolve hook. This hook is only executed after a configuration is fully resolved but not when it is partially resolved for computing task dependencies.

Consider the following code that showcases the deprecated behavior:

build.gradle.kts
plugins {
    id("java-library")
}

configurations.runtimeClasspath {
    // `beforeResolve` is not called before the configuration is partially resolved for
    // build dependencies, but only before a full graph resolution.
    // Configurations should not be mutated in this hook
    incoming.beforeResolve {
        // Add a dependency on `com:foo` if not already present
        if (allDependencies.none { it.group == "com" && it.name == "foo" }) {
            configurations.implementation.get().dependencies.add(project.dependencies.create("com:foo:1.0"))
        }
    }
}

tasks.register("resolve") {
    val conf: FileCollection = configurations["runtimeClasspath"]

    // Wire build dependencies
    dependsOn(conf)

    // Resolve dependencies
    doLast {
        assert(conf.files.map { it.name } == listOf("foo-1.0.jar"))
    }
}

For the following use cases, consider these alternatives when replacing a beforeResolve hook:

  • Adding dependencies: Use a DependencyFactory and addLater or addAllLater on DependencySet.

  • Changing dependency versions: Use preferred version constraints.

  • Adding excludes: Use Component Metadata Rules to adjust dependency-level excludes, or withDependencies to add excludes to a configuration.

  • Roles: Configuration roles should be set upon creation and not changed afterward.

  • Hierarchy: Configuration hierarchy (extendsFrom) should be set upon creation. Mutating the hierarchy prior to resolution is highly discouraged but permitted within a withDependencies hook.

  • Resolution Strategy: Mutating a configuration’s ResolutionStrategy is still permitted in a beforeResolve hook; however, this is not recommended.

Filtered Configuration file and fileCollection methods are deprecated

In an ongoing effort to simplify the Gradle API, the following methods that support filtering based on declared dependencies have been deprecated:

  • files(Dependency…​)

  • files(Spec)

  • files(Closure)

  • fileCollection(Dependency…​)

  • fileCollection(Spec)

  • fileCollection(Closure)

  • getFiles(Spec)

  • getFirstLevelModuleDependencies(Spec)

  • getFirstLevelModuleDependencies(Spec)

  • getFiles(Spec)

  • getArtifacts(Spec)

To mitigate this deprecation, consider the example below that leverages the ArtifactView API along with the componentFilter method to select a subset of a Configuration’s artifacts:

build.gradle.kts
val conf by configurations.creating

dependencies {
    conf("com.thing:foo:1.0")
    conf("org.example:bar:1.0")
}

tasks.register("filterDependencies") {
    val files: FileCollection = conf.incoming.artifactView {
        componentFilter {
            when(it) {
                is ModuleComponentIdentifier ->
                    it.group == "com.thing" && it.module == "foo"
                else -> false
            }
        }
    }.files

    doLast {
        assert(files.map { it.name } == listOf("foo-1.0.jar"))
    }
}
build.gradle
configurations {
    conf
}

dependencies {
    conf "com.thing:foo:1.0"
    conf "org.example:bar:1.0"
}

tasks.register("filterDependencies") {
    FileCollection files = configurations.conf.incoming.artifactView {
        componentFilter {
            it instanceof ModuleComponentIdentifier
                && it.group == "com.thing"
                && it.module == "foo"
        }
    }.files

    doLast {
        assert files*.name == ["foo-1.0.jar"]
    }
}

Contrary to the deprecated Dependency filtering methods, componentFilter does not consider the transitive dependencies of the component being filtered. This allows for more granular control over which artifacts are selected.

Deprecated Namer of Task and Configuration

Task and Configuration have a Namer inner class (also called Namer) that can be used as a common way to retrieve the name of a task or configuration. Now that these types implement Named, these classes are no longer necessary and have been deprecated. They will be removed in Gradle 9.0.0. Use Named.Namer.INSTANCE instead.

The super interface, Namer, is not being deprecated.

Unix mode-based file permissions deprecated

A new API for defining file permissions has been added in Gradle 8.3, see:

The new API has now been promoted to stable, and the old methods have been deprecated:

Deprecated setting retention period directly on local build cache

In previous versions, cleanup of the local build cache entries ran every 24 hours, and this interval could not be configured. The retention period was configured using buildCache.local.removeUnusedEntriesAfterDays.

In Gradle 8.0, a new mechanism was added to configure the cleanup and retention periods for various resources in Gradle User Home. In Gradle 8.8, this mechanism was extended to permit the retention configuration of local build cache entries, providing improved control and consistency.

  • Specifying Cleanup.DISABLED or Cleanup.ALWAYS will now prevent or force the cleanup of the local build cache

  • Build cache entry retention is now configured via an init-script, in the same manner as other caches.

If you want build cache entries to be retained for 30 days, remove any calls to the deprecated method:

buildCache {
    local {
        // Remove this line
        removeUnusedEntriesAfterDays = 30
    }
}

Add a file like this in ~/.gradle/init.d/cache.init.gradle.kts:

beforeSettings {
    caches {
        buildCache.setRemoveUnusedEntriesAfterDays(30)
    }
}

Calling buildCache.local.removeUnusedEntriesAfterDays is deprecated, and this method will be removed in Gradle 9.0.0. If set to a non-default value, this deprecated setting will take precedence over Settings.caches.buildCache.setRemoveUnusedEntriesAfterDays().

Deprecated Kotlin DSL gradle-enterprise plugin block extension

In settings.gradle.kts (Kotlin DSL), you can use gradle-enterprise in the plugins block to apply the Gradle Enterprise plugin with the same version as gradle --scan.

plugins {
    `gradle-enterprise`
}

There is no equivalent to this in settings.gradle (Groovy DSL).

Gradle Enterprise has been renamed Develocity, and the com.gradle.enterprise plugin has been renamed com.gradle.develocity. Therefore, the gradle-enterprise plugin block extension has been deprecated and will be removed in Gradle 9.0.0.

The Develocity plugin must be applied with an explicit plugin ID and version. There is no develocity shorthand available in the plugins block:

plugins {
    id("com.gradle.develocity") version "3.17.3"
}

If you want to continue using the Gradle Enterprise plugin, you can specify the deprecated plugin ID:

plugins {
    id("com.gradle.enterprise") version "3.17.3"
}

We encourage you to use the latest released Develocity plugin version, even when using an older Gradle version.

Potential breaking changes
Changes in the Problems API

We have implemented several refactorings of the Problems API, including a significant change in how problem definitions and contextual information are handled. The complete design specification can be found here.

In implementing this spec, we have introduced the following breaking changes to the ProblemSpec interface:

  • The label(String) and description(String) methods have been replaced with the id(String, String) method and its overloaded variants.

Changes to collection properties

The following incubating API introduced in 8.7 have been removed:

  • MapProperty.insert*(…​)

  • HasMultipleValues.append*(…​)

Replacements that better handle conventions are under consideration for a future 8.x release.

Upgrade to Groovy 3.0.21

Groovy has been updated to Groovy 3.0.21.

Since the previous version was 3.0.17, the 3.0.18 and 3.0.19, and 3.0.20 changes are also included.

Some changes in static type checking have resulted in source-code incompatibilities. Starting with 3.0.18, if you cast a closure to an Action without generics, the closure parameter will be Object instead of any explicit type specified. This can be fixed by adding the appropriate type to the cast, and the redundant parameter declaration can be removed:

// Before
tasks.create("foo", { Task it -> it.description = "Foo task" } as Action)
// Fixed
tasks.create("foo", { it.description = "Foo task" } as Action<Task>)
Upgrade to ASM 9.7

ASM was upgraded from 9.6 to 9.7 to ensure earlier compatibility for Java 23.

Upgrading from 8.6 and earlier

Potential breaking changes
Upgrade to Kotlin 1.9.22

The embedded Kotlin has been updated from 1.9.10 to Kotlin 1.9.22.

Upgrade to Apache SSHD 2.10.0

Apache SSHD has been updated from 2.0.0 to 2.10.0.

Replacement and upgrade of JSch

JSch has been replaced by com.github.mwiede:jsch and updated from 0.1.55 to 0.2.16

Upgrade to Eclipse JGit 5.13.3

Eclipse JGit has been updated from 5.7.0 to 5.13.3.

This includes reworking the way that Gradle configures JGit for SSH operations by moving from JSch to Apache SSHD.

Upgrade to Apache Commons Compress 1.25.0

Apache Commons Compress has been updated from 1.21 to 1.25.0. This change may affect the checksums of the produced jars, zips, and other archive types because the metadata of the produced artifacts may differ.

Upgrade to ASM 9.6

ASM was upgraded from 9.5 to 9.6 for better support of multi-release jars.

Upgrade of the version catalog parser

The version catalog parser has been upgraded and is now compliant with version 1.0.0 of the TOML spec.

This should not impact catalogs that use the recommended syntax or were generated by Gradle for publication.

Deprecations
Deprecated registration of plugin conventions

Using plugin conventions has been emitting warnings since Gradle 8.2. Now, registering plugin conventions will also trigger deprecation warnings. For more information, see the section about plugin convention deprecation.

Referencing tasks and domain objects by "name"() in Kotlin DSL

In Kotlin DSL, it is possible to reference a task or other domain object by its name using the "name"() notation.

There are several ways to look up an element in a container by name:

tasks {
    "wrapper"() // 1 - returns TaskProvider<Task>
    "wrapper"(Wrapper::class) // 2 - returns TaskProvider<Wrapper>
    "wrapper"(Wrapper::class) { // 3 - configures a task named wrapper of type Wrapper
    }
    "wrapper" { // 4 - configures a task named wrapper of type Task
    }
}

The first notation is deprecated and will be removed in Gradle 9.0.0. Instead of using "name"() to reference a task or domain object, use named("name") or one of the other supported notations.

The above example would be written as:

tasks {
    named("wrapper") // returns TaskProvider<Task>
}

The Gradle API and Groovy build scripts are not impacted by this.

Deprecated invalid URL decoding behavior

Before Gradle 8.3, Gradle would decode a CharSequence given to Project.uri(Object) using an algorithm that accepted invalid URLs and improperly decoded others. Gradle now uses the URI class to parse and decode URLs, but with a fallback to the legacy behavior in the event of an error.

Starting in Gradle 9.0.0, the fallback will be removed, and an error will be thrown instead.

To fix a deprecation warning, invalid URLs that require the legacy behavior should be re-encoded to be valid URLs, such as in the following examples:

Table 1. Legacy URL Conversions
Original Input New Input Reasoning

file:relative/path

relative/path

The file scheme does not support relative paths.

file:relative/path%21

relative/path!

Without a scheme, the path is taken as-is, without decoding.

https://example.com/my folder/

https://example.com/my%20folder/

Spaces are not valid in URLs.

https://example.com/my%%badly%encoded%path

https://example.com/my%25%25badly%25encoded%25path

% must be encoded as %25 in URLs, and no %-escapes should be invalid.

file::somepath

somepath

URIs should be hierarchical.

Deprecated SelfResolvingDependency

The SelfResolvingDependency interface has been deprecated for removal in Gradle 9.0.0. This type dates back to the first versions of Gradle, where some dependencies could be resolved independently. Now, all dependencies should be resolved as part of a dependency graph using a Configuration.

Currently, ProjectDependency and FileCollectionDependency implement this interface. In Gradle 9.0.0, these types will no longer implement SelfResolvingDependency. Instead, they will both directly implement Dependency.

As such, the following methods of ProjectDependency and FileCollectionDependency will no longer be available:

  • resolve

  • resolve(boolean)

  • getBuildDependencies

Consider the following scripts that showcase the deprecated interface and its replacement:

build.gradle.kts
plugins {
    id("java-library")
}

dependencies {
    implementation(files("bar.txt"))
    implementation(project(":foo"))
}

tasks.register("resolveDeprecated") {
    // Wire build dependencies (calls getBuildDependencies)
    dependsOn(configurations["implementation"].dependencies.toSet())

    // Resolve dependencies
    doLast {
        configurations["implementation"].dependencies.withType<FileCollectionDependency>() {
            assert(resolve().map { it.name } == listOf("bar.txt"))
            assert(resolve(true).map { it.name } == listOf("bar.txt"))
        }
        configurations["implementation"].dependencies.withType<ProjectDependency>() {
            // These methods do not even work properly.
            assert(resolve().map { it.name } == listOf<String>())
            assert(resolve(true).map { it.name } == listOf<String>())
        }
    }
}

tasks.register("resolveReplacement") {
    val conf = configurations["runtimeClasspath"]

    // Wire build dependencies
    dependsOn(conf)

    // Resolve dependencies
    val files = conf.files
    doLast {
        assert(files.map { it.name } == listOf("bar.txt", "foo.jar"))
    }
}
Deprecated members of the org.gradle.util package now report their deprecation

These members will be removed in Gradle 9.0.0.

  • Collection.stringize(Collection)

Upgrading from 8.5 and earlier

Potential breaking changes
Upgrade to JaCoCo 0.8.11

JaCoCo has been updated to 0.8.11.

DependencyAdder renamed to DependencyCollector

The incubating DependencyAdder interface has been renamed to DependencyCollector. A getDependencies method has been added to the interface that returns all declared dependencies.

Deprecations
Deprecated calling registerFeature using the main source set

Calling registerFeature on the java extension using the main source set is deprecated and will change behavior in Gradle 10.

Currently, features created while calling usingSourceSet with the main source set are initialized differently than features created while calling usingSourceSet with any other source set. Previously, when using the main source set, new implementation, compileOnly, runtimeOnly, api, and compileOnlyApi configurations were created, and the compile and runtime classpaths of the main source set were configured to extend these configurations.

Starting in Gradle 10, the main source set will be treated like any other source set. With the java-library plugin applied (or any other plugin that applies the java plugin), calling usingSourceSet with the main source set will throw an exception. This is because the java plugin already configures a main feature. Only if the java plugin is not applied will the main source set be permitted when calling usingSourceSet.

Code that currently registers features with the main source set, such as:

build.gradle.kts
plugins {
    id("java-library")
}

java {
    registerFeature("feature") {
        usingSourceSet(sourceSets["main"])
    }
}
build.gradle
plugins {
    id("java-library")
}

java {
    registerFeature("feature") {
        usingSourceSet(sourceSets.main)
    }
}

Should instead, create a separate source set for the feature and register the feature with that source set:

build.gradle.kts
plugins {
    id("java-library")
}

sourceSets {
    create("feature")
}

java {
    registerFeature("feature") {
        usingSourceSet(sourceSets["feature"])
    }
}
build.gradle
plugins {
    id("java-library")
}

sourceSets {
    feature
}

java {
    registerFeature("feature") {
        usingSourceSet(sourceSets.feature)
    }
}
Deprecated publishing artifact dependencies with explicit name to Maven repositories

Publishing dependencies with an explicit artifact with a name different from the dependency’s artifactId to Maven repositories has been deprecated. This behavior is still permitted when publishing to Ivy repositories. It will result in an error in Gradle 9.0.0.

When publishing to Maven repositories, Gradle will interpret the dependency below as if it were declared with coordinates org:notfoo:1.0:

build.gradle.kts
dependencies {
    implementation("org:foo:1.0") {
        artifact {
            name = "notfoo"
        }
    }
}
build.gradle
dependencies {
    implementation("org:foo:1.0") {
        artifact {
            name = "notfoo"
        }
    }
}

Instead, this dependency should be declared as:

build.gradle.kts
dependencies {
    implementation("org:notfoo:1.0")
}
build.gradle
dependencies {
    implementation("org:notfoo:1.0")
}
Deprecated ArtifactIdentifier

The ArtifactIdentifier class has been deprecated for removal in Gradle 9.0.0.

Deprecate mutating DependencyCollector dependencies after observation

Starting in Gradle 10, mutating dependencies sourced from a DependencyCollector, after those dependencies have been observed will result in an error. The DependencyCollector interface is used to declare dependencies within the test suites DSL.

Consider the following example where a test suite’s dependency is mutated after it is observed:

build.gradle.kts
plugins {
    id("java-library")
}

testing.suites {
    named<JvmTestSuite>("test") {
        dependencies {
            // Dependency is declared on a `DependencyCollector`
            implementation("com:foo")
        }
    }
}

configurations.testImplementation {
    // Calling `all` here realizes/observes all lazy sources, including the `DependencyCollector`
    // from the test suite block. Operations like resolving a configuration similarly realize lazy sources.
    dependencies.all {
        if (this is ExternalDependency && group == "com" && name == "foo" && version == null) {
            // Dependency is mutated after observation
            version {
                require("2.0")
            }
        }
    }
}

In the above example, the build logic uses iteration and mutation to try to set a default version for a particular dependency if the version is not already set. Build logic like the above example creates challenges in resolving declared dependencies, as reporting tools will display this dependency as if the user declared the version as "2.0", even though they never did. Instead, the build logic can avoid iteration and mutation by declaring a preferred version constraint on the dependency’s coordinates. This allows the dependency management engine to use the version declared on the constraint if no other version is declared.

Consider the following example that replaces the above iteration with an indiscriminate preferred version constraint:

build.gradle.kts
dependencies {
    constraints {
        testImplementation("com:foo") {
            version {
                prefer("2.0")
            }
        }
    }
}

Upgrading from 8.4 and earlier

Potential breaking changes
Upgrade to Kotlin 1.9.20

The embedded Kotlin has been updated to Kotlin 1.9.20.

Changes to Groovy task conventions

The groovy-base plugin is now responsible for configuring source and target compatibility version conventions on all GroovyCompile tasks.

If you are using this task without applying grooy-base, you will have to manually set compatibility versions on these tasks. In general, the groovy-base plugin should be applied whenever working with Groovy language tasks.

Provider.filter

The type of argument passed to Provider.filter is changed from Predicate to Spec for a more consistent API. This change should not affect anyone using Provider.filter with a lambda expression. However, this might affect plugin authors if they don’t use SAM conversions to create a lambda.

Deprecations
Deprecated members of the org.gradle.util package now report their deprecation

These members will be removed in Gradle 9.0.0:

  • VersionNumber.parse(String)

  • VersionNumber.compareTo(VersionNumber)

Deprecated depending on resolved configuration

When resolving a Configuration, selecting that same configuration as a variant is sometimes possible. Configurations should be used for one purpose (resolution, consumption or dependency declarations), so this can only occur when a configuration is marked as both consumable and resolvable.

This can lead to circular dependency graphs, as the resolved configuration is used for two purposes.

To avoid this problem, plugins should mark all resolvable configurations as canBeConsumed=false or use the resolvable(String) configuration factory method when creating configurations meant for resolution.

In Gradle 9.0.0, consuming configurations in this manner will no longer be allowed and result in an error.

Including projects without an existing directory

Gradle will warn if a project is added to the build where the associated projectDir does not exist or is not writable. Starting with version 9.0.0, Gradle will not run builds if a project directory is missing or read-only. If you intend to dynamically synthesize projects, make sure to create directories for them as well:

settings.gradle.kts
include("project-without-directory")
project(":project-without-directory").projectDir.mkdirs()
settings.gradle
include 'project-without-directory'
project(":project-without-directory").projectDir.mkdirs()

Upgrading from 8.3 and earlier

Potential breaking changes
Upgrade to Kotlin 1.9.10

The embedded Kotlin has been updated to Kotlin 1.9.10.

XML parsing now requires recent parsers

Gradle 8.4 now configures XML parsers with security features enabled. If your build logic depends on old XML parsers that don’t support secure parsing, your build may fail. If you encounter a failure, check and update or remove any dependency on legacy XML parsers.

If you are an Android user, please upgrade your AGP version to 8.3.0 or higher to fix the issue caused by AGP itself. See the Update XML parser used in AGP for Gradle 8.4 compatibility for more details.

If you are unable to upgrade XML parsers coming from your build logic dependencies, you can force the use of the XML parsers built into the JVM. In OpenJDK, for example, this can be done by adding the following to gradle.properties:

systemProp.javax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl
systemProp.javax.xml.transform.TransformerFactory=com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl
systemProp.javax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl

See the CVE-2023-42445 advisory for more details and ways to enable secure XML processing on previous Gradle versions.

EAR plugin with customized JEE 1.3 descriptor

Gradle 8.4 forbids external XML entities when parsing XML documents. If you use the EAR plugin and configure the application.xml descriptor via the EAR plugin’s DSL and customize the descriptor using withXml {} and use asElement{} in the customization block, then the build will now fail for security reasons.

build.gradle.kts
plugins {
    id("ear")
}
ear {
    deploymentDescriptor {
        version = "1.3"
        withXml {
            asElement()
        }
    }
}
build.gradle
plugins {
    id("ear")
}
ear {
    deploymentDescriptor {
        version = "1.3"
        withXml {
            asElement()
        }
    }
}

If you happen to use asNode() instead of asElement(), then nothing changes, given asNode() simply ignores external DTDs.

You can work around this by running your build with the javax.xml.accessExternalDTD system property set to http.

On the command line, add this to your Gradle invocation:

-Djavax.xml.accessExternalDTD=http

To make this workaround persistent, add the following line to your gradle.properties:

systemProp.javax.xml.accessExternalDTD=http

Note that this will enable HTTP access to external DTDs for the whole build JVM. See the JAXP documentation for more details.

Deprecations
Deprecated GenerateMavenPom methods

The following methods on GenerateMavenPom are deprecated and will be removed in Gradle 9.0.0. They were never intended to be public API.

  • getVersionRangeMapper

  • withCompileScopeAttributes

  • withRuntimeScopeAttributes

Upgrading from 8.2 and earlier

Potential breaking changes
Deprecated Project.buildDir can cause script compilation failure

With the deprecation of Project.buildDir, buildscripts that are compiled with warnings as errors could fail if the deprecated field is used.

See the deprecation entry for details.

TestLauncher API no longer ignores build failures

The TestLauncher interface is part of the Tooling API, specialized for running tests. It is a logical extension of the BuildLauncher that can only launch tasks. A discrepancy has been reported in their behavior: if the same failing test is executed, BuildLauncher will report a build failure, but TestLauncher won’t. Originally, this was a design decision in order to continue the execution and run the tests in all test tasks and not stop at the first failure. At the same time, this behavior can be confusing for users as they can experience a failing test in a successful build. To make the two APIs more uniform, we made TestLauncher also fail the build, which is a potential breaking change. Tooling API clients should explicitly pass --continue to the build to continue the test execution even if a test task fails.

Fixed variant selection behavior with ArtifactView and ArtifactCollection

The dependency resolution APIs for selecting different artifacts or files (Configuration.getIncoming().artifactView { } and Configuration.getIncoming().getArtifacts()) captured immutable copies of the underlying `Configuration’s attributes to use for variant selection. If the `Configuration’s attributes were changed after these methods were called, the artifacts selected by these methods could be unexpected.

Consider the case where the set of attributes on a Configuration is changed after an ArtifactView is created:

build.gradle.kts
tasks {
    myTask {
        inputFiles.from(configurations.classpath.incoming.artifactView {
            attributes {
                // Add attributes to select a different type of artifact
            }
        }.files)
    }
}

configurations {
    classpath {
        attributes {
            // Add more attributes to the configuration
        }
    }
}

The inputFiles property of myTask uses an artifact view to select a different type of artifact from the configuration classpath. Since the artifact view was created before the attributes were added to the configuration, Gradle could not select the correct artifact.

Some builds may have worked around this by also putting the additional attributes into the artifact view. This is no longer necessary.

Upgrade to Kotlin 1.9.0

The embedded Kotlin has been updated from 1.8.20 to Kotlin 1.9.0. The Kotlin language and API levels for the Kotlin DSL are still set to 1.8 for backward compatibility. See the release notes for Kotlin 1.8.22 and Kotlin 1.8.21.

Kotlin 1.9 dropped support for Kotlin language and API level 1.3. If you build Gradle plugins written in Kotlin with this version of Gradle and need to support Gradle <7.0 you need to stick to using the Kotlin Gradle Plugin <1.9.0 and configure the Kotlin language and API levels to 1.3. See the Compatibility Matrix for details about other versions.

Eager evaluation of Configuration attributes

Gradle 8.3 updates the org.gradle.libraryelements and org.gradle.jvm.version attributes of JVM Configurations to be present at the time of creation, as opposed to previously, where they were only present after the Configuration had been resolved or consumed. In particular, the value for org.gradle.jvm.version relies on the project’s configured toolchain, meaning that querying the value for this attribute will finalize the value of the project’s Java toolchain.

Plugins or build logic that eagerly queries the attributes of JVM configurations may now cause the project’s Java toolchain to be finalized earlier than before. Attempting to modify the toolchain after it has been finalized will result in error messages similar to the following:

The value for property 'implementation' is final and cannot be changed any further.
The value for property 'languageVersion' is final and cannot be changed any further.
The value for property 'vendor' is final and cannot be changed any further.

This situation may arise when plugins or build logic eagerly query an existing JVM Configuration’s attributes to create a new Configuration with the same attributes. Previously, this logic would have omitted the two above-noted attributes entirely, while now, the same logic will copy the attributes and finalize the project’s Java toolchain. To avoid early toolchain finalization, attribute-copying logic should be updated to query the source Configuration’s attributes lazily:

build.gradle.kts
fun <T> copyAttribute(attribute: Attribute<T>, from: AttributeContainer, to: AttributeContainer) =
    to.attributeProvider<T>(attribute, provider { from.getAttribute(attribute)!! })

val source = configurations["runtimeClasspath"].attributes
configurations {
    create("customRuntimeClasspath") {
        source.keySet().forEach { key ->
            copyAttribute(key, source, attributes)
        }
    }
}
build.gradle
def source = configurations.runtimeClasspath.attributes
configurations {
    customRuntimeClasspath {
        source.keySet().each { key ->
            attributes.attributeProvider(key, provider { source.getAttribute(key) })
        }
    }
}
Deprecations
Deprecated Project.buildDir is to be replaced by Project.layout.buildDirectory

The Project.buildDir property is deprecated. It uses eager APIs and has ordering issues if the value is read in build logic and then later modified. It could result in outputs ending up in different locations.

It is replaced by a DirectoryProperty found at Project.layout.buildDirectory. See the ProjectLayout interface for details.

Note that, at this stage, Gradle will not print deprecation warnings if you still use Project.buildDir. We know this is a big change, and we want to give the authors of major plugins time to stop using it.

Switching from a File to a DirectoryProperty requires adaptations in build logic. The main impact is that you cannot use the property inside a String to expand it. Instead, you should leverage the dir and file methods to compute your desired location.

Here is an example of creating a file where the following:

build.gradle.kts
// Returns a java.io.File
file("$buildDir/myOutput.txt")
build.gradle
// Returns a java.io.File
file("$buildDir/myOutput.txt")

Should be replaced by:

build.gradle.kts
// Compatible with a number of Gradle lazy APIs that accept also java.io.File
val output: Provider<RegularFile> = layout.buildDirectory.file("myOutput.txt")

// If you really need the java.io.File for a non lazy API
output.get().asFile

// Or a path for a lazy String based API
output.map { it.asFile.path }
build.gradle
// Compatible with a number of Gradle lazy APIs that accept also java.io.File
Provider<RegularFile> output = layout.buildDirectory.file("myOutput.txt")

// If you really need the java.io.File for a non lazy API
output.get().asFile

// Or a path for a lazy String based API
output.map { it.asFile.path }

Here is another example for creating a directory where the following:

build.gradle.kts
// Returns a java.io.File
file("$buildDir/outputLocation")
build.gradle
// Returns a java.io.File
file("$buildDir/outputLocation")

Should be replaced by:

build.gradle.kts
// Compatible with a number of Gradle APIs that accept a java.io.File
val output: Provider<Directory> = layout.buildDirectory.dir("outputLocation")

// If you really need the java.io.File for a non lazy API
output.get().asFile

// Or a path for a lazy String based API
output.map { it.asFile.path }
build.gradle
// Compatible with a number of Gradle APIs that accept a java.io.File
Provider<Directory> output = layout.buildDirectory.dir("outputLocation")

// If you really need the java.io.File for a non lazy API
output.get().asFile

// Or a path for a lazy String based API
output.map { it.asFile.path }
Deprecated ClientModule dependencies

ClientModule dependencies are deprecated and will be removed in Gradle 9.0.0.

Client module dependencies were originally intended to allow builds to override incorrect or missing component metadata of external dependencies by defining the metadata locally. This functionality has since been replaced by Component Metadata Rules.

Consider the following client module dependency example:

build.gradle.kts
dependencies {
    implementation(module("org:foo:1.0") {
        dependency("org:bar:1.0")
        module("org:baz:1.0") {
            dependency("com:example:1.0")
        }
    })
}
build.gradle
dependencies {
    implementation module("org:foo:1.0") {
        dependency "org:bar:1.0"
        module("org:baz:1.0") {
            dependency "com:example:1.0"
        }
    }
}

This can be replaced with the following component metadata rule:

build-logic/src/main/kotlin/my-plugin.gradle.kts
@CacheableRule
abstract class AddDependenciesRule @Inject constructor(val dependencies: List<String>) : ComponentMetadataRule {
    override fun execute(context: ComponentMetadataContext) {
        listOf("compile", "runtime").forEach { base ->
            context.details.withVariant(base) {
                withDependencies {
                    dependencies.forEach {
                        add(it)
                    }
                }
            }
        }
    }
}
build.gradle.kts
dependencies {
    components {
        withModule<AddDependenciesRule>("org:foo") {
            params(listOf(
                "org:bar:1.0",
                "org:baz:1.0"
            ))
        }
        withModule<AddDependenciesRule>("org:baz") {
            params(listOf("com:example:1.0"))
        }
    }

    implementation("org:foo:1.0")
}
build-logic/src/main/groovy/my-plugin.gradle
@CacheableRule
abstract class AddDependenciesRule implements ComponentMetadataRule {

    List<String> dependencies

    @Inject
    AddDependenciesRule(List<String> dependencies) {
        this.dependencies = dependencies
    }

    @Override
    void execute(ComponentMetadataContext context) {
        ["compile", "runtime"].each { base ->
            context.details.withVariant(base) {
                withDependencies {
                    dependencies.each {
                        add(it)
                    }
                }
            }
        }
    }
}
build.gradle
dependencies {
    components {
        withModule("org:foo", AddDependenciesRule) {
            params([
                "org:bar:1.0",
                "org:baz:1.0"
            ])
        }
        withModule("org:baz", AddDependenciesRule) {
            params(["com:example:1.0"])
        }
    }

    implementation "org:foo:1.0"
}
Earliest supported Develocity plugin version is 3.13.1

Starting in Gradle 9.0.0, the earliest supported Develocity plugin version is 3.13.1. The plugin versions from 3.0 up to 3.13 will be ignored when applied.

Upgrade to version 3.13.1 or later of the Develocity plugin. You can find the latest available version on the Gradle Plugin Portal. More information on the compatibility can be found here.

Upgrading from 8.1 and earlier

Potential breaking changes
Upgrade to Kotlin 1.8.20

The embedded Kotlin has been updated to Kotlin 1.8.20. For more information, see What’s new in Kotlin 1.8.20.

Note that there is a known issue with Kotlin compilation avoidance that can cause OutOfMemory exceptions in compileKotlin tasks if the compilation classpath contains very large JAR files. This applies to builds applying the Kotlin plugin v1.8.20 or the kotlin-dsl plugin.

You can work around it by disabling Kotlin compilation avoidance in your gradle.properties file:

kotlin.incremental.useClasspathSnapshot=false

See KT-57757 for more information.

Upgrade to Groovy 3.0.17

Groovy has been updated to Groovy 3.0.17.

Since the previous version was 3.0.15, the 3.0.16 changes are also included.

Upgrade to Ant 1.10.13

Ant has been updated to Ant 1.10.13.

Since the previous version was 1.10.11, the 1.10.12 changes are also included.

Upgrade to CodeNarc 3.2.0

The default version of CodeNarc has been updated to CodeNarc 3.2.0.

Upgrade to PMD 6.55.0

PMD has been updated to PMD 6.55.0.

Since the previous version was 6.48.0, all changes since then are included.

Upgrade to JaCoCo 0.8.9

JaCoCo has been updated to 0.8.9.

Plugin compatibility changes

A plugin compiled with Gradle >= 8.2 that makes use of the Kotlin DSL functions Project.the<T>(), Project.the(KClass) or Project.configure<T> {} cannot run on Gradle ⇐ 6.1.

Deferred or avoided configuration of some tasks

When performing dependency resolution, Gradle creates an internal representation of the available Configurations. This requires inspecting all configurations and artifacts. Processing artifacts created by tasks causes those tasks to be realized and configured.

This internal representation is now created more lazily, which can change the order in which tasks are configured. Some tasks may never be configured.

This change may cause code paths that relied on a particular order to no longer function, such as conditionally adding attributes to a configuration based on the presence of certain attributes.

This impacted the bnd plugin and JUnit5 build.

We recommend not modifying domain objects (configurations, source sets, tasks, etc) from configuration blocks for other domain objects that may not be configured.

For example, avoid doing something like this:

    configurations {
        val myConfig = create("myConfig")
    }

    tasks.register("myTask") {
            // This is not safe, as the execution of this block may not occur, or may not occur in the order expected
          configurations["myConfig"].attributes {
              attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class.java, Usage.JAVA_RUNTIME))
          }
    }
Deprecations
CompileOptions method deprecations

The following methods on CompileOptions are deprecated:

  • getAnnotationProcessorGeneratedSourcesDirectory()

  • setAnnotationProcessorGeneratedSourcesDirectory(File)

  • setAnnotationProcessorGeneratedSourcesDirectory(Provider<File>)

Current usages of these methods should migrate to DirectoryProperty getGeneratedSourceOutputDirectory()

Using configurations incorrectly

Gradle will now warn at runtime when methods of Configuration are called inconsistently with the configuration’s intended usage.

This change is part of a larger ongoing effort to make the intended behavior of configurations more consistent and predictable and to unlock further speed and memory improvements.

Currently, the following methods should only be called with these listed allowed usages:

  • resolve() - RESOLVABLE configurations only

  • files(Closure), files(Spec), files(Dependency…), fileCollection(Spec), fileCollection(Closure), fileCollection(Dependency…) - RESOLVABLE configurations only

  • getResolvedConfigurations() - RESOLVABLE configurations only

  • defaultDependencies(Action) - DECLARABLE configurations only

  • shouldResolveConsistentlyWith(Configuration) - RESOLVABLE configurations only

  • disableConsistentResolution() - RESOLVABLE configurations only

  • getDependencyConstraints() - DECLARABLE configurations only

  • copy(), copy(Spec), copy(Closure), copyRecursive(), copyRecursive(Spec), copyRecursive(Closure) - RESOLVABLE configurations only

Intended usage is noted in the Configuration interface’s Javadoc. This list is likely to grow in future releases.

Starting in Gradle 9.0.0, using a configuration inconsistently with its intended usage will be prohibited.

Also note that although it is not currently restricted, the getDependencies() method is only intended for use with DECLARABLE configurations. The getAllDependencies() method, which retrieves all declared dependencies on a configuration and any superconfigurations, will not be restricted to any particular usage.

Deprecated access to plugin conventions

The concept of conventions is outdated and superseded by extensions to provide custom DSLs.

To reflect this in the Gradle API, the following elements are deprecated:

  • org.gradle.api.Project.getConvention()

  • org.gradle.api.plugins.Convention

  • org.gradle.api.internal.HasConvention

Gradle Core plugins still register their conventions in addition to their extensions for backwards compatibility.

It is deprecated to access any of these conventions and their properties. Doing so will now emit a deprecation warning. This will become an error in Gradle 9.0.0. You should prefer accessing the extensions and their properties instead.

For specific examples, see the next sections.

Prominent community plugins already migrated to using extensions to provide custom DSLs. Some of them still register conventions for backward compatibility. Registering conventions does not emit a deprecation warning yet to provide a migration window. Future Gradle versions will do.

Also note that Plugins compiled with Gradle ⇐ 8.1 that make use of the Kotlin DSL functions Project.the<T>(), Project.the(KClass) or Project.configure<T> {} will emit a deprecation warning when run on Gradle >= 8.2. To fix this these plugins should be recompiled with Gradle >= 8.2 or changed to access extensions directly using extensions.getByType<T>() instead.

Deprecated base plugin conventions

The convention properties contributed by the base plugin have been deprecated and scheduled for removal in Gradle 9.0.0. For more context, see the section about plugin convention deprecation.

The conventions are replaced by the base { } configuration block backed by BasePluginExtension. The old convention object defines the distsDirName, libsDirName, and archivesBaseName properties with simple getter and setter methods. Those methods are available in the extension only to maintain backward compatibility. Build scripts should solely use the properties of type Property:

build.gradle.kts
plugins {
    base
}

base {
    archivesName.set("gradle")
    distsDirectory.set(layout.buildDirectory.dir("custom-dist"))
    libsDirectory.set(layout.buildDirectory.dir("custom-libs"))
}
build.gradle
plugins {
    id 'base'
}

base {
    archivesName = "gradle"
    distsDirectory = layout.buildDirectory.dir('custom-dist')
    libsDirectory = layout.buildDirectory.dir('custom-libs')
}
Deprecated application plugin conventions

The convention properties the application plugin contributed have been deprecated and scheduled for removal in Gradle 9.0.0. For more context, see the section about plugin convention deprecation.

The following code will now emit deprecation warnings:

build.gradle.kts
plugins {
    application
}

applicationDefaultJvmArgs = listOf("-Dgreeting.language=en") // Accessing a convention
build.gradle
plugins {
    id 'application'
}

applicationDefaultJvmArgs = ['-Dgreeting.language=en'] // Accessing a convention

This should be changed to use the application { } configuration block, backed by JavaApplication, instead:

build.gradle.kts
plugins {
    application
}

application {
    applicationDefaultJvmArgs = listOf("-Dgreeting.language=en")
}
build.gradle
plugins {
    id 'application'
}

application {
    applicationDefaultJvmArgs = ['-Dgreeting.language=en']
}
Deprecated java plugin conventions

The convention properties the java plugin contributed have been deprecated and scheduled for removal in Gradle 9.0.0. For more context, see the section about plugin convention deprecation.

The following code will now emit deprecation warnings:

build.gradle.kts
plugins {
    id("java")
}

configure<JavaPluginConvention> { // Accessing a convention
    sourceCompatibility = JavaVersion.VERSION_18
}
build.gradle
plugins {
    id 'java'
}

sourceCompatibility = 18 // Accessing a convention

This should be changed to use the java { } configuration block, backed by JavaPluginExtension, instead:

build.gradle.kts
plugins {
    id("java")
}

java {
    sourceCompatibility = JavaVersion.VERSION_18
}
build.gradle
plugins {
    id 'java'
}

java {
    sourceCompatibility = JavaVersion.VERSION_18
}
Deprecated war plugin conventions

The convention properties contributed by the war plugin have been deprecated and scheduled for removal in Gradle 9.0.0. For more context, see the section about plugin convention deprecation.

The following code will now emit deprecation warnings:

build.gradle.kts
plugins {
    id("war")
}

configure<WarPluginConvention> { // Accessing a convention
    webAppDirName = "src/main/webapp"
}
build.gradle
plugins {
    id 'war'
}

webAppDirName = 'src/main/webapp' // Accessing a convention

Clients should configure the war task directly. Also, tasks.withType(War.class).configureEach(…​) can be used to configure each task of type War.

build.gradle.kts
plugins {
    id("war")
}

tasks.war {
    webAppDirectory.set(file("src/main/webapp"))
}
build.gradle
plugins {
    id 'war'
}

war {
    webAppDirectory = file('src/main/webapp')
}
Deprecated ear plugin conventions

The convention properties contributed by the ear plugin have been deprecated and scheduled for removal in Gradle 9.0.0. For more context, see the section about plugin convention deprecation.

The following code will now emit deprecation warnings:

build.gradle.kts
plugins {
    id("ear")
}

configure<EarPluginConvention> { // Accessing a convention
    appDirName = "src/main/app"
}
build.gradle
plugins {
    id 'ear'
}

appDirName = 'src/main/app' // Accessing a convention

Clients should configure the ear task directly. Also, tasks.withType(Ear.class).configureEach(…​) can be used to configure each task of type Ear.

build.gradle.kts
plugins {
    id("ear")
}

tasks.ear {
    appDirectory.set(file("src/main/app"))
}
build.gradle
plugins {
    id 'ear'
}

ear {
    appDirectory = file('src/main/app')  // use application metadata found in this folder
}
Deprecated project-report plugin conventions

The convention properties contributed by the project-reports plugin have been deprecated and scheduled for removal in Gradle 9.0.0. For more context, see the section about plugin convention deprecation.

The following code will now emit deprecation warnings:

build.gradle.kts
plugins {
    `project-report`
}

configure<ProjectReportsPluginConvention> {
    projectReportDirName = "custom" // Accessing a convention
}
build.gradle
plugins {
    id 'project-report'
}

projectReportDirName = "custom" // Accessing a convention

Configure your report task instead:

build.gradle.kts
plugins {
    `project-report`
}

tasks.withType<HtmlDependencyReportTask>() {
    projectReportDirectory.set(project.layout.buildDirectory.dir("reports/custom"))
}
build.gradle
plugins {
    id 'project-report'
}

tasks.withType(HtmlDependencyReportTask) {
    projectReportDirectory = project.layout.buildDirectory.dir("reports/custom")
}
Configuration method deprecations

The following method on Configuration is deprecated for removal:

  • getAll()

Obtain the set of all configurations from the project’s configurations container instead.

Relying on automatic test framework implementation dependencies

In some cases, Gradle will load JVM test framework dependencies from the Gradle distribution to execute tests. This existing behavior can lead to test framework dependency version conflicts on the test classpath. To avoid these conflicts, this behavior is deprecated and will be removed in Gradle 9.0.0. Tests using TestNG are unaffected.

To prepare for this change in behavior, either declare the required dependencies explicitly or migrate to Test Suites, where these dependencies are managed automatically.

Test Suites

Builds that use test suites will not be affected by this change. Test suites manage the test framework dependencies automatically and do not require dependencies to be explicitly declared. See the user manual for further information on migrating to test suites.

Manually declaring dependencies

In the absence of test suites, dependencies must be manually declared on the test runtime classpath:

  • If using JUnit 5, an explicit runtimeOnly dependency on junit-platform-launcher is required in addition to the existing implementation dependency on the test engine.

  • If using JUnit 4, only the existing implementation dependency on junit 4 is required.

  • If using JUnit 3, a test runtimeOnly dependency on junit 4 is required in addition to a compileOnly dependency on junit 3.

build.gradle.kts
dependencies {
    // If using JUnit Jupiter
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // If using JUnit Vintage
    testCompileOnly("junit:junit:4.13.2")
    testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.9.2")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // If using JUnit 4
    testImplementation("junit:junit:4.13.2")

    // If using JUnit 3
    testCompileOnly("junit:junit:3.8.2")
    testRuntimeOnly("junit:junit:4.13.2")
}
build.gradle
dependencies {
    // If using JUnit Jupiter
    testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // If using JUnit Vintage
    testCompileOnly 'junit:junit:4.13.2'
    testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.9.2'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // If using JUnit 4
    testImplementation 'junit:junit:4.13.2'

    // If using JUnit 3
    testCompileOnly 'junit:junit:3.8.2'
    testRuntimeOnly 'junit:junit:4.13.2'
}
BuildIdentifier and ProjectComponentSelector method deprecations

The following methods on BuildIdentifier are deprecated:

  • getName()

  • isCurrentBuild()

You could use these methods to distinguish between different project components with the same name but from different builds. However, for certain composite build setups, these methods do not provide enough information to guarantee uniqueness.

Current usages of these methods should migrate to BuildIdentifier.getBuildPath().

Similarly, the method ProjectComponentSelector.getBuildName() is deprecated. Use ProjectComponentSelector.getBuildPath() instead.

Upgrading from 8.0 and earlier

CACHEDIR.TAG files are created in global cache directories

Gradle now emits a CACHEDIR.TAG file in some global cache directories, as specified in Cache marking.

This may cause these directories to no longer be searched or backed up by some tools. To disable it, use the following code in an init script in the Gradle User Home:

init.gradle.kts
beforeSettings {
    caches {
        // Disable cache marking for all caches
        markingStrategy.set(MarkingStrategy.NONE)
    }
}
init.gradle
beforeSettings { settings ->
    settings.caches {
        // Disable cache marking for all caches
        markingStrategy = MarkingStrategy.NONE
    }
}
Configuration cache options renamed

In this release, the configuration cache feature was promoted from incubating to stable. As such, all properties originally mentioned in the feature documentation (which had an unsafe part in their names, e.g., org.gradle.unsafe.configuration-cache) were renamed, in some cases, by removing the unsafe part of the name.

Incubating property Finalized property

org.gradle.unsafe.configuration-cache

org.gradle.configuration-cache

org.gradle.unsafe.configuration-cache-problems

org.gradle.configuration-cache.problems*

org.gradle.unsafe.configuration-cache.max-problems

org.gradle.configuration-cache.max-problems

Note that the original org.gradle.unsafe.configuration-cache…​ properties continue to be honored in this release, and no warnings will be produced if they are used, but they will be deprecated and removed in a future release.

Potential breaking changes
Kotlin DSL scripts emit compilation warnings

Compilation warnings from Kotlin DSL scripts are printed to the console output. For example, the use of deprecated APIs in Kotlin DSL will emit warnings each time the script is compiled.

This is a potentially breaking change if you are consuming the console output of Gradle builds.

Configuring Kotlin compiler options with the kotlin-dsl plugin applied

If you are configuring custom Kotlin compiler options on a project with the kotlin-dsl plugin applied you might encounter a breaking change.

In previous Gradle versions, the kotlin-dsl plugin was adding required compiler arguments on afterEvaluate {}. Now that the Kotlin Gradle Plugin provides lazy configuration properties, our kotlin-dsl plugin switched to adding required compiler arguments to the lazy properties directly. As a consequence, if you were setting freeCompilerArgs the kotlin-dsl plugin is now failing the build because its required compiler arguments are overridden by your configuration.

build.gradle.kts
plugins {
    `kotlin-dsl`
}

tasks.withType(KotlinCompile::class).configureEach {
    kotlinOptions { // Deprecated non-lazy configuration options
        freeCompilerArgs = listOf("-Xcontext-receivers")
    }
}

With the configuration above you would get the following build failure:

* What went wrong
Execution failed for task ':compileKotlin'.
> Kotlin compiler arguments of task ':compileKotlin' do not work for the `kotlin-dsl` plugin. The 'freeCompilerArgs' property has been reassigned. It must instead be appended to. Please use 'freeCompilerArgs.addAll(\"your\", \"args\")' to fix this.

You must change this to adding your custom compiler arguments to the lazy configuration properties of the Kotlin Gradle Plugin for them to be appended to the ones required by the kotlin-dsl plugin:

build.gradle.kts
plugins {
    `kotlin-dsl`
}

tasks.withType(KotlinCompile::class).configureEach {
    compilerOptions { // New lazy configuration options
        freeCompilerArgs.addAll("-Xcontext-receivers")
    }
}

If you were already adding to freeCompilerArgs instead of setting its value, you should not experience a build failure.

New API introduced may clash with existing Gradle DSL code

When a new property or method is added to an existing type in the Gradle DSL, it may clash with names already used in user code.

When a name clash occurs, one solution is to rename the element in user code.

This is a non-exhaustive list of API additions in 8.1 that may cause name collisions with existing user code.

Using unsupported API to start external processes at configuration time is no longer allowed with the configuration cache enabled

Since Gradle 7.5, using Project.exec, Project.javaexec, and standard Java and Groovy APIs to run external processes at configuration time has been considered an error only if the feature preview STABLE_CONFIGURATION_CACHE was enabled. With the configuration cache promotion to a stable feature in Gradle 8.1, this error is detected regardless of the feature preview status. The configuration cache chapter has more details to help with the migration to the new provider-based APIs to execute external processes at configuration time.

Builds that do not use the configuration cache, or only start external processes at execution time are not affected by this change.

Deprecations
Mutating core plugin configuration usage

The allowed usage of a configuration should be immutable after creation. Mutating the allowed usage on a configuration created by a Gradle core plugin is deprecated. This includes calling any of the following Configuration methods:

  • setCanBeConsumed(boolean)

  • setCanBeResolved(boolean)

These methods now emit deprecation warnings on these configurations, except for certain special cases which make allowances for the existing behavior of popular plugins. This rule does not yet apply to detached configurations or configurations created in buildscripts and third-party plugins. Calling setCanBeConsumed(false) on apiElements or runtimeElements is not yet deprecated in order to avoid warnings that would be otherwise emitted when using select popular third-party plugins.

This change is part of a larger ongoing effort to make the intended behavior of configurations more consistent and predictable, and to unlock further speed and memory improvements in this area of Gradle.

The ability to change the allowed usage of a configuration after creation will be removed in Gradle 9.0.0.

Reserved configuration names

Configuration names "detachedConfiguration" and "detachedConfigurationX" (where X is any integer) are reserved for internal use when creating detached configurations.

The ability to create non-detached configurations with these names will be removed in Gradle 9.0.0.

Calling select methods on the JavaPluginExtension without the java component present

Starting in Gradle 8.1, calling any of the following methods on JavaPluginExtension without the presence of the default java component is deprecated:

  • withJavadocJar()

  • withSourcesJar()

  • consistentResolution(Action)

This java component is added by the JavaPlugin, which is applied by any of the Gradle JVM plugins including:

  • java-library

  • application

  • groovy

  • scala

Starting in Gradle 9.0.0, calling any of the above listed methods without the presence of the default java component will become an error.

WarPlugin#configureConfiguration(ConfigurationContainer)

Starting in Gradle 8.1, calling WarPlugin#configureConfiguration(ConfigurationContainer) is deprecated. This method was intended for internal use and was never intended to be used as part of the public interface.

Starting in Gradle 9.0.0, this method will be removed without replacement.

Relying on conventions for custom Test tasks

By default, when applying the java plugin, the testClassesDirs and classpath of all Test tasks have the same convention. Unless otherwise changed, the default behavior is to execute the tests from the default test TestSuite by configuring the task with the classpath and testClassesDirs from the test suite. This behavior will be removed in Gradle 9.0.0.

While this existing default behavior is correct for the use case of executing the default unit test suite under a different environment, it does not support the use case of executing an entirely separate set of tests.

If you wish to continue including these tests, use the following code to avoid the deprecation warning in 8.1 and prepare for the behavior change in 9.0.0. Alternatively, consider migrating to test suites.

build.gradle.kts
val test by testing.suites.existing(JvmTestSuite::class)
tasks.named<Test>("myTestTask") {
    testClassesDirs = files(test.map { it.sources.output.classesDirs })
    classpath = files(test.map { it.sources.runtimeClasspath })
}
build.gradle
tasks.myTestTask {
    testClassesDirs = testing.suites.test.sources.output.classesDirs
    classpath = testing.suites.test.sources.runtimeClasspath
}
Modifying Gradle Module Metadata after a publication has been populated

Altering the GMM (e.g., changing a component configuration variants) after a Maven or Ivy publication has been populated from their components is now deprecated. This feature will be removed in Gradle 9.0.0.

Eager population of the publication can happen if the following methods are called:

Previously, the following code did not generate warnings, but it created inconsistencies between published artifacts:

build.gradle.kts
publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["java"])
        }
        create<IvyPublication>("ivy") {
            from(components["java"])
        }
    }
}

// These calls eagerly populate the Maven and Ivy publications

(publishing.publications["maven"] as MavenPublication).artifacts
(publishing.publications["ivy"] as IvyPublication).artifacts

val javaComponent = components["java"] as AdhocComponentWithVariants
javaComponent.withVariantsFromConfiguration(configurations["apiElements"]) { skip() }
javaComponent.withVariantsFromConfiguration(configurations["runtimeElements"]) { skip() }
build.gradle
publishing {
    publications {
        maven(MavenPublication) {
            from components.java
        }
        ivy(IvyPublication) {
            from components.java
        }
    }
}

// These calls eagerly populate the Maven and Ivy publications

publishing.publications.maven.artifacts
publishing.publications.ivy.artifacts

components.java.withVariantsFromConfiguration(configurations.apiElements) { skip() }
components.java.withVariantsFromConfiguration(configurations.runtimeElements) { skip() }

In this example, the Maven and Ivy publications will contain the main JAR artifacts for the project, whereas the GMM module file will omit them.

Running tests on JVM versions 6 and 7

Running JVM tests on JVM versions older than 8 is deprecated. Testing on these versions will become an error in Gradle 9.0.0

Applying Kotlin DSL precompiled scripts published with Gradle < 6.0

Applying Kotlin DSL precompiled scripts published with Gradle < 6.0 is deprecated. Please use a version of the plugin published with Gradle >= 6.0.

Applying the kotlin-dsl together with Kotlin Gradle Plugin < 1.8.0

Applying the kotlin-dsl together with Kotlin Gradle Plugin < 1.8.0 is deprecated. Please let Gradle control the version of kotlin-dsl by removing any explicit kotlin-dsl version constraints from your build logic. This will let the kotlin-dsl plugin decide which version of the Kotlin Gradle Plugin to use. If you explicitly declare which version of the Kotlin Gradle Plugin to use for your build logic, update it to >= 1.8.0.

Accessing libraries or bundles from dependency version catalogs in the plugins {} block of a Kotlin script

Accessing libraries or bundles from dependency version catalogs in the plugins {} block of a Kotlin script is deprecated. Please only use versions or plugins from dependency version catalogs in the plugins {} block.

Using ValidatePlugins task without a Java Toolchain

Using a task of type ValidatePlugins without applying the Java Toolchains plugin —or any other Java plugin that applies the Java Toolchains plugin—was deprecated in Gradle 8.1 and is now an error in Gradle 9.0.0.

Deprecated members of the org.gradle.util package now report their deprecation

These members will be removed in Gradle 9.0.0.

  • WrapUtil.toDomainObjectSet(…​)

  • GUtil.toCamelCase(…​)

  • GUtil.toLowerCase(…​)

  • ConfigureUtil

Deprecated JVM vendor IBM Semeru

The enum constant JvmVendorSpec.IBM_SEMERU is now deprecated and will be removed in Gradle 9.0.0.

Please replace it by its equivalent JvmVendorSpec.IBM to avoid warnings and potential errors in the next major version release.

Setting custom build layout on StartParameter and GradleBuild

Following the related previous deprecation of the behaviour in Gradle 7.1, it is now also deprecated to use related StartParameter and GradleBuild properties. These properties will be removed in Gradle 9.0.0.

Setting custom build file using buildFile property in GradleBuild task has been deprecated.

Please use the dir property instead to specify the root of the nested build. Alternatively, consider using one of the recommended alternatives for GradleBuild task.

Setting custom build layout using StartParameter methods setBuildFile(File) and setSettingsFile(File) as well as the counterpart getters getBuildFile() and getSettingsFile() have been deprecated.

Please use standard locations for settings and build files:

  • settings file in the root of the build

  • build file in the root of each subproject

Deprecated org.gradle.cache.cleanup property

The org.gradle.cache.cleanup property in gradle.properties under Gradle User Home has been deprecated. Please use the cache cleanup DSL instead to disable or modify the cleanup configuration.

Since the org.gradle.cache.cleanup property may still be needed for older versions of Gradle, this property may still be present and no deprecation warnings will be printed as long as it is also configured via the DSL. The DSL value will always take preference over the org.gradle.cache.cleanup property. If the desired configuration is to disable cleanup for older versions of Gradle (using org.gradle.cache.cleanup), but to enable cleanup with the default values for Gradle versions at or above Gradle 8, then cleanup should be configured to use Cleanup.DEFAULT:

cache-settings.gradle.kts
if (GradleVersion.current() >= GradleVersion.version("8.0")) {
    apply(from = "gradle8/cache-settings.gradle.kts")
}
cache-settings.gradle
if (GradleVersion.current() >= GradleVersion.version('8.0')) {
    apply from: "gradle8/cache-settings.gradle"
}
gradle8/cache-settings.gradle.kts
beforeSettings {
    caches {
        cleanup.set(Cleanup.DEFAULT)
    }
}
gradle8/cache-settings.gradle
beforeSettings { settings ->
    settings.caches {
        cleanup = Cleanup.DEFAULT
    }
}
Deprecated using relative paths to specify Java executables

Using relative file paths to point to Java executables is now deprecated and will become an error in Gradle 10. This is done to reduce confusion about what such relative paths should resolve against.

Calling Task.getConvention(), Task.getExtensions() from a task action

Calling Task.getConvention(), Task.getExtensions() from a task action at execution time is now deprecated and will be made an error in Gradle 9.0.0.

See the configuration cache chapter for details on how to migrate these usages to APIs that are supported by the configuration cache.

Deprecated running test task successfully when no test executed

Running the Test task successfully when no test was executed is now deprecated and will become an error in Gradle 9. Note that it is not an error when no test sources are present, in this case the test task is simply skipped. It is only an error when test sources are present, but no test was selected for execution. This is changed to avoid accidental successful test runs due to erroneous configuration.

Changes in the IDE integration
Workaround for false positive errors shown in Kotlin DSL plugins {} block using version catalog is not needed anymore

Version catalog accessors for plugin aliases in the plugins {} block aren’t shown as errors in IntelliJ IDEA and Android Studio Kotlin script editor anymore.

If you were using the @Suppress("DSL_SCOPE_VIOLATION") annotation as a workaround, you can now remove it.

If you were using the Gradle Libs Error Suppressor IntelliJ IDEA plugin, you can now uninstall it.

After upgrading Gradle to 8.1 you will need to clear the IDE caches and restart.

Upgrading from Gradle 7.x to 8.0

This chapter provides the information you need to migrate your Gradle 7.x builds to Gradle 8.0. For migrating from Gradle 6.x, see the older migration guide first.

We recommend the following steps for all users:

  1. Try running gradle help --scan and view the deprecations view of the generated Build Scan.

    Deprecations View in a Build Scan

    This is so that you can see any deprecation warnings that apply to your build.

    Alternatively, you can run gradle help --warning-mode=all to see the deprecations in the console, though it may not report as much detailed information.

  2. Update your plugins.

    Some plugins will break with this new version of Gradle, for example because they use internal APIs that have been removed or changed. The previous step will help you identify potential problems by issuing deprecation warnings when a plugin does try to use a deprecated part of the API.

  3. Run gradle :wrapper --gradle-version 8.0.2 to update the project to 8.0.2.

  4. Try to run the project and debug any errors using the Troubleshooting Guide.

Upgrading from 7.6 and earlier

Warnings that are now errors
Referencing tasks in an included build with finalizedBy, mustRunAfter or shouldRunAfter

Referencing tasks contained in an included build with any of the following methods now results in an execution time error:

  • finalizedBy

  • mustRunAfter

  • shouldRunAfter

Creating TAR trees from resources without backing files

Creating a TAR tree from a resource with no backing file is no longer supported. Instead, convert the resource to a file and use project.tarTree() on the file. For more information, see TAR trees from resources without backing files.

Using invalid Java toolchain specifications

Usage of invalid Java toolchain specifications is no longer supported. Related build errors can be avoided by making sure that language version is set on all toolchain specifications. See user manual for more information.

Using automatic toolchain downloading without having a repository configured

Automatic toolchain downloading without explicitly providing repositories to use is no longer supported. See user manual for more information.

Changing test framework after setting test framework options is now an error

When configuring the built-in test task for Java, Groovy, and Scala projects, Gradle no longer allows you to change the test framework used by the Test task after configuring options. This was deprecated since it silently discarded configuration in some cases.

The following code example now produces an error:

test {
   options {
   }

   useJUnitPlatform()
}

Instead, you can:

test {
   // select test framework before configuring options
   useJUnitPlatform()
   options {
   }
}

Additionally, setting the test framework multiple times to the same framework now accumulates any options that might be set on the framework. Previously, each time the framework was set, it would cause the framework options to be overwritten.

The following code now results in both the "foo" and "bar" tags to be included for the test task:

test {
   useJUnitPlatform {
        includeTags("foo")
   }
}
tasks.withType(Test).configureEach {
   // previously, this would overwrite the included tags to only include "bar"
   useJUnitPlatform {
        includeTags("bar")
   }
}
Removed APIs
Legacy ArtifactTransform API

The legacy ArtifactTransform API has been removed. For more information, see Registering artifact transforms extending ArtifactTransform.

Legacy IncrementalTaskInputs API

The legacy IncrementalTaskInputs API has been removed. For more information, see IncrementalTaskInputs type is deprecated. This change also affects Kotlin Gradle Plugin and Android Gradle Plugin. With Gradle 8.0 you should use Kotlin Gradle Plugin 1.6.10 or later and Android Gradle Plugin 7.3.0 with android.experimental.legacyTransform.forceNonIncremental=true property or later.

Legacy AntlrSourceVirtualDirectory API

The legacy AntlrSourceVirtualDirectory API has been removed. This change affects the antlr plugin. In Gradle 8.0 and above, use the AntlrSourceDirectorySet source set extension instead.

JvmPluginsHelper

A deprecated configureDocumentationVariantWithArtifact method of the JvmPluginsHelper class which did not require a FileResolver has been removed. This was an internal API, but may have been accessed by plugins. Supply a FileResolver to the overloaded version of this method instead.

Groovydoc API Cleanup

The deprecated isIncludePrivate property of the Groovydoc task type has been removed. Use the access property along with the GroovydocAccess#PRIVATE constant instead.

JavaApplication API Cleanup

The deprecated mainClassName property of the JavaApplication interface has been removed. Use the mainClass property instead.

DefaultDomainObjectSet API Cleanup

The deprecated DefaultDomainObjectSet(Class) constructor has been removed. This was an internal API, but may have been used by plugins.

JacocoPluginExtension API Cleanup

The deprecated reportsDir property of the JacocoPluginExtension has been removed. Use the reportsDirectory property instead.

DependencyInsightReportTask API Cleanup

The deprecated legacyShowSinglePathToDependnecy property of the DependencyInsightReportTask task type has been removed. Use the showSinglePathToDependency property instead.

Report and TestReport API Cleanup

The deprecated destination, and enabled properties of the Report type have been removed. Use the outputLocation and required properties instead.

The deprecated testResultDirs property of the TestReport task type has been removed. Use the testResults property instead.

JacocoMerge Task Removed

The deprecated JacocoMerge task type has been removed. The same functionality is also available on the JacocoReport task.

JavaExec API Cleanup

The deprecated main property of the JavaExec task type has been removed. Use the mainClass property instead.

AbstractExecTask API Cleanup

The deprecated execResult getter property of the AbstractExecTask task type has been removed. Use the executionResult getter property instead.

AbstractTestTask API Cleanup

The deprecated binResultsDir property of the AbstractTestTask task type has been removed. Use the binaryResultsDirectory property instead.

SourceDirectorySet API Cleanup

The deprecated outputDir property of the SourceDirectorySet type has been removed. Use the destinationDirectory property instead.

VersionCatalog API Cleanup

The deprecated findDependency(String) method and dependencyAliases property of the VersionCatalog type have been removed. Use the findLibrary(String) method and libraryAliases property instead.

The deprecated alias(String) method of the VersionCatalogBuilder type has been removed. Use the library(String, String, String) or plugin(String, String) methods instead.

WorkerExecutor API Cleanup

The deprecated submit(Class, Action) method of the WorkerExecutor interface has been removed. Instead, obtain a WorkQueue via the noIsolation(), classLoaderIsolation(), and processIsolation(), methods and use the submit(Class, Action) method on the WorkQueue instead.

DependencySubstitution API Cleanup

The deprecated with(ComponentSelector) method of the DependencySubstitution type’s inner Substitution type’s has been removed. Use the using(ComponentSelector) method instead.

AbstractArchiveTask API Cleanup

The deprecated appendix, archiveName, archivePath, baseName, classifier, destinationDir, extension and version properties of the AbstractArchiveTask task type have been removed. Use the archiveAppendix, archiveFileName , archiveFile, archiveBaseName, archiveClassifier, destinationDirectory, archiveExtension and archiveVersion properties instead.

AbstractCompile API Deprecations

The previously deprecated destinationDir property of the AbstractCompile remains deprecated, and will now emit a deprecation warning upon use. It is now scheduled for removal in Gradle 9.0.0. Use the destinationDirectory property instead.

ResolvedComponentResult API Cleanup

The deprecated getVariant method of the ResolvedComponentResult interface has been removed. Use the getVariants method instead.

Code quality plugins API Cleanup

The deprecated antBuilder property of the Checkstyle, CodeNarc and Pmd task types has been removed. Use the Project type’s ant property instead.

Usage API Cleanup

The deprecated public fields JAVA_API_CLASSES, JAVA_API_JARS, JAVA_RUNTIME_CLASSES, JAVA_RUNTIME_JARS and JAVA_RUNTIME_RESOURCES of the Usage type have been removed. The values are available in the internal JavaEcosystemSupport class for compatibility with previously published modules, but should not be used for any new publishing.

ExternalDependency API Cleanup

The deprecated setForce(boolean) method of the ExternalDependency interface has been removed. Use the version(Action) method to configure strict versions instead.

Build-scan method removed from Kotlin DSL

The deprecated build-scan plugin application method has been removed from the Kotlin DSL. Use the gradle-enterprise method instead.

Configuration extension methods removed from Kotlin DSL

The Kotlin DSL added specialized extension methods for NamedDomainObjectProvider<Configuration> that are available when looking up a configuration by name. These extensions allowed builds to access some properties of a Configuration when using an instance of NamedDomainObjectProvider<Configuration> directly:

configurations.compileClasspath.files // equivalent to configurations.compileClasspath.get().files
configurations.compileClasspath.singleFile // equivalent to configurations.compileClasspath.get().singleFile

All of these extensions have been removed from the API, but the methods are still available for plugins compiled against older versions of Gradle.

  • NamedDomainObjectProvider<Configuration>.addToAntBuilder

  • NamedDomainObjectProvider<Configuration>.all

  • NamedDomainObjectProvider<Configuration>.allArtifacts

  • NamedDomainObjectProvider<Configuration>.allDependencies

  • NamedDomainObjectProvider<Configuration>.allDependencyConstraints

  • NamedDomainObjectProvider<Configuration>.artifacts

  • NamedDomainObjectProvider<Configuration>.asFileTree

  • NamedDomainObjectProvider<Configuration>.asPath

  • NamedDomainObjectProvider<Configuration>.attributes

  • NamedDomainObjectProvider<Configuration>.buildDependencies

  • NamedDomainObjectProvider<Configuration>.contains

  • NamedDomainObjectProvider<Configuration>.copy

  • NamedDomainObjectProvider<Configuration>.copyRecursive

  • NamedDomainObjectProvider<Configuration>.defaultDependencies

  • NamedDomainObjectProvider<Configuration>.dependencies

  • NamedDomainObjectProvider<Configuration>.dependencyConstraints

  • NamedDomainObjectProvider<Configuration>.description

  • NamedDomainObjectProvider<Configuration>.exclude

  • NamedDomainObjectProvider<Configuration>.excludeRules

  • NamedDomainObjectProvider<Configuration>.extendsFrom

  • NamedDomainObjectProvider<Configuration>.fileCollection

  • NamedDomainObjectProvider<Configuration>.files

  • NamedDomainObjectProvider<Configuration>.filter

  • NamedDomainObjectProvider<Configuration>.getTaskDependencyFromProjectDependency

  • NamedDomainObjectProvider<Configuration>.hierarchy

  • NamedDomainObjectProvider<Configuration>.incoming

  • NamedDomainObjectProvider<Configuration>.isCanBeConsumed

  • NamedDomainObjectProvider<Configuration>.isCanBeResolved

  • NamedDomainObjectProvider<Configuration>.isEmpty

  • NamedDomainObjectProvider<Configuration>.isTransitive

  • NamedDomainObjectProvider<Configuration>.isVisible

  • NamedDomainObjectProvider<Configuration>.minus

  • NamedDomainObjectProvider<Configuration>.outgoing

  • NamedDomainObjectProvider<Configuration>.plus

  • NamedDomainObjectProvider<Configuration>.resolutionStrategy

  • NamedDomainObjectProvider<Configuration>.resolve

  • NamedDomainObjectProvider<Configuration>.resolvedConfiguration

  • NamedDomainObjectProvider<Configuration>.setDescription

  • NamedDomainObjectProvider<Configuration>.setExtendsFrom

  • NamedDomainObjectProvider<Configuration>.setTransitive

  • NamedDomainObjectProvider<Configuration>.singleFile

  • NamedDomainObjectProvider<Configuration>.state

  • NamedDomainObjectProvider<Configuration>.withDependencies

You should prefer to directly reference the methods from Configuration.

Potential breaking changes
JavaForkOptions getJvmArgs() and getAllJvmArgs() return immutable lists

The lists of JVM arguments retrieved from the JavaForkOptions interface are now immutable.

Previously, modifications of the returned list were silently ignored.

Nullable annotations better reflect actual nullability of API

In some APIs, nullability was not correctly annotated and APIs that did allow null or returned null were marked as non-null. In Java or Groovy, this mismatch did not cause problems at compile time. In Kotlin, this mismatch made valid code difficult to write because the language would not allow you to pass null.

One particular example was returning null from a Provider#map or Provider#flatMap. In both APIs, Gradle allows you to return null, but in the Kotlin DSL this was considered illegal.

This correction may cause compilation errors in code that expected non-null.

Plugins, tasks and extension classes are abstract

Most public classes for plugins, tasks and extensions have been made abstract. This was done to make it easier to remove boilerplate from Gradle’s implementation.

Plugins that are affected by this change should make their classes abstract as well. Gradle uses runtime class decoration to implement abstract methods as long as the object is instantiated via ObjectFactory or some other automatic mechanism (like managed properties). Those methods should never be directly implemented.

Wrapper task configuration

If gradle-wrapper.properties contains the distributionSha256Sum property, you must specify a sum. You can specify a sum in the wrapped task configuration or with the --gradle-distribution-sha256-sum task option.

Changes in the AbstractCodeQualityPlugin class

The deprecated AbstractCodeQualityPlugin.getJavaPluginConvention() method was removed in Gradle 8.0. You should use JavaPluginExtension instead.

Remove implicit --add-opens for Gradle workers

Before Gradle 8.0, Gradle workers on JDK9+ automatically opened JDK modules java.base/java.util and java.base/java.lang by passing --add-opens CLI arguments. This enabled code executed in a Gradle worker to perform deep reflection on JDK internals without warning or failing. Workers no longer use these implicit arguments.

This affects all internal Gradle workers, which are used for a variety of tasks:

  • code-quality plugins (Checkstyle, CodeNarc, Pmd)

  • ScalaDoc

  • AntlrTask

  • JVM compiler daemons

  • tasks executed using process isolation via the Worker API

New warnings and errors may appear in any tools, extensions, or plugins that perform deep reflection into JDK internals with the worker API.

These errors can be resolved by updating the violating code or dependency. Updates may include:

  • code-quality tools

  • annotation processors

  • any Gradle plugins which use the worker API

For some examples of possible error or warning outputs which may arise due to this change, see Removes implicit --add-opens for test workers.

SourceSet classesDirs no longer depends upon the entire SourceSet as a task dependency

Prior to Gradle 8.0, the task dependencies for SourceSetOutput.classesDirs included tasks that did not produce class files. This meant that a task which depends on classesDirs would also depend on classes, processResources, and any other task dependency added to SourceSetOutput. This behavior was potentially an error because the classesDirs property did not contain the output for processResources. Since 8.0, this implicit dependency is removed. Now, depending on classesDirs only executes the tasks which directly produce files in the classes directories.

Consider the following buildscript:

plugins {
    id 'java-library'
}
// Task lists all files in the given classFiles FileCollection
tasks.register("listClassFiles", ListClassFiles) {
    classFiles.from(java.sourceSets.main.output.classesDirs)
}

Previously, the listClassFiles task depended on compileJava, processResources, and classes. Now, only compileJava is a task dependency of listClassFiles.

If a task in your build relied on the previous behavior, you can instead use the entire SourceSetOutput as an input, which contains all classes and resources.

If that is not feasible, you can restore the previous behavior by adding more task dependencies to classesDirs:

java {
    sourceSets {
        main {
            output.classesDirs.builtBy(output)
        }
    }
}
Minimal supported Kotlin Gradle Plugin version changed

Gradle 7.x supports Kotlin Gradle Plugin 1.3.72 and above. Kotlin Gradle Plugin versions above 1.6.21 are not tested with Gradle 7.x. Gradle 8.x supports Kotlin Gradle Plugin 1.6.10 and above. You can use a lower Kotlin language version by modifying the language version and api version setting in the Kotlin compilation tasks.

Minimal supported Android Gradle Plugin version changed

Gradle 7.x supports Android Gradle Plugin (AGP) 4.1 and above. AGP versions above 7.3 are not tested with Gradle 7.x. Gradle 8.x supports AGP 8 and above. Gradle 8.x supports AGP 7.3 and above if you configure the following property:

android.experimental.legacyTransform.forceNonIncremental=true
Change to AntBuilder parent class

Previously, org.gradle.api.AntBuilder extended the deprecated groovy.util.AntBuilder class. It now extends groovy.ant.AntBuilder.

PluginDeclaration is not serializable

org.gradle.plugin.devel.PluginDeclaration is not serializable anymore. If you need to serialize it, you can convert it into your own, serializable class.

Gradle does not use equals for serialized values in up-to-date checks

Gradle now does not try to use equals when comparing serialized values in up-to-date checks. For more information see Relying on equals for up-to-date checks is deprecated.

Task and transform validation warnings introduced in Gradle 7.x are now errors

Gradle introduced additional task and artifact transform validation warnings in the Gradle 7.x series. Those warnings are now errors in Gradle 8.0 and will fail the build.

Warnings that became errors:

Gradle does not ignore empty directories for file-trees with @SkipWhenEmpty

Previously Gradle used to detect if an input file collection annotated with @SkipWhenEmpty consisted only of file trees and then ignored directories automatically. To ignore directories in Gradle 8.0 and later, the input property needs to be explicitly annotated with @IgnoreEmptyDirectories. For more information see File trees and empty directory handling.

Format of JavaVersion has changed for Java 9 and Java 10

The string format of the JavaVersion has changed to match the official Java versioning. Starting from Java 9, the language version must not contain the 1. prefix. This affects the format of the sourceCompatiblity and targetCompatibility properties on the JavaCompile task and JavaExtension. The old format is still supported when resolving the JavaVersion from a string.

Gradle 7.6

Gradle 8.0

1.8

1.8

1.9

9

1.10

10

11

11

Precompiled script plugins use strict Kotlin DSL accessor generation by default

In precompiled script plugins, type safe Kotlin DSL accessor generation now fails the build if a plugin fails to apply.

Starting in Gradle 7.6, builds could enable this behavior with the org.gradle.kotlin.dsl.precompiled.accessors.strict system property. This behavior is now default. The property has been deprecated and its usage should be removed. You can find more information about this property below.

Init scripts are applied to buildSrc builds

Init scripts specified using --init-script are now applied to buildSrc builds. In previous releases these were applied to included builds but not `buildSrc builds.

This behavior is now consistent for buildSrc and included builds.

Gradle no longer runs the build task for buildSrc builds

When Gradle builds the output of buildSrc it runs only the tasks that produce that output, which is typically the jar task. In previous releases Gradle would run the build task.

This means that the tests of buildSrc and its subprojects are not built and executed automatically and must now be explicitly requested.

This behavior is now consistent for buildSrc and included builds.

You can run the tests for buildSrc in the same way as projects in included builds, for example by running gradle buildSrc:build.

buildFinished { } hook for buildSrc runs after all tasks have executed

The buildFinished {} hook for buildSrc now runs after all tasks have completed. In previous releases this hook would run immediately after the tasks for buildSrc completed and before any requested tasks started.

This behavior is now consistent for buildSrc and included builds.

Changes to paths of included builds

In order to handle conflicts between nested included build names better, Gradle now uses the directory hierarchy of included builds to assign the build path. If you are running tasks from the command line in nested included builds, then you may need to adjust your invocation.

For example, if you have the following hierarchy:

.
├── settings.gradle.kts
└── nested
    ├── settings.gradle.kts
    └── nestedNested
        └── settings.gradle.kts
settings.gradle.kts
includeBuild("nested")
nested/settings.gradle.kts
includeBuild("nestedNested")
.
├── settings.gradle
└── nested
    ├── settings.gradle
    └── nestedNested
        └── settings.gradle
settings.gradle
includeBuild("nested")
nested/settings.gradle
includeBuild("nestedNested")

Before Gradle 8.0, you ran gradle :nestedNested:compileJava. In Gradle 8.0 the invocation changes to gradle :nested:nestedNested:compileJava.

Adding jst.ejb with the eclipse wtp plugin now removes the jst.utility facet

The eclipse wtp plugin adds the jst.utility facet to java projects. Now, adding the jst.ejb facet implicitly removes the jst.utility facet:

eclipse {
    wtp {
        facet {
            facet name: 'jst.ejb', version: '3.2'
        }
    }
}
Simplifying PMD custom rules configuration

Previously, you had to explicitly configure PMD to ignore default rules with ruleSets = []. In the Gradle 8.0, setting ruleSetConfig or ruleSetFiles to a non-empty value implicitly ignores default rules.

Report getOutputLocation return type changed from Provider to Property

The outputLocation property of the Report now returns a value of type Property<? extends FileSystemLocation>. Previously, outputLocation returned a value of type Provider<? extends FileSystemLocation>.

This change makes the Report API more internally consistent, and allows for more idiomatic configuration of reporting tasks.

The former, now @Deprecated usage:

tasks.named('test') {
    reports.junitXml.setDestination(layout.buildDirectory.file('reports/my-report-old').get().asFile) // DEPRECATED
}

can be replaced with:

tasks.named('test') {
    reports.junitXml.outputLocation = layout.buildDirectory.dir('reports/my-report')
}

Many built-in and custom reports, such as those used by JUnit, implement this interface. Plugins compiled against an earlier version of Gradle containing the previous method signature may need to be recompiled to be used with newer versions of Gradle containing the new signature.

Removed external plugin validation plugin

The incubating plugin ExternalPluginValidationPlugin has been removed. Use the java-gradle-plugin's validatePlugins task to validate plugins under development.

Reproducible archives can change compared to past versions

Gradle changes the compression library used for creating archives from an Ant based one to Apache Commons Compress™. As a consequence archives created from the same content, are unlikely to end up identical byte-by-byte to their older versions, created with the old library.

Upgrade to Kotlin 1.8.10

The embedded Kotlin has been updated to Kotlin 1.8.10. Also see Kotlin 1.8.0 release notes. For more information, see the release notes for Kotlin

Updated the Kotlin DSL to Kotlin API Level 1.8

Previously, the Kotlin DSL used Kotlin API level 1.4. Starting with Gradle 8.0, the Kotlin DSL uses Kotlin API level 1.8. This change brings all the improvements made to the Kotlin language and standard library since Kotlin 1.4.0.

For information about breaking and nonbreaking changes in this upgrade, see the following links to the Kotlin documentation:

Note that the Kotlin Gradle Plugin 1.8.0 started using Java toolchains. It is recommended you configure a toolchain instead of defining Java sourceCompatibility/targetCompatibility in Kotlin projects.

Also note that the Kotlin Gradle Plugin 1.8.0 introduced compilerOptions with lazy configuration properties as a replacement for kotlinOptions which did not support lazy configuration. It is recommended you configure Kotlin compilation using compilerOptions instead of kotlinOptions.

kotlinDslPluginOptions.jvmTarget is deprecated

Previously, you could use kotlinDslPluginOptions.jvmTarget to configure which JVM target should be used for compiling code when using the kotlin-dsl plugin.

Starting with Gradle 8.0, kotlinDslPluginOptions.jvmTarget is deprecated. You should configure a Java Toolchain instead.

If you already have a Java Toolchain configured and kotlinDslPluginOptions.jvmTarget unset then Gradle 8.0 will now use the Java Toolchain as the JVM target instead of the previous default target (1.8).

Java Base Plugin now sets Jar, War, and Ear destination directory defaults

Previously, the base plugin configured the destinationDirectory of Jar, War, and Ear tasks to the directory specified by BasePluginExtension#getLibsDirectory. In Gradle 8.0, java-base handles this configuration. No changes are required for projects that already apply the java-base plugin directly or indirectly through the java, application, java-library, or other JVM ecosystem plugins.

Upload Task should not be used

The Upload task remains deprecated and is now scheduled for removal in Gradle 9.0.0. Although this type remains, it is no longer functional and will throw an exception upon running. It is preserved solely to avoid breaking plugins. Use the tasks in the maven-publish or ivy-publish plugins instead.

Configurations no longer allowed as Dependencies

Adding a Configuration as a dependency in the dependencies DSL block, or programmatically using the DependencyHandler classes' doAdd(Configuration, Object, Closure) method, is no longer allowed and will fail with an exception. To replicate many aspects of this behavior, extend configurations using the extendsFrom(Configuration) method on Configuration instead.

Deprecated for consumption configurations are now non-consumable

The following configurations were never meant to be consumed:

  • The antlr configuration created by the AntlrPlugin

  • The zinc configuration created by the ScalaBasePlugin

  • The providedCompile and providedRuntime configurations created by the WarPlugin

These configurations were deprecated for consumption and are now no longer consumable. Attempting to consume them will result in an error.

Identical consumable configurations are now an error

If a project has multiple consumable configurations that share the same attributes and capabilities declaration, the build will fail when publishing or resolving as a dependency that project. This was previously deprecated.

The outgoingVariants report will warn about this for impacted configurations.

Toolchain-based tasks for JVM projects

Starting with Gradle 8.0, all core Java tasks that have toolchain support are now using toolchains unconditionally. If JavaBasePlugin is applied, the convention value for tool properties on the task is defined by the toolchain configured on the java extension. In case no toolchains are explicitly configured, the toolchain corresponding to the JVM running Gradle is used.

Similarly, tasks from the Groovy and Scala plugins also rely on toolchains to determine on which JVM they are executed.

Scala compilation target

With the toolchain changes described above, Scala compilation tasks are now always provided with a target or release parameter. The exact parameter and value depend on toolchain usage, or not, and Scala version.

See the Scala plugin documentation for details.

pluginBundle dropped in Plugin Publish plugin

Gradle 8 no longer supports the pluginBundle extension. Its functionality has been merged into the gradlePlugin block. These changes require recent versions of the Plugin Publish plugin (1.0.+). Documentation on configuring plugin publication can be found both on the Portal and in the user manual.

Upgrading from 7.5 and earlier

Updates to Attribute Disambiguation Rules related methods

The AttributeSchema.setAttributeDisambiguationPrecedence(List) and AttributeSchema.getAttributeDisambiguationPrecedence() methods now accept and return List instead of Collection to better indicate that the order of the elements in those collection is significant.

Strict Kotlin DSL precompiled script plugins accessors generation

Type safe Kotlin DSL accessors generation for precompiled script plugins does not fail the build by default if a plugin requested in such precompiled scripts fails to be applied. Because the cause could be environmental and for backwards compatibility reasons, this behaviour hasn’t changed yet.

Back in Gradle 7.1 the :generatePrecompiledScriptPluginAccessors task responsible for the accessors generation has been marked as non-cacheable by default. The org.gradle.kotlin.dsl.precompiled.accessors.strict system property was introduced in order to offer an opt-in to a stricter mode of operation that fails the build when a plugin application fails, and enable the build cache for that task.

Starting with Gradle 7.6, non-strict accessors generation for Kotlin DSL precompiled script plugins has been deprecated. This will change in Gradle 8.0. Strict accessor generation will become the default. To opt in to the strict behavior, set the 'org.gradle.kotlin.dsl.precompiled.accessors.strict' system property to true.

This can be achieved persistently in the gradle.properties file in your build root directory:

systemProp.org.gradle.kotlin.dsl.precompiled.accessors.strict=true
Potential breaking changes
Upgrade to Kotlin 1.7.10

The embedded Kotlin has been updated to Kotlin 1.7.10.

Gradle doesn’t ship with the kotlin-gradle-plugin but the upgrade to 1.7.10 can bring the new version. For example when you use the kotlin-dsl plugin.

The kotlin-gradle-plugin version 1.7.10 changes the type hierarchy of the KotlinCompile task type. It doesn’t extend from AbstractCompile anymore. If you used to select Kotlin compilation tasks by AbstractCompile you need to change that to KotlinCompile.

For example, this

tasks.named<AbstractCompile>("compileKotlin")

needs to be changed to

tasks.named<KotlinCompile>("compileKotlin")

In the same vein, if you used to filter tasks by AbstractCompile you won’t obtain the Kotlin compilation tasks anymore:

tasks.withType<AbstractCompile>().configureEach {
    // ...
}

needs to be changed to

tasks.withType<AbstractCompile>().configureEach {
    // ...
}
tasks.withType<KotlinCompile>().configureEach {
    // ...
}
Upgrade to Groovy 3.0.13

Groovy has been updated to Groovy 3.0.13.

Since the previous version was 3.0.10, the 3.0.11 and 3.0.12 changes are also included.

Upgrade to CodeNarc 3.1.0

The default version of CodeNarc has been updated to 3.1.0.

Upgrade to PMD 6.48.0

PMD has been updated to PMD 6.48.0.

Configuring a non-existing executable now fails

When configuring an executable explicitly for JavaCompile or Test tasks, Gradle will now emit an error if this executable does not exist. In the past, the task would be executed with the default toolchain or JVM running the build.

Changes to dependency declarations in Test Suites

As part of the ongoing effort to evolve Test Suites, dependency declarations in the Test Suites dependencies block are now strongly typed. This will help make this incubating API more discoverable and easier to use in an IDE.

In some cases, this requires syntax changes. For example, build scripts that previously added Test Suite dependencies with the following syntax:

testing {
  suites {
    register<JvmTestSuite>("integrationTest") {
      dependencies {
        implementation(project)
      }
    }
  }
}

will now fail to compile, with a message like:

None of the following functions can be called with the arguments supplied:
public operator fun DependencyAdder.invoke(dependencyNotation: CharSequence): Unit defined in org.gradle.kotlin.dsl
public operator fun DependencyAdder.invoke(dependency: Dependency): Unit defined in org.gradle.kotlin.dsl
public operator fun DependencyAdder.invoke(files: FileCollection): Unit defined in org.gradle.kotlin.dsl
public operator fun DependencyAdder.invoke(dependency: Provider<out Dependency>): Unit defined in org.gradle.kotlin.dsl
public operator fun DependencyAdder.invoke(externalModule: ProviderConvertible<out MinimalExternalModuleDependency>): Unit defined in org.gradle.kotlin.dsl

To fix this, replace the reference to project with a call to project():

testing {
  suites {
    register<JvmTestSuite>("integrationTest") {
      dependencies {
        implementation(project())
      }
    }
  }
}

Other syntax effected by this change includes:

  • You cannot use Provider<String> as a dependency declaration.

  • You cannot use a Map as a dependency declaration for Kotlin or Java.

  • You cannot use a bundle as a dependency declaration directly (implementation(libs.bundles.testing)). Use implementation.bundle(libs.bundles.testing) instead.

For more information, see the updated declare an additional test suite example in the JVM Test Suite Plugin section of the user guide.

Deprecations
Usage of invalid Java toolchain specifications is now deprecated

Along with the Java language version, the Java toolchain DSL allows configuring other criteria such as specific vendors or VM implementations. Starting with Gradle 7.6, toolchain specifications that configure other properties without specifying the language version are considered invalid. Invalid specifications are deprecated and will become build errors in Gradle 8.0.

See more details about toolchain configuration in the user manual.

Deprecated members of the org.gradle.util package now report their deprecation

These members will be removed in Gradle 9.0.0.

  • ClosureBackedAction

  • CollectionUtils

  • ConfigureUtil

  • DistributionLocator

  • GFileUtils

  • GradleVersion.getBuildTime()

  • GradleVersion.getNextMajor()

  • GradleVersion.getRevision()

  • GradleVersion.isValid()

  • GUtil

  • NameMatcher

  • NameValidator

  • RelativePathUtil

  • TextUtil

  • SingleMessageLogger

  • VersionNumber

  • WrapUtil

Internal DependencyFactory was renamed

The internal org.gradle.api.internal.artifacts.dsl.dependencies.DependencyFactory type was renamed to org.gradle.api.internal.artifacts.dsl.dependencies.DependencyFactoryInternal. As an internal type, it should not be used, but for compatibility reasons the inner ClassPathNotation type is still available. This name for the type is deprecated and will be removed in Gradle 8.0. The public API for this is on DependencyHandler, with methods such as localGroovy() providing the same functionality.

Replacement collections in org.gradle.plugins.ide.idea.model.IdeaModule

The testResourcesDirs and testSourcesDirs fields and their getters and setters have been deprecated. Replace usages with the now stable getTestSources() and getTestResources() methods and their respective setters. These new methods return and are backed by ConfigurableFileCollection instances for improved flexibility of use. Gradle now warns upon usage of these deprecated methods. They will be removed in a future version of Gradle.

Replacement methods in org.gradle.api.tasks.testing.TestReport

The getDestinationDir(), setDestinationDir(File), and getTestResultDirs() and setTestResultDirs(Iterable) methods have been deprecated. Replace usages with the now stable getDestinationDirectory() and getTestResults() methods and their associated setters. These deprecated elements will be removed in a future version of Gradle.

Deprecated implicit references to outer scope methods in some configuration blocks

Prior to Gradle 7.6, Groovy scripts permitted access to root project configure methods within named container configure methods that throw `MissingMethodException`s. Consider the following snippets for examples of this behavior:

Gradle permits access to the top-level repositories block from within the configurations block when the provided closure is otherwise an invalid configure closure for a Configuration. In this case, the repositories closure executes as if it were called at the script-level, and creates an unconfigured repositories Configuration:

configurations {
    repositories {
        mavenCentral()
    }
    someConf {
        canBeConsumed = false
        canBeResolved = false
    }
}

The behavior also applies to closures which do not immediately execute. In this case, afterResolve only executes when the resolve task runs. The distributions closure is a valid top-level script closure. But it is an invalid configure closure for a Configuration. This example creates the conf Configuration immediately. During resolve task execution, the distributions block executed as if it were declared at the script-level:

configurations {
    conf.incoming.afterResolve {
        distributions {
            myDist {
                contents {}
            }
        }
    }
}

task resolve {
    dependsOn configurations.conf
    doFirst {
        configurations.conf.files() // Trigger `afterResolve`
    }
}

As of Gradle 7.6, this behavior is deprecated. Starting with Gradle 8.0, this behavior will be removed. Instead, Gradle will throw the underlying MissingMethodException. To mitigate this change, consider the following solutions:

configurations {
    conf.incoming.afterResolve {
        // Fully qualify the reference.
        project.distributions {
            myDist {
                contents {}
            }
        }
    }
}
configurations {
    conf
}

// Extract the script-level closure to the script root scope.
configurations.conf.incoming.afterResolve {
    distributions {
        myDist {
            contents {}
        }
    }
}

Upgrading from 7.4 and earlier

IncrementalTaskInputs type is deprecated

The IncrementalTaskInputs type was used to implement incremental tasks, that is to say tasks that can be optimized to run on a subset of changed inputs instead of the whole input. This type had a number of drawbacks. In particular using this type it was not possible to determine what input a change was associated with.

You should now use the InputChanges type instead. Please refer to the userguide section about implementing incremental tasks for more details.

Potential breaking changes
Version catalog only accepts a single TOML import file

Only a single file will be accepted when using a from import method. This means that notations, which resolve to multiple files (e.g. the Project.files(java.lang.Object…​) method, when more then one file is passed) will result in a build failure.

Updates to default tool integration versions
Classpath file generated by the eclipse plugin has changed

Project dependencies defined in test configurations get the test=true classpath attribute. All source sets and dependencies defined by the JVM Test Suite plugin are also marked as test code by default. You can now customize test source sets and dependencies via the eclipse plugin DSL:

eclipse {
    classpath {
        testSourceSets = [sourcesSets.test, sourceSets.myTestSourceSet]
        testConfigurations = [configuration.myTestConfiguration]
    }
}

Alternatively, you can adjust or remove classpath attributes in the eclipse.classpath.file.whenMerged { } block.

Signing plugin defaults to gpg instead of gpg2 when using the GPG command

The signature plugin’s default executable when using the GPG command changed from gpg2 to gpg. The change was motivated as GPG 2.x became stable, and distributions started to migrate by not linking the gpg2 executable.

In order to set the old default, the executable can be manually defined in gradle.properties:

signing.gnupg.executable=gpg2
mustRunAfter constraints no longer violated by finalizedBy dependencies

In previous Gradle versions, mustRunAfter constraints between regular tasks and finalizer task dependencies would not be honored.

For a concrete example, consider the following task graph definition:

tasks {
    register("dockerTest") {
        dependsOn("dockerUp")     // dependsOn createContainer mustRunAfter removeContainer
        finalizedBy("dockerStop") // dependsOn removeContainer
    }

    register("dockerUp") {
        dependsOn("createContainer")
    }

    register("dockerStop") {
        dependsOn("removeContainer")
    }

    register("createContainer") {
        mustRunAfter("removeContainer")
    }

    register("removeContainer") {
    }
}

The relevant constraints are:

  • dockerStop is a finalizer of dockerTest so it must be run after dockerTest;

  • removeContainer is a dependency of dockerStop so it must be run before dockerStop;

  • createContainer must run after removeContainer;

Prior to Gradle 7.5, gradle dockerTest would yield the following order of execution, in violation of the mustRunAfter constraint between :createContainer and :removeContainer:

> Task :createContainer UP-TO-DATE
> Task :dockerUp UP-TO-DATE
> Task :dockerTest UP-TO-DATE
> Task :removeContainer UP-TO-DATE
> Task :dockerStop UP-TO-DATE

Starting with Gradle 7.5, mustRunAfter constraints are fully honored yielding the following order of execution:

> Task :removeContainer UP-TO-DATE
> Task :createContainer UP-TO-DATE
> Task :dockerUp UP-TO-DATE
> Task :dockerTest UP-TO-DATE
> Task :dockerStop UP-TO-DATE
Scala Zinc version updated to 1.6.1

Zinc is the Scala incremental compiler that allows Gradle to always compile the minimal set of files needed by the current file changes. It takes into account which methods are being used and which have changed, which means it’s much more granular than just interfile dependencies.

Zinc version has been updated to the newest available one in order to benefit from all the recent bugfixes. Due to that, if you use zincVersion setting it’s advised to remove it and only use the default version, because Gradle will only be able to compile Scala code with Zinc versions set to 1.6.x or higher.

Removes implicit --add-opens for test workers

Prior to Gradle 7.5, JDK modules java.base/java.util and java.base/java.lang were automatically opened in test workers on JDK9+ by passing --add-opens CLI arguments. This meant any tests were able to perform deep reflection on JDK internals without warning or failing. This caused tests to be unreliable by allowing code to pass when it would otherwise fail in a production environment.

These implicit arguments have been removed and are no longer added by default. If your code or any of your dependencies are performing deep reflection into JDK internals during test execution, you may see the following behavior changes:

Before Java 16, new build warnings are shown. These new warnings are printed to stderr and will not fail the build:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.google.inject.internal.cglib.core.ReflectUtils$2 (file:/.../testng-5.12.1.jar) to <method>
WARNING: Please consider reporting this to the maintainers of com.google.inject.internal.cglib.core.ReflectUtils$2
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

With Java 16 or higher, exceptions are thrown that fail the build:

// Thrown by TestNG
java.lang.reflect.InaccessibleObjectException: Unable to make <method> accessible: module java.base does not "opens java.lang" to unnamed module @1e92bd61
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
	at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
    ...

// Thrown by ProjectBuilder
org.gradle.api.GradleException: Could not inject synthetic classes.
	at org.gradle.initialization.DefaultLegacyTypesSupport.injectEmptyInterfacesIntoClassLoader(DefaultLegacyTypesSupport.java:91)
	at org.gradle.testfixtures.internal.ProjectBuilderImpl.getGlobalServices(ProjectBuilderImpl.java:182)
	at org.gradle.testfixtures.internal.ProjectBuilderImpl.createProject(ProjectBuilderImpl.java:111)
	at org.gradle.testfixtures.ProjectBuilder.build(ProjectBuilder.java:120)
	...
Caused by: java.lang.RuntimeException: java.lang.IllegalAccessException: module java.base does not open java.lang to unnamed module @1e92bd61

In most cases, these errors can be resolved by updating the code or dependency performing the illegal access. If the code-under-test or the newest version of the dependency in question performs illegal access by design, the old behavior can be restored by opening the java.base/java.lang and java.base/java.util modules manually with --add-opens:

tasks.withType(Test).configureEach {
    jvmArgs(["--add-opens=java.base/java.lang=ALL-UNNAMED",
             "--add-opens=java.base/java.util=ALL-UNNAMED"]
}

If you are developing Gradle plugins, ProjectBuilder relies on reflection in the java.base/java.lang module. Gradle will automatically add the appropriate --add-opens flag to tests when the java-gradle-plugin plugin is applied.

If you are using TestNG, versions prior to 5.14.6 perform illegal reflection. Updating to at least 5.14.6 should fix the incompatibility.

Checkstyle tasks use toolchains and execute in parallel by default

The Checkstyle plugin now uses the Gradle worker API to run Checkstyle as an external worker process. Multiple Checkstyle tasks may now run in parallel within a project.

Some projects will need to increase the amount of memory available to Checkstyle to avoid out of memory errors. You can increase the maximum memory for the Checkstyle process by setting the maxHeapSize for the Checkstyle task. By default, the process will start with a maximum heap size of 512MB.

We also recommend to update Checkstyle to version 9.3 or later.

Missing files specified with relative paths when running Checkstyle

Gradle 7.5 consistently sets the current working directory for the Checkstyle task to $GRADLE_USER_HOME/workers. This may cause problems with custom Checkstyle tasks or Checkstyle configuration files that assume a different directory for relative paths.

Previously, Gradle selected the current working directory based on the directory where you ran Gradle. If you ran Gradle in:

  • the root directory of a project: Gradle uses the root directory as the current working directory.

  • a nested directory of a project: Gradle uses the root directory of the subproject as the current working directory.

In version 7.5 and above, Gradle consistently sets the current working directory for the Checkstyle task to $GRADLE_USER_HOME/workers.

Deprecations
Converting files to a classpath where paths contain file separator

Java has the concept of a path separator which is used to separate individual paths in a list of paths, for example in a classpath string. The individual paths must not contain the path separator. Consequently, using @FileCollection.getAsPath() for files with paths that contain a path separator has been deprecated, and it will be an error in Gradle 8.0 and later. Using a file collection with paths which contain a path separator may lead to incorrect builds, since Gradle doesn’t find the files as inputs, or even to build failures when the path containing the path separator is illegal on the operating system.

dependencyInsight --singlepath option is deprecated

For consistency, this was changed to --single-path. The API method has remained the same, this only affects the CLI.

Groovydoc includePrivate property is deprecated

There is a new access property that allows finer control over what is included in the Groovydoc.

Provider-based API must be used to run external processes at the configuration time

Using Project.exec, Project.javaexec, and standard Java and Groovy APIs to run external processes at the configuration time is now deprecated when the configuration cache is enabled. It will be an error in Gradle 8.0 and later. Gradle 7.5 introduces configuration cache-compatible ways to execute and obtain output of an external process with the provider-based APIs or a custom implementation of the ValueSource interface. The configuration cache chapter has more details to help with the migration to the new APIs.

Upgrading from 7.3 and earlier

Deprecations
AdoptOpenJDK toolchain download

Following the move from AdoptOpenJDK to Adoptium, under the Eclipse foundation, it is no longer possible to download an AdoptOpenJDK build from their end point. Instead, an Eclipse Temurin or IBM Semeru build is returned.

Gradle 7.4+ will now emit a deprecation warning when the AdoptOpenJDK vendor is specified in the toolchain specification and it is used by auto provisioning. If you must use AdoptOpenJDK, you should turn off auto-download. If an Eclipse Temurin or IBM Semeru build works for you, specify JvmVendorSpec.ADOPTIUM or JvmVendorSpec.IBM as the vendor or leave the vendor unspecified.

File trees and empty directory handling

When using @SkipWhenEmpty on an input file collection, Gradle skips the task when it determines that the input is empty. If the input file collection consists only of file trees, Gradle ignores directories for the emptiness check. Though when checking for changes to the input file collection, Gradle only ignores directories when the @IgnoreEmptyDirectories annotation is present.

Gradle will now ignore directories for both the @SkipWhenEmpty check and for determining changes consistently. Until Gradle 8.0, Gradle will detect if an input file collection annotated with @SkipWhenEmpty consists only of file trees and then ignore directories automatically. Moreover, Gradle will issue a deprecation warning to advise the user that the behavior will change in Gradle 8.0, and that the input property should be annotated with @IgnoreEmptyDirectories. To ignore directories in Gradle 8.0 and later, the input property needs to be annotated with @IgnoreEmptyDirectories.

Finally, using @InputDirectory implies @IgnoreEmptyDirectories, so no changes are necessary when using this annotation. The same is true for inputs.dir() when registering an input directory via the runtime API.

Using LazyPublishArtifact without a FileResolver is deprecated

When using a LazyPublishArtifact without a FileResolver, a different file resolution strategy is used, which duplicates some logic in the FileResolver.

To improve consistency, LazyPublishArtifact should be used with a FileResolver, and will require it in the future.

This also affects other internal APIs that use LazyPublishArtifact, which now also have deprecation warnings where needed.

TAR trees from resources without backing files

It is possible to create TAR trees from arbitrary resources. If the resource is not created via project.resources, then it may not have a backing file. Creating a TAR tree from a resource with no backing file has been deprecated. Instead, convert the resource to a file and use project.tarTree() on the file. To convert the resource to a file you can use a custom task or use dependency management to download the file via a URL. This way, Gradle is able to apply optimizations like up-to-date checks instead of re-running the logic to create the resource every time.

Unique attribute sets

The set of Attributes associated with a consumable configuration within a project, must be unique across all other configurations within that project which share the same set of Capabilitys.

This will be checked at the end of configuring variant configurations, as they are locked against further mutation.

If the set of attributes is shared across configurations, consider adding an additional attribute to one of the variants for the sole purpose of disambiguation.

Provider#forUseAtConfigurationTime() has been deprecated

Provider#forUseAtConfigurationTime is now deprecated and scheduled for removal in Gradle 9.0.0. Clients should simply remove the call.

The call was mandatory on providers of external values such as system properties, environment variables, Gradle properties and file contents meant to be used at configuration time together with the configuration cache feature.

Starting with version 7.4 Gradle will implicitly treat an external value used at configuration time as a configuration cache input.

Clients are also free to use standard Java APIs such as System#getenv to read environment variables, System#getProperty to read system properties as well as Gradle APIs such as Project#property(String) and Project#findProperty(String) to read Gradle properties at configuration time. The Provider based APIs are still the recommended way to connect external values to task inputs for maximum configuration cache reuse.

Task execution listeners and events

The Gradle configuration cache does not support listeners and events that have direct access to Task and Project instances, which allows Gradle to execute tasks in parallel and to store the minimal amount of data in the configuration cache. In order to move towards an API that is consistent whether the configuration cache is enabled or not, the following APIs are deprecated and will be removed or be made an error in Gradle 8.0:

See the configuration cache chapter for details on how to migrate these usages to APIs that are supported by the configuration cache.

Build finished events

Build finished listeners are not supported by the Gradle configuration cache. And so, the following API are deprecated and will be removed in Gradle 8.0:

See the configuration cache chapter for details on how to migrate these usages to APIs that are supported by the configuration cache.

Calling Task.getProject() from a task action

Calling Task.getProject() from a task action at execution time is now deprecated and will be made an error in Gradle 8.0. This method can be used during configuration time, but it is recommended to avoid doing this.

See the configuration cache chapter for details on how to migrate these usages to APIs that are supported by the configuration cache.

Calling Task.getTaskDependencies() from a task action

Calling Task.getTaskDependencies() from a task action at execution time is now deprecated and will be made an error in Gradle 8.0. This method can be used during configuration time, but it is recommended to avoid doing this.

See the configuration cache chapter for details on how to migrate these usages to APIs that are supported by the configuration cache.

Using a build service from a task without explicitly declaring usage

Gradle needs the information so it can properly honor the build service lifecycle and its usage constraints.

This will become an error in a future Gradle version.

This can be done either by consuming the service via a @ServiceReference property (since 8.0) or by invoking usesService() on the task (since 6.1).

Check the Shared Build Services documentation for more information.

VersionCatalog and VersionCatalogBuilder deprecations

Some methods in VersionCatalog and VersionCatalogBuilder are now deprecated and scheduled for removal in Gradle 8.0. Specific replacements can be found in the JavaDoc of the affected methods.

These methods were changed to improve the consistency between the libs.versions.toml file and the API classes.

Upgrading from 7.2 and earlier

Potential breaking changes
Updates to bundled Gradle dependencies
Application order of plugins in the plugins block

The order in which plugins in the plugins block were actually applied was inconsistent and depended on how a plugin was added to the class path.

Now the plugins are always applied in the same order they are declared in the plugins block which in rare cases might change behavior of existing builds.

Effects of exclusion on substituted dependencies in dependency resolution

Prior to this version, a dependency substitution target could not be excluded from a dependency graph. This was caused by checking for exclusions prior to performing the substitution. Now Gradle will also check for exclusion on the substitution result.

Version catalog

Generated accessors no longer give access to the type unsafe API. You have to use the version catalog extension instead.

Toolchain support in Scala

When using toolchains in Scala, the -target option of the Scala compiler will now be set automatically. This means that using a version of Java that cannot be targeted by a version of Scala will result in an error. Providing this flag in the compiler options will disable this behaviour and allow to use a higher Java version to compile for a lower bytecode target.

Declaring input or output directories which contain unreadable content

For up-to-date checks Gradle relies on tracking the state of the inputs and the outputs of a task. Gradle used to ignore unreadable files in the input or outputs to support certain use-cases, although it cannot track their state. Declaring input or output directories on tasks which contain unreadable content has been deprecated and these use-cases are now supported by declaring the task to be untracked. Use the @UntrackedTask annotation or the Task.doNotTrackState() method to declare a task as untracked.

When you are using a Copy task for copying single files into a directory which contains unreadable files, use the method Task.doNotTrackState().

Upgrading from 7.1 and earlier

Potential breaking changes
Security changes to application start scripts and Gradle wrapper scripts

Due to CVE-2021-32751, gradle, gradlew and start scripts generated by Gradle’s application plugin have been updated to avoid situations where these scripts could be used for arbitrary code execution when an attacker is able to change environment variables.

You can use the latest version of Gradle to generate a gradlew script and use it to execute an older version of Gradle.

This should be transparent for most users; however, there may be changes for Gradle builds that rely on the environment variables JAVA_OPTS or GRADLE_OPTS to pass parameters with complicated quote escaping. Contact us if you suspect something has broken your build and you cannot find a solution.

Updates to bundled Gradle dependencies
Deprecations
Using Java lambdas as task actions

When using a Java lambda to implement a task action, Gradle cannot track the implementation and the task will never be up-to-date or served from the build cache. Since it is easy to add such a task action, using task actions implemented by Java lambdas is now deprecated. See Validation problems for more details how to fix the issue.

Relying on equals for up-to-date checks is deprecated

When a task input is annotated with @Input and is not a type Gradle understand directly (like String), then Gradle uses the serialized form of the input for up-to-date checks and the build cache key. Historically, Gradle also loads the serialized value from the last execution and then uses equals() to compare it to the current value for up-to-date checks. Doing so is error prone, doesn’t work with the build cache and has a performance impact, therefore it has been deprecated. Instead of using @Input on a type Gradle doesn’t understand directly, use @Nested and annotate the properties of the type accordingly.

Upgrading from 7.0 and earlier

Potential breaking changes
The org.gradle.util package is now a public API

Officially, the org.gradle.util package is not part of the public API. But, because this package name doesn’t contain the word internal, many Gradle plugins already consider as one. Gradle 7.1 addresses the situation and marks the package as public. The classes that were unintentionally exposed are either deprecated or removed, depending on their external usage.

The following classes have known usages in external plugins and are now deprecated and set for removal in Gradle 8.0:
  • VersionNumber

  • TextUtil

  • WrapUtil

  • RelativePathUtil

  • DistributionLocator

  • SingleMessageLogger

  • ConfigureUtil

ConfigureUtil is being removed without a replacement. Plugins can avoid the need for using ConfigureUtil.

The following classes have only internal usages and were moved from org.gradle.util to the org.gradle.util.internal package:
  • Resources

  • RedirectStdOutAndErr

  • Swapper

  • StdInSwapper

  • IncubationLogger

  • RedirectStdIn

  • MultithreadedTestRule

  • DisconnectableInputStream

  • BulkReadInputStream

  • MockExecutor

  • FailsWithMessage

  • FailsWithMessageExtension

  • TreeVisitor

  • AntUtil

  • JarUtil

The last set of classes have no external or internal usages and therefore were deleted:
  • DiffUtil

  • NoopChangeListener

  • EnumWithClassBody

  • AlwaysTrue

  • ReflectionEqualsMatcher

  • DynamicDelegate

  • IncubationLogger

  • NoOpChangeListener

  • DeferredUtil

  • ChangeListener

The return type of source set extensions have changed

The following source sets are contributed via an extension with a custom type:

The 'idiomatic' DSL declaration is backward compatible:

sourceSets {
    main {
        groovy {
            // ...
        }
    }
}

However, the return type of the groovy block has changed to the extension type. This means that the following snippet no longer works in Gradle 7.1:

 sourceSets {
     main {
         GroovySourceSet sourceSet = groovy {
             // ...
         }
     }
 }
Start scripts require bash shell

The command used to start Gradle, the Gradle wrapper as well as the scripts generated by the application plugin now require bash shell.

Deprecations
Using convention mapping with properties with type Provider is deprecated

Convention mapping is an internal feature that is been replaced by the Provider API. When mixing convention mapping with the Provider API, unexpected behavior can occur. Gradle emits a deprecation warning when a property in a task, extension or other domain object uses convention mapping with the Provider API.

To fix this, the plugin that configures the convention mapping for the task, extension or domain object needs to be changed to use the Provider API only.

Setting custom build layout

Command line options:

  • -c, --settings-file for specifying a custom settings file location

  • -b, --build-file for specifying a custom build file location

have been deprecated.

Setting custom build file using buildFile property in GradleBuild task has been deprecated.

Please use the dir property instead to specify the root of the nested build.

Setting custom build layout using StartParameter methods setBuildFile(File) and setSettingsFile(File) as well as the counterpart getters getBuildFile() and getSettingsFile() have been deprecated.

Please use standard locations for settings and build files:

  • settings file in the root of the build

  • build file in the root of each subproject

For the use case where custom settings or build files are used to model different behavior (similar to Maven profiles), consider using system properties with conditional logic. For example, given a piece of code in either settings or build file:

if (System.getProperty("profile") == "custom") {
    println("custom profile")
} else {
    println("default profile")
}

You can pass the profile system property to Gradle using gradle -Dprofile=custom to execute the code in the custom profile branch.

Substitution.with replaced with Substitution.using

Dependency substitutions using with method have been deprecated and are replaced with using method that also allows chaining. For example, a dependency substitution rule substitute(project(':a')).with(project(':b')) should be replaced with substitute(project(':a')).using(project(':b')). With chaining you can, for example, add a reason for a substitution like this: substitute(project(':a')).using(project(':b')).because("a reason").

Properties deprecated in JavaExec task
  • The main getters and setters in JavaExec task have been deprecated. Use the mainClass property instead.

Deprecated properties in compile task
Non-hierarchical project layouts

Gradle 7.1 deprecated project layouts where subprojects were located outside of the project root. However, based on community feedback we decided to roll back in Gradle 7.4 and removed the deprecation. As a consequence, the Settings.includeFlat() method is deprecated in Gradle 7.1, 7.2, and 7.3 only.

Deprecated Upload task

Gradle used to have two ways of publishing artifacts. Now, the situation has been cleared and all build should use the maven-publish plugin. The last remaining artifact of the old way of publishing is the Upload task that has been deprecated and scheduled for removal in Gradle 8.0. Existing clients should migrate to the maven-publish plugin.

Deprecated conventions

The concept of conventions is outdated and superseded by extensions. To reflect this in the Gradle API, the following elements are now deprecated:

The internal usages of conventions have been also cleaned up (see the deprecated items below).

Plugin authors migrate to extensions if they replicate the changes we’ve done internally. Here are some examples:

Deprecated consumption of internal plugin configurations

Some core Gradle plugins declare configurations that are used by the plugin itself and are not meant to be published or consumed by another subproject directly. Gradle did not explicitly prohibit this. Gradle 7.1 deprecates consumption of those configurations and this will become an error in Gradle 8.0.

The following plugin configurations have been deprecated for consumption:

plugin configurations deprecated for consumption

codenarc

codenarc

pmd

pmd

checkstyle

checkstyle

antlr

antlr

jacoco

jacocoAnt, jacocoAgent

scala

zinc

war

providedCompile, providedRuntime

If your use case needs to consume any of the above mentioned configurations in another project, please create a separate consumable configuration that extends from the internal ones. For example:

plugins {
    id("codenarc")
}
configurations {
    codenarc {
        // because currently this is consumable until Gradle 8.0 and can clash with the configuration below depending on the attributes set
        canBeConsumed = false
    }
    codenarcConsumable {
        extendsFrom(codenarc)
        canBeConsumed = true
        canBeResolved = false
        // the attributes below make this configuration consumable by a `java-library` project using `implementation` configuration
        attributes {
            attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.JAR))
            attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
            attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(TargetJvmEnvironment, TargetJvmEnvironment.STANDARD_JVM));
        }
    }
}
Deprecated custom source set interfaces

The following source set interfaces are now deprecated and scheduled for removal in Gradle 8.0:

  • org.gradle.api.tasks.GroovySourceSet

  • org.gradle.api.plugins.antlr.AntlrSourceVirtualDirectory (removed)

  • org.gradle.api.tasks.ScalaSourceSet

Clients should configure the sources with their plugin-specific configuration:

For example, here’s how you configure the groovy sources from a plugin:

GroovySourceDirectorySet groovySources = sourceSet.getExtensions().getByType(GroovySourceDirectorySet.class);
groovySources.setSrcDirs(Arrays.asList("sources/groovy"));
Registering artifact transforms extending ArtifactTransform

When Gradle first introduced artifact transforms, it used the base class ArtifactTransform for implementing them. Gradle 5.3 introduced the interface TransformAction for implementing artifact transforms, replacing the previous class ArtifactTransform and addressing various shortcomings. Using the registration method DependencyHandler.registerTransform(Action) for ArtifactTransform has been deprecated. Migrate your artifact transform to use TransformAction and use DependencyHandler.registerTransform(Class, Action) instead. See the user manual for more information on implementing TransformAction.

Compatibility Matrix

The sections below describe Gradle’s compatibility with several integrations. Versions not listed here may or may not work.

Java Runtime

Gradle runs on the Java Virtual Machine (JVM), which is often provided by either a JDK or JRE. A JVM version between 17 and 26 is required to execute Gradle. JVM 27 and later versions are not yet supported.

The Gradle wrapper, Gradle client, Tooling API client, and TestKit client are compatible with JVM 8.

JDK 6 and above can be used for compilation. JVM 8 and above can be used for executing tests.

Any fully supported version of Java can be used for compilation or testing. However, the latest Java version may only be supported for compilation or testing, not for running Gradle. Support is achieved using toolchains and applies to all tasks supporting toolchains.

See the table below for the Java version supported by a specific Gradle release:

Table 2. Java Compatibility
Java version Support for toolchains Support for running Gradle

8

N/A

2.0 to 8.14.x

9

N/A

4.3 to 8.14.x

10

N/A

4.7 to 8.14.x

11

N/A

5.0 to 8.14.x

12

N/A

5.4 to 8.14.x

13

N/A

6.0 to 8.14.x

14

N/A

6.3 to 8.14.x

15

6.7

6.7 to 8.14.x

16

7.0

7.0 to 8.14.x

17

7.3

7.3 and after

18

7.5

7.5 and after

19

7.6

7.6 and after

20

8.1

8.3 and after

21

8.4

8.5 and after

22

8.7

8.8 and after

23

8.10

8.10 and after

24

8.14

8.14 and after

25

9.1.0

9.1.0 and after

26

9.4.0

9.4.0 and after

Note
We only list versions in the table above once we have tested that they work without any warnings. However, thanks to the toolchain support, Gradle will often work with the latest Java version before then. We encourage users to try it out and let us know.

Kotlin

Gradle is tested with Kotlin 2.0.0 through 2.4.20-Beta1. Beta and RC versions may or may not work.

Table 3. Embedded Kotlin version
Embedded Kotlin version Minimum Gradle version Kotlin Language version

1.3.10

5.0

1.3

1.3.11

5.1

1.3

1.3.20

5.2

1.3

1.3.21

5.3

1.3

1.3.31

5.5

1.3

1.3.41

5.6

1.3

1.3.50

6.0

1.3

1.3.61

6.1

1.3

1.3.70

6.3

1.3

1.3.71

6.4

1.3

1.3.72

6.5

1.3

1.4.20

6.8

1.3

1.4.31

7.0

1.4

1.5.21

7.2

1.4

1.5.31

7.3

1.4

1.6.21

7.5

1.4

1.7.10

7.6

1.4

1.8.10

8.0

1.8

1.8.20

8.2

1.8

1.9.0

8.3

1.8

1.9.10

8.4

1.8

1.9.20

8.5

1.8

1.9.22

8.7

1.8

1.9.23

8.9

1.8

1.9.24

8.10

1.8

2.0.20

8.11

1.8

2.0.21

8.12

1.8

2.2.0

9.0.0

2.2

2.2.20

9.2.0

2.2

2.2.21

9.3.0

2.2

2.3.0

9.4.0

2.2

2.3.20

9.5.0

2.2

2.3.21

9.6.0

2.2

2.4.0

9.7.0

2.2

Groovy

Gradle is tested with Groovy 1.5.8 through 5.0.2.

Gradle plugins written in Groovy must use Groovy 4.x for compatibility with Gradle and Groovy DSL build scripts.

Android

Gradle is tested with Android Gradle Plugin 9.0 through 9.4.0-alpha03. Alpha and beta versions may or may not work.

Target Platforms

Gradle supports a defined set of platform targets, which are combinations of:

  • Operating system and version

  • Architecture

  • File system watching compatibility

The following table lists the officially supported platforms for Gradle:

Table 4. Supported Platforms
OS Architecture

Windows 10

AMD64

Windows 11

AMD64, AArch64

Ubuntu 22

AMD64, AArch64

macOS 13+

AMD64, AArch64

Alpine 3.20

AMD64

CentOS Stream 9

AMD64

Note
Currently, all Gradle tests run with the default file-systems of the platform, i.e. ext4 for Ubuntu, Amazon Linux and CentOS, NTFS for Windows, and APFS for macOS.

Platforms not listed above may work with Gradle but are not actively tested.

The Feature Lifecycle

Gradle is under constant development. New versions are delivered on a regular and frequent basis (approximately every six weeks) as described in the section on end-of-life support.

Continuous improvement combined with frequent delivery allows new features to be available to users early. Early users provide invaluable feedback, which is incorporated into the development process.

Getting new functionality into the hands of users regularly is a core value of the Gradle platform.

At the same time, API and feature stability are taken very seriously and considered a core value of the Gradle platform. Design choices and automated testing are engineered into the development process and formalized by the section on backward compatibility.

The Gradle feature lifecycle has been designed to meet these goals. It also communicates to users of Gradle what the state of a feature is. The term feature typically means an API or DSL method or property in this context, but it is not restricted to this definition. Command line arguments and modes of execution (e.g. the Build Daemon) are two examples of other features.

Feature States

Features can be in one of four states:

1. Internal

Internal features are not designed for public use and are only intended to be used by Gradle itself. They can change in any way at any point in time without any notice. Therefore, we recommend avoiding the use of such features. Internal features are not documented. If it appears in this User Manual, the DSL Reference, or the API Reference, then the feature is not internal.

Internal features may evolve into public features.

2. Incubating

Features are introduced in the incubating state to allow real-world feedback to be incorporated into the feature before making it public. It also gives users willing to test potential future changes early access.

A feature in an incubating state may change in future Gradle versions until it is no longer incubating. Changes to incubating features for a Gradle release will be highlighted in the release notes for that release. The incubation period for new features varies depending on the feature’s scope, complexity, and nature.

Features in incubation are indicated. In the source code, all methods/properties/classes that are incubating are annotated with incubating. This results in a special mark for them in the DSL and API references.

If an incubating feature is discussed in this User Manual, it will be explicitly said to be in the incubating state.

Feature Preview API

The feature preview API allows certain incubating features to be activated by adding enableFeaturePreview('FEATURE') in your settings file. Individual preview features will be announced in release notes.

When incubating features are either promoted to public or removed, the feature preview flags for them become obsolete, have no effect, and should be removed from the settings file.

3. Public

The default state for a non-internal feature is public. Anything documented in the User Manual, DSL Reference, or API reference that is not explicitly said to be incubating or deprecated is considered public. Features are said to be promoted from an incubating state to public. The release notes for each release indicate which previously incubating features are being promoted by the release.

A public feature will never be removed or intentionally changed without undergoing deprecation. All public features are subject to the backward compatibility policy.

4. Deprecated

Some features may be replaced or become irrelevant due to the natural evolution of Gradle. Such features will eventually be removed from Gradle after being deprecated. A deprecated feature may become stale until it is finally removed according to the backward compatibility policy.

Deprecated features are indicated to be so. In the source code, all methods/properties/classes that are deprecated are annotated with “@java.lang.Deprecated” which is reflected in the DSL and API References. In most cases, there is a replacement for the deprecated element, which will be described in the documentation. Using a deprecated feature will result in a runtime warning in Gradle’s output.

The use of deprecated features should be avoided. The release notes for each release indicate any features being deprecated by the release.

Backward compatibility Policy

Gradle provides backward compatibility across major versions (e.g., 1.x, 2.x, etc.). Once a public feature is introduced in a Gradle release, it will remain indefinitely unless deprecated. Once deprecated, it may be removed in the next major release. Deprecated features may be supported across major releases, but this is not guaranteed.

Release end-of-life Policy

Every day, a new nightly build of Gradle is created.

This contains all of the changes made through Gradle’s extensive continuous integration tests during that day. Nightly builds may contain new changes that may or may not be stable.

The Gradle team creates a pre-release distribution called a release candidate (RC) for each minor or major release. When no problems are found after a short time (usually a week), the release candidate is promoted to a general availability (GA) release. If a regression is found in the release candidate, a new RC distribution is created, and the process repeats. Release candidates are supported for as long as the release window is open, but they are not intended to be used for production. Bug reports are greatly appreciated during the RC phase.

The Gradle team may create additional patch releases to replace the final release due to critical bug fixes or regressions. For instance, Gradle 5.2.1 replaces the Gradle 5.2 release.

Once a release candidate has been made, all feature development moves on to the next release for the latest major version. As such, each minor Gradle release causes the previous minor releases in the same major version to become end-of-life (EOL). EOL releases do not receive bug fixes or feature backports.

For major versions, Gradle will backport critical fixes and security fixes to the last minor in the previous major version. For example, when Gradle 7 was the latest major version, several releases were made in the 6.x line, including Gradle 6.9 (and subsequent releases).

As such, each major Gradle release causes:

  • The previous major version becomes maintenance only. It will only receive critical bug fixes and security fixes.

  • The major version before the previous one to become end-of-life (EOL), and that release line will not receive any new fixes.

FUNDAMENTALS

Core Concepts

Gradle automates building, testing, and deployment of software from information in build scripts.

gradle basic 1

Core concepts

Gradle builds are defined in terms of projects and tasks, configured using build scripts written in Groovy or Kotlin.

Concept What It Means

Build

The process and environment for producing outputs. A build includes one or more projects and their build scripts.

Project

A piece of software that can be built, such as an application or library. A build may have a single root project or multiple subprojects.

Task

A basic unit of work, like compiling code or running tests. Tasks are declared in build scripts or added by plugins.

Build Script

A configuration file (build.gradle(.kts)) that defines tasks, dependencies, and other instructions that tell Gradle how to build a project.

Plugin

Used to extend Gradle’s capabilities (like the Java plugin). Plugins often add tasks and conventions to projects.

Dependency

External or internal resources required by a project. Gradle automatically resolves these during the build.

Project structure

Many developers will interact with Gradle for the first time through an existing project.

The presence of the gradlew and gradlew.bat files in the root directory of a project is a clear indicator that Gradle is used.

A Gradle project will look similar to the following:

project
├── gradle                          // (1)
├── gradlew                             // (2)
├── gradlew.bat                         // (2)
├── settings.gradle(.kts)           // (3)
├── subproject-a
│   ├── build.gradle(.kts)              // (4)
│   └── src/                        // (5)
└── subproject-b
    ├── build.gradle(.kts)              // (4)
    └── src/                        // (5)
  1. Gradle directory to store wrapper files and more

  2. Gradle wrapper scripts - THIS IS A GRADLE PROJECT!

  3. Gradle settings file to define a root project name and subprojects

  4. Gradle build scripts of the two subprojects - subproject-a and subproject-b

  5. Source code and/or additional files for the projects

Invoking Gradle

In the IDE

Gradle is built-in to many IDEs including Android Studio, IntelliJ IDEA, Visual Studio Code, Eclipse, and NetBeans.

Gradle can be automatically invoked when you build, clean, or run your app in the IDE.

gradle ide invocation

Consult the manual for the IDE of your choice to learn more about how Gradle can be used and configured.

On the Command Line

Gradle can be invoked in the command line once installed:

$ gradle build
$ gradle test
$ gradle clean build

Most projects do not use the installed version of Gradle but rather the Gradle Wrapper.

With the Gradle Wrapper

The wrapper is a script that invokes a declared version of Gradle and is the recommended way to execute a Gradle build:

$ ./gradlew build
gradle terminal invocation

Wrapper Basics

The recommended way to execute any Gradle build is with the Gradle Wrapper.

gradle basic 2

The wrapper script invokes a declared version of Gradle, downloading it beforehand if necessary.

wrapper workflow

It is available as a gradlew or gradlew.bat file in the project root directory:

root
├── gradlew     // THE WRAPPER FOR Linux / macOS
├── gradlew.bat // THE WRAPPER FOR Windows
└── ...

If your project does not include these files, it is likely not a Gradle project—or the wrapper has not been set up yet.

Tip
The wrapper is not something you download from the internet. You must generate it by running gradle :wrapper from a machine with Gradle installed.

The wrapper provides the following benefits:

  1. Automatically downloads and uses a specific Gradle version.

  2. Standardizes a project on a given Gradle version.

  3. Provisions the same Gradle version for different users and environments (IDEs, CI servers…​).

  4. Makes it easy to run Gradle builds without installing Gradle manually.

Using the Gradle Wrapper

It’s important to distinguish between two ways of running Gradle:

  1. Using a system-installed Gradle distribution — by running the gradle command.

  2. Using the Gradle Wrapper — by running the gradlew or gradlew.bat script included in a Gradle project.

The Gradle Wrapper is always the recommended way to execute a build to ensure a reliable, controlled, and standardized execution of the build.

  1. Using a system-installed Gradle distribution:

    $ gradle build
  2. Using the Gradle Wrapper:

    • Wrapper invocation on a Linux or OSX machine:

      $ ./gradlew build
    • Wrapper invocation on Windows PowerShell:

      $ gradlew.bat build

If you want to run the command in a different directory, you must provide the relative path to the wrapper:

$ ../gradlew build

The following console output demonstrates the use of the wrapper on a Windows machine, in the command prompt (cmd), for a Java-based project:

$ gradlew.bat build
Downloading https://services.gradle.org/distributions/gradle-5.0-all.zip
.....................................................................................
Unzipping C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-all\ac27o8rbd0ic8ih41or9l32mv\gradle-5.0-all.zip to C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-al\ac27o8rbd0ic8ih41or9l32mv
Set executable permissions for: C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-all\ac27o8rbd0ic8ih41or9l32mv\gradle-5.0\bin\gradle

BUILD SUCCESSFUL in 12s
1 actionable task: 1 executed

Understanding the Wrapper files

The following files are part of the Gradle Wrapper:

.
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar  // (1)
│       └── gradle-wrapper.properties   // (2)
├── gradlew // (3)
└── gradlew.bat // (4)
  1. gradle-wrapper.jar: This is a small JAR file that contains the Gradle Wrapper code. It is responsible for downloading and installing the correct version of Gradle for a project if it’s not already installed.

  2. gradle-wrapper.properties: This file contains configuration properties for the Gradle Wrapper, such as the distribution URL (where to download Gradle from) and the distribution type (ZIP or TARBALL).

  3. gradlew: This is a shell script (Unix-based systems) that acts as a wrapper around gradle-wrapper.jar. It is used to execute Gradle tasks on Unix-based systems without needing to manually install Gradle.

  4. gradlew.bat: This is a batch script (Windows) that serves the same purpose as gradlew but is used on Windows systems.

Important
You should never alter these files.

If you want to view or update the Gradle version of your project, use the command line:

$ ./gradlew --version
$ ./gradlew :wrapper --gradle-version 7.2
$ gradlew.bat --version
$ gradlew.bat :wrapper --gradle-version 7.2
Warning
Do not edit the wrapper files manually.

Command-Line Interface Basics

The command-line interface is the primary method of interacting with Gradle outside the IDE.

gradle basic 2

The Gradle CLI is the primary way to interact with a Gradle build from the terminal. You can use it to run tasks, inspect the build, manage dependencies, and control logging, all through flexible and powerful command-line options.

Tip
Use of the Gradle Wrapper is highly encouraged. Substitute ./gradlew (in macOS / Linux) or gradlew.bat (in Windows) for gradle in the following examples.

Running commands

To execute Gradle commands, use the following simple structure:

gradle [taskName...] [--option-name...]

You can specify one or more tasks separated by spaces.

gradle [taskName1 taskName2...] [--option-name...]

For example, to run a task named build, simply type:

gradle build

To clean first, and then build:

gradle clean build

Command-line options

Gradle commands can include various options to adjust their behavior. Options can appear before or after task names, like so:

gradle [--option-name...] [taskName...]

For options that accept a value, use an equals sign (=) for clarity:

gradle [...] --console=plain

Some options are toggles and have opposite forms. For instance, to enable or disable the build cache:

gradle build --build-cache
gradle build --no-build-cache

Gradle also provides short-option equivalents for convenience. The following two commands are equivalent:

gradle --help
gradle -h

Executing tasks

In Gradle, tasks belong to specific projects. To clearly indicate which task you want to run, especially in multi-project builds, use a colon (:) as a project separator.

To execute a task named test at the root project level, use:

gradle :test

For nested subprojects, specify the full path using colons:

gradle :subproject:test

If you run a task without any colons, Gradle executes the task in the current directory’s project context:

gradle test

Task options

Some tasks accept their own specific options. Pass these options directly after the task name, prefixed with --.

Here’s how you can pass a custom option:

gradle taskName --exampleOption=exampleValue

Settings File Basics

The settings file (settings.gradle(.kts)) is the entry point of every Gradle project.

gradle basic 3

The primary purpose of the settings file is to define the project structure, usually adding subprojects to your build. Therefore in:

  • Single-project builds, the settings file is optional.

  • Multi-project builds, the settings file is mandatory and declares all subprojects.

Settings script

The settings file is a script. It is either a settings.gradle file written in Groovy or a settings.gradle.kts file in Kotlin.

The Groovy DSL and the Kotlin DSL are the only accepted languages for Gradle scripts.

The settings file is typically located in the root directory of the project since it defines the structure of the build, such as which projects are included. Without a settings file, Gradle treats the build as a single-project build by default.

Let’s take a look at an example and break it down:

settings.gradle.kts
rootProject.name = "root-project"   // (1)

include("sub-project-a")            // (2)
include("sub-project-b")
include("sub-project-c")
  1. Define the project name.

  2. Add subprojects.

settings.gradle
rootProject.name = 'root-project'   // (1)

include('sub-project-a')            // (2)
include('sub-project-b')
include('sub-project-c')
  1. Define the project name.

  2. Add subprojects.

1. Define the project name

The settings file defines your project name:

rootProject.name = "root-project"

There is only one root project per build.

2. Add subprojects

The settings file defines the structure of the project by including subprojects, if there are any:

include("sub-project-a")
include("sub-project-b")
include("sub-project-c")

The settings script is evaluated before any build scripts, making it the right place to enable or configure build-wide features such as plugin management, included builds, version catalogs, and more. We will explore these Gradle features in the advanced concepts section.

Based on the example settings file, Gradle expects the project to look as follows:

.
├── settings.gradle(.kts)   // (1)
├── sub-project-a
│   └── build.gradle(.kts)      // (2)
├── sub-project-b
│   └── build.gradle(.kts)      // (2)
└── sub-project-c
    └── build.gradle(.kts)      // (2)
  1. The settings.gradle(.kts) file.

  2. The three subprojects, each with their own build.gradle(.kts) file.

Build File Basics

Generally, a build script (build.gradle(.kts)) details build configuration, tasks, and plugins.

gradle basic 4

Every Gradle build comprises at least one build script.

Build scripts

The build script is either a build.gradle file written in Groovy or a build.gradle.kts file in Kotlin.

The Groovy DSL and the Kotlin DSL are the only accepted languages for Gradle scripts.

In multi-project builds, each subproject typically has its own build file located in its root directory.

Inside a build script, you’ll typically specify:

  • Plugins: Tools that extend Gradle’s functionality for tasks like compiling code, running tests, or packaging artifacts.

  • Dependencies: External libraries and tools your project uses.

Specifically, build scripts contain two main types of dependencies:

  • Gradle and Build Script Dependencies: These include plugins and libraries required by Gradle itself or the build script logic.

  • Project Dependencies: Libraries required directly by your project’s source code to compile and run correctly.

Let’s take a look at an example and break it down:

app/build.gradle.kts
plugins {   // (1)
    // Apply the application plugin to add support for building a CLI application in Java.
    application
}
dependencies {  // (2)
    // Use JUnit Jupiter for testing.
    testImplementation(libs.junit.jupiter)

    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // This dependency is used by the application.
    implementation(libs.guava)
}
application {   // (3)
    // Define the main class for the application.
    mainClass = "org.example.App"
}
app/build.gradle
plugins {   // (1)
    // Apply the application plugin to add support for building a CLI application in Java.
    id 'application'
}
dependencies {  // (2)
    // Use JUnit Jupiter for testing.
    testImplementation libs.junit.jupiter

    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // This dependency is used by the application.
    implementation libs.guava
}
application {   // (3)
    // Define the main class for the application.
    mainClass = 'org.example.App'
}
  1. Add plugins.

  2. Add dependencies.

  3. Use convention properties.

1. Add plugins

Plugins extend Gradle’s functionality and can contribute tasks to a project.

Adding a plugin to a build is called applying a plugin and makes additional functionality available.

app/build.gradle.kts
plugins {   // (1)
    // Apply the application plugin to add support for building a CLI application in Java.
    application
}
app/build.gradle
plugins {   // (1)
    // Apply the application plugin to add support for building a CLI application in Java.
    id 'application'
}

The application plugin facilitates creating an executable JVM application.

Applying the Application plugin also implicitly applies the Java plugin. The java plugin adds Java compilation along with testing and bundling capabilities to a project.

2. Add dependencies

Your project needs external libraries to compile, run, and test.

In this example, the project uses JUnit Jupiter for testing and Google’s Guava library in the main application code:

app/build.gradle.kts
dependencies {  // (2)
    // Use JUnit Jupiter for testing.
    testImplementation(libs.junit.jupiter)

    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // This dependency is used by the application.
    implementation(libs.guava)
}
app/build.gradle
dependencies {  // (2)
    // Use JUnit Jupiter for testing.
    testImplementation libs.junit.jupiter

    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // This dependency is used by the application.
    implementation libs.guava
}

3. Use convention properties

A plugin adds tasks to a project. It also adds properties and methods to a project.

The application plugin defines tasks that package and distribute an application, such as the run task.

The Application plugin provides a way to declare the main class of a Java application, which is required to execute the code.

app/build.gradle.kts
application {   // (3)
    // Define the main class for the application.
    mainClass = "org.example.App"
}
app/build.gradle
application {   // (3)
    // Define the main class for the application.
    mainClass = 'org.example.App'
}

In this example, the main class (i.e., the point where the program’s execution begins) is org.example.App.

Build scripts are evaluated during the configuration phase of a build, and they serve as the main entry point for defining a (sub)project’s build logic. In addition to applying plugins and setting convention properties, build scripts can:

  • Declare dependencies

  • Configure tasks

  • Reference shared settings (from version catalogs or convention plugins)

Dependencies and Dependency Management Basics

Gradle has built-in support for dependency management.

gradle basic 7

Dependency management is an automated technique for declaring and resolving external resources required by a project (i.e., dependencies).

Dependencies include JARs, libraries, or source code that support building your project. They are declared in build scripts.

Gradle automatically handles downloading, caching, and resolving these dependencies, saving you from managing them manually. It also handles version conflicts and supports flexible version declarations.

Declaring Your Dependencies

To add a dependency to your project, specify a dependency in the dependencies {} block of your build.gradle(.kts) file.

The following build.gradle(.kts) file adds two dependencies to the project:

build.gradle.kts
plugins {
    id("java-library")  // (1)
}

dependencies {
    implementation("com.google.guava:guava:32.1.2-jre") // (2)
    api("org.apache.juneau:juneau-marshall:8.2.0")      // (3)
}
build.gradle
plugins {
    id("java-library")  // (1)
}

dependencies {
    implementation("com.google.guava:guava:32.1.2-jre") // (2)
    api("org.apache.juneau:juneau-marshall:8.2.0")      // (3)
}
  1. Applies the Java Library plugin.

  2. Adds a dependency on Google’s Guava library used in production code.

  3. Adds a dependency on Apache’s Juneau Marshall library, used in library code.

Dependencies in Gradle are grouped by buckets called configurations, which define when, where, and how the dependency is used. Configurations define the scope of your dependencies. In the example above:

  • implementation is used for dependencies needed to compile and run your production code.

  • api is used for dependencies that should be exposed to consumers of your library.

When you apply certain plugins, Gradle automatically creates the right configurations for your project and makes them available to use.

For example, applying java or java-library adds configurations like implementation, api, compileOnly, runtimeOnly, and testImplementation so you can declare dependencies in the right place without extra setup.

The Android Gradle Plugin (AGP) adds variant-aware configs like debugImplementation, releaseImplementation, androidTestImplementation, and even per–build-type/flavor ones like freeDebugImplementation.

Kotlin Multiplatform (KMP) creates source set–scoped configs such as commonMainImplementation, commonTestImplementation, androidMainImplementation, iosArm64MainImplementation, etc.—so you depend exactly where the code runs.

Viewing Project Dependencies

You can inspect the dependency tree using the dependencies task. For example, to view the dependencies of the :app project:

$ ./gradlew :app:dependencies

Gradle will output the dependency tree, grouped by configuration:

$ ./gradlew :app:dependencies
> Task :app:dependencies

------------------------------------------------------------
Project ':app'
------------------------------------------------------------

...

runtimeClasspath - Runtime classpath of source set 'main'.
+--- org.apache.juneau:juneau-marshall:8.2.0
|    \--- org.apache.httpcomponents:httpcore:4.4.13
\--- com.google.guava:guava:32.1.2-jre
     +--- com.google.guava:guava-parent:32.1.2-jre
     |    +--- com.google.code.findbugs:jsr305:3.0.2 (c)
     |    +--- org.checkerframework:checker-qual:3.33.0 (c)
     |    \--- com.google.errorprone:error_prone_annotations:2.18.0 (c)
     +--- com.google.guava:failureaccess:1.0.1
     +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
     +--- com.google.code.findbugs:jsr305 -> 3.0.2
     +--- org.checkerframework:checker-qual -> 3.33.0
     \--- com.google.errorprone:error_prone_annotations -> 2.18.0

Using a Version Catalog

A version catalog provides a centralized and consistent way to manage dependency coordinates and versions across your entire build. Instead of declaring versions directly in each build.gradle(.kts) file, you define them once in a libs.versions.toml file.

This makes it easier to:

  • Share common dependency declarations between subprojects

  • Avoid duplication and version inconsistencies

  • Enforce dependency and plugin versions across large projects

The version catalog typically contains four sections:

  1. [versions] to declare the version numbers that plugins and libraries will reference.

  2. [libraries] to define the libraries used in the build files.

  3. [bundles] to define a set of dependencies.

  4. [plugins] to define plugins.

Here’s an example:

gradle/libs.versions.toml
[versions]
guava = "32.1.2-jre"
juneau = "8.2.0"

[libraries]
guava = { group = "com.google.guava", name = "guava", version.ref = "guava" }
juneau-marshall = { group = "org.apache.juneau", name = "juneau-marshall", version.ref = "juneau" }

Place this file in the gradle/ directory of your project as libs.versions.toml. Gradle will pick it up automatically and expose its contents through the libs accessor in your build scripts. IDEs like IntelliJ and Android Studio will also pick up this metadata for code completion.

Once defined, you can reference these aliases directly in your build file:

build.gradle.kts
dependencies {
    implementation(libs.guava)
    api(libs.juneau.marshall)
}
build.gradle
dependencies {
    implementation(libs.guava)
    api(libs.juneau.marshall)
}

Next Step: Learn about Tasks >>

Task Basics

A task represents some independent unit of work that a build performs, such as compiling classes, creating a JAR, generating Javadoc, or publishing archives to a repository.

gradle basic 5

Tasks are the building blocks of every Gradle build.

Common types of tasks include:

  • Compiling source code

  • Running tests

  • Packaging output (e.g., creating a JAR or APK)

  • Generating documentation (e.g., Javadoc)

  • Publishing build artifacts to repositories

Each task is independent but can depend on other tasks to run first. Gradle uses this information to figure out the most efficient order to execute tasks — skipping anything that’s already up to date.

Running a task

To run a task, use the Gradle Wrapper from your project’s root directory. For example, to run the build task:

$ ./gradlew build

This will run the build task and all of its dependencies.

If you have the application plugin applied in your build file, the run task should be available. You can run your project like this:

$ ./gradlew run

Example output:

> Task :app:compileJava
> Task :app:processResources NO-SOURCE
> Task :app:classes

> Task :app:run
Hello World!

BUILD SUCCESSFUL in 904ms
2 actionable tasks: 2 executed

Gradle ran all the tasks required to execute your application, including compiling it first. In this example, the output of the run task is a Hello World statement printed on the console.

Listing available tasks

Gradle plugins and your build script define which tasks are available in a project. To see them:

$ ./gradlew tasks

This shows a categorized list of tasks:

Application tasks
-----------------
run - Runs this project as a JVM application

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.

...

Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.

...

Other tasks
-----------
compileJava - Compiles main Java source.

...

You can run any of these tasks directly using the ./gradlew <task-name> command.

Task dependencies

Most tasks don’t run in isolation. Gradle knows which tasks depend on which others, and will automatically run them in the correct order.

For example, when you run ./gradlew build, Gradle also runs tasks like compileJava, test, and jar first — because build depends on them:

$ ./gradlew build
> Task :app:compileJava
> Task :app:processResources NO-SOURCE
> Task :app:classes
> Task :app:jar
> Task :app:startScripts
> Task :app:distTar
> Task :app:distZip
> Task :app:assemble
> Task :app:check
> Task :app:build

BUILD SUCCESSFUL in 764ms
7 actionable tasks: 7 executed

You don’t need to worry about ordering — Gradle figures it out for you.

Incremental Builds and Build Caching Basic

Gradle uses two main features to reduce build time: incremental builds and build caching.

gradle basic 8

Task Outcome Labels

When you run a Gradle build with verbose mode turned on, each task prints a short outcome label that describes what happened during execution. These labels help you understand Gradle’s behavior and performance optimizations like incremental build and build caching:

> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :jar FROM-CACHE
> Task :test SKIPPED
> Task :publish
Task Label Meaning

UP-TO-DATE

The task’s inputs and outputs haven’t changed since the last run, so it was skipped.

FROM-CACHE

The task was skipped and its outputs were restored from the local or remote build cache.

NO-SOURCE

The task had no source files to process (e.g., no Java files to compile), so it was skipped.

SKIPPED

The task was not executed due to a condition (e.g., only-if rule or command-line flags).

The task ran normally and produced outputs.

FAILED

The task ran but encountered an error.

Incremental Builds

An incremental build is a build that avoids running tasks whose inputs have not changed since the previous build. Re-executing such tasks is unnecessary if they would only re-produce the same output.

For incremental builds to work, tasks must define their inputs and outputs. Gradle will determine whether those input or outputs have changed at build time. If they have changed, Gradle will execute the task. Otherwise, it will skip execution.

Incremental builds are always enabled, and the best way to see them in action is to turn on verbose mode. With verbose mode, each task state is labeled during a build:

$ ./gradlew compileJava --console=verbose
> Task :buildSrc:generateExternalPluginSpecBuilders UP-TO-DATE
> Task :buildSrc:extractPrecompiledScriptPluginPlugins UP-TO-DATE
> Task :buildSrc:compilePluginsBlocks UP-TO-DATE
> Task :buildSrc:generatePrecompiledScriptPluginAccessors UP-TO-DATE
> Task :buildSrc:generateScriptPluginAdapters UP-TO-DATE
> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:processResources UP-TO-DATE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :list:compileJava UP-TO-DATE
> Task :utilities:compileJava UP-TO-DATE
> Task :app:compileJava UP-TO-DATE

BUILD SUCCESSFUL in 374ms
12 actionable tasks: 12 up-to-date

When you run a task that has been previously executed and hasn’t changed, then UP-TO-DATE is printed next to the task.

Tip
To permanently enable verbose mode, add org.gradle.console=verbose to your gradle.properties file.

Build Caching

Incremental Builds are a great optimization that helps avoid work already done. If a developer continuously changes a single file, there is likely no need to rebuild all the other files in the project.

However, what happens when the same developer switches to a new branch created last week? The files are rebuilt, even though the developer is building something that has been built before.

This is where a build cache is helpful.

The build cache stores previous build results and restores them when needed. It prevents the redundant work and cost of executing time-consuming and expensive processes.

When the build cache has been used to repopulate the local directory, the tasks are marked as FROM-CACHE:

$ ./gradlew compileJava --build-cache
> Task :buildSrc:generateExternalPluginSpecBuilders UP-TO-DATE
> Task :buildSrc:extractPrecompiledScriptPluginPlugins UP-TO-DATE
> Task :buildSrc:compilePluginsBlocks UP-TO-DATE
> Task :buildSrc:generatePrecompiledScriptPluginAccessors UP-TO-DATE
> Task :buildSrc:generateScriptPluginAdapters UP-TO-DATE
> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:processResources UP-TO-DATE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :list:compileJava FROM-CACHE
> Task :utilities:compileJava FROM-CACHE
> Task :app:compileJava FROM-CACHE

BUILD SUCCESSFUL in 364ms
12 actionable tasks: 3 from cache, 9 up-to-date

Once the local directory has been repopulated, the next execution will mark tasks as UP-TO-DATE and not FROM-CACHE.

The build cache allows you to share and reuse unchanged build and test outputs across teams. This speeds up local and CI builds since cycles are not wasted re-building binaries unaffected by new code changes.

Next Step: Learn about Plugins >>

Plugin Basics

Gradle is built on a flexible plugin system.

Out of the box, Gradle provides core infrastructure like dependency resolution, task orchestration, and incremental builds. Most functionality — like compiling Java, building Android apps, or publishing artifacts — comes from plugins.

gradle basic 6

A plugin is a reusable piece of software that provides additional functionality to the Gradle build system. It can:

  • Add new tasks to your build (like compileJava or test)

  • Add new configurations (like implementation or runtimeOnly)

  • Contribute DSL elements (like application {} or publishing {})

Plugins are applied to build scripts using the plugins block (Kotlin DSL or Groovy DSL), and they bring in all the logic needed for a specific domain or workflow.

Common Plugins

Here are some popular plugins and what they do:

Java Library Plugin (java-library)

Compiles Java source code, generates Javadoc, and packages classes into a JAR. Adds tasks like compileJava, javadoc, and jar.

Google Services Plugin (com.google.gms.google-services)

Configures Firebase and Google APIs in Android builds. Adds DSL like googleServices {} and tasks like generateReleaseAssets.

Gradle Bintray Plugin (com.jfrog.bintray)

Publishes artifacts to Bintray (or other Maven-style repositories) using a bintray {} configuration block.

Applying Plugins

Applying a plugin to a project allows the plugin to extend the project’s capabilities.

You apply plugins in the build script using a plugin id (a globally unique identifier / name) and a version:

plugins {
    id("«plugin id»").version("«plugin version»")
}

For example:

plugins {
    id("java-library")
    id("com.diffplug.spotless").version("6.25.0")
}

This tells Gradle to:

  • Apply the built-in java-library plugin, which adds tasks for compiling Java, running tests, and packaging libraries.

  • Apply the community-maintained spotless plugin (version 6.25.0), which adds code formatting tasks and integrates tools like ktlint, prettier, and google-java-format.

Plugin Distribution and Availability

Gradle plugins come from different sources, and you can choose the right type depending on your use case.

1. Core Plugins

Gradle Core plugins are a set of plugins that are included in the Gradle distribution itself. These plugins provide essential functionality for building and managing projects.

Core plugins are unique in that they provide short names, such as java-library for the core JavaLibraryPlugin. You can apply them by ID with no extra setup:

plugins {
    id("java-library")
}

These plugins are maintained by the Gradle team. See the Core Plugin Reference for a full list.

2. Community Plugins

Community plugins are plugins developed by the Gradle community, rather than being part of the core Gradle distribution. These plugins provide additional functionality that may be specific to certain use cases or technologies.

Gradle’s plugin ecosystem includes thousands of open-source plugins shared by the community. These are typically published to the Gradle Plugin Portal and can be applied by ID and version:

plugins {
    id("org.springframework.boot").version("3.1.5")
}

Gradle will automatically download the plugin when the build runs. See the Gradle Plugin Portal to search for a plugin.

3. Custom / Local Plugins

You can also write your own plugins — either for use in a single project or shared across multiple projects in the same build.

Custom plugins are typically written in Java, Kotlin, or Groovy and follow the same structure as published plugins.

They are normally applied by name:

plugins {
    id("my.custom-conventions")
}

Next Step: Learn about Build Scan >>

Build Scan Basics

A Build Scan is a representation of metadata captured as you run your build.

gradle basic 1

About Build Scan

Gradle captures your build metadata and sends it to the Build Scan Service. The service then transforms the metadata into information you can analyze and share with others.

build scan 1

The information that Build Scan collects can be an invaluable resource when troubleshooting, collaborating on, or optimizing the performance of your builds.

For example, with a Build Scan, it’s no longer necessary to copy and paste error messages or include all the details about your environment each time you want to ask a question on Stack Overflow, Slack, or the Gradle Forum. Instead, copy the link to your latest Build Scan.

build scan 2

Enable Build Scan

To enable a Build Scan on a Gradle command, add --scan to the command line option:

$ ./gradlew build --scan

You may be prompted to agree to the terms to use Build Scan.

Visit Develocity at gradle.com to learn more.

Publishing to a specific Develocity server

If you have a Develocity server, you can publish your Build Scan reports to that server instead of Develocity at gradle.com.

You enable this with the --develocity-url command line option:

 ./gradlew build --develocity-url=https://develocity.example.com

Check the Develocity Gradle plugin documentation for more details.

Captured Information

To see what data is captured and sent in a Build Scan, refer to the Captured Information section in the Gradle Develocity Plugin documentation.

Ready to build something? Start with the Beginner Tutorial.

Next Step: Start the Tutorial >>

Anatomy of a Gradle Build

A Gradle build is made up of several key components that work together to automate tasks like compiling code, running tests, and packaging artifacts.

gradle basic 1

At a high level, a build includes:

  • Projects, which represent things you’re building (like apps or libraries)

  • Tasks, which define the work to do (like compiling or testing)

  • Build scripts, which configure the projects and tasks

  • Plugins, which extend the build with reusable logic

Let’s first quickly review the directories in a simple Gradle project to better understand the components of a build:

gradle-project          // (1)
├── app
│   ├── build.gradle.kts    // (2)
│   └── ...
├── settings.gradle.kts     // (3)
├── gradle                  // (4)
│   └── ...
├── gradlew             // (5)
└── gradlew.bat         // (5)
gradle-project      // (1)
├── app
│   ├── build.gradle    // (2)
│   └── ...
├── settings.gradle     // (3)
├── gradle              // (4)
│   └── ...
├── gradlew                 // (5)
└── gradlew.bat             // (5)
  1. Root Directory - Gradle Project

  2. Build Script - Project-level configuration

  3. Settings File - Project inclusion and build identity

  4. Gradle Files - Wrapper executables and version pinning

  5. Gradle Wrapper - Script that automatically downloads and runs the correct Gradle version for a project

This basic Gradle build has a root project called gradle-project and contains one subproject called app.

The settings file in the root directory lets Gradle know about the structure of the build.

The build file in the app directory contains build logic specific to the app subproject such as declaring dependencies and applying plugins.

Build Directory

The build/ directory is the default output directory at the root of each project where Gradle places all generated files during a build.

It is created automatically when you run tasks like build, assemble, test, or others that produce outputs:

gradle-project
├── app
│   ├── build.gradle.kts
│   └── ...
├── settings.gradle.kts
├── gradle
│   └── ...
├── build                   // (1)
├── gradlew
└── gradlew.bat
gradle-project
├── app
│   ├── build.gradle
│   └── ...
├── settings.gradle
├── gradle
│   └── ...
├── build                   // (1)
├── gradlew
└── gradlew.bat
  1. Build directory

To remove the entire build/ directory and all its contents, use:

$ ./gradlew clean

This is useful when you want to ensure a fresh build with no leftover outputs.

Warning
Gradle creates the build directory by default, so it must be writable. If a custom build directory is specified, it must exist and also be writable.

Gradle uses two main directories to perform and manage its work: the Gradle User Home Directory and the Project Root Directory.

author gradle 2

Project Root Directory

The Project Root Directory contains all source files from your project.

It also contains files and directories Gradle generates, such as .gradle and build, as well as the Gradle configuration directory: gradle.

Tip
The gradle and .gradle directories are different.

While gradle is usually checked into source control, build and .gradle directories contain the output of your builds, caches, and other transient files Gradle uses to support features like incremental builds.

In this case, ./gradle-project is the project root directory:

gradle-project  // (1)
├── .gradle         // (2)
│   ├── 4.8             // (3)
│   ├── 4.9             // (3)
│   └── ⋮
├── build           // (4)
├── gradle
│   └── wrapper // (5)
├── gradle.properties   // (6)
├── gradlew         // (7)
├── gradlew.bat     // (7)
├── settings.gradle(.kts)   // (8)
├── subproject-one              // (9)
|   └── build.gradle(.kts)  // (10)
├── subproject-two              // (9)
|   └── build.gradle(.kts)  // (10)
└── ⋮
  1. Root Project

  2. Project-specific cache directory generated by Gradle.

  3. Version-specific caches (e.g., to support incremental builds).

  4. The build directory of this project.

  5. Contains the JAR file and configuration of the Gradle Wrapper.

  6. Project-specific Gradle configuration properties.

  7. Scripts for executing builds using the Gradle Wrapper.

  8. The project’s settings file where the list of subprojects is defined.

  9. Usually, a project is organized into one or multiple subprojects.

  10. Each subproject has its own Gradle build script.

Gradle User Home Directory

By default, the Gradle User Home (~/.gradle or C:\Users\<USERNAME>\.gradle) stores global configuration properties, initialization scripts, caches, and log files.

It can be set with the environment variable GRADLE_USER_HOME. Note that this directory is often abbreviated as GUH.

Warning
GRADLE_USER_HOME is not to be confused with the GRADLE_HOME, the optional installation directory for Gradle.

It is roughly structured as follows:

~/.gradle   // (1)
├── caches      // (2)
│   ├── 4.8     // (3)
│   ├── 4.9     // (3)
│   ├── ⋮
│   ├── jars-3      // (4)
│   └── modules-2   // (4)
├── daemon              // (5)
│   ├── ⋮
│   ├── 4.8
│   └── 4.9
├── init.d                  // (6)
│   └── my-setup.gradle
├── jdks                    // (7)
│   ├── ⋮
│   └── jdk-14.0.2+12
├── wrapper
│   └── dists                   // (8)
│       ├── ⋮
│       ├── gradle-4.8-bin
│       ├── gradle-4.9-all
│       └── gradle-4.9-bin
└── gradle.properties           // (9)
  1. Gradle User Home

  2. Global cache directory (for everything that is not project-specific).

  3. Version-specific caches (e.g., to support incremental builds).

  4. Shared caches (e.g., for artifacts of dependencies).

  5. Registry and logs of the Gradle Daemon.

  6. Global initialization scripts.

  7. JDKs downloaded by the toolchain support.

  8. Distributions downloaded by the Gradle Wrapper.

  9. Global Gradle configuration properties.

Structuring Multi-Project Builds

As your codebase grows, organizing it into multiple subprojects becomes essential for maintainability, performance, and reuse. Gradle supports this with multi-project builds.

gradle basic 9

While some small projects and monolithic applications may contain a single build file and source tree, it is often more common for a project to have been split into smaller, interdependent modules. The word "interdependent" is vital, as you typically want to link the many modules together through a single build.

Gradle supports this scenario through multi-project builds. This is sometimes referred to as a multi-module project. Gradle refers to modules as subprojects.

A multi-project build consists of one root project and one or more subprojects.

Multi-Project Structure

The following represents the structure of a sample multi-project build that contains three subprojects:

multi project structure

The directory structure should look as follows:

.
├── gradlew
├── gradlew.bat
├── settings.gradle(.kts)   // (1)
├── sub-project-1
│   └── build.gradle(.kts)      // (2)
├── sub-project-2
│   └── build.gradle(.kts)      // (2)
└── sub-project-3
    └── build.gradle(.kts)      // (2)
  1. The settings.gradle(.kts) file should include all subprojects.

  2. Each subproject should have its own build.gradle(.kts) file.

In this example, the root settings file will look as follows:

settings.gradle.kts
include("sub-project-1", "sub-project-2", "sub-project-3")
settings.gradle
include('sub-project-1', 'sub-project-2', 'sub-project-3')
Note
The order in which the subprojects (modules) are included does not matter.

The Gradle community has two standards for multi-project build structures:

  1. Multi-Project Builds using buildSrc - where buildSrc is a subproject-like directory at the Gradle project root containing shared build logic.

  2. Composite Builds including build-logic - a build that includes other builds where build-logic is a build directory at the Gradle project root containing reusable build logic.

multi project standards

In either case, the build-logic and buildSrc folders are used to organize build logic.

Each approach has trade-offs. buildSrc is easier to get started with but less flexible. Composite builds require a bit more setup but scale better and align with Gradle’s long-term best practices for sharing build logic.

Multi-Project Paths

A project path has the following pattern: it starts with an optional colon, which denotes the root project.

The root project, :, is the only project in a path not specified by its name. The rest of a project path is a colon-separated sequence of project names, where the next project is a subproject of the previous project:

:sub-project-1

You can see the project paths when running gradle projects:

------------------------------------------------------------
Root project 'project'
------------------------------------------------------------

Root project 'project'
+--- Project ':sub-project-1'
\--- Project ':sub-project-2'

Project paths usually reflect the filesystem layout, but there are exceptions. Most notably for composite builds.

Executing tasks by name

The command gradle test will execute the test task in any subprojects relative to the current working directory that has that task.

If you run the command from the root project directory, you will run test in sub-project-1, sub-project-2, and sub-project-3.

The basic rule behind Gradle’s behavior is to execute all tasks down the hierarchy with this name. And complain if there is no such task found in any of the subprojects traversed.

Note
Some task selectors, like help or dependencies, will only run the task on the project they are invoked on and not on all the subprojects to reduce the amount of information printed on the screen.
Executing tasks by fully qualified name

In a multi-project build, you can run tasks for a specific subproject by using the task’s fully qualified name. This name combines the project path and the task name.

For example, to run the build task in sub-project-1, use ./gradlew :sub-project-1:build. This ensures that only sub-project-1’s `build task is executed, rather than running build across the entire build.

You can use this pattern for any task. For example, to list all tasks available in sub-project-3, run ./gradlew :sub-project-3:tasks.

Multi-Project Builds using buildSrc

Multi-project builds allow you to organize projects with many modules, wire dependencies between those modules, and easily share common build logic amongst them.

For example, if the project above had common build logic between sub-project-1, sub-project-2 and sub-project-3, it could be structured as follows:

.
├── gradlew
├── gradlew.bat
├── settings.gradle(.kts)
├── buildSrc        // (1)
│   ├── build.gradle.kts
│   └── src/main/*/shared-build-conventions.gradle(.kts)    // (2)
├── sub-project-1
│   └── build.gradle(.kts)              // (3)
├── sub-project-2
│   └── build.gradle(.kts)              // (3)
└── sub-project-3
    └── build.gradle(.kts)              // (3)
  1. Gradle recognized buildSrc folder

  2. Contains common build logic from sub-project-1, sub-project-2 and sub-project-3

  3. Applies shared-build-conventions.gradle(.kts)

The buildSrc directory is automatically recognized by Gradle. It is a good place to define and maintain shared configuration or imperative build logic, such as custom tasks or plugins.

buildSrc is automatically included in your build as a special subproject if a build.gradle(.kts) file is found under buildSrc.

Consult the Sharing Build Logic using buildSrc chapter to learn more.

Composite Builds including build-logic

Composite Builds, also referred to as included builds, are best for sharing logic between builds (not subprojects) or isolating access to shared build logic.

Let’s take the previous example. The logic in buildSrc has been turned into a project that contains plugins and can be published and worked on independently of the root project build.

The plugin is moved to its own build called build-logic with its own build script and settings file:

.
├── gradlew
├── gradlew.bat
├── settings.gradle(.kts)
├── build-logic         // (1)
│   ├── settings.gradle.kts
│   └── conventions
│       ├── build.gradle.kts
│       └── src/main/kotlin/shared-build-conventions.gradle.kts // (2)
├── sub-project-1
│   └── build.gradle(.kts)              // (3)
├── sub-project-2
│   └── build.gradle(.kts)              // (3)
└── sub-project-3
    └── build.gradle(.kts)              // (3)
  1. Separate Gradle build called build-logic

  2. Contains common build logic from sub-project-1, sub-project-2 and sub-project-3

  3. Applies shared-build-conventions.gradle(.kts)

Note
The fact that build-logic is located in a subdirectory of the root project is irrelevant. The folder could be located outside the root project if desired.

The root settings file includes the entire build-logic build:

settings.gradle.kts
include("sub-project-1", "sub-project-2", "sub-project-3")
includeBuild("build-logic")
settings.gradle
include('sub-project-1', 'sub-project-2', 'sub-project-3')
includeBuild('build-logic')

There’s no reason that any of the subprojects in a multi-project build couldn’t themselves be composite builds. This allows teams to independently develop and test build logic or components, then include them in a larger build as needed. For example:

.
├── gradlew
├── gradlew.bat
├── settings.gradle(.kts)
├── build-logic             // (1)
│   ├── settings.gradle(.kts)
│   └── conventions
│       └── build.gradle(.kts)
├── project-1                   // (2)
│   ├── settings.gradle(.kts)
│   ├── client
│   │   └── build.gradle(.kts)
│   └── server
│       └── build.gradle(.kts)
├── project-2                   // (3)
│   ├── settings.gradle(.kts)
│   └── lib
│       └── build.gradle(.kts)
└── project-3                   // (4)
    ├── settings.gradle(.kts)
    ├── app-plugin
    │   └── build.gradle(.kts)
    ├── client-plugin
    │   └── build.gradle(.kts)
    └── server-plugin
        └── build.gradle(.kts)
  1. Separate Gradle build called build-logic

  2. Separate Gradle build called project-1 with 2 of its own subproject

  3. Separate Gradle build called project-2 with 1 of its own subproject

  4. Separate Gradle build called project-3 with 3 of its own subprojects

In this setup, a team could work on project-3 as an entirely independent build. Once their changes are complete, another team could test and validate those changes by integrating `project-3’s changes into the full root build.

Consult the Composite Builds chapter to learn more.

Gradle Build Lifecycle

The build lifecycle is the sequence of phases Gradle executes to turn your build scripts, source code, and more, into completed work. From initializing the build environment to configuring projects and finally executing tasks.

gradle basic 10

Build Phases

A Gradle build has three distinct phases.

author gradle 1

Gradle runs these phases in order:

Phase 1. Initialization Phase 2. Configuration Phase 3. Execution

- Detects the settings file
- Creates a Settings instance
- Evaluates the settings file to determine which projects (and included builds) make up the build
- Creates a Project instance for every project

- Evaluates the build files of every project participating in the build
- Evaluates the configuration (input/output) of tasks
- Creates a task graph for requested tasks

- Schedules and executes the selected tasks

build lifecycle example

The following example shows which parts of settings and build files correspond to various build phases:

settings.gradle.kts
rootProject.name = "basic"
println("This is executed during the initialization phase.")
build.gradle.kts
// Configuration Phase
println("This is executed during the configuration phase.")

tasks.register("configured") {
    println("This is also executed during the configuration phase.")
}

// Execution Phase
tasks.register("test") {
    doLast {
        println("This is executed during the execution phase.")
    }
}

// Configurations AND Execution Phase
tasks.register("testBoth") {
    println("This is executed during the configuration phase as well.")
    doFirst {
        println("This is executed first during the execution phase.")
    }
    doLast {
        println("This is executed last during the execution phase.")
    }
}
settings.gradle
rootProject.name = 'basic'
println 'This is executed during the initialization phase.'
build.gradle
// Configuration Phase
println 'This is executed during the configuration phase.'

tasks.register('configured') {
    println 'This is also executed during the configuration phase.'
}

// Execution Phase
tasks.register('test') {
    doLast {
        println 'This is executed during the execution phase.'
    }
}

// Configurations AND Execution Phase
tasks.register('testBoth') {
    println 'This is executed during the configuration phase as well.'
    doFirst {
	  println 'This is executed first during the execution phase.'
	}
	doLast {
	  println 'This is executed last during the execution phase.'
	}
}

The following command executes the test task and testBoth task specified above. Because Gradle only configures the tasks that are required to run (i.e., the requested task and its dependencies), the configured task will not be configured or executed:

$ ./gradlew test testBoth
This is executed during the initialization phase.

> Configure project :
This is executed during the configuration phase.
This is executed during the configuration phase as well.

> Task :test
This is executed during the execution phase.

> Task :testBoth
This is executed first during the execution phase.
This is executed last during the execution phase.

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
$ ./gradlew test testBoth
This is executed during the initialization phase.

> Configure project :
This is executed during the configuration phase.
This is executed during the configuration phase as well.

> Task :test
This is executed during the execution phase.

> Task :testBoth
This is executed first during the execution phase.
This is executed last during the execution phase.

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
Phase 1. Initialization

In the initialization phase, Gradle detects the set of projects (root and subprojects) and included builds participating in the build.

Gradle first evaluates the settings file, settings.gradle(.kts), and instantiates a Settings object.

Then, Gradle instantiates Project object instances for each project included in the build (using includeBuild() or include() in the settings file).

Phase 2. Configuration

In the configuration phase, Gradle adds tasks and other properties to the projects found by the initialization phase.

Gradle constructs the task graph by understanding the dependencies between tasks.

Phase 3. Execution

In the execution phase, Gradle runs tasks.

Gradle uses the task execution graphs generated by the configuration phase to determine which tasks to execute. Gradle can execute tasks in parallel.

Task Graphs

As a build author, you write build logic by defining tasks and declaring how they depend on one another. Gradle uses this information to construct a task graph during the configuration phase that models the relationships between these tasks.

For example, if your project includes tasks such as buildHtml, assembleDocs, and createDocs, and you declare that assembleDocs depends on buildHtml, and createDocs depends on assembleDocs, Gradle constructs a graph with this order: buildHtmlassembleDocscreateDocs.

Gradle builds the task graph before executing any task(s).

Across all projects in the build, tasks form a Directed Acyclic Graph (DAG).

This diagram shows two example task graphs, one abstract and the other concrete, with dependencies between tasks represented as arrows:

task dag examples

Your build scripts and plugins are responsible for declaring this task dependency graph, either explicitly via the task dependency mechanism (e.g., dependsOn) or implicitly using task annotation (e.g., by wiring task inputs and outputs).

Hooking into the Build Lifecycle

As your build logic becomes more advanced, you might need to execute custom actions during specific build phases. Gradle exposes several APIs that let you listen to or react to key events in the lifecycle.

For a detailed guide on available callbacks and listeners, refer to Lifecycle API.

Writing Build Scripts

The initialization phase in the Gradle Build lifecycle finds the settings file.

gradle basic 11

When Gradle evaluates the settings file, it creates a single Settings instance.

Then, for each project declared in the settings file, Gradle creates a corresponding Project instance.

Gradle then locates the associated build script (e.g., build.gradle(.kts)) and uses it during the configuration phase to configure each Project object.

Anatomy of a Build Script

Gradle build scripts are written in either Groovy DSL or Kotlin DSL (domain-specific language). The build script is either a *.gradle file in Groovy or a *.gradle.kts file in Kotlin.

As a build script executes, it configures either a Settings object or Project object and its children.

Build
Build
Tip
There is a third type of build script that also configures a Gradle object, but it is not covered in the intermediate concepts.
Script Structure

A Gradle script consists of two main types of elements:

  1. Statements: Top-level expressions that execute immediately during the initialization (for settings scripts) or configuration (for build scripts) phase.

  2. Blocks: Nested sections (Groovy closures or Kotlin lambdas) passed to configuration methods. These blocks apply settings to Gradle objects like project, pluginManagement, dependencyResolutionManagement, repositories, or dependencies.

Examples of common blocks include:

api/build.gradle.kts
plugins {
    id("java")
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("junit:junit:4.13")
    implementation(project(":shared"))
}
api/build.gradle
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "junit:junit:4.13"
    implementation project(':shared')
}

In this case, we are looking at a build script. Therefore, each block corresponds to a method on the Project object, also referred to as the Project API, and is evaluated with a delegate or receiver (more on that below).

Closures and Lambdas

Gradle scripts are based on dynamic closures in Groovy or static lambdas in Kotlin:

  • In Groovy, blocks are closures, and Gradle dynamically delegates method/property calls to a target object.

  • In Kotlin, blocks are lambdas with receivers, and Gradle statically types the this object inside the block.

This delegation allows concise configuration:

repositories {
    mavenCentral()
}

In this case, the repositories {} block is a method call where the closure configures a RepositoryHandler instance.

repositories {
    mavenCentral()
}

In this case, the repositories {} block is a method call, and the lambda configures a RepositoryHandler instance.

Inside the block, mavenCentral() is a method on that receiver, so no qualifier is needed.

Delegates and Receivers

Every configuration block executes in the context of an object:

  • In Groovy, this is the block’s delegate.

  • In Kotlin, this is the block’s receiver.

Inside the dependencies {} block, for instance, the implementation(…​) method is delegated to the DependencyHandler:

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib")
}

This behavior allows intuitive configuration but can sometimes obscure where a method is coming from. For clarity, you can use explicit references like project.dependencies.implementation(…​).

Variables

Build scripts support two types of variables:

  1. Local Variables

  2. Extra Properties

Local Variables

Declare local variables with the val keyword. Local variables are only visible in the scope where they have been declared. They are a feature of the underlying Kotlin language.

Declare local variables with the def keyword. Local variables are only visible in the scope where they have been declared. They are a feature of the underlying Groovy language.

build.gradle.kts
val dest = "dest"

tasks.register<Copy>("copy") {
    from("source")
    into(dest)
}
build.gradle
def dest = 'dest'

tasks.register('copy', Copy) {
    from 'source'
    into dest
}
Extra Properties

Gradle provides extra properties for storing user-defined data on enhanced objects such as project.

Extra properties are accessible via:

  • extra property in Kotlin.

  • ext property in Groovy.

build.gradle.kts
plugins {
    id("java-library")
}

val springVersion = "3.1.0.RELEASE"
extra["springVersion"] = springVersion
val emailNotification = "build@master.org"
extra["emailNotification"] = emailNotification

sourceSets.all { extra["purpose"] = null }

sourceSets {
    main {
        extra["purpose"] = "production"
    }
    test {
        extra["purpose"] = "test"
    }
    create("plugin") {
        extra["purpose"] = "production"
    }
}

tasks.register("printProperties") {
    val springVersion = springVersion
    val emailNotification = emailNotification
    val productionSourceSets = provider {
        sourceSets.matching { it.extra["purpose"] == "production" }.map { it.name }
    }
    doLast {
        println(springVersion)
        println(emailNotification)
        productionSourceSets.get().forEach { println(it) }
    }
}
build.gradle
plugins {
    id 'java-library'
}

ext {
    springVersion = "3.1.0.RELEASE"
    emailNotification = "build@master.org"
}

sourceSets.all { ext.purpose = null }

sourceSets {
    main {
        purpose = "production"
    }
    test {
        purpose = "test"
    }
    plugin {
        purpose = "production"
    }
}

tasks.register('printProperties') {
    def springVersion = springVersion
    def emailNotification = emailNotification
    def productionSourceSets = provider {
        sourceSets.matching { it.purpose == "production" }.collect { it.name }
    }
    doLast {
        println springVersion
        println emailNotification
        productionSourceSets.get().each { println it }
    }
}
$ gradle -q printProperties
3.1.0.RELEASE
build@master.org
main
plugin

Gradle uses special syntax for defining extra properties to ensure fail-fast behavior. This means Gradle will immediately detect if you try to set a property that hasn’t been declared, helping you catch mistakes early.

Extra properties are attached to the object that owns them (such as project). Unlike local variables, extra properties have a wider scope, you can access them anywhere the owning object is visible, including from subprojects accessing their parent project’s properties.

Line-by-Line Execution

Gradle executes build scripts top to bottom during the configuration phase. That means:

  1. Code is evaluated immediately in order.

  2. Statements outside of configuration blocks execute eagerly.

  3. Properties and logic should be deferred using Provider or lazy APIs when possible (more on this in the next section).

This top-down execution model means the order of declarations can affect behavior, especially when using variables or configuring tasks.

Example Breakdown

Now, let’s take a look at an example and break it down:

build.gradle.kts
plugins {   // (1)
    id("application")
}

repositories {  // (2)
    mavenCentral()
}

dependencies {  // (3)
    testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    implementation("com.google.guava:guava:32.1.1-jre")
}

application {   // (4)
    mainClass = "com.example.Main"
}

tasks.named<Test>("test") { // (5)
    useJUnitPlatform()
}

tasks.named<Javadoc>("javadoc").configure {
    exclude("app/Internal*.java")
    exclude("app/internal/*")
}

tasks.register<Zip>("zip-reports") {
    from("Reports/")
    include("*")
    archiveFileName.set("Reports.zip")
    destinationDirectory.set(file("/dir"))
}
build.gradle
plugins {   // (1)
    id 'application'
}

repositories {  // (2)
    mavenCentral()
}

dependencies {  // (3)
    testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.3'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    implementation 'com.google.guava:guava:32.1.1-jre'
}

application {   // (4)
    mainClass = 'com.example.Main'
}

tasks.named('test', Test) { // (5)
    useJUnitPlatform()
}

tasks.named('javadoc', Javadoc).configure {
    exclude 'app/Internal*.java'
    exclude 'app/internal/*'
}

tasks.register('zip-reports', Zip) {
    from 'Reports/'
    include '*'
    archiveFileName = 'Reports.zip'
    destinationDirectory = file('/dir')
}
  1. Apply plugins to the build.

  2. Define the locations where dependencies can be found.

  3. Add dependencies.

  4. Set properties.

  5. Register and configure tasks.

1. Apply plugins to the build

Plugins are used to extend Gradle. They are also used to modularize and reuse project configurations.

Plugins can be applied using the PluginDependenciesSpec plugins script block.

The plugins block is preferred:

build.gradle.kts
plugins {   // (1)
    id("application")
}
build.gradle
plugins {   // (1)
    id 'application'
}

In the example, the application plugin, which is included with Gradle, has been applied, describing our project as a Java application.

2. Define the locations where dependencies can be found

A project generally has a number of dependencies it needs to do its work. Dependencies include plugins, libraries, or components that Gradle must download for the build to succeed.

The build script lets Gradle know where to look for the binaries of the dependencies. More than one location can be provided:

build.gradle.kts
repositories {  // (2)
    mavenCentral()
}
build.gradle
repositories {  // (2)
    mavenCentral()
}

In the example, the guava library and the JetBrains Kotlin plugin (org.jetbrains.kotlin.jvm) will be downloaded from the Maven Central Repository.

3. Add dependencies

A project generally has a number of dependencies it needs to do its work. These dependencies are often libraries of precompiled classes that are imported in the project’s source code.

Dependencies are managed via configurations and are retrieved from repositories.

Use the DependencyHandler returned by Project.getDependencies() method to manage the dependencies. Use the RepositoryHandler returned by Project.getRepositories() method to manage the repositories.

build.gradle.kts
dependencies {  // (3)
    testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    implementation("com.google.guava:guava:32.1.1-jre")
}
build.gradle
dependencies {  // (3)
    testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.3'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    implementation 'com.google.guava:guava:32.1.1-jre'
}

In the example, the application code uses Google’s guava libraries. Guava provides utility methods for collections, caching, primitives support, concurrency, common annotations, string processing, I/O, and validations.

4. Set properties

A plugin can add properties and methods to a project using extensions.

The Project object has an associated ExtensionContainer object that contains all the settings and properties for the plugins that have been applied to the project.

In the example, the application plugin added an application property, which is used to detail the main class of our Java application:

build.gradle.kts
application {   // (4)
    mainClass = "com.example.Main"
}
build.gradle
application {   // (4)
    mainClass = 'com.example.Main'
}
5. Register and configure tasks

Tasks perform some basic piece of work, such as compiling classes, or running unit tests, or zipping up a WAR file.

While tasks are typically defined in plugins, you may need to register or configure tasks in build scripts.

Registering a task adds the task to your project.

You can register tasks in a project using the TaskContainer.register(java.lang.String) method:

build.gradle.kts
tasks.register<Zip>("zip-reports") {
    from("Reports/")
    include("*")
    archiveFileName.set("Reports.zip")
    destinationDirectory.set(file("/dir"))
}
build.gradle
tasks.register('zip-reports', Zip) {
    from 'Reports/'
    include '*'
    archiveFileName = 'Reports.zip'
    destinationDirectory = file('/dir')
}

You may have seen usage of the TaskContainer.create(java.lang.String) method which should be avoided.

tasks.create<Zip>("zip-reports") { }
Tip
register(), which enables task configuration avoidance, is preferred over create().

You can locate a task to configure it using the TaskCollection.named(java.lang.String) method:

build.gradle.kts
tasks.named<Test>("test") { // (5)
    useJUnitPlatform()
}
build.gradle
tasks.named('test', Test) { // (5)
    useJUnitPlatform()
}

The example below configures the Javadoc task to automatically generate HTML documentation from Java code:

build.gradle.kts
tasks.named<Javadoc>("javadoc").configure {
    exclude("app/Internal*.java")
    exclude("app/internal/*")
}
build.gradle
tasks.named('javadoc', Javadoc).configure {
    exclude 'app/Internal*.java'
    exclude 'app/internal/*'
}

Accessing Project Properties in Build Scripts

In a Gradle build script, you can refer to project-level properties like name, version, or group without needing to qualify them with project:

build.gradle.kts
println(name)
println(project.name)
build.gradle
println name
println project.name
$ gradle -q check
project-api
project-api

This works because of how Gradle evaluates build scripts:

  • In Groovy, Gradle dynamically delegates unqualified references like name to the Project object.

  • In Kotlin, the build script is compiled as an extension of the Project type, so you can directly access its properties.

While you can always use project.name to be explicit, using the shorthand name is common and safe in most situations.

Accessing Settings Properties in Settings Scripts

Just like build scripts operate within a Project context, settings scripts (settings.gradle(.kts)) operate within a Settings context.

This means you can refer to properties and methods available on the Settings object, often without qualification.

For example:

println(rootProject.name)
println(name)

In a settings.gradle(.kts) script, both of these print the name of the root project. That’s because:

  • In Groovy, unqualified property references like name are dynamically delegated to the Settings object.

  • In Kotlin, the script is compiled as an extension of the Settings class, so name and pluginManagement {} are directly accessible.

Unlike in build scripts, where name refers to the current subproject, in settings scripts name typically refers to the root project name, and it can be set explicitly:

rootProject.name = "my-awesome-project"

Default Script Imports

To make build scripts more concise, Gradle automatically adds a set of import statements to scripts.

As a result, instead of writing throw new org.gradle.api.tasks.StopExecutionException(), you can write throw new StopExecutionException().

Gradle Managed Types

Gradle Managed Types are the building blocks for writing modern, efficient, and cache-friendly build logic.

gradle basic 12

When writing build logic, you’ll often reach for standard Groovy or Kotlin types. However, Gradle provides its own managed types that are lazy, making them far better suited for build logic.

These types enable:

  • Incremental builds

  • Build cache support

  • Configuration cache compatibility

  • Accurate tracking of task inputs and outputs

For example, instead of using a String, you should use a Property<String>:

build.gradle
tasks.register("demoTask") {
    // Eager evaluation: happens immediately during configuration
    def eagerMessage = "Hello from eager"

    // Lazy evaluation: will only be computed if the task runs during execution
    def lazyMessage = objects.property(String)
    lazyMessage.set(providers.provider {
        return "Hello from lazy"
    })

    // This block runs during the configuration phase (when the build script is loaded)
    println ">> DURING CONFIGURATION"
    println ">>> eagerMessage type: ${eagerMessage.getClass()}"
    println ">>> eagerMessage value: $eagerMessage"
    println ">>> lazyMessage type: ${lazyMessage.getClass()}"
    println ">>> lazyMessage value: $lazyMessage"

    // This block runs during the execution phase (when the task is actually run)
    doLast {
        println ">> DURING EXECUTION"
        println ">>> eagerMessage type: ${eagerMessage.getClass()}"
        println ">>> eagerMessage value: $eagerMessage"
        // Now the provider is evaluated and the actual value is computed and printed
        println ">>> lazyMessage type: ${lazyMessage.getClass()}"
        println ">>> lazyMessage value: ${lazyMessage.get()}"
    }
}
$ ./gradlew demoTask
>> DURING CONFIGURATION
>>> eagerMessage type: class kotlin.String
>>> eagerMessage value: Hello from eager
>>> lazyMessage type: class org.gradle.api.internal.provider.DefaultProperty
>>> lazyMessage value: property(java.lang.String, map(java.lang.String provider(?) check-type()))

> Task :app:demoTask
>> DURING EXECUTION
>>> eagerMessage type: class kotlin.String
>>> eagerMessage value: Hello from eager
>>> lazyMessage type: class org.gradle.api.internal.provider.DefaultProperty
>>> lazyMessage value: Hello from lazy

This example showcases that:

  • Eager values are computed immediately when the script is loaded (configuration phase).

  • Lazy values (like Property or Provider) defer computation until the task is executed (execution phase).

It’s always best to ensure Gradle evaluates task logic only during the execution phase to avoid wasting time during configuration. Using lazy types and values in Gradle ensures builds are fast, efficient, and compatible with performance features like incremental builds, the Build Cache, and the Configuration Cache.

Common Managed Types

Here is a list of common Gradle managed types you can use in your build logic:

Type Purpose Use-Case

Property<T>

Lazy scalar value (e.g., a version number)

A version string

ListProperty<T>

Lazy list of values (e.g., compiler args)

A list of compiler args

MapProperty<K, V>

Lazy map of values (e.g., environment vars)

A map of Maven Pom properties

RegularFileProperty

Lazy reference to a single file (input or output)

A single file input/output

DirectoryProperty

Lazy reference to a directory (input or output)

A destination directory

Provider<T>

Lazily evaluated, read-only value (e.g., task output)

A dependency on task output

Gradle distinguishes between eager and lazy evaluation to control when values are computed and how they participate in up-to-date checks, caching, and task wiring. Let’s review the differences one more time:

Eager Evaluation

  • Happens immediately during the configuration phase.

  • Values are resolved as soon as the line is executed.

  • Prevents Gradle from optimizing build logic.

build.gradle.kts
val version = project.version.toString()         // evaluated now
val file = File("build/output.txt")              // evaluated now
myTask.outputFile.set(file)
build.gradle
def version = project.version.toString()         // evaluated now
def file = new File('build/output.txt')          // evaluated now
myTask.outputFile.set(file)

These eager values are resolved even if the task isn’t run, breaking configuration cache compatibility and reducing build performance.

Lazy Evaluation

  • Defers computation until it’s actually needed (usually during task execution).

  • Enables Gradle to apply caching, parallel execution, and configuration avoidance.

  • Fully supports configuration cache and incremental builds.

build.gradle.kts
val outputFile: RegularFileProperty = project.objects.fileProperty()
outputFile.set(layout.buildDirectory.file("output.txt"))    // evaluated later
build.gradle
RegularFileProperty outputFile = project.objects.fileProperty()
outputFile.set(layout.buildDirectory.file('output.txt'))    // evaluated later

Here, nothing is resolved immediately—Gradle delays evaluation until it’s actually required.

Let’s take a look at an example.

Example

This task will break the Configuration Cache due to eager access:

build.gradle.kts
// Not Configuration Cache compatible
tasks.register("printVersion") {
    doLast {
        val eager_version = project.version.toString()
        println("Version is $eager_version")
    }
}
build.gradle
// Not Configuration Cache compatible
tasks.register('printVersion') {
    doLast {
        def eager_version = project.version.toString()
        println "Version is $eager_version"
    }
}

The fix: use a lazy Provider or a Property<String>:

build.gradle.kts
// Configuration Cache compatible
tasks.register("printVersionLazy") {
    val lazy_version: Property<String> = project.objects.property(String::class.java)
    lazy_version.set(project.version.toString())
    doLast {
        println("Version is ${lazy_version.get()}")
    }
}
build.gradle
// Configuration Cache compatible
tasks.register('printVersionLazy') {
    Property<String> lazy_version = project.objects.property(String)
    lazy_version.set(project.version.toString())
    doLast {
        println "Version is ${lazy_version.get()}"
    }
}

Declaring and Managing Dependencies

Gradle provides a rich and flexible model for declaring dependencies, managing versions, and resolving conflicts across builds.

gradle basic 13

Declare Dependencies

The dependencies{} block is where you declare the external libraries, internal modules, or files your project needs to compile, run, or test:

build.gradle.kts
dependencies {
    implementation("com.google.guava:guava:30.0-jre")
    runtimeOnly("org.apache.commons:commons-lang3:3.14.0")
}
build.gradle
dependencies {
    implementation("com.google.guava:guava:30.0-jre")
    runtimeOnly("org.apache.commons:commons-lang3:3.14.0")
}

Each dependency is added to a bucket configuration. For example implementation, runtimeOnly, or testImplementation. Bucket configurations defines where that dependency is used (compile classpath, runtime only, tests, etc.). The set of configurations available depends on the plugins you apply (e.g., java/java-library, Android Gradle Plugin (AGP), Kotlin Multiplatform (KMP), etc.).

Gradle recommends the single-string notation for external modules. The map notation is deprecated as of Gradle 9.1.0 and will fail your build in Gradle 10:

build.gradle.kts
dependencies {
    // GOOD: single-string notation
    implementation("com.google.guava:guava:32.1.2-jre")
    // BAD: map notation
    implementation(group = "com.google.guava", name = "guava", version = "32.1.2-jre")
}
build.gradle
dependencies {
    // GOOD: single-string notation
    implementation 'com.google.guava:guava:32.1.2-jre'
    // BAD: map notation (deprecated, triggers --warning-mode=fail)
    implementation group: 'com.google.guava', name: 'guava', version: '32.1.2-jre'
}

Centralize Versions with Version Catalogs

Gradle recommends using version catalogs to declare dependency versions in a single, reusable location:

gradle/libs.versions.toml
[versions]
guava = "33.3.1-jre"
junit-jupiter = "5.11.3"

[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }

You can then use these aliases in your build scripts:

app/build.gradle.kts
dependencies {  // (2)
    // Use JUnit Jupiter for testing.
    testImplementation(libs.junit.jupiter)

    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // This dependency is used by the application.
    implementation(libs.guava)
}
app/build.gradle
dependencies {  // (2)
    // Use JUnit Jupiter for testing.
    testImplementation libs.junit.jupiter

    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // This dependency is used by the application.
    implementation libs.guava
}

Enforce and Constrain Versions

Gradle allows you to constrain dependency versions to avoid unwanted upgrades or enforce known good versions:

build.gradle.kts
dependencies {
    implementation("org.apache.httpcomponents:httpclient:4.5.4")
    implementation("commons-codec:commons-codec") {
        version {
            strictly("1.9")
        }
    }
}
build.gradle
dependencies {
    implementation("org.apache.httpcomponents:httpclient:4.5.4")
    implementation("commons-codec:commons-codec") {
        version {
            strictly("1.9")
        }
    }
}

You can also constrain a module globally:

build.gradle.kts
dependencies {
    implementation("org.apache.httpcomponents:httpclient")
    constraints {
        implementation("org.apache.httpcomponents:httpclient:4.5.3") {
            because("previous versions have a bug impacting this application")
        }
        implementation("commons-codec:commons-codec:1.11") {
            because("version 1.9 pulled from httpclient has bugs affecting this application")
        }
    }
}
build.gradle
dependencies {
    implementation('org.apache.httpcomponents:httpclient')
    constraints {
        implementation('org.apache.httpcomponents:httpclient:4.5.3') {
            because('previous versions have a bug impacting this application')
        }
        implementation('commons-codec:commons-codec:1.11') {
            because('version 1.9 pulled from httpclient has bugs affecting this application')
        }
    }
}

Resolve Conflicts with Capabilities

Sometimes multiple libraries provide the same functionality under different coordinates. This can lead to classpath conflicts:

build.gradle.kts
dependencies {
    implementation("jaxen:jaxen:1.1.6")     // Transitive dependency that brings XPath functionality
    implementation("org.jdom:jdom2:2.0.6")  // Also offers XPath functionality
}
build.gradle
dependencies {
    implementation 'jaxen:jaxen:1.1.6'     // Transitive dependency that brings XPath functionality
    implementation 'org.jdom:jdom2:2.0.6'  // Also offers XPath functionality
}

Gradle lets you model these cases using capabilities. For example:

build.gradle.kts
dependencies {
    implementation("jaxen:jaxen:1.1.6") {
        capabilities {
            requireCapability("xml:xpath-support")
        }
    }
    implementation("org.jdom:jdom2:2.0.6")
}
build.gradle
dependencies {
    implementation('jaxen:jaxen:1.1.6') {
        capabilities {
            requireCapability('xml:xpath-support')
        }
    }
    implementation 'org.jdom:jdom2:2.0.6'
}

Then declare capabilities via a component metadata rule:

build.gradle.kts
dependencies {
    components {
        withModule("jaxen:jaxen") {
            allVariants {
                withCapabilities {
                    addCapability("xml", "xpath-support", "1.0")
                }
            }
        }
        withModule("org.jdom:jdom2") {
            allVariants {
                withCapabilities {
                    addCapability("xml", "xpath-support", "1.0")
                }
            }
        }
    }
}
build.gradle
dependencies {
    components {
        withModule('jaxen:jaxen') {
            allVariants {
                withCapabilities {
                    addCapability('xml', 'xpath-support', '1.0')
                }
            }
        }
        withModule('org.jdom:jdom2') {
            allVariants {
                withCapabilities {
                    addCapability('xml', 'xpath-support', '1.0')
                }
            }
        }
    }
}

Gradle will now treat jaxen and jdom2 as alternate implementations and select the one with a required capability.

There are many more ways to influence dependency resolution in Gradle. Consult the Dependency Management chapter to learn more.

Creating and Registering Tasks

The work that Gradle can do on a project is defined by one or more tasks.

gradle basic 14

A task represents some independent unit of work that a build performs. This might be compiling some classes, creating a JAR, generating Javadoc, or publishing some archives to a repository.

When a user runs ./gradlew build in the command line, Gradle will execute the build task along with any other tasks it depends on.

Task Types

A task type defines what kind of work a task can do. It’s like a blueprint or class.

Gradle includes many built-in task types, such as Copy, Jar, and Test, and you can also define your own. By default, a task is of type DefaultTask .

Let’s start with a simple custom task that prints a message:

build.gradle.kts
tasks.register("hello") {
    doLast {
        println("Hello world!")
    }
}
build.gradle
tasks.register('hello') {
    doLast {
        println 'Hello world!'
    }
}

You just registered a task called hello of type DefaultTask and gave it an action using doLast{}.

A task is created in the build script using the TaskContainer.register() method, which allows it to be then used in the build logic.

When you run the hello task in the command-line using ./gradlew hello, it prints your message:

$ ./gradlew hello
Hello world!

When you register (i.e. create) a task in your build script, you can:

  • Use the default task type (DefaultTask) and define the behavior inline.

  • Use a built-in task type, like Copy, to take advantage of pre-defined behavior.

  • Create and use a custom task type if you need reusable behavior across tasks.

This example registers a task called copyTask which copies \*.war files from the source directory to the target directory using the Copy built-in task type:

build.gradle.kts
tasks.register<Copy>("copyTask") {
    from("source")
    into("target")
    include("*.war")
}
build.gradle
tasks.register('copyTask', Copy) {
    from("source")
    into("target")
    include("*.war")
}

Built-in Task Types

Gradle provides many built-in task types with common and popular functionality, such as copying or deleting files.

This registers a Gradle task named removeOutput of type Delete. When the task runs, it will delete the file build/outputs/1.txt relative to the project directory.

build.gradle.kts
tasks.register<Delete>("removeOutput") {
    delete(layout.buildDirectory.file("outputs/1.txt"))
}
build.gradle
tasks.register('removeOutput', Delete) {
    delete layout.buildDirectory.file("outputs/1.txt")
}

There are many task types developers can take advantage of, including GroovyDoc, Zip, Jar, JacocoReport, Sign, or Delete, which are detailed in the DSL.

Custom Task Types

Gradle tasks are a subclass of Task.

In the example below, the HelloTask class, a custom task type, is created by extending DefaultTask (our default task type):

build.gradle.kts
// Extend the DefaultTask class to create a HelloTask class
abstract class HelloTask : DefaultTask() {
    @TaskAction
    fun hello() {
        println("hello from HelloTask")
    }
}

// Register the hello Task with type HelloTask
tasks.register<HelloTask>("hello") {
    group = "Custom tasks"
    description = "A lovely greeting task."
}
build.gradle
// Extend the DefaultTask class to create a HelloTask class
class HelloTask extends DefaultTask {
    @TaskAction
    void hello() {
        println("hello from HelloTask")
    }
}

// Register the hello Task with type HelloTask
tasks.register("hello", HelloTask) {
    group = "Custom tasks"
    description = "A lovely greeting task."
}

The hello task is registered with the new type HelloTask.

Executing our new hello task results in the following:

$ ./gradlew hello
> Task :app:hello
hello from HelloTask

The Gradle help task can reveal the specifications of the hello task:

$ ./gradlew help --task hello
> Task :help
Detailed task information for hello

Path
:app:hello

Type
HelloTask (Build_gradle$HelloTask)

Options
--rerun     Causes the task to be re-run even if up-to-date.

Description
A lovely greeting task.

Group
Custom tasks

Task Input and Outputs

For a custom task to do useful work, it typically needs some inputs which it uses to produce outputs.

A task can declare those inputs (files, values) and outputs (files it creates). Ideally, these inputs and outputs leverage Gradle managed types. This helps Gradle skip work when nothing has changed:

build.gradle.kts
abstract class CreateAFileTask : DefaultTask() {
    @get:Input
    abstract val fileText: Property<String>

    @Input
    val fileName = "myfile.txt"

    @OutputFile
    val myFile: File = File(fileName)

    @TaskAction
    fun action() {
        myFile.createNewFile()
        myFile.writeText(fileText.get())
    }
}
build.gradle
abstract class CreateAFileTask extends DefaultTask {
    @Input
    abstract Property<String> getFileText()

    @Input
    final String fileName = "myfile.txt"

    @OutputFile
    final File myFile = new File(fileName)

    @TaskAction
    void action() {
        myFile.createNewFile()
        myFile.text = fileText.get()
    }
}

Now Gradle knows what the task needs and what it produces. If nothing changes, the task is skipped.

Task Action

Task actions are the blocks of code that define what the custom task does when it runs.

Every task can have one or more actions, and they’re executed during the execution phase of the Gradle build lifecycle.

In the example below, a custom task type is created called GreetingTask. The @TaskAction annotation marks a method that Gradle should call when the task of this type is executed:

build.gradle.kts
abstract class GreetingTask : DefaultTask() {
    @TaskAction
    fun greet() {
        println("hello from GreetingTask")
    }
}

// Create a task using the task type
tasks.register<GreetingTask>("hello")
build.gradle
abstract class GreetingTask extends DefaultTask {
    @TaskAction
    def greet() {
        println 'hello from GreetingTask'
    }
}

// Create a task using the task type
tasks.register('hello', GreetingTask)

A task action can also be added using doLast {} or doFirst {}:

build.gradle.kts
tasks.register("hello") {
    doLast {
        println("Hello world!")
    }
}
build.gradle
tasks.register('hello') {
    doLast {
        println 'Hello world!'
    }
}

In this example, the action is println("Hello world!"). It will run when the task is executed.

Task Group and Description

Group and description are metadata properties used to organize and document tasks. They are primarily used to make the project easier to navigate for developers.

  1. The group acts as a category for the task. When you run ./gradlew tasks, Gradle clusters all tasks with the same group name together.

  2. The description is a short summary explaining what the task actually does.

Task Dependencies

You can declare tasks that depend on other tasks:

build.gradle.kts
tasks.register("hello") {
    doLast {
        println("Hello world!")
    }
}
tasks.register("intro") {
    dependsOn("hello")
    doLast {
        println("I'm Gradle")
    }
}
build.gradle
tasks.register('hello') {
    doLast {
        println 'Hello world!'
    }
}
tasks.register('intro') {
    dependsOn tasks.hello
    doLast {
        println "I'm Gradle"
    }
}
$ gradle -q intro
Hello world!
I'm Gradle

The dependency of taskX to taskY may be declared before taskY is defined:

build.gradle.kts
tasks.register("taskX") {
    dependsOn("taskY")
    doLast {
        println("taskX")
    }
}
tasks.register("taskY") {
    doLast {
        println("taskY")
    }
}
build.gradle
tasks.register('taskX') {
    dependsOn 'taskY'
    doLast {
        println 'taskX'
    }
}
tasks.register('taskY') {
    doLast {
        println 'taskY'
    }
}
$ ./gradlew -q taskX
taskY
taskX

The hello task from the previous example is updated to include a dependency:

build.gradle.kts
tasks.register("hello") {
    group = "Custom"
    description = "A lovely greeting task."
    doLast {
        println("Hello world!")
    }
    dependsOn(tasks.assemble)
}
build.gradle
tasks.register('hello') {
    group = "Custom"
    description = "A lovely greeting task."
    doLast {
        println("Hello world!")
    }
    dependsOn(tasks.assemble)
}

The hello task now depends on the assemble task, which means that Gradle must execute the assemble task before it can execute the hello task:

$ ./gradlew :app:hello
> Task :app:compileJava UP-TO-DATE
> Task :app:processResources NO-SOURCE
> Task :app:classes UP-TO-DATE
> Task :app:jar UP-TO-DATE
> Task :app:startScripts UP-TO-DATE
> Task :app:distTar UP-TO-DATE
> Task :app:distZip UP-TO-DATE
> Task :app:assemble UP-TO-DATE

> Task :app:hello
Hello world!

Task Configuration

Once registered, tasks can be accessed via the TaskProvider API for further configuration.

For instance, you can add behavior to an existing task:

build.gradle.kts
tasks.register("hello") {
    doLast {
        println("Hello Earth")
    }
}
tasks.named("hello") {
    doFirst {
        println("Hello Venus")
    }
}
tasks.named("hello") {
    doLast {
        println("Hello Mars")
    }
}
tasks.named("hello") {
    doLast {
        println("Hello Jupiter")
    }
}
build.gradle
tasks.register('hello') {
    doLast {
        println 'Hello Earth'
    }
}
tasks.named('hello') {
    doFirst {
        println 'Hello Venus'
    }
}
tasks.named('hello') {
    doLast {
        println 'Hello Mars'
    }
}
tasks.named('hello') {
    doLast {
        println 'Hello Jupiter'
    }
}
$ ./gradlew -q hello
Hello Venus
Hello Earth
Hello Mars
Hello Jupiter
Tip
The calls doFirst and doLast can be executed multiple times. They add an action to the beginning or the end of the task’s actions list. When the task executes, the actions in the action list are executed in order.

A task is optionally configured in a build script using the TaskCollection.named() method.

Task Classification

There are two classes of tasks that can be executed:

  1. Actionable tasks have some action(s) attached to do work in your build: compileJava.

  2. Lifecycle tasks are tasks with no actions attached: assemble, build.

Typically, a lifecycle tasks depends on many actionable tasks, and is used to execute many tasks at once.

Task Performance

To write "good" Gradle tasks, you should focus on three things: speed, clarity, and intelligence.

1. Load configuration lazily

A "fast" task is one that loads configuration lazily.

Use lazy APIs like tasks.register() instead of eager ones like tasks.create(). register() tells Gradle to only configure the task if someone actually runs it. If you use create(), Gradle sets up that task every single time you do anything (even just checking the version), which adds up to a very slow experience.

2. Define inputs and outputs

A "clear" task is one that knows when it doesn’t need to run.

By defining your inputs and outputs, Gradle can perform up-to-date checks. If you run the task twice and nothing has changed, Gradle should say UP-TO-DATE and finish in milliseconds.

3. Do work during execution

A "smart" task does the work during the Execution Phase, not the Configuration Phase.

One of the most common beginner mistakes is putting code in the wrong place:

  • Configuration Block: Use this only to set up settings (like the group or description).

  • Execution Block (doLast or @TaskAction): Use this for the actual work (moving files, compiling code, etc.)

4. Document Your Work

A "good" task defines a group and a description. When a teammate runs ./gradlew tasks, your custom task will appear in a neat category with a clear explanation of what it does, rather than being buried in the "Other" category.

Next Step: Learn about Plugins >>

Working with Plugins

Much of Gradle’s functionality is delivered via plugins, including core plugins distributed with Gradle, third-party plugins, and custom plugins defined within builds.

gradle basic 15

Plugins introduce new tasks (e.g., JavaCompile), domain objects (e.g., SourceSet), conventions (e.g., locating Java source at src/main/java), and extend core or other plugin objects.

Plugins in Gradle are essential for automating common build tasks, integrating with external tools or services, and tailoring the build process to meet specific project needs. They also serve as the primary mechanism for organizing build logic.

Plugin Distribution

You can leverage plugins from Gradle and the Gradle community or create your own.

Plugins are available in three ways:

  1. Core plugins - Gradle develops and maintains a set of Core Plugins.

  2. Community plugins - Gradle plugins shared in a remote repository such as Maven or the Gradle Plugin Portal.

  3. Custom plugins - Gradle enables users to create plugins using APIs.

Gradle provides core plugins (e.g., JavaPlugin, GroovyPlugin, MavenPublishPlugin, etc.) as part of its distribution, which means they are available with Gradle itself.

Core plugins are applied in a build script using the plugin name:

plugins {
    id «plugin name»
}

For example:

build.gradle.kts
plugins {
    id("java")
}
build.gradle
plugins {
    id 'java'
}

Non-core (community and custom) plugins must be resolved (i.e. located) before they can be applied. Non-core plugins are identified by a unique ID and a version in the build file:

plugins {
    id «plugin id» version «plugin version»
}

For example:

build.gradle.kts
plugins {
    id("com.gradleup.shadow") version "8.3.4"
}
build.gradle
plugins {
    id 'com.gradleup.shadow' version '8.3.4'
}

And the location of the plugin must be specified in the settings file as needed:

settings.gradle.kts
pluginManagement {  // (1)
    repositories {
        gradlePluginPortal()
    }
}
settings.gradle
pluginManagement {  // (1)
    repositories {
        gradlePluginPortal()
    }
}

The location of the plugin could also be an included build or buildSrc for example.

Applying Plugins

Over time, Gradle has introduced several ways to apply plugins, depending on their source and the scope in which they are needed (such as whether they apply to a single project or multiple subprojects).

Let’s take a look at all the available ways a plugin can be applied:

# To Use For example:

1

Apply a plugin to a project.

The plugins block in the build file.

plugins {
  id("org.barfuin.gradle.taskinfo") version "2.1.0"
}

2

Apply a plugin to multiple projects.

The subprojects or allprojects blocks in the root build file. Not Recommended

plugins {
    id("org.barfuin.gradle.taskinfo") version "2.1.0"
}
allprojects {
    apply(plugin = "org.barfuin.gradle.taskinfo")
    repositories {
        mavenCentral()
    }
}

3

Apply a plugin to multiple projects.

The plugins block in the root build file.

plugins {
    id("com.gradleup.shadow") version "8.3.4" apply false
    id("io.ratpack.ratpack-java") version "1.8.2" apply false
}

4

Apply a plugin to multiple projects.

A convention plugin in the buildSrc directory. Recommended.

plugins {
    id("my-convention.gradle.taskinfo")
}

5

Apply a plugin needed for the build script itself.

The buildscript block in the build file itself. Legacy.

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.barfuin.gradle.taskinfo:gradle-taskinfo:2.1.0")
  }
}
apply(plugin = "org.barfuin.gradle.taskinfo")

6

Apply a script plugins.

Applying a plugin when type-safe accessors are not available.

The legacy apply() method in the build file. Not Recommended. Legacy.

apply<MyCustomBarfuinTaskInfoPlugin>()
1. Applying plugins using the plugins{} block

The plugin DSL provides a concise and convenient way to declare plugin dependencies.

The plugins block configures an instance of PluginDependenciesSpec:

plugins {
    application                                     // by name
    java                                            // by name
    id("java")                                      // by id - recommended
    id("org.jetbrains.kotlin.jvm") version "1.9.0"  // by id - recommended
}

Core Gradle plugins are unique in that they provide short names, such as java for the core JavaPlugin.

To apply a core plugin, the short name can be used:

build.gradle.kts
plugins {
    java
}
build.gradle
plugins {
    id 'java'
}

All other binary plugins must use the fully qualified form of the plugin id (e.g., com.github.foo.bar).

To apply a community plugin from Gradle plugin portal, the fully qualified plugin id, a globally unique identifier, must be used:

build.gradle.kts
plugins {
    id("org.springframework.boot") version "3.3.1"
}
build.gradle
plugins {
    id 'org.springframework.boot' version '3.3.1'
}

See PluginDependenciesSpec for more information on using the Plugin DSL.

Limitations of the plugins DSL

The plugins DSL provides a convenient syntax for users and the ability for Gradle to determine which plugins are used quickly. This allows Gradle to:

  • Optimize the loading and reuse of plugin classes.

  • Provide editors with detailed information about the potential properties and values in the build script.

However, the DSL requires that plugins be defined statically.

There are some key differences between the plugins {} block mechanism and the "traditional" apply() method mechanism. There are also some constraints and possible limitations.

The plugins{} block can only be used in a project’s build script build.gradle(.kts) and the settings.gradle(.kts) file. It must appear before any other block. It cannot be used in script plugins or init scripts.

Constrained Syntax

The plugins {} block does not support arbitrary code.

It is constrained to be idempotent (produce the same result every time) and side effect-free (safe for Gradle to execute at any time).

The form is:

plugins {
    id(«plugin id»)                             // (1)
    id(«plugin id») version «plugin version»    // (2)
}
  1. for core Gradle plugins or plugins already available to the build script

  2. for binary Gradle plugins that need to be resolved

Where «plugin id» and «plugin version» are a string.

Where «plugin id» and «plugin version» must be constant, literal strings.

The plugins{} block must also be a top-level statement in the build script. It cannot be nested inside another construct (e.g., an if-statement or for-loop).

2. Applying plugins to all subprojects{} or allprojects{}

Suppose you have a multi-project build, you probably want to apply plugins to some or all of the subprojects in your build but not to the root project.

While the default behavior of the plugins{} block is to immediately resolve and apply the plugins, you can use the apply false syntax to tell Gradle not to apply the plugin to the current project. Then, use the plugins{} block without the version in subprojects' build scripts:

settings.gradle.kts
include("hello-a")
include("hello-b")
include("goodbye-c")
build.gradle.kts
plugins {
    // These plugins are not automatically applied.
    // They can be applied in subprojects as needed (in their respective build files).
    id("com.example.hello") version "1.0.0" apply false
    id("com.example.goodbye") version "1.0.0" apply false
}

allprojects {
    // Apply the common 'java' plugin to all projects (including the root)
    plugins.apply("java")
}

subprojects {
    // Apply the 'java-library' plugin to all subprojects (excluding the root)
    plugins.apply("java-library")
}
hello-a/build.gradle.kts
plugins {
    id("com.example.hello")
}
hello-b/build.gradle.kts
plugins {
    id("com.example.hello")
}
goodbye-c/build.gradle.kts
plugins {
    id("com.example.goodbye")
}
settings.gradle
include 'hello-a'
include 'hello-b'
include 'goodbye-c'
build.gradle
plugins {
    // These plugins are not automatically applied.
    // They can be applied in subprojects as needed (in their respective build files).
    id 'com.example.hello' version '1.0.0' apply false
    id 'com.example.goodbye' version '1.0.0' apply false
}

allprojects {
    // Apply the common 'java' plugin to all projects (including the root)
    apply(plugin: 'java')
}

subprojects {
    // Apply the 'java-library' plugin to all subprojects (excluding the root)
    apply(plugin: 'java-library')
}
hello-a/build.gradle
plugins {
    id 'com.example.hello'
}
hello-b/build.gradle
plugins {
    id 'com.example.hello'
}
goodbye-c/build.gradle
plugins {
    id 'com.example.goodbye'
}

You can also encapsulate the versions of external plugins by composing the build logic using your own convention plugins.

3. Applying plugins declared in the root project

You can apply plugins from the root or parent project in a multi-project build to share common logic and behavior with other projects. The root/parent project is the project at the top of the directory hierarchy.

You should use the plugins {} block because it ensures that the plugin is applied and configured before the project’s evaluation phase. This way, you can safely use type-safe accessors for any model elements introduced by the plugin:

settings.gradle.kts
rootProject.name = "multi-project-build"
include("domain", "infra", "http")
build.gradle.kts
plugins {
    id("com.gradleup.shadow") version "8.3.4" apply false
    id("io.ratpack.ratpack-java") version "2.0.0-rc-1" apply false
}
domain/build.gradle.kts
plugins {
    `java-library`
}

dependencies {
    api("javax.measure:unit-api:1.0")
    implementation("tec.units:unit-ri:1.0.3")
}
infra/build.gradle.kts
plugins {
    `java-library`
    id("com.gradleup.shadow")
}
http/build.gradle.kts
plugins {
    java
    id("io.ratpack.ratpack-java")
}

dependencies {
    implementation(project(":domain"))
    implementation(project(":infra"))
    implementation(ratpack.dependency("dropwizard-metrics"))
}

In the root/parent build.gradle(.kts), plugins are declared but not applied (via apply false). This approach, while optional, makes the plugins available to be explicitly applied in specific subprojects. Without apply false, plugins declared in the root project cannot be explicitly applied only to certain subprojects.

Note
apply false is optional. If apply false is not used when declaring plugins in the root build file, those plugins would automatically be applied to the root project.

In the infra subproject, the com.gradleup.shadow plugin, which was made available in the root project, is explicitly applied. The http subproject applies io.ratpack.ratpack-java. The domain subproject does not apply a plugin from the root.

4. Applying convention plugins from the buildSrc directory

buildSrc is an optional directory at the Gradle project root that contains build logic (i.e., plugins) used in building the main project. You can apply plugins that reside in a project’s buildSrc directory as long as they have a defined ID.

The following example shows how to tie the plugin implementation class my.MyPlugin, defined in buildSrc, to the id "my-plugin":

buildSrc/build.gradle.kts
plugins {
    `java-gradle-plugin`
}

gradlePlugin {
    plugins {
        create("myPlugins") {
            id = "my-plugin"
            implementationClass = "my.MyPlugin"
        }
    }
}
buildSrc/build.gradle
plugins {
    id 'java-gradle-plugin'
}

gradlePlugin {
    plugins {
        myPlugins {
            id = 'my-plugin'
            implementationClass = 'my.MyPlugin'
        }
    }
}

The plugin can then be applied by ID:

build.gradle.kts
plugins {
    id("my-plugin")
}
build.gradle
plugins {
    id 'my-plugin'
}
5. Applying plugins using the buildscript{} block

To define libraries or plugins used in the build script itself, you can use the buildscript block. The buildscript block is also used for specifying where to find those dependencies.

This approach is less common with newer versions of Gradle, as the plugins {} block simplifies plugin usage. However, buildscript {} may be necessary when dealing with custom or non-standard plugin repositories as well as libraries dependencies:

build.gradle.kts
import org.yaml.snakeyaml.Yaml
import java.io.File

buildscript {
    repositories {
        maven {
            url = uri("https://plugins.gradle.org/m2/")
        }
        mavenCentral()  // Where to find the plugin
    }
    dependencies {
        classpath("org.yaml:snakeyaml:1.19") // The library's classpath dependency
        classpath("com.gradleup.shadow:shadow-gradle-plugin:8.3.4") // Plugin dependency for legacy plugin application
    }
}

// Applies legacy Shadow plugin
apply(plugin = "com.gradleup.shadow")

// Uses the library in the build script
val yamlContent = """
        name: Project
    """.trimIndent()
val yaml = Yaml()
val data: Map<String, Any> = yaml.load(yamlContent)
build.gradle
import org.yaml.snakeyaml.Yaml

buildscript {
    repositories { // Where to find the plugin or library
        maven {
            url = uri("https://plugins.gradle.org/m2/")
        }
        mavenCentral()
    }
    dependencies {
        classpath 'org.yaml:snakeyaml:1.19' // The library's classpath dependency
        classpath 'com.gradleup.shadow:shadow-gradle-plugin:8.3.4' // Plugin dependency for legacy plugin application
    }
}

// Applies legacy Shadow plugin
apply plugin: 'com.gradleup.shadow'

// Uses the library in the build script
def yamlContent = """
        name: Project Name
    """
def yaml = new Yaml()
def data = yaml.load(yamlContent)
6. Applying script plugins using the legacy apply() method

A script plugin is an ad-hoc plugin, typically written and applied in the same build script. It is applied using the legacy application method:

build.gradle.kts
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        println("Plugin ${this.javaClass.simpleName} applied on ${project.name}")
    }
}

apply<MyPlugin>()
build.gradle
class MyPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println("Plugin ${this.getClass().getSimpleName()} applied on ${project.name}")
    }
}

apply plugin: MyPlugin

Plugin Management

The pluginManagement{} block is used to configure repositories for plugin resolution and to define version constraints for plugins that are applied in the build scripts.

The pluginManagement{} block can be used in a settings.gradle(.kts) file, where it must be the first block in the file:

settings.gradle.kts
pluginManagement {
    plugins {
    }
    resolutionStrategy {
    }
    repositories {
    }
}
rootProject.name = "plugin-management"
settings.gradle
pluginManagement {
    plugins {
    }
    resolutionStrategy {
    }
    repositories {
    }
}
rootProject.name = 'plugin-management'

The block can also be used in Initialization Script:

init.gradle.kts
settingsEvaluated {
    pluginManagement {
        plugins {
        }
        resolutionStrategy {
        }
        repositories {
        }
    }
}
init.gradle
settingsEvaluated { settings ->
    settings.pluginManagement {
        plugins {
        }
        resolutionStrategy {
        }
        repositories {
        }
    }
}
Custom Plugin Repositories

By default, the plugins{} DSL resolves plugins from the public Gradle Plugin Portal.

Many build authors would also like to resolve plugins from private Maven or Ivy repositories because they contain proprietary implementation details or to have more control over what plugins are available to their builds.

To specify custom plugin repositories, use the repositories{} block inside pluginManagement{}:

settings.gradle.kts
pluginManagement {
    repositories {
        maven(url = file("./maven-repo"))
        gradlePluginPortal()
        ivy(url = file("./ivy-repo"))
    }
}
settings.gradle
pluginManagement {
    repositories {
        maven {
            url = file('./maven-repo')
        }
        gradlePluginPortal()
        ivy {
            url = file('./ivy-repo')
        }
    }
}

This tells Gradle to first look in the Maven repository at ../maven-repo when resolving plugins and then to check the Gradle Plugin Portal if the plugins are not found in the Maven repository. If you don’t want the Gradle Plugin Portal to be searched, omit the gradlePluginPortal() line. Finally, the Ivy repository at ../ivy-repo will be checked.

Plugin Version Management

A plugins{} block inside pluginManagement{} allows all plugin versions for the build to be defined in a single location. Plugins can then be applied by id to any build script via the plugins{} block.

One benefit of setting plugin versions this way is that the pluginManagement.plugins{} does not have the same constrained syntax as the build script plugins{} block. This allows plugin versions to be taken from gradle.properties, or loaded via another mechanism.

Managing plugin versions via pluginManagement:

kotlin/settings.gradle.kts
pluginManagement {
  val helloPluginVersion = providers.gradleProperty("helloPluginVersion").get()
  plugins {
    id("com.example.hello") version "${helloPluginVersion}"
  }
}
kotlin/build.gradle.kts
plugins {
    id("com.example.hello")
}
common/gradle.properties
helloPluginVersion=1.0.0
groovy/settings.gradle
pluginManagement {
  plugins {
        id 'com.example.hello' version "${helloPluginVersion}"
    }
}
groovy/build.gradle
plugins {
    id 'com.example.hello'
}
common/gradle.properties
helloPluginVersion=1.0.0

The plugin version is loaded from gradle.properties and configured in the settings script, allowing the plugin to be added to any project without specifying the version.

Plugin Resolution Rules

Plugin resolution rules allow you to modify plugin requests made in plugins{} blocks, e.g., changing the requested version or explicitly specifying the implementation artifact coordinates.

To add resolution rules, use the resolutionStrategy{} inside the pluginManagement{} block:

settings.gradle.kts
pluginManagement {
    resolutionStrategy {
        eachPlugin {
            if (requested.id.namespace == "com.example") {
                useModule("com.example:sample-plugins:1.0.0")
            }
        }
    }
    repositories {
        maven {
            url = uri("./maven-repo")
        }
        gradlePluginPortal()
        ivy {
            url = uri("./ivy-repo")
        }
    }
}
settings.gradle
pluginManagement {
    resolutionStrategy {
        eachPlugin {
            if (requested.id.namespace == 'com.example') {
                useModule('com.example:sample-plugins:1.0.0')
            }
        }
    }
    repositories {
        maven {
            url = file('./maven-repo')
        }
        gradlePluginPortal()
        ivy {
            url = file('./ivy-repo')
        }
    }
}

This tells Gradle to use the specified plugin implementation artifact instead of its built-in default mapping from plugin ID to Maven/Ivy coordinates.

Custom Maven and Ivy plugin repositories must contain plugin marker artifacts and the artifacts that implement the plugin. Read Gradle Plugin Development Plugin for more information on publishing plugins to custom repositories.

See PluginManagementSpec for complete documentation for using the pluginManagement{} block.

Plugin Marker Artifacts

Since the plugins{} DSL block only allows for declaring plugins by their globally unique plugin id and version properties, Gradle needs a way to look up the coordinates of the plugin implementation artifact.

To do so, Gradle will look for a Plugin Marker Artifact with the coordinates plugin.id:plugin.id.gradle.plugin:plugin.version. This marker needs to have a dependency on the actual plugin implementation. Publishing these markers is automated by the java-gradle-plugin.

For example, the following complete sample from the sample-plugins project shows how to publish a com.example.hello plugin and a com.example.goodbye plugin to both an Ivy and Maven repository using the combination of the java-gradle-plugin, the maven-publish plugin, and the ivy-publish plugin.

build.gradle.kts
plugins {
    `java-gradle-plugin`
    `maven-publish`
    `ivy-publish`
}

group = "com.example"
version = "1.0.0"

gradlePlugin {
    plugins {
        create("hello") {
            id = "com.example.hello"
            implementationClass = "com.example.hello.HelloPlugin"
        }
        create("goodbye") {
            id = "com.example.goodbye"
            implementationClass = "com.example.goodbye.GoodbyePlugin"
        }
    }
}

publishing {
    repositories {
        maven {
            url = uri(layout.buildDirectory.dir("maven-repo"))
        }
        ivy {
            url = uri(layout.buildDirectory.dir("ivy-repo"))
        }
    }
}
build.gradle
plugins {
    id 'java-gradle-plugin'
    id 'maven-publish'
    id 'ivy-publish'
}

group = 'com.example'
version = '1.0.0'

gradlePlugin {
    plugins {
        hello {
            id = 'com.example.hello'
            implementationClass = 'com.example.hello.HelloPlugin'
        }
        goodbye {
            id = 'com.example.goodbye'
            implementationClass = 'com.example.goodbye.GoodbyePlugin'
        }
    }
}

publishing {
    repositories {
        maven {
            url = layout.buildDirectory.dir('maven-repo')
        }
        ivy {
            url = layout.buildDirectory.dir('ivy-repo')
        }
    }
}

Running gradle publish in the sample directory creates the following Maven repository layout (the Ivy layout is similar):

plugin markers

Legacy Plugin Application

With the introduction of the plugins DSL, users should have little reason to use the legacy method of applying plugins. It is documented here in case a build author cannot use the plugin DSL due to restrictions in how it currently works.

build.gradle.kts
apply(plugin = "java")
build.gradle
apply plugin: 'java'

Plugins can be applied using a plugin id. In the above case, we are using the short name "java" to apply the JavaPlugin.

Rather than using a plugin id, plugins can also be applied by simply specifying the class of the plugin:

build.gradle.kts
apply<JavaPlugin>()
build.gradle
apply plugin: JavaPlugin

The JavaPlugin symbol in the above sample refers to the JavaPlugin. This class does not strictly need to be imported as the org.gradle.api.plugins package is automatically imported in all build scripts (see Default imports).

Furthermore, one needs to append the ::class suffix to identify a class literal in Kotlin instead of .class in Java.

Furthermore, it is unnecessary to append .class to identify a class literal in Groovy as it is in Java.

You may also see the apply method used to include an entire build file:

build.gradle.kts
apply(from = "other.gradle.kts")
build.gradle
apply from: 'other.gradle'

Using a Version Catalog

When a project uses a version catalog, plugins can be referenced via aliases when applied.

Let’s take a look at a simple Version Catalog:

libs.versions.toml
[versions]
groovy = "3.0.5"
checkstyle = "8.37"

[libraries]
groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" }
groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = { strictly = "[3.8, 4.0[", prefer = "3.9" } }

[bundles]
groovy = ["groovy-core", "groovy-json", "groovy-nio"]

[plugins]
versions = { id = "com.github.ben-manes.versions", version = "0.45.0" }

Then a plugin can be applied to any build script using the alias method:

build.gradle.kts
plugins {
    `java-library`
    alias(libs.plugins.versions)
}
build.gradle
plugins {
    id 'java-library'
    alias(libs.plugins.versions)
}
Tip
Gradle generates type safe accessors for catalog items.

Ready to build something? Start with the Intermediate Tutorial.

Next Step: Start the Tutorial >>

Plugin Introduction

A Gradle plugin is an add-on that enhances your build by adding tasks, applying conventions, or enabling specific configurations.

Plugins lets you extend and organize build logic in a modular and reusable way.

You apply plugins using the plugins block:

build.gradle.kts
plugins {
    id("org.springframework.boot") version "3.3.1"
}
build.gradle
plugins {
    id 'org.springframework.boot' version '3.3.1'
}

When you apply a plugin, it typically does one or both of the following:

  1. Adds tasks to your build - Plugins often expose tasks that perform a specific action.

    For example, the Maven Publish Plugin adds a publishToMavenLocal task, which uploads artifacts to a local Maven repository.

  2. Applies behavior and conventions - Some plugins automatically configure parts of your build when applied.

    For example, the Java Plugin creates the main and test source sets and applies sensible defaults for compiling and testing Java code.

Most plugins provide DSL blocks you can use to fine-tune their behavior in build scripts. For instance, the Java plugin lets you configure how tests are run using the test task:

build.gradle.kts
tasks.withType<Test>().configureEach {
    useJUnitPlatform {
        includeEngines("junit-vintage")
        // excludeEngines("junit-jupiter")
    }
}
build.gradle
tasks.withType(Test).configureEach {
    useJUnitPlatform {
        includeEngines 'junit-vintage'
        // excludeEngines 'junit-jupiter'
    }
}

And Java options using the java {} block:

build.gradle.kts
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}
build.gradle
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

Plugin Types

Gradle supports three types of plugins, each applied at a different phase of the build lifecycle. Init, settings, and project plugins.

The most common and popular plugins are project plugins.

plugin intro advanced 1
1. Init Plugins
  • Applied globally to all builds. Loaded from ~/.gradle/init.gradle[.kts] or passed via the --init-script command-line option.

  • Configure the Gradle runtime before the build even starts. Useful for enforcing corporate policies, setting up shared CI configuration, or applying global conventions.

  • Implement the Plugin<Gradle> interface.

2. Settings Plugins
  • Applied in settings.gradle[.kts] files.

  • Configure the build layout, such as which projects are included or how plugins are resolved.

  • Implement the Plugin<Settings> interface.

3. Project Plugins
  • Applied in build.gradle[.kts] files.

  • Configure tasks, dependencies, and project-specific behavior.

  • Implement the Plugin<Project> interface.

Plugin Scope

The scope of a plugin is determined by the interface it implements. The scope determines where the plugin can be applied and what parts of the Gradle lifecycle it can modify. Each scope gives you access to different configuration APIs.

plugin intro advanced 2

Each plugin type is defined by the interface it implements and the stage of the build lifecycle where it applies:

Plugin Type Interface Scope Applied In

Init Plugin

Plugin<Gradle>

Global

init.gradle[.kts] or --init-script

Settings Plugin

Plugin<Settings>

Build Layout

settings.gradle[.kts]

Project Plugin

Plugin<Project>

Per-Project

build.gradle[.kts]

Plugin Sources

Gradle plugins come from three different places, the Gradle distribution itself, external community sources, or your own internal builds.

1. Core Plugins
  • Bundled with Gradle itself (e.g., java, application).

  • You don’t author or publish them, they’re built-in.

2. Community Plugins
  • Created and published by the community.

  • Typically hosted on the Gradle Plugin Portal.

  • Distributed as binary JARs.

3. Local Plugins
  • Custom plugins you write yourself, for your own builds.

  • Useful for sharing logic across subprojects or internal builds.

Authoring Plugins

Gradle provides two main ways to create your own custom plugins. This is code you write to extend Gradle’s capabilities and modularize your build logic.

plugin intro advanced 3
1. Precompiled Script Plugins
  • Written as .gradle.kts or .gradle files.

  • Compiled automatically during the build.

  • Ideal for bundling simple conventions.

2. Binary Plugins
  • Typically written in Java, Kotlin, or Groovy.

  • Packaged as JARs and reusable across multiple builds.

  • Can be published to repositories for sharing.

Pre-compiled Script Plugins

The simplest plugin you can develop is called a precompiled script plugins.

These are Kotlin (.kts) or Groovy (.gradle) scripts. They behave like normal Gradle plugins, but you write them using the same syntax as regular build scripts.

Some benefits of precompiled script plugins:

  • Encapsulation: Keep your build.gradle(.kts) files clean by moving reusable logic into named plugins.

  • Tooling support: Full IDE support, like autocomplete and navigation.

  • Avoid boilerplate: No need to write full plugin classes or register plugins manually.

  • Faster adoption: Use familiar script syntax while reaping the benefits of structured plugin development.

Convention Plugins

Most precompiled script plugins are used as convention plugins. A Gradle term for reusable build logic that applies standard plugin configurations, defaults, and behaviors across many projects.

Convention plugins are especially useful in large or multi-project builds where consistency is important.

They typically look like this:

buildSrc/src/main/kotlin/java-library-convention.gradle.kts
plugins {
    `java-library`
    checkstyle
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

checkstyle {
    maxWarnings = 0
    // ...
}

tasks.withType<JavaCompile> {
    options.isWarnings = true
    // ...
}

dependencies {
    testImplementation("junit:junit:4.13")
    // ...
}
buildSrc/src/main/groovy/java-library-convention.gradle
plugins {
    id 'java-library'
    id 'checkstyle'
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

checkstyle {
    maxWarnings = 0
    // ...
}

tasks.withType(JavaCompile) {
    options.warnings = true
    // ...
}

dependencies {
    testImplementation("junit:junit:4.13")
    // ...
}

And are applied in other projects like this:

build.gradle.kts
plugins {
    `java-library-convention`
}
build.gradle
plugins {
    id 'java-library-convention'
}

Structuring Precompiled Script Plugins

Precompiled script plugins are typically placed in either:

  1. A dedicated buildSrc directory in your build, or

  2. A separate included build (often named build-logic).

1. Using buildSrc

Precompiled script plugins or convention plugins will often be found in the special buildSrc directory:

.
└── buildSrc
    ├── build.gradle.kts
    └── src
       └── main
          └── kotlin
             └── myproject.java-conventions.gradle.kts
.
└── buildSrc
    ├── build.gradle
    └── src
       └── main
          └── groovy
             └── myproject.java-conventions.gradle
2. Using a Composite Build

Precompiled script plugins or convention plugins will often be found in a Composite Build with a name similar to build-logic:

build-logic
├── settings.gradle.kts
├── build.gradle.kts
└── src
   └── main
      └── kotlin
         └── myproject.java-conventions.gradle.kts
build-logic
├── settings.gradle
├── build.gradle
└── src
   └── main
      └── groovy
         └── myproject.java-conventions.gradle

Applying a Precompiled Script Plugin

You apply a precompiled script plugin using its ID, derived from the script filename (excluding the .gradle.kts or .gradle extension).

plugins {
    id("myproject.java-conventions")
}
plugins {
    id("myproject.java-conventions")
}

The script itself acts as the plugin. There is no need to explicitly implement the Plugin interface; a requirement for binary plugins.

Publishing a Precompiled Script Plugin

You can publish a precompiled script plugin to a repository, either a public one such as the Gradle Plugin Portal, or a private artifact repository like Maven, Artifactory, or Nexus.

However, publishing precompiled script plugins is not recommended (they are meant for internal use only). To publish them, you should first convert the precompiled script plugin into a binary plugin.

Example of a Precompiled Script Plugin

Let’s walk through creating a simple convention plugin using a precompiled script plugin in buildSrc.

1. Create a buildSrc directory

In the root of your project, add a buildSrc directory:

.   // root project
├── ...     // other project dirs and files
└── buildSrc
    ├── build.gradle.kts
    └── src
       └── main
          └── kotlin
.   // root project
├── ...     // other project dirs and files
└── buildSrc
    ├── build.gradle
    └── src
       └── main
          └── groovy
2. Add a build file

Create buildSrc/build.gradle.kts with the supporting build logic your convention plugin will need to compile:

buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    gradlePluginPortal()
}
buildSrc/build.gradle
plugins {
    id 'groovy-gradle-plugin'
}

repositories {
    gradlePluginPortal()
}
3. Create a plugin script

Inside buildSrc/src/main/kotlin or buildSrc/src/main/groovy, write your convention plugin as a .gradle(.kts) file:

.
└── buildSrc
    ├── build.gradle.kts
    └── src
       └── main
          └── kotlin
             └── myproject.java-conventions.gradle.kts

In this example, the myproject.java-conventions.gradle(.kts) file contains the following code:

buildSrc/src/main/kotlin/myproject.java-conventions.gradle.kts
plugins {
    id("java")
}

// Disable the test report for the individual test task
tasks.named<Test>("test") {
    reports.html.required = false
}

// Share the test report data to be aggregated for the whole project
configurations.create("binaryTestResultsElements") {
    isCanBeResolved = false
    isCanBeConsumed = true
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("test-report-data"))
    }
    outgoing.artifact(tasks.test.map { task -> task.getBinaryResultsDirectory().get() })
}

repositories {
    mavenCentral()
}
buildSrc/src/main/groovy/myproject.java-conventions.gradle
plugins {
    id 'java'
}

// Disable the test report for the individual test task
test {
    reports.html.required = false
}

// Share the test report data to be aggregated for the whole project
configurations {
    binaryTestResultsElements {
        canBeResolved = false
        canBeConsumed = true
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
            attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, 'test-report-data'))
        }
        outgoing.artifact(test.binaryResultsDirectory)
    }
}

repositories {
    mavenCentral()
}

The name of the script (myproject.java-conventions.gradle(.kts)) becomes the plugin ID: myproject.java-conventions.

4. Apply the plugin in your project

In the build.gradle(.kts) of a consuming project, apply the plugin:

core/build.gradle.kts
plugins {
    id("myproject.java-conventions")
}

dependencies {
    testImplementation("junit:junit:4.13")
}
core/build.gradle
plugins {
    id 'myproject.java-conventions'
}

dependencies {
    testImplementation 'junit:junit:4.13'
}

Binary Plugin

Creating your own custom plugin might be a great solution when Gradle doesn’t offer the specific capabilities your project needs. This is where binary plugins come in.

A binary plugin is a plugin that is implemented in a compiled language and is packaged as a JAR file. Binary plugins must implement the Plugin interface.

Implementing the Plugin Interface

For example, this is a very simple "Hello World" plugin:

build.gradle.kts
abstract class SamplePlugin : Plugin<Project> { // (1)
    override fun apply(project: Project) {  // (2)
        project.tasks.register("ScriptPlugin") {
            doLast {
                println("Hello world from the build file!")
            }
        }
    }
}

apply<SamplePlugin>() // (3)
build.gradle
class SamplePlugin implements Plugin<Project> { // (1)
    void apply(Project project) {   // (2)
        project.tasks.register("ScriptPlugin") {
            doLast {
                println("Hello world from the build file!")
            }
        }
    }
}

apply plugin: SamplePlugin // (3)
  1. Extend the org.gradle.api.Plugin interface.

  2. Override the apply method.

1. Extend the org.gradle.api.Plugin interface

Create a class that extends the Plugin interface:

build.gradle.kts
abstract class SamplePlugin : Plugin<Project> {
}
build.gradle
class SamplePlugin implements Plugin<Project> {
}
2. Override the apply method

Add tasks and other logic in the apply() method:

build.gradle.kts
override fun apply() {
}
build.gradle
void apply(Project project) {
}

Let’s take a look at a more realistic example next.

Binary Plugin Development

When creating a plugin in Gradle, you will most likely offer tasks users can run and DSL blocks that users can use to configure your plugin.

Let’s create a binary plugin that offers both.

When you write a plugin in Gradle, if you want to share it between multiple projects then the best option is to create the plugin in a separate repository. This way, you can publish it to a private or public Maven repository, and then apply it in whatever project you need.

Components of a Plugin

First, let’s review the three components of a plugin:

plugin 1
Component Role Description

Plugin Class

Entry point

Defines what happens when the plugin is applied. Typically, this involves registering tasks or modifying the project configuration.

Extension Class

Configuration

Holds configuration data provided by users via the build script.

Task Class

Behavior

Implements logic that is executed when the task is run. The plugin will typically register a task and wire it up with values from the extension.

Our simple plugin compares the size of two files. It is called FileSizeDiff. Here’s the suggested directory structure:

.
└── plugin
    ├── settings.gradle.kts
    ├── build.gradle.kts
    └── src
       └── main
           └── java/org/example
               ├── FileSizeDiffTask.java
               ├── FileSizeDiffPlugin.java
               └── FileSizeDiffExtension.java
.
└── plugin
    ├── settings.gradle
    ├── build.gradle
    └── src
       └── main
           └── java/org/example
               ├── FileSizeDiffTask.java
               ├── FileSizeDiffPlugin.java
               └── FileSizeDiffExtension.java

This build.gradle(.kts) for this filesizediff plugin looks as follows:

plugin/build.gradle.kts
plugins {
    `java-gradle-plugin`    // (1)
}

group = "org.example"   // (3)
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {  // (2)
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

gradlePlugin {  // (3)
    plugins {
        create("filesizediff") {
            id = "org.example.filesizediff"
            implementationClass = "org.example.FileSizeDiffPlugin"
        }
    }
}
plugin/build.gradle
plugins {
    id('java-gradle-plugin')    // (1)
}

group = "org.example"   // (3)
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {  // (2)
    testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
    testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
    testRuntimeOnly('org.junit.platform:junit-platform-launcher')
}

gradlePlugin {  // (3)
    plugins {
        filesizediff {
            id = 'org.example.filesizediff'
            implementationClass = 'org.example.FileSizeDiffPlugin'
        }
    }
}
  1. java-gradle-plugin - Sets up various configurations you need to do plugin development in Java

  2. junit testing framework - A popular Java testing framework

  3. Plugin configuration - Sets an id of org.example.filesizediff which we can use to reference the plugin

1. Extension

The extension defines the plugin’s configurable inputs, two file properties in this case:

src/main/java/org/example/FileSizeDiffExtension.java
package org.example;

import org.gradle.api.file.RegularFileProperty;

public interface FileSizeDiffExtension {

    RegularFileProperty getFile1();

    RegularFileProperty getFile2();
}

The two properties representing the input files are of the RegularFileProperty type which extends Property and are therefore lazy.

2. Task

The task does the bulk of the work:

src/main/java/org/example/FileSizeDiffTask.java
package org.example;

import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;

import java.nio.file.Files;
import java.io.File;
import java.io.IOException;

public abstract class FileSizeDiffTask extends DefaultTask {

    @InputFile
    public abstract RegularFileProperty getFile1();

    @InputFile
    public abstract RegularFileProperty getFile2();

    @OutputFile
    public abstract RegularFileProperty getResultFile();

    @TaskAction
    public void diff() throws IOException {
        File f1 = getFile1().getAsFile().get();
        File f2 = getFile2().getAsFile().get();

        String output;
        if (f1.length() == f2.length()) {
            output = "Files have the same size: " + f1.length() + " bytes";
        } else {
            String larger = f1.length() > f2.length() ? f1.getName() : f2.getName();
            long size = Math.max(f1.length(), f2.length());
            output = larger + " was larger: " + size + " bytes";
        }

        File result = getResultFile().get().getAsFile();
        Files.writeString(result.toPath(), output);

        System.out.println(output);
        System.out.println("Wrote diff result to " + result.getAbsolutePath());
    }
}

The task defines two input file properties and one output file property:

  • The input files represent the files to compare.

  • The output file is where the plugin writes the diff result, defaulting to build/diff-result.txt.

The @InputFile and @OutputFile annotations tell Gradle to track these properties for incremental builds and caching, so the task will only re-run if the inputs or outputs change.

The plugin is responsible for mapping the user-defined values from the extension to the task’s input properties.

The task logic is implemented in a method annotated with @TaskAction, which:

  • Compares the size of the two input files.

  • Generates a descriptive text result.

  • Writes that result to the output file and prints it to standard output.

3. Plugin

This class wires up the extension and task when the plugin is applied:

src/main/java/org/example/FileSizeDiffPlugin.java
package org.example;

import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.tasks.TaskProvider;

public class FileSizeDiffPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        // Register the extension
        FileSizeDiffExtension extension = project.getExtensions().create("diff", FileSizeDiffExtension.class);

        // Register and configure the task
        project.getTasks().register("fileSizeDiff", FileSizeDiffTask.class, task -> {
            task.getFile1().convention(extension.getFile1());
            task.getFile2().convention(extension.getFile2());
            task.getResultFile().convention(project.getLayout().getBuildDirectory().file("diff-result.txt"));
        });
    }
}

In the plugin class, we override the apply method, which is invoked when the plugin is applied to a project via build.gradle(.kts).

Within this method, the plugin does two things:

  1. Creates an extension named filesizediff: This allows users to configure the plugin using a diff {} block in their build script. The configuration values are stored in an instance of FileSizeDiffExtension.

  2. Registers a task of type FileSizeDiffTask: The task is given the name fileSizeDiff, and its properties (file1 and file2) are mapped from the corresponding properties in the diff extension. This ensures the task uses the values provided by the user in their build script.

This enables the following usage in a build.gradle(.kts) file:

build.gradle.kts
plugins {
    id("org.example.filesizediff")
}

diff {
    file1 = file("a.txt")
    file2 = file("b.txt")
}
build.gradle
plugins {
    id("org.example.filesizediff")
}

diff {
    file1 = file('a.txt')
    file2 = file('b.txt')
}

Binary Plugin Testing

Gradle provides a robust mechanism for testing binary plugins.

Gradle’s TestKit allows you to programmatically execute synthetic builds that use the plugin under development, all within the plugin’s own build.

A well-tested plugin typically includes two levels of testing:

  1. Unit Tests

  2. Functional Tests

The directory of our plugin looks as follows:

.
└── plugin
    ├── settings.gradle.kts
    ├── build.gradle.kts
    └── src
       ├── main
       │   └── java/org/example
       │       ├── FileSizeDiffTask.java
       │       ├── FileSizeDiffPlugin.java
       │       └── FileSizeDiffExtension.java
       ├── test
       │   └── java/org/example
       │       └── FileSizeDiffPluginTest.java
       └── functionalTest
           └── java/org/example
               └── FileSizeDiffPluginFunctionalTest.java
.
└── plugin
    ├── settings.gradle
    ├── build.gradle
    └── src
       ├── main
       │   └── java/org/example
       │       ├── FileSizeDiffTask.java
       │       ├── FileSizeDiffPlugin.java
       │       └── FileSizeDiffExtension.java
       ├── test
       │   └── java/org/example
       │       └── FileSizeDiffPluginTest.java
       └── functionalTest
           └── java/org/example
               └── FileSizeDiffPluginFunctionalTest.java

1. Unit Test

Unit tests validate the internal behavior of your plugin in a lightweight, isolated project. These tests simulate applying your plugin without needing a full Gradle execution.

They test:

  • The plugin applies without errors

  • Tasks or extensions are registered correctly

The unit test for the filesizediff plugin looks as follows:

src/test/java/org/example/FileSizeDiffPluginTest
package org.example;

import org.gradle.testfixtures.ProjectBuilder;
import org.gradle.api.Project;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class FileSizeDiffPluginTest {
    @Test void pluginRegistersATask() {
        // Create a test project and apply the plugin
        Project project = ProjectBuilder.builder().build();
        project.getPlugins().apply("org.example.filesizediff");

        // Verify the result
        assertNotNull(project.getTasks().findByName("fileSizeDiff"));
    }
}

This test checks that the org.example.filesizediff plugin is applied and a fileSizeDiff task is added.

Unit tests are fast and useful for verifying the basic mechanics of your plugin logic.

2. Functional Test

Functional tests (also known as end-to-end plugin tests) use Gradle’s TestKit to launch real Gradle builds in a temporary directory. This is how you verify your plugin works correctly in real-world usage.

They test:

  • The plugin can be applied to a real build

  • The plugin task runs correctly and produces the expected output

  • Users can configure the plugin via the DSL

The functional test for the filesizediff plugin looks as follows:

src/functionalTest/java/org/example/FileSizeDiffPluginFunctionalTest.java
package org.example;

import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class FileSizeDiffPluginFunctionalTest {
    // Temporary directory for each test, automatically cleaned up after the test run
    @TempDir
    File projectDir;

    // Helper to get reference to build.gradle in the temp project
    private File getBuildFile() {
        return new File(projectDir, "build.gradle");
    }

    // Helper to get reference to settings.gradle in the temp project
    private File getSettingsFile() {
        return new File(projectDir, "settings.gradle");
    }

    // Create minimal build and settings files before each test
    @BeforeEach
    void setup() throws IOException {
        // Empty settings.gradle
        writeString(getSettingsFile(), "");

        // Apply the plugin and configure the extension in build.gradle
        writeString(getBuildFile(), """
            plugins {
                id("org.example.filesizediff")
            }
            diff {
                file1 = file("a.txt")
                file2 = file("b.txt")
            }
        """
        );
    }

    // Test case: both input files have the same size (empty)
    @Test
    void canDiffTwoFilesOfTheSameSize() throws IOException {
        // Create empty file a.txt
        writeString(new File(projectDir, "a.txt"), "");
        // Create empty file b.txt
        writeString(new File(projectDir, "b.txt"), "");

        // Run the build with the plugin classpath and invoke the fileSizeDiff task
        BuildResult result = GradleRunner.create()
                .withProjectDir(projectDir)
                .withPluginClasspath()
                .withArguments("fileSizeDiff")
                .build();

        // Verify the output message and successful task result
        assertTrue(result.getOutput().contains("Files have the same size"));
        assertEquals(TaskOutcome.SUCCESS, result.task(":fileSizeDiff").getOutcome());
    }

    // Test case: first file is larger than second file
    @Test
    void canDiffTwoFilesOfDiffSize() throws IOException {
        // File a.txt has 7 bytes
        writeString(new File(projectDir, "a.txt"), "dsdsdad");
        // File b.txt is empty
        writeString(new File(projectDir, "b.txt"), "");

        // Run the build and invoke the plugin task
        BuildResult result = GradleRunner.create()
                .withProjectDir(projectDir)
                .withPluginClasspath()
                .withArguments("fileSizeDiff")
                .build();

        // Verify the output message indicates a.txt is larger
        assertTrue(result.getOutput().contains("a.txt was larger: 7 bytes"));
        assertEquals(TaskOutcome.SUCCESS, result.task(":fileSizeDiff").getOutcome());
    }

    // Helper method to write string content to a file
    private static void writeString(File file, String string) throws IOException {
        Files.writeString(file.toPath(), string);
    }
}

GradleRunner is the core API provided by TestKit for executing builds in a test environment. GradleRunner simulates a Gradle invocation. Each test creates a temporary project with a build.gradle file, input files, and runs the plugin task using GradleRunner.

Unlike unit tests in src/test/java that test individual classes, functional tests live in src/functionalTest/java and verify that your plugin behaves correctly in a real build. Gradle doesn’t automatically recognize custom test source sets, so you need to declare a functionalTest source set and configure a task to run it.

Fortunately, gradle init can scaffold this setup for you:

plugin/build.gradle.kts
plugins {
    `java-gradle-plugin`    // (1)
}

group = "org.example"   // (3)
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {  // (2)
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

gradlePlugin {  // (3)
    plugins {
        create("filesizediff") {
            id = "org.example.filesizediff"
            implementationClass = "org.example.FileSizeDiffPlugin"
        }
    }
}

// Created by gradle init

// Add a source set for the functional test suite
val functionalTestSourceSet = sourceSets.create("functionalTest") {
}

configurations["functionalTestImplementation"].extendsFrom(configurations["testImplementation"])
configurations["functionalTestRuntimeOnly"].extendsFrom(configurations["testRuntimeOnly"])

// Add a task to run the functional tests
val functionalTest = tasks.register<Test>("functionalTest") {
    description = "Runs functional tests."
    group = "verification"
    testClassesDirs = functionalTestSourceSet.output.classesDirs
    classpath = functionalTestSourceSet.runtimeClasspath
    useJUnitPlatform()
}

gradlePlugin.testSourceSets.add(functionalTestSourceSet)

tasks.named<Task>("check") {
    // Run the functional tests as part of `check`
    dependsOn(functionalTest)
}

tasks.named<Test>("test") {
    // Use JUnit Jupiter for unit tests.
    useJUnitPlatform()
}
plugin/build.gradle
plugins {
    id('java-gradle-plugin')    // (1)
}

group = "org.example"   // (3)
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {  // (2)
    testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
    testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
    testRuntimeOnly('org.junit.platform:junit-platform-launcher')
}

gradlePlugin {  // (3)
    plugins {
        filesizediff {
            id = 'org.example.filesizediff'
            implementationClass = 'org.example.FileSizeDiffPlugin'
        }
    }
}

// Created by gradle init

// Add a source set for the functional test suite
sourceSets {
    functionalTest {
    }
}

configurations {
    functionalTestImplementation.extendsFrom(testImplementation)
    functionalTestRuntimeOnly.extendsFrom(testRuntimeOnly)
}

// Add a task to run the functional tests
tasks.register('functionalTest', Test) {
    description = 'Runs functional tests.'
    group = 'verification'
    testClassesDirs = sourceSets.functionalTest.output.classesDirs
    classpath = sourceSets.functionalTest.runtimeClasspath
    useJUnitPlatform()
}

// Include functional test source set in plugin validation
gradlePlugin.testSourceSets.add(sourceSets.functionalTest)

// Run functional tests as part of check
tasks.named('check', Task) {
    dependsOn tasks.named('functionalTest')
}

// Use JUnit Platform for unit tests
tasks.named('test', Test) {
    useJUnitPlatform()
}

Consumer Project

You can optionally create a consumer project that uses your plugin:

.
├── settings.gradle.kts // Include custom plugin
├── build.gradle.kts    // Applies custom plugin
│
└── plugin
    ├── settings.gradle.kts
    ├── build.gradle.kts
    └── src
       ...
.
├── settings.gradle // Include custom plugin
├── build.gradle    // Applies custom plugin
│
└── plugin
    ├── settings.gradle
    ├── build.gradle
    └── src
       ...

First you can point to the plugin as an included build in the consumer settings file:

settings.gradle.kts
rootProject.name = "consumer"

includeBuild("plugin")
settings.gradle
rootProject.name = 'consumer'

includeBuild("plugin")

Then in the build file of the consumer project, you apply the plugin and test it:

build.gradle.kts
plugins {
    id("org.example.filesizediff")
}

diff {
    file1 = file("a.txt")
    file2 = file("b.txt")
}
build.gradle
plugins {
    id("org.example.filesizediff")
}

diff {
    file1 = file('a.txt')
    file2 = file('b.txt')
}

In the consumer project, you can create dummy a.txt and b.txt files and run ./gradlew fileSizeDiff:

$ ./gradlew fileSizeDiff
> Task :fileSizeDiff
Files have the same size: 0 bytes
Wrote diff result to /home/user/gradle/samples/build/diff-result.txt

BUILD SUCCESSFUL in 0s

Binary Plugin Publishing

Once your plugin is complete and tested, you may want to publish it so it can be reused across multiple builds or even shared with others.

There are a number of plugins available to publish your plugin. For now, we will use the core maven-publish plugin. The maven-publish plugin lets you publish artifacts (like JARs, libraries, or plugins) to a Maven repository.

Apply the Maven Publish Plugin

In your plugin/build.gradle(.kts), apply the maven-publish plugin:

plugins {
  `java-gradle-plugin`
  `maven-publish`
}

A lot of the information needed to publish the plugin - group, version, and id - is already in your build file:

plugin/build.gradle.kts
group = "org.example"   // (3)
version = "1.0.0"
gradlePlugin {  // (3)
    plugins {
        create("filesizediff") {
            id = "org.example.filesizediff"
            implementationClass = "org.example.FileSizeDiffPlugin"
        }
    }
}
plugin/build.gradle
group = "org.example"   // (3)
version = "1.0.0"
gradlePlugin {  // (3)
    plugins {
        filesizediff {
            id = 'org.example.filesizediff'
            implementationClass = 'org.example.FileSizeDiffPlugin'
        }
    }
}

group = "org.example" sets the group ID of the plugin.

version = "1.0.0" sets the version of the plugin artifact.

The gradlePlugin {} block is part of the java-gradle-plugin and declares a binary plugin. It registers the full plugin name org.example.filesizediff, backed by a class called FileDiffPlugin. This is the class that implements Plugin<Project> and defines what happens when the plugin is applied.

Configure the Publishing Block

In your plugin/build.gradle(.kts) file, configure the plugin for publication by defining where to publish it. Inside the publishing {} block, you specify the repository to publish to:

build.gradle.kts
publishing {
    repositories {
        maven {
            url = uri("${layout.projectDirectory}/publish")
        }
    }
}
build.gradle
publishing {
    repositories {
        maven {
            url = uri("${layout.projectDirectory}/publish")
        }
    }
}

Publish the Plugin

To publish, run:

$ ./gradlew publish

This will generate and install the plugin JAR and associated metadata (like pom.xml) into the specified Maven repository.

Ready to build something? Start with the Advanced Tutorial.

Next Step: Start the Tutorial >>

BEST PRACTICES

Best Practices for Authoring Gradle Builds

Introduction

Gradle is a powerful and flexible tool for building software, but along with these benefits comes complexity and the need for clear guidance on best practices. There are many scenarios where how to best use Gradle is unclear, as Gradle provides several ways of accomplishing the same goals. Often there is a need to determine if a current solution is likely to cause problems down the line, or if problems could have been avoided in advance by using Gradle differently. This guide aims to help Gradle authors navigate these scenarios and make informed decisions about how to use Gradle effectively.

What this Guide is

This section outlines best practices for authoring Gradle builds that are maintainable, function properly, and are aligned with Gradle’s core design philosophy. Some of these practices are things to do, and some are things to avoid doing.

This guide is informed by real-world experience in some of the largest software ecosystems using Gradle. It targets all levels of Gradle expertise, from beginners to advanced users. Not every practice here will be applicable to every build, however these are meant to be widely applicable and beneficial to many different usage scenarios.

This guide is not meant as instructions for how to use Gradle. For that purpose, we provide extensive tutorials and reference materials elsewhere in the Gradle documentation.

Principles

The items in this guide are meant to be brief, understandable, and actionable.

  • Build authors should be able to quickly recognize when they are following best practices and when they are not.

  • After reading, they should understand why a particular approach is beneficial or problematic and where to find further reference material.

  • Recommendations should be widely applicable to many or most typical builds and should provide specific, concrete advice.

Index

To view all Gradle Best Practices at a glance, along with the Gradle version they were introduced in, visit the Index Page.

Best Practices Index

The table below provides a complete list of documented Gradle Best Practices.

There are currently X Best Practices.

Use this as a quick reference to track newly added recommendations, check adoption status, or explore areas for improving your build:

Title Section Added in Gradle Version

Use Kotlin DSL

General

8.14

Use the Latest Minor Version of Gradle

General

8.14

Apply Plugins Using the plugins Block

General

8.14

Do Not Use Internal APIs

General

8.14

Set build flags in gradle.properties

General

9.0.0

Name Your Root Project

General

9.2.0

Do not use gradle.properties in subprojects

General

9.2.0

Avoid afterEvaluate

General

9.6.0

Consider use of @Incubating APIs carefully

General

9.7.0

Modularize Your Builds

Structuring Builds

9.0.0

Do Not Put Source Files in the Root Project

Structuring Builds

9.0.0

Favor build-logic Composite Builds for Build Logic

Structuring Builds

9.0.0

Avoid Unintentionally Creating Empty Projects

Structuring Builds

9.1.0

Use Convention Plugins

Structuring Builds

9.3.0

Single GAV String

Dependencies

8.14

Use Version Catalogs to Centralize Dependency Versions

Dependencies

9.0.0

Name Version Catalog Entries Appropriately

Dependencies

9.0.0

Set up your Dependency Repositories in the Settings file

Dependencies

9.0.0

Don’t Explicitly Depend on the Kotlin Standard Library

Dependencies

9.0.0

Avoid Redundant Dependency Declarations

Dependencies

9.0.0

Use Content Filtering with multiple Repositories

Dependencies

9.1.0

Apply Exclusions Narrowly

Dependencies

9.2.0

Always Declare Attributes on Consumable and Resolvable Configurations

Dependencies

9.7.0

Avoid DependsOn

Task

8.14

Favor @CacheableTask and @DisableCachingByDefault over cacheIf(Spec) and doNotCacheIf(String, Spec)

Task

8.14

Group and Describe custom Tasks

Task

9.0.0

Do not call get() on a Provider outside a Task action

Task

9.1.0

Don’t resolve Configurations before Task Execution

Task

9.1.0

Avoid using eager APIs on File Collections

Task

9.1.0

Prefer @PathSensitivity.NONE for file inputs and @PathSensitivity.RELATIVE for directories

Task

9.2.0

Use unique output files and directories

Task

9.3.0

Don’t hardcode task names when referring to them

Task

9.7.0

Don’t access a Project instance during Task Execution

Task

9.7.0

Wiring Task Outputs with map and flatMap

Task

9.7.0

Enable UTF‑8

Performance

9.0.0

Use the Build Cache

Performance

9.1.0

Use the Configuration Cache

Performance

9.1.0

Avoid Expensive Computations in Configuration Phase

Performance

9.0.0

Prefer the -bin Gradle Distribution

Performance

9.4.0

Validate the Gradle Distribution SHA-256 Checksum

Security

9.1.0

Validate the Gradle Wrapper on every Upgrade

Security

9.3.0

Do not Run ./gradlew on Untrusted Projects

Security

9.7.0

Build Output Should Be Byte-for-Byte Reproducible

Security

9.7.0

Test your custom Task and Plugins with TestKit

Testing

9.4.0

General Gradle Best Practices

Use Kotlin DSL

Prefer the Kotlin DSL (build.gradle.kts) over the Groovy DSL (build.gradle) when authoring new builds or creating new subprojects in existing builds.

Explanation

The Kotlin DSL offers several advantages over the Groovy DSL:

  • Strict typing: IDEs provide better auto-completion and navigation with the Kotlin DSL.

  • Improved readability: Code written in Kotlin is often easier to follow and understand.

  • Single-language stack: Projects that already use Kotlin for production and test code don’t need to introduce Groovy just for the build.

Since Gradle 8.0, Kotlin DSL is the default for new builds created with gradle init. Android Studio also defaults to Kotlin DSL.

Use the Latest Minor Version of Gradle

Stay on the latest minor version of the major Gradle release you’re using, and regularly update your plugins to the latest compatible versions.

Explanation

Gradle follows a fairly predictable, time-based release cadence. Only the latest minor version of the current and previous major release is actively supported.

We recommend the following strategy:

  • Try upgrading directly to the latest minor version of your current major Gradle release.

  • If that fails, upgrade one minor version at a time to isolate regressions or compatibility issues.

Each new minor version includes:

  • Performance and stability improvements.

  • Deprecation warnings that help you prepare for the next major release.

  • Fixes for known bugs and security vulnerabilities.

Use the wrapper task to update your project:

./gradlew :wrapper --gradle-version <version>

You can also install the latest Gradle versions easily using tools like SDKMAN! or Homebrew, depending on your platform.

Plugin Compatibility

Always use the latest compatible version of each plugin:

  • Upgrade Gradle before plugins.

  • Test plugin compatibility using shadow jobs.

  • Consult changelogs when updating.

Subscribe to the Gradle newsletter to stay informed about new Gradle releases, features, and plugins.

Apply Plugins Using the plugins Block

You should always use the plugins block to apply plugins in your build scripts.

Explanation

The plugins block is the preferred way to apply plugins in Gradle. The plugins API allows Gradle to better manage the loading of plugins and it is both more concise and less error-prone than adding dependencies to the buildscript’s classpath explicitly in order to use the apply method.

It allows Gradle to optimize the loading and reuse of plugin classes and helps inform tools about the potential properties and values in extensions the plugins will add to the build script. It is constrained to be idempotent (produce the same result every time) and side effect-free (safe for Gradle to execute at any time).

Example
Don’t Do This
build.gradle.kts
buildscript {
    repositories {
        gradlePluginPortal() // (1)
    }

    dependencies {
        classpath("com.google.protobuf:com.google.protobuf.gradle.plugin:0.9.4") // (2)
    }
}

apply(plugin = "java") // (3)
apply(plugin = "com.google.protobuf") // (4)
build.gradle
buildscript {
    repositories {
        gradlePluginPortal() // (1)
    }

    dependencies {
        classpath("com.google.protobuf:com.google.protobuf.gradle.plugin:0.9.4") // (2)
    }
}

apply plugin: "java" // (3)
apply plugin: "com.google.protobuf" // (4)
  1. Declare a Repository: To use the legacy plugin application syntax, you need to explicitly tell Gradle where to find a plugin.

  2. Declare a Plugin Dependency: To use the legacy plugin application syntax with third-party plugins, you need to explicitly tell Gradle the full coordinates of the plugin.

  3. Apply a Core Plugin: This is very similar using either method.

  4. Apply a Third-Party Plugin: The syntax is the same as for core Gradle plugins, but the version is not present at the point of application in your buildscript.

Do This Instead
build.gradle.kts
plugins {
    id("java") // (1)
    id("com.google.protobuf").version("0.9.4") // (2)
}
build.gradle
plugins {
    id("java") // (1)
    id("com.google.protobuf").version("0.9.4") // (2)
}
  1. Apply a Core Plugin: This is very similar using either method.

  2. Apply a Third-Party Plugin: You specify the version using method chaining in the plugins block itself.

Don’t Assume your Plugin is Applied after Another

Gradle’s plugin application is deterministic but opaque. It is difficult to reason about, especially across multiple build scripts, projects, convention plugins, or included builds.

As a result, you should not write build logic or plugins that depend on a specific plugin application order.

Explanation

In a single build.gradle(.kts) file, plugins appear to be applied sequentially:

build.gradle.kts
plugins {
    id("pluginA")
    id("pluginB")
}
build.gradle
plugins {
    id("pluginA")
    id("pluginB")
}

However, in multi-project builds, with multiple build.gradle(.kts) files or a convention plugin, plugin application can be hard to determine:

app/build.gradle.kts
plugins {
    id("pluginA")
    id("pluginB")
}
lib/build.gradle.kts
plugins {
    id("pluginC")
    id("pluginD")
}
app/build.gradle
plugins {
    id("pluginA")
    id("pluginB")
}
lib/build.gradle
plugins {
    id("pluginC")
    id("pluginD")
}

You cannot assume whether pluginA, pluginB, pluginC, or pluginD will be applied first because pluginA could apply pluginD.

Build Engineers

Writing build logic that assumes plugin ordering can lead to brittle behavior and fragile builds that break when project structure changes.

Don’t rely on blocks like allprojects {}, subprojects {}, or afterEvaluate {} that are highly dependent on project structure and file layout. They can be difficult to decipher and may depend on configuration details that are hard to understand completely without running the build.

Avoid build logic that assumes ordering between different projects, included builds, or applied scripts. While the application order is deterministic, minor structural changes (such as adding a new project or renaming an include) can easily result in an unexpected change to that plugin application order.

Plugin Developers

Users should be able to apply your plugin in either order and have it behave correctly:

build.gradle.kts
plugins {
  id("my-plugin")
  id("plugin-i-depend-on")
}
build.gradle
plugins {
  id("my-plugin")
  id("plugin-i-depend-on")
}

or

build.gradle.kts
plugins {
  id("plugin-i-depend-on")
  id("my-plugin")
}
build.gradle
plugins {
  id("plugin-i-depend-on")
  id("my-plugin")
}

If your plugin only works in one of these cases, it’s relying on plugin order and will be fragile in real builds.

If your plugin cannot function without another plugin, apply it explicitly at the start of Plugin.apply:

MyPlugin.kt
// Ensure required plugin is applied
project.pluginManager.apply("com.example.required-plugin")
MyPlugin.groovy
// Ensure required plugin is applied
project.pluginManager.apply('com.example.required-plugin')

If your plugin only needs to integrate with another plugin when it’s present, react to its application using pluginManager.withPlugin() or plugins.configureEach {}:

MyPlugin.kt
// Configure behavior that depends on required-plugin using the plugin id (preferred)
project.pluginManager.withPlugin("com.example.required-plugin") {  }

// Configure behavior that depends on RequiredPlugin using the plugin class (if no id is available)
project.plugins.configureEach { plugin ->
    when (plugin) { is com.example.RequiredPlugin -> {  } }
}
MyPlugin.groovy
// Configure behavior that depends on required-plugin using the plugin id (preferred)
project.pluginManager.withPlugin('com.example.required-plugin') {  }

// Configure behavior that depends on RequiredPlugin using the plugin class (if no id is available)
project.plugins.configureEach { plugin ->
    if (plugin instanceof com.example.RequiredPlugin) {  }
}

This is order-independent and safe.

Example

Given the following project layout:

.
├── app/
│   └── build.gradle.kts
├── buildSrc/
│   ├── build.gradle.kts
│   └── src/main/kotlin/MyPlugin.kt
├── settings.gradle.kts
└── build.gradle.kts
.
├── app/
│   └── build.gradle
├── buildSrc/
│   ├── build.gradle
│   └── src/main/groovy/MyPlugin.groovy
├── settings.gradle
└── build.gradle
Don’t Do This

This setup makes several assumptions about the java plugin.

In the root build, subprojects {} and afterEvaluate {} obscure when a plugin is applied and attempt to force ordering:

build.gradle.kts
subprojects {
    // Apply the Java plugin to every subproject
    afterEvaluate {
        // This runs after the app subproject’s build script is evaluated and results in an error
        pluginManager.apply("java")
    }
}
build.gradle
subprojects {
    // Apply the Java plugin to every subproject
    afterEvaluate {
        // This runs after the app subproject’s build script is evaluated and results in an error
        apply plugin: 'java'
    }
}

In the app subproject, the build file uses extensions.getByType(…​) which assumes java has already been applied:

app/build.gradle.kts
plugins {
    id("myplugin")
}
// Assumes 'java' plugin is present
extensions.getByType<org.gradle.api.plugins.JavaPluginExtension>().apply {
    toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}
app/build.gradle
plugins {
    id 'myplugin'
}
// Assumes 'java' plugin is present
project.extensions.getByType(org.gradle.api.plugins.JavaPluginExtension).with {
    toolchain.languageVersion.set(org.gradle.jvm.toolchain.JavaLanguageVersion.of(21))
}

In the plugin implementation, MyPlugin.kt or MyPlugin.groovy also assumes java is already applied:

buildSrc/src/main/kotlin/MyPlugin.kt
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Assumes 'java' plugin is present
        // WARNING: This will fail if the 'java' plugin hasn't been applied yet.
        project.extensions.getByType(JavaPluginExtension::class.java).toolchain {
            languageVersion.set(JavaLanguageVersion.of(21))
        }
    }
}
buildSrc/src/main/groovy/MyPlugin.groovy
class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
        // Assumes 'java' plugin is present
        // WARNING: This will fail if the 'java' plugin hasn't been applied yet.
        project.extensions.configure(JavaPluginExtension) {
            it.toolchain {
                it.languageVersion.set(JavaLanguageVersion.of(21))
            }
        }
    }
}
Do This Instead

The fixed version removes afterEvaluate and avoids assumptions about when or where the java plugin is applied.

The root build file has been deleted as there is no longer a need to use subprojects {}.

In the app subproject, the build file uses plugins.withPlugin("java") {} to safely configure tasks once java is applied:

app/build.gradle.kts
pluginManager.withPlugin("java") {
    extensions.configure<org.gradle.api.plugins.JavaPluginExtension> {
        toolchain.languageVersion.set(JavaLanguageVersion.of(21))
    }
}
app/build.gradle
project.pluginManager.withPlugin('java') {
    project.extensions.configure(org.gradle.api.plugins.JavaPluginExtension) {
        it.toolchain {
            languageVersion.set(JavaLanguageVersion.of(21))
        }
    }
}

In the plugin implementation, MyPlugin.kt or MyPlugin.groovy explicitly applies the java plugin:

buildSrc/src/main/kotlin/MyPlugin.kt
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // If your plugin requires 'java', apply it so order doesn’t matter
        project.pluginManager.apply("java")
        // Now it's safe to configure Java things immediately
        project.extensions.configure(JavaPluginExtension::class.java) {
            toolchain.languageVersion.set(JavaLanguageVersion.of(21))
        }
    }
}
buildSrc/src/main/groovy/MyPlugin.groovy
class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
        // If your plugin requires 'java', apply it so order doesn’t matter
        project.pluginManager.apply('java')
        // Now it's safe to configure Java things immediately
        project.extensions.configure(JavaPluginExtension) {
            it.toolchain {
                it.languageVersion.set(JavaLanguageVersion.of(21))
            }
        }
    }
}

Do Not Use Internal APIs

Do not use APIs from a package where any segment of the package is internal, or types that have Internal or Impl as a suffix in the name.

Explanation

Using internal APIs is inherently risky and can cause significant problems during upgrades. Gradle and many plugins (such as Android Gradle Plugin and Kotlin Gradle Plugin) treat these internal APIs as subject to unannounced breaking changes during any new Gradle release, even during minor releases. There have been numerous cases where even highly experienced plugin developers have been bitten by their usage of such APIs leading to unexpected breakages for their users.

If you require specific functionality that is missing, it’s best to submit a feature request. As a temporary workaround consider copying the necessary code into your own codebase and extending a Gradle public type with your own custom implementation using the copied code.

Example
Don’t Do This
build.gradle.kts
import org.gradle.api.internal.attributes.AttributeContainerInternal

configurations.create("bad") {
    attributes {
        attribute(Usage.USAGE_ATTRIBUTE, objects.named<Usage>(Usage.JAVA_RUNTIME))
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>(Category.LIBRARY))
    }
    val badMap = (attributes as AttributeContainerInternal).asMap() // (1)
    logger.warn("Bad map")
    badMap.forEach { (key, value) ->
        logger.warn("$key -> $value")
    }
}
build.gradle
import org.gradle.api.internal.attributes.AttributeContainerInternal

configurations.create("bad") {
    attributes {
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
    }
    def badMap = (attributes as AttributeContainerInternal).asMap() // (1)
    logger.warn("Bad map")
    badMap.each {
        logger.warn("${it.key} -> ${it.value}")
    }
}
  1. Casting to AttributeContainerInternal and using toMap() should be avoided as it relies on an internal API.

Do This Instead
build.gradle.kts
configurations.create("good") {
    attributes {
        attribute(Usage.USAGE_ATTRIBUTE, objects.named<Usage>(Usage.JAVA_RUNTIME))
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>(Category.LIBRARY))
    }
    val goodMap = attributes.keySet().associate { // (1)
        Attribute.of(it.name, it.type) to attributes.getAttribute(it)
    }
    logger.warn("Good map")
    goodMap.forEach { (key, value) ->
        logger.warn("$key -> $value")
    }
}
build.gradle
configurations.create("good") {
    attributes {
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
    }
    def goodMap = attributes.keySet().collectEntries {
        [Attribute.of(it.name, it.type), attributes.getAttribute(it as Attribute<Object>)]
    }
    logger.warn("Good map")
    goodMap.each {
        logger.warn("$it.key -> $it.value")
    }
}
  1. Implementing your own version of toMap() that only uses public APIs is a lot more robust.

Set Build Flags in gradle.properties

Set Gradle build property flags in the gradle.properties file.

Explanation

Instead of using command-line options or environment variables, set build flags in the root project’s gradle.properties file.

Gradle comes with a long list of Gradle properties, which have names that begin with org.gradle and can be used to configure the behavior of the build tool. These properties can have a major impact on build performance, so it’s important to understand how they work.

You should not rely on supplying these properties via the command-line for every Gradle invocation. Providing these properties via the command line is intended for short-term testing and debugging purposes, but it’s prone to being forgotten or inconsistently applied across environments. A permanent, idiomatic location to set and share these properties is in the gradle.properties file located in the root project directory. This file should be added to source control in order to share these properties across different machines and between developers.

You should understand the default values of the properties your build uses and avoid explicitly setting properties to those defaults. Any change to a property’s default value in Gradle will follow the standard deprecation cycle, and users will be properly notified.

Note
Properties set this way are not inherited across build boundaries when using composite builds.
Example
Don’t Do This
├── build.gradle.kts
└── settings.gradle.kts
├── build.gradle
└── settings.gradle
build.gradle.kts
tasks.register("first") {
    doLast {
        throw GradleException("First task failing as expected")
    }
}

tasks.register("second") {
    doLast {
        logger.lifecycle("Second task succeeding as expected")
    }
}

tasks.register("run") {
    dependsOn("first", "second")
}
build.gradle
tasks.register("first") {
    doLast {
        throw new GradleException("First task failing as expected")
    }
}

tasks.register("second") {
    doLast {
        logger.lifecycle("Second task succeeding as expected")
    }
}

tasks.register("run") {
    dependsOn("first", "second")
}

This build is run with gradle run -Dorg.gradle.continue=true, so that the failure of the first task does not prevent the second task from executing.

This relies on person running the build to remember to set this property, which is error prone and not portable across different machines and environments.

Do This Instead
├── build.gradle.kts
└── gradle.properties
└── settings.gradle.kts
├── build.gradle
└── gradle.properties
└── settings.gradle
gradle.properties
org.gradle.continue=true

This build sets the org.gradle.continue property in the gradle.properties file.

Now it can be executed using only gradle run, and the continue property will always be set automatically across all environments.

Name Your Root Project

Always name your root project in the settings.gradle(.kts) file.

Explanation

While an empty settings.gradle(.kts) file is enough to create a multi-project build, you should always set the rootProject.name property.

By default, the root project’s name is taken from the directory containing the build. This can be problematic if the directory name contains spaces, Gradle logical path separators, or other special characters. It also makes task paths dependent on the directory name, rather than being reliably defined.

Explicitly setting the root project’s name ensures consistency across environments. Project names appear in error messages, logs, and reports, and builds often run on different machines, such as CI servers. Builds may execute on a variety of machines or environments, such as CI servers, and should report the same root project name anywhere to make the project more comprehensible.

Example
Don’t Do This
settings.gradle.kts
// Left empty
settings.gradle
// Left empty

In this build, the settings file is empty and the root project has no explicit name. Running the projects report shows that Gradle assigns an implicit name to the root project, derived from the build’s current directory.

Unfortunately that name varies based on where the project currently lives. For example, if the project is checked out into a directory named some-directory-name, the output of ./gradlew projects will look like this:

> Task :projects

Projects:

------------------------------------------------------------
Root project 'some-directory-name'
------------------------------------------------------------
Do This Instead
settings.gradle.kts
rootProject.name = "my-example-project"
settings.gradle
rootProject.name = "my-example-project"

In this build, the root project is explicitly named. The explicit name my-example-project will be used in all reports, logs, and error messages. Regardless of where the project lives, the output of ./gradlew projects will look like this:

nameYourRootProject-do.out
> Task :projects

Projects:

------------------------------------------------------------
Root project 'my-example-project'
------------------------------------------------------------

Project hierarchy:

Root project 'my-example-project'
No sub-projects

To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :tasks

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Do not use gradle.properties in subprojects

Do not place a gradle.properties file inside subprojects to configure your build.

Explanation

Gradle allows gradle.properties files in both the root project and subprojects, but support for subproject properties is inconsistent. Gradle itself and many popular plugins (such as the Android Gradle Plugin and Kotlin Gradle Plugin) do not reliably handle this pattern.

Using subproject gradle.properties files also makes it harder to understand and debug your build. Property values may be scattered across multiple locations, overridden in unexpected ways, or difficult to trace back to their source.

If you need to set properties for a single subproject, define them directly in that subproject’s build.gradle(.kts). If you need to apply properties across multiple subprojects, extract the configuration into a convention plugin.

Example
Don’t Do This
├── app
│   ├── ⋮
│   ├── build.gradle.kts
│   └── gradle.properties
├── utilities
│   ├── ⋮
│   ├── build.gradle.kts
│   └── gradle.properties
└── settings.gradle.kts
├── app
│   ├── ⋮
│   ├── build.gradle
│   └── gradle.properties
├── utilities
│   ├── ⋮
│   ├── build.gradle
│   └── gradle.properties
└── settings.gradle
gradle.properties
# This file is located in /app
propertyA=fixedValue
propertyB=someValue
build.gradle.kts
// This file is located in /app
tasks.register("printProperties") { // (1)
    val propA = project.findProperty("propertyA") // (2)
    val propB = project.findProperty("propertyB")

    doLast {
        println("propertyA in app: $propA")
        println("propertyB in app: $propB")
    }
}
build.gradle
// This file is located in /app
tasks.register("printProperties") { // (1)
    def propA = project.findProperty("propertyA") // (2)
    def propB = project.findProperty("propertyB")

    doLast {
        println "propertyA in app: $propA"
        println "propertyB in app: $propB"
    }
}
gradle.properties
# This file is located in /util
propertyA=fixedValue
propertyB=otherValue
build.gradle.kts
// This file is located in /util
tasks.register("printProperties") {
    val propA = project.findProperty("propertyA")
    val propB = project.findProperty("propertyB") // (3)

    doLast {
        println("propertyA in util: $propA")
        println("propertyB in util: $propB")
    }
}
build.gradle
// This file is located in /util
tasks.register("printProperties") {
    def propA = project.findProperty("propertyA")
    def propB = project.findProperty("propertyB") // (3)

    doLast {
        println "propertyA in util: $propA"
        println "propertyB in util: $propB"
    }
}
  1. Register a task that uses the value of properties in each subproject.

  2. The task reads properties, which are supplied by the project-local app/gradle.properties file. propertyA does not vary between subprojects.

  3. 'util’s print task reads the properties which are supplied by util/gradle.properties. propertyB varies between subprojects.

This structure requires duplicating properties that are shared between subprojects and is not guaranteed to remain supported.

Do This Instead
├── buildSrc
│   └──  ⋮
├── app
│   ├── ⋮
│   └── build.gradle.kts
├── utilities
│   ├── ⋮
│   └── build.gradle.kts
├── settings.gradle.kts
└── gradle.properties
├── buildSrc
│   └──  ⋮
├── app
│   ├── ⋮
│   └──  build.gradle
├── utilities
│   ├── ⋮
│   └── build.gradle
├── settings.gradle
└── gradle.properties
gradle.properties
# This file is located in the root of the build
propertyA=fixedValue
propertyB=someValue
ProjectProperties.kt
import org.gradle.api.provider.Property

interface ProjectProperties { // (1)
    val propertyA: Property<String>
    val propertyB: Property<String>
}
ProjectProperties.groovy
import org.gradle.api.provider.Property

interface ProjectProperties { // (1)
    Property<String> getPropertyA()
    Property<String> getPropertyB()
}
project-properties.gradle.kts
extensions.create<ProjectProperties>("myProperties") // (2)

tasks.register("printProperties") { // (3)
    val myProperties = project.extensions.getByName("myProperties") as ProjectProperties
    val projectName = project.name

    doLast {
        println("propertyA in ${projectName}: ${myProperties.propertyA.get()}")
        println("propertyB in ${projectName}: ${myProperties.propertyB.get()}")
    }
}
project-properties.gradle
extensions.create("myProperties", ProjectProperties) // (2)

tasks.register("printProperties") { // (3)
    def myProperties = project.extensions.getByName("myProperties") as ProjectProperties
    def projectName = project.name

    doLast {
        println("propertyA in ${projectName}: ${myProperties.propertyA.get()}")
        println("propertyB in ${projectName}: ${myProperties.propertyB.get()}")
    }
}
build.gradle.kts
// This file is located in /app
plugins { // (4)
    id("project-properties")
}

myProperties { // (5)
    propertyA = providers.gradleProperty("propertyA")
    propertyB = providers.gradleProperty("propertyB")
}
build.gradle
// This file is located in /app
plugins { // (4)
    id "project-properties"
}

myProperties { // (5)
    propertyA = providers.gradleProperty("propertyA")
    propertyB = providers.gradleProperty("propertyB")
}
build.gradle.kts
// This file is located in /util
plugins {
    id("project-properties")
}

myProperties {
    propertyA = providers.gradleProperty("propertyA")
    propertyB = "otherValue" // (6)
}
build.gradle
// This file is located in /util
plugins {
    id "project-properties"
}

myProperties {
    propertyA = providers.gradleProperty("propertyA")
    propertyB = "otherValue" // (6)
}
  1. Define a simple extension type in buildSrc to hold property values.

  2. Register that property in a convention plugin.

  3. Register tasks using property values in the convention plugin.

  4. Apply the convention plugin in each subproject.

  5. Set the extension’s property values in each subproject’s build script. This uses the values defined in the root gradle.properties file. The task reads values from the extension, not directly from the project properties.

  6. When values need to vary between subprojects, they can be set directly on the extension.

This structure uses an extension type to hold values, allowing properties to be strongly typed, and for property values and operations on properties to be defined in a single location. Overriding values per subproject remains straightforward.

Avoid afterEvaluate

Do not use project.afterEvaluate {} to configure tasks, wire properties, or react to plugin application. Use lazy properties and pluginManager.withPlugin() instead.

Explanation

afterEvaluate registers a callback that runs after Gradle finishes evaluating and configuring a project. It was historically used to "delay" reading a value until configuration was complete — for example, reading an extension property that users set at the bottom of their build script, or checking whether another plugin was applied.

This pattern is outdated, and problematic for several reasons:

  • Ordering is fragile. Multiple afterEvaluate callbacks execute in registration order. If two plugins or scripts both use afterEvaluate, one may see stale or incomplete configuration depending on which was registered first. This creates subtle bugs that are extremely difficult to diagnose.

  • It defeats task configuration avoidance. Tasks registered or configured inside afterEvaluate are touched eagerly during configuration, even if they will never execute. This may cause unnecessary work and slow down the configuration phase of the build.

  • It is incompatible with the Configuration Cache. afterEvaluate callbacks capture mutable project state that cannot be serialized reliably.

Gradle’s lazy Property and Provider types solve the same underlying problem — deferring value resolution — without any of these drawbacks. A Property<T> can be wired at configuration time but its value is resolved only when needed, typically during task execution. This makes configuration order-independent and fully compatible with the configuration cache.

Similarly, pluginManager.withPlugin() reacts to plugin application safely and immediately, regardless of when the plugin is actually applied — no callback ordering to worry about.

When afterEvaluate may still be appropriate

There are narrow use cases where afterEvaluate remains the only available hook:

  • Fail-fast validation — verifying that required project configuration has been set and failing the build early with a clear error message.

  • Logging or reporting — printing diagnostic information about the project’s final configuration state.

Even in these cases, exercise caution: your afterEvaluate must be the last (or only) one registered to see the final configuration state. If another plugin registers an afterEvaluate after yours, your callback may see incomplete configuration.

If you find yourself reaching for afterEvaluate because Gradle’s lazy APIs do not cover your use case, consider filing a bug. afterEvaluate should be a last resort, not a first choice.

Example

Given the following project layout:

.
├── build.gradle.kts
├── buildSrc/
│   ├── build.gradle.kts
│   └── src/main/kotlin/
│       └── AppInfoPlugin.kt
└── settings.gradle.kts
.
├── build.gradle
├── buildSrc/
│   ├── build.gradle
│   └── src/main/groovy/
│       └── AppInfoPlugin.groovy
└── settings.gradle
Don’t Do This

The plugin uses afterEvaluate to delay reading the extension value and to check whether java-library was applied:

buildSrc/src/main/kotlin/AppInfoPlugin.kt
interface AppInfoExtension {
    val appName: Property<String>
}

class AppInfoPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val extension = project.extensions.create("appInfo", AppInfoExtension::class.java)

        project.afterEvaluate { // (1)
            val name = extension.appName.getOrElse("unnamed") // (2)

            tasks.register("printAppInfo") { // (3)
                doLast {
                    println("App: $name")
                }
            }

            if (plugins.hasPlugin("java-library")) { // (4)
                tasks.named("printAppInfo") {
                    doLast {
                        println("Jar: $name.jar")
                    }
                }
                tasks.named("jar", Jar::class.java) {
                    archiveBaseName.set(name)
                }
            }
        }
    }
}
buildSrc/src/main/groovy/AppInfoPlugin.groovy
interface AppInfoExtension {
    Property<String> getAppName()
}

class AppInfoPlugin implements Plugin<Project> {
    void apply(Project project) {
        def extension = project.extensions.create("appInfo", AppInfoExtension)

        project.afterEvaluate { // (1)
            def name = extension.appName.getOrElse("unnamed") // (2)

            project.tasks.register("printAppInfo") { // (3)
                doLast {
                    println "App: $name"
                }
            }

            if (project.plugins.hasPlugin("java-library")) { // (4)
                project.tasks.named("printAppInfo") {
                    doLast {
                        println "Jar: ${name}.jar"
                    }
                }
                project.tasks.named("jar", Jar) {
                    archiveBaseName.set(name)
                }
            }
        }
    }
}
  1. The plugin’s afterEvaluate runs before any afterEvaluate registered later in the build script — ordering depends on registration order.

  2. getOrElse reads the property’s current value immediately. If the value is set in a later afterEvaluate, this will never see it.

  3. Registering a task inside afterEvaluate defeats task configuration avoidance.

  4. Checking plugin presence inside afterEvaluate assumes all plugins have been applied before this callback runs.

The build script applies the plugin and sets the extension value in its own afterEvaluate:

build.gradle.kts
plugins {
    id("java-library")
    id("app-info-plugin")
}

afterEvaluate {
    the<AppInfoExtension>().appName.set("my-app") // (1)
}
build.gradle
plugins {
    id 'java-library'
    id 'app-info-plugin'
}

afterEvaluate {
    appInfo { // (1)
        appName.set('my-app')
    }
}
  1. This afterEvaluate runs after the plugin’s — by the time it sets the name, the plugin has already captured the default value.

Running printAppInfo outputs unnamednot my-app as the user intended:

> Task :printAppInfo
App: unnamed
Jar: unnamed.jar

BUILD SUCCESSFUL in 0s
5 actionable tasks: 5 executed

The plugin’s afterEvaluate callback was registered first (during Plugin.apply()) and ran first, reading the property before the build script’s afterEvaluate had a chance to set it.

Do This Instead

The proper way to write this plugin uses lazy Property types and pluginManager.withPlugin():

buildSrc/src/main/kotlin/AppInfoPlugin.kt
interface AppInfoExtension {
    val appName: Property<String>
}

class AppInfoPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val extension = project.extensions.create("appInfo", AppInfoExtension::class.java)
        extension.appName.convention("unnamed") // (1)

        project.tasks.register("printAppInfo") {
            val name = extension.appName
            doLast {
                println("App: ${name.get()}") // (2)
            }
        }

        project.pluginManager.withPlugin("java-library") { // (3)
            project.tasks.named("printAppInfo") {
                val jarName = extension.appName
                doLast {
                    println("Jar: ${jarName.get()}.jar")
                }
            }
            project.tasks.named("jar", Jar::class.java) {
                archiveBaseName.set(extension.appName)
            }
        }
    }
}
buildSrc/src/main/groovy/AppInfoPlugin.groovy
interface AppInfoExtension {
    Property<String> getAppName()
}

class AppInfoPlugin implements Plugin<Project> {
    void apply(Project project) {
        def extension = project.extensions.create("appInfo", AppInfoExtension)
        extension.appName.convention("unnamed") // (1)

        project.tasks.register("printAppInfo") {
            def name = extension.appName
            doLast {
                println "App: ${name.get()}" // (2)
            }
        }

        project.pluginManager.withPlugin("java-library") { // (3)
            project.tasks.named("printAppInfo") {
                def jarName = extension.appName
                doLast {
                    println "Jar: ${jarName.get()}.jar"
                }
            }
            project.tasks.named("jar", Jar) {
                archiveBaseName.set(extension.appName)
            }
        }
    }
}
  1. convention() provides a default value that is used only if no explicit value is set via set().

  2. The value is resolved at execution time via get() — configuration order does not matter.

  3. pluginManager.withPlugin() fires when the plugin is applied, regardless of order. If the plugin is never applied, the callback is never invoked.

The build script is nearly identical — the change is in the plugin, not the consumer:

build.gradle.kts
plugins {
    id("java-library")
    id("app-info-plugin")
}

appInfo {
    appName.set("my-app") // (1)
}
build.gradle
plugins {
    id 'java-library'
    id 'app-info-plugin'
}

appInfo {
    appName.set('my-app') // (1)
}
  1. The value is set during normal configuration. Because the plugin wires the Property lazily, it is only read at execution time.

Running printAppInfo now correctly outputs my-app:

$ gradlew printAppInfo
> Task :printAppInfo
App: my-app
Jar: my-app.jar

BUILD SUCCESSFUL in 0s
5 actionable tasks: 5 executed

Consider use of @Incubating APIs carefully

Use Incubating APIs deliberately and with awareness of trade-offs. New or unstable Incubating features can require updates even on minor Gradle upgrades, while some features are close to Stable and already widely adopted. Weigh the benefits against the likely increase in maintenance effort.

Explanation

Compared to many other ecosystems, Gradle is very strict with Incubating features. As long as there are outstanding issues related to the feature, it will not be promoted to the Stable state. An Incubating state means that breaking changes can happen in non-major releases. However, the closer the feature is to being Stable, the less likely it is to change. There are a number of features that are already widely adopted and recommended as best practices, even though they are still incubating, like dependencyResolutionManagement.repositories.

When considering using an Incubating feature, the best approach is to check its maturity and stability:

  1. Check which Gradle version introduced it.

  2. Check related GitHub issues, how many are linked, and whether any would affect you.

If the feature is new or still has many linked issues, consider its use carefully. It may introduce an additional maintenance cost for your build setup, as you may need to update your build scripts even on minor version updates.

Note
Remember that the goal of Incubating features is to put them in users' hands early to gather feedback and guide their evolution. If you are using such features, please provide feedback and report bugs if you encounter any.

Best Practices for Structuring Builds

Modularize Your Builds

Modularize your builds by splitting your code into multiple projects.

Explanation

Splitting your build’s source into multiple Gradle projects (modules) is essential for leveraging Gradle’s automatic work avoidance and parallelization features. When a source file changes, Gradle only recompiles the affected projects. If all your sources reside in a single project, Gradle can’t avoid recompilation and won’t be able to run tasks in parallel. Splitting your source into multiple projects can provide additional performance benefits by minimizing each subproject’s compilation classpath and ensuring code generating tools such as annotation and symbol processors run only on the relevant files.

Do this soon. Don’t wait until you hit some arbitrary number of source files or classes to do this, instead structure your build into multiple projects from the start using whatever natural boundaries exist in your codebase.

Exactly how to best split your source varies with every build, as it depends on the particulars of that build. Here are some common patterns we found that can work well and make cohesive projects:

  • API vs. Implementation

  • Front-end vs. Back-end

  • Core business logic vs. UI

  • Vertical slices (e.g., feature modules each containing UI + business logic)

  • Inputs to source generation vs. their consumers

  • Or simply closely related classes.

Ultimately, the specific scheme matters less than ensuring that your build is split logically and consistently.

Expanding a build to hundreds of projects is common, and Gradle is designed to scale to this size and beyond. In the extreme, tiny projects containing only a class or two are probably counterproductive. However, you should typically err on the side of adding more projects rather than fewer.

Example
Don’t Do This
A common way to structure new builds
├── app // This project contains a mix of classes
│    ├── build.gradle.kts
│    └── src
│        └── main
│            └── java
│                └── org
│                    └── example
│                        └── CommonsUtil.java
│                        └── GuavaUtil.java
│                        └── Main.java
│                        └── Util.java
├── settings.gradle.kts
A common way to structure new builds
├── app // This project contains a mix of classes
│    ├── build.gradle
│    └── src
│        └── main
│            └── java
│                └── org
│                    └── example
│                        └── CommonsUtil.java
│                        └── GuavaUtil.java
│                        └── Main.java
│                        └── Util.java
├── settings.gradle
settings.gradle.kts
include("app") // (1)
settings.gradle
include("app") // (1)
build.gradle.kts
plugins {
    application // (2)
}

dependencies {
    implementation("com.google.guava:guava:31.1-jre") // (3)
    implementation("commons-lang:commons-lang:2.6")
}

application {
    mainClass = "org.example.Main"
}
build.gradle
plugins {
    id 'application' // (2)
}

dependencies {
    implementation 'com.google.guava:guava:31.1-jre' // (3)
    implementation 'commons-lang:commons-lang:2.6'
}

application {
    mainClass = "org.example.Main"
}
  1. This build contains only a single project (in addition to the root project) that contains all the source code. If there is any change to any source file, Gradle will have to recompile and rebuild everything. While incremental compilation will help (especially in this simplified example) this is still less efficient then avoidance. Gradle also won’t be able to run any tasks in parallel, since all these tasks are in the same project, so this design won’t scale nicely.

  2. As there is only a single project in this build, the application plugin must be applied here. This means that the application plugin will affect all source files in the build, even those which have no need for it.

  3. Likewise, the dependencies here are only needed by each particular implmentation of util. There’s no need for the implementation using Guava to have access to the Commons library, but it does because they are all in the same project. This also means that the classpath for each subproject is much larger than it needs to be, which can lead to longer build times and other confusion.

Do This Instead
A better way to structure this build
├── app
│    ├── build.gradle.kts
│    └── src
│        └── main
│            └── java
│                └── org
│                    └── example
│                        └── Main.java
├── settings.gradle.kts
├── util
│    ├── build.gradle.kts
│    └── src
│        └── main
│            └── java
│                └── org
│                    └── example
│                        └── Util.java
├── util-commons
│    ├── build.gradle.kts
│    └── src
│        └── main
│            └── java
│                └── org
│                    └── example
│                        └── CommonsUtil.java
└── util-guava
    ├── build.gradle.kts
    └── src
        └── main
            └── java
                └── org
                    └── example
                        └── GuavaUtil.java
A better way to structure this build
├── app // App contains only the core application logic
│    ├── build.gradle
│    └── src
│        └── main
│            └── java
│                └── org
│                    └── example
│                        └── Main.java
├── settings.gradle
├── util // Util contains only the core utility logic
│    ├── build.gradle
│    └── src
│        └── main
│            └── java
│                └── org
│                    └── example
│                        └── Util.java
├── util-commons // One particular implementation of util, using Apache Commons
│    ├── build.gradle
│    └── src
│        └── main
│            └── java
│                └── org
│                    └── example
│                        └── CommonsUtil.java
└── util-guava // Another implementation of util, using Guava
    ├── build.gradle
    └── src
        └── main
            └── java
                └── org
                    └── example
                        └── GuavaUtil.java
settings.gradle.kts
include("app") // (1)
include("util")
include("util-commons")
include("util-guava")
settings.gradle
include("app") // (1)
include("util")
include("util-commons")
include("util-guava")
build.gradle.kts
// This is the build.gradle file for the app module

plugins {
    application // (2)
}

dependencies { // (3)
    implementation(project(":util-guava"))
    implementation(project(":util-commons"))
}

application {
    mainClass = "org.example.Main"
}
build.gradle
// This is the build.gradle file for the app module

plugins {
    id "application" // (2)
}

dependencies { // (3)
    implementation project(":util-guava")
    implementation project(":util-commons")
}

application {
    mainClass = "org.example.Main"
}
build.gradle.kts
// This is the build.gradle file for the util-commons module

plugins { // (4)
    `java-library`
}

dependencies { // (5)
    api(project(":util"))
    implementation("commons-lang:commons-lang:2.6")
}
build.gradle
// This is the build.gradle file for the util-commons module

plugins { // (4)
    id "java-library"
}

dependencies { // (5)
    api project(":util")
    implementation "commons-lang:commons-lang:2.6"
}
build.gradle.kts
// This is the build.gradle file for the util-guava module

plugins {
    `java-library`
}

dependencies {
    api(project(":util"))
    implementation("com.google.guava:guava:31.1-jre")
}
build.gradle
// This is the build.gradle file for the util-guava module

plugins {
    id "java-library"
}

dependencies {
    api project(":util")
    implementation "com.google.guava:guava:31.1-jre"
}
  1. This build logically splits the source into multiple projects. Each project can be built independently, and Gradle can run tasks in parallel. This means that if you change a single source file in one of the projects, Gradle will only need to recompile and rebuild that project, not the entire build.

  2. The application plugin is only applied to the app project, which is the only project that needs it.

  3. Each project only adds the dependencies it needs. This means that the classpath for each subproject is much smaller, which can lead to faster build times and less confusion.

  4. Each project only adds the specific plugins it needs.

  5. Each project only adds the dependencies it needs. Projects can effectively use API vs. Implementation separation.

Do Not Put Source Files in the Root Project

Do not put source files in your root project; instead, put them in a separate project.

Explanation

The root project is a special Project in Gradle that serves as the entry point for your build.

It is the place to configure some settings and conventions that apply globally to the entire build, that are not configured via Settings. For example, you can declare (but not apply) plugins here to ensure the same plugin version is consistently available across all projects and define other configurations shared by all projects within the build.

Note
Be careful not to apply plugins unnecessarily in the root project - many plugins only affect source code and should only be applied to the projects that contain source code.

The root project should not be used for source files, instead they should be located in a separate Gradle project.

Setting up your build like this from the start will also make it easier to add new projects as your build grows in the future.

Example
Don’t Do This
A common way to structure new builds
├── build.gradle.kts // Applies the `java-library` plugin to the root project
├── settings.gradle.kts
└── src // This directory shouldn't exist
    └── main
        └── java
            └── org
                └── example
                    └── MyClass1.java
A common way to structure new builds
├── build.gradle // Applies the `java-library` plugin to the root project
├── settings.gradle
└── src // This directory shouldn't exist
    └── main
        └── java
            └── org
                └── example
                    └── MyClass1.java
build.gradle.kts
plugins { // (1)
    `java-library`
}
build.gradle
plugins {
    id 'java-library' // (1)
}
  1. The java-library plugin is applied to the root project, as there are Java source files are in the root project.

Do This Instead
A better way to structure new builds
├── core
│    ├── build.gradle.kts // Applies the `java-library` plugin to only the `core` project
│    └── src // Source lives in a "core" (sub)project
│        └── main
│            └── java
│                └── org
│                    └── example
│                        └── MyClass1.java
└── settings.gradle.kts
A better way to structure new builds
├── core
│    ├── build.gradle // Applies the `java-library` plugin to only the `core` project
│    └── src // Source lives in a "core" (sub)project
│        └── main
│            └── java
│                └── org
│                    └── example
│                        └── MyClass1.java
└── settings.gradle
settings.gradle.kts
include("core") // (1)
settings.gradle
include("core") // (1)
build.gradle.kts
// This is the build.gradle.kts file for the core module

plugins { // (2)
    `java-library`
}
build.gradle
// This is the build.gradle file for the core module

plugins { // (2)
    id 'java-library'
}
  1. The root project exists only to configure the build, informing Gradle of a (sub)project named core.

  2. The java-library plugin is only applied to the core project, which contains the Java source files.

Favor build-logic Composite Builds for Build Logic

You should set up a Composite Build (often called an "included build") to hold your build logic—including any custom plugins, convention plugins, and other build-specific customizations.

Explanation

The preferred location for build logic is an included build (typically named build-logic), not in buildSrc.

The automatically available buildSrc is great for rapid prototyping, but it comes with some subtle disadvantages:

  • There are classloader differences in how these 2 approaches behave that can be surprising; included builds are treated just like external dependencies, which is a simpler mental model. Dependency resolution behaves subtly differently in buildSrc.

  • There can potentially be fewer task invalidations in a build when files in an included build are modified, leading to faster builds. Any change in buildSrc causes the entire build to become out-of-date, whereas changes in a subproject of an included build only cause projects in the build using the products of that particular subproject to be out-of-date.

  • Included builds are complete Gradle builds and can be opened, worked on, and built independently as standalone projects. It is straightforward to publish their products, including plugins, in order to share them with other projects.

  • The buildSrc project automatically applies the java plugin, which may be unnecessary.

One important caveat to this recommendation is when creating Settings plugins. Defining these in a build-logic project requires it to be included in the pluginManagement block of the main build’s settings.gradle(.kts) file, in order to make these plugins available to the build early enough to be applied to the Settings instance. This is possible, but reduces Build Caching capability, potentially impacting performance. A better solution is to use a separate, minimal, included build (e.g. build-logic-settings) to hold only Settings plugins.

Another potential reason to use buildSrc is if you have a very large number of subprojects within your included build-logic. Applying a different set of build-logic plugins to the subprojects in your including build will result in a different classpath being used for each. This may have performance implications and make your build harder to understand. Using different plugin combinations can cause features like Build Services to break in difficult to diagnose ways.

Ideally, there would be no difference between using buildSrc and an included build, as buildSrc is intended to behave like an implicitly available included build. However, due to historical reasons, these subtle differences still exist. As this changes, this recommendation may be revised in the future. For now, these differences can introduce confusion.

Since setting up a composite build requires only minimal additional configuration, we recommend using it over buildSrc in most cases, especially for creating convention plugins.

Example
Don’t Do This
├── build.gradle.kts
├── buildSrc
│    ├── build.gradle.kts
│    └── src
│        └── main
│            └── java
│                └── org
│                    └── example
│                        ├── MyPlugin.java
│                        └── MyTask.java
└── settings.gradle.kts
├── build.gradle
├── buildSrc
│    ├── build.gradle
│    └── src
│        └── main
│            └── java
│                └── org
│                    └── example
│                        ├── MyPlugin.java
│                        └── MyTask.java
└── settings.gradle
build.gradle.kts
// This file is located in /buildSrc

plugins {
    `java-gradle-plugin`
}

gradlePlugin {
    plugins {
        create("myPlugin") {
            id = "org.example.myplugin"
            implementationClass = "org.example.MyPlugin"
        }
    }
}
build.gradle
// This file is located in /buildSrc

plugins {
    id "java-gradle-plugin"
}

gradlePlugin {
    plugins {
        create("myPlugin") {
            id = "org.example.myplugin"
            implementationClass = "org.example.MyPlugin"
        }
    }
}

Set up a Plugin Build: This is the same using either method.

settings.gradle.kts
rootProject.name = "favor-composite-builds"
settings.gradle
rootProject.name = "favor-composite-builds"

buildSrc products are automatically usable: There is no additional configuration with this method.

Do This Instead
├── build-logic
│    ├── plugin
│    │    ├── build.gradle.kts
│    │    └── src
│    │        └── main
│    │            └── java
│    │                └── org
│    │                    └── example
│    │                        ├── MyPlugin.java
│    │                        └── MyTask.java
│    └── settings.gradle.kts
├── build.gradle.kts
└── settings.gradle.kts
├── build-logic
│    ├── plugin
│    │    ├── build.gradle
│    │    └── src
│    │        └── main
│    │            └── java
│    │                └── org
│    │                    └── example
│    │                        ├── MyPlugin.java
│    │                        └── MyTask.java
│    └── settings.gradle
├── build.gradle
└── settings.gradle
build.gradle.kts
// This file is located in /build-logic/plugin

plugins {
    `java-gradle-plugin`
}

gradlePlugin {
    plugins {
        create("myPlugin") {
            id = "org.example.myplugin"
            implementationClass = "org.example.MyPlugin"
        }
    }
}
build.gradle
// This file is located in /build-logic/plugin

plugins {
    id "java-gradle-plugin"
}

gradlePlugin {
    plugins {
        create("myPlugin") {
            id = "org.example.myplugin"
            implementationClass = "org.example.MyPlugin"
        }
    }
}

Set up a Plugin Build: This is the same using either method.

settings.gradle.kts
// This file is located in the root project

includeBuild("build-logic") // (1)

rootProject.name = "favor-composite-builds"
settings.gradle
// This file is located in the root project

includeBuild("build-logic") // (1)

rootProject.name = "favor-composite-builds"
settings.gradle.kts
// This file is located in /build-logic

rootProject.name = "build-logic"

include("plugin") // (2)
settings.gradle
// This file is located in /build-logic

rootProject.name = "build-logic"

include("plugin") // (2)
  1. Composite builds must be explicitly included: Use the includeBuild method to locate and include a build in order to use its products.

  2. Structure your included build into subprojects: This allows the main build to only depend on the necessary parts of the included build.

Avoid Unintentionally Creating Empty Projects

When using a hierarchical directory structure to organize your Gradle projects, make sure to avoid unintentionally creating empty projects in your build.

Explanation

When you use the Settings.include() method to include a project in your Grade settings file, you typically include projects by supplying the directory name like include("featureA"). This usage assumes that featureA is located at the root of your build.

You can include projects located in nested subdirectories by specifying their full project path using : as a separator between path segments. For instance, if project search was located in a subdirectory named features, itself located in a subdirectory named subs, you could call include(":subs:features:search") to include it.

Nesting projects in a sensible hierarchical directory structure is common practice in larger Gradle builds. This approach helps organize large builds and improves comprehensibility, compared to placing all projects directly under the build’s root.

However, without further configuration, Gradle will create empty projects for each element in every hierarchical path, even if some of those directories do not contain actual Gradle projects. In the example above, Gradle will create a project named :subs, a project named :subs:features, and a project named :subs:features:search. This behavior is usually not intended, as you likely only want to include the search project.

Unused projects - even if empty - can surprise maintainers, clutter reports, and make your build harder to understand. They also introduce unintended side effects. If you use allprojects { …​ } or subprojects { …​ }, plugins and configuration blocks will apply to every project, including the empty ones. This can degrade build performance. Additionally, invoking tasks on deeply nested projects requires using the full project path, such as gradle :subs:features:search:build, instead of the shorter gradle :search:build.

To avoid these downsides when using a hierarchical project structure, you can provide a flat name when including the project and explicitly set the Project.projectDir property for any projects located in nested directories:

include(':my-web-module')
project(':my-web-module').projectDir = file("subs/web/my-web-module")

This will prevent Gradle from creating empty projects for each element of the project’s path.

Note
Always use an identical logical project name and physical project location to avoid confusion. Don’t include a project named :search and locate it at features/ui/default-search-toolbar, as this will lead to confusion about the location of the project. Instead, locate this project at features/ui/search.

You should avoid unnecessarily deep directory structures. For builds containing only a few projects, it’s usually better to keep the structure flat by placing all projects at the root of the build. This eliminates the need to explicitly set projectDir. Within the context of a particular build, the pathless project name should clearly indicate where the project is located. You can also run the projects report for more information about the projects in your build and their locations.

If you find yourself facing ambiguity about project locations, consider simplifying the directory layout by flattening the structure, or using longer, more descriptive project names.

Example
Don’t Do This
├── settings.gradle.kts
├── app/ // (1)
│   ├── build.gradle.kts
│   └── src/
└── subs/ // (2)
    └── web/ // (3)
        ├── my-web-module/ // (4)
            ├── src/
            └── build.gradle.kts
├── settings.gradle
├── app/ // (1)
│   ├── build.gradle
│   └── src/
└── subs/ // (2)
    └── web/ // (3)
        ├── my-web-module/ // (4)
            ├── src/
            └── build.gradle
  1. A project named app located at the root of the build

  2. A directory named subs that is not intended to represent a Gradle project, but is used to organize the build

  3. Another organizational directory not intended to represent a Gradle project

  4. A Gradle project named my-web-module that should be included in the build

settings.gradle.kts
include(":app") // (1)
include(":subs:web:my-web-module") // (2)
settings.gradle
include(":app") // (1)
include(":subs:web:my-web-module") // (2)
  1. Including the app project located at the root of the build requires no additional configuration

  2. Including a project named :subs:my-web-module located in a nested subdirectory causes Gradle to create empty projects for each element of the path

avoidEmptyProjects-avoid.out
> Task :projects

Projects:

------------------------------------------------------------
Root project 'avoidEmptyProjects-avoid'
------------------------------------------------------------

Location: /home/user/gradle/samples

Project hierarchy:

Root project 'avoidEmptyProjects-avoid'
+--- Project ':app'
\--- Project ':subs'
     \--- Project ':subs:web'
          \--- Project ':subs:web:my-web-module'

Project locations:

project ':app' - /app
project ':subs' - /subs
project ':subs:web' - /subs/web
project ':subs:web:my-web-module' - /subs/web/my-web-module

To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :app:tasks

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

The output of running the projects report on the above build shows that Gradle created empty projects for :subs and :subs:web.

Do This Instead
settings.gradle.kts
include(":app")

include(":my-web-module")
project(":my-web-module").projectDir = file("subs/web/my-web-module") // (1)
settings.gradle
include(":app")

include(":my-web-module")
project(":my-web-module").projectDir = file("subs/web/my-web-module") // (1)
  1. After including the :subs:web:my-web-module project, its projectDir property is set to the physical location of the project

avoidEmptyProjects-do.out
> Task :projects

Projects:

------------------------------------------------------------
Root project 'avoidEmptyProjects-do'
------------------------------------------------------------

Location: /home/user/gradle/samples

Project hierarchy:

Root project 'avoidEmptyProjects-do'
+--- Project ':app'
\--- Project ':my-web-module'

Project locations:

project ':app' - /app
project ':my-web-module' - /subs/web/my-web-module

To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :app:tasks

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

The output of running the projects report on the above build shows that now Gradle only creates the intended projects for this build.

You can also now invoke tasks on the my-web-module project using the shorter name :my-web-module like gradle :my-web-module:build, instead of gradle :subs:web:my-web-module:build.

Use Convention Plugins for Common Build Logic

Use convention plugins to encapsulate and reuse shared build logic across multiple projects in your build.

Explanation

Instead of duplicating configuration across multiple build scripts, you can easily move common logic into a reusable convention plugins.

This approach offers several benefits:

  • Reduces duplication: Shared build logic lives in one place, making your build easier to understand.

  • Unlocks modularization: Convention plugins can apply other convention plugins, allowing you to orchestrate your build logic from small pieces.

  • Centralizes configuration: Updates to build behavior can be made in one file instead of many.

  • Keeps build files clean: Project build files stay focused on project-specific configuration.

  • Improves IDE support: IDEs can better understand and validate build logic when it is structured in plugins.

Convention plugins are quicker to create than typed binary plugins extending the Plugin class. They are often a better choice for build logic that does not need to be shared outside a build, and that is simple enough to not require additional type safeness and testability benefits. Unlike binary plugins, convention plugins allow accessing plugin extensions, tasks and configurations via static accessors in build scripts written in Kotlin.

While setting up convention plugins takes some initial effort, it pays off by simplifying maintenance, improving comprehensibility, and making it easier to add new projects as your codebase grows.

As mentioned in Favor build-logic Composite Builds for Build Logic, we recommend placing your convention plugins in an included build (often named build-logic) instead of buildSrc.

Example
Don’t Do This
project-a/build.gradle.kts
plugins {
    `java-library`
}

// Duplicated configuration across multiple build files
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

tasks.withType<JavaCompile>().configureEach {
    options.encoding = "UTF-8"
    options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation")) // (1)
}

tasks.test {
    useJUnitPlatform()
    maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1 // (2)
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.3") // (3)
}
project-b/build.gradle.kts
plugins {
    `java-library`
}

// Duplicated configuration across multiple build files
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

tasks.withType<JavaCompile>().configureEach {
    options.encoding = "UTF-8"
    options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation")) // (1)
}

tasks.test {
    useJUnitPlatform()
    maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1 // (2)
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.3") // (3)
    api("com.google.guava:guava:23.0") // (4)
}
project-a/build.gradle
plugins {
    id("java-library")
}

// Duplicated configuration across multiple build files
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

tasks.withType(JavaCompile).configureEach {
    options.encoding = "UTF-8"
    options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"] // (1)
}

test {
    useJUnitPlatform()
    maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 // (2)
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.3") // (3)
}
project-b/build.gradle
plugins {
    id("java-library")
}

// Duplicated configuration across multiple build files
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

tasks.withType(JavaCompile).configureEach {
    options.encoding = "UTF-8"
    options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"] // (1)
}

test {
    useJUnitPlatform()
    maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 // (2)
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.3") // (3)
    api("com.google.guava:guava:23.0") // (4)
}
  1. Common compiler settings repeated across multiple projects.

  2. Shared test configuration that must be maintained in multiple places.

  3. Common dependencies that could be managed centrally.

  4. Unique project dependencies.

Do This Instead

Create included build containing convention plugins for your build in build-logic and add it to your settings file:

settings.gradle.kts
pluginManagement {
    includeBuild("build-logic") // (1)
}
build-logic/build.gradle.kts
plugins {
    `kotlin-dsl` // (2)
}
settings.gradle
pluginManagement {
    includeBuild("build-logic") // (1)
}
build-logic/build.gradle
plugins {
    id("groovy-gradle-plugin") // (2)
}
  1. Include the build-logic build, which defines convention plugins.

  2. Enable the use of Kotlin DSL in build-logic.

Create convention plugins for each type of project in build-logic:

build-logic/src/main/kotlin/my.base-java-library.gradle.kts
plugins {
    `java-library`
}

java { // (1)
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

tasks.withType<JavaCompile>().configureEach {
    options.encoding = "UTF-8"
    options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation"))
}
build-logic/src/main/kotlin/my.java-library.gradle.kts
plugins { // (2)
    id("my.base-java-library")
    id("my.java-use-junit5")
}
build-logic/src/main/kotlin/my.java-use-junit5.gradle.kts
plugins {
    `java-library`
}

tasks.withType<Test>().configureEach { // (3)
    useJUnitPlatform()
    maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
}
build-logic/src/main/groovy/my.base-java-library.gradle
plugins {
    id("java-library")
}

java { // (1)
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

tasks.withType(JavaCompile).configureEach {
    options.encoding = "UTF-8"
    options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"]
}
build-logic/src/main/groovy/my.java-library.gradle
plugins { // (2)
    id("my.base-java-library")
    id("my.java-use-junit5")
}
build-logic/src/main/groovy/my.java-use-junit5.gradle
plugins {
    id("java-library")
}

tasks.withType(Test).configureEach { // (3)
    useJUnitPlatform()
    maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
}
  1. Default settings for a Java library plugin.

  2. A convention plugin can apply other convention plugins.

  3. JUnit 5 configuration moved to a convention plugin.

And apply these plugin in any build files to use the shared logic:

project-a/build.gradle.kts
plugins {
    id("my.java-library") // (6)
}
project-b/build.gradle.kts
plugins {
    id("my.java-library") // (6)
}

dependencies {
    api("com.google.guava:guava:23.0") // (7)
}
project-a/build.gradle
plugins {
    id("my.java-library") // (6)
}
project-b/build.gradle
plugins {
    id("my.java-library") // (6)
}

dependencies {
    api("com.google.guava:guava:23.0") // (7)
}

Best Practices for Dependencies

Use Version Catalogs to Centralize Dependency Versions

Version Catalogs provide a centralized, declarative way to manage dependency versions throughout a build.

Explanation

When you define your dependency versions in a single, shared version catalog, you reduce duplication and make upgrades easier. Instead of changing dozens of build.gradle(.kts) files, you update the version in one place. This simplifies maintenance, improves consistency, and reduces the risk of accidental version drift between modules. Consistent version declarations across projects also make it easier to reason about behavior during testing—especially in modular builds where transitive upgrades can silently change runtime behavior in later stages of the build.

However, version catalogs only influence declared versions, not resolved versions. Use them in combination with dependency locking and version alignment to enforce consistency across builds. To influence resolved versions, check out platforms.

Example
Don’t Do This

Avoid declaring versions in project.ext, constants, or local variables:

build.gradle.kts
plugins {
    id("java-library")
    id("com.github.ben-manes.versions").version("0.45.0")
}
val groovyVersion = "3.0.5"

dependencies {
    api("org.codehaus.groovy:groovy:$groovyVersion")
    api("org.codehaus.groovy:groovy-json:$groovyVersion")
    api("org.codehaus.groovy:groovy-nio:$groovyVersion")

    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")

    implementation("org.apache.commons:commons-lang3") {
        version {
            strictly("[3.8, 4.0[")
            prefer("3.9")
        }
    }
}
build.gradle
plugins {
    id('java-library')
    id('com.github.ben-manes.versions').version('0.45.0')
}
def groovyVersion = '3.0.5'

dependencies {
    api("org.codehaus.groovy:groovy:$groovyVersion")
    api("org.codehaus.groovy:groovy-json:$groovyVersion")
    api("org.codehaus.groovy:groovy-nio:$groovyVersion")

    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")

    implementation("org.apache.commons:commons-lang3") {
        version {
            strictly("[3.8, 4.0[")
            prefer("3.9")
        }
    }
}

Avoid misusing version catalogs for unrelated concerns:

  • Don’t use them to store shared strings or non-library constants

  • Don’t overload them with arbitrary logic or plugin-specific configuration

Do This Instead

Use a centralized libs.versions.toml file in your gradle/ directory:

gradle/libs.versions.toml
[versions]
groovy = "3.0.5"
junit-jupiter = "5.10.0"

[libraries]
groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" }
groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = { strictly = "[3.8, 4.0[", prefer = "3.9" } }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }

[bundles]
groovy = ["groovy-core", "groovy-json", "groovy-nio"]

[plugins]
versions = { id = "com.github.ben-manes.versions", version = "0.45.0" }
build.gradle.kts
plugins {
    id("java-library")
    alias(libs.plugins.versions)
}
dependencies {
    api(libs.bundles.groovy)
    testImplementation(libs.junit.jupiter)
    implementation(libs.commons.lang3)
}
build.gradle
plugins {
    id('java-library')
    alias(libs.plugins.versions)
}
dependencies {
    api(libs.bundles.groovy)
    testImplementation(libs.junit.jupiter)
    implementation(libs.commons.lang3)
}

Name Version Catalog Entries Appropriately

Consistent and descriptive names in your version catalog enhance readability and maintainability across your build scripts.

Explanation

Version catalogs provide a centralized way to manage dependencies by mapping full dependency coordinates to concise, reusable aliases like airlift-aircompressor. Adopting clear naming conventions for those aliases ensures that developers can easily identify and use dependencies throughout the project.

Aliases are typically made up of 1 to 3 segments. For example org.apache.commons:commons-lang3 could be represented as commonsLang3, apache-commonsLang3, or commons-lang3.

The following guidelines help in naming catalog entries effectively:

  1. Use dashes to separate segments: Prefer hyphen/dashes (-) over underscores (_) to separate different parts of the entry name.

    Example: For org.apache.logging.log4j:log4j-api, use log4j-api

  2. Derive the first segment from the project group: Use a unique identifier from the project’s group ID as the first segment. Do not include the top level domain in the segment (com, org, net, dev).

    Example: For com.fasterxml.jackson.core:jackson-databind, use jackson-databind or jackson-core-databind

  3. Derive the second segment from the artifact ID: Use a unique identifier from the artifact ID as the second segment.

    Example: For com.linecorp.armeria:armeria-grpc, use armeria-grpc

  4. Avoid generic terms in the segments: Exclude terms that are obvious or implied in the context of your project (core, java, gradle, module, sdk), especially if the term appears by itself.

    Example: For com.google.googlejavaformat:google-java-format, use google-java-format, not google-java or java

  5. Omit redundant segments: If the group and artifact IDs are the same, avoid repeating them.

    Example: For io.ktor:ktor-client-core, use ktor-client-core, not ktor-ktor-client-core

  6. Convert internal dashes to camelCase: If the artifact ID contains dashes, convert them to camelCase for better readability in code.

    Example: spring-boot-starter-web becomes springBootStarterWeb

  7. Suffix plugin libraries with -plugin: When referencing a plugin as a library (not in the [plugins] section), append -plugin to the name.

    Example: For org.owasp:dependency-check-gradle, use dependency-check-plugin

Example
gradle/libs.versions.toml
[versions]
slf4j = "2.0.13"
jackson = "2.17.1"
groovy = "3.0.5"
checkstyle = "8.37"
commonsLang = "3.9"

[libraries]
# SLF4J
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }

# Jackson
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-dataformatCsv = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-csv", version.ref = "jackson" }

# Groovy bundle
groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" }
groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" }

# Apache Commons Lang
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = { strictly = "[3.8, 4.0[", prefer = "3.9" } }

[bundles]
groovy = ["groovy-core", "groovy-json", "groovy-nio"]

[plugins]
versions = { id = "com.github.ben-manes.versions", version = "0.45.0" }
build.gradle.kts
plugins {
    id("java-library")
    alias(libs.plugins.versions)
}

repositories {
    mavenCentral()
}

dependencies {
    // SLF4J
    implementation(libs.slf4j.api)

    // Jackson
    implementation(libs.jackson.databind)
    implementation(libs.jackson.dataformatCsv)

    // Groovy bundle
    api(libs.bundles.groovy)

    // Commons Lang
    implementation(libs.commons.lang3)
}
build.gradle
plugins {
    id 'java-library'
    alias(libs.plugins.versions)
}

repositories {
    mavenCentral()
}

dependencies {
    // SLF4J
    implementation libs.slf4j.api

    // Jackson
    implementation libs.jackson.databind
    implementation libs.jackson.dataformatCsv

    // Groovy bundle
    api libs.bundles.groovy

    // Commons Lang
    implementation libs.commons.lang3
}

Set up your Dependency Repositories in the Settings file

Declare your repositories for your plugins and dependencies in settings.gradle.kts.

Explanation

Using settings.gradle.kts file to declare repositories has several benefits:

  • Avoids repetition: Centralizing repository declarations eliminates the need to repeat them in each project’s build.gradle.kts.

  • Improves debuggability: Ensures all projects resolve dependencies during resolution from the same repositories, in a consistent order.

  • Matches the build model: Repositories are not part of the project definition; they are part of global build logic, so settings is a more appropriate place for them.

Note
While dependencyResolutionManagement.repositories is an incubating API, it is the preferred way of declaring repositories.
Example
Don’t Do This

You could set up repositories in individual build.gradle.kts files with:

build.gradle.kts
buildscript {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}

plugins {
    id("java")
}

repositories {
    mavenCentral()
}
build.gradle
buildscript {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}

plugins {
    id("java")
}

repositories {
    mavenCentral()
}
Do This Instead

Instead, you should set them up in settings.gradle.kts like this:

settings.gradle.kts
pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        mavenCentral()
    }
}
settings.gradle
pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        mavenCentral()
    }
}

Don’t Explicitly Depend on the Kotlin Standard Library

The Kotlin Gradle Plugin automatically adds a dependency on the Kotlin standard library (stdlib) to each source set, so there is no need to declare it explicitly.

Explanation

The version of the standard library added is the same as the version of the Kotlin Gradle Plugin applied to the project. If your build does not require a specific or different version of the standard library, you should avoid adding it manually.

Note
Setting the kotlin.stdlib.default.dependency property to false prevents the Kotlin plugin from automatically adding the Kotlin standard library dependency to your project. This can be useful in specific scenarios, such as when you want to manage the Kotlin standard library dependency version manually.
Example
Don’t Do This
build.gradle.kts
plugins {
    kotlin("jvm").version("2.4.0")
}

dependencies {
    api(kotlin("stdlib")) // (1)
}
build.gradle
plugins {
    id("org.jetbrains.kotlin.jvm") version "2.4.0"
}

dependencies {
    api("org.jetbrains.kotlin:kotlin-stdlib:2.4.0") // (1)
}
  1. stdlib is explicitly depended upon: This project contains an implicit dependency on the Kotlin standard library, which is required to compile its source code.

Do This Instead
build.gradle.kts
plugins {
    kotlin("jvm").version("2.4.0") // (1)
}
build.gradle
plugins {
    id("org.jetbrains.kotlin.jvm") version "2.4.0"  // (1)
}
  1. stdlib dependency is not included explicitly: The standard library remains available for use, and source code requiring it can be compiled without any issues.

Avoid Redundant Dependency Declarations

Avoid declaring the same dependency multiple times, especially when it is already available transitively or through another configuration.

Explanation

Duplicating dependencies in Gradle build scripts can lead to:

  • Increased maintenance: Declaring a dependency in multiple places makes it harder to manage.

  • Unexpected behavior: Declaring the same dependency in multiple configurations (e.g., compileOnly and implementation) can result in hard-to-diagnose classpath issues.

Example
Don’t Do This
build.gradle.kts
plugins {
    `java-library`
}

dependencies {
    api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") // (1)
}
build.gradle
plugins {
    id 'java-library'
}

dependencies {
    api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") // (1)
}
  1. Redundant dependency in implementation scope.

Do This Instead
build.gradle.kts
plugins {
    `java-library`
}

dependencies {
    api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") // (1)
}
build.gradle
plugins {
    id 'java-library'
}

dependencies {
    api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") // (1)
}
  1. Declare dependency once

Declare Dependencies using a single GAV (group:artifact:version) String

When declaring dependencies without a version catalog, prefer using the single GAV string notation implementation("org.example:library:1.0"). Avoid using the named argument notation. The named argument notation has been deprecated and will no longer be supported starting in Gradle 10.

Explanation

All of these declarations will be treated equivalently when Gradle resolves dependencies. However, the single-string form is more concise, easier to read, and is widely adopted in the broader JVM ecosystem.

This format is also recommended by Maven Central in its documentation and usage examples, making it the most familiar and consistent style for developers across tools.

Example
Don’t Do This
build.gradle.kts
dependencies {
    implementation(group = "com.fasterxml.jackson.core", name = "jackson-databind", version = "32.17.0")  // (1)
    api(group = "com.google.guava", name = "guava", version = "32.1.2-jre") {
        exclude(group = "com.google.code.findbugs", module = "jsr305")  // (2)
    }
}
build.gradle
dependencies {
    implementation(group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.17.0') // (1)
    api(group: 'com.google.guava', name: 'guava', version: '32.1.2-jre') {
        exclude(group: 'com.google.code.findbugs', module: 'jsr305')    // (2)
    }
}
  1. Avoid the named argument notation when declaring dependencies

  2. Other modifiers methods and constraints like exclude are not included in this recommendation and can use named argument notation as needed

Do This Instead
build.gradle.kts
dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0") // (1)
    api("com.google.guava:guava:32.1.2-jre") {
        exclude(group = "com.google.code.findbugs", module = "jsr305")  // (2)
    }
}
build.gradle
dependencies {
    implementation('com.fasterxml.jackson.core:jackson-databind:2.17.0') // (1)
    api('com.google.guava:guava:32.1.2-jre') {
        exclude(group: 'com.google.code.findbugs', module: 'jsr305')    // (2)
    }
}
  1. Use the string notation instead when declaring dependencies

  2. Other modifiers methods and constraints like exclude are not included in this recommendation and can use named argument notation as needed

Use Content Filtering with multiple Repositories

When using multiple repositories in a build, use repository content filtering to ensure that dependencies are resolved from an appropriate repository.

Explanation

If your build declares more than one repository, you should declare content filters on these repositories to ensure you search for and obtain dependencies from the correct place.

Content filtering is necessary if you have a reason to restrict searching for a dependency to a particular repository, and can be a good idea even if acceptable dependency artifacts exist in multiple locations.

When possible, you should use the exclusiveContent feature to restrict dependencies to a particular known repository.

Content filtering has three main benefits:

  1. Performance, since you only query repositories for dependencies that should actually exist within them

  2. Security, by avoiding asking potentially every repository for every dependency (even ones they shouldn’t contain), you improve resiliency to supply chain attacks by avoiding leaking information about your dependencies to other repositories, or even downloading potentially malicious artifacts

  3. Reliability, by avoiding searching repositories that contain invalid or incorrect metadata for particular dependencies, which could result in obtaining incorrect transitive dependencies

Repositories will be searched for dependencies that pass their filters in the order they are declared. Often the last repository is declared without any filters in order to serve as a default fallback repository that is queried for any dependencies that don’t pass the filters present on the other repositories.

Warning
Carefully consider using content filtering with a fallback repository. This can pose a security risk, so make sure you fully trust the fallback repository. This setup can result in inadvertently (and silently) resolving dependencies from the fallback repository that were intended to come from filtered repositories if the dependencies were not available in those repositories.
Example
Don’t Do This

Don’t add multiple repositories without content filtering:

settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        mavenCentral()
        google()
    }
}
settings.gradle
dependencyResolutionManagement {
    repositories {
        mavenCentral()
        google()
    }
}
Do This Instead

Use content filtering to ensure that the proper repositories are searched first for the expected artifacts:

settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google {
            content {
                // Use this repository for androidx and GMS dependencies
                includeGroupByRegex("androidx.*")
                includeGroup("com.google.gms")
            }
        }
        // Specify the fallback repository last
        mavenCentral()
    }
}
settings.gradle
dependencyResolutionManagement {
    repositories {
        google {
            content {
                // Use this repository for androidx and GMS dependencies
                includeGroupByRegex("androidx.*")
                includeGroup("com.google.gms")
            }
        }
        // Specify the fallback repository last
        mavenCentral()
    }
}

In many cases, it is better to use exclusive content filtering, as it ensures that dependencies can only be found in the expected repository. If they are not present there, they will not be found at all.

settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        exclusiveContent {
            forRepository {
                google()
            }
            filter {
                // Only use this repository, and use this repository only, for androidx and GMS dependencies
                includeGroupByRegex("androidx.*")
                includeGroup("com.google.gms")
            }
        }
        // Specify the fallback repository last
        mavenCentral()
    }
}
settings.gradle
dependencyResolutionManagement {
    repositories {
        exclusiveContent {
            forRepository {
                google()
            }
            filter {
                // Only use this repository, and use this repository only, for androidx and GMS dependencies
                includeGroupByRegex("androidx.*")
                includeGroup("com.google.gms")
            }
        }
        // Specify the fallback repository last
        mavenCentral()
    }
}

Apply Exclusions Narrowly

When excluding transitive dependencies, apply exclusions as narrowly as possible.

Explanation

Sometimes you may need to exclude transitive dependencies that cause conflicts or issues in your project.

Exclusions can negatively affect dependency resolution performance. Applying exclusions as narrowly as possible minimizes this impact. It also reduces the risks of inadvertently and silently excluding dependencies that are required elsewhere in your build, and of accidental runtime dependency clashes.

Gradle offers several ways to exclude transitive dependencies. When excluding transitive dependencies, keep the scope as narrow as possible:

  • Attach exclusions to specific dependencies rather than applying them to an entire configuration.

  • Exclude a single module from a group, instead of excluding the entire group.

  • Avoid global exclusions using configurations.all { …​ } or configurations.configureEach { …​ }.

Example
Don’t Do This
build.gradle.kts
dependencies {
    implementation("org.apache.commons:commons-pool2:2.12.1") // (1)
    implementation("org.hibernate:hibernate-core:3.6.10.Final")
    // ... other dependencies ...
}

configurations {
    "implementation" {
        exclude(group = "cglib") // (2)
    }

    "implementation" {
        exclude(group = "org.ow2.asm", module = "asm-util") // (3)
    }
}

configurations.configureEach {
    exclude(group = "javassist", module = "javassist") // (4)
}
build.gradle
dependencies {
    implementation("org.apache.commons:commons-pool2:2.12.1") // (1)
    implementation("org.hibernate:hibernate-core:3.6.10.Final")
    // ... other dependencies ...
}

configurations {
    implementation {
        exclude(group: "cglib") // (2)
    }

    implementation {
        exclude(group: "org.ow2.asm", module: "asm-util") // (3)
    }
}

configurations.configureEach {
    exclude(group: "javassist", module: "javassist") // (4)
}
  1. The commons-pool2 dependency transitively includes cglib:cglib and org.ow2.asm:asm-util as optional dependencies - we want to exclude both. hibernate-core transitively optionally includes cglib:cglib, and also javaassist:javassist - we want to exclude both.

  2. This excludes every module provided by the cglib group from every dependency in the implementation configuration. If other current or future dependencies in this project rely on different modules from cglib, those dependencies may fail to resolve, leading to compilation or runtime errors.

  3. This excludes org.ow2.asm:asm-util from every dependency in the implementation configuration. If future dependencies rely on org.ow2.asm:asm-util, they may fail at compile or runtime because the module will be silently excluded.

  4. This excludes javaassist:javassist from all dependencies in all configurations, including those added by plugins or in the future, which carries the same risks as above, but on a larger scale.

Do This Instead

Exclude transitive dependencies as narrowly as possible, ideally on individual dependencies:

build.gradle.kts
dependencies {
    implementation("org.apache.commons:commons-pool2:2.12.1") { // (1)
        exclude(group = "cglib", module = "cglib") // (2)
        exclude(group = "org.ow2.asm", module = "asm-util")

    }
    implementation("org.hibernate:hibernate-core:3.6.10.Final") {
        exclude(group = "cglib", module = "cglib")
        exclude(group = "javassist", module = "javassist") // (3)
    }
    // ... other dependencies ...
}
build.gradle
dependencies {
    implementation("org.apache.commons:commons-pool2:2.12.1") { // (1)
        exclude(group: "cglib", module: "cglib") // (2)
        exclude(group: "org.ow2.asm", module: "asm-util")

    }
    implementation("org.hibernate:hibernate-core:3.6.10.Final") {
        exclude(group: "cglib", module: "cglib")
        exclude(group: "javassist", module: "javassist") // (3)
    }
    // ... other dependencies ...
}
  1. Exclusions are applied only to the dependency that actually transitively includes them.

  2. All exclusions apply to a particular module instead of every module from a particular group.

  3. javaassist:javassist is only excluded from the hibernate-core dependency - the only dependency that transitively includes it.

Though it may seem repetitive to exclude the same transitive dependencies from multiple dependencies, this approach is safer, more performant, less likely to cause accidental runtime crashes, and makes it clearer which dependencies are affected by each exclusion.

Always Declare Attributes on Consumable and Resolvable Configurations

When creating custom configurations that are meant to be consumed or resolved, always declare at least one attribute on them.

Explanation

Gradle uses variant-aware dependency resolution to select the appropriate variants of a project. Attributes serve as the primary matching mechanism between resolvable configurations (what a consumer needs) and consumable configurations (what a producer provides as variants).

Even if a producer project has only one consumable configuration, omitting attributes will cause variant-aware resolution to fail with the following error:

Unable to find a matching variant of project :producer:
  - No variants exist.

While Gradle can resolve configurations by selecting them directly by name, this approach is outdated:

  • It was deprecated for Maven repositories in Gradle 8.10 and removed in Gradle 9.0.0. It is only necessary for use with Ivy repositories. This mechanism can still be used for local project dependencies, but should be avoided.

  • Configuration names should be treated as internal implementation details. Relying on them for resolution forces the consumer to know too much about the producer’s internal structure.

When adding attributes to a configuration, common attributes to use include:

  • Category.CATEGORY_ATTRIBUTE - Indicates the category of the component (e.g., LIBRARY, DOCUMENTATION)

  • For JVM Projects:

    • Usage.USAGE_ATTRIBUTE - Indicates which classpath this variant represents (e.g., JAVA_RUNTIME, JAVA_API)

    • LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE - Indicates the packaging of the classes (e.g., JAR, CLASSES)

While attributes are not yet strictly required for all resolvable configurations, omitting them leads to fragile builds. Declaring sensible attributes ensures your build remains reliable as it scales and evolves.

Example
Don’t Do This

Avoid creating consumable and resolvable configurations without attributes:

In producer/build.gradle(.kts):

producer/build.gradle.kts
configurations.consumable("customElements") // (1)

val generateFile = tasks.register("generateFile") {
    val outputFile = layout.buildDirectory.file("custom/output.txt")
    outputs.file(outputFile)
    doLast {
        outputFile.get().asFile.writeText("Custom output from producer")
    }
}

artifacts {
    add("customElements", generateFile)
}
producer/build.gradle
configurations.consumable('customElements') // (1)

def generateFile = tasks.register('generateFile') {
    def outputFile = layout.buildDirectory.file('custom/output.txt')
    outputs.file(outputFile)
    doLast {
        outputFile.get().asFile.text = 'Custom output from producer'
    }
}

artifacts {
    customElements generateFile
}
  1. No attributes declared on the configuration in the producer build: This consumable configuration lacks attributes to describe what it provides.

In consumer/build.gradle(.kts):

consumer/build.gradle.kts
val customElementsDependencies = configurations.dependencyScope("customElementsDependencies")

dependencies {
    customElementsDependencies(project(path = ":producer", configuration = "customElements"))
}

val customElements = configurations.resolvable("customElements") { // (1)
    extendsFrom(customElementsDependencies.get())
}

tasks.register("resolveCustom") {
    inputs.files(customElements.get())
    doLast {
        inputs.files.forEach { file: File ->
            logger.lifecycle("Resolved: ${file.name}")
        }
    }
}
consumer/build.gradle
def customElementsDependenciesProvider = configurations.dependencyScope('customElementsDependencies')

dependencies {
    customElementsDependencies(project(path: ':producer', configuration: 'customElements'))
}

def customElements = configurations.resolvable('customElements') { // (1)
    extendsFrom(customElementsDependenciesProvider.get())
}

tasks.register('resolveCustom') {
    inputs.files(customElements)
    doLast {
        inputs.files.each { file ->
            logger.lifecycle("Resolved: ${file.name}")
        }
    }
}
  1. No attributes declared on the configuration in the consumer build: This resolvable configuration lacks attributes to describe what it needs.

This approach works only because we explicitly name the configuration in the dependency declaration (configuration = "customElements"). Resolving configurations by name should be avoided.

Do This Instead

Always declare attributes on consumable and resolvable configurations:

In producer/build.gradle(.kts):

producer/build.gradle.kts
val CUSTOM_ATTRIBUTE = Attribute.of("custom", String::class.java) // (1)
dependencies.attributesSchema.attribute(CUSTOM_ATTRIBUTE)

val generateFile = tasks.register("generateFile") {
    val outputFile = layout.buildDirectory.file("custom/output.txt")
    outputs.file(outputFile)
    doLast {
        outputFile.get().asFile.writeText("Custom output from producer")
    }
}

configurations {
    consumable("customElements") {
        attributes { // (2)
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
            attribute(CUSTOM_ATTRIBUTE, "my-custom-value")
        }
        outgoing {
            artifact(generateFile)
        }
    }
}
producer/build.gradle
def CUSTOM_ATTRIBUTE = Attribute.of("custom", String) // (1)
dependencies.attributesSchema.attribute(CUSTOM_ATTRIBUTE)

def generateFile = tasks.register('generateFile') {
    def outputFile = layout.buildDirectory.file('custom/output.txt')
    outputs.file(outputFile)
    doLast {
        outputFile.get().asFile.text = 'Custom output from producer'
    }
}

configurations {
    consumable('customElements') {
        attributes { // (2)
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
            attribute(CUSTOM_ATTRIBUTE, "my-custom-value")
        }
        outgoing {
            artifact(generateFile)
        }
    }
}
  1. New Attribute type defined in producer: The consumable configuration defines a new custom attribute that contains project-specific variant identification information. The new Attribute is also added to the Gradle Attribute schema to ensure type-safety.

  2. Attributes declared in producer: The consumable configuration adds Category = LIBRARY, and custom attribute = my-custom-value. These attributes will be used to identify this variant during resolution.

In consumer/build.gradle(.kts):

consumer/build.gradle.kts
val customElementsDependencies = configurations.dependencyScope("customElementsDependencies")

dependencies {
    customElementsDependencies(project(":producer")) // (1)
}

val CUSTOM_ATTRIBUTE = Attribute.of("custom", String::class.java) // (2)
dependencies.attributesSchema.attribute(CUSTOM_ATTRIBUTE)

val customElements = configurations.resolvable("customElements") {
    extendsFrom(customElementsDependencies.get())
    attributes { // (3)
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
        attribute(CUSTOM_ATTRIBUTE, "my-custom-value")
    }
}

tasks.register("resolveCustom") {
    inputs.files(customElements.get())
    doLast {
        inputs.files.forEach { file: File ->
            logger.lifecycle("Resolved: ${file.name}")
        }
    }
}
consumer/build.gradle
def customElementsDependenciesProvider = configurations.dependencyScope('customElementsDependencies')

dependencies {
    customElementsDependencies(project(':producer')) // (1)
}

def CUSTOM_ATTRIBUTE = Attribute.of("custom", String) // (2)
dependencies.attributesSchema.attribute(CUSTOM_ATTRIBUTE)


def customElements = configurations.resolvable('customElements') {
    extendsFrom(customElementsDependenciesProvider.get())
    attributes { // (3)
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
        attribute(CUSTOM_ATTRIBUTE, "my-custom-value")
    }
}


tasks.register('resolveCustom') {
    inputs.files(customElements)
    doLast {
        inputs.files.each { file ->
            logger.lifecycle("Resolved: ${file.name}")
        }
    }
}
  1. The project dependency does not use configuration name: It is no longer necessary to specify the configuration name in the dependency declaration, as variant-aware dependency resolution will be used to select the correct variant.

  2. New Attribute type defined in consumer: The resolvable configuration defines the same new custom attribute that contains project-specific variant identification information.

  3. Attributes declared in consumer: The resolvable configuration is identified as Category = LIBRARY, and custom attribute = my-custom-value.

By declaring compatible attributes, you decouple your projects from internal naming conventions, resulting in a more robust and maintainable build.

Best Practices for Tasks

Avoid DependsOn

The task dependsOn method should only be used for lifecycle tasks (tasks without task actions).

Explanation

Tasks with actions should declare their inputs and outputs so that Gradle’s up-to-date checking can automatically determine when these tasks need to be run or rerun.

Using dependsOn to link tasks is a much coarser-grained mechanism that does not allow Gradle to understand why a task requires a prerequisite task to run, or which specific files from a prerequisite task are needed. dependsOn forces Gradle to assume that every file produced by a prerequisite task is needed by this task. This can lead to unnecessary task execution and decreased build performance.

Example

Here is a task that writes output to two separate files:

build.gradle.kts
abstract class SimplePrintingTask : DefaultTask() {
    @get:OutputFile
    abstract val messageFile: RegularFileProperty

    @get:OutputFile
    abstract val audienceFile: RegularFileProperty

    @TaskAction // (1)
    fun run() {
        messageFile.get().asFile.writeText("Hello")
        audienceFile.get().asFile.writeText("World")
    }
}

tasks.register<SimplePrintingTask>("helloWorld") { // (2)
    messageFile.set(layout.buildDirectory.file("message.txt"))
    audienceFile.set(layout.buildDirectory.file("audience.txt"))
}
build.gradle
abstract class SimplePrintingTask extends DefaultTask {
    @OutputFile
    abstract RegularFileProperty getMessageFile()

    @OutputFile
    abstract RegularFileProperty getAudienceFile()

    @TaskAction // (1)
    void run() {
        messageFile.get().asFile.write("Hello")
        audienceFile.get().asFile.write("World")
    }
}

tasks.register("helloWorld", SimplePrintingTask) { // (2)
    messageFile = layout.buildDirectory.file("message.txt")
    audienceFile = layout.buildDirectory.file("audience.txt")
}
  1. Task With Multiple Outputs: helloWorld task prints "Hello" to its messageFile and "World" to its audienceFile.

  2. Registering the Task: helloWorld produces "message.txt" and "audience.txt" outputs.

Don’t Do This

If you want to translate the greeting in the message.txt file using another task, you could do this:

build.gradle.kts
abstract class SimpleTranslationTask : DefaultTask() {
    @get:InputFile
    abstract val messageFile: RegularFileProperty

    @get:OutputFile
    abstract val translatedFile: RegularFileProperty

    init {
        messageFile.convention(project.layout.buildDirectory.file("message.txt"))
        translatedFile.convention(project.layout.buildDirectory.file("translated.txt"))
    }

    @TaskAction // (1)
    fun run() {
        val message = messageFile.get().asFile.readText(Charsets.UTF_8)
        val translatedMessage = if (message == "Hello") "Bonjour" else "Unknown"

        logger.lifecycle("Translation: " + translatedMessage)
        translatedFile.get().asFile.writeText(translatedMessage)
    }
}

tasks.register<SimpleTranslationTask>("translateBad") {
    dependsOn(tasks.named("helloWorld")) // (2)
}
build.gradle
abstract class SimpleTranslationTask extends DefaultTask {
    @InputFile
    abstract RegularFileProperty getMessageFile()

    @OutputFile
    abstract RegularFileProperty getTranslatedFile()

    SimpleTranslationTask() {
        messageFile.convention(project.layout.buildDirectory.file("message.txt"))
        translatedFile.convention(project.layout.buildDirectory.file("translated.txt"))
    }

    @TaskAction // (1)
    void run() {
        def message = messageFile.get().asFile.text
        def translatedMessage = message == "Hello" ? "Bonjour" : "Unknown"

        logger.lifecycle("Translation: " + translatedMessage)
        translatedFile.get().asFile.write(translatedMessage)
    }
}

tasks.register("translateBad", SimpleTranslationTask) {
    dependsOn(tasks.named("helloWorld")) // (2)
}
  1. Translation Task Setup: translateBad requires helloWorld to run first to produce the message file otherwise it will fail with an error as the file does not exist.

  2. Explicit Task Dependency: Running translateBad will cause helloWorld to run first, but Gradle does not understand why.

Do This Instead

Instead, you should explicitly wire task inputs and outputs like this:

build.gradle.kts
abstract class SimpleTranslationTask : DefaultTask() {
    @get:InputFile
    abstract val messageFile: RegularFileProperty

    @get:OutputFile
    abstract val translatedFile: RegularFileProperty

    init {
        messageFile.convention(project.layout.buildDirectory.file("message.txt"))
        translatedFile.convention(project.layout.buildDirectory.file("translated.txt"))
    }

    @TaskAction // (1)
    fun run() {
        val message = messageFile.get().asFile.readText(Charsets.UTF_8)
        val translatedMessage = if (message == "Hello") "Bonjour" else "Unknown"

        logger.lifecycle("Translation: " + translatedMessage)
        translatedFile.get().asFile.writeText(translatedMessage)
    }
}

tasks.register<SimpleTranslationTask>("translateGood") {
    inputs.file(tasks.named<SimplePrintingTask>("helloWorld").map { messageFile }) // (1)
}
build.gradle
abstract class SimpleTranslationTask extends DefaultTask {
    @InputFile
    abstract RegularFileProperty getMessageFile()

    @OutputFile
    abstract RegularFileProperty getTranslatedFile()

    SimpleTranslationTask() {
        messageFile.convention(project.layout.buildDirectory.file("message.txt"))
        translatedFile.convention(project.layout.buildDirectory.file("translated.txt"))
    }

    @TaskAction // (1)
    void run() {
        def message = messageFile.get().asFile.text
        def translatedMessage = message == "Hello" ? "Bonjour" : "Unknown"

        logger.lifecycle("Translation: " + translatedMessage)
        translatedFile.get().asFile.write(translatedMessage)
    }
}

tasks.register("translateGood", SimpleTranslationTask) {
    inputs.file(tasks.named("helloWorld", SimplePrintingTask).map { messageFile }) // (1)
}
  1. Register Implicit Task Dependency: translateGood requires only one of the files that is produced by helloWorld.

Gradle now understands that translateGood requires helloWorld to have run successfully first because it needs to create the message.txt file which is then used by the translation task. Gradle can use this information to optimize task scheduling. Using the map method avoids eagerly retrieving the helloWorld task until the output is needed to determine if translateGood should run.

Favor @CacheableTask and @DisableCachingByDefault over cacheIf(Spec) and doNotCacheIf(String, Spec)

The cacheIf and doNotCacheIf methods should only be used in situations where the cacheability of a task varies between different task instances or cannot be determined until the task is executed by Gradle. You should instead favor annotating the task class itself with @CacheableTask annotation for any task that is always cacheable. Likewise, the @DisableCachingByDefault should be used to always disable caching for all instances of a task type.

Explanation

Annotating a task type will ensure that each task instance of that type is properly understood by Gradle to be cacheable (or not cacheable). This removes the need to remember to configure each of the task instances separately in build scripts.

Using the annotations also documents the intended cacheability of the task type within its own source, appearing in Javadoc and making the task’s behavior clear to other developers without requiring them to inspect each task instance’s configuration. It is also slightly more efficient than running a test to determine cacheability.

Remember that only tasks that produce reproducible and relocatable output should be marked as @CacheableTask.

Example
Don’t Do This

If you want to reuse the output of a task, you shouldn’t do this:

build.gradle.kts
abstract class BadCalculatorTask : DefaultTask() { // (1)
    @get:Input
    abstract val first: Property<Int>

    @get:Input
    abstract val second: Property<Int>

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun run() {
        val result = first.get() + second.get()
        logger.lifecycle("Result: $result")
        outputFile.get().asFile.writeText(result.toString())
    }
}

tasks.register<Delete>("clean") {
    delete(layout.buildDirectory)
}

tasks.register<BadCalculatorTask>("addBad1") {
    first = 10
    second = 25
    outputFile = layout.buildDirectory.file("badOutput.txt")
    outputs.cacheIf { true } // (2)
}

tasks.register<BadCalculatorTask>("addBad2") { // (3)
    first = 3
    second = 7
    outputFile = layout.buildDirectory.file("badOutput2.txt")
}
build.gradle
abstract class BadCalculatorTask extends DefaultTask {
    @Input
    abstract Property<Integer> getFirst()

    @Input
    abstract Property<Integer> getSecond()

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void run() {
        def result = first.get() + second.get()
        logger.lifecycle("Result: " + result)
        outputFile.get().asFile.write(result.toString())
    }
}

tasks.register("clean", Delete) {
    delete layout.buildDirectory
}

tasks.register("addBad1", BadCalculatorTask) {
    first = 10
    second = 25
    outputFile = layout.buildDirectory.file("badOutput.txt")
    outputs.cacheIf { true }
}

tasks.register("addBad2", BadCalculatorTask) {
    first = 3
    second = 7
    outputFile = layout.buildDirectory.file("badOutput2.txt")
}
  1. Define a Task: The BadCalculatorTask type is deterministic and produces relocatable output, but is not annotated.

  2. Mark the Task Instance as Cacheable: This example shows how to mark a specific task instance as cacheable.

  3. Forget to Mark a Task Instance as Cacheable: Unfortunately, the addBad2 instance of the BadCalculatorTask type is not marked as cacheable, so it will not be cached, despite behaving the same as addBad1.

Do This Instead

As this task meets the criteria for cacheability (we can imagine a more complex calculation in the @TaskAction that would benefit from automatic work avoidance via caching), you should mark the task type itself as cacheable like this:

build.gradle.kts
@CacheableTask // (1)
abstract class GoodCalculatorTask : DefaultTask() {
    @get:Input
    abstract val first: Property<Int>

    @get:Input
    abstract val second: Property<Int>

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun run() {
        val result = first.get() + second.get()
        logger.lifecycle("Result: $result")
        outputFile.get().asFile.writeText(result.toString())
    }
}

tasks.register<Delete>("clean") {
    delete(layout.buildDirectory)
}

tasks.register<GoodCalculatorTask>("addGood1") { // (2)
    first = 10
    second = 25
    outputFile = layout.buildDirectory.file("goodOutput.txt")
}

tasks.register<GoodCalculatorTask>("addGood2") {
    first = 3
    second = 7
    outputFile = layout.buildDirectory.file("goodOutput2.txt")
}
build.gradle
@CacheableTask // (1)
abstract class GoodCalculatorTask extends DefaultTask {
    @Input
    abstract Property<Integer> getFirst()

    @Input
    abstract Property<Integer> getSecond()

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void run() {
        def result = first.get() + second.get()
        logger.lifecycle("Result: " + result)
        outputFile.get().asFile.write(result.toString())
    }
}

tasks.register("clean", Delete) {
    delete layout.buildDirectory
}

tasks.register("addGood1", GoodCalculatorTask) {
    first = 10
    second = 25
    outputFile = layout.buildDirectory.file("goodOutput.txt")
}

tasks.register("addGood2", GoodCalculatorTask) { // (2)
    first = 3
    second = 7
    outputFile = layout.buildDirectory.file("goodOutput2.txt")
}
  1. Annotate the Task Type: Applying the @CacheableTask to a task type informs Gradle that instances of this task should always be cached.

  2. Nothing Else Needs To Be Done: When we register task instances, nothing else needs to be done - Gradle knows to cache them.

Do not call get() on a Provider outside a Task action

When configuring tasks and extensions do not call get() on a provider, use map(), or flatMap() instead.

Explanation

A provider should be evaluated as late as possible. Calling get() forces immediate evaluation, which can trigger unintended side effects, such as:

  • The value of the provider becomes an input to configuration, causing potential configuration cache misses.

  • The value may be evaluated too early, meaning you might not be using the final or correct value of the property. This may lead to painful and hard to debug ordering issues.

  • It breaks Gradle’s ability to build dependencies and to track task inputs and outputs, making automatic task dependency wiring impossible. See Working with task inputs and outputs

It is preferable to avoid explicitly evaluating a Provider at all, and deferring to map/flatMap to connect Providers to Providers implicitly.

Example

Here is a task that writes an input String to a file:

build.gradle.kts
abstract class MyTask : DefaultTask() {
    @get:Input
    abstract val myInput: Property<String>

    @get:OutputFile
    abstract val myOutput: RegularFileProperty

    @TaskAction
    fun doAction() {
        val outputFile = myOutput.get().asFile
        val outputText = myInput.get() // (1)
        println(outputText)
        outputFile.writeText(outputText)
    }
}

val currentEnvironment: Provider<String> = providers.gradleProperty("currentEnvironment").orElse("234") // (2)
build.gradle
abstract class MyTask extends DefaultTask {
    @Input
    abstract Property<String> getMyInput()

    @OutputFile
    abstract RegularFileProperty getMyOutput()

    @TaskAction
    void doAction() {
        def outputFile = myOutput.get().asFile
        def outputText = myInput.get() // (1)
        println(outputText)
        outputFile.write(outputText)
    }
}

Provider<String> currentEnvironment = providers.gradleProperty("currentEnvironment").orElse("234") // (2)
  1. Using Provider.get() in the task action

  2. Gradle property that we wish to use as input

Don’t Do This

You could call get() at configuration time to set up this task:

build.gradle.kts
tasks.register<MyTask>("avoidThis") {
    myInput = "currentEnvironment=${currentEnvironment.get()}"  // (1)
    myOutput = layout.buildDirectory.get().asFile.resolve("output-avoid.txt")  // (2)
}
build.gradle
tasks.register("avoidThis", MyTask) {
    myInput = "currentEnvironment=${currentEnvironment.get()}"  // (1)
    myOutput = new File(layout.buildDirectory.get().asFile, "output-avoid.txt")  // (2)
}
  1. Reading the value of currentEnvironment at configuration time: This value might change by the time the task start executing.

  2. Reading the value of buildDirectory at configuration time: This value might change by the time the task start executing.

Do This Instead

Instead, you should explicitly wire task inputs and outputs like this:

build.gradle.kts
tasks.register<MyTask>("doThis") {
    myInput = currentEnvironment.map { "currentEnvironment=$it" }  // (1)
    myOutput = layout.buildDirectory.file("output-do.txt")  // (2)
}
build.gradle
tasks.register("doThis", MyTask) {
    myInput = currentEnvironment.map { "currentEnvironment=$it" }  // (1)
    myOutput = layout.buildDirectory.file("output-do.txt")  // (2)
}
  1. Using map() to transform currentEnvironment: map transform runs only when the value is read.

  2. Using file() to create a new Provider<RegularFile>: the value of the buildDirectory is only checked when the value of the provider is read.

Group and Describe custom Tasks

When defining custom task types or registering ad-hoc tasks, always set a clear group and description.

Explanation

A good group name is short, lowercase, and reflects the purpose or domain of the task. For example: documentation, verification, release, or publishing.

Before creating a new group, look for an existing group name that aligns with your task’s intent. It’s often better to reuse an established category to keep the task output organized and familiar to users.

This information is used in the Tasks Report (shown via ./gradlew tasks) to group and describe available tasks in a readable format.

Providing a group and description ensures that your tasks are:

  • Displayed clearly in the report

  • Categorized appropriately

  • Understandable to other users (and to your future self)

Note
Tasks with no group are hidden from the Tasks Report unless --all is specified.
Example
Don’t Do This

Tasks without a group appear under the "other" category in ./gradlew tasks --all output, making them harder to locate:

app/build.gradle.kts
tasks.register("generateDocs") {
    // Build logic to generate documentation
}
app/build.gradle
tasks.register('generateDocs') {
    // Build logic to generate documentation
}
$ gradlew :app:tasks --all

> Task :app:tasks

------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------

Other tasks
-----------
compileJava - Compiles main Java source.
compileTestJava - Compiles test Java source.
generateDocs
processResources - Processes main resources.
processTestResources - Processes test resources.
startScripts - Creates OS specific scripts to run the project as a JVM application.
Do this Instead

When defining custom tasks, always assign a clear group and description:

app/build.gradle.kts
tasks.register("generateDocs") {
    group = "documentation"
    description = "Generates project documentation from source files."
    // Build logic to generate documentation
}
app/build.gradle
tasks.register('generateDocs') {
    group = 'documentation'
    description = 'Generates project documentation from source files.'
    // Build logic to generate documentation
}
$ gradlew :app:tasks --all

> Task :app:tasks

------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------

Documentation tasks
-------------------
generateDocs - Generates project documentation from source files.
javadoc - Generates Javadoc API documentation for the 'main' feature.

Avoid using eager APIs on File Collections

When working with Gradle’s file collection types, be careful to avoid triggering dependency resolution during the configuration phase.

Explanation

Gradle’s Configuration and FileCollection types extend the JDK’s Collection<File> interface.

However, calling some available methods from this interface—such as .size(), .isEmpty(), getFiles(), asPath(), or .toList()—on these Gradle types will implicitly trigger resolution of their dependencies. The same is possible using Kotlin stdlib collection extension methods or Groovy GDK collection extensions. Converting a Configuration to a Set<File> also discards any implicit task dependencies it carries.

You should avoid using these methods when configuring your build. Instead, use the methods defined directly on the Gradle interfaces - this is a necessary first step towards preventing eager resolutions. Be sure to use lazy types and APIs that defer resolution to wire task dependencies and inputs correctly. Some methods that cause resolution are not obvious. Be sure to check the actual behavior when using configurations in an atypical way.

Example
Don’t Do This
build.gradle.kts
abstract class FileCounterTask: DefaultTask() {
    @get:InputFiles
    abstract val countMe: ConfigurableFileCollection

    @TaskAction
    fun countFiles() {
        logger.lifecycle("Count: " + countMe.files.size)
    }
}

tasks.register<FileCounterTask>("badCountingTask") {
    if (!configurations.runtimeClasspath.get().isEmpty()) { // (1)
        logger.lifecycle("Resolved: " + (configurations.runtimeClasspath.get().state == RESOLVED))
        countMe.from(configurations.runtimeClasspath)
    }
}

tasks.register<FileCounterTask>("badCountingTask2") {
    val files = configurations.runtimeClasspath.get().files // (2)
    countMe.from(files)
    logger.lifecycle("Resolved: " + (configurations.runtimeClasspath.get().state == RESOLVED))
}

tasks.register<FileCounterTask>("badCountingTask3") {
    val files = configurations.runtimeClasspath.get() + layout.projectDirectory.file("extra.txt") // (3)
    countMe.from(files)
    logger.lifecycle("Resolved: " + (configurations.runtimeClasspath.get().state == RESOLVED))
}

tasks.register<Zip>("badZippingTask") { // (4)
    if (!configurations.runtimeClasspath.get().isEmpty()) {
        logger.lifecycle("Resolved: " + (configurations.runtimeClasspath.get().state == RESOLVED))
        from(configurations.runtimeClasspath)
    }
}
build.gradle
abstract class FileCounterTask extends DefaultTask {
    @InputFiles
    abstract ConfigurableFileCollection getCountMe();

    @TaskAction
    void countFiles() {
        logger.lifecycle("Count: " + countMe.files.size())
    }
}

tasks.register("badCountingTask", FileCounterTask) {
    if (!configurations.runtimeClasspath.isEmpty()) { // (1)
        logger.lifecycle("Resolved: " + (configurations.runtimeClasspath.state == RESOLVED))
        countMe.from(configurations.runtimeClasspath)
    }
}

tasks.register("badCountingTask2", FileCounterTask) {
    def files = configurations.runtimeClasspath.files // (2)
    countMe.from(files)
    logger.lifecycle("Resolved: " + (configurations.runtimeClasspath.state == RESOLVED))
}

tasks.register("badCountingTask3", FileCounterTask) {
    def files = configurations.runtimeClasspath + layout.projectDirectory.file("extra.txt") // (3)
    countMe.from(files)
    logger.lifecycle("Resolved: " + (configurations.runtimeClasspath.state == RESOLVED))
}

tasks.register("badZippingTask", Zip) { // (4)
    if (!configurations.runtimeClasspath.isEmpty()) {
        logger.lifecycle("Resolved: " + (configurations.runtimeClasspath.state == RESOLVED))
        from(configurations.runtimeClasspath)
    }
}
  1. isEmpty() causes resolution: Many seemingly harmless Collection API methods like isEmpty() cause Gradle to resolve dependencies.

  2. Accessing files directly: Using getFiles() to access the files in a Configuration will also cause Gradle to resolve the file collection.

  3. Adding a file via plus operator: Using the plus operator will force the runtimeClasspath configuration to be resolved implicitly. The implementation of Configuration doesn’t override the plus operator for regular files, therefore it falls back to using the eager API, which causes resolution.

  4. Be careful with indirect inputs: Some built-in tasks, for example subtypes of AbstractCopyTask like Zip, allow adding inputs indirectly and can have the same problems.

Do This Instead

To avoid issues, always defer resolution until the execution phase. Use APIs that support lazy evaluation.

build.gradle.kts
abstract class FileCounterTask: DefaultTask() {
    @get:InputFiles
    abstract val countMe: ConfigurableFileCollection

    @TaskAction
    fun countFiles() {
        logger.lifecycle("Count: " + countMe.files.size)
    }
}

tasks.register<FileCounterTask>("goodCountingTask") {
    countMe.from(configurations.runtimeClasspath) // (1)
    countMe.from(layout.projectDirectory.file("extra.txt"))
    logger.lifecycle("Resolved: " + (configurations.runtimeClasspath.get().state == RESOLVED))
}
build.gradle
abstract class FileCounterTask extends DefaultTask {
    @InputFiles
    abstract ConfigurableFileCollection getCountMe();

    @TaskAction
    void countFiles() {
        logger.lifecycle("Count: " + countMe.files.size())
    }
}

tasks.register("goodCountingTask", FileCounterTask) {
    countMe.from(configurations.runtimeClasspath) // (1)
    countMe.from(layout.projectDirectory.file("extra.txt")) // (2)
    logger.lifecycle("Resolved: " + (configurations.runtimeClasspath.state == RESOLVED))
}
  1. Add configurations to Task properties or Specs directly: This will defer resolution until the task is executed.

  2. Add files to Specs separately: This allows combining files with file collections without triggering implicit resolutions.

Don’t resolve Configurations before Task Execution

Resolving configurations before the task execution phase can lead to incorrect results and slower builds.

Explanation

Resolving a configuration - either directly via calling its resolve() method or indirectly via accessing its set of artifacts - returns a set of files that does not preserve references to the tasks that produced those files.

Configurations are file collections and can be added to @InputFiles properties on other tasks. It is important to do this correctly to avoid breaking automatic task dependency wiring between a consumer task and any tasks that are implicitly required to produce the artifacts being consumed. For example, if a configuration contains a project dependency, Gradle knows that consumers of the configuration must first run any tasks that produce that project’s artifacts.

In addition to correctness concerns, resolving configurations during the configuration phase can slow down the build, even when running unrelated tasks (e.g., help) that don’t require the resolved dependencies.

Example
Don’t Do This
build.gradle.kts
dependencies {
    runtimeOnly(project(":lib")) // (1)
}

abstract class BadClasspathPrinter : DefaultTask() {
    @get:InputFiles
    var classpath: Set<File> = emptySet() // (2)

    private fun calculateDigest(fileOrDirectory: File): Int {
        require(fileOrDirectory.exists()) { "File or directory $fileOrDirectory doesn't exist" }
        return 0 // actual implementation is stripped
    }

    @TaskAction
    fun run() {
        logger.lifecycle(
            classpath.joinToString("\n") {
                val digest = calculateDigest(it) // (3)
                "$it#$digest"
            }
        )
    }
}

tasks.register("badClasspathPrinter", BadClasspathPrinter::class) {
    classpath = configurations.named("runtimeClasspath").get().resolve() // (4)
}
build.gradle
dependencies {
    runtimeOnly(project(":lib")) // (1)
}

abstract class BadClasspathPrinter extends DefaultTask {
    @InputFiles
    Set<File> classpath = [] as Set // (2)

    protected int calculateDigest(File fileOrDirectory) {
        if (!fileOrDirectory.exists()) {
            throw new IllegalArgumentException("File or directory $fileOrDirectory doesn't exist")
        }
        return 0 // actual implementation is stripped
    }

    @TaskAction
    void run() {
        logger.lifecycle(
            classpath.collect { file ->
                def digest = calculateDigest(file) // (3)
                "$file#$digest"
            }.join("\n")
        )
    }
}

tasks.register("badClasspathPrinter", BadClasspathPrinter) {
    classpath = configurations.named("runtimeClasspath").get().resolve() // (4)
}
  1. Add project dependency: The :lib project must be built in order to resolve the runtime classpath successfully.

  2. Declare input property as Set of files: A simple Set input doesn’t track task dependencies.

  3. Dependency artifacts are used to calculate digest: Artifacts from the already resolved classpath are used to calculate the digest.

  4. Resolve runtimeClasspath: The implicit task dependency on :library:jar task is lost here when the configuration is resolved prior to task execution. The lib project will not be built when the :app:badClasspathPrinter task is run, leading to a failure in calculateDigest because the lib.jar file will not exist.

Do This Instead

To avoid issues, always defer resolution to the execution phase by using lazy APIs like FileCollection.

build.gradle.kts
dependencies {
    runtimeOnly(project(":lib")) // (1)
}

abstract class GoodClasspathPrinter : DefaultTask() {
    @get:InputFiles
    abstract val classpath: ConfigurableFileCollection // (2)

    private fun calculateDigest(fileOrDirectory: File): Int {
        require(fileOrDirectory.exists()) { "File or directory $fileOrDirectory doesn't exist" }
        return 0 // actual implementation is stripped
    }

    @TaskAction
    fun run() {
        logger.lifecycle(
            classpath.joinToString("\n") {
                val digest = calculateDigest(it) // (3)
                "$it#$digest"
            }
        )
    }
}

tasks.register("goodClasspathPrinter", GoodClasspathPrinter::class.java) {
    classpath.from(configurations.named("runtimeClasspath")) // (4)
}
build.gradle
dependencies {
    runtimeOnly(project(":lib")) // (1)
}

abstract class GoodClasspathPrinter extends DefaultTask {

    @InputFiles
    abstract ConfigurableFileCollection getClasspath() // (2)

    protected int calculateDigest(File fileOrDirectory) {
        if (!fileOrDirectory.exists()) {
            throw new IllegalArgumentException("File or directory $fileOrDirectory doesn't exist")
        }
        return 0 // actual implementation is stripped
    }

    @TaskAction
    void run() {
        logger.lifecycle(
            classpath.collect { file ->
                def digest = calculateDigest(file) // (3)
                "$file#$digest"
            }.join("\n")
        )
    }
}

tasks.register("goodClasspathPrinter", GoodClasspathPrinter) {
    classpath.from(configurations.named("runtimeClasspath")) // (4)
}
  1. Write to a file in the output directory: This is the same.

  2. Declare input files property as ConfigurableFileCollection: This lazy collection type will track task dependencies.

  3. Dependency artifacts are resolved to calculate digest: The classpath will be resolved at execution time to calculate the digest.

  4. Configuration is passed to input property directly: Using from causes the configuration to be lazily wired to the input property. The configuration will be resolved when necessary, preserving task dependencies. The output reveals that the lib project is now built when the :app:goodClasspathPrinter task is run because of the implicit task dependency, and the lib.jar file is found when calculating the digest.

Use @PathSensitivity.NONE for file inputs and @PathSensitivity.RELATIVE for directories

Use @PathSensitivity.NONE for file inputs and @PathSensitivity.RELATIVE for directory inputs.

Explanation

Tasks should generally care about the contents of their input files, not their location on disk.

When annotating file-based input properties (for example, @InputFile or @InputFiles collections), use @PathSensitivity.NONE. This tells Gradle to ignore the path and only consider the file contents when determining whether a task is up-to-date.

For directory-based inputs (for example, @InputDirectory or @InputFiles collections), use @PathSensitivity.RELATIVE. This tells Gradle to also consider only the name of the directory (ignoring its absolute location) and to relativize the paths of all files within that directory to it when doing up-to-date checks.

Using PathSensitivity.NAME_ONLY or @PathSensitivity.ABSOLUTE is generally incorrect.

PathSensitivity.NAME_ONLY tells Gradle to consider a file’s name in addition to its contents, which is rarely useful.

@PathSensitivity.ABSOLUTE tells Gradle to consider a file’s complete absolute path. This prevents Build Cache and Configuration Cache hits across different machines or checkout locations, making your build non-relocatable. It can also lead to confusing behavior where the same build produces different task outcomes when run from different directories. If no @PathSensitive annotation is provided, PathSensitivity.ABSOLUTE is the default.

Example
Don’t Do This
build.gradle.kts
abstract class AnimalSearchTask : DefaultTask() {
    @get:Input
    abstract val find: Property<String>

    @get:InputFile
    @get:PathSensitive(PathSensitivity.ABSOLUTE) // (1)
    abstract val candidatesFile: RegularFileProperty

    @get:OutputFile
    abstract val resultsFile: RegularFileProperty

    @TaskAction
    fun search() {
        if (candidatesFile.get().getAsFile().readLines().contains(find.get())) {
            val msg = "Found a " + find.get() + "!"
            getLogger().lifecycle(msg)
            resultsFile.get().asFile.writeText(msg)
        }
    }
}

val useAlternateInput = providers.gradleProperty("useAlternateInput").isPresent()

val copyTask = tasks.register<Copy>("copy") {
    from(layout.projectDirectory.file("candidates.txt"))
    destinationDir = (if (useAlternateInput) { layout.buildDirectory.dir("alternateSearchInput") } else { layout.buildDirectory.dir("searchInput") }).get().asFile
}

tasks.register<AnimalSearchTask>("search") {
    find = "cat"
    candidatesFile.fileProvider(copyTask.map { File(it.destinationDir, "candidates.txt") })
    resultsFile = layout.buildDirectory.file("searchOutput/results.txt")
    dependsOn(copyTask)
}
build.gradle
abstract class AnimalSearchTask extends DefaultTask {
    @Input
    abstract Property<String> getFind()

    @InputFile
    @PathSensitive(PathSensitivity.ABSOLUTE) // (1)
    abstract RegularFileProperty getCandidatesFile()

    @OutputFile
    abstract RegularFileProperty getResultsFile()

    @TaskAction
    void search() {
        if (candidatesFile.get().getAsFile().readLines().contains(find.get())) {
            def msg = "Found a " + find.get() + "!"
            getLogger().lifecycle(msg)
            resultsFile.get().asFile.text = msg
        }
    }
}

def useAlternateInput = providers.gradleProperty("useAlternateInput").isPresent()

def copyTask = tasks.register("copy", Copy) {
    from(layout.projectDirectory.file("candidates.txt"))
    destinationDir = (useAlternateInput ? layout.buildDirectory.dir("alternateSearchInput") : layout.buildDirectory.dir("searchInput")).get().asFile // (2)
}

tasks.register("search", AnimalSearchTask) {
    find = "cat"
    candidatesFile.fileProvider(copyTask.map { new File(it.destinationDir, "candidates.txt") }) // (3)
    resultsFile = layout.buildDirectory.file("searchOutput/results.txt")
    dependsOn(copyTask)
}
  1. The AnimalSearchTask task type uses a file input property annotated with @PathSensitivity.ABSOLUTE. This means that the absolute path of the input file is used to determine if the task is UP-TO-DATE or if it can be loaded from cache. Yet the path is irrelevant for the operation of the task’s @TaskAction, which only cares about file contents.

  2. The copy task will move the exact same candidates.txt to different destination directories, depending on if the useAlternateInput project property is set.

  3. The search task is wired to use as input whatever the file the copy task moved to its destinationDir. Despite the contents of the file being the same, when enabling the -PuseAlternateInput after a successful build, the search task will be out-of-date due to its different directory, and the search will be rerun.

Do this Instead
build.gradle.kts
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE) // (1)
abstract val candidatesFile: RegularFileProperty
build.gradle
@InputFile
@PathSensitive(PathSensitivity.NONE) // (1)
abstract RegularFileProperty getCandidatesFile()
  1. Everything remains the same, except that the input property is now annotated with @PathSensitivity.NONE. Only the contents of this input file matter to this task. When the search task is rerun with -PuseAlternateInput, it remains UP-TO-DATE.

Use unique output files and directories

Overlapping output files or directories cause tasks to rerun unnecessarily and waste work.

Explanation

Gradle tracks all output files and directories declared by tasks to decide whether a task needs to be rerun. For example, if the contents of a task’s output directory change after its last execution, Gradle will rerun that task.

Ensuring that each task uses its own unique output files and directories, both within a project and across the entire build, prevents unnecessary work.

Example
Don’t Do This
build.gradle.kts
abstract class GreetingTask : DefaultTask() {
    @get:Input
    abstract val type: Property<String>
    @get:OutputDirectory
    abstract val outputDirectory: DirectoryProperty

    @TaskAction
    fun run() {
        val outFileName = type.get() + ".txt"
        val message = "Hello " + type.get()
        outputDirectory.file(outFileName).get().asFile.writeText(message) // (1)
    }
}

abstract class ConsumerTask : DefaultTask() {
    @get:InputDirectory
    abstract val inputDirectory: DirectoryProperty
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun run() {
        val message = inputDirectory.get().file("a.txt").asFile.readText() // (2)
        outputFile.get().asFile.writeText(message)
    }
}

val greeterA = tasks.register<GreetingTask>("greeterA") {
    type = "a"
    outputDirectory = layout.buildDirectory.dir("greetings") // (3)
}
tasks.register<GreetingTask>("greeterB") {
    type = "b"
    outputDirectory = layout.buildDirectory.dir("greetings") // (4)
}

tasks.register<ConsumerTask>("consumer") {
    inputDirectory = greeterA.flatMap { it.outputDirectory } // (5)
    outputFile = layout.buildDirectory.file("consumerOutput.txt")
}
build.gradle
abstract class GreetingTask extends DefaultTask {
    @Input
    abstract Property<String> getType()
    @OutputDirectory
    abstract DirectoryProperty getOutputDirectory()

    @TaskAction
    void run() {
        def outFileName = type.get() + ".txt"
        def message = "Hello " + type.get()
        outputDirectory.file(outFileName).get().asFile.text = message // (1)
    }
}

abstract class ConsumerTask extends DefaultTask {
    @InputDirectory
    abstract DirectoryProperty getInputDirectory()
    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void run() {
        def message = inputDirectory.get().file("a.txt").asFile.text // (2)
        outputFile.get().asFile.write(message)
    }
}

def greeterA = tasks.register("greeterA", GreetingTask) {
    type = "a"
    outputDirectory = layout.buildDirectory.dir("greetings") // (3)
}
tasks.register("greeterB", GreetingTask) {
    type = "b"
    outputDirectory = layout.buildDirectory.dir("greetings") // (4)
}

tasks.register("consumer", ConsumerTask) {
    inputDirectory = greeterA.flatMap { it.outputDirectory } // (5)
    outputFile = layout.buildDirectory.file("consumerOutput.txt")
}
  1. Write to a file in the output directory: This task produces a single file in the outputDirectory, named based on the type input.

  2. Read a specific file in the input directory: This task only needs to read a single a.txt file in the input directory.

  3. Set output directory: Sets outputDirectory to a subdirectory in buildDirectory.

  4. Set output directory: Same as above, using the same shared greetings directory.

  5. Wire greeterA to consumer: Makes sure that greeterA runs and produces the output directory before it is used by consumer.

With this setup, if you run the consumer task, then greeterB, the consumer task will be invalidated.

The next time consumer is run it will not be UP-TO-DATE and will have to run again despite not using the output from greeterB.

This happens because greeterB changes the contents of the shared output directory greetings, which is an output of greeterA that consumer depends on (despite consumer only actually using the unchanged a.txt file in that directory).

Do This Instead

To avoid issues, avoid using shared task output directories and files.

Instead, tasks should only declare the exact outputs and consume the exact inputs that they actually produce and consume.

The simplest change to make here is to use distinct output directories for each GreetingTask. This alone is sufficient to fix the problem.

build.gradle.kts
val greeterA = tasks.register<GreetingTask>("greeterA") {
    type = "a"
    outputDirectory = layout.buildDirectory.dir("greetings")
}
tasks.register<GreetingTask>("greeterB") {
    type = "b"
    outputDirectory = layout.buildDirectory.dir("greetings-2") // (1)
}
build.gradle
def greeterA = tasks.register("greeterA", GreetingTask) {
    type = "a"
    outputDirectory = layout.buildDirectory.dir("greetings")
}
tasks.register("greeterB", GreetingTask) {
    type = "b"
    outputDirectory = layout.buildDirectory.dir("greetings-2") // (1)
}
  1. Set unique output directories: Each GreetingTask is assigned its own unique output directory based on the type input.

Now when running consumer task, then greeterB, then consumer task remains UP-TO-DATE as Gradle knows that it is not using the output from greeterB, since greeterA and greeterB write to distinct output directories.

However, a more complete and idiomatic approach realizes that:

  1. Tasks that produce single output files should make this clear from the type of their @Output properties.

  2. Tasks that only consume single input files should make this clear from the type of their @Input properties.

build.gradle.kts
abstract class GreetingTask : DefaultTask() {
    @get:Input
    abstract val type: Property<String>
    @get:OutputFile
    abstract val outputFile: RegularFileProperty // (1)

    @TaskAction
    fun run() {
        val message = "Hello " + type.get()
        outputFile.get().asFile.writeText(message)
    }
}

abstract class ConsumerTask : DefaultTask() {
    @get:InputFile
    abstract val inputFile: RegularFileProperty // (2)
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun run() {
        val message = inputFile.get().asFile.readText()
        outputFile.get().asFile.writeText(message)
    }
}

val greeterA = tasks.register<GreetingTask>("greeterA") {
    type = "a"
    outputFile = layout.buildDirectory.dir("greetings").map { it.file("a.txt") } // (3)
}
tasks.register<GreetingTask>("greeterB") {
    type = "b"
    outputFile = layout.buildDirectory.dir("greetings").map { it.file("b.txt") }
}

tasks.register<ConsumerTask>("consumer") {
    inputFile = greeterA.map { it.outputFile.get() } // (4)
    outputFile = layout.buildDirectory.file("consumerOutput.txt")
}
build.gradle
abstract class GreetingTask extends DefaultTask {
    @Input
    abstract Property<String> getType()
    @OutputFile
    abstract RegularFileProperty getOutputFile() // (1)

    @TaskAction
    void run() {
        def message = "Hello " + type.get()
        outputFile.get().asFile.text = message
    }
}

abstract class ConsumerTask extends DefaultTask {
    @InputFile
    abstract RegularFileProperty getInputFile() // (2)
    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void run() {
        def message = inputFile.get().asFile.text
        outputFile.get().asFile.write(message)
    }
}

def greeterA = tasks.register("greeterA", GreetingTask) {
    type = "a"
    outputFile = layout.buildDirectory.dir("greetings").map { it.file("a.txt") } // (3)
}
tasks.register("greeterB", GreetingTask) {
    type = "b"
    outputFile = layout.buildDirectory.dir("greetings").map { it.file("b.txt") }
}

tasks.register("consumer", ConsumerTask) {
    inputFile = greeterA.map { it.outputFile.get() } // (4)
    outputFile = layout.buildDirectory.file("consumerOutput.txt")
}
  1. Write to a specific output file: This task produces a single file to a directly specified outputFile without registering an entire output directory.

  2. Read a specific file: Unlike the previous example the input is a single directly specified inputFile file.

  3. Set output file: Sets outputFile to a file that is inside a shared subdirectory of buildDirectory.

  4. Wire greeterA to consumer: Makes sure that greeterA produces the output file before it is used by consumer by wiring task inputs to outputs directly.

Now when running consumer task, then greeterB, the consumer task remains UP-TO-DATE as Gradle knows that it is not using the output from greeterB, since greeterA and greeterB produce and consume distinct files (that happen to be in created in the same directory).

Don’t hardcode Task names unless they are documented as Public API

Hardcoding task names can make a build fragile when upgrading Gradle or third-party plugins.

Explanation

Some task names in Gradle are part of the Gradle public API and are stable. Most other task names in Gradle and third-party plugins are internal implementation details. They may be renamed or removed in future versions and should not be relied upon. Task types are also not guaranteed to remain stable; plugin authors may refactor them.

In practice, configuring tasks by type is usually more robust than depending on specific names, especially in ecosystems like Android or Kotlin where many tasks are generated dynamically.

Prefer, in order:

  1. Plugin DSL - Configure tasks via extension blocks provided by a plugin that are automatically wired to the tasks that plugin creates. This is the most future-proof and stable option.

  2. Task types - More robust than names in many cases, but can still change across plugin versions, depending on the plugin’s backwards compatibility policies.

  3. Task names - Use only when they are explicitly documented as public API.

As a plugin author, provide a DSL extension for users to configure behavior declaratively. Internally wire the configuration into your tasks. Avoid exposing task names as part of your public API unless you are prepared to maintain them with deprecation cycles.

Example
Don’t Do This
build.gradle.kts
plugins {
    id("java-library")
    id("maven-publish")
}

tasks.named<JavaCompile>("compileJava").configure { // (1)
    sourceCompatibility = "17"
    targetCompatibility = "17"
}

publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["java"])
        }
    }
}

tasks.named<GenerateMavenPom>("generatePomFileForMavenPublication").configure { // (2)
    pom.url = "sample.gradle.org"
}
build.gradle
plugins {
    id("java-library")
    id("maven-publish")
}

tasks.named("compileJava") { // (1)
    sourceCompatibility = "17"
    targetCompatibility = "17"
}

publishing {
    publications {
        maven(MavenPublication) {
            from(components.java)
        }
    }
}

tasks.named("generatePomFileForMavenPublication") { // (2)
    pom.url = "sample.gradle.org"
}
  1. Looking up compileJava by name: This relies on the hardcoded "compileJava" string.

  2. Using tasks.named to get the generatePomFileForMavenPublication task: This unnecessarily relies on this task’s name, even though it’s the only GenerateMavenPom task that needs configuring.

Do This Instead

A better option for the compileJava configuration is to use a public constant — this task name is part of public API, so referencing it via the constant is safe:

build.gradle.kts
tasks.named<JavaCompile>(JavaPlugin.COMPILE_JAVA_TASK_NAME) { // (1)
    sourceCompatibility = "17"
    targetCompatibility = "17"
}
build.gradle
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME) { // (1)
    sourceCompatibility = "17"
    targetCompatibility = "17"
}
  1. Using tasks.named with JavaPlugin.COMPILE_JAVA_TASK_NAME: Replaces the hardcoded string with a public constant.

The best option is to use the plugin DSL where possible — this avoids referring to tasks by name or type at all:

build.gradle.kts
java { // (1)
    setSourceCompatibility(JavaVersion.VERSION_17)
}

publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["java"])
            pom.url = "sample.gradle.org" // (2)
        }
    }
}
build.gradle
java { // (1)
    setSourceCompatibility(JavaVersion.VERSION_17)
}

publishing {
    publications {
        maven(MavenPublication) {
            from(components.java)
            pom.url = "sample.gradle.org" // (2)
        }
    }
}
  1. Using Java DSL: Entirely avoids implementation details of java-library plugin and sets sourceCompatibility for all JavaCompile tasks.

  2. Using publishing DSL: Avoids hardcoding task names and ensures that the generated POM has the expected URL.

Don’t access a Project instance during Task Execution

Do not access a Project instance inside a task action or task property.

Explanation

Accessing the Project instance during task execution is not compatible with the Configuration Cache and should be avoided. Alternatives exist for almost every use case involving data or operations from the Project object:

  • Specify the data you were previously retrieving from the Project instance as an explicit task input.

  • Use similar functionality already exposed through the Task object (e.g., Task.getLogger() instead of Project.getLogger()).

  • Capture Project values in a local variable during task configuration, which can then be referenced during execution.

Note
Accessing the Project instance during task configuration is expected and safe — this is how you wire task input properties to project properties. The problem arises when Project is accessed during task execution (e.g., inside @TaskAction or doLast).
Example
Don’t Do This
build.gradle.kts
abstract class VersionTask : DefaultTask() {

    @get:OutputDirectory
    abstract val outputDirectory: DirectoryProperty

    @TaskAction
    fun run() {
        val outputFile = outputDirectory.file("build_version.txt")
        outputFile.get().asFile.writeText(project.version.toString()) // (1)
    }
}

tasks.register<VersionTask>("generateVersionFile") {
    outputDirectory.set(project.layout.buildDirectory)
}
build.gradle
abstract class VersionTask extends DefaultTask {

    @OutputDirectory
    abstract DirectoryProperty getOutputDirectory()

    @TaskAction
    void run() {
        def outputFile = outputDirectory.file("build_version.txt")
        outputFile.get().asFile.text = project.version.toString() // (1)
    }
}

tasks.register("generateVersionFile", VersionTask) {
    outputDirectory = project.layout.buildDirectory
}
  1. Reading project.version in a task action: Inside the task’s action, the Project instance is accessed to read the version property.

There are two main problems with this setup:

  1. Accessing the Project instance during task execution will cause Configuration Cache failures.

  2. Because the version is not declared as a task input, Gradle cannot track it. This leads to incorrect up-to-date results if the project version changes.

Do This Instead

To ensure compatibility, avoid accessing the Project instance during task execution. Instead, tasks should explicitly declare all required inputs.

build.gradle.kts
abstract class VersionTask : DefaultTask() {
    @get:Input
    abstract val version: Property<String> // (1)

    @get:OutputDirectory
    abstract val outputDirectory: DirectoryProperty

    @TaskAction
    fun run() {
        outputDirectory.file("build_version.txt").get().asFile.writeText(version.get())
    }
}

tasks.register<VersionTask>("generateVersionFile") {
    version.set(project.version.toString()) // (2)
    outputDirectory.set(project.layout.buildDirectory.dir("build-info")) // (3)
}
build.gradle
abstract class VersionTask extends DefaultTask {
    @Input
    abstract Property<String> getVersion() // (1)

    @OutputDirectory
    abstract DirectoryProperty getOutputDirectory()

    @TaskAction
    void run() {
        outputDirectory.file("build_version.txt").get().asFile.text = version.get()
    }
}

tasks.register("generateVersionFile", VersionTask) {
    version = project.version // (2)
    outputDirectory = project.layout.buildDirectory.dir("build-info") // (3)
}
  1. Declare the version as an input property: This allows Gradle to track the version string for up-to-date checks.

  2. Assign the version during configuration: It is safe to read project.version during the configuration phase to assign it to a task input.

  3. Set the output location: Use project.layout during configuration to define the task’s output directory.

Now, when running the task, there are no "hidden inputs" that Gradle cannot track. This ensures that up-to-date checks accurately determine when a task needs to run again. Additionally, because the Project object is not accessed after configuration, the Configuration Cache will function correctly.

Wire lazy task outputs using map and flatMap

Use flatMap to extract a Provider-typed output from a task, and map to transform the resulting value. Together, they preserve the task dependency chain so Gradle knows which tasks must run first.

Explanation

When wiring task outputs to inputs, flatMap extracts a Provider-typed output from a task while preserving the task dependency that the output Provider carries. Then map transforms that value. For example, reading a file’s contents or extracting a path, without breaking the chain. The typical pattern looks like:

consumer.inputContent.set(
    producer.flatMap { it.outputFile }       // extract the Provider, keeping the task dependency
        .map { it.asFile.readText() }        // transform the value lazily at execution time
)
consumer.inputContent.set(
    producer.flatMap { it.outputFile }       // extract the Provider, keeping the task dependency
        .map { it.asFile.text }              // transform the value lazily at execution time
)

For dependency tracking to work, task outputs should be directly annotated abstract getters:

@get:OutputFile
abstract val outputFile: RegularFileProperty
@OutputFile
abstract RegularFileProperty getOutputFile()

With this pattern, flatMap reliably carries the task dependency and map safely transforms the result.

A few specific patterns break this chain:

  1. Do not call .get() inside map or flatMap — the file doesn’t exist yet at configuration time, breaking lazy configuration and task dependency tracking

  2. Do not use standalone provider {} to wrap task outputsprovider {} produces a disconnected provider with no task dependency information

  3. Use directly annotated abstract getters for task outputsflatMap can only track dependencies through directly annotated abstract getters, not task outputs that exist only to define transforms of other properties. Getters that return Provider instead of Property also break tracking, even when annotated.

  4. Use map for plain values, flatMap for Provider-typed outputsflatMap requires its closure to return a Provider; use map to extract File or String values

1. Do not call .get() inside map or flatMap

Calling .get() inside map to read file content fails because the file does not exist yet at configuration time — the producer task has not run. This pattern also breaks the Configuration Cache.

Both examples below use the same generatorTask producer; only the consumer differs.

Don’t Do This
build.gradle.kts
val generatorTask = tasks.register<GeneratorTask>("generator") {
    outputFile.set(layout.buildDirectory.file("eager-output.txt"))
}

tasks.register<ConsumerTask>("consumeEager") {
    inputFile.set(generatorTask.flatMap { it.outputFile })
    inputContent.set(generatorTask.map {
        it.outputFile.get().asFile.readText() // (1)
    })
}
build.gradle
def generatorTask = tasks.register('generator', GeneratorTask) {
    outputFile = layout.buildDirectory.file('eager-output.txt')
}

tasks.register('consumeEager', ConsumerTask) {
    inputFile = generatorTask.flatMap { it.outputFile }
    inputContent = generatorTask.map {
        it.outputFile.get().asFile.text // (1)
    }
}
  1. File read at configuration time: .get() realizes the file before the producer task has run; reading it here fails because the file doesn’t exist yet.

Do This Instead

Use the flatMap/map chain instead. The map on a flatMap result is evaluated lazily at execution time, when the file exists:

build.gradle.kts
val generatorTask = tasks.register<GeneratorTask>("generator") {
    outputFile.set(layout.buildDirectory.file("output.txt"))
}

tasks.register<ConsumerTask>("consumeLazy") {
    inputFile.set(generatorTask.flatMap { it.outputFile }) // (1)
    inputContent.set(
        generatorTask.flatMap { it.outputFile }
            .map { it.asFile.readText() } // (2)
    )
}
build.gradle
def generatorTask = tasks.register('generator', GeneratorTask) {
    outputFile = layout.buildDirectory.file('output.txt')
}

tasks.register('consumeLazy', ConsumerTask) {
    inputFile = generatorTask.flatMap { it.outputFile } // (1)
    inputContent = generatorTask.flatMap { it.outputFile }
        .map { it.asFile.text } // (2)
}
  1. Lazy wiring via flatMap: extracts the outputFile Provider while preserving the dependency on generator.

  2. Read content lazily: chaining map on the flatMap result defers reading until execution time.

This preserves the task dependency because the provider chain remains connected to the original task.

2. Do not use standalone provider {} to wrap task outputs

A standalone provider {} creates a provider with no connection to any task. Gradle cannot determine that it depends on the producer task, so the producer will not be added to the task graph.

Both examples below use the same generatorTask producer; only the consumer differs.

Don’t Do This
build.gradle.kts
val generatorTask = tasks.register<GeneratorTask>("generator") {
    outputFile.set(layout.buildDirectory.file("output.txt"))
}

tasks.register<ConsumerTask>("consumeNaked") {
    inputFile.set(generatorTask.flatMap { it.outputFile })
    inputContent.set(provider { // (1)
        generatorTask.get().outputFile.get().asFile.readText()
    })
}
build.gradle
def generatorTask = tasks.register('generator', GeneratorTask) {
    outputFile = layout.buildDirectory.file('output.txt')
}

tasks.register('consumeNaked', ConsumerTask) {
    inputFile = generatorTask.flatMap { it.outputFile }
    inputContent = providers.provider { // (1)
        generatorTask.get().outputFile.get().asFile.text
    }
}
  1. Disconnected provider {} block: has no link to the generator task; Gradle cannot infer that consumeNaked depends on generator from this provider alone.

Do This Instead

Use flatMap to extract the Provider-typed output, then chain map to transform the value:

build.gradle.kts
val generatorTask = tasks.register<GeneratorTask>("generator") {
    outputFile.set(layout.buildDirectory.file("output.txt"))
}

tasks.register<ConsumerTask>("consumeLazy") {
    inputFile.set(generatorTask.flatMap { it.outputFile }) // (1)
    inputContent.set(
        generatorTask.flatMap { it.outputFile }
            .map { it.asFile.readText() } // (2)
    )
}
build.gradle
def generatorTask = tasks.register('generator', GeneratorTask) {
    outputFile = layout.buildDirectory.file('output.txt')
}

tasks.register('consumeLazy', ConsumerTask) {
    inputFile = generatorTask.flatMap { it.outputFile } // (1)
    inputContent = generatorTask.flatMap { it.outputFile }
        .map { it.asFile.text } // (2)
}
  1. Lazy wiring via flatMap: extracts the outputFile Provider while preserving the dependency on generator.

  2. Read content lazily: chaining map on the flatMap result defers reading until execution time.

3. Use directly annotated abstract getters for task outputs

When a task’s output is computed via .map {} rather than directly annotated with @OutputFile, flatMap may extract the mapped provider without preserving the task dependency.

Don’t Do This
build.gradle.kts
abstract class ProducerTask @Inject constructor(
    objectFactory: ObjectFactory
) : DefaultTask() {
    @get:Internal
    val someDirectory = objectFactory.directoryProperty()

    // This property is DERIVED via map - not directly annotated
    @get:OutputFile
    val outputFile = someDirectory.map { it.file("output.txt") }

    @TaskAction
    fun execute() {
        outputFile.get().asFile.writeText("content")
    }
}

val derivedProducer = tasks.register<ProducerTask>("produceDerived") {
    someDirectory.set(layout.buildDirectory.dir("output"))
}

tasks.register<Sync>("consumeDerived") {
    from(derivedProducer.flatMap { it.outputFile }) // (1)
    into(layout.buildDirectory.dir("sync"))
}
build.gradle
abstract class ProducerTask extends DefaultTask {
    @Internal
    abstract DirectoryProperty getSomeDirectory()

    // This property is DERIVED via map - not directly annotated
    @OutputFile
    Provider<RegularFile> getOutputFile() {
        return someDirectory.map { it.file('output.txt') }
    }

    @TaskAction
    void execute() {
        outputFile.get().asFile.text = 'content'
    }
}

def derivedProducer = tasks.register('produceDerived', ProducerTask) {
    someDirectory = layout.buildDirectory.dir('output')
}

tasks.register('consumeDerived', Sync) {
    from(derivedProducer.flatMap { it.outputFile }) // (1)
    into(layout.buildDirectory.dir('sync'))
}
  1. Derived output property: when outputFile is built via .map { …​ } rather than annotated directly, flatMap may lose the task dependency.

Do This Instead

As a workaround, use .map { it.property.get() } to preserve the dependency:

build.gradle.kts
abstract class ProducerTask @Inject constructor(
    objectFactory: ObjectFactory
) : DefaultTask() {
    @get:Internal
    val someDirectory = objectFactory.directoryProperty()

    @get:OutputFile
    val outputFile = someDirectory.map { it.file("output.txt") }

    @TaskAction
    fun execute() {
        outputFile.get().asFile.writeText("content")
    }
}

val derivedProducer = tasks.register<ProducerTask>("produceDerived") {
    someDirectory.set(layout.buildDirectory.dir("output"))
}

tasks.register<Sync>("consumeDerivedSync") {
    from(derivedProducer.map { it.outputFile.get() }) // (1)
    into(layout.buildDirectory.dir("sync"))
}
build.gradle
abstract class ProducerTask extends DefaultTask {
    @Internal
    abstract DirectoryProperty getSomeDirectory()

    @OutputFile
    Provider<RegularFile> getOutputFile() {
        return someDirectory.map { it.file('output.txt') }
    }

    @TaskAction
    void execute() {
        outputFile.get().asFile.text = 'content'
    }
}

def derivedProducer = tasks.register('produceDerived', ProducerTask) {
    someDirectory = layout.buildDirectory.dir('output')
}

tasks.register('consumeDerivedSync', Sync) {
    from(derivedProducer.map { it.outputFile.get() }) // (1)
    into(layout.buildDirectory.dir('sync'))
}
  1. Workaround with .map { it.property.get() }: preserves the task dependency at the cost of an explicit .get() call.

The preferred approach is to use directly annotated abstract getters instead of derived ones, so that flatMap works reliably:

build.gradle.kts
abstract class DirectProducerTask : DefaultTask() {
    // Directly annotated property - not derived
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun execute() {
        outputFile.get().asFile.writeText("content")
    }
}

val directProducer = tasks.register<DirectProducerTask>("directProducer") {
    outputFile.set(layout.buildDirectory.file("output/output.txt"))
}

tasks.register<Sync>("consumeDirect") {
    from(directProducer.flatMap { it.outputFile }) // (1)
    into(layout.buildDirectory.dir("sync-direct"))
}
build.gradle
abstract class DirectProducerTask extends DefaultTask {
    // Directly annotated property - not derived
    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void execute() {
        outputFile.get().asFile.text = 'content'
    }
}

def directProducer = tasks.register('directProducer', DirectProducerTask) {
    outputFile = layout.buildDirectory.file('output/output.txt')
}

tasks.register('consumeDirect', Sync) {
    from(directProducer.flatMap { it.outputFile }) // (1)
    into(layout.buildDirectory.dir('sync-direct'))
}
  1. Directly annotated abstract getter: with @OutputFile on an abstract RegularFileProperty, flatMap carries the task dependency reliably.

4. Use map for plain values, flatMap for Provider-typed outputs

When a task uses old-style getters (like File) instead of Provider-typed outputs, use map instead of flatMap to extract the value. The flatMap closure must return a Provider; wrapping a plain value in a standalone provider {} would compile, but — as in item 2 — it would drop the task dependency.

Do This
build.gradle.kts
abstract class LegacyTask : DefaultTask() {
    // Old-style eager property (before Provider API)
    @get:OutputDirectory
    lateinit var destinationDir: File

    @TaskAction
    fun execute() {
        destinationDir.mkdirs()
        File(destinationDir, "output.txt").writeText("content")
    }
}

val legacy = tasks.register<LegacyTask>("legacy") {
    destinationDir = layout.buildDirectory.dir("docs").get().asFile
}

tasks.register<ConsumerTask>("consumeLegacy") {
    inputFile.fileProvider(legacy.map { File(it.destinationDir, "output.txt") }) // (1)
    inputContent.set(legacy.map { File(it.destinationDir, "output.txt").readText() })
}
build.gradle
abstract class LegacyTask extends DefaultTask {
    // Old-style eager property (before Provider API)
    @OutputDirectory
    File destinationDir

    @TaskAction
    void execute() {
        destinationDir.mkdirs()
        new File(destinationDir, 'output.txt').text = 'content'
    }
}

def legacy = tasks.register('legacy', LegacyTask) {
    destinationDir = layout.buildDirectory.dir('docs').get().asFile
}

tasks.register('consumeLegacy', ConsumerTask) {
    inputFile.fileProvider(legacy.map { new File(it.destinationDir, 'output.txt') }) // (1)
    inputContent = legacy.map { new File(it.destinationDir, 'output.txt').text }
}
  1. Use map for non-Provider outputs: when the producer exposes a plain File instead of a Provider, map is the right extractor.

Best Practices for Performance

Prefer the -bin Gradle Distribution

Gradle publishes two distribution variants for each release: -bin (binaries only) and -all (binaries, sources, and documentation). For most builds, you should prefer the smaller -bin distribution.

Using -bin reduces download size and verification effort, speeds up CI and developer builds, and limits the number of artifacts you need to trust.

Explanation

Each Gradle release provides:

  • gradle-<version>-bin.zip – binaries only

  • gradle-<version>-all.zip – binaries plus sources and offline documentation

In modern setups:

  • IDEs and build tools can download sources and documentation directly from repositories or online docs (even when using -bin releases).

  • The -all distribution is rarely required outside of specific offline or air-gapped environments.

Preferring -bin helps because:

  • It reduces download and cache size for CI and local builds.

  • There is less to verify and fewer artifacts to trust (one smaller archive instead of a larger “everything included” zip).

  • It shortens the feedback loop when upgrading Gradle.

In special cases (for example, fully offline environments), you can still use -all, but it should be a conscious exception rather than the default.

Don’t Do This

Make sure your Wrapper doesn’t point to the -all distribution:

gradle/wrapper/gradle-wrapper.properties:

distributionUrl=https\://services.gradle.org/distributions/gradle-<version>-all.zip
Do This Instead

Configure the Wrapper to use the -bin distribution:

gradle/wrapper/gradle-wrapper.properties:

distributionUrl=https\://services.gradle.org/distributions/gradle-<version>-bin.zip

Use UTF-8 File Encoding

Set UTF-8 as the default file encoding to ensure consistent behavior across platforms.

Explanation

Use UTF-8 as the default file encoding to ensure consistent behavior across environments and avoid caching issues caused by platform-dependent default encodings.

This is especially important when working with build caching, since differences in file encoding between environments can cause unexpected cache misses.

To enforce UTF-8 encoding, add the following to your gradle.properties file:

org.gradle.jvmargs=-Dfile.encoding=UTF-8
Note
Do not rely on the default encoding of the underlying JVM or operating system, as this may differ between environments and lead to inconsistent behavior.

Use the Build Cache

Use the Build Cache to save time by reusing outputs produced by previous builds.

Explanation

The Build Cache avoids re-executing tasks when their inputs haven’t changed by reusing outputs from previous builds.

This prevents redundant work. If the inputs are the same, the outputs will be too, resulting in faster, more efficient builds.

Example
Don’t Do This

Build caching is disabled by default:

gradle.properties
# caching is off by default
# org.gradle.caching=false
Do This Instead

To enable the Build Cache, add the following to your gradle.properties file:

gradle.properties
org.gradle.caching=true

When you build your project for the first time, Gradle populates the cache with the outputs of tasks like compilation.

Even if you run ./gradlew clean to delete the build directory, Gradle can reuse cached outputs in subsequent builds.

$ ./gradlew clean
:clean
BUILD SUCCESSFUL

On subsequent builds, instead of executing the :compileJava task again, the outputs of the task will be loaded from the Build Cache:

$ ./gradlew compileJava
> Task :compileJava FROM-CACHE

BUILD SUCCESSFUL in 0s
1 actionable task: 1 from cache

Use the Configuration Cache

Use the Configuration Cache to significantly improve build performance by caching the result of the configuration phase and reusing it in subsequent builds.

Explanation

The Configuration Cache works by saving the result of the configuration phase. On the next build, if nothing relevant has changed, Gradle skips configuration entirely and loads the cached task graph from disk, jumping straight to task execution.

This can dramatically reduce build time for large builds, but it’s just as valuable for smaller builds where configuration overhead can dominate short iterations. Faster feedback helps developers stay focused, without waiting on redundant configuration work.

It’s important to understand how this differs from the Build Cache. The Build Cache stores the outputs of task execution, while the Configuration Cache stores the configured task graph before execution begins. These are independent mechanisms that solve different problems, but they are designed to work together for optimal performance.

Note
The Configuration Cache is the preferred way to execute Gradle builds, but it is not enabled by default. Many existing builds and plugins are not yet fully compatible, and adopting it may involve refactoring of build logic. Enabling it by default could lead to unexpected build failures, so Gradle uses an opt-in adoption model to allow teams to verify compatibility and adopt configuration caching incrementally and safely.
Example
Don’t Do This

Configuration Caching is not enabled by default:

gradle.properties
# caching is off by default
# org.gradle.configuration-cache=false
Do This Instead

To enable the Configuration Cache, add the following to your gradle.properties file:

gradle.properties
org.gradle.configuration-cache=true

When you build your project for the first time, Gradle stores the outcome of the configuration phase, including the task graph, in the Configuration Cache.

$ ./gradlew compileJava
Configuration cache entry stored.
> Task :processResources NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :compileJava
> Task :classes
> Task :compileTestJava NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :jar
> Task :assemble
> Task :build

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

On subsequent builds, instead of reconfiguring tasks like :compileJava, Gradle loads the task graph from the Configuration Cache and proceeds directly to execution.

$ ./gradlew compileJava
Configuration cache entry reused.
> Task :processResources NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :compileJava
> Task :classes
> Task :compileTestJava NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :jar
> Task :assemble
> Task :build

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

Avoid Expensive Computations in Configuration Phase

Avoid expensive computations in the configuration phase, instead, move them to task actions.

Explanation

In order for Gradle to execute tasks it first needs to build the project task graph. As part of discovering what tasks to include in the task graph, Gradle will configure all the tasks that are directly requested, any task dependencies of the requested tasks, and also any tasks that are not lazily registered. This work is done in the configuration phase.

Performing expensive or slow operations such as file or network I/O, or CPU-heavy calculations in the configuration phase forces these to run even when they might be unnecessary to complete the requested work of the invoked tasks. It is better to move these operations to task actions so that they run only when required.

Example
Don’t Do This
build.gradle.kts
abstract class MyTask : DefaultTask() {
    @get:Input
    lateinit var computationResult: String
    @TaskAction
    fun run() {
        logger.lifecycle(computationResult)
    }
}

fun heavyWork(): String {
    println("Start heavy work")
    Thread.sleep(5000)
    println("Finish heavy work")
    return "Heavy computation result"
}

tasks.register<MyTask>("myTask") {
    computationResult = heavyWork() // (1)
}
build.gradle
abstract class MyTask extends DefaultTask {
    @Input
    String computationResult
    @TaskAction
    void run() {
        logger.lifecycle(computationResult)
    }
}

String heavyWork() {
    logger.lifecycle("Start heavy work")
    Thread.sleep(5000)
    logger.lifecycle("Finish heavy work")
    return "Heavy computation result"
}

tasks.register("myTask", MyTask) {
    computationResult = heavyWork() // (1)
}
  1. Performing heavy computation during configuration phase.

Do This Instead
build.gradle.kts
abstract class MyTask : DefaultTask() {
    @TaskAction
    fun run() {
        logger.lifecycle(heavyWork()) // (1)
    }

    fun heavyWork(): String {
        logger.lifecycle("Start heavy work")
        Thread.sleep(5000)
        logger.lifecycle("Finish heavy work")
        return "Heavy computation result"
    }
}

tasks.register<MyTask>("myTask")
build.gradle
abstract class MyTask extends DefaultTask {
    @TaskAction
    void run() {
        logger.lifecycle(heavyWork()) // (1)
    }
    String heavyWork() {
        logger.lifecycle("Start heavy work")
        Thread.sleep(5000)
        logger.lifecycle("Finish heavy work")
        return "Heavy computation result"
    }
}

tasks.register("myTask", MyTask)
  1. Performing heavy computation during execution phase in a task action.

Best Practices for Security

Validate the Gradle Distribution SHA-256 Checksum

Set distributionSha256Sum in gradle-wrapper.properties to verify the integrity of the downloaded Gradle distribution.

Explanation

Always set the distributionSha256Sum property in your gradle-wrapper.properties file to verify the integrity of the downloaded Gradle distribution. This ensures the gradle-X.X-bin.zip file matches the official SHA-256 checksum published by Gradle, protecting your build from corruption or tampering.

distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionSha256Sum=2b3f4...sha256-here...f4511

This validation step enhances security by preventing the execution of compromised or incomplete Gradle distributions.

The official SHA-256 checksums can be found on the Gradle releases page.

Validate the Gradle Wrapper on every Upgrade

The Gradle Wrapper runs before your build logic and can deeply affect your build. You should treat any change to the Wrapper as security-sensitive.

Validate the Wrapper JAR and distribution settings every time you upgrade Gradle.

Explanation

When you update Gradle, two Wrapper files typically change:

  • gradle/wrapper/gradle-wrapper.jar

  • gradle/wrapper/gradle-wrapper.properties (distributionUrl and optionally distributionSha256Sum)

You should verify that:

  • The Wrapper JAR is the official binary published by Gradle (not tampered with).

  • The Wrapper JAR checksum matches one of the values published at gradle.org/release-checksums.

  • The distribution URL points to the expected Gradle release.

  • The distribution checksum (if configured via distributionSha256Sum) matches the release you intend to use (also listed on gradle.org/release-checksums).

Running an unverified Wrapper risks executing untrusted code before any of your build’s safeguards run.

Do this

If you use the setup-gradle action (version v4 or newer) for GitHub Actions, Wrapper validation will be performed automatically. The action validates the checksum of every gradle-wrapper.jar in your repository and fails the build if it finds any unknown Wrapper JAR.

If you use a different GitHub Actions setup, you can use the dedicated Gradle Wrapper validation action instead.

Do not Run ./gradlew on Untrusted Projects

When working with an untrusted project, you should exercise caution before running ./gradlew.

Explanation

The gradlew (Gradle Wrapper) script is an executable that downloads and runs a specific version of Gradle. It should be generated by running the Wrapper task with a trusted Gradle installation, but there is no guarantee that a project’s authors have done this. The script or gradle-wrapper.jar could have been modified. The only way to verify this is through checksum verification.

Running an unverified wrapper is similar to running any other untrusted script from the internet. Build scripts (build.gradle, settings.gradle, etc.) can also execute arbitrary code during the configuration phase.

An unreviewed wrapper or build script could potentially access or modify files on your system, install malicious software, or compromise your environment (e.g., stealing environment variables like GITHUB_TOKEN or AWS_SECRET_KEY).

Note
Opening an untrusted project in an IDE such as IntelliJ IDEA or Android Studio will typically trigger a Gradle sync automatically, which executes build logic. Review the project’s wrapper and build scripts before opening it in your IDE.
Do this
1. Review the Build Scripts
  • Inspect Build Files: Look at build.gradle(.kts), settings.gradle(.kts), and gradle.properties for suspicious dependencies, plugins, custom exec tasks, or obfuscated code.

2. Verify the Gradle Wrapper Files
  • Checksum Verification: Use checksum validation to ensure gradle-wrapper.jar hasn’t been tampered with.

  • Check the URL: Open gradle/wrapper/gradle-wrapper.properties and ensure the distributionUrl points strictly to the official site: https://services.gradle.org/.

  • Regenerate the Wrapper: If you have a trusted Gradle installation, delete the project’s gradlew, gradlew.bat, and gradle/wrapper/gradle-wrapper.jar, then run gradle wrapper (not ./gradlew). Compare the regenerated files against the originals, any differences indicate the project’s wrapper may have been modified.

3. Work in an Isolated Environment

You can optionally run the project in a Docker container or a Virtual Machine (VM). This ensures that even if a malicious script runs, it cannot access your files.

Build Output Should Be Byte-for-Byte Reproducible

A build is reproducible when the same source code produces byte-for-byte identical output files, on any machine, at any time.

Explanation

Reproducibility matters far beyond the publishing flow. If binaries remain identical, then downstream consumers can verify that they match the source. Build systems typically verify artifact checksums; non-determinism in artifact bytes defeats caches and deduplication logic without providing any benefit.

With this strong form of reproducibility, the question "Did my change actually change anything?" is easy to answer: compare the output hashes. Gradle’s own up-to-date checks and remote build cache also rely on stable inputs producing stable outputs.

Some common sources of non-determinism in builds are file timestamps inside archives, the order of entries inside archives, and the SDK used to compile classes. For JVM builds, keeping wall-clock timestamps on .class files inside a .jar, or letting archive entries follow the filesystem’s directory-iteration order, means that two clean builds of the same source produce slightly different artifacts with different checksums even though the output is functionally identical.

Note
Since Gradle 9.0.0, archive artifacts are set to be reproducible by default.

This is list of examples is not exhaustive. There are other ways to introduce non-reproducibility in your project. One good test to confirm reproducibility is to try re-building an older version on a different machine, and comparing the output to the published binary.

Example
Don’t Do This
build.gradle.kts
plugins {
    `java-library` // (1)
}

tasks.named<Jar>("jar") {
    isPreserveFileTimestamps = true   // (2)
    isReproducibleFileOrder = false   // (3)
}
build.gradle
plugins {
    id 'java-library' // (1)
}

tasks.named('jar', Jar) {
    preserveFileTimestamps = true   // (2)
    reproducibleFileOrder = false   // (3)
}
  1. No java { toolchain { …​ } } block is declared. Gradle has no default toolchain, so the JDK used for compilation falls back to whichever JAVA_HOME is active. Different machines (and even different JDK patch releases on the same machine) emit different bytecode.

  2. Sets preserveFileTimestamps to true, overriding the Gradle 9.0+ default of false. Every entry inside the jar is stamped with the build’s wall-clock time, so the same source produces a different jar on every build.

  3. Sets reproducibleFileOrder to false, overriding the Gradle 9.0+ default of true. Archive entries are ordered by the filesystem’s directory-iteration order, which varies by OS and filesystem.

Do This Instead
build.gradle.kts
plugins {
    `java-library`
}

java {
    toolchain {
        // Choose your project's required version
        languageVersion = JavaLanguageVersion.of(21) // (1)
    }
}
// (2)
build.gradle
plugins {
    id 'java-library'
}

java {
    toolchain {
        // Choose your project's required version
        languageVersion = JavaLanguageVersion.of(21) // (1)
    }
}
// (2)
  1. Pin the JDK used to compile so the build is decoupled from each developer’s local JAVA_HOME and from whichever JDK happens to be on the CI image; Gradle auto-provisions a matching JDK if one isn’t found.

  2. Leave archive defaults alone — Gradle 9.0+ sets preserveFileTimestamps = false and reproducibleFileOrder = true on every AbstractArchiveTask (Jar, War, Ear, Zip, Tar). The performance cost of these defaults is negligible: reproducibleFileOrder adds a sort over directory listings that’s dominated by I/O and compression work, and dropping timestamps actually makes archives marginally smaller and avoids unnecessary downstream cache invalidation. If you maintain a build that still supports an older Gradle version, set both flags explicitly with tasks.withType<AbstractArchiveTask>().configureEach { …​ }.

Best Practices for Testing

Test your custom Task and Plugins with TestKit

You should test any custom tasks or plugins you create using Gradle TestKit.

Explanation

Gradle’s flexibility supports a natural evolution of custom types as they mature.

Creating new tasks and plugins directly in a build.gradle(.kts) file is a great way to prototype new functionality. As that functionality stabilizes, you should extract these definitions into buildSrc or a standalone plugin project for better reusability and maintainability. Once types exist outside of a single build file, you can easily write functional tests for them using TestKit.

A mature build should include functional tests for its custom types to ensure they behave as expected.

Example
Don’t Do This

The following build defines a custom task and a custom plugin that applies it within the build.gradle(.kts) file. The plugin adds multiple custom tasks that print a greeting using properties defined in a custom extension.

This is a common pattern for prototyping new functionality, but it lacks tests to verify the behavior of the custom types. The only way to verify that the task and plugin work as intended is to run the build manually and inspect the output.

build.gradle.kts
import java.time.Instant

interface MyExtension {
    val firstName: Property<String>
    val lastName: Property<String>
}

var greeter = "Hello"

@CacheableTask // (1)
abstract class MyTask: DefaultTask() {
    @get:Input
    abstract val firstName: Property<String>
    @get:Input
    abstract val lastName: Property<String>
    @get:Input
    abstract val greeting: Property<String>

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    private final val today = Instant.now() // (2)

    @TaskAction
    fun run() {
        val output = outputFile.asFile.get()
        val result = "${greeting.get()}, ${firstName.get()} ${lastName.get()}, it's currently\n$today"
        println(result)
        output.writeText(result)
    }
}

abstract class MyPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        val extension = project.extensions.create("myExtension", MyExtension::class.java)

        project.tasks.register<MyTask>("task1") {
            outputFile.convention(project.layout.buildDirectory.file("output1.txt"))
        }

        project.tasks.register<MyTask>("task2") {
            outputFile.convention(project.layout.buildDirectory.file("output2.txt"))
        }

        project.tasks.withType<MyTask>().configureEach {
            group = "Custom Tasks"
            firstName.convention(extension.firstName)
            lastName.convention(extension.firstName) // (3)
            greeting.convention("Hi")
        }
    }
}

apply<MyPlugin>()

configure<MyExtension> {
    firstName = "John"
    lastName = "Smith"
}

tasks.named<MyTask>("task2") {
    greeter = "Bonjour" // (4)
}
build.gradle
import java.time.Instant

interface MyExtension {
    Property<String> getFirstName()
    Property<String> getLastName()
}

def greeter = "Hello"

@CacheableTask // (1)
abstract class MyTask extends DefaultTask {
    @Input
    abstract Property<String> getFirstName()
    @Input
    abstract Property<String> getLastName()
    @Input
    abstract Property<String> getGreeting()

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    private final Instant today = Instant.now() // (2)

    @TaskAction
    void run() {
        def output = outputFile.asFile.get()
        def result = "${greeting.get()}, ${firstName.get()} ${lastName.get()}, it's currently\n$today"
        println result
        output.text = result
    }
}

abstract class MyPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        def extension = project.extensions.create("myExtension", MyExtension)

        project.tasks.register("task1", MyTask) { task ->
            task.outputFile.convention(project.layout.buildDirectory.file("output1.txt"))
        }

        project.tasks.register("task2", MyTask) { task ->
            task.outputFile.convention(project.layout.buildDirectory.file("output2.txt"))
        }

        project.tasks.withType(MyTask).configureEach { task ->
            task.group = "Custom Tasks"
            task.firstName.convention(extension.firstName)
            task.lastName.convention(extension.firstName) // (3)
            task.greeting.convention("Hi")
        }
    }
}

apply plugin: MyPlugin

myExtension {
    firstName = "John"
    lastName = "Smith"
}

tasks.named("task2", MyTask) {
    greeter = "Bonjour" // (4)
}

In this case, there are several problems:

  1. The Task is declared as cacheable: However, the task’s output differs depending on when it is run, so it should not be cached.

  2. The current time is used as an undeclared input: Inputs to a task must be explicitly declared using the appropriate annotations, otherwise Gradle cannot track changes to them.

  3. Error in task property wiring: The lastName property is linked to the firstName property on the extension, which is likely a mistake.

  4. The wrong variable is assigned during task configuration: The greeter variable from the buildscript is mistakenly assigned instead of the task’s greeting property.

Do This Instead

In this updated version of the build, the custom types are defined in an included build-logic composite build, which now includes a basic set of functional tests using Gradle TestKit.

While these custom types are written in Java for demonstration purposes, they could just as easily be implemented in Groovy or Kotlin. Because they reside in a separate, complete Gradle build, they can be thoroughly tested using TestKit.

├── build-logic/
│   ├── src
│   │   ├── main
│   │   │   └── java
│   │   │       └── org
│   │   │           └── example
│   │   │               └── MyExtension.java
│   │   │               └── MyPlugin.java
│   │   │               └── MyTask.java
│   │   └── functionalTest
│   │       └── java
│   │           └── org
│   │               └── example
│   │                   └── MyPluginFunctionalTest.java
│   ├── build.gradle.kts
│   └── settings.gradle.kts
├── settings.gradle.kts
└── build.gradle.kts
├── build-logic/
│   ├── src
│   │   ├── main
│   │   │   └── java
│   │   │       └── org
│   │   │           └── example
│   │   │               └── MyExtension.java
│   │   │               └── MyPlugin.java
│   │   │               └── MyTask.java
│   │   └── functionalTest
│   │       └── java
│   │           └── org
│   │               └── example
│   │                   └── MyPluginFunctionalTest.java
│   ├── build.gradle
│   └── settings.gradle
├── settings.gradle
└── build.gradle

We’ve corrected the issues with the plugin and task from the previous example:

MyTask.java
package org.example;

import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import java.io.File;
import java.nio.file.Files;
import java.time.Instant;
import java.io.IOException;

@CacheableTask
public abstract class MyTask extends DefaultTask {
    @Input
    public abstract Property<String> getFirstName();
    @Input
    public abstract Property<String> getLastName();
    @Input
    public abstract Property<String> getGreeting();

    @OutputFile
    public abstract RegularFileProperty getOutputFile();

    @Input // (1)
    public abstract Property<Instant> getToday();

    @TaskAction
    public void run() throws IOException {
        File output = getOutputFile().getAsFile().get();
        String result = String.format("%s, %s %s, it's currently\n%s", getGreeting().get(), getFirstName().get(), getLastName().get(), getToday().get());
        System.out.println(result);
        Files.writeString(output.toPath(), result);
    }
}
  1. Today is a proper @Input: This allows Gradle’s UP-TO-DATE checking to properly consider it when rerunning the task.

MyPlugin.java
package org.example;

import org.gradle.api.Plugin;
import org.gradle.api.Project;
import java.time.Instant;

public abstract class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        MyExtension extension = project.getExtensions().create("myExtension", MyExtension.class);

        project.getTasks().register("task1", MyTask.class, task -> {
            task.getOutputFile().convention(project.getLayout().getBuildDirectory().file("output1.txt"));
        });

        project.getTasks().register("task2", MyTask.class, task -> {
            task.getOutputFile().convention(project.getLayout().getBuildDirectory().file("output2.txt"));
        });

        project.getTasks().withType(MyTask.class).configureEach(task -> {
            task.setGroup("Custom Tasks");
            task.getFirstName().convention(extension.getFirstName());
            task.getLastName().convention(extension.getLastName()); // (1)
            task.getGreeting().convention("Hi");
            task.getToday().convention(Instant.now()); // (2)
        });
    }
}
  1. Corrected typo in assignment: The last name convention set to the value of the last name from the extension.

  2. Today is set to the same value on all tasks: Only calling Instant.now() once, rather than every time a task is created.

Writing and running the tests as described below helped identify the bugs in the plugin implementation.

We defined a functional test suite within the build-logic project. Because TestKit tests tend to be slower and more complex than unit tests, they are typically kept separate. Locating these tests in a dedicated functionalTest suite also clarifies their purpose for other developers.

build.gradle.kts
val functionalTest = testing.suites.register("functionalTest", JvmTestSuite::class) { // (1)
    useJUnitJupiter()
    dependencies {
        implementation("commons-io:commons-io:2.16.1")
        implementation(project())
        implementation(gradleTestKit()) // (2)
    }
}

tasks.check {
    dependsOn(functionalTest)
}

gradlePlugin {
    plugins {
        register("org.example.myplugin") {
            implementationClass = "org.example.MyPlugin"
        }
    }
    testSourceSets(functionalTest.get().sources) // (3)
}
build.gradle
def functionalTest = testing.suites.register("functionalTest", JvmTestSuite) { // (1)
    useJUnitJupiter()
    dependencies {
        implementation("commons-io:commons-io:2.16.1")
        implementation(project())
        implementation(gradleTestKit()) // (2)
    }
}

tasks.check {
    dependsOn(functionalTest)
}

gradlePlugin {
    plugins {
        register("org.example.myplugin") {
            implementationClass = "org.example.MyPlugin"
        }
    }

    testSourceSets functionalTest.get().sources // (3)
}
  1. Define the new test suite: Creates a functionalTest JVM Test Suite.

  2. Add TestKit dependency: This contains the GradleRunner class we’ll use to write tests.

  3. Make the java-gradle-plugin aware of the new test suite: Now the usable plugin source from the project’s main production code will be available to these tests.

With this setup in place, we can write functional tests for our plugin in the build-logic/src/functionalTest/java directory. You can run ./gradlew :build-logic:functionalTest or ./gradlew :build-logic:check from the root project directory to execute these tests.

Note
By default, tests within the included build-logic build are not executed when you run tests in the root project. Because the root project only requires the build artifacts from build-logic, Gradle will build the project without running its internal tests. To run them, you must explicitly invoke the tasks: ./gradlew :build-logic:check.

In the functional test class, we use TestKit to initialize a temporary Gradle project, apply our plugin, and verify that it behaves as expected.

MyPluginFunctionalTest.java
package org.example;

import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.io.File;
import java.io.IOException;

import org.apache.commons.io.FileUtils;

import static org.gradle.testkit.runner.TaskOutcome.FROM_CACHE;
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class MyPluginFunctionalTest {
    @TempDir File testProjectDir;
    private File settingsFile;
    private File buildFile;
    private File cacheDir = new File("build-cache");

    @BeforeEach
    public void setup() throws IOException {
        settingsFile = new File(testProjectDir, "settings.gradle");
        Files.writeString(settingsFile.toPath(), "rootProject.name = 'test-project'");

        buildFile = new File(testProjectDir, "build.gradle");
    }

    @Test
    public void testTaskRegistration() throws IOException { // (1)
        String buildFileContent = """
            plugins {
                id("org.example.myplugin")
            }
        """;
        Files.writeString(buildFile.toPath(), buildFileContent);

        BuildResult result = GradleRunner.create()
            .withProjectDir(testProjectDir)
            .withPluginClasspath()
            .forwardOutput()
            .withArguments("tasks", "--all")
            .build();

        assertContainsIgnoringEol("""
            Custom Tasks tasks
            ------------------
            task1
            task2
            """,
            result.getOutput()
        );
    }

    @Test
    public void testTaskExecution() throws IOException { // (2)
        File outputFile = new File(testProjectDir, "build/output.txt");

        String buildFileContent = """
            plugins {
                id("org.example.myplugin")
            }

            myExtension {
                firstName = "John"
                lastName = "Smith"
            }

            tasks.task1 {
                outputFile = project.layout.buildDirectory.file("output.txt")
            }
        """;
        Files.writeString(buildFile.toPath(), buildFileContent);

        GradleRunner.create()
            .withProjectDir(testProjectDir)
            .withPluginClasspath()
            .withArguments("task1")
            .build();

        String actual = Files.readString(outputFile.toPath());
        assertTrue(actual.startsWith("Hi, John Smith, it's currently"));
    }

    @Test
    public void testTaskDeterminism() throws IOException { // (3)
        File outputFile = new File(testProjectDir, "build/output.txt");

        String buildFileContent = """
            plugins {
                id("org.example.myplugin")
            }

            myExtension {
                firstName = "John"
                lastName = "Smith"
            }

            tasks.task1 {
                outputFile = project.layout.buildDirectory.file("output.txt")
                today = ZonedDateTime.parse("2026-01-12T16:00:00-05:00").toInstant()
            }
        """;
        Files.writeString(buildFile.toPath(), buildFileContent);

        GradleRunner runner = GradleRunner.create()
            .withProjectDir(testProjectDir)
            .withPluginClasspath();

        runner.withArguments("task1").build();
        String output1 = Files.readString(outputFile.toPath());

        runner.withArguments("task1", "--rerun-tasks").build();
        String output2 = Files.readString(outputFile.toPath());

        assertEquals(output1, output2);
    }

    @Test
    public void testTaskCacheability() throws IOException { // (4)
        String buildFileContent = """
            plugins {
                id("org.example.myplugin")
            }

            myExtension {
                firstName = "John"
                lastName = "Smith"
            }

            tasks.task1 {
                outputFile = project.layout.buildDirectory.file("output.txt")
                today = ZonedDateTime.parse("2026-01-12T16:00:00-05:00").toInstant()
            }
        """;
        Files.writeString(buildFile.toPath(), buildFileContent);

        GradleRunner runner = GradleRunner.create()
            .withProjectDir(testProjectDir)
            .withPluginClasspath()
            .forwardOutput();

        BuildResult result = runner.withArguments("--build-cache", "task1").build();
        assertEquals(SUCCESS, result.task(":task1").getOutcome());

        FileUtils.deleteDirectory(new File(testProjectDir, "build"));

        result = runner.withArguments("--build-cache", "task1").build();
        assertEquals(FROM_CACHE, result.task(":task1").getOutcome());
    }

    private static void assertContainsIgnoringEol(String expected, String actual) {
        assertTrue(normalizeEol(actual).contains(normalizeEol(expected)));
    }

    private static String normalizeEol(String s) {
        return s.replace("\r\n", "\n").replace("\r", "\n");
    }
}

Looking at the example tests here, you can see various techniques used to verify the plugin’s behavior against actual, ad-hoc Gradle builds defined within the tests:

  1. testTaskRegistration: This test runs the tasks report and verifies the output.

  2. testTaskExecution: This test runs the custom task and verifies its output file.

  3. testTaskDeterminism: This test runs the build twice - forcing tasks to rerun the second time - to ensure the output is identical, which is necessary for caching.

  4. testTaskCacheability: This test checks that when the task is run twice in a row, the second run is loaded from cache.

These examples only scratch the surface of what you can achieve with TestKit. For instance, a comprehensive test for cacheability should verify that changes to inputs correctly trigger re-execution and include tests for relocatability.

You can find more information in the Gradle TestKit documentation.

RUNTIME AND CONFIGURATION

Command-Line Interface

The command-line interface is the primary method of interacting with Gradle.

The following is a reference for executing and customizing the Gradle command-line. It also serves as a reference when writing scripts or configuring continuous integration.

Use of the Gradle Wrapper is highly encouraged. Substitute ./gradlew (in macOS / Linux) or gradlew.bat (in Windows) for gradle in the following examples.

Executing Gradle on the command-line conforms to the following structure:

gradle [taskName...] [--option-name...]

Options are allowed before and after task names.

gradle [--option-name...] [taskName...]

If multiple tasks are specified, you should separate them with a space.

gradle [taskName1 taskName2...] [--option-name...]

Options that accept values can be specified with or without = between the option and argument. The use of = is recommended.

gradle [...] --console=plain

Options that enable behavior have long-form options with inverses specified with --no-. The following are opposites.

gradle [...] --build-cache
gradle [...] --no-build-cache

Many long-form options have short-option equivalents. The following are equivalent:

gradle --help
gradle -h
Note
Many command-line flags can be specified in gradle.properties to avoid needing to be typed. See the Configuring build environment guide for details.

Command-line usage

The following sections describe the use of the Gradle command-line interface.

Some plugins also add their own command line options. For example, --tests, which is added by Java test filtering. For more information on exposing command line options for your own tasks, see Declaring command-line options.

Executing tasks

You can learn about what projects and tasks are available in the project reporting section.

Most builds support a common set of tasks known as lifecycle tasks. These include the build, assemble, and check tasks.

To execute a task called myTask on the root project, type:

$ gradle :myTask

This will run the single myTask and all of its dependencies.

Specify options for tasks

To pass an option to a task, prefix the option name with -- after the task name:

$ gradle :exampleTask --exampleOption=exampleValue
Disambiguate task options from built-in options

Gradle does not prevent tasks from registering options that conflict with Gradle’s built-in options, like --profile or --help.

You can fix conflicting task options from Gradle’s built-in options with a -- delimiter before the task name in the command:

$ gradle [--built-in-option-name...] -- [taskName...] [--task-option-name...]

Consider a task named mytask that accepts an option named profile:

  • In gradle mytask --profile, Gradle accepts --profile as the built-in Gradle option.

  • In gradle -- mytask --profile=value, Gradle passes --profile as a task option.

Executing tasks in multi-project builds

In a multi-project build, subproject tasks can be executed with : separating the subproject name and task name. The following are equivalent when run from the root project:

$ gradle :subproject:taskName
$ gradle subproject:taskName

You can also run a task for all subprojects using a task selector that consists of only the task name.

The following command runs the test task for all subprojects when invoked from the root project directory:

$ gradle test

To recap:

// Run a task in the root project only
$ gradle :exampleTask --exampleOption=exampleValue

// Run a task that may exist in the root or any subproject (ambiguous if defined in more than one)
$ gradle exampleTask --exampleOption=exampleValue

// Run a task in a specific subproject
$ gradle subproject:exampleTask --exampleOption=exampleValue
$ gradle :subproject:exampleTask --exampleOption=exampleValue
Note
Some tasks selectors, like help or dependencies, will only run the task on the project they are invoked on and not on all the subprojects.

When invoking Gradle from within a subproject, the project name should be omitted:

$ cd subproject
$ gradle taskName
Tip
When executing the Gradle Wrapper from a subproject directory, reference gradlew relatively. For example: ../gradlew taskName.
Executing multiple tasks

You can also specify multiple tasks. The tasks' dependencies determine the precise order of execution, and a task having no dependencies may execute earlier than it is listed on the command-line.

For example, the following will execute the test and deploy tasks in the order that they are listed on the command-line and will also execute the dependencies for each task.

$ gradle test deploy
Command line order safety

Although Gradle will always attempt to execute the build quickly, command line ordering safety will also be honored.

For example, the following will execute clean and build along with their dependencies:

$ ./gradlew clean build

However, the intention implied in the command line order is that clean should run first and then build. It would be incorrect to execute clean after build, even if doing so would cause the build to execute faster since clean would remove what build created.

Conversely, if the command line order was build followed by clean, it would not be correct to execute clean before build. Although Gradle will execute the build as quickly as possible, it will also respect the safety of the order of tasks specified on the command line and ensure that clean runs before build when specified in that order.

Note that command line order safety relies on tasks properly declaring what they create, consume, or remove.

Excluding tasks from execution

You can exclude a task from being executed using the -x or --exclude-task command-line option and providing the name of the task to exclude:

$ gradle dist --exclude-task test
> Task :compile
compiling source

> Task :dist
building the distribution

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
commandLineTutorialTasks

You can see that the test task is not executed, even though the dist task depends on it. The test task’s dependencies, such as compileTest, are not executed either. The dependencies of test that other tasks depend on, such as compile, are still executed.

Forcing tasks to execute

You can force Gradle to execute all tasks ignoring up-to-date checks using the --rerun-tasks option:

$ ./gradlew test --rerun-tasks

This will force test and all task dependencies of test to execute. It is similar to running gradle clean test, but without the build’s generated output being deleted.

Alternatively, you can tell Gradle to rerun a specific task using the --rerun built-in task option.

Continue the build after a task failure

By default, Gradle aborts execution and fails the build when any task fails. This allows the build to complete sooner and prevents cascading failures from obfuscating the root cause of an error.

You can use the --continue option to force Gradle to execute every task when a failure occurs:

$ ./gradlew test --continue

When executed with --continue, Gradle executes every task in the build if all the dependencies for that task are completed without failure.

For example, tests do not run if there is a compilation error in the code under test because the test task depends on the compilation task. Gradle outputs each of the encountered failures at the end of the build.

Note
If any tests fail, many test suites fail the entire test task. Code coverage and reporting tools frequently run after the test task, so "fail fast" behavior may halt execution before those tools run.
Name abbreviation

When you specify tasks on the command-line, you don’t have to provide the full name of the task. You can provide enough of the task name to identify the task uniquely. For example, it is likely gradle che is enough for Gradle to identify the check task.

The same applies to project names. You can execute the check task in the library subproject with the gradle lib:che command.

You can use camel case patterns for more complex abbreviations. These patterns are expanded to match camel case and kebab case names. For example, the pattern foBa (or fB) matches fooBar and foo-bar.

More concretely, you can run the compileTest task in the my-awesome-library subproject with the command gradle mAL:cT.

$ ./gradlew mAL:cT
> Task :my-awesome-library:compileTest
compiling unit tests

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Abbreviations can also be used with the -x command-line option.

Tracing name expansion

For complex projects, it might be ambiguous if the intended tasks were executed. When using abbreviated names, a single typo can lead to the execution of unexpected tasks.

When INFO, or more verbose logging is enabled, the output will contain extra information about the project and task name expansion.

For example, when executing the mAL:cT command on the previous example, the following log messages will be visible:

No exact project with name ':mAL' has been found. Checking for abbreviated names.
Found exactly one project that matches the abbreviated name ':mAL': ':my-awesome-library'.
No exact task with name ':cT' has been found. Checking for abbreviated names.
Found exactly one task name, that matches the abbreviated name ':cT': ':compileTest'.

Common tasks

The following are task conventions applied by built-in and most major Gradle plugins.

Computing all outputs

It is common in Gradle builds for the build task to designate assembling all outputs and running all checks:

$ ./gradlew build
Running applications

It is common for applications to run with the run task, which assembles the application and executes some script or binary:

$ ./gradlew run
Running all checks

It is common for all verification tasks, including tests and linting, to be executed using the check task:

$ ./gradlew check
Cleaning outputs

You can delete the contents of the build directory using the clean task. Doing so will cause pre-computed outputs to be lost, causing significant additional build time for the subsequent task execution:

$ ./gradlew clean

Project reporting

Gradle provides several built-in tasks which show particular details of your build. This can be useful for understanding your build’s structure and dependencies, as well as debugging problems.

Listing projects

Running the projects task gives you a list of the subprojects of the selected project, displayed in a hierarchy:

$ ./gradlew projects

You also get a project report with Build Scan.

Listing tasks

Running gradle tasks gives you a list of the main tasks of the selected project. This report shows the default tasks for the project, if any, and a description for each task:

$ ./gradlew tasks

By default, this report shows only those tasks assigned to a task group.

Groups (such as verification, publishing, help, build…​) are available as the header of each section when listing tasks:

> Task :tasks

Build tasks
-----------
assemble - Assembles the outputs of this project.

Build Setup tasks
-----------------
init - Initializes a new Gradle build.

Distribution tasks
------------------
assembleDist - Assembles the main distributions

Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.

You can obtain more information in the task listing using the --all option:

$ ./gradlew tasks --all

The option --no-all can limit the report to tasks assigned to a task group.

If you need to be more precise, you can display only the tasks from a specific group using the --group option:

$ ./gradlew tasks --group="build setup"

You can use the --provenance option to show where each task was registered (e.g., by a plugin or build script):

$ ./gradlew tasks --provenance
Build tasks
-----------
assemble - Assembles the outputs of this project. (registered by plugin 'org.gradle.language.base.plugins.LifecycleBasePlugin')
build - Assembles and tests this project. (registered by plugin 'org.gradle.language.base.plugins.LifecycleBasePlugin')
Show task usage details

Running gradle help --task someTask gives you detailed information about a specific task:

$ ./gradlew -q help --task libs
Detailed task information for libs

Paths
     :api:libs (registered in build file 'api/build.gradle')
     :webapp:libs (registered in build file 'webapp/build.gradle')

Type
     Task (org.gradle.api.Task)

Options
     --rerun     Causes the task to be re-run even if up-to-date.

Description
     Builds the JAR

Group
     build

This information includes the full task path, the task type, possible task-specific command line options, and the description of the given task. The output also includes provenance information showing where the task was registered (e.g., in a build file or by a plugin).

You can get detailed information about the task class types using the --types option or using --no-types to hide this information.

Reporting dependencies

Build Scan gives a full, visual report of what dependencies exist on which configurations, transitive dependencies, and dependency version selection. They can be invoked using the --scan options:

$ ./gradlew myTask --scan

This will give you a link to a web-based report, where you can find dependency information like this:

Build Scan dependencies report
Listing project dependencies

Running the dependencies task gives you a list of the dependencies of the selected project, broken down by configuration. For each configuration, the direct and transitive dependencies of that configuration are shown in a tree.

Below is an example of this report:

$ ./gradlew dependencies
> Task :app:dependencies

------------------------------------------------------------
Project ':app'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- project :model
|    \--- org.json:json:20220924
+--- com.google.inject:guice:5.1.0
|    +--- javax.inject:javax.inject:1
|    +--- aopalliance:aopalliance:1.0
|    \--- com.google.guava:guava:30.1-jre -> 28.2-jre
|         +--- com.google.guava:failureaccess:1.0.1
|         +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
|         +--- com.google.code.findbugs:jsr305:3.0.2
|         +--- org.checkerframework:checker-qual:2.10.0 -> 3.28.0
|         +--- com.google.errorprone:error_prone_annotations:2.3.4
|         \--- com.google.j2objc:j2objc-annotations:1.3
+--- com.google.inject:guice:{strictly 5.1.0} -> 5.1.0 (c)
+--- org.json:json:{strictly 20220924} -> 20220924 (c)
+--- javax.inject:javax.inject:{strictly 1} -> 1 (c)
+--- aopalliance:aopalliance:{strictly 1.0} -> 1.0 (c)
+--- com.google.guava:guava:{strictly [28.0-jre, 28.5-jre]} -> 28.2-jre (c)
+--- com.google.guava:guava:{strictly 28.2-jre} -> 28.2-jre (c)
+--- com.google.guava:failureaccess:{strictly 1.0.1} -> 1.0.1 (c)
+--- com.google.guava:listenablefuture:{strictly 9999.0-empty-to-avoid-conflict-with-guava} -> 9999.0-empty-to-avoid-conflict-with-guava (c)
+--- com.google.code.findbugs:jsr305:{strictly 3.0.2} -> 3.0.2 (c)
+--- org.checkerframework:checker-qual:{strictly 3.28.0} -> 3.28.0 (c)
+--- com.google.errorprone:error_prone_annotations:{strictly 2.3.4} -> 2.3.4 (c)
\--- com.google.j2objc:j2objc-annotations:{strictly 1.3} -> 1.3 (c)

Concrete examples of build scripts and output available in Viewing and debugging dependencies.

Running the buildEnvironment task visualises the buildscript dependencies of the selected project, similarly to how gradle dependencies visualizes the dependencies of the software being built:

$ ./gradlew buildEnvironment

Running the dependencyInsight task gives you an insight into a particular dependency (or dependencies) that match specified input:

$ ./gradlew dependencyInsight --dependency [...] --configuration [...]

The --configuration parameter restricts the report to a particular configuration such as compileClasspath.

Listing project properties

Running the properties task gives you a list of the properties of the selected project:

$ ./gradlew -q api:properties
------------------------------------------------------------
Project ':api'
------------------------------------------------------------

allprojects: [project ':api']
ant: org.gradle.api.internal.project.DefaultAntBuilder@12345
antBuilderFactory: org.gradle.api.internal.project.DefaultAntBuilderFactory@12345
artifacts: org.gradle.api.internal.artifacts.dsl.DefaultArtifactHandler_Decorated@12345
asDynamicObject: DynamicObject for project ':api'
baseClassLoaderScope: org.gradle.api.internal.initialization.DefaultClassLoaderScope@12345

You can also query a single property with the optional --property argument:

$ ./gradlew -q api:properties --property allprojects
------------------------------------------------------------
Project ':api'
------------------------------------------------------------

allprojects: [project ':api']

Command-line completion

Gradle provides bash and zsh tab completion support for tasks, options, and Gradle properties through gradle-completion (installed separately):

gradle completion 4.0

Debugging options

-?, -h, --help

Shows a help message with the built-in CLI options organized into logical sections. To show project-contextual options, including help on a specific task, see the help task.

-v, --version

Prints Gradle, Groovy, Ant, Launcher & Daemon JVM, and operating system version information and exit without executing any tasks.

-V, --show-version

Prints Gradle, Groovy, Ant, Launcher & Daemon JVM, and operating system version information and continue execution of specified tasks.

-S, --full-stacktrace

Print out the full (very verbose) stacktrace for any exceptions. See also logging options.

-s, --stacktrace

Print out the stacktrace also for user exceptions (e.g. compile error). See also logging options.

--scan

Create a Build Scan with fine-grained information about all aspects of your Gradle build.

-Dorg.gradle.debug=true

A Gradle property that debugs the Gradle Daemon process. Gradle will wait for you to attach a debugger at localhost:5005 by default.

-Dorg.gradle.debug.host=(host address)

A Gradle property that specifies the host address to listen on or connect to when debug is enabled. In the server mode on Java 9 and above, passing * for the host will make the server listen on all network interfaces. By default, no host address is passed to JDWP, so on Java 9 and above, the loopback address is used, while earlier versions listen on all interfaces.

-Dorg.gradle.debug.port=(port number)

A Gradle property that specifies the port number to listen on when debug is enabled. Default is 5005.

-Dorg.gradle.debug.server=(true,false)

A Gradle property that if set to true and debugging is enabled, will cause Gradle to run the build with the socket-attach mode of the debugger. Otherwise, the socket-listen mode is used. Default is true.

-Dorg.gradle.debug.suspend=(true,false)

A Gradle property that if set to true and debugging is enabled, the JVM running Gradle will suspend until a debugger is attached. Default is true.

Performance options

Try these options when optimizing and improving build performance.

Many of these options can be specified in the gradle.properties file, so command-line flags are unnecessary.

--build-cache, --no-build-cache

Toggles the Gradle Build Cache. Gradle will try to reuse outputs from previous builds. Default is off.

--configuration-cache, --no-configuration-cache

Toggles the Configuration Cache. Gradle will try to reuse the build configuration from previous builds. Default is off.

--configuration-cache-problems=(fail,warn)

Configures how the configuration cache handles problems. Default is fail.

Set to warn to report problems without failing the build.

Set to fail to report problems and fail the build if there are any problems.

--configure-on-demand, --no-configure-on-demand Incubating

Toggles configure-on-demand. Only relevant projects are configured in this build run. Default is off.

--isolated-projects, --no-isolated-projects Incubating

Toggles Isolated Projects. Projects are configured in parallel. Implies --configuration-cache. Default is off.

--max-workers

Sets the maximum number of workers that Gradle may use. Default is number of processors.

--parallel, --no-parallel

Build projects in parallel. For limitations of this option, see Parallel Project Execution. Default is off.

--priority

Specifies the scheduling priority for the Gradle daemon and all processes launched by it. Values are normal or low. Default is normal.

--profile

Generates a high-level performance report in the layout.buildDirectory.dir("reports/profile") directory. --scan is preferred.

--scan

Generate a Build Scan with detailed performance diagnostics.

Build Scan performance report
--watch-fs, --no-watch-fs

Toggles watching the file system. When enabled, Gradle reuses information it collects about the file system between builds. Enabled by default on operating systems where Gradle supports this feature.

Gradle daemon options

You can manage the Gradle Daemon through the following command line options.

--daemon, --no-daemon

Use the Gradle Daemon to run the build. Starts the daemon if not running or the existing daemon is busy. Default is on.

--foreground

Starts the Gradle Daemon in a foreground process.

--status (Standalone command)

Run gradle --status to list running and recently stopped Gradle daemons. It only displays daemons of the same Gradle version.

--stop (Standalone command)

Run gradle --stop to stop all Gradle Daemons of the same version.

-Dorg.gradle.daemon.idletimeout=(number of milliseconds)

A Gradle property wherein the Gradle Daemon will stop itself after this number of milliseconds of idle time. Default is 10800000 (3 hours).

Logging options

Setting log level

You can customize the verbosity of Gradle logging with the following options, ordered from least verbose to most verbose.

-Dorg.gradle.logging.level=(quiet,warn,lifecycle,info,debug)

A Gradle property that sets the logging level.

-q, --quiet

Log errors only.

-w, --warn

Set log level to warn.

-i, --info

Set log level to info.

-d, --debug

Log in debug mode (includes normal stacktrace).

Lifecycle is the default log level.

Customizing log format

You can control the use of rich output (colors and font variants) by specifying the console mode in the following ways:

-Dorg.gradle.console=(auto,plain,colored,rich,verbose)

A Gradle property that specifies the console mode. Different modes are described immediately below.

--console=(auto,plain,colored,rich,verbose)

Specifies which type of console output to generate.

Set to plain to generate plain text only. This option disables all color and other rich output in the console output. This is the default when Gradle is not attached to a terminal.

Set to colored to generate colored output without rich status information such as progress bars.

Set to auto (the default) to enable color and other rich output in the console output when the build process is attached to a console or to generate plain text only when not attached to a console. This is the default when Gradle is attached to a terminal.

Set to rich to enable color and other rich output in the console output, regardless of whether the build process is not attached to a console. When not attached to a console, the build output will use ANSI control characters to generate the rich output.

Set to verbose to enable color and other rich output like rich with output task names and outcomes at the lifecycle log level, (as is done by default in Gradle 3.5 and earlier).

Note
When the NO_COLOR environment variable is set and non-empty, Gradle suppresses color output regardless of the console mode. Bold, underline, and rich features like progress bars are unaffected. See no-color.org.
Reporting problems
--problems-report (enabled by default) Incubating

Enable the generation of build/reports/problems-report.html. This is the default behaviour. The report is generated with problems provided to the Problems API.

--no-problems-report Incubating

Disable the generation of build/reports/problems-report.html, by default this report is generated with problems provided to the Problems API.

Showing or hiding warnings

By default, Gradle won’t display all warnings (e.g. deprecation warnings). Instead, Gradle will collect them and render a summary at the end of the build like:

Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0.

You can control the verbosity of warnings on the console with the following options:

-Dorg.gradle.warning.mode=(all,fail,none,summary)

A Gradle property that specifies the warning mode. Different modes are described immediately below.

--warning-mode=(all,fail,none,summary)

Specifies how to log warnings. Default is summary.

Set to all to log all warnings.

Set to fail to log all warnings and fail the build if there are any warnings.

Set to summary to suppress all warnings and log a summary at the end of the build.

Set to none to suppress all warnings, including the summary at the end of the build.

In all and fail modes, Gradle infers a source location for every problem rather than only the first ones. Past the stack-capture cap (50 problems by default, 5000 under Isolated Projects), the trace is reduced to the originating build logic and the calling script (enough to locate the problem) rather than the full call chain.

Rich console

Gradle’s rich console displays extra information while builds are running.

Gradle Rich Console

Features:

  • Progress bar and timer visually describe the overall status

  • Parallel work-in-progress lines below describe what is happening now

  • Colors and fonts are used to highlight significant output and errors

Execution options

The following options affect how builds are executed by changing what is built or how dependencies are resolved.

--include-build

Run the build as a composite, including the specified build.

--offline

Specifies that the build should operate without accessing network resources.

-U, --refresh-dependencies

Refresh the state of dependencies.

--continue

Continue task execution after a task failure.

-m, --dry-run

Run Gradle with all task actions disabled. Use this to show which task would have executed.

--task-graph Since 9.1.0

Run Gradle with all task actions disabled and print the task dependency graph.

-t, --continuous

Enables continuous build. Gradle does not exit and will re-execute tasks when task file inputs change.

--write-locks

Indicates that all resolved configurations that are lockable should have their lock state persisted.

--update-locks <group:name>[,<group:name>]*

Indicates that versions for the specified modules have to be updated in the lock file.

This flag also implies --write-locks.

-a, --no-rebuild

Do not rebuild project dependencies. Useful for debugging and fine-tuning buildSrc, but can lead to wrong results. Use with caution!

Dependency verification options

Learn more about this in dependency verification.

-F=(strict,lenient,off), --dependency-verification=(strict,lenient,off)

Configures the dependency verification mode.

The default mode is strict.

-M, --write-verification-metadata

Generates checksums for dependencies used in the project (comma-separated list) for dependency verification.

--refresh-keys

Refresh the public keys used for dependency verification.

--export-keys

Exports the public keys used for dependency verification.

Environment options

You can customize many aspects of build scripts, settings, caches, and so on through the options below.

-g, --gradle-user-home

Specifies the Gradle User Home directory. The default is the .gradle directory in the user’s home directory.

-p, --project-dir

Specifies the start directory for Gradle. Defaults to current directory.

--project-cache-dir

Specifies the project-specific cache directory. Default value is .gradle in the root project directory.

-D, --system-prop

Sets a system property of the JVM, for example -Dmyprop=myvalue.

-I, --init-script

Specifies an initialization script.

-P, --project-prop

Sets a project property of the root project, for example -Pmyprop=myvalue.

-Dorg.gradle.jvmargs

A Gradle property that sets JVM arguments.

-Dorg.gradle.java.home

A Gradle property that sets the JDK home dir.

Non-interactive mode

By default, Gradle may prompt the user for input on the console when an interactive terminal is available. You can disable all interactive prompting by using the --non-interactive command-line option, or by setting the org.gradle.console.interactive Gradle property to false (for example in gradle.properties, GRADLE_OPTS, or via -D).

When interactive prompting is disabled, Gradle uses default values instead of prompting. This is useful for running Gradle in automated environments such as CI pipelines, scripts, and AI agents.

--non-interactive Incubating

Do not prompt for user input.

Task options

Tasks may define task-specific options which are different from most of the global options described in the sections above (which are interpreted by Gradle itself, can appear anywhere in the command line, and can be listed using the --help option).

Task options:

  1. Are consumed and interpreted by the tasks themselves;

  2. Must be specified immediately after the task in the command-line;

  3. May be listed using gradle help --task someTask (see Show task usage details).

To learn how to declare command-line options for your own tasks, see Declaring and Using Command Line Options.

Built-in task options

Built-in task options are options available as task options for all tasks. At this time, the following built-in task options exist:

--rerun

Causes the task to be rerun even if up-to-date. Similar to --rerun-tasks, but for a specific task.

Bootstrapping new projects

Creating new Gradle builds

Use the built-in gradle init task to create a new Gradle build, with new or existing projects.

$ gradle init

Most of the time, a project type is specified. Available types include basic (default), java-library, java-application, and more. See init plugin documentation for details.

$ gradle init --type java-library
Standardize and provision Gradle

The built-in gradle :wrapper task generates a script, gradlew, that invokes a declared version of Gradle, downloading it beforehand if necessary.

$ ./gradlew :wrapper --gradle-version=8.1

You can also specify --distribution-type=(bin|all), --gradle-distribution-url, --gradle-distribution-sha256-sum in addition to --gradle-version.
Full details on using these options are documented in the Gradle wrapper section.

Continuous build

Continuous Build allows you to automatically re-execute the requested tasks when file inputs change. You can execute the build in this mode using the -t or --continuous command-line option.

Learn more in Continuous Builds.

Logging and Output

The build log is the primary way Gradle communicates what’s happening during a build. Clear logging helps you quickly understand your build status, identify issues, and troubleshoot effectively. Too much noise in the logs can obscure important warnings or errors.

Gradle provides flexible logging controls, enabling you to adjust verbosity and detail according to your needs.

Gradle Log levels

Gradle defines six primary log levels:

Level Description

ERROR

Error messages

QUIET

Important information messages

WARNING

Warning messages

LIFECYCLE

Progress information messages

INFO

Information messages

DEBUG

Debug messages

The default logging level is LIFECYCLE, providing progress updates without overwhelming detail.

Choosing and setting a log level

You can set the log level either through command-line options or by configuring the gradle.properties file.

CLI Option Property Outputs Log Levels

-q or --quiet

org.gradle.logging.level=quiet

QUIET and higher

-w or --warn

org.gradle.logging.level=warn

WARN and higher

no logging options

LIFECYCLE and higher

-i or --info

org.gradle.logging.level=info

INFO and higher

-d or --debug

org.gradle.logging.level=debug

DEBUG and higher (all log messages)

For example, to set a consistent log level in your project’s gradle.properties file:

org.gradle.logging.level=info

Similarly on the command line:

$ ./gradlew run --info

You can emit log messages directly from build scripts and tasks using Gradle’s built-in logger:

build.gradle.kts
tasks.register("logtask") {
    doLast {
        logger.lifecycle("Lifecycle: Build progress info.")
        logger.info("Info: Additional insights.")
        logger.debug("Debug: Detailed troubleshooting info.")
    }
}
build.gradle
tasks.register('logtask') {
    doLast {
        logger.lifecycle('Lifecycle: Build progress info.')
        logger.info('Info: Additional insights.')
        logger.debug('Debug: Detailed troubleshooting info.')
    }
}

Use appropriate log levels (lifecycle, info, debug) to ensure your build output is clear and informative.

Caution
The DEBUG log level can expose sensitive security information to the console.
Stacktrace options

Stacktraces are useful for diagnosing issues during a build failure. You can control stacktrace output via command-line options or properties:

CLI Option Gradle Property Stacktrace Shown

--stacktrace or -s

org.gradle.logging.stacktrace=all

Truncated stacktraces are printed. We recommend this over full stacktraces. Groovy full stacktraces are extremely verbose due to the underlying dynamic invocation mechanisms. Yet they usually do not contain relevant information about what has gone wrong in your code. This option renders stacktraces for deprecation warnings.

--full-stacktrace or -S

org.gradle.logging.stacktrace=full

The full stacktraces are printed out. This option renders stacktraces for deprecation warnings.

(none)

(none)

No stacktraces are printed to the console in case of a build error (e.g., a compile error). Only in case of internal exceptions will stacktraces be printed. If the DEBUG log level is chosen, truncated stacktraces are always printed.

For example, to always display a full stacktrace on build errors, set in gradle.properties:

org.gradle.logging.stacktrace=full

Logging Sensitive Information

Running Gradle with the DEBUG log level can potentially expose sensitive information to the console and build log.

This information might include:

  • Environment variables

  • Private repository credentials

  • Build cache and Develocity credentials

  • Plugin Portal publishing credentials

It’s important to avoid using the DEBUG log level when running on public Continuous Integration (CI) services. Build logs on these services are accessible to the public and can expose sensitive information. Even on private CI services, logging sensitive credentials may pose a risk depending on your organization’s threat model. It’s advisable to discuss this with your organization’s security team.

Some CI providers attempt to redact sensitive credentials from logs, but this process is not foolproof and typically only redacts exact matches of pre-configured secrets.

If you suspect that a Gradle Plugin may inadvertently expose sensitive information, please contact our security team for assistance with disclosure.

Custom log messages

A simple option for logging in your build file is to write messages to standard output. Gradle redirects anything written to standard output to its logging system at the QUIET log level:

build.gradle.kts
println("A message which is logged at QUIET level")
build.gradle
println 'A message which is logged at QUIET level'

Gradle also provides a logger property to a build script, which is an instance of Logger. This interface extends the SLF4J Logger interface and adds a few Gradle-specific methods. Below is an example of how this is used in the build script:

build.gradle.kts
logger.quiet("An info log message which is always logged.")
logger.error("An error log message.")
logger.warn("A warning log message.")
logger.lifecycle("A lifecycle info log message.")
logger.info("An info log message.")
logger.debug("A debug log message.")
logger.trace("A trace log message.") // Gradle never logs TRACE level logs
build.gradle
logger.quiet('An info log message which is always logged.')
logger.error('An error log message.')
logger.warn('A warning log message.')
logger.lifecycle('A lifecycle info log message.')
logger.info('An info log message.')
logger.debug('A debug log message.')
logger.trace('A trace log message.') // Gradle never logs TRACE level logs

Use the link typical SLF4J pattern to replace a placeholder with an actual value in the log message.

build.gradle.kts
logger.info("A {} log message", "info")
build.gradle
logger.info('A {} log message', 'info')

You can also hook into Gradle’s logging system from within other classes used in the build (classes from the buildSrc directory, for example) with an SLF4J logger. You can use this logger the same way as you use the provided logger in the build script.

build.gradle.kts
import org.slf4j.LoggerFactory

val slf4jLogger = LoggerFactory.getLogger("some-logger")
slf4jLogger.info("An info log message logged using SLF4j")
build.gradle
import org.slf4j.LoggerFactory

def slf4jLogger = LoggerFactory.getLogger('some-logger')
slf4jLogger.info('An info log message logged using SLF4j')

Logging from external tools and libraries

Internally, Gradle uses Ant and Ivy. Both have their own logging system. Gradle redirects their logging output into the Gradle logging system.

There is a 1:1 mapping from the Ant/Ivy log levels to the Gradle log levels, except the Ant/Ivy TRACE log level, which is mapped to the Gradle DEBUG log level. This means the default Gradle log level will not show any Ant/Ivy output unless it is an error or a warning.

Many tools out there still use the standard output for logging. By default, Gradle redirects standard output to the QUIET log level and standard error to the ERROR level. This behavior is configurable.

The project object provides a LoggingManager, which allows you to change the log levels that standard out or error are redirected to when your build script is evaluated.

build.gradle.kts
logging.captureStandardOutput(LogLevel.INFO)
println("A message which is logged at INFO level")
build.gradle
logging.captureStandardOutput LogLevel.INFO
println 'A message which is logged at INFO level'

To change the log level for standard out or error during task execution, use a LoggingManager.

build.gradle.kts
tasks.register("logInfo") {
    logging.captureStandardOutput(LogLevel.INFO)
    doFirst {
        println("A task message which is logged at INFO level")
    }
}
build.gradle
tasks.register('logInfo') {
    logging.captureStandardOutput LogLevel.INFO
    doFirst {
        println 'A task message which is logged at INFO level'
    }
}

Gradle also integrates with the Java Util Logging, Jakarta Commons Logging and Log4j logging toolkits. Any log messages your build classes write using these logging toolkits will be redirected to Gradle’s logging system.

Changing what Gradle logs

Warning

This feature is deprecated and will be removed in the next major version without a replacement.

The configuration cache limits the ability to customize Gradle’s logging UI. The custom logger can only implement supported listener interfaces. These interfaces do not receive events when the configuration cache entry is reused because the configuration phase is skipped.

You can replace much of Gradle’s logging UI with your own. You could do this if you want to customize the UI somehow - to log more or less information or to change the formatting. Simply replace the logging using the Gradle.useLogger(java.lang.Object) method. This is accessible from a build script, an init script, or via the embedding API. Note that this completely disables Gradle’s default output. Below is an example init script that changes how task execution and build completion are logged:

customLogger.init.gradle.kts
useLogger(CustomEventLogger())

@Suppress("deprecation")
class CustomEventLogger() : BuildAdapter(), TaskExecutionListener {

    override fun beforeExecute(task: Task) {
        println("[${task.name}]")
    }

    override fun afterExecute(task: Task, state: TaskState) {
        println()
    }

    override fun buildFinished(result: BuildResult) {
        println("build completed")
        if (result.failure != null) {
            (result.failure as Throwable).printStackTrace()
        }
    }
}
customLogger.init.gradle
useLogger(new CustomEventLogger())

@SuppressWarnings("deprecation")
class CustomEventLogger extends BuildAdapter implements TaskExecutionListener {

    void beforeExecute(Task task) {
        println "[$task.name]"
    }

    void afterExecute(Task task, TaskState state) {
        println()
    }
    
    void buildFinished(BuildResult result) {
        println 'build completed'
        if (result.failure != null) {
            result.failure.printStackTrace()
        }
    }
}
$ ./gradlew -I customLogger.init.gradle.kts build
> Task :compile
[compile]
compiling source

> Task :testCompile
[testCompile]
compiling test source

> Task :test
[test]
running unit tests

> Task :build
[build]

build completed
3 actionable tasks: 3 executed
$ ./gradlew -I customLogger.init.gradle build
> Task :compile
[compile]
compiling source

> Task :testCompile
[testCompile]
compiling test source

> Task :test
[test]
running unit tests

> Task :build
[build]

build completed
3 actionable tasks: 3 executed

Your logger can implement any of the listener interfaces listed below. When you register a logger, only the logging for the interfaces it implements is replaced. Logging for the other interfaces is left untouched. You can find out more about the listener interfaces in Build lifecycle events.

Gradle Wrapper

The recommended way to execute any Gradle build is with the help of the Gradle Wrapper (referred to as "Wrapper").

The Wrapper is a script (called gradlew or gradlew.bat) that invokes a declared version of Gradle, downloading it beforehand if necessary. Instead of running gradle build using the installed Gradle, you use the Gradle Wrapper by calling ./gradlew build.

wrapper workflow

The Gradle Wrapper isn’t distributed as a standalone download. It’s created using the gradle :wrapper task.

There are three ways to use the Wrapper:

  1. Adding the Wrapper - You set up a new Gradle project and add the Wrapper to it.

  2. Using the Wrapper - You run a project with the Wrapper that already provides it.

  3. Upgrading the Wrapper - You upgrade the Wrapper to a new version of Gradle.

When using the Wrapper instead of the installed Gradle, you gain the following benefits:

  • Standardizes a project on a given Gradle version for more reliable and robust builds.

  • Provisioning the Gradle version for different users is done with a simple Wrapper definition change.

  • Provisioning the Gradle version for different execution environments (e.g., IDEs or Continuous Integration servers) is done with a simple Wrapper definition change.

The following sections explain each of these use cases in more detail.

1. Adding the Gradle Wrapper

The Gradle Wrapper is not something you download.

Generating the Wrapper (and its files) requires an installed version of Gradle as described in Installation.

Tip
The Build Init Plugin (gradle init) automatically generates the Wrapper files when creating a new project.

Every vanilla Gradle build comes with a built-in task called wrapper. The task is listed under the group "Build Setup tasks" when listing the tasks.

Note
When invoking the wrapper task, use gradle :wrapper. The : prefix explicitly targets the root project, which is the only project where the wrapper task is relevant. This matters for Configure on Demand and Isolated Projects, where addressing the root project directly avoids unnecessary project configuration.

Executing the wrapper task generates the necessary Wrapper files in the project directory:

$ gradle :wrapper
> Task :wrapper

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Tip

To make the Wrapper files available to other developers and execution environments, you need to check them into version control.

Wrapper files, including the JAR file, are small. Adding the JAR file to version control is expected. Some organizations do not allow projects to submit binary files to version control, and there is no workaround available.

The generated Wrapper properties file, gradle/wrapper/gradle-wrapper.properties, stores the information about the Gradle distribution:

  • The server hosting the Gradle distribution.

  • The type of Gradle distribution. By default, the -bin distribution contains only the runtime but no sample code and documentation.

  • The Gradle version used for executing the build. By default, the wrapper task picks the same Gradle version used to generate the Wrapper files.

  • Optionally, a timeout in ms used when downloading the Gradle distribution.

  • Optionally, a boolean to set the validation of the distribution URL.

The following is an example of the generated distribution URL in gradle/wrapper/gradle-wrapper.properties:

distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
Tip

Use the -bin distribution for most builds.

The -bin distribution contains only the runtime needed to build and run Gradle. It’s smaller to download and faster to cache on CI systems compared to the -all distribution, which also includes Gradle’s full source code and documentation.

All of those aspects are configurable at the time of generating the Wrapper files with the help of the following command line options:

--gradle-version

The Gradle version used for downloading and executing the Wrapper. The resulting distribution URL is validated before it is written to the properties file.

For Gradle versions starting with major version 9, the version can be specified using only the major or minor version number. In such cases, the latest normal release matching that major or minor version will be used. For example, 9 resolves to the latest 9.x.y release, and 9.1 resolves to the latest 9.1.x release.

The following labels are allowed:

--distribution-type

The Gradle distribution type used for the Wrapper. Available options are bin and all. The default value is bin.

--gradle-distribution-url

The full URL pointing to the Gradle distribution ZIP file. This option makes --gradle-version and --distribution-type obsolete, as the URL already contains this information. This option is valuable if you want to host the Gradle distribution inside your company’s network. The URL is validated before it is written to the properties file.

--gradle-distribution-sha256-sum

The SHA256 hash sum used for verifying the downloaded Gradle distribution.

--network-timeout

The network timeout to use when downloading the Gradle distribution, in ms. The default value is 10000.

--no-validate-url

Disables the validation of the configured distribution URL.

--validate-url

Enables the validation of the configured distribution URL. Enabled by default.

--retries

The number of retries to attempt when downloading the Gradle distribution. The default value is 0, meaning retrying is disabled, downloading will be attempted a single time. Negative values will be ignored and the default will be used.

--retry-back-off-ms

The initial back off in milliseconds to wait between download retries (if enabled, see --retries). The delay doubles after each consecutive failure. The default value is 500 milliseconds, meaning we will wait half a second after the first failed download attempt, 1 second after the second attempt and so on. Negative values will be ignored and the default will be used.

If the distribution URL is configured with --gradle-version or --gradle-distribution-url, the URL is validated by sending a HEAD request in the case of the https scheme or by checking the existence of the file in the case of the file scheme.

Let’s assume the following use case to illustrate combining command line options. You would like to generate the Wrapper with version 9.7.0 and use the -all distribution to include the Gradle source code and documentation alongside the runtime.

The following command-line execution captures those requirements:

$ gradle :wrapper --gradle-version 9.7.0 --distribution-type all
> Task :wrapper

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

As a result, you can find the desired information (the generated distribution URL) in the Wrapper properties file:

distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-all.zip

Let’s have a look at the following project layout to illustrate the expected Wrapper files:

.
├── a-subproject
│   └── build.gradle.kts
├── settings.gradle.kts
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
└── gradlew.bat
.
├── a-subproject
│   └── build.gradle
├── settings.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
└── gradlew.bat

A Gradle project typically provides a settings.gradle(.kts) file and one build.gradle(.kts) file for each subproject. The Wrapper files live alongside in the gradle directory and the root directory of the project.

The following list explains their purpose:

gradle-wrapper.jar

The Wrapper JAR file containing code for downloading the Gradle distribution.

gradle-wrapper.properties

A properties file responsible for configuring the Wrapper runtime behavior e.g. the Gradle version compatible with this version. Note that more generic settings, like configuring the Wrapper to use a proxy, need to go into a different file.

gradlew, gradlew.bat

A shell script and a Windows batch script for executing the build with the Wrapper.

You can go ahead and execute the build with the Wrapper without installing the Gradle runtime. If the project you are working on does not contain those Wrapper files, you will need to generate them.

2. Using the Gradle Wrapper

It is always recommended to execute a build with the Wrapper to ensure a reliable, controlled, and standardized execution of the build. Using the Wrapper looks like running the build with a Gradle installation. Depending on the operating system you either run gradlew or gradlew.bat instead of the gradle command.

The following console output demonstrates the use of the Wrapper on a Windows machine for a Java-based project:

$ gradlew.bat build
Downloading https://services.gradle.org/distributions/gradle-5.0-all.zip
.....................................................................................
Unzipping C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-all\ac27o8rbd0ic8ih41or9l32mv\gradle-5.0-all.zip to C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-al\ac27o8rbd0ic8ih41or9l32mv
Set executable permissions for: C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-all\ac27o8rbd0ic8ih41or9l32mv\gradle-5.0\bin\gradle

BUILD SUCCESSFUL in 12s
1 actionable task: 1 executed

If the Gradle distribution was not provisioned to GRADLE_USER_HOME before, the Wrapper will download it and store it in GRADLE_USER_HOME. Any subsequent build invocation will reuse the existing local distribution as long as the distribution URL in the Gradle properties doesn’t change.

Note
The Wrapper shell script and batch file reside in the root directory of a single or multi-project Gradle build. You will need to reference the correct path to those files in case you want to execute the build from a subproject directory e.g. ../../gradlew tasks.

3. Upgrading the Gradle Wrapper

Projects typically want to keep up with the times and upgrade their Gradle version to benefit from new features and improvements.

The recommended option is to run the wrapper task and provide the target Gradle version as described in Adding the Gradle Wrapper. Using the wrapper task ensures that any optimizations made to the Wrapper shell script or batch file with that specific Gradle version are applied to the project.

As usual, you should commit the changes to the Wrapper files to version control.

Note that running the wrapper task once will update gradle-wrapper.properties only, but leave the Wrapper itself in gradle-wrapper.jar untouched. This is usually fine as new versions of Gradle can be run even with older Wrapper files.

Note
If you want all the Wrapper files to be completely up-to-date, you will need to run the wrapper task a second time.

The following command upgrades the Wrapper to the latest version:

$ ./gradlew :wrapper --gradle-version latest   // MacOs, Linux
$ gradlew.bat :wrapper --gradle-version latest // Windows
BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed

The following command upgrades the Wrapper to a specific version:

$ ./gradlew :wrapper --gradle-version 9.7.0 // MacOs, Linux
$ gradlew.bat :wrapper --gradle-version 9.7.0 // Windows
BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed

Once you have upgraded the Wrapper, you can check that it’s the version you expected by executing ./gradlew --version.

Tip
Don’t forget to run the wrapper task again to download the Gradle distribution binaries (if needed) and update the gradlew and gradlew.bat files.

Another way to upgrade the Gradle version is by manually changing the distributionUrl property in the Wrapper’s gradle-wrapper.properties file. The tip above also applies in this case.

Note
Since Gradle 9.0.0, the version always uses the X.Y.Z format. Using only the major or minor version is not supported in gradle-wrapper.properties.

Customizing the Gradle Wrapper

Most users of Gradle are happy with the default runtime behavior of the Wrapper. However, organizational policies, security constraints or personal preferences might require you to dive deeper into customizing the Wrapper.

Thankfully, the built-in wrapper task exposes numerous options to bend the runtime behavior to your needs. Most configuration options are exposed by the underlying task type Wrapper.

Let’s assume you grew tired of defining the -all distribution type on the command line every time you upgrade the Wrapper. You can save yourself some keyboard strokes by re-configuring the wrapper task.

build.gradle.kts
tasks.wrapper {
    distributionType = Wrapper.DistributionType.ALL
}
build.gradle
tasks.named('wrapper') {
    distributionType = Wrapper.DistributionType.ALL
}

With the configuration in place, running ./gradlew :wrapper --gradle-version 9.7.0 is enough to produce a distributionUrl value in the Wrapper properties file that will request the -all distribution:

distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-all.zip

Check out the API documentation for a more detailed description of the available configuration options. You can also find various samples for configuring the Wrapper in the Gradle distribution.

Authenticated Gradle Distribution Download

The Gradle Wrapper can download Gradle distributions from servers using HTTP Basic Authentication or with a static HTTP Bearer Token. This enables you to host the Gradle distribution on a private protected server.

You can specify a username and password in two different ways depending on your use case: as system properties or directly embedded in the distributionUrl. Credentials in system properties take precedence over the ones embedded in distributionUrl.

Tip

Any Authentication (HTTP Basic / Bearer Token) should only be used with HTTPS URLs and not plain HTTP ones. With Basic Authentication, the user credentials are sent in clear text. With Bearer Token, the Token itself is sent in clear text.

System properties can be specified in the .gradle/gradle.properties file in the user’s home directory or by other means.

To specify the HTTP Basic Authentication credentials, add the following lines to the system properties file:

systemProp.gradle.wrapperUser=username
systemProp.gradle.wrapperPassword=password

Embedding credentials in the distributionUrl in the gradle/wrapper/gradle-wrapper.properties file also works. Please note that this file is to be committed into your source control system.

Tip
Shared credentials embedded in distributionUrl should only be used in a controlled environment.

To specify the HTTP Basic Authentication credentials in distributionUrl, add the following line:

distributionUrl=https://username:password@somehost/path/to/gradle-distribution.zip

This can be used in conjunction with a proxy, authenticated or not. See Accessing the web via a proxy for more information on how to configure the Wrapper to use a proxy.

Warning
Using systemProp.gradle.wrapperUser and systemProp.gradle.wrapperPassword may leak your credentials if the build attempts to download the Gradle Wrapper from a server other than your private one.

To safeguard your credentials, prefix these system properties with your private server’s hostname, replacing all dots (.) with underscores (_).

Note
Hostnames are not case-sensitive thus Gradle converts the hostname in the property name into lowercase. That is, if you wrote EXAMPLE.COM in your Wrapper configuration, then you have to use the string example_com in the system property key.

For example, if your private server hostname is your.example.com, use the following system properties:

systemProp.gradle.your_example_com.wrapperUser=username
systemProp.gradle.your_example_com.wrapperPassword=password

To authenticate with a static HTTP Bearer Token, add the following line to your Gradle properties file:

systemProp.gradle.your_example_com.wrapperToken=some-api-token
Note
If a wrapperToken is specified, it will take precedence over wrapperUser and wrapperPassword system properties (and also over user & password values embedded in the URL). It is strongly recommended to include the hostname in the property to prevent the token from being sent to an unintended server by accident.
Configuring Wrapper Retries

The Gradle Wrapper can be configured to automatically retry downloading the Gradle distribution. This is useful in environments with unstable network connections.

You can configure the number of retries and the back off between attempts in the gradle-wrapper.properties file:

retries=3
retryBackOffMs=1000 # initial delay; doubles on each subsequent failure
Verification of Downloaded Gradle Distributions

The Gradle Wrapper allows for verification of the downloaded Gradle distribution via SHA-256 hash sum comparison. This increases security against targeted attacks by preventing a man-in-the-middle attacker from tampering with the downloaded Gradle distribution.

To enable this feature, download the .sha256 file associated with the Gradle distribution you want to verify.

Downloading the SHA-256 File

You can download the .sha256 file from the stable releases or release candidate and nightly releases. The format of the file is a single line of text that is the SHA-256 hash of the corresponding zip file.

You can also reference the list of Gradle distribution checksums.

Configuring Checksum Verification

Add the downloaded (SHA-256 checksum) hash sum to gradle-wrapper.properties using the distributionSha256Sum property or use --gradle-distribution-sha256-sum on the command-line:

distributionSha256Sum=371cb9fbebbe9880d147f59bab36d61eee122854ef8c9ee1ecf12b82368bcf10

Gradle will report a build failure if the configured checksum does not match the checksum found on the server hosting the distribution. Checksum verification is only performed if the configured Wrapper distribution hasn’t been downloaded yet.

Note
The wrapper task fails if gradle-wrapper.properties contains distributionSha256Sum, but the task configuration does not define a sum. Executing the wrapper task preserves the distributionSha256Sum configuration when the Gradle version does not change.

Verifying the Integrity of the Gradle Wrapper JAR

The Wrapper JAR is a binary file that will be executed on the computers of developers and build servers. As with all such files, you should ensure it’s trustworthy before executing it.

Since the Wrapper JAR is usually checked into a project’s version control system, there is the potential for a malicious actor to replace the original JAR with a modified one by submitting a pull request that only upgrades the Gradle version.

The Setup Gradle GitHub Action automatically validates the Wrapper JAR as part of every build. As of v4, Wrapper validation is built in, no separate action is needed.

Gradle also publishes the checksums of all releases (except for version 3.3 to 4.0.2, which did not generate reproducible JARs), so you can manually verify the integrity of the Wrapper JAR.

Automatically Verifying the Gradle Wrapper JAR on GitHub

The Setup Gradle GitHub Action validates the Wrapper JAR automatically. See the Wrapper validation documentation for configuration options.

Manually Verifying the Gradle Wrapper JAR

You can manually verify the checksum of the Wrapper JAR to ensure that it has not been tampered with by running the following commands on one of the major operating systems.

Manually verifying the checksum of the Wrapper JAR on Linux:

$ cd gradle/wrapper
$ curl --location --output gradle-wrapper.jar.sha256 \
       https://services.gradle.org/distributions/gradle-9.7.0-wrapper.jar.sha256
$ echo " gradle-wrapper.jar" >> gradle-wrapper.jar.sha256
$ sha256sum --check gradle-wrapper.jar.sha256
gradle-wrapper.jar: OK

Manually verifying the checksum of the Wrapper JAR on macOS:

$ cd gradle/wrapper
$ curl --location --output gradle-wrapper.jar.sha256 \
       https://services.gradle.org/distributions/gradle-9.7.0-wrapper.jar.sha256
$ echo " gradle-wrapper.jar" >> gradle-wrapper.jar.sha256
$ sha256sum --check gradle-wrapper.jar.sha256
gradle-wrapper.jar: OK

Manually verifying the checksum of the Wrapper JAR on Windows (using PowerShell):

> $expected = Invoke-RestMethod -Uri https://services.gradle.org/distributions/gradle-9.7.0-wrapper.jar.sha256
> $actual = (Get-FileHash gradle\wrapper\gradle-wrapper.jar -Algorithm SHA256).Hash.ToLower()
> @{$true = 'OK: Checksum match'; $false = "ERROR: Checksum mismatch!`nExpected: $expected`nActual:   $actual"}[$actual -eq $expected]
OK: Checksum match
Verifying the Gradle Wrapper JAR with PGP

In addition to checksum verification, you can verify the Wrapper JAR using its PGP signature. Gradle publishes PGP signatures for all distribution artifacts, including the Wrapper JAR.

First, import the Gradle public key (see Gradle Keys for details):

$ curl --location https://keys.openpgp.org/vks/v1/by-fingerprint/7B79310A0A3F8B081C8A0B4EE0A56BE8F025F892 | gpg --import

Then download the signature file and verify:

$ cd gradle/wrapper
$ curl --location --output gradle-wrapper.jar.asc \
       https://services.gradle.org/distributions/gradle-9.7.0-wrapper.jar.asc
$ gpg --verify gradle-wrapper.jar.asc gradle-wrapper.jar

A successful verification shows Good signature from "Gradle Inc. <info@gradle.com>".

Troubleshooting a Checksum Mismatch

If the checksum does not match the one you expected, chances are the wrapper task wasn’t executed with the upgraded Gradle distribution.

You should first check whether the actual checksum matches a different Gradle version.

Here are the commands you can run on the major operating systems to generate the actual checksum of the Wrapper JAR.

Generating the checksum of the Wrapper JAR on Linux:

$ sha256sum gradle/wrapper/gradle-wrapper.jar
d81e0f23ade952b35e55333dd5f1821585e887c6d24305aeea2fbc8dad564b95 gradle/wrapper/gradle-wrapper.jar

Generating the actual checksum of the Wrapper JAR on macOS:

$ sha256sum gradle/wrapper/gradle-wrapper.jar
d81e0f23ade952b35e55333dd5f1821585e887c6d24305aeea2fbc8dad564b95 gradle/wrapper/gradle-wrapper.jar

Generating the actual checksum of the Wrapper JAR on Windows (using PowerShell):

> (Get-FileHash gradle\wrapper\gradle-wrapper.jar -Algorithm SHA256).Hash.ToLower()
d81e0f23ade952b35e55333dd5f1821585e887c6d24305aeea2fbc8dad564b95

Once you know the actual checksum, check whether it’s listed on https://gradle.org/release-checksums/. If it is listed, you have verified the integrity of the Wrapper JAR. If the version of Gradle that generated the Wrapper JAR doesn’t match the version in gradle/wrapper/gradle-wrapper.properties, it’s safe to run the :wrapper task again to update the Wrapper JAR.

If the checksum is not listed on the page, the Wrapper JAR might be from a milestone, release candidate, or nightly build or may have been generated by Gradle 3.3 to 4.0.2. Try to find out how it was generated but treat it as untrustworthy until proven otherwise. If you think the Wrapper JAR was compromised, please let the Gradle team know by sending an email to security@gradle.com.

Gradle Daemon

The Gradle Daemon is a long-lived, persistent process that runs in the background and hosts Gradle’s execution engine. It dramatically reduces build times using caching, runtime optimizations, and eliminating JVM startup overhead.

The Gradle Client vs. the Gradle Daemon

To run any Gradle command, your system starts two separate Java Virtual Machine (JVM) processes, which can use different Java versions.

Understanding their distinct roles and how their respective JVM versions are determined is key to effective build configuration and debugging:

Process Role JVM Version Source

Gradle Client JVM

Process that starts when you run gradle or ./gradlew and stays alive for the entire build invocation.
It connects to (or starts) a Daemon, sends the build request, and streams output back to the console.

Uses the JVM that launches the wrapper script (e.g. JAVA_HOME, java on PATH, or your IDE).

Gradle Daemon JVM

Long-lived process that executes the build and caches state between runs.

Uses the JVM set by:
- org.gradle.java.home (properties)
- Tooling API requests (e.g., IDE)
- Daemon JVM toolchains
By default, the Gradle Client will look for compatible daemons* or start a new one if none are available.

*Gradle reuses an existing daemon only if its Java home/version and JVM args (e.g., org.gradle.jvmargs, GRADLE_OPTS) are identical. Changing JVM args (like increasing memory) will spawn a new daemon. In the rare case where the daemon has been disabled (--no-daemon or -Dorg.gradle.daemon=false) and the Client process is compatible, the Gradle Daemon uses the JVM that launched the Gradle Client.

Understanding the Daemon

A daemon is a computer program that runs as a background process rather than being under the direct control of an interactive user.

Gradle runs on the Java Virtual Machine (JVM) and uses several supporting libraries with non-trivial initialization time. Startups can be slow. The Gradle Daemon solves this problem.

The Gradle Daemon is a long-lived background process that reduces the time it takes to run a build.

The Gradle Daemon reduces build times by:

  • Caching project information across builds

  • Running in the background so every Gradle build doesn’t have to wait for JVM startup

  • Benefiting from continuous runtime optimization in the JVM

  • Watching the file system to calculate exactly what needs to be rebuilt before you run a build

The Gradle Client sends the Gradle Daemon build information such as command line arguments, project directories, and environment variables so that it can run the build. The Daemon is responsible for resolving dependencies, executing build scripts, creating and running tasks; when it is done, it sends the client the output. Communication between the client and the Daemon happens via a local socket connection.

Daemons use the JVM’s default minimum heap size.

If the requested build environment does not specify a maximum heap size, the Daemon uses up to 512MB of heap. 512MB is adequate for most builds. Larger builds with hundreds of subprojects, configuration, and source code may benefit from a larger heap size.

Check Daemon status

To get a list of running Daemons and their statuses, use the --status command:

$ gradle --status
   PID STATUS   INFO
 28486 IDLE     7.5
 34247 BUSY     7.5

Currently, a given Gradle version can only connect to Daemons of the same version. This means the status output only shows Daemons spawned running the same version of Gradle as the current project.

Find Daemons

If you have installed the Java Development Kit (JDK), you can view live daemons with the jps command.

$ jps
33920 Jps
27171 GradleDaemon
22792

Live Daemons appear under the name GradleDaemon. Because this command uses the JDK, you can view Daemons running any version of Gradle.

Enable Daemon

Gradle enables the Daemon by default since Gradle 3.0. If your project doesn’t use the Daemon, you can enable it for a single build with the --daemon flag when you run a build:

$ gradle <task> --daemon

This flag overrides any settings that disable the Daemon in your project or user gradle.properties files.

To enable the Daemon by default in older Gradle versions, add the following setting to the gradle.properties file in the project root or your Gradle User Home (GRADLE_USER_HOME):

gradle.properties
org.gradle.daemon=true

Disable Daemon

You can disable the Daemon in multiple ways but there are important considerations:

Single-use Daemon

If the JVM args of the client process don’t match what the build requires, a single-used Daemon (disposable JVM) is created. This means the Daemon is required for the build, so it is created, used, and then stopped at the end of the build.

No Daemon

If the JAVA_OPTS and GRADLE_OPTS match org.gradle.jvmargs, the Daemon will not be used at all since the build happens in the client JVM.

Disable for a build

To disable the Daemon for a single build, pass the --no-daemon flag when you run a build:

$ gradle <task> --no-daemon

This flag overrides any settings that enable the Daemon in your project including the gradle.properties files.

Disable for a project

To disable the Daemon for all builds of a project, add org.gradle.daemon=false to the gradle.properties file in the project root.

Disable for a user

On Windows, this command disables the Daemon for the current user:

(if not exist "%USERPROFILE%/.gradle" mkdir "%USERPROFILE%/.gradle") && (echo. >> "%USERPROFILE%/.gradle/gradle.properties" && echo org.gradle.daemon=false >> "%USERPROFILE%/.gradle/gradle.properties")

On UNIX-like operating systems, the following Bash shell command disables the Daemon for the current user:

mkdir -p ~/.gradle && echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties
Disable globally

There are two recommended ways to disable the Daemon globally across an environment:

  • add org.gradle.daemon=false to the $GRADLE_USER_HOME/gradle.properties` file

  • add the flag -Dorg.gradle.daemon=false to the GRADLE_OPTS environment variable

Don’t forget to make sure your JVM arguments and GRADLE_OPTS / JAVA_OPTS match if you want to completely disable the Daemon and not simply invoke a single-use one.

Stop Daemon

It can be helpful to stop the Daemon when troubleshooting or debugging a failure.

Daemons automatically stop given any of the following conditions:

  • Available system memory is low

  • Daemon has been idle for 3 hours

To stop running Daemon processes, use the following command:

$ gradle --stop

This terminates all Daemon processes started with the same version of Gradle used to execute the command.

You can also kill Daemons manually with your operating system. To find the PIDs for all Daemons regardless of Gradle version, see Find Daemons.

Troubleshooting the Daemon

The Gradle Daemon is a long-lived background process, as such, it can sometimes encounter issues.

If builds start behaving unexpectedly, try stopping and restarting the Daemon:

$ gradle --stop

If you see a warning like: Multiple Gradle daemons might be spawned because the Gradle JDK and JAVA_HOME locations are different.

Gradle is telling you that it’s using more than one Java version across your environment. This can lead to multiple daemons being started, increasing memory usage unnecessarily.

To resolve this, make sure your Java versions match across:

  1. Your environment (JAVA_HOME)

  2. Your build script (if using toolchains)

  3. Your IDE’s configured JDK

To check JAVA_HOME, run this in your terminal:

echo $JAVA_HOME

Your project may be using a specific toolchain in your build.gradle(.kts) file. Check for similar code:

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

If your build uses a toolchain, ensure it matches the JAVA_HOME value, or at least know they differ intentionally. You should also check your IDE settings to make sure they match as well.

Daemon Logs

The Gradle Daemon writes detailed log files to help diagnose build issues and understand daemon behavior.

These logs are stored in the Gradle User Home directory under daemon/<gradle-version>/:

~/.gradle/daemon/9.4.0/
├── daemon-<pid>.out.log
├── daemon-<pid>.out.log
└── registry.bin

Each daemon process creates its own log file named daemon-<pid>.out.log, where <pid> is the process ID of that daemon instance.

Automatic Log Cleanup

Gradle automatically cleans up old daemon logs to prevent the daemon directory from growing indefinitely.

By default, daemon logs older than 14 days are deleted during build execution as part of Gradle’s periodic cache cleanup. This cleanup runs alongside other cache cleanup operations.

The cleanup is triggered during builds:

  • In the background between builds when using the daemon (default behavior)

  • In the foreground after the build when using --no-daemon

The 14-day retention period for daemon logs can be configured via an init script, just like other cache retention periods. See Configuring cleanup of caches and distributions for configuration examples.

Daemon JVM Toolchains

By default, the Gradle Daemon runs with the same JVM installation that started the build. Gradle defaults to the current shell path and JAVA_HOME environment variable to locate a usable JVM.

Alternatively, a different JVM installation can be specified for the build using the org.gradle.java.home Gradle property or programmatically through the Tooling API.

Building on the toolchain feature, you can now use declarative criteria to specify the JVM requirements for the build.

If the Daemon JVM criteria configuration is provided, it takes precedence over JAVA_HOME and org.gradle.java.home.

Daemon JVM criteria

The Daemon JVM criteria is controlled by the updateDaemonJvm task, similar to how the wrapper task updates the wrapper properties file.

Warning
This process requires toolchain download repositories to be configured. See below for details.

When the task runs, it creates or updates the criteria in the gradle/gradle-daemon-jvm.properties file.

To configure the generation, you can use command line options:

$ ./gradlew updateDaemonJvm --jvm-version=17

Or configure the task in the build script of the root project:

build.gradle.kts
tasks.named<UpdateDaemonJvm>("updateDaemonJvm") {
    languageVersion = JavaLanguageVersion.of(17)
}
build.gradle
tasks.named("updateDaemonJvm") {
    languageVersion = JavaLanguageVersion.of(17)
}

And then run the task:

$ ./gradlew updateDaemonJvm

Both of these actions will produce a file like the following one:

gradle/gradle-daemon-jvm.properties
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://example.com/...
toolchainUrl.FREE_BSD.X86_64=https\://example.com/...
toolchainUrl.LINUX.AARCH64=https\://example.com/...
toolchainUrl.LINUX.X86_64=https\://example.com/...
toolchainUrl.MAC_OS.AARCH64=https\://example.com/...
toolchainUrl.MAC_OS.X86_64=https\://example.com/...
toolchainUrl.UNIX.AARCH64=https\://example.com/...
toolchainUrl.UNIX.X86_64=https\://example.com/...
toolchainUrl.WINDOWS.X86_64=https\://example.com/...
toolchainVersion=17

If you run the updateDaemonJvm task without any arguments, and the properties file does not exist, then the version of the current JVM used by the Daemon will be used.

On the next execution of the build, the Gradle client will use this file to locate a compatible JVM installation and start the Daemon with it.

Similar to the wrapper, the generated gradle-daemon-jvm.properties file should be checked into version control. This ensures that any developer or CI server running the build will use the same JVM version.

Specifying a JVM vendor

The JVM vendor, like the JVM version, can be used as a criteria to select a compatible JVM installation for the build. If no vendor is specified, Gradle considers all vendors compatible.

By default, running updateDaemonJvm to create the gradle-daemon-jvm.properties file will not generate a JVM vendor criteria. To specify a vendor, either configure it in the build script, using the same syntax as the Java toolchain spec, or pass it on the command line:

$ ./gradlew updateDaemonJvm --jvm-version=17 --jvm-vendor=adoptium

List of recognized vendors:

Known Vendors Acceptable Strings toolchainVendor Value

Adoptium / Eclipse Temurin

adoptium, temurin, eclipse foundation

ADOPTIUM

AdoptOpenJDK

adoptopenjdk, aoj

ADOPTOPENJDK

Amazon Corretto

amazon, corretto

AMAZON

Apple

apple

APPLE

Azul Zulu

azul, zulu

AZUL

BellSoft

bellsoft, liberica

BELLSOFT

GraalVM

graalvm, graal vm

GRAAL_VM

Hewlett Packard

hp, hewlett

HEWLETT_PACKARD

IBM

ibm, semeru, international business machines corporation

IBM

JetBrains

jetbrains, jbr

JETBRAINS

Microsoft

microsoft

MICROSOFT

Oracle

oracle

ORACLE

SAP

sap

SAP

Tencent

tencent, kona

TENCENT

Some vendors will be recognized from more than one set of characters. All vendor strings are case-insensitive. You can view the list of recognized vendors by running ./gradlew help --task updateDaemonJvm.

If the specified vendor is not one of the recognized equivalents, Gradle will match it exactly. For example, "MyCustomJVM" would require an exact match of the vendor name.

Requesting a native-image capable JDK

Both the CLI options and the task configuration allow to request a JDK that is native-image capable.

$ ./gradlew updateDaemonJvm --jvm-version=17 --native-image-capable

See the toolchain documentation section for more information.

Auto-detection and auto-provisioning

The Daemon JVM is auto-detected using the same logic as project JVM toolchains.

With auto-provisioning, the logic is simpler, as Gradle can only look up a download URL matching the platform inside the gradle-daemon-jvm.properties file. The URL is then used to download a JDK if none can be found locally.

The properties used for disabling auto-detection and auto-provisioning affect the Daemon toolchain resolution logic:

org.gradle.java.installations.auto-detect=false
org.gradle.java.installations.auto-download=false
Configuring provisioning URLs
Note
There are currently no CLI options for configuring this.

By default, the updateDaemonJvm task attempts to generate download URLs for JDKs on various platforms (OS and architecture) that match the specified criteria. Gradle needs to consider more than the current running platform as the build could be run on different platforms.

Gradle sets, by convention, build platforms based on architectures X86_64 and AARCH64 for the following operating systems:

These platforms can be configured through the toolchainPlatforms property of the UpdateDaemonJvm task.

build.gradle.kts
tasks.named<UpdateDaemonJvm>("updateDaemonJvm") {
    val myPlatforms = mutableListOf(
        BuildPlatformFactory.of(
            org.gradle.platform.Architecture.AARCH64,
            org.gradle.platform.OperatingSystem.MAC_OS
        )
    )
    toolchainPlatforms.set(myPlatforms)
}
build.gradle
tasks.named("updateDaemonJvm") {
    def myPlatforms = [
        BuildPlatformFactory.of(
            org.gradle.platform.Architecture.AARCH64,
            org.gradle.platform.OperatingSystem.MAC_OS
        )
    ]
    toolchainPlatforms.set(myPlatforms)
}

Gradle resolves JDK download URLs for these platforms by using the configured toolchain download repositories. If no such repositories are configured and the toolchainPlatforms property has at least one value, the updateDaemonJvm task will fail.

Alternatively, users can directly configure the JDK URLs for specific platforms using the toolchainDownloadUrls property. This property is a Map<BuildPlatform, URI> and can be configured as shown in the following example:

build.gradle.kts
tasks.named<UpdateDaemonJvm>("updateDaemonJvm") {
    toolchainDownloadUrls = mapOf(
        BuildPlatformFactory.of(org.gradle.platform.Architecture.AARCH64, org.gradle.platform.OperatingSystem.MAC_OS) to uri("https://server?platform=MAC_OS.AARCH64"),
        BuildPlatformFactory.of(org.gradle.platform.Architecture.AARCH64, org.gradle.platform.OperatingSystem.WINDOWS) to uri("https://server?platform=WINDOWS.AARCH64")
    )
}
build.gradle
tasks.named("updateDaemonJvm") {
    toolchainDownloadUrls = [(BuildPlatformFactory.of(org.gradle.platform.Architecture.AARCH64, org.gradle.platform.OperatingSystem.MAC_OS)) : uri("https://server?platform=MAC_OS.AARCH64"),
                             (BuildPlatformFactory.of(org.gradle.platform.Architecture.AARCH64, org.gradle.platform.OperatingSystem.WINDOWS)) : uri("https://server?platform=WINDOWS.AARCH64")]
}
Tip
A full package name is required for org.gradle.platform.Architecture and org.gradle.platform.OperatingSystem due to a naming conflict with other types in different packages, which are resolved first alphabetically.

Running ./gradlew updateDaemonJvm produces the following:

#This file is generated by updateDaemonJvm
toolchainUrl.MAC_OS.AARCH64=https\://server?platform\=MAC_OS.AARCH64
toolchainUrl.WINDOWS.AARCH64=https\://server?platform\=WINDOWS.AARCH64
toolchainVersion=17

If you want to disable the generation of URLs by the updateDaemonJvm task:

build.gradle.kts
tasks.named<UpdateDaemonJvm>("updateDaemonJvm") {
    toolchainDownloadUrls.empty()
}
build.gradle
tasks.named("updateDaemonJvm") {
    toolchainPlatforms = []
}

Removing all platforms means that there is no longer a need for the toolchain download repositories to be configured.

Tools & IDEs

The Gradle Tooling API used by IDEs and other tools to integrate with Gradle always uses the Gradle Daemon to execute builds. If you execute Gradle builds from within your IDE, you already use the Gradle Daemon. There is no need to enable it for your environment.

Continuous Integration

We recommend using the Daemon for developer machines and Continuous Integration (CI) servers.

Compatibility

Gradle starts a new Daemon if no idle or compatible Daemons exist.

The following values determine compatibility:

  • Requested build environment, including the following:

    • Java version

    • JVM attributes

    • JVM properties

  • Gradle version

Compatibility is based on exact matches of these values. For example:

  • If a Daemon is available with a Java 8 runtime, but the requested build environment calls for Java 10, then the Daemon is not compatible.

  • If a Daemon is available running Gradle 7.0, but the current build uses Gradle 7.4, then the Daemon is not compatible.

Certain properties of a Java runtime are immutable: they cannot be changed once the JVM has started. The following JVM system properties are immutable:

  • file.encoding

  • user.language

  • user.country

  • user.variant

  • java.io.tmpdir

  • javax.net.ssl.keyStore

  • javax.net.ssl.keyStorePassword

  • javax.net.ssl.keyStoreType

  • javax.net.ssl.trustStore

  • javax.net.ssl.trustStorePassword

  • javax.net.ssl.trustStoreType

  • com.sun.management.jmxremote

The following JVM attributes controlled by startup arguments are also immutable:

  • The maximum heap size (the -Xmx JVM argument)

  • The minimum heap size (the -Xms JVM argument)

  • The boot classpath (the -Xbootclasspath argument)

  • The "assertion" status (the -ea argument)

If the requested build environment requirements for any of these properties and attributes differ from the Daemon’s JVM requirements, the Daemon is not compatible.

Note
For more information about build environments, see the build environment documentation.

Performance Impact

The Daemon can reduce build times by 15-75% when you build the same project repeatedly.

In between builds, the Daemon waits idly for the next build. As a result, your machine only loads Gradle into memory once for multiple builds instead of once per build. This is a significant performance optimization.

Runtime Code Optimizations

The JVM gains significant performance from runtime code optimization: optimizations applied to code while it runs.

JVM implementations like OpenJDK’s Hotspot progressively optimize code during execution. Consequently, subsequent builds can be faster purely due to this optimization process.

With the Daemon, perceived build times can drop dramatically between a project’s 1st and 10th builds.

Memory Caching

The Daemon enables in-memory caching across builds. This includes classes for plugins and build scripts.

Similarly, the Daemon maintains in-memory caches of build data, such as the hashes of task inputs and outputs for incremental builds.

Performance Monitoring

Gradle actively monitors heap usage to detect memory leaks in the Daemon.

When a memory leak exhausts available heap space, the Daemon:

  1. Finishes the currently running build.

  2. Restarts before running the next build.

Gradle enables this monitoring by default.

To disable this monitoring, set the org.gradle.daemon.performance.enable-monitoring Daemon option to false.

You can do this on the command line with the following command:

$ gradle <task> -Dorg.gradle.daemon.performance.enable-monitoring=false

Or you can configure the property in the gradle.properties file in the project root or your GRADLE_USER_HOME (Gradle User Home):

gradle.properties
org.gradle.daemon.performance.enable-monitoring=false

Gradle-managed Directories and Caches

Gradle uses two main directories to perform and manage its work and stores several caches under them:

author gradle 2

1. Gradle User Home directory

By default, the Gradle User Home (~/.gradle or C:\Users\<USERNAME>\.gradle) stores global configuration properties, initialization scripts, caches, and log files.

It can be set with the environment variable GRADLE_USER_HOME.

Tip
Not to be confused with the GRADLE_HOME, the optional installation directory for Gradle.

It is roughly structured as follows:

├── caches              // (1)
├── daemon              // (2)
├── init.d              // (3)
├── jdks                // (4)
├── wrapper
│   └── dists           // (5)
└── gradle.properties   // (6)
  1. Caches.

  2. Registry and logs of the Gradle Daemon.

  3. Global initialization scripts.

  4. JDKs downloaded by the toolchain support.

  5. Distributions downloaded by the Gradle Wrapper.

  6. Global Gradle configuration properties.

2. Project Root directory

The project root directory contains all source files from your project.

It also contains files and directories Gradle generates, such as .gradle and build.

While the former are usually checked into source control, the latter are transient files Gradle uses to support features like incremental builds.

The anatomy of a typical project root directory looks as follows:

├── .gradle                 // (1)
├── build                   // (2)
├── gradle
│   └── wrapper             // (3)
├── gradle.properties       // (4)
├── gradlew                 // (5)
├── gradlew.bat             // (5)
├── settings.gradle.kts     // (6)
├── subproject-one          // (7)
|   └── build.gradle.kts    // (8)
├── subproject-two          // (7)
|   └── build.gradle.kts    // (8)
└── ⋮
  1. Project-specific cache directory generated by Gradle.

  2. The build directory of this project into which Gradle generates all build artifacts.

  3. Contains the JAR file and configuration of the Gradle Wrapper.

  4. Project-specific Gradle configuration properties.

  5. Scripts for executing builds using the Gradle Wrapper.

  6. The project’s settings file where the list of subprojects is defined.

  7. Usually, a project is organized into one or multiple subprojects.

  8. Each subproject has its own Gradle build script.

3. Caches

Gradle uses several on-disk caches to make repeat builds faster. These caches live under the directories described in Gradle User Home and Project Root. In most cases they do not require manual management. Gradle creates, uses, and cleans them automatically.

Version-specific caches

These caches live under caches/<gradle-version>/ in the Gradle User Home and are tied to a specific Gradle release. They are recreated on first use of that Gradle version.

Directory Contents

<version>/generated-gradle-jars

JARs that Gradle generates on first use of the Gradle version.

<version>/kotlin-dsl

Compiled Kotlin DSL scripts and accessors.

<version>/scripts

Compiled Groovy DSL scripts.

Shared caches

These caches live directly under caches/ in the Gradle User Home and are shared across Gradle versions. The trailing number in each directory name is the on-disk format version; see Format versions in cache directory names.

Directory Contents

build-cache-1

Local Build Cache entries: task outputs keyed by input hashes.

jars-9

Instrumented JARs produced by Gradle’s classpath instrumentation.

modules-2

Downloaded module dependencies (POMs, JARs, metadata). See Dependency caching.

transforms-3

Outputs of artifact transforms.

Project-local caches

These caches live under .gradle/ in the project root and are specific to a single project.

Directory Contents

.gradle/<version>

Version-specific build state for incremental build (snapshots of task inputs and outputs from the previous build).

.gradle/configuration-cache

Configuration Cache state for this project: the serialized task graph, configuration inputs, and metadata used to skip the configuration phase on subsequent builds.

Format versions in cache directory names

Several caches use a trailing format version in their directory name (for example, modules-2, jars-9, transforms-3, build-cache-1). The number changes when Gradle changes the on-disk layout for that cache.

When multiple format versions of the same cache exist on disk (for example, both transforms-3 and transforms-4), the older one is left behind from a previous Gradle version and is cleaned up after the inactivity threshold. Only the current format is being written by the current Gradle version.

Cleanup of caches and distributions

Gradle automatically cleans its caches in both the Gradle User Home and the project directory.

By default, the cleanup runs in the background when the Gradle daemon is stopped or shut down.

If using --no-daemon, it runs in the foreground after the build session.

Gradle User Home cleanup

The following cleanup strategies are applied periodically (by default, once every 24 hours):

  • Version-specific caches in all caches/<GRADLE_VERSION>/ directories are checked for whether they are still in use.

    If not, directories for release versions are deleted after 30 days of inactivity, and snapshot versions after 7 days.

  • Shared caches in caches/ (e.g., jars-*) are checked for whether they are still in use.

    If no Gradle version still uses them, they are deleted.

  • Files in shared caches used by the current Gradle version in caches/ (e.g., jars-9 or modules-2) are checked for when they were last accessed.

    Depending on whether the file can be recreated locally or downloaded from a remote repository, it will be deleted after 7 or 30 days, respectively.

  • Gradle distributions in wrapper/dists/ are checked for whether they are still in use, i.e., whether there’s a corresponding version-specific cache directory.

    Unused distributions are deleted.

  • Daemon log files in daemon/<GRADLE_VERSION>/ directories are checked for their last modification time.

    Log files older than 14 days are automatically deleted to prevent the daemon directory from growing indefinitely.

Project cache cleanup

From version 4.10 onwards, Gradle automatically cleans the project-specific cache directory.

After building the project, version-specific cache directories in .gradle/9.7.0/ are checked periodically (at most, every 24 hours) to determine whether they are still in use. They are deleted if they haven’t been used for 7 days.

Configuring cleanup of caches and distributions

The retention periods of the various caches can be configured.

Caches are classified into six categories:

  1. Released wrapper distributions: Distributions and related version-specific caches corresponding to released versions (e.g., 4.6.2 or 8.0).

    Default retention for unused versions is 30 days.

  2. Snapshot wrapper distributions: Distributions and related version-specific caches corresponding to snapshot versions (e.g. 7.6-20221130141522+0000).

    Default retention for unused versions is 7 days.

  3. Downloaded resources: Shared caches downloaded from a remote repository (e.g., cached dependencies).

    Default retention for unused resources is 30 days.

  4. Created resources: Shared caches that Gradle creates during a build (e.g., artifact transforms).

    Default retention for unused resources is 7 days.

  5. Build cache: The local build cache (e.g., build-cache-1).

    Default retention for unused build cache entries is 7 days.

  6. Daemon logs: Log files from Gradle Daemon processes in daemon/<GRADLE_VERSION>/ directories.

    Default retention for daemon logs is 14 days.

The retention period for each category can be configured independently via an init script in the Gradle User Home:

gradleUserHome/init.d/cache-settings.init.gradle.kts
beforeSettings {
    caches {
        releasedWrappers.setRemoveUnusedEntriesAfterDays(45)
        snapshotWrappers.setRemoveUnusedEntriesAfterDays(10)
        downloadedResources.setRemoveUnusedEntriesAfterDays(45)
        createdResources.setRemoveUnusedEntriesAfterDays(10)
        buildCache.setRemoveUnusedEntriesAfterDays(5)
        daemonLogs.setRemoveUnusedEntriesAfterDays(14)
    }
}
gradleUserHome/init.d/cache-settings.init.gradle
beforeSettings { settings ->
    settings.caches {
        releasedWrappers.removeUnusedEntriesAfterDays = 45
        snapshotWrappers.removeUnusedEntriesAfterDays = 10
        downloadedResources.removeUnusedEntriesAfterDays = 45
        createdResources.removeUnusedEntriesAfterDays = 10
        buildCache.removeUnusedEntriesAfterDays = 5
        daemonLogs.removeUnusedEntriesAfterDays = 14
    }
}

The frequency at which cache cleanup is invoked is also configurable.

There are three possible settings:

  1. DEFAULT: Cleanup is performed periodically in the background (currently once every 24 hours).

  2. DISABLED: Never cleanup Gradle User Home.

    This is useful in cases where Gradle User Home is ephemeral or delaying cleanup is desirable until an explicit point.

  3. ALWAYS: Cleanup is performed at the end of each build session.

    This is useful in cases where it’s desirable to ensure that cleanup has occurred before proceeding.

    However, this performs cache cleanup during the build (rather than in the background), which can be expensive, so this option should only be used when necessary.

To disable cache cleanup:

gradleUserHome/init.d/cache-settings.init.gradle.kts
beforeSettings {
    caches {
        cleanup = Cleanup.DISABLED
    }
}
gradleUserHome/init.d/cache-settings.init.gradle
beforeSettings { settings ->
    settings.caches {
        cleanup = Cleanup.DISABLED
    }
}
Note
Cache cleanup settings can only be configured via init scripts and should be placed under the init.d directory in Gradle User Home. This effectively couples the configuration of cache cleanup to the Gradle User Home those settings apply to and limits the possibility of different conflicting settings from different projects being applied to the same directory.
Multiple versions of Gradle sharing a Gradle User Home

It is common to share a single Gradle User Home between multiple versions of Gradle.

As stated above, caches in Gradle User Home are version-specific. Different versions of Gradle will perform maintenance on only the version-specific caches associated with each version.

On the other hand, some caches are shared between versions (e.g., the dependency artifact cache or the artifact transform cache).

Beginning with Gradle version 8.0, the cache cleanup settings can be configured to custom retention periods. However, older versions have fixed retention periods (7 or 30 days, depending on the cache). These shared caches could be accessed by versions of Gradle with different settings to retain cache artifacts.

This means that:

  • If the retention period is not customized, all versions that perform cleanup will have the same retention periods. There will be no effect due to sharing a Gradle User Home with multiple versions.

  • If the retention period is customized for Gradle versions greater than or equal to version 8.0 to use retention periods shorter than the previously fixed periods, there will also be no effect.

    The versions of Gradle aware of these settings will cleanup artifacts earlier than the previously fixed retention periods, and older versions will effectively not participate in the cleanup of shared caches.

  • If the retention period is customized for Gradle versions greater than or equal to version 8.0 to use retention periods longer than the previously fixed periods, the older versions of Gradle may clean the shared caches earlier than what is configured.

    In this case, if it is desirable to maintain these shared cache entries for newer versions for longer retention periods, they will not be able to share a Gradle User Home with older versions. They will need to use a separate directory.

Another consideration when sharing the Gradle User Home with versions of Gradle before version 8.0 is that the DSL elements to configure the cache retention settings are unavailable in earlier versions, so this must be accounted for in any init script shared between versions. This can easily be handled by conditionally applying a version-compliant script.

Note
The version-compliant script should reside somewhere other than the init.d directory (such as a sub-directory), so it is not automatically applied.

To configure cache cleanup in a version-safe manner:

gradleUserHome/init.d/cache-settings.init.gradle.kts
if (GradleVersion.current() >= GradleVersion.version("8.0")) {
    apply(from = "gradle8/cache-settings.init.gradle.kts")
}
gradleUserHome/init.d/cache-settings.init.gradle
if (GradleVersion.current() >= GradleVersion.version('8.0')) {
    apply from: "gradle8/cache-settings.init.gradle"
}

Version-compliant cache configuration script:

gradleUserHome/init.d/gradle8/cache-settings.init.gradle.kts
beforeSettings {
    caches {
        releasedWrappers { setRemoveUnusedEntriesAfterDays(45) }
        snapshotWrappers { setRemoveUnusedEntriesAfterDays(10) }
        downloadedResources { setRemoveUnusedEntriesAfterDays(45) }
        createdResources { setRemoveUnusedEntriesAfterDays(10) }
        buildCache { setRemoveUnusedEntriesAfterDays(5) }
    }
}
gradleUserHome/init.d/gradle8/cache-settings.init.gradle
beforeSettings { settings ->
    settings.caches {
        releasedWrappers.removeUnusedEntriesAfterDays = 45
        snapshotWrappers.removeUnusedEntriesAfterDays = 10
        downloadedResources.removeUnusedEntriesAfterDays = 45
        createdResources.removeUnusedEntriesAfterDays = 10
        buildCache.removeUnusedEntriesAfterDays = 5
    }
}
Cache marking

Beginning with Gradle version 8.1, Gradle supports marking caches with a CACHEDIR.TAG file.

It follows the format described in the Cache Directory Tagging Specification. The purpose of this file is to allow tools to identify the directories that do not need to be searched or backed up.

By default, the directories caches, wrapper/dists, daemon, and jdks in the Gradle User Home are marked with this file.

Configuring cache marking

The cache marking feature can be configured via an init script in the Gradle User Home:

gradleUserHome/init.d/cache-settings.init.gradle.kts
beforeSettings {
    caches {
        // Disable cache marking for all caches
        markingStrategy = MarkingStrategy.NONE
    }
}
gradleUserHome/init.d/cache-settings.init.gradle
beforeSettings { settings ->
    settings.caches {
        // Disable cache marking for all caches
        markingStrategy = MarkingStrategy.NONE
    }
}
Note
Cache marking settings can only be configured via init scripts and should be placed under the init.d directory in Gradle User Home. This effectively couples the configuration of cache marking to the Gradle User Home to which those settings apply and limits the possibility of different conflicting settings from different projects being applied to the same directory.

Build Environment Configuration

Configuring the build environment is a powerful way to customize the build process. There are many mechanisms available. By leveraging these mechanisms, you can make your Gradle builds more flexible and adaptable to different environments and requirements.

Available mechanisms

Gradle provides multiple mechanisms for configuring the behavior of Gradle itself and specific projects:

Mechanism Information Example

Command line interface

Flags that configure build behavior and Gradle features

--rerun

Project properties

Properties specific to your Gradle project

TestFilter::isFailOnNoMatchingTests=false

System properties

Properties that are passed to the Gradle runtime (JVM)

http.proxyHost=somehost.org

Gradle properties

Properties that configure Gradle settings and the Java process that executes your build

org.gradle.logging.level=quiet

Environment variables

Properties that configure build behavior based on the environment

JAVA_HOME

Priority for configurations

When configuring Gradle behavior, you can use these methods, but you must consider their priority.

Gradle reads gradle.properties configurations in the following order (highest precedence first):

Priority Method Location Notes

1

Command-line

> Command line

Flags have precedence over properties and environment variables

2

System properties

> Project Root Dir

Stored in a gradle.properties file

3

Gradle properties

> GRADLE_USER_HOME
> Project Root Dir
> GRADLE_HOME

Stored in a gradle.properties file

4

Environment variables

> Environment

Sourced by the environment that executes Gradle

Here are all possible configurations of specifying the JDK installation directory in order of priority:

  1. Command Line

    $ ./gradlew exampleTask -Dorg.gradle.java.home=/path/to/your/java/home --scan
  2. Gradle Properties File

    gradle.properties
    org.gradle.java.home=/path/to/your/java/home
  3. Environment Variable

    $ export JAVA_HOME=/path/to/your/java/home

The gradle.properties file

Gradle properties, system properties, and project properties can be found in the gradle.properties file:

gradle.properties
# Gradle properties
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.jvmargs=-Duser.language=en -Duser.country=US -Dfile.encoding=UTF-8

# System properties
systemProp.pts.enabled=true
systemProp.log4j2.disableJmx=true
systemProp.file.encoding = UTF-8

# Project properties
kotlin.code.style=official
android.nonTransitiveRClass=false
spring-boot.version = 2.2.1.RELEASE

You can place the gradle.properties file in the root directory of your project, the Gradle user home directory (GRADLE_USER_HOME), or the directory where Gradle is optionally installed (GRADLE_HOME).

When resolving properties, Gradle first looks in the user-level gradle.properties file located in GRADLE_USER_HOME, then in the project-level gradle.properties file, and finally in the gradle.properties file located in GRADLE_HOME, with user-level properties taking precedence over project-level and installation-level properties.

Note

See our Best Practice recommendation on where to locate gradle.properties for further guidance.

Project properties

Project properties are specific to your Gradle project, they can be used to customize your build. Project properties can be accessed in your build files and get passed in from an external source when your build is executed. Project properties can be retrieved in build scripts lazily using providers.gradleProperty().

Setting a project property

You have four options to add project properties, listed in order of priority:

  1. Command Line: You can add project properties directly to your Project object via the -P command line option.

    $ ./gradlew build -PmyProperty='Hi, world'
  2. System Property: Gradle creates specially-named system properties for project properties which you can set using the -D command line flag or gradle.properties file. For the project property myProperty, the system property created is called org.gradle.project.myProperty.

    $ ./gradlew build -Dorg.gradle.project.myProperty='Hi, world'
    gradle.properties
    org.gradle.project.myProperty='Hi, world'
  3. Gradle Properties File: You can also set project properties in gradle.properties files.

    gradle.properties
    myProperty='Hi, world'

    The gradle.properties file can be located in a number of places, including <GRADLE_USER_HOME>/gradle.properties, ./gradle.properties (the project root) and <GRADLE_HOME>/gradle.properties.

    If the same property is defined in multiple locations, the file’s location determines its precedence.

  4. Environment Variables: You can set project properties with environment variables. If the environment variable name looks like ORG_GRADLE_PROJECT_myProperty='Hi, world', then Gradle will set a myProperty property on your project object, with the value of Hi, world.

    $ export ORG_GRADLE_PROJECT_myProperty='Hi, world'

    This is typically the preferred method for supplying project properties, especially secrets, to unattended builds like those running on CI servers.

Accessing a project property

The recommended way to access a project property is through the Provider API:

val myProperty: Provider<String> = providers.gradleProperty("myProperty")

The returned Provider is lazy and compatible with the Configuration Cache. The provider resolves the property from the build-level sources listed above: command-line -P arguments, org.gradle.project.* system properties, ORG_GRADLE_PROJECT_* environment variables, and gradle.properties files in the Gradle User Home, build root, and Gradle installation directories.

Note

providers.gradleProperty() does not include properties from gradle.properties files in subproject directories, nor extra properties or other properties set dynamically on individual Project instances. To access those, use project.findProperty("name") or extra properties directly.

For other ways to access properties, see the following methods on the Project object:

  • findProperty("name") — returns the value or null, checking dynamically configured containers, like extra properties, and parent projects as well.

  • property("name") — returns the value or throws if the property is missing

  • hasProperty("name") — checks if the property exists

It is possible to change the behavior of a task based on project properties specified at invocation time. Suppose you’d like to ensure release builds are only triggered by CI. A simple way to handle this is through an isCI project property:

build.gradle.kts
tasks.register("performRelease") {
    val isCI = providers.gradleProperty("isCI")
    doLast {
        if (isCI.isPresent) {
            println("Performing release actions")
        } else {
            throw InvalidUserDataException("Cannot perform release outside of CI")
        }
    }
}
build.gradle
tasks.register('performRelease') {
    def isCI = providers.gradleProperty("isCI")
    doLast {
        if (isCI.present) {
            println("Performing release actions")
        } else {
            throw new InvalidUserDataException("Cannot perform release outside of CI")
        }
    }
}
$ ./gradlew performRelease -PisCI=true --quiet
Performing release actions

Note that running ./gradlew performRelease yields the same results as long as your gradle.properties file includes isCI=true:

gradle.properties
isCI=true
$ ./gradlew performRelease --quiet
Performing release actions

Command-line flags

The command line interface and the available flags are described in its own section.

System properties

System properties are variables set at the JVM level and accessible to the Gradle build process. System properties can be retrieved in build scripts lazily using providers.systemProperty().

Setting a system property

You have two options to add system properties listed in order of priority:

  1. Command Line: Using the -D command-line option, you can pass a system property to the JVM, which runs Gradle. The -D option of the gradle command has the same effect as the -D option of the java command.

    $ ./gradlew build -Dgradle.wrapperUser=myuser
  2. Gradle Properties File: You can also set system properties in gradle.properties files with the prefix systemProp.

    gradle.properties
    systemProp.gradle.wrapperUser=myuser
System properties reference

For a quick reference, the following are common system properties:

gradle.wrapperUser=(myuser)

Specify username to download Gradle distributions from servers using HTTP Basic Authentication.

gradle.wrapperPassword=(mypassword)

Specify password for downloading a Gradle distribution using the Gradle wrapper.

gradle.user.home=(path to directory)

Specify the GRADLE_USER_HOME directory.

https.protocols

Specify the supported TLS versions in a comma-separated format. e.g., TLSv1.2,TLSv1.3.

Additional Java system properties are listed here.

In a multi-project build, systemProp properties set in any project except the root will be ignored. Only the root project’s gradle.properties file will be checked for properties that begin with systemProp.

Gradle properties

Gradle properties configure Gradle itself and usually have the name org.gradle.*. Gradle properties should not be used in build logic, their values should not be read/retrieved in build scripts.

Setting a Gradle property

You have two options to add Gradle properties listed in order of priority:

  1. Command Line: Using the -D command-line option, you can pass a Gradle property:

    $ ./gradlew build -Dorg.gradle.caching.debug=false
  2. Gradle Properties File: Place these settings into a gradle.properties file and commit it to your version control system.

    gradle.properties
    org.gradle.caching.debug=false

The final configuration considered by Gradle is a combination of all Gradle properties set on the command line and your gradle.properties files. If an option is configured in multiple locations, the first one found in any of these locations wins:

Priority Method Location Details

1

Command line interface

.

In the command line using -D.

2

gradle.properties file

GRADLE_USER_HOME

Stored in a gradle.properties file in the GRADLE_USER_HOME.

3

gradle.properties file

Project Root Dir

Stored in a gradle.properties file in a project directory, then its parent project’s directory up to the project’s root directory.

4

gradle.properties file

GRADLE_HOME

Stored in a gradle.properties file in the GRADLE_HOME, the optional Gradle installation directory.

Note
The location of the GRADLE_USER_HOME may have been changed beforehand via the -Dgradle.user.home system property passed on the command line.
Gradle properties reference

For reference, the following properties are common Gradle properties:

org.gradle.caching=(true,false)

When set to true, Gradle will reuse task outputs from any previous build when possible, resulting in much faster builds.

Default is false; the build cache is not enabled.

org.gradle.caching.debug=(true,false)

When set to true, individual input property hashes and the build cache key for each task are logged on the console.

Default is false.

org.gradle.configuration-cache=(true,false)

Enables configuration caching. Gradle will try to reuse the build configuration from previous builds.

Default is false.

org.gradle.configureondemand=(true,false)

Enables incubating configuration-on-demand, where Gradle will attempt to configure only necessary projects.

Default is false.

org.gradle.console=(auto,plain,colored,rich,verbose)

Customize console output coloring or verbosity.

Default depends on how Gradle is invoked.

org.gradle.console.interactive=(true,false) Incubating

When set to false, Gradle does not prompt the user for input on the console and uses default values instead. This is useful for automated environments such as CI pipelines, scripts, and AI agents.

Default is true.

org.gradle.continue=(true,false)

If enabled, continue task execution after a task failure, else stop task execution after a task failure.

Default is false.

org.gradle.daemon=(true,false)

When set to true the Gradle Daemon is used to run the build.

Default is true.

org.gradle.daemon.idletimeout=(# of idle millis)

Gradle Daemon will terminate itself after a specified number of idle milliseconds.

Default is 10800000 (3 hours).

org.gradle.debug=(true,false)

When set to true, Gradle will run the build with remote debugging enabled, listening on port 5005. Note that this is equivalent to adding -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 to the JVM command line and will suspend the virtual machine until a debugger is attached.

Default is false.

org.gradle.java.home=(path to JDK home)

Specifies the Java home for the Gradle build process. The value can be set to either a jdk or jre location; however, using a JDK is safer depending on what your build does. This does not affect the version of Java used to launch the Gradle client VM.

You can also control the JVM used to run Gradle itself using the Daemon JVM criteria.

Default is derived from your environment (JAVA_HOME or the path to java) if the setting is unspecified.

org.gradle.java.installations.auto-detect=(true,false)

When set to true, Gradle will automatically detect the JDK installations available on the system when locating Java toolchains. All JDK installation sources are additive, meaning if multiple sources are specified, Gradle will consider all sources when finding JDK installations.

Default is true.

org.gradle.java.installations.auto-download=(true,false)

When set to true, Gradle will automatically download JDK installations when locating Java toolchains. All JDK installation sources are additive, meaning if multiple sources are specified, Gradle will consider all sources when finding JDK installations.

Default is true.

org.gradle.java.installations.paths=(list of JDK installations)

When set, Gradle will use the specified JDK installation paths when locating Java toolchains. This setting is a comma-separated list of paths to directories where JDK installations are located. All JDK installation sources are additive, meaning if multiple sources are specified, Gradle will consider all sources when finding JDK installations.

Default is empty, meaning Gradle will not use any explicitly declared paths to find JDK installations.

org.gradle.java.installations.fromEnv=(list of environment variables)

When set, Gradle will use the JDK installations specified in the listed environment variables when locating Java toolchains. This setting is a comma-separated list of environment variable names. All JDK installation sources are additive, meaning if multiple sources are specified, Gradle will consider all sources when finding JDK installations.

Default is empty, meaning Gradle will not use any environment variables to find JDK installations.

org.gradle.jvmargs=(JVM arguments)

Specifies the JVM arguments used for the Gradle Daemon. The setting is particularly useful for configuring JVM memory settings for build performance. This does not affect the JVM settings for the Gradle client VM.

Default is -Xmx512m "-XX:MaxMetaspaceSize=384m".

org.gradle.logging.level=(quiet,warn,lifecycle,info,debug)

When set to quiet, warn, info, or debug, Gradle will use this log level. The values are not case-sensitive.

Default is lifecycle level.

org.gradle.parallel=(true,false)

When configured, Gradle will fork up to org.gradle.workers.max JVMs to execute projects in parallel.

Default is false.

org.gradle.priority=(low,normal)

Specifies the scheduling priority for the Gradle daemon and all processes launched by it.

Default is normal.

org.gradle.projectcachedir=(directory)

Specify the project-specific cache directory. Defaults to .gradle in the root project directory."

Default is .gradle.

org.gradle.problems.report=(true|false)

Enable (true) or disable (false) the generation of build/reports/problems-report.html. true is the default. The report is generated with problems provided to the Problems API.

org.gradle.tooling.parallel=(true,false) Since 9.4.0

Allows running Tooling Model Builders in parallel, which can improve IDE Sync times. An explicit value will override org.gradle.parallel in this context.

Default is the value of org.gradle.parallel.

org.gradle.isolated-projects=(true,false) Since 9.7.0

Enables Isolated Projects, which configures projects in parallel and implies enabling the Configuration Cache.

Default is false.

org.gradle.isolated-projects.diagnostics=(true,false) Since 9.7.0

Enables Diagnostics mode for Isolated Projects, which runs project configuration sequentially to surface violations across all projects in a deterministic manner. Requires Isolated Projects to be enabled.

Default is false.

org.gradle.isolated-projects.dangerously-ignore-problems=(true,false) Since 9.7.0

Ignores Isolated Projects violations so that a parallel build or sync can be timed to estimate the speedup before violations are fixed. Violations are still reported but do not fail the build. Build outputs may be incorrect. Requires Isolated Projects to be enabled.

Default is false.

org.gradle.vfs.verbose=(true,false)

Configures verbose logging when watching the file system.

Default is false.

org.gradle.vfs.watch=(true,false)

Toggles watching the file system. When enabled, Gradle reuses information it collects about the file system between builds.

Default is true on operating systems where Gradle supports this feature.

org.gradle.warning.mode=(all,fail,summary,none)

When set to all, summary, or none, Gradle will use different warning type display.

Default is summary.

org.gradle.workers.max=(max # of worker processes)

When configured, Gradle will use a maximum of the given number of workers.

Default is the number of CPU processors.

Environment variables

Gradle provides a number of environment variables, which are listed below. Environment variables can be retrieved in build scripts lazily using providers.environmentVariable().

Setting environment variables

Let’s take an example that sets the $JAVA_HOME environment variable:

$ set JAVA_HOME=C:\Path\To\Your\Java\Home   // Windows
$ export JAVA_HOME=/path/to/your/java/home  // Mac/Linux

You can access environment variables as properties in the build script using the System.getenv() method:

task printEnvVariables {
    doLast {
        println "JAVA_HOME: ${System.getenv('JAVA_HOME')}"
    }
}
Environment variables reference

The following environment variables are available for the gradle command:

GRADLE_HOME

Installation directory for Gradle.

Can be used to specify a local Gradle version instead of using the wrapper.

You can add GRADLE_HOME/bin to your PATH for specific applications and use cases (such as testing an early release for Gradle).

JAVA_OPTS

Used to pass JVM options and custom settings to the JVM.

export JAVA_OPTS="-Xmx18928m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6309m"

GRADLE_OPTS

Specifies JVM arguments to use when starting the Gradle client VM.

The client VM is lightweight and mainly handles command-line input and output, so it’s uncommon to change its VM options.

While GRADLE_OPTS typically doesn’t affect the daemon, you can use it to pass arguments to the daemon by setting the org.gradle.jvmargs system property. This is the recommended way to configure the daemon’s JVM options, such as heap size or memory settings, via an environment variable.

GRADLE_USER_HOME

Specifies the GRADLE_USER_HOME directory for Gradle to store its global configuration properties, initialization scripts, caches, log files and more.

Defaults to USER_HOME/.gradle if not set.

GRADLE_DAEMON_BIND_ADDRESS

Specifies the IP address or hostname that Gradle uses to bind the local socket for client-daemon and cross-daemon communication.

By default, Gradle automatically selects the loopback address (or wildcard address as a fallback) for this socket. This may not work correctly in environments with specific network configurations, such as multiple network interfaces, multiple TCP/IP stacks etc.

Setting this variable skips auto-detection and uses the provided address directly:

GRADLE_DAEMON_BIND_ADDRESS=192.168.1.10 ./gradlew build
JAVA_HOME

Specifies the JDK installation directory to use for the client VM.

This VM is also used for the daemon unless a different one is specified in a Gradle properties file with org.gradle.java.home or using the Daemon JVM criteria.

GRADLE_LIBS_REPO_OVERRIDE

Overrides for the default Gradle library repository.

Can be used to specify a default Gradle repository URL in org.gradle.plugins.ide.internal.resolver.

Useful override to specify an internally hosted repository if your company uses a firewall/proxy.

NO_COLOR

When present and non-empty, disables color output while preserving other styling (bold, underline) and rich features (progress bars, animations).

Follows the no-color.org convention. See Customizing log format for more options.

Build Lifecycle

The build lifecycle is the sequence of phases Gradle executes to turn your build scripts into completed work, from initializing the build environment to configuring projects and finally executing tasks.

Build Phases

A build has three distinct phases. Gradle runs these phases in order:

Phase 1. Initialization Phase 2. Configuration Phase 3. Execution

Detects projects and included builds

Configures projects and builds a task graph

Schedules and executes the selected tasks

build lifecycle example
Phase 1. Initialization

In the initialization phase, Gradle detects the set of projects (root and subprojects) and included builds participating in the build.

Gradle first runs any init scripts, then evaluates the settings file, settings.gradle(.kts), and instantiates a Settings object.

Then, Gradle instantiates Project object instances for each project included in the build (using includeBuild() or include() in the settings file).

Phase 2. Configuration

In the configuration phase, Gradle registers tasks and other properties to the projects found by the initialization phase.

Gradle evaluates the build files of every participating project, then constructs the task graph by analyzing the input and output dependencies of tasks.

Note
When the Configuration Cache is enabled, the configuration phase also includes a serialization sub-phase, where Gradle stores the task graph while simultaneously precomputing some of the task state and resolving dependencies. This is work that would otherwise be deferred to execution. See Configuration Cache - How It Works for more details.
Phase 3. Execution

In the execution phase, Gradle runs the selected tasks.

Gradle uses the task execution graph generated by the configuration phase to determine which tasks to execute and in what order.

Gradle can execute tasks that don’t depend on each other, in the same project, in parallel.

The Phases in Build Scripts

The following example shows which parts of settings and build files correspond to various build phases:

settings.gradle.kts
rootProject.name = "basic"
println("This is executed during the initialization phase.")
build.gradle.kts
// Configuration Phase
println("This is executed during the configuration phase.")

tasks.register("configured") {
    println("This is also executed during the configuration phase.")
}

// Execution Phase
tasks.register("test") {
    doLast {
        println("This is executed during the execution phase.")
    }
}

// Configurations AND Execution Phase
tasks.register("testBoth") {
    println("This is executed during the configuration phase as well.")
    doFirst {
        println("This is executed first during the execution phase.")
    }
    doLast {
        println("This is executed last during the execution phase.")
    }
}
settings.gradle
rootProject.name = 'basic'
println 'This is executed during the initialization phase.'
build.gradle
// Configuration Phase
println 'This is executed during the configuration phase.'

tasks.register('configured') {
    println 'This is also executed during the configuration phase.'
}

// Execution Phase
tasks.register('test') {
    doLast {
        println 'This is executed during the execution phase.'
    }
}

// Configurations AND Execution Phase
tasks.register('testBoth') {
    println 'This is executed during the configuration phase as well.'
    doFirst {
	  println 'This is executed first during the execution phase.'
	}
	doLast {
	  println 'This is executed last during the execution phase.'
	}
}

The following command executes the test and testBoth tasks specified above:

$ ./gradlew test testBoth
This is executed during the initialization phase.

> Configure project :
This is executed during the configuration phase.
This is executed during the configuration phase as well.

> Task :test
This is executed during the execution phase.

> Task :testBoth
This is executed first during the execution phase.
This is executed last during the execution phase.

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
$ ./gradlew test testBoth
This is executed during the initialization phase.

> Configure project :
This is executed during the configuration phase.
This is executed during the configuration phase as well.

> Task :test
This is executed during the execution phase.

> Task :testBoth
This is executed first during the execution phase.
This is executed last during the execution phase.

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

Because Gradle only configures requested tasks and their prerequisites, the configured task never configures.

Build Lifecycle Timeline

Understanding the precise sequence in which Gradle evaluates scripts and triggers hooks is the key to mastering build logic. This timeline provides a comprehensive overview of the Gradle build flow when the Configuration Cache and Isolated Projects are disabled:

Phase Seq. Hook / Block Location Purpose

Initialization

1

Init Scripts

~/.gradle/init.d/*.gradle

Global environment setup (e.g., enterprise repos).

2

gradle.beforeSettings

init script or settings.gradle(.kts)

Runs before the settings file is even parsed.

3

pluginManagement { …​ }

settings.gradle(.kts)

Must be first. Defines plugin repos and version rules.

4

plugins { …​ }

settings.gradle(.kts)

Applies plugins to the Settings object (e.g., Build Scans).

5

Settings Script Body

settings.gradle(.kts)

Evaluates include(":project") to define build structure.

6

gradle.settingsEvaluated

init script or settings.gradle(.kts)

Settings are fully processed; Project objects are created.

7

gradle.projectsLoaded

init script or settings.gradle(.kts)

All project instances exist but aren’t configured yet.

Configuration

8

gradle.lifecycle.beforeProject

init script or build.gradle(.kts)

Not recommended. Fires immediately before each project starts evaluating.

9

project.beforeEvaluate

init script or parent project’s build.gradle(.kts)

Runs before a specific project’s build script is evaluated.

10

buildscript { …​ }

build.gradle(.kts)

Sets the classpath for the build script itself.

11

plugins { …​ }

build.gradle(.kts)

Applies plugins and adds DSL/Tasks.

12

Build Script Body

build.gradle(.kts)

Registers tasks (tasks.register) and configures properties.

13

project.afterEvaluate

build.gradle(.kts)

Not recommended. Runs after a specific project is evaluated; useful for reacting to another project’s configuration.

14

gradle.lifecycle.afterProject

init script or root build.gradle(.kts)

Fires immediately after each project finishes evaluating.

15

gradle.projectsEvaluated

init script or root build.gradle(.kts)

All project scripts have run; final chance to tweak the graph.

16

Task Graph Construction

Internal (TaskExecutionGraph, TaskExecutionGraphListener, TaskExecutionListener)

Gradle calculates the DAG based on requested tasks.

Execution

17a

Task Input Snapshotting

Internal (onlyIf{}, upToDateWhen{})

Evaluated per task to determine whether it should be skipped or executed.

17b

Dependency Graph Resolution*

Internal (ResolutionResult, ResolvedComponentResult, ResolvedVariantResult)

Constructs a dependency graph based on declared dependencies.

17c

Artifact Resolution*

Internal (ArtifactCollection, ResolvedArtifact)

Dependencies are downloaded from repositories.

18

Task Execution

Console / CLI (@TaskAction,doFirst{},doLast{})

Tasks run.

* Dependency Graph Resolution and Artifact Resolution are normally deferred until the Execution Phase. However, any build logic that eagerly accesses a configuration’s resolved state, such as calling configuration.files, configuration.resolvedConfiguration, or iterating a configuration directly, will trigger resolution immediately at that point in the build lifecycle.

Important
When the Configuration Cache is enabled, Dependency Graph Resolution and Artifact Resolution must be completed during the Configuration Phase rather than at execution time, since they are required to fully describe the task graph before the result can be serialized. You can refer to Configuration Cache - How It Works to learn more.

Task Graphs

As a build author, you write build logic by defining tasks and declaring how they depend on one another. Gradle uses this information to construct a task graph during the configuration phase that models the relationships between these tasks.

For example, if your project includes tasks such as build, assemble, and createDocs, and you declare that assemble depends on build, and createDocs depends on assemble, Gradle constructs a graph with this order: buildassemblecreateDocs.

Gradle builds the task graph before executing any task(s).

Across all projects in the build, tasks form a Directed Acyclic Graph (DAG).

This diagram shows two example task graphs, one abstract and the other concrete, with dependencies between tasks represented as arrows:

task dag examples

Hooks into the Gradle Build Lifecycle

Gradle exposes several APIs that let you listen to or react to key points in the build lifecycle.

Some APIs let you hook into those phases in init scripts, settings.gradle(.kts) or build.gradle(.kts) to customize or inspect the build as Gradle configures it. These hooks let you:

  • react to the structure of the build (root + subprojects),

  • configure all projects before their own build scripts are evaluated,

  • debug or collect information during configuration.

Lifecycle API

The GradleLifecycle API, accessible through the gradle object, can be used to register actions to be executed at certain points in the build lifecycle:

buildStarted -> DEPRECATED in Gradle 6
 └─ beforeSettings
     └── settingsEvaluated
          └── projectsLoaded
               ├── beforeProject
               ├── afterProject
               └── projectsEvaluated
                     └── buildFinished -> DEPRECATED in Gradle 7

Here is a quick overview:

# Hook Best for

1

gradle.beforeSettings

Early wiring from an init script: global logging/metrics, tweaking gradle.startParameter, choosing different settings files, or setting values on the Settings object before it’s evaluated.

2

gradle.settingsEvaluated

Adjusting repositories, enabling build scans, reading environment variables, or changing build layout.

3

gradle.projectsLoaded

Applying configuration to every project (for example, adding a plugin to all subprojects).

4

gradle.lifecycle.beforeProject

Applying defaults, logging project configuration start.

5

gradle.lifecycle.afterProject

Validating configuration or detecting missing plugins.

6

gradle.projectsEvaluated

Performing cross-project validation, configuration summaries, or creating aggregated tasks.

beforeSettings

Where: init script.
When: By the time settings.gradle(.kts) is executing, beforeSettings has already fired.

callbacks.init.gradle.kts
// 1. beforeSettings: tweak start parameters / log early info
// before the build settings have been loaded and evaluated.
gradle.beforeSettings {
    println("[beforeSettings] gradleUserHome = ${gradle.gradleUserHomeDir}")

    // Example: default to --parallel if an env var is set
    if (System.getenv("CI") == "true") {
        println("[beforeSettings] Enabling parallel execution on CI")
        gradle.startParameter.isParallelProjectExecutionEnabled = true
    } else {
        println("[beforeSettings] Disabling parallel execution, not on CI")
        gradle.startParameter.isParallelProjectExecutionEnabled = false
    }
}
callbacks.init.gradle
// 1. beforeSettings: tweak start parameters / log early info
// before the build settings have been loaded and evaluated.
gradle.beforeSettings {
    println("[beforeSettings] gradleUserHome = ${gradle.gradleUserHomeDir}")

    // Example: default to --parallel if an env var is set
    if (System.getenv("CI") == "true") {
        println("[beforeSettings] Enabling parallel execution on CI")
        gradle.startParameter.parallelProjectExecutionEnabled = true
    } else {
        println("[beforeSettings] Disabling parallel execution, not on CI")
        gradle.startParameter.parallelProjectExecutionEnabled = false
    }

    println("")
}
settingsEvaluated

Where: settings.gradle(.kts).
When: After the settings file finishes evaluating.

callbacks.init.gradle.kts
// 2. settingsEvaluated: adjust build layout / repositories / scan config
// when the build settings have been loaded and evaluated.
gradle.settingsEvaluated {
    println("[settingsEvaluated] rootProject = ${rootProject.name}")

    // Example: enforce a company-wide pluginManagement repo
    pluginManagement.repositories.apply {
        println("[settingsEvaluated] Ensuring company plugin repo is configured")
        mavenCentral()
    }
}
callbacks.init.gradle
// 2. settingsEvaluated: adjust build layout / repositories / scan config
// when the build settings have been loaded and evaluated.
gradle.settingsEvaluated { settings ->
    println("[settingsEvaluated] rootProject = ${settings.rootProject.name}")

    // Example: enforce a company-wide pluginManagement repo
    settings.pluginManagement.repositories.with {
        println("[settingsEvaluated] Ensuring company plugin repo is configured")
        mavenCentral()
    }

    println("")
}
projectsLoaded

Where: settings.gradle(.kts).
When: All projects have been discovered but not yet configured.

callbacks.init.gradle.kts
// 3. projectsLoaded: we know the full project graph, but nothing configured yet
// to be called when the projects for the build have been created from the settings.
gradle.projectsLoaded {
    println("[projectsLoaded] Projects discovered: " + rootProject.allprojects.joinToString { it.name })

    // Example: Add a custom property (using the extra properties extension)
    allprojects {
        println("[projectsLoaded] Setting extra property on ${name}")
        extensions.extraProperties["isInitScriptConfigured"] = true
    }
}
callbacks.init.gradle
// 3. projectsLoaded: we know the full project graph, but nothing configured yet
// to be called when the projects for the build have been created from the settings.
gradle.projectsLoaded {
    println "[projectsLoaded] Projects discovered: " + rootProject.allprojects.collect { it.name }.join(', ')

    // Example: Add a custom property (using the extra properties extension)
    allprojects {
        println("[projectsLoaded] Setting extra property on ${name}")
        extensions.extraProperties["isInitScriptConfigured"] = true
    }
}
beforeProject

Where: build.gradle(.kts) or anywhere you have access to the Gradle instance.
When: For each project as its build script is evaluated.

callbacks.init.gradle.kts
// to be called immediately before a project is evaluated.
gradle.lifecycle.beforeProject {
    println("[lifecycle.beforeProject] Started configuring ${path}")
}

// 4. beforeProject: runs before each build.gradle(.kts) is evaluated
// to be called immediately before a project is evaluated.
gradle.beforeProject {
    println("[beforeProject] Started configuring ${path}")

    println("[beforeProject] Setup a global build directory for ${name}")
    layout.buildDirectory.set(
        layout.projectDirectory.dir("build")
    )
}
callbacks.init.gradle
// to be called immediately before a project is evaluated.
gradle.lifecycle.beforeProject {
    println("[lifecycle.beforeProject] Started configuring ${path}")
}

// 4. beforeProject: runs before each build.gradle(.kts) is evaluated
// to be called immediately before a project is evaluated.
gradle.beforeProject { project ->
    println("[beforeProject] Started configuring ${project.path}")

    println("[beforeProject] Setup a global build directory for ${project.name}")
    project.layout.buildDirectory.set(
        project.layout.projectDirectory.dir("build")
    )
}
Tip
When Isolated Projects is enabled, applying a plugin that is resolved lazily — for example a convention plugin from a build registered via pluginManagement.includeBuild(…​), or a third-party plugin from a repository — requires additional setup. See Applying plugins from a lifecycle callback.
afterProject

Where: build.gradle(.kts) or anywhere you have access to the Gradle instance.
When: For each project as its build script is evaluated.

callbacks.init.gradle.kts
// 5. afterProject: runs after each build.gradle(.kts) is evaluated
// to be called immediately after a project is evaluated.
gradle.afterProject {
    println("[afterProject] Finished configuring ${path}")

    // Example: apply the Java plugin to all projects that don’t have any plugin yet
    if (plugins.hasPlugin("java")) {
        println("[afterProject] ${path} already has the java plugin")
    } else {
        println("[afterProject] Applying java plugin to ${path}")
        apply(plugin = "java")
    }
}

// to be called immediately after a project is evaluated.
gradle.lifecycle.afterProject {
    println("[lifecycle.afterProject] Finished configuring ${path}")
}
callbacks.init.gradle
// 5. afterProject: runs after each build.gradle(.kts) is evaluated
// to be called immediately after a project is evaluated.
gradle.afterProject { project ->
    println("[afterProject] Finished configuring ${project.path}")

    // Example: apply the Java plugin to all projects that don’t have any plugin yet
    if (project.plugins.hasPlugin("java")) {
        println("[afterProject] ${project.path} already has the java plugin")
    } else {
        println("[afterProject] Applying java plugin to ${project.path}")
        project.apply(plugin: "java")
    }
}

// to be called immediately after a project is evaluated.
gradle.lifecycle.afterProject {
    println("[lifecycle.afterProject] Finished configuring ${path}")
}
Tip
When Isolated Projects is enabled, applying a plugin that is resolved lazily — for example a convention plugin from a build registered via pluginManagement.includeBuild(…​), or a third-party plugin from a repository — requires additional setup. See Applying plugins from a lifecycle callback.
projectsEvaluated

Where: build.gradle(.kts) or anywhere you have access to the Gradle instance.
When: After all projects have been evaluated (i.e., all build.gradle(.kts) files are read and configuration is complete), but before task graph is finalized and before execution starts.

callbacks.init.gradle.kts
// 6. projectsEvaluated: all projects are fully configured, safe for cross-project checks
// to be called when all projects for the build have been evaluated.
gradle.projectsEvaluated {
    println("[projectsEvaluated] All projects evaluated")

    // Example: globally configure the java plugin
    allprojects {
        extensions.findByType<JavaPluginExtension>()?.let { javaExtension ->
            if (javaExtension.toolchain.languageVersion.isPresent) {
                println("[projectsEvaluated] ${path} uses Java plugin with toolchain ${javaExtension.toolchain.displayName}")
            } else {
                println("[projectsEvaluated] WARNING: ${path} uses Java plugin but no toolchain is configured, setting Java 17")
                javaExtension.toolchain.languageVersion.set(JavaLanguageVersion.of(17))
            }
        }
    }
}
callbacks.init.gradle
// 6. projectsEvaluated: all projects are fully configured, safe for cross-project checks
// to be called when all projects for the build have been evaluated.
gradle.projectsEvaluated {
    println("[projectsEvaluated] All projects evaluated")

    // Example: globally configure the java plugin
    allprojects { project ->
        def javaExtension = project.extensions.findByType(JavaPluginExtension)
        if(javaExtension) {
            if (javaExtension.toolchain.languageVersion.orNull != null) {
                println("[projectsEvaluated] ${path} uses Java plugin with toolchain ${javaExtension.toolchain.displayName}")
            } else {
                println("[projectsEvaluated] WARNING: ${path} uses Java plugin but no toolchain is configured, setting Java 17")
                javaExtension.toolchain.languageVersion.set(JavaLanguageVersion.of(17))
            }
        }
    }
}
Build Listeners

Listeners are interfaces you implement to react to build lifecycle events.

Many BuildListeners are deprecated and/or are not Configuration Cache friendly, use them with caution:

Interface What it observes Works with CC?

BuildListener

Fires when projects are loaded/evaluated.

ProgressListener

Fires when the execution of an operation progresses.

(Partially Deprecated) TaskActionListener

Fires when a task has started or completed its action.

(Deprecated) TaskExecutionListener

Fires before and after each task executes.

(Deprecated) TaskExecutionGraphListener

Fires when the task execution graph has been populated.

OperationCompletionListener

Fires when an operation completes.

DependencyResolutionListener

Fires before and after dependency resolution.

ProjectEvaluationListener

Fires before and after each project is evaluated.

TestListener

Fires at different times during test execution.

You implement a listener interface and typically register it on the gradle object. The TaskExecutionGraphListener fires once after the task graph has been calculated, but before any task executes.

This example uses graph.allTasks to inspect what Gradle plans to run, and graph.hasTask() to conditionally adjust behaviour:

build.gradle.kts
gradle.taskGraph.addTaskExecutionGraphListener { graph ->
    val tasks = graph.allTasks.joinToString("\n  ") { it.path }
    println("[TaskExecutionGraphListener] Graph is ready. Tasks to execute:\n  $tasks")

    if (graph.hasTask(":skippedTask")) {
        println("[TaskExecutionGraphListener] :skippedTask is in the graph (it will still be skipped via onlyIf)")
    }
}

tasks.register("hello") {
    group = "demo"
    description = "Prints a greeting. Always runs — observe TaskExecutionListener output."
    doLast {
        println("Hello from the :hello task!")
    }
}

tasks.register("upToDateTask") {
    group = "demo"
    description = "Writes a file. Runs once, then reports UP-TO-DATE on subsequent runs."

    val outputFile = layout.buildDirectory.file("up-to-date-output.txt")
    outputs.file(outputFile)

    doLast {
        outputFile.get().asFile.writeText("generated")
        println("upToDateTask: file written.")
    }
}

tasks.register("skippedTask") {
    group = "demo"
    description = "Always skipped via onlyIf — observe SKIPPED in TaskExecutionListener."
    onlyIf { false }
    doLast {
        println("This never prints.")
    }
}
build.gradle
gradle.taskGraph.addTaskExecutionGraphListener { graph ->
    def tasks = graph.allTasks.collect { it.path }.join("\n  ")
    println("[TaskExecutionGraphListener] Graph is ready. Tasks to execute:\n  ${tasks}")

    if (graph.hasTask(":skippedTask")) {
        println("[TaskExecutionGraphListener] :skippedTask is in the graph (it will still be skipped via onlyIf)")
    }
}

tasks.register("hello") {
    group = "demo"
    description = "Prints a greeting. Always runs."
    doLast {
        println("Hello from the :hello task!")
    }
}

tasks.register("upToDateTask") {
    group = "demo"
    description = "Writes a file. Runs once, then reports UP-TO-DATE on subsequent runs."

    def outputFile = layout.buildDirectory.file("up-to-date-output.txt")
    outputs.file(outputFile)

    doLast {
        outputFile.get().asFile.text = "generated"
        println("upToDateTask: file written.")
    }
}

tasks.register("skippedTask") {
    group = "demo"
    description = "Always skipped via onlyIf."
    onlyIf { false }
    doLast {
        println("This never prints.")
    }
}

Note that tasks skipped via onlyIf still appear in the graph; skipping is evaluated later, per task, during execution:

[TaskExecutionGraphListener] Graph is ready. Tasks to execute:
  :hello
  :skippedTask
  :upToDateTask
  :runAll
[TaskExecutionGraphListener] :skippedTask is in the graph (it will still be skipped via onlyIf)

> Task :hello
Hello from the :hello task!

> Task :skippedTask SKIPPED

> Task :upToDateTask
upToDateTask: file written.

> Task :runAll

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
Build Services

Build Services are used to share state or resources across tasks and across the build lifecycle.

This example implements OperationCompletionListener directly on the service to count tasks as they complete, and uses close() as an end-of-build hook to print the final summary, whether the build succeeded or failed:

build.gradle.kts
abstract class BuildDurationService
    : BuildService<BuildServiceParameters.None>, AutoCloseable {

    private val startTime = System.currentTimeMillis()

    // Called once at the end of the build — reliable teardown hook
    override fun close() {
        val elapsed = System.currentTimeMillis() - startTime
        println("─────────────────────────────────────")
        println("  Build duration : ${elapsed}ms")
        println("─────────────────────────────────────")
    }
}

val buildDurationService = gradle.sharedServices.registerIfAbsent("buildDuration", BuildDurationService::class) {
    maxParallelUsages = 1
}

tasks.register("taskA") {
    group = "demo"
    usesService(buildDurationService)
    val service = buildDurationService
    doLast {
        service.get()
        println("taskA running...")
        Thread.sleep(200)
    }
}

tasks.register("taskB") {
    group = "demo"
    doLast {
        println("taskB running...")
        Thread.sleep(300)
    }
}
build.gradle
abstract class BuildDurationService
    implements BuildService<BuildServiceParameters.None>, AutoCloseable {

    private final long startTime = System.currentTimeMillis()

    // Called once at the end of the build — reliable teardown hook
    @Override
    void close() {
        def elapsed = System.currentTimeMillis() - startTime
        println("─────────────────────────────────────")
        println("  Build duration : ${elapsed}ms")
        println("─────────────────────────────────────")
    }
}

def buildDurationService = gradle.sharedServices.registerIfAbsent("buildDuration", BuildDurationService) {
    maxParallelUsages = 1
}

tasks.register("taskA") {
    group = "demo"
    usesService(buildDurationService)
    def service = buildDurationService
    doLast {
        service.get()
        println("taskA running...")
        Thread.sleep(200)
    }
}

tasks.register("taskB") {
    group = "demo"
    doLast {
        println("taskB running...")
        Thread.sleep(300)
    }
}

The hooks are used to calculate the duration of the build:

> Task :taskA
taskA running...

> Task :taskB
taskB running...

> Task :runAll
─────────────────────────────────────
  Build duration : 510ms
─────────────────────────────────────

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
Flow Actions Incubating

FlowAction is a newer API (introduced in Gradle 7.6) that lets you run logic in response to build lifecycle events, most commonly, when the build finishes. It was introduced as a Configuration Cache and Isolated Projects compatible alternative to BuildListener and other deprecated lifecycle listeners.

To use a Flow Action, you define a class implementing FlowAction<T> and a matching Parameters interface that declares the inputs your action needs. You then register it inside a Plugin using FlowScope, which is injected by Gradle alongside FlowProviders:

build.gradle.kts
abstract class PrintBuildResultPlugin : Plugin<Project> {

    @get:Inject
    abstract val flowScope: FlowScope

    @get:Inject
    abstract val flowProviders: FlowProviders

    override fun apply(target: Project) {
        flowScope.always(BuildResultPrinter::class.java) {
            parameters.buildResult.set(flowProviders.buildWorkResult)
        }
    }
}

abstract class BuildResultPrinter : FlowAction<BuildResultPrinter.Parameters> {

    interface Parameters : FlowParameters {
        @get:Input
        val buildResult: Property<BuildWorkResult>
    }

    override fun execute(parameters: Parameters) {
        val result = parameters.buildResult.get()
        if (result.failure.isPresent) {
            println("Build failed: ${result.failure.get().message}")
        } else {
            println("Build succeeded")
        }
    }
}
build.gradle
abstract class PrintBuildResultPlugin implements Plugin<Project> {

    @Inject
    abstract FlowScope getFlowScope()

    @Inject
    abstract FlowProviders getFlowProviders()

    @Override
    void apply(Project target) {
        flowScope.always(BuildResultPrinter) {
            parameters.buildResult.set(flowProviders.buildWorkResult)
        }
    }
}

abstract class BuildResultPrinter implements FlowAction<BuildResultPrinter.Parameters> {

    interface Parameters extends FlowParameters {
        @Input
        Property<BuildWorkResult> getBuildResult()
    }

    @Override
    void execute(Parameters parameters) {
        def result = parameters.buildResult.get()
        if (result.failure.isPresent()) {
            println "Build failed: ${result.failure.get().message}"
        } else {
            println "Build succeeded"
        }
    }
}

As expected, we see the build finish success message:

Build succeeded

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Build Scan

Gradle provides multiple ways to inspect your build:

  1. Profile with a Build Scan

  2. Local profile reports

  3. Low level profiling

What is a Build Scan?

A Build Scan is a persistent, shareable record of what happened when running a build. They provide detailed insights you can use to diagnose performance issues, identify build failures, and share context with your team.

A Build Scan is created locally but published to a secure external server managed by Gradle. They do not run code on your machine, and they never modify your build. You can choose whether to publish a scan, and each scan is assigned a unique (private-by-default) URL.

Note
For information on what data is collected and how it’s handled, see the Gradle.com Privacy Policy.

In Gradle 4.3 and above, you can create a Build Scan using the --scan command line option:

$ ./gradlew build --scan

For older Gradle versions, the Develocity Plugin User Manual explains how to generate a Build Scan.

At the end of your build, Gradle displays a URL where you can find your Build Scan:

BUILD SUCCESSFUL in 2s
4 actionable tasks: 4 executed

Publishing Build Scan...
https://gradle.com/s/e6ircx2wjbf7e

This section explains how to profile your build with a Build Scan.

1. Profile with a Build Scan

The performance page can help use a Build Scan to profile a build. To get there, click "Performance" in the left hand navigation menu or follow the "Explore performance" link on the Build Scan home page:

build scan home
Figure 1. Performance page link on Build Scan home page

The performance page shows how long it took to complete different stages of a build. This page shows how long it took to:

  • start up

  • configure the build’s projects

  • resolve dependencies

  • execute tasks

You also get details about environmental properties, such as whether a daemon was used or not.

build scan performance page
Figure 2. Build Scan performance page

In the above Build Scan, configuration takes over 13 seconds. Click on the "Configuration" tab to break this stage into component parts, exposing the cause of the slowness.

build scan configuration breakdown
Figure 3. Build Scan configuration breakdown

Here you can see the scripts and plugins applied to the project in descending order of how long they took to apply. The slowest plugin and script applications are good candidates for optimization. For example, the script script-b.gradle was applied once but took 3 seconds. Expand that row to see where the build applied this script.

script b application
Figure 4. Showing the application of script-b.gradle to the build

You can see that subproject :app1 applied the script once, from inside of that subproject’s build.gradle file.

2. Profile report

If you prefer not to use a Build Scan, you can generate an HTML report in the build/reports/profile directory of your root project. To generate this report, use the --profile command-line option:

$ ./gradlew --profile <tasks>

Each profile report has a timestamp in its name to avoid overwriting existing ones.

The report displays a breakdown of the time taken to run the build. However, this breakdown is not as detailed as a Build Scan. The following profile report shows the different categories available:

Sample Gradle profile report
Figure 5. An example profile report

3. Low level profiling

Sometimes your build can be slow even though your build scripts do everything right. This often comes down to inefficiencies in plugins and custom tasks or constrained resources. Use the Gradle Profiler to find these kinds of bottlenecks. With the Gradle Profiler, you can define scenarios like "Running 'assemble' after making an ABI-breaking change" and run your build several times to collect profiling data. Use the Profiler to produce a Build Scan. Or combine it with method profilers like JProfiler and YourKit. These profilers can help you find inefficient algorithms in custom plugins. If you find that something in Gradle itself slows down your build, don’t hesitate to send a profiler snapshot to performance@gradle.com.

Performance categories

Both a Build Scan and local profile reports break down build execution into the same categories. The following sections explain those categories.

Startup

This reflects Gradle’s initialization time, which consists mostly of:

  • JVM initialization and class loading

  • Downloading the Gradle distribution if you’re using the wrapper

  • Starting the daemon if a suitable one isn’t already running

  • Executing Gradle initialization scripts

Even when a build execution has a long startup time, subsequent runs usually see a dramatic drop off in startup time. Persistently slow build startup times are usually the result of problems in your init scripts. Double check that the work you’re doing there is necessary and performant.

Settings and buildSrc

After startup, Gradle initializes your project. Usually, Gradle only processes your settings file. If you have custom build logic in a buildSrc directory, Gradle also processes that logic. After building buildSrc once, Gradle considers it up to date. The up-to-date checks take significantly less time than logic processing. If your buildSrc phase takes too much time, consider breaking it out into a separate project. You can then add that project’s JAR artifact as a dependency.

The settings file rarely contains code with significant I/O or computation. If you find that Gradle takes a long time to process it, use more traditional profiling methods, like the Gradle Profiler, to determine the cause.

Loading projects

It normally doesn’t take a significant amount of time to load projects, nor do you have any control over it. The time spent here is basically a function of the number of projects you have in your build.

Continuous Builds

Continuous Build allows you to automatically re-execute the requested tasks when file inputs change. You can execute the build in this mode using the -t or --continuous command-line option.

For example, you can continuously run the test task and all dependent tasks by running:

$ ./gradlew test --continuous

Gradle will behave as if you ran gradle test after a change to sources or tests that contribute to the requested tasks. This means unrelated changes (such as changes to build scripts) will not trigger a rebuild. To incorporate build logic changes, the continuous build must be restarted manually.

Continuous build uses file system watching to detect changes to the inputs. If file system watching does not work on your system, then continuous build won’t work either. In particular, continuous build does not work when using --no-daemon.

When Gradle detects a change to the inputs, it will not trigger the build immediately. Instead, it will wait until no additional changes are detected for a certain period of time - the quiet period. You can configure the quiet period in milliseconds by the Gradle property org.gradle.continuous.quietperiod.

Terminating Continuous Build

If Gradle is attached to an interactive input source, such as a terminal, the continuous build can be exited by pressing CTRL-D (On Microsoft Windows, it is required to also press ENTER or RETURN after CTRL-D).

If Gradle is not attached to an interactive input source (e.g. is running as part of a script), the build process must be terminated (e.g. using the kill command or similar).

If the build is being executed via the Tooling API, the build can be cancelled using the Tooling API’s cancellation mechanism.

Limitations

Under some circumstances, continuous build may not detect changes to inputs.

Creating input directories

Sometimes, creating an input directory that was previously missing does not trigger a build, due to the way file system watching works. For example, creating the src/main/java directory may not trigger a build. Similarly, if the input is a filtered file tree and no files are matching the filter, the creation of matching files may not trigger a build.

Inputs of untracked tasks

Changes to the inputs of untracked tasks or tasks that have no outputs may not trigger a build.

Changes to files outside of project directories

Gradle only watches for changes to files inside the project directory. Changes to files outside the project directory will go undetected and not trigger a build.

Build cycles

Gradle starts watching for changes just before a task executes. If a task modifies its own inputs while executing, Gradle will detect the change and trigger a new build. If every time the task executes, the inputs are modified again, the build will be triggered again. This isn’t unique to continuous build. A task that modifies its own inputs will never be considered up-to-date when run "normally" without continuous build.

If your build enters a build cycle like this, you can track down the task by looking at the list of files reported changed by Gradle. After identifying the file(s) that are changed during each build, you should look for a task that has that file as an input. In some cases, it may be obvious (e.g., a Java file is compiled with compileJava). In other cases, you can use --info logging to find the task that is out-of-date due to the identified files.

In general, Gradle will not detect changes to symbolic links or to files referenced via symbolic links.

Changes to build logic are not considered

The current implementation does not recalculate the build model on subsequent builds. This means that changes to task configuration, or any other change to the build model, are effectively ignored.

Continuous Build in IDEs

Most modern IDEs like IntelliJ IDEA, Android Studio, or Eclipse already offer their own form of continuous compilation and test execution.

For example, If you’re only interested in triggering something on file changes (e.g., running tests or generating docs), you might consider using IntelliJ’s File Watchers feature as an alternative for specific tasks.

Gradle’s --continuous mode is designed for command-line use and is not typically integrated into IDE workflows. To use continuous build in an IDE environment, run ./gradlew <task> --continuous from a terminal window alongside the IDE.

File System Watching

Gradle maintains a Virtual File System (VFS) to calculate what needs to be rebuilt on repeat builds of a project. By watching the file system, Gradle keeps the VFS current between builds.

Enable

Gradle enables file system watching by default for supported operating systems since Gradle 7.

Run the build with the '--watch-fs' flag to force file system watching for a build.

To force file system watching for all builds (unless disabled with --no-watch-fs), add the following value to gradle.properties:

org.gradle.vfs.watch=true

Disable

To disable file system watching:

  • use the --no-watch-fs flag

  • set org.gradle.vfs.watch=false in gradle.properties

Excluding files and directories

Gradle automatically excludes some common directories (like .git, .gradle, and build/) from file system watching.

Locations managed by Gradle are never watched, because Gradle is their only writer and keeps the virtual file system up to date directly. This includes Gradle’s global caches and the project cache directory — the .gradle directory in the root project, or a custom location configured with --project-cache-dir. Because the project cache directory is not watched, it can be placed on any file system, including one that does not support watching, without affecting file system watching.

There is no public mechanism to configure additional file system watch excludes.

To reduce unnecessary watching and re-execution, consider limiting the inputs of specific tasks using fileTree.exclude().

Supported Operating Systems

Gradle uses native operating system features to watch the file system. Gradle supports file system watching on the following operating systems:

  • Windows 10, version 1709 and later

  • Linux, tested on the following distributions:

    • Ubuntu 16.04

    • CentOS Stream 9

    • Red Hat Enterprise Linux (RHEL) 8

    • Amazon Linux 2

    • Alpine Linux 3.20

  • macOS 12 (Monterey) or later on Intel and ARM architectures

Supported File Systems

File system watching supports the following file system types:

  • APFS

  • btrfs

  • ext3

  • ext4

  • XFS

  • HFS+

  • NTFS

Gradle also supports VirtualBox’s shared folders.

Network file systems like Samba and NFS are not supported. Microsoft Dev Drives (ReFS) are also not supported.

Unsupported File Systems

When enabled by default, file system watching acts conservatively when it encounters content on unsupported file systems. This can happen if you mount a project directory or subdirectory from a network drive. Gradle doesn’t retain information about unsupported file systems between builds when enabled by default. If you explicitly enable file system watching, Gradle retains information about unsupported file systems between builds.

To verify that the operating system actually delivers events for a watched directory, Gradle writes a small probe file (file-system.probe). This probe is always created in the .gradle directory under the build root. The probe is removed when the build finishes.

Files and directories in your project that are accessed via symlinks do not benefit from file system-watching optimizations.

Logging

To view information about Virtual File System (VFS) changes at the beginning and end of a build, enable verbose VFS logging.

Set the org.gradle.vfs.verbose Daemon option to true to enable verbose logging.

You can do this on the command line with the following command:

$ gradle <task> -Dorg.gradle.vfs.verbose=true

Or configure the property in the gradle.properties file in the project root or your Gradle User Home:

org.gradle.vfs.verbose=true

This produces the following output at the start and end of the build:

$ ./gradlew assemble --watch-fs -Dorg.gradle.vfs.verbose=true
Received 3 file system events since last build while watching 1 locations
Virtual file system retained information about 2 files, 2 directories and 0 missing files since last build
> Task :compileJava NO-SOURCE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :jar UP-TO-DATE
> Task :assemble UP-TO-DATE

BUILD SUCCESSFUL in 58ms
1 actionable task: 1 up-to-date
Received 5 file system events during the current build while watching 1 locations
Virtual file system retains information about 3 files, 2 directories and 2 missing files until next build

On Windows and macOS, Gradle might report changes received since the last build, even if you haven’t changed anything. These are harmless notifications about changes to Gradle’s caches and can be safely ignored.

Troubleshooting

Gradle does not detect some changes

Please let us know on the Gradle community Slack. If a build declares its inputs and outputs correctly, this should not happen. So it’s either a bug we must fix or your build lacks declaration for some inputs or outputs.

VFS state dropped due to lost state

Did you receive a message that reads Dropped VFS state due to lost state during a build? Please let us know on the Gradle community Slack. This means that your build cannot benefit from file system watching for one of the following reasons:

  • the Daemon received an unknown file system event

  • too many changes happened, and the watching API couldn’t handle it

Too many open files on macOS

If you receive the java.io.IOException: Too many open files error on macOS, raise your open files limit. See this post for more details.

Adjust inotify watches limit on Linux

File system watching uses inotify on Linux. Depending on the size of your build, it may be necessary to increase inotify limits. If you are using an IDE, then you probably already had to increase the limits in the past.

File system watching uses one inotify watch per watched directory. You can see the current limit of inotify watches per user by running:

cat /proc/sys/fs/inotify/max_user_watches

To increase the limit to e.g. 512K watches run the following:

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p --system

Each used inotify watch takes up to 1KB of memory. Assuming inotify uses all the 512K watches then file system watching could use up to 500MB. In a memory-constrained environment, you may want to disable file system watching.

Inspect inotify instances limit on Linux

File system watching initializes one inotify instance per daemon. You can see the current limit of inotify instances per user by running:

cat /proc/sys/fs/inotify/max_user_instances

The default per-user instances limit should be high enough, so we don’t recommend increasing that value manually.

DSLS AND APIS

Groovy

A Groovy Build Script Primer

Ideally, a Groovy build script looks mostly like configuration: setting some properties of the project, configuring dependencies, declaring tasks, and so on. That configuration is based on Groovy language constructs. This primer aims to explain what those constructs are and — most importantly — how they relate to Gradle’s API documentation.

The Project object

As Groovy is an object-oriented language based on Java, its properties and methods apply to objects. In some cases, the object is implicit — particularly at the top level of a build script, i.e. not nested inside a {} block.

Consider this fragment of build script, which contains an unqualified property and block:

version = '1.0.0.GA'

configurations {
    ...
}

Both version and configurations {} are part of org.gradle.api.Project.

This example reflects how every Groovy build script is backed by an implicit instance of Project. If you see an unqualified element and you don’t know where it’s defined, always check the Project API documentation to see if that’s where it’s coming from.

Caution

Avoid using Groovy MetaClass programming techniques in your build scripts. Gradle provides its own API for adding dynamic runtime properties.

Use of Groovy-specific metaprogramming can cause builds to retain large amounts of memory between builds that will eventually cause the Gradle daemon to run out-of-memory.

Properties
<obj>.<name>                // Get a property value
<obj>.<name> = <value>      // Set a property to a new value
"$<name>"                   // Embed a property value in a string
"${<obj>.<name>}"           // Same as previous (embedded value)
Examples
version = '1.0.1'
myCopyTask.description = 'Copies some files'

file("$projectDir/src")
println "Destination: ${myCopyTask.destinationDir}"

A property represents some state of an object. The presence of an = sign is a clear indicator that you’re looking at a property. Otherwise, a qualified name — it begins with <obj>. — without any other decoration is also a property.

If the name is unqualified, then it may be one of the following:

  • A task instance with that name.

  • A property on Project.

  • An extra property defined elsewhere in the project.

  • A property of an implicit object within a block.

  • A local variable defined earlier in the build script.

Note that plugins can add their own properties to the Project object. The API documentation lists all the properties added by core plugins. If you’re struggling to find where a property comes from, check the documentation for the plugins that the build uses.

Tip
When referencing a project property in your build script that is added by a non-core plugin, consider prefixing it with project. — it’s clear then that the property belongs to the project object.
Properties in the API documentation

The Groovy DSL reference shows properties as they are used in your build scripts, but the Javadocs only display methods. That’s because properties are implemented as methods behind the scenes:

  • A property can be read if there is a method named get<PropertyName> with zero arguments that returns the same type as the property.

  • A property can be modified if there is a method named set<PropertyName> with one argument that has the same type as the property and a return type of void.

Note that property names usually start with a lower-case letter, but that letter is upper case in the method names. So the getter method getProjectVersion() corresponds to the property projectVersion. This convention does not apply when the name begins with at least two upper-case letters, in which case there is not change in case. For example, getRAM() corresponds to the property RAM.

Examples
project.getVersion()
project.version

project.setVersion('1.0.1')
project.version = '1.0.1'
Methods
<obj>.<name>()              // Method call with no arguments
<obj>.<name>(<arg>, <arg>)  // Method call with multiple arguments
<obj>.<name> <arg>, <arg>   // Method call with multiple args (no parentheses)
Examples
myCopyTask.include '**/*.xml', '**/*.properties'

ext.resourceSpec = copySpec()   // `copySpec()` comes from `Project`

file('src/main/java')
println 'Hello, World!'

A method represents some behavior of an object, although Gradle often uses methods to configure the state of objects as well. Methods are identifiable by their arguments or empty parentheses. Note that parentheses are sometimes required, such as when a method has zero arguments, so you may find it simplest to always use parentheses.

Note
Gradle has a convention whereby if a method has the same name as a collection-based property, then the method appends its values to that collection.
Blocks

Blocks are also methods, just with specific types for the last argument.

<obj>.<name> {
     ...
}

<obj>.<name>(<arg>, <arg>) {
     ...
}
Examples
plugins {
    id 'java-library'
}

configurations {
    assets
}

sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
    }
}

dependencies {
    implementation project(':util')
}

Blocks are a mechanism for configuring multiple aspects of a build element in one go. They also provide a way to nest configuration, leading to a form of structured data.

There are two important aspects of blocks that you should understand:

  1. They are implemented as methods with specific signatures.

  2. They can change the target ("delegate") of unqualified methods and properties.

Both are based on Groovy language features and we explain them in the following sections.

Block method signatures

You can easily identify a method as the implementation behind a block by its signature, or more specifically, its argument types. If a method corresponds to a block:

For example, Project.copy(Action) matches these requirements, so you can use the syntax:

copy {
    into layout.buildDirectory.dir("tmp")
    from 'custom-resources'
}

That leads to the question of how into() and from() work. They’re clearly methods, but where would you find them in the API documentation? The answer comes from understanding object delegation.

Delegation

The section on properties lists where unqualified properties might be found. One common place is on the Project object. But there is an alternative source for those unqualified properties and methods inside a block: the block’s delegate object.

To help explain this concept, consider the last example from the previous section:

copy {
    into layout.buildDirectory.dir("tmp")
    from 'custom-resources'
}

All the methods and properties in this example are unqualified. You can easily find copy() and layout in the Project API documentation, but what about into() and from()? These are resolved against the delegate of the copy {} block. What is the type of that delegate? You’ll need to check the API documentation for that.

There are two ways to determine the delegate type, depending on the signature of the block method:

  • For Action arguments, look at the type’s parameter.

    In the example above, the method signature is copy(Action<? super CopySpec>) and it’s the bit inside the angle brackets that tells you the delegate type — CopySpec in this case.

  • For Closure arguments, the documentation will explicitly say in the description what type is being configured or what type the delegate it (different terminology for the same thing).

Hence you can find both into() and from() on CopySpec. You might even notice that both of those methods have variants that take an Action as their last argument, which means you can use block syntax with them.

All new Gradle APIs declare an Action argument type rather than Closure, which makes it very easy to pick out the delegate type. Even older APIs have an Action variant in addition to the old Closure one.

Local variables
def <name> = <value>        // Untyped variable
<type> <name> = <value>     // Typed variable
Examples
def i = 1
String errorMsg = 'Failed, because reasons'

Local variables are a Groovy construct — unlike extra properties — that can be used to share values within a build script.

Caution

Avoid using local variables in the root of the project, i.e. as pseudo project properties. They cannot be read outside of the build script and Gradle has no knowledge of them.

Within a narrower context — such as configuring a task — local variables can occasionally be helpful.

Kotlin

Gradle Kotlin DSL Primer

Gradle’s Kotlin DSL offers an alternative to the traditional Groovy DSL, delivering an enhanced editing experience in supported IDEs with features like better content assist, refactoring, and documentation.

This chapter explores the key Kotlin DSL constructs and demonstrates how to use them to interact with the Gradle API.

Tip
If you are interested in migrating an existing Gradle build to the Kotlin DSL, please also check out the dedicated migration page.
Prerequisites
IDE support

The Kotlin DSL is fully supported by IntelliJ IDEA and Android Studio. While other IDEs lack advanced tools for editing Kotlin DSL files, you can still import Kotlin-DSL-based builds and work with them as usual.

Build import Syntax highlighting 1 Semantic editor 2

IntelliJ IDEA

Android Studio

Eclipse IDE

CLion

Apache NetBeans

Visual Studio Code (LSP)

Visual Studio

1 Kotlin syntax highlighting in Gradle Kotlin DSL scripts
2 Code completion, navigation to sources, documentation, refactorings etc…​ in Gradle Kotlin DSL scripts

As noted in the limitations, you must import your project using the Gradle model to enable content assist and refactoring tools for Kotlin DSL scripts in IntelliJ IDEA.

Builds with slow configuration time might affect the IDE responsiveness, so please check out the performance section to help resolve such issues.

Automatic build import vs. automatic reloading of script dependencies

Both IntelliJ IDEA and Android Studio will detect when you make changes to your build logic and offer two suggestions:

  1. Import the whole build again:

    IntelliJ IDEA
    IntelliJ IDEA
  2. Reload script dependencies when editing a build script:

    Reload script dependencies

We recommend disabling automatic build import while enabling automatic reloading of script dependencies. This approach provides early feedback when editing Gradle scripts while giving you control over when the entire build setup synchronizes with your IDE.

See the Troubleshooting section to learn more.

Kotlin DSL scripts

Just like its Groovy-based counterpart, the Kotlin DSL is built on Gradle’s Java API. Everything in a Kotlin DSL script is Kotlin code, compiled and executed by Gradle. Many of the objects, functions, and properties in your build scripts come from the Gradle API and the APIs of applied plugins.

Tip
Use the Kotlin DSL reference search to explore available members.
Script file names
  • Groovy DSL script files use the .gradle file name extension.

  • Kotlin DSL script files use the .gradle.kts file name extension.

To activate the Kotlin DSL, use the .gradle.kts extension for your build scripts instead of .gradle. This also applies to the settings file (e.g., settings.gradle.kts) and initialization scripts.

You can mix Groovy DSL and Kotlin DSL scripts within the same build. For example, a Kotlin DSL build script can apply a Groovy DSL one, and different projects in a multi-project build can use either.

To improve IDE support, we recommend following these conventions:

  • Name settings scripts (or any script backed by a Gradle Settings object) using the pattern *.settings.gradle.kts. This includes script plugins applied from settings scripts.

  • Name initialization scripts using the pattern *.init.gradle.kts or simply init.gradle.kts.

This helps the IDE identify the object "backing" the script, whether it’s Project, Settings, or Gradle.

Implicit imports

All Kotlin DSL build scripts come with implicit imports, including:

  • The default Gradle API imports

  • The Kotlin DSL API, which includes types from the following packages:

    • org.gradle.kotlin.dsl

    • org.gradle.kotlin.dsl.plugins.dsl

    • org.gradle.kotlin.dsl.precompile

    • java.util.concurrent.Callable

    • java.util.concurrent.TimeUnit

    • java.math.BigDecimal

    • java.math.BigInteger

    • java.io.File

    • javax.inject.Inject

Avoid Using Internal Kotlin DSL APIs

Using internal Kotlin DSL APIs in plugins and build scripts can break builds when either Gradle or plugins are updated.

The Kotlin DSL API extends the public Gradle API with types listed in the corresponding API docs found in the packages above (but not in their subpackages).

Compilation warnings

Gradle Kotlin DSL scripts are compiled by Gradle during the configuration phase of your build.

Deprecation warnings found by the Kotlin compiler are reported on the console when compiling the scripts:

> Configure project :
w: build.gradle.kts:4:5: 'getter for uploadTaskName: String!' is deprecated. Deprecated in Java

It is possible to configure your build to fail on any warning emitted during script compilation by setting the org.gradle.kotlin.dsl.allWarningsAsErrors Gradle property to true:

gradle.properties
org.gradle.kotlin.dsl.allWarningsAsErrors=true
Type-safe model accessors

The Groovy DSL allows you to reference many build model elements by name, even if they are defined at runtime, such as named configurations or source sets.

For example, when the Java plugin is applied, you can access the implementation configuration via configurations.implementation.

The Kotlin DSL replaces this dynamic resolution with type-safe model accessors, which work with model elements contributed by plugins.

Understanding when type-safe model accessors are available

The Kotlin DSL currently provides various sets of type-safe model accessors, each tailored to different scopes.

For the main project build scripts and precompiled project script plugins:

Type-safe model accessors Example

Dependency and artifact configurations

implementation and runtimeOnly (contributed by the Java Plugin)

Project extensions and conventions, and extensions on them

sourceSets

Extensions on the dependencies and repositories containers, and extensions on them

testImplementation (contributed by the Java Plugin), mavenCentral

Elements in the tasks and configurations containers

compileJava (contributed by the Java Plugin), test

Elements in project-extension containers

Source sets contributed by the Java Plugin that are added to the sourceSets container: sourceSets.main.java { setSrcDirs(listOf("src/main/java")) }

For the main project settings script and precompiled settings script plugins:

Type-safe model accessors Example

Project extensions and conventions, contributed by Settings plugins, and extensions on them

pluginManagement, dependencyResolutionManagement

Important

Initialization scripts and script plugins do not have type-safe model accessors. These limitations will be removed in a future Gradle release.

The set of type-safe model accessors available is determined right before evaluating the script body, immediately after the plugins {} block. Model elements contributed after that point, such as configurations defined in your build script, will not work with type-safe model accessors:

build.gradle.kts
// Applies the Java plugin
plugins {
    id("java")
}

repositories {
    mavenCentral()
}

// Access to 'implementation' (contributed by the Java plugin) works here:
dependencies {
    implementation("org.apache.commons:commons-lang3:3.12.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher") // Add this if needed for runtime
}

// Add a custom configuration
configurations.create("customConfiguration")
// Type-safe accessors for 'customConfiguration' will NOT be available because it was created after the plugins block
dependencies {
    customConfiguration("com.google.guava:guava:32.1.2-jre") // ❌ Error: No type-safe accessor for 'customConfiguration'
}

However, this means you can use type-safe accessors for any model elements contributed by plugins that are applied by parent projects.

The following project build script demonstrates how you can access various configurations, extensions and other elements using type-safe accessors:

build.gradle.kts
plugins {
    `java-library`
}

dependencies {                              // (1)
    api("junit:junit:4.13")
    implementation("junit:junit:4.13")
    testImplementation("junit:junit:4.13")
}

configurations {                            // (1)
    implementation {
        resolutionStrategy.failOnVersionConflict()
    }
}

sourceSets {                                // (2)
    main {                                  // (3)
        java.srcDir("src/core/java")
    }
}

java {                                      // (4)
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

tasks {
    test {                                  // (5)
        testLogging.showExceptions = true
        useJUnit()
    }
}
  1. Uses type-safe accessors for the api, implementation and testImplementation dependency configurations contributed by the Java Library Plugin

  2. Uses an accessor to configure the sourceSets project extension

  3. Uses an accessor to configure the main source set

  4. Uses an accessor to configure the java source for the main source set

  5. Uses an accessor to configure the test task

Tip

Your IDE is aware of the type-safe accessors and will include them in its suggestions.

This applies both at the top level of your build scripts, where most plugin extensions are added to the Project object, and within the blocks that configure an extension.

Note that accessors for elements of containers such as configurations, tasks, and sourceSets leverage Gradle’s configuration avoidance APIs. For example, on tasks, accessors are of type TaskProvider<T> and provide a lazy reference and lazy configuration of the underlying task.

Here are some examples illustrating when configuration avoidance applies:

build.gradle.kts
tasks.test {
    // lazy configuration
    useJUnitPlatform()
}

// Lazy reference
val testProvider: TaskProvider<Test> = tasks.test

testProvider {
    // lazy configuration
}

// Eagerly realized Test task, defeats configuration avoidance if done out of a lazy context
val test: Test = tasks.test.get()

For all other containers, accessors for elements are of type NamedDomainObjectProvider<T>, providing the same behavior:

build.gradle.kts
val mainSourceSetProvider: NamedDomainObjectProvider<SourceSet> = sourceSets.named("main")
Understanding what to do when type-safe model accessors are not available

Consider the sample build script shown above, which demonstrates the use of type-safe accessors. The following sample is identical, except it uses the apply() method to apply the plugin.

In this case, the build script cannot use type-safe accessors because the apply() call occurs in the body of the build script. You must use another techniques instead, as demonstrated here:

build.gradle.kts
apply(plugin = "java-library")

dependencies {
    "api"("junit:junit:4.13")
    "implementation"("junit:junit:4.13")
    "testImplementation"("junit:junit:4.13")
}

configurations {
    "implementation" {
        resolutionStrategy.failOnVersionConflict()
    }
}

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

configure<JavaPluginExtension> {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

tasks {
    named<Test>("test") {
        testLogging.showExceptions = true
    }
}

Type-safe accessors are unavailable for model elements contributed by the following:

  • Plugins applied via the apply(plugin = "id") method.

  • The project build script.

  • Script plugins, via apply(from = "script-plugin.gradle.kts").

  • Plugins applied via cross-project configuration.

You cannot use type-safe accessors in binary Gradle plugins implemented in Kotlin.

If you can’t find a type-safe accessor, fall back to using the normal API for the corresponding types. To do so, you need to know the names and/or types of the configured model elements. We will now show you how these can be discovered by examining the script in detail.

Artifact configurations

The following sample demonstrates how to reference and configure artifact configurations without type-safe accessors:

build.gradle.kts
apply(plugin = "java-library")

dependencies {
    "api"("junit:junit:4.13")
    "implementation"("junit:junit:4.13")
    "testImplementation"("junit:junit:4.13")
}

configurations {
    "implementation" {
        resolutionStrategy.failOnVersionConflict()
    }
}

The code looks similar to that of the type-safe accessors, except that the configuration names are string literals. You can use string literals for configuration names in dependency declarations and within the configurations {} block.

While the IDE won’t be able to help you discover the available configurations, you can look them up either in the corresponding plugin’s documentation or by running ./gradlew dependencies.

Project extensions

Project extensions have both a name and a unique type. However, the Kotlin DSL only needs to know the type to configure them.

The following sample shows the sourceSets {} and java {} blocks from the original example build script. The configure<T>() function is used with the corresponding type:

build.gradle.kts
apply(plugin = "java-library")

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

configure<JavaPluginExtension> {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

Note that sourceSets is a Gradle extension on Project of type SourceSetContainer. In the example above, java refers to the SourceSet.java property (a SourceDirectorySet), not the top-level JavaPluginExtension.

You can discover available extensions by either reviewing the documentation for the applied plugins or running ./gradlew kotlinDslAccessorsReport. The report generates the Kotlin code needed to access the model elements contributed by the applied plugins, providing both names and types.

As a last resort, you can check the plugin’s source code, though this should not be necessary in most cases.

You can also use the the<T>() function if you only need a reference to the extension without configuring it, or if you want to perform a one-line configuration:

build.gradle.kts
the<SourceSetContainer>()["main"].java.srcDir("src/main/java")

The snippet above also demonstrates one way to configure elements of a project extension that is a container.

Elements in project-extension containers

Container-based project extensions, such as SourceSetContainer, allow you to configure the elements they hold.

In our sample build script, we want to configure a source set named main within the source set container. We can do this by using the named() method instead of an accessor:

build.gradle.kts
apply(plugin = "java-library")

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

All elements within a container-based project extension have a name, so you can use this technique in all such cases.

For project extensions and conventions, you can discover what elements are present in any container by either checking the documentation for the applied plugins or by running ./gradlew kotlinDslAccessorsReport.

As a last resort, you may also review the plugin’s source code to find out what it does.

Tasks

Tasks are not managed through a container-based project extension, but they are part of a container that behaves in a similar way.

This means that you can configure tasks in the same way as you do for source sets. The following example illustrates this approach:

build.gradle.kts
apply(plugin = "java-library")

tasks {
    named<Test>("test") {
        testLogging.showExceptions = true
    }
}

We are using the Gradle API to refer to tasks by name and type, rather than using accessors.

Note that it is necessary to specify the type of the task explicitly. If you don’t, the script won’t compile because the inferred type will be Task, not Test, and the testLogging property is specific to the Test task type.

However, you can omit the type if you only need to configure properties or call methods that are common to all tasks, i.e., those declared on the Task interface.

You can discover what tasks are available by running ./gradlew tasks.

To find out the type of a given task, run ./gradlew help --task <taskName>, as demonstrated here:

❯ ./gradlew help --task test
...
Type
     Test (org.gradle.api.tasks.testing.Test)

The IDE can assist you with the required imports, so you only need the simple names of the types, without the package name part. In this case, there’s no need to import the Test task type, as it is part of the Gradle API and is therefore imported implicitly.

Working with container objects

The Gradle build model makes extensive use of container objects (or simply "containers").

For example, configurations and tasks are containers that hold Configuration and Task objects, respectively. Community plugins also contribute containers, such as the android.buildTypes container contributed by the Android Plugin.

The Kotlin DSL provides multiple ways for build authors to interact with containers. We will explore each of these methods, using the tasks container as an example.

Tip
You can leverage the type-safe accessors described in another section when configuring existing elements on supported containers. That section also explains which containers support type-safe accessors.
Using the container API

All containers in Gradle implement NamedDomainObjectContainer<DomainObjectType>. Some containers can hold objects of different types and implement PolymorphicDomainObjectContainer<BaseType>. The simplest way to interact with containers is through these interfaces.

The following example demonstrates how you can use the named() method to configure existing tasks, and the register() method to create new tasks:

build.gradle.kts
tasks.named("check")                    // (1)
tasks.register("myTask1")               // (2)

tasks.named<JavaCompile>("compileJava") // (3)
tasks.register<Copy>("myCopy1")         // (4)

tasks.named("assemble") {               // (5)
    dependsOn(":myTask1")
}
tasks.register("myTask2") {             // (6)
    description = "Some meaningful words"
}

tasks.named<Test>("test") {             // (7)
    testLogging.showStackTraces = true
}
tasks.register<Copy>("myCopy2") {       // (8)
    from("source")
    into("destination")
}
  1. Gets a reference of type Task to the existing task named check

  2. Registers a new untyped task named myTask1

  3. Gets a reference to the existing task named compileJava of type JavaCompile

  4. Registers a new task named myCopy1 of type Copy

  5. Gets a reference to the existing (untyped) task named assemble and configures it — you can only configure properties and methods that are available on Task with this syntax

  6. Registers a new untyped task named myTask2 and configures it — you can only configure properties and methods that are available on Task in this case

  7. Gets a reference to the existing task named test of type Test and configures it — in this case you have access to the properties and methods of the specified type

  8. Registers a new task named myCopy2 of type Copy and configures it

Note
The above sample relies on the configuration avoidance APIs. If you need or want to eagerly configure or register container elements, simply replace named() with getByName() and register() with create().
Configuring multiple container elements together

When configuring several elements of a container, you can group interactions in a block to avoid repeating the container’s name on each interaction.

The following example demonstrates a combination of type-safe accessors and the container API:

build.gradle.kts
tasks {
    test {
        testLogging.showStackTraces = true
    }
    val myCheck = register("myCheck") {
        doLast { /* assert on something meaningful */ }
    }
    check {
        dependsOn(myCheck)
    }
    register("myHelp") {
        doLast { /* do something helpful */ }
    }
}
Working with extra properties

Extra properties are available on any object that implements the ExtensionAware interface.

The Kotlin DSL provides the extra accessor for working with extra properties as a map:

build.gradle.kts
extra["myNewProperty"] = "initial value"  // (1)

val myExtraProperty = extra["myNewProperty"] as String  // (2)
val myExtraNullableProperty = extra["myNullableProperty"] as String?  // (3)
  1. Creates a new extra property called myNewProperty in the current context (the project in this case) and initializes it with the value "initial value"

  2. Access an existing extra property from the current context (the project in this case)

  3. Does the same as the previous line but allows the property to have a null value

This approach works for all Gradle scripts: project build scripts, script plugins, settings scripts, and initialization scripts.

You can also access extra properties on a root project from a subproject using the following syntax:

my-sub-project/build.gradle.kts
val myNewProperty = rootProject.extra["myNewProperty"] as String  // (1)
  1. Reads the root project’s myNewProperty extra property

Extra properties aren’t just limited to projects. For example, Task extends ExtensionAware, so you can attach extra properties to tasks as well.

Here’s an example that defines a new reportType on the test task and then uses that property to initialize another task:

build.gradle.kts
tasks {
    test {
        extra["reportType"] = "dev"  // (1)
        doLast {
            // Use 'reportType' for post-processing of reports
        }
    }

    register<Zip>("archiveTestReports") {
        from(test.map { it.reports.html.outputLocation })
        archiveAppendix = test.map { it.extra["reportType"] as String } // (2)
    }
}
  1. Creates a new reportType extra property on the test task

  2. Reads the test task’s reportType extra property to configure the archiveTestReports task

Working with Gradle types

Property, Provider, and NamedDomainObjectProvider are types that represent deferred and lazy evaluation of values and objects. The Kotlin DSL provides a specialized syntax for working with these types.

Using a Property

A property represents a value that can be set and read lazily:

  • Setting a value: property.set(value) or property = value

  • Accessing the value: property.get()

build.gradle.kts
val myProperty: Property<String> = project.objects.property(String::class.java)

myProperty.set("Hello, Gradle!") // Set the value
println(myProperty.get())        // Access the value

// Using .get() to read the value
val propValue: String = myProperty.get()
println(propValue)

// Using assignment syntax
myProperty = "Hi, Gradle!" // Set the value
println(myProperty.get())  // Access the value
Using a Provider

A provider represents a read-only, lazily-evaluated value:

  • Accessing the value: provider.get()

  • Chaining: provider.map { transform(it) }

build.gradle.kts
val versionProvider: Provider<String> = project.provider { "1.0.0" }

println(versionProvider.get()) // Access the value

// Chaining transformations
val majorVersion: Provider<String> = versionProvider.map { it.split(".")[0] }
println(majorVersion.get()) // Prints: "1"
Using a NamedDomainObjectProvider

A named domain object provider represents a lazily-evaluated named object from a Gradle container (like tasks or extensions):

  • Accessing the object: namedObjectProvider.get()

  • Configuring the object: namedObjectProvider.configure { …​ }

build.gradle.kts
val myTaskProvider: NamedDomainObjectProvider<Task> = tasks.named("build")

// Configuring the task
myTaskProvider.configure {
    doLast {
        println("Build task completed!")
    }
}

// Accessing the task
val myTask: Task = myTaskProvider.get()
Lazy property assignment

Gradle’s Kotlin DSL supports lazy property assignment using the = operator.

Lazy property assignment reduces verbosity when lazy properties are used. It works for properties that are publicly seen as final (without a setter) and have type Property or ConfigurableFileCollection. Since properties must be final, we generally recommend avoiding custom setters for properties with lazy types and, if possible, implementing such properties via an abstract getter.

Using the = operator is the preferred way to call set() in the Kotlin DSL:

build.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

abstract class WriteJavaVersionTask : DefaultTask() {
    @get:Input
    abstract val javaVersion: Property<String>
    @get:OutputFile
    abstract val output: RegularFileProperty

    @TaskAction
    fun execute() {
        output.get().asFile.writeText("Java version: ${javaVersion.get()}")
    }
}

tasks.register<WriteJavaVersionTask>("writeJavaVersion") {
    javaVersion.set("17") // (1)
    javaVersion = "17" // (2)
    javaVersion = java.toolchain.languageVersion.map { it.toString() } // (3)
    output = layout.buildDirectory.file("writeJavaVersion/javaVersion.txt")
}
  1. Set value with the .set() method

  2. Set value with lazy property assignment using the = operator

  3. The = operator can be used also for assigning lazy values

IDE support

Lazy property assignment is supported from IntelliJ 2022.3 and from Android Studio Giraffe.

Kotlin DSL Plugin

The Kotlin DSL Plugin provides a convenient way to develop Kotlin-based projects that contribute build logic. This includes buildSrc projects, included builds, and Gradle plugins.

The plugin achieves this by doing the following:

  • Applies the Kotlin Plugin, which adds support for compiling Kotlin source files.

  • Adds the kotlin-stdlib, kotlin-reflect, and gradleKotlinDsl() dependencies to the compileOnly and testImplementation configurations, enabling the use of those Kotlin libraries and the Gradle API in your Kotlin code.

  • Configures the Kotlin compiler with the same settings used for Kotlin DSL scripts, ensuring consistency between your build logic and those scripts:

  • Enables support for precompiled script plugins.

Each Gradle release is meant to be used with a specific version of the kotlin-dsl plugin. Compatibility between arbitrary Gradle releases and kotlin-dsl plugin versions is not guaranteed. Using an unexpected version of the kotlin-dsl plugin will emit a warning and can cause hard-to-diagnose problems.

This is the basic configuration you need to use the plugin:

buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    // The org.jetbrains.kotlin.jvm plugin requires a repository
    // where to download the Kotlin compiler dependencies from.
    mavenCentral()
}

The Kotlin DSL Plugin leverages Java Toolchains. By default, the code will target Java 8.

Embedded Kotlin

Gradle embeds Kotlin in order to provide support for Kotlin-based scripts.

Kotlin versions

Gradle ships with kotlin-compiler-embeddable plus matching versions of kotlin-stdlib and kotlin-reflect libraries. For details, see the Kotlin section of Gradle’s compatibility matrix. The kotlin package from those modules is visible through the Gradle classpath.

The compatibility guarantees provided by Kotlin apply for both backward and forward compatibility.

Backward compatibility

Our approach is to only make backward-incompatible Kotlin upgrades with major Gradle releases. We clearly document the Kotlin version shipped with each release and announce upgrade plans ahead of major releases.

Plugin authors aiming to maintain compatibility with older Gradle versions must limit their API usage to what is supported by those versions. This is no different from working with any new API in Gradle. For example, if a new API for dependency resolution is introduced, a plugin must either drop support for older Gradle versions or organize its code to conditionally execute the new code path on compatible versions.

Forward compatibility

The primary compatibility concern lies between the external kotlin-gradle-plugin version and the kotlin-stdlib version shipped with Gradle. More broadly, this applies to any plugin that transitively depends on kotlin-stdlib and its version provided by Gradle. As long as the versions are compatible, everything should work as expected. This issue will diminish as the Kotlin language matures.

Kotlin compiler arguments

The following Kotlin compiler arguments are used for compiling Kotlin DSL scripts, as well as Kotlin sources and scripts in projects with the kotlin-dsl plugin applied:

-java-parameters

Generate metadata for Java >= 1.8 reflection on method parameters. See Kotlin/JVM compiler options in the Kotlin documentation for more information.

-jvm-default=enable

Makes all non-abstract members of Kotlin interfaces default for the Java classes implementing them. This is to provide a better interoperability with Java and Groovy for plugins written in Kotlin. See Default methods in interfaces in the Kotlin documentation for more information.

-Xsam-conversions=class

Sets up the implementation strategy for SAM (single abstract method) conversion to always generate anonymous classes, instead of using the invokedynamic JVM instruction. This is to provide a better support for configuration cache and incremental build. See KT-44912 in the Kotlin issue tracker for more information.

-Xjsr305=strict & -Xjspecify-annotations=strict

Sets up Kotlin’s Java interoperability to strictly follow JSR-305 and JSpecify annotations for increased null safety. See Calling Java code from Kotlin in the Kotlin documentation for more information.

Interoperability

When mixing languages in your build logic, you may have to cross language boundaries. An extreme example would be a build that uses tasks and plugins that are implemented in Java, Groovy and Kotlin, while also using both Kotlin DSL and Groovy DSL build scripts.

Kotlin is designed with Java Interoperability in mind. Existing Java code can be called from Kotlin in a natural way, and Kotlin code can be used from Java rather smoothly as well.
— Kotlin reference documentation

Both calling Java from Kotlin and calling Kotlin from Java are very well covered in the Kotlin reference documentation.

The same mostly applies to interoperability with Groovy code. In addition, the Kotlin DSL provides several ways to opt into Groovy semantics, which we look at next.

Static extensions

Both the Groovy and Kotlin languages support extending existing classes via Groovy Extension modules and Kotlin extensions.

To call a Kotlin extension function from Groovy, call it as a static function, passing the receiver as the first parameter:

build.gradle
TheTargetTypeKt.kotlinExtensionFunction(receiver, "parameters", 42, aReference)

Kotlin extension functions are package-level functions. You can learn how to locate the name of the type declaring a given Kotlin extension in the Package-Level Functions section of the Kotlin reference documentation.

To call a Groovy extension method from Kotlin, the same approach applies: call it as a static function passing the receiver as the first parameter:

build.gradle.kts
TheTargetTypeGroovyExtension.groovyExtensionMethod(receiver, "parameters", 42, aReference)
Named parameters and default arguments

Both the Groovy and Kotlin languages support named function parameters and default arguments, although they are implemented very differently. Kotlin has fully-fledged support for both, as described in the Kotlin language reference under named arguments and default arguments. Groovy implements named arguments in a non-type-safe way based on a Map<String, ?> parameter, which means they cannot be combined with default arguments. In other words, you can only use one or the other in Groovy for any given method.

Calling Kotlin from Groovy

To call a Kotlin function that has named arguments from Groovy, just use a normal method call with positional parameters:

build.gradle
kotlinFunction("value1", "value2", 42)

There is no way to provide values by argument name.

To call a Kotlin function that has default arguments from Groovy, always pass values for all the function parameters.

Calling Groovy from Kotlin

To call a Groovy function with named arguments from Kotlin, you need to pass a Map<String, ?>, as shown in this example:

build.gradle.kts
groovyNamedArgumentTakingMethod(mapOf(
    "parameterName" to "value",
    "other" to 42,
    "and" to aReference))

To call a Groovy function with default arguments from Kotlin, always pass values for all the parameters.

Groovy closures from Kotlin

You may sometimes have to call Groovy methods that take Closure arguments from Kotlin code. For example, some third-party plugins written in Groovy expect closure arguments.

Note
Gradle plugins written in any language should prefer the type Action<T> type in place of closures. Groovy closures and Kotlin lambdas are automatically mapped to arguments of that type.

In order to provide a way to construct closures while preserving Kotlin’s strong typing, two helper methods exist:

  • closureOf<T> {}

  • delegateClosureOf<T> {}

Both methods are useful in different circumstances and depend upon the method you are passing the Closure instance into.

Some plugins expect simple closures, as with the Bintray plugin:

build.gradle.kts
bintray {
    pkg(closureOf<PackageConfig> {
        // Config for the package here
    })
}

In other cases, like with the Gretty Plugin when configuring farms, the plugin expects a delegate closure:

build.gradle.kts
farms {
    farm("OldCoreWar", delegateClosureOf<FarmExtension> {
        // Config for the war here
    })
}

There sometimes isn’t a good way to tell, from looking at the source code, which version to use. Usually, if you get a NullPointerException with closureOf<T> {}, using delegateClosureOf<T> {} will resolve the problem.

These two utility functions are useful for configuration closures, but some plugins might expect Groovy closures for other purposes. The KotlinClosure0 to KotlinClosure2 types allows adapting Kotlin functions to Groovy closures with more flexibility:

build.gradle.kts
somePlugin {

    // Adapt parameter-less function
    takingParameterLessClosure(KotlinClosure0({
        "result"
    }))

    // Adapt unary function
    takingUnaryClosure(KotlinClosure1<String, String>({
        "result from single parameter $this"
    }))

    // Adapt binary function
    takingBinaryClosure(KotlinClosure2<String, String, String>({ a, b ->
        "result from parameters $a and $b"
    }))
}
The Kotlin DSL Groovy Builder

If some plugin makes heavy use of Groovy metaprogramming, then using it from Kotlin or Java or any statically-compiled language can be very cumbersome.

The Kotlin DSL provides a withGroovyBuilder {} utility extension that attaches the Groovy metaprogramming semantics to objects of type Any.

The following example demonstrates several features of the method on the object target:

build.gradle.kts
target.withGroovyBuilder {                                          // (1)

    // GroovyObject methods available                               // (2)
    if (hasProperty("foo")) { /*...*/ }
    val foo = getProperty("foo")
    setProperty("foo", "bar")
    invokeMethod("name", arrayOf("parameters", 42, aReference))

    // Kotlin DSL utilities
    "name"("parameters", 42, aReference)                            // (3)
        "blockName" {                                               // (4)
            // Same Groovy Builder semantics on `blockName`
        }
    "another"("name" to "example", "url" to "https://example.com/") // (5)
}
  1. The receiver is a GroovyObject and provides Kotlin helpers

  2. The GroovyObject API is available

  3. Invoke the methodName method, passing some parameters

  4. Configure the blockName property, maps to a Closure taking method invocation

  5. Invoke another method taking named arguments, maps to a Groovy named arguments Map<String, ?> taking method invocation

Using a Groovy script

Another option when dealing with problematic plugins that assume a Groovy DSL build script is to configure them in a Groovy DSL build script that is applied from the main Kotlin DSL build script:

dynamic-groovy-plugin-configuration.gradle
native {    // (1)
    dynamic {
        groovy as Usual
    }
}
build.gradle.kts
plugins {
    id("dynamic-groovy-plugin") version "1.0"   // (2)
}
apply(from = "dynamic-groovy-plugin-configuration.gradle")  // (3)
  1. The Groovy script uses dynamic Groovy to configure plugin

  2. The Kotlin build script requests and applies the plugin

  3. The Kotlin build script applies the Groovy script

Troubleshooting

The IDE support is provided by two components:

  1. Kotlin Plugin (used by IntelliJ IDEA/Android Studio).

  2. Gradle.

The level of support varies based on the versions of each.

If you encounter issues, first run ./gradlew tasks from the command line to determine if the problem is specific to the IDE. If the issue persists on the command line, it likely originates from the build itself rather than IDE integration.

However, if the build runs successfully on the command line but your script editor reports errors, try restarting your IDE and invalidating its caches.

If the issue persists, and you suspect a problem with the Kotlin DSL script editor, try the following:

  • Run ./gradlew tasks to gather more details.

  • Check the logs in one of these locations:

    • $HOME/Library/Logs/gradle-kotlin-dsl on macOS

    • $HOME/.gradle-kotlin-dsl/log on Linux

    • $HOME/AppData/Local/gradle-kotlin-dsl/log on Windows

  • Report the issue on the Gradle issue tracker, including as much detail as possible.

From version 5.1 onward, the log directory is automatically cleaned. Logs are checked periodically (at most, every 24 hours), and files are deleted if unused for 7 days.

If this doesn’t help pinpoint the problem, you can enable the org.gradle.kotlin.dsl.logging.tapi system property in your IDE. This causes the Gradle Daemon to log additional details in its log file located at $HOME/.gradle/daemon.

In IntelliJ IDEA, enable this property by navigating to Help > Edit Custom VM Options…​ and adding: -Dorg.gradle.kotlin.dsl.logging.tapi=true.

For IDE problems outside the Kotlin DSL script editor, please open issues in the corresponding IDE’s issue tracker:

Lastly, if you face problems with Gradle itself or with the Kotlin DSL, please open issues on the Gradle issue tracker.

Limitations
  • The Kotlin DSL is known to be slower than the Groovy DSL on first use, for example with clean checkouts or on ephemeral continuous integration agents. Changing something in the buildSrc directory also has an impact as it invalidates build-script caching. The main reason for this is the slower script compilation for Kotlin DSL.

  • In IntelliJ IDEA, you must import your project from the Gradle model in order to get content assist and refactoring support for your Kotlin DSL build scripts.

  • Kotlin DSL script compilation avoidance has known issues. If you encounter problems, it can be disabled by setting the org.gradle.kotlin.dsl.scriptCompilationAvoidance system property to false.

  • The Kotlin DSL will not support the model {} block, which is part of the discontinued Gradle Software Model.

If you run into trouble or discover a suspected bug, please report the issue in the Gradle issue tracker.

Migrating build logic from Groovy to Kotlin

This section will walk you through converting your Groovy-based Gradle build scripts to Kotlin.

Gradle’s newer Kotlin DSL provides a pleasant editing experience in supported IDEs: content-assist, refactoring, documentation, and more.

IntelliJ IDEA and Android Studio
Tip

Please also read the Gradle Kotlin DSL Primer to learn the specificities, limitations and usage of the Gradle Kotlin DSL.

The rest of the user manual contain build script excerpts that demonstrate both the Groovy DSL and the Kotlin DSL. This is the best place where to find how to do this and what with each DSL ; and it covers all Gradle features from using plugins to customizing the dependency resolution behavior.

Before you start migrating

Please read: It’s helpful to understand the following important information before you migrate:

  • Using the latest versions of Gradle, applied plugins, and your IDE should be your first move.

  • Kotlin DSL is fully supported in Intellij IDEA and Android Studio. Other IDEs, such as Eclipse or NetBeans, do not yet provide helpful tools for editing Gradle Kotlin DSL files, however, importing and working with Kotlin DSL-based builds work as usual.

  • In IntelliJ IDEA, you must import your project from the Gradle model to get content-assist and refactoring tools for Kotlin DSL scripts.

  • There are some situations where the Kotlin DSL is slower. First use, on clean checkouts or ephemeral CI agents for example, are known to be slower. The same applies to the scenario in which something in the buildSrc directory changes, which invalidates build-script caching. Builds with slow configuration time might affect the IDE responsiveness, please check out the documentation on Gradle performance.

  • You must run Gradle with Java 8 or higher. Java 7 is not supported.

  • The embedded Kotlin compiler is known to work on Linux, macOS, Windows, Cygwin, FreeBSD and Solaris on x86-64 architectures.

  • Knowledge of Kotlin syntax and basic language features is very helpful. The Kotlin reference documentation and Kotlin Koans should be useful to you.

  • Use of the plugins {} block to declare Gradle plugins significantly improves the editing experience, and is highly recommended. Consider adopting it in your Groovy build scripts before converting them to Kotlin.

  • The Kotlin DSL will not support model {} elements. This is part of the discontinued Gradle Software Model.

Read more in the Gradle Kotlin DSL Primer.

If you run to trouble or a suspected bug, please take advantage of the gradle/gradle issue tracker.

You don’t have to migrate all at once! Both Groovy and Kotlin-based build scripts can apply other scripts of either language. You can find inspiration for any Gradle features not covered in the Kotlin DSL samples.

Prepare your Groovy scripts

Some simple Kotlin and Groovy language differences can make converting scripts tedious:

  • Groovy strings can be quoted with single quotes 'string' or double quotes "string" whereas Kotlin requires double quotes "string".

  • Groovy allows to omit parentheses when invoking functions whereas Kotlin always requires the parentheses.

  • The Gradle Groovy DSL allows to omit the = assignment operator when assigning properties whereas Kotlin always requires the assignment operator.

As a first migration step, it is recommended to prepare your Groovy build scripts by

  • unifying quotes using double quotes,

  • disambiguating function invocations and property assignments (using respectively parentheses and assignment operator).

The former can easily be done by searching for ' and replacing by ". For example,

group = 'com.acme'
dependencies {
    implementation 'com.acme:example:1.0'
}

becomes:

group "com.acme"
dependencies {
    implementation "com.acme:example:1.0"
}

The next step is a bit more involved as it may not be trivial to distinguish function invocations and property assignments in a Groovy script. A good strategy is to make all ambiguous statements property assignments first and then fix the build by turning the failing ones to function invocations.

For example,

group "com.acme"
dependencies {
    implementation "com.acme:example:1.0"
}

becomes:

group = "com.acme"                          // (1)
dependencies {
    implementation("com.acme:example:1.0")  // (2)
}
  1. Property assignment

  2. Function invocation

While staying valid Groovy, it is now unambiguous and close to the Kotlin syntax, making it easier to then rename the script to turn it into a Gradle Kotlin DSL script.

It is important to note that while Groovy extra properties can be modified using an object’s ext property, in Kotlin they are modified using the extra property. It is important to look at each object and update the build scripts accordingly.

You can find an example in the userguide.

Script file naming
Note
Groovy DSL script files use the .gradle file name extension. Kotlin DSL script files use the .gradle.kts file name extension.

To use the Kotlin DSL, simply name your files build.gradle.kts instead of build.gradle.

The settings file, settings.gradle, can also be renamed settings.gradle.kts.

In a multi-project build, you can have some modules using the Groovy DSL (with build.gradle) and others using the Kotlin DSL (with build.gradle.kts).

On top of that, apply the following conventions for better IDE support:

  • Name scripts that are applied to Settings according to the pattern *.settings.gradle.kts,

  • Name init scripts according to the pattern *.init.gradle.kts.

Applying plugins

Just like with the Groovy DSL, there are two ways to apply Gradle plugins:

Here’s an example using the declarative plugins {} block:

build.gradle.kts
plugins {
    java
    jacoco
    `maven-publish`
    id("org.springframework.boot") version "3.4.4"
}
build.gradle
plugins {
    id 'java'
    id 'jacoco'
    id 'maven-publish'
    id 'org.springframework.boot' version '3.4.4'
}

The Kotlin DSL provides property extensions for all Gradle core plugins, as shown above with the java, jacoco or maven-publish declaration.

Third party plugins can be applied the same way as with the Groovy DSL. Except for the double quotes and parentheses. You can also apply core plugins with that style. But the statically-typed accessors are recommended since they are type-safe and will be autocompleted by your IDE.

You can also use the imperative apply syntax, but then non-core plugins must be included on the classpath of the build script:

build.gradle.kts
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:3.4.4")
    }
}

apply(plugin = "java")
apply(plugin = "jacoco")
apply(plugin = "org.springframework.boot")
build.gradle
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath('org.springframework.boot:spring-boot-gradle-plugin:3.4.4')
    }
}

apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'org.springframework.boot'
Note

We strongly recommend that you use the plugins {} block in preference to the apply() function.

The declarative nature of the plugins {} block enables the Kotlin DSL to provide type-safe accessors to the extensions, configurations and other features contributed by the applied plugins, which makes it easy for IDEs to discover the details of the plugins' models and makes them easy to configure.
See the plugins {} block documentation in the Gradle user manual for more information.

Configuring plugins

Many plugins come with extensions to configure them. If those plugins are applied using the declarative plugins {} block, then Kotlin extension functions are made available to configure their extension, the same way as in Groovy. The following sample shows how this works for the Jacoco Plugin.

build.gradle.kts
plugins {
    jacoco
}

jacoco {
    toolVersion = "0.8.1"
}
build.gradle
plugins {
    id 'jacoco'
}

jacoco {
    toolVersion = '0.8.1'
}

By contrast, if you use the imperative apply() function to apply a plugin, then you will have to use the configure<T>() function to configure that plugin. The following sample shows how this works for the Checkstyle Plugin by explicitly declaring the plugin’s extension class — CheckstyleExtension — in the configure<T>() function:

build.gradle.kts
apply(plugin = "checkstyle")

configure<CheckstyleExtension> {
    maxErrors = 10
}
build.gradle
apply plugin: "checkstyle"

checkstyle {
    maxErrors = 10
}

Again, we strongly recommend that you apply plugins declaratively via the plugins {} block.

Knowing what plugin-provided extensions are available

Because your IDE knows about the configuration elements that a plugin provides, it will include those elements when you ask your IDE for suggestions. This will happen both at the top level of your build scripts — most plugin extensions are added to the Project object — and within an extension’s configuration block.

You can also run the :kotlinDslAccessorsReport task to learn about the extensions contributed by all applied plugins. It prints the Kotlin code you can use to access those extensions and provides the name and type of the accessor methods.

If the plugin you want to configure relies on groovy.lang.Closure in its method signatures or uses other dynamic Groovy semantics, more work will be required to configure that plugin from a Kotlin DSL build script. See the interoperability section of the Gradle Kotlin DSL documentation for more information on how to call Groovy code from Kotlin code or to keep that plugin’s configuration in a Groovy script.

Plugins also contribute tasks that you may want to configure directly. This topic is covered in the Configuring tasks section below.

Keeping build scripts declarative

To get the most benefits of the Gradle Kotlin DSL you should strive to keep your build scripts declarative. The main thing to remember here is that in order to get type-safe accessors, plugins must be applied before the body of build scripts.

It is strongly recommended to read about configuring plugins with the Gradle Kotlin DSL in the Gradle user manual.

If your build is a multi-project build, like mostly all Android builds for example, please also read the subsequent section about multi-project builds.

Finally, there are strategies to use the plugins {} block with plugins that aren’t published with the correct metadata, such as the Android Gradle Plugin.

Configuration avoidance

Gradle 4.9 introduced a new API for creating and configuring tasks in build scripts and plugins. The intent is for this new API to eventually replace the existing API.

One of the major differences between the existing and new Gradle Tasks API is whether or not Gradle spends the time to create Task instances and run configuration code. The new API allows Gradle to delay or completely avoid configuring tasks that will never be executed in a build. For example, when compiling code, Gradle does not need to configure tasks that run tests.

See the Evolving the Gradle API to reduce configuration time blog post and the Task Configuration Avoidance chapter in the user manual for more information.

The Gradle Kotlin DSL embraces configuration avoidance by making the type-safe model accessors leverage the new APIs and providing DSL constructs to make them easier to use. Rest assured, the whole Gradle API remains available.

Configuring tasks

The syntax for configuring tasks is where the Groovy and Kotlin DSLs start to differ significantly.

build.gradle.kts
tasks.jar {
    archiveFileName = "foo.jar"
}
build.gradle
tasks.jar {
    archiveFileName = 'foo.jar'
}

Note that in Kotlin the tasks.jar {} notation leverage the configuration avoidance API and defer the configuration of the jar task.

If the type-safe task accessor tasks.jar isn’t available, see the configuring plugins section above, you can fallback to using the tasks container API. The Kotlin flavor of the following sample is strictly equivalent to the one using the type-safe accessor above:

build.gradle.kts
tasks.named<Jar>("jar") {
    archiveFileName = "foo.jar"
}
build.gradle
tasks.named('jar') {
    archiveFileName = 'foo.jar'
}

Note that since Kotlin is a statically typed language, it is necessary to specify the type of the task explicitly. Otherwise, the script will not compile because the inferred type will be Task, not Jar, and the archiveName property is specific to the Jar task type.

If configuration avoidance is getting in your way migrating and you want to eagerly configure a task just like Groovy you can do so by using the eager configuration API on the tasks container:

build.gradle.kts
tasks.getByName<Jar>("jar") {
    archiveFileName = "foo.jar"
}
build.gradle
tasks.getByName('jar') {
    archiveFileName = 'foo.jar'
}

Working with containers in the Gradle Kotlin DSL is documented in detail here.

Knowing the type of a task

If you don’t know what type a task has, then you can find that information out via the built-in help task. Simply pass it the name of the task you’re interested in using the --task option, like so:

❯ ./gradlew help --task jar
...
Type
     Jar (org.gradle.api.tasks.bundling.Jar)

Let’s bring all this together by running through a quick worked example that configures the bootJar and bootRun tasks of a Spring Boot project:

build.gradle.kts
plugins {
    java
    id("org.springframework.boot") version "3.4.4"
}

tasks.bootJar {
    archiveFileName = "app.jar"
    mainClass = "com.example.demo.Demo"
}

tasks.bootRun {
    mainClass = "com.example.demo.Demo"
    args("--spring.profiles.active=demo")
}
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.4'
}

tasks.bootJar {
    archiveFileName = 'app.jar'
    mainClass = 'com.example.demo.Demo'
}

tasks.bootRun {
    mainClass = 'com.example.demo.Demo'
    args '--spring.profiles.active=demo'
}

This is pretty self explanatory. The main difference is that the task configuration automatically becomes lazy when using the Kotlin DSL accessors.

Now, for the sake of the example, let’s look at the same configuration applied using the API instead of the type-safe accessors that may not be available depending on the build logic structure, see the corresponding documentation in the Gradle user manual for more information.

We first determine the types of the bootJar and bootRun tasks via the help task:

❯ ./gradlew help --task bootJar
...
Type
     BootJar (org.springframework.boot.gradle.tasks.bundling.BootJar)
❯ ./gradlew help --task bootRun
...
Type
     BootRun (org.springframework.boot.gradle.tasks.run.BootRun)

Now that we know the types of the two tasks, we can import the relevant types — BootJar and BootRun — and configure the tasks as required. Note that the IDE can assist us with the required imports, so we only need the simple names, i.e. without the full packages. Here’s the resulting build script, complete with imports:

build.gradle.kts
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

// TODO:Finalize Upload Removal - Issue #21439
plugins {
    java
    id("org.springframework.boot") version "3.4.4"
}

tasks.named<BootJar>("bootJar") {
    archiveFileName = "app.jar"
    mainClass = "com.example.demo.Demo"
}

tasks.named<BootRun>("bootRun") {
    mainClass = "com.example.demo.Demo"
    args("--spring.profiles.active=demo")
}
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.4'
}

tasks.named('bootJar') {
    archiveFileName = 'app.jar'
    mainClass = 'com.example.demo.Demo'
}

tasks.named('bootRun') {
    mainClass = 'com.example.demo.Demo'
    args '--spring.profiles.active=demo'
}
Creating tasks

Creating tasks can be done using the script top-level function named task(…​):

build.gradle.kts
task("greeting") {
    doLast { println("Hello, World!") }
}
build.gradle
task greeting {
    doLast { println 'Hello, World!' }
}

Note that the above eagerly configures the created task with both Groovy and Kotlin DSLs.

Registering or creating tasks can also be done on the tasks container, respectively using the register(…​) and create(…​) functions as shown here:

build.gradle.kts
tasks.register("greeting") {
    doLast { println("Hello, World!") }
}
build.gradle
tasks.register('greeting') {
    doLast { println('Hello, World!') }
}
build.gradle.kts
tasks.create("greeting") {
    doLast { println("Hello, World!") }
}
build.gradle
tasks.create('greeting') {
    doLast { println('Hello, World!') }
}

The samples above create untyped, ad-hoc tasks, but you will more commonly want to create tasks of a specific type. This can also be done using the same register() and create() methods. Here’s an example that creates a new task of type Zip:

build.gradle.kts
tasks.register<Zip>("docZip") {
    archiveFileName = "doc.zip"
    from("doc")
}
build.gradle
tasks.register('docZip', Zip) {
    archiveFileName = 'doc.zip'
    from 'doc'
}
build.gradle.kts
tasks.create<Zip>("docZip") {
    archiveFileName = "doc.zip"
    from("doc")
}
build.gradle
tasks.create(name: 'docZip', type: Zip) {
    archiveFileName = 'doc.zip'
    from 'doc'
}
Configurations and dependencies

Declaring dependencies in existing configurations is similar to the way it’s done in Groovy build scripts, as you can see in this example:

build.gradle.kts
plugins {
    `java-library`
}
dependencies {
    implementation("com.example:lib:1.1")
    runtimeOnly("com.example:runtime:1.0")
    testImplementation("com.example:test-support:1.3") {
        exclude(module = "junit")
    }
    testRuntimeOnly("com.example:test-junit-jupiter-runtime:1.3")
}
build.gradle
plugins {
    id 'java-library'
}
dependencies {
    implementation 'com.example:lib:1.1'
    runtimeOnly 'com.example:runtime:1.0'
    testImplementation('com.example:test-support:1.3') {
        exclude(module: 'junit')
    }
    testRuntimeOnly 'com.example:test-junit-jupiter-runtime:1.3'
}

Each configuration contributed by an applied plugin is also available as a member of the configurations container, so you can reference it just like any other configuration.

Knowing what configurations are available

The easiest way to find out what configurations are available is by asking your IDE for suggestions within the configurations container.

You can also use the :kotlinDslAccessorsReport task, which prints the Kotlin code for accessing the configurations contributed by applied plugins and provides the names for all of those accessors.

Note that if you do not use the plugins {} block to apply your plugins, then you won’t be able to configure the dependency configurations provided by those plugins in the usual way. Instead, you will have to use string literals for the configuration names, which means you won’t get IDE support:

build.gradle.kts
apply(plugin = "java-library")
dependencies {
    "implementation"("com.example:lib:1.1")
    "runtimeOnly"("com.example:runtime:1.0")
    "testImplementation"("com.example:test-support:1.3") {
        exclude(module = "junit")
    }
    "testRuntimeOnly"("com.example:test-junit-jupiter-runtime:1.3")
}
build.gradle
apply plugin: 'java-library'
dependencies {
    implementation 'com.example:lib:1.1'
    runtimeOnly 'com.example:runtime:1.0'
    testImplementation('com.example:test-support:1.3') {
        exclude(module: 'junit')
    }
    testRuntimeOnly 'com.example:test-junit-jupiter-runtime:1.3'
}

This is just one more reason to use the plugins {} block whenever you can!

Custom configurations and dependencies

Sometimes you need to create your own configurations and attach dependencies to them. The following example declares two new configurations:

  • db, to which we add a PostgreSQL dependency

  • integTestImplementation, which is configured to extend the testImplementation configuration and to which we add a different dependency

build.gradle.kts
val db = configurations.create("db")
val integTestImplementation = configurations.create("integTestImplementation") {
    extendsFrom(configurations["testImplementation"])
}

dependencies {
    db("org.postgresql:postgresql")
    integTestImplementation("com.example:integ-test-support:1.3")
}
build.gradle
configurations {
    db
    integTestImplementation {
        extendsFrom testImplementation
    }
}

dependencies {
    db 'org.postgresql:postgresql'
    integTestImplementation 'com.example:integ-test-support:1.3'
}

Note that we can only use the db(…​) and integTestImplementation(…​) notation within the dependencies {} block in the above example because both configurations are created and referenced beforehand via the create() method. If the configurations were defined elsewhere, you could only reference them either by first accessing them via configurations — as opposed to configurations.create() — or by using string literals within the dependencies {} block. The following example demonstrates both approaches:

build.gradle.kts
// get the existing 'testRuntimeOnly' configuration
val testRuntimeOnly = configurations["testRuntimeOnly"]

dependencies {
    testRuntimeOnly("com.example:test-junit-jupiter-runtime:1.3")
    "db"("org.postgresql:postgresql")
    "integTestImplementation"("com.example:integ-test-support:1.3")
}
Migration strategies

As we’ve seen above, both scripts using the Kotlin DSL and those using the Groovy DSL can participate in the same build. In addition, Gradle plugins from the buildSrc directory, an included build or an external location can be implemented using any JVM language. This makes it possible to migrate a build progressively, piece by piece, without blocking your team.

Two approaches to migrations stand out:

  • Migrating the existing syntax of your build to Kotlin, bit by bit, while retaining the structure — what we call a mechanical migration

  • Restructuring your build logic towards Gradle best practices and switching to Kotlin DSL as part of that effort

Both approaches are viable. A mechanical migration will be enough for simple builds. A complex and highly dynamic build may require some restructuring anyway, so in such cases reimplementing build logic to follow Gradle best practice makes sense.

Since applying Gradle best practices will make your builds easier to use and faster, we recommend that you migrate all projects in that way eventually, but it makes sense to focus on the projects that have to be restructured first and those that would benefit most from the improvements.

Also consider that the more parts of your build logic rely on the dynamic aspects of Groovy, the harder they will be to use from the Kotlin DSL. You’ll find recipes on how to cross the dynamic boundaries from static Kotlin in the interoperability section of the Gradle Kotlin DSL documentation, regardless of where the dynamic Groovy build logic resides.

There are two key best practices that make it easier to work within the static context of the Kotlin DSL:

  • Using the plugins {} block

  • Putting local build logic in the build’s buildSrc directory

The plugins {} block is about keeping your build scripts declarative in order to get the best out of the Kotlin DSL.

Utilizing the buildSrc project is about organizing your build logic into shared local plugins and conventions that are easily testable and provide good IDE support.

Kotlin DSL build structure samples

Depending on your build structure you might be interested in the following user manual chapters:

Interoperability

When mixing languages in your build logic, you may have to cross language boundaries. An extreme example would be a build that uses tasks and plugins that are implemented in Java, Groovy and Kotlin, while also using both Kotlin DSL and Groovy DSL build scripts.

Quoting the Kotlin reference documentation:

Kotlin is designed with Java Interoperability in mind. Existing Java code can be called from Kotlin in a natural way, and Kotlin code can be used from