Gradle User Manual: Version 9.7.0
- OVERVIEW
- RELEASES
- FUNDAMENTALS
- Core Concepts
- Wrapper Basics
- Command-Line Interface Basics
- Settings File Basics
- Build File Basics
- Dependencies and Dependency Management Basics
- Task Basics
- Incremental Builds and Build Caching Basic
- Plugin Basics
- Build Scan Basics
- Anatomy of a Gradle Build
- Structuring Multi-Project Builds
- Gradle Build Lifecycle
- Writing Build Scripts
- Gradle Managed Types
- Declaring and Managing Dependencies
- Creating and Registering Tasks
- Working with Plugins
- Plugin Introduction
- Pre-compiled Script Plugins
- Binary Plugin
- Binary Plugin Development
- Binary Plugin Testing
- Binary Plugin Publishing
- BEST PRACTICES
- RUNTIME AND CONFIGURATION
- DSLS AND APIS
- CORE PLUGINS
- Gradle Plugin Reference
- The Java Plugin
- The Java Library Plugin
- The Java Platform Plugin
- The Groovy Plugin
- The Scala Plugin
- The ANTLR Plugin
- The JVM Test Suite Plugin
- The Test Report Aggregation Plugin
- C++ Application
- C++ Library
- C++ Unit Test
- Swift Application
- Swift Library
- XCTest
- The Application Plugin
- The War Plugin
- The Ear Plugin
- The Distribution Plugin
- The Java Library Distribution Plugin
- The Checkstyle Plugin
- The PMD Plugin
- The JaCoCo Plugin
- The JaCoCo Report Aggregation Plugin
- The CodeNarc Plugin
- The Eclipse Plugins
- The IDEA Plugin
- Visual Studio
- Xcode
- The Base Plugin
- Build Init Plugin
- The Signing Plugin
- Gradle Plugin Development Plugin
- The Project Report Plugin
- The Build Dashboard Plugin
- TASK DEVELOPMENT
- PLUGIN DEVELOPMENT
- DEPENDENCY MANAGEMENT
- Dependency Management
- 1. Declaring dependencies
- 2. Dependency Configurations
- 3. Declaring repositories
- 4. Centralizing dependencies
- 5. Dependency Constraints and Conflict Resolution
- Declaring Dependencies
- Viewing Dependencies
- Declaring Versions and Ranges
- Declaring Dependency Constraints
- Creating Dependency Configurations
- Gradle distribution-specific dependencies
- Declaring Repositories Basics
- Centralizing Repository Declarations
- Repository Types
- Metadata Formats
- Supported Protocols
- Filtering Repository Content
- Platforms
- Version Catalogs
- Using Catalogs with Platforms
- Dependency Resolution Consistency
- Resolving Specific Artifacts
- Capabilities
- Variants and Attributes
- Artifact Views
- Artifact Transforms
- Locking Versions
- Using Resolution Rules
- Modifying Dependency Metadata
- Dependency Caching
- Dependency Resolution
- Graph Resolution
- Variant Selection and Attribute Matching
- Artifact Resolution
- Publishing a project as module
- Gradle Module Metadata
- Signing artifacts
- Customizing publishing
- The Maven Publish Plugin
- The Ivy Publish Plugin
- GRADLE MANAGED TYPES
- JVM BUILDS
- C++ BUILDS
- SWIFT BUILDS
- GRADLE ON CI
- STRUCTURING BUILDS
- OPTIMIZING GRADLE BUILDS
- Improve the Performance of Gradle Builds
- Build Cache
- Use cases for the build cache
- Build cache performance
- Important concepts
- Caching Java projects
- Caching Android projects
- Debugging and diagnosing Build Cache misses
- Solving common problems
- Configuration Cache
- Enabling and Configuring the Configuration Cache
- Configuration Cache Requirements for your Build Logic
- Debugging and Troubleshooting the Configuration Cache
- Configuration Cache Status
- Isolated Projects
- SECURING BUILDS
- INTEGRATION
OVERVIEW
Gradle User Manual
Gradle Build Tool
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++.
Compatible IDEs
All major IDEs support Gradle, including Android Studio, IntelliJ IDEA, Visual Studio Code, Eclipse, and NetBeans.
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:
For Build Engineers
Build engineers that are ready to configure custom build logic should start here:
For Plugin Developers
Plugin authors that are ready to develop and publish their own plugins should start here:
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.
Copyright
© 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.
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.
The tutorial covers:
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.
This section covers:
2. Intermediate Gradle Tutorial
The tutorial will take you from Gradle initialization all the way through registering tasks and the basics of plugins.
The tutorial covers:
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.
This section covers:
2. Advanced Gradle Tutorial
The tutorial will take you from Gradle initialization all the way through creating and publishing a binary plugin.
The tutorial covers:
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)
└── ⋮
-
Project root directory.
-
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
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 Properties → Advanced System Settings → Environmental 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:
-
Try running
gradle help --scanand view the deprecations view of the generated Build Scan.
This lets you see any deprecation warnings that apply to your build.
Alternatively, you can run
gradle help --warning-mode=allto see the deprecations in the console, though it may not report as much detailed information. -
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.
-
Run
gradle :wrapper --gradle-version 9.7.0to update the project to 9.7.0. -
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.
See Excluding files and directories for details.
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 through9.5.0. -
Support for Java
26has 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:
// 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.
-
The configuration
Actionis now invoked even when the action declaresNoneas its parameters type. Calls likeWorkQueue.submit(MyAction.class, p → …),FlowActionSpec.parameters { … },ValueSourceSpec.parameters { … },BuildServiceSpec.parameters { … }andTransformSpec.parameters { … }now always run the configuration block. When the parameters type isNone, the block receives theNonesingleton 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 forNonewill now run. -
getParameters()returns theNonesingleton instead ofnullfor parameterless actions. Inside aWorkAction,TransformAction,BuildService,ValueSourceorFlowActionthat declaresNoneas its parameters type,getParameters()now returns theNonesingleton. Previously it could returnnull. 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@Unmanagedannotations. -
The component-model base API in
org.gradle.platform.baseandorg.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, andnative-component, along with their source-set, binary-spec, build-type, flavor, and prebuilt-library types inorg.gradle.nativeplatformandorg.gradle.language.*. -
The native test plugins
cunit,cunit-test-suite,google-test, andgoogle-test-test-suite, along with the rule-basedtest-suite-baseinfrastructure inorg.gradle.testing.base. -
The
componentReportanddependentComponentsdiagnostic tasks.
Replace software-model-based builds with the current component model:
-
For C and C++, use the
cpp-applicationandcpp-libraryplugins. -
For Swift, use the
swift-applicationandswift-libraryplugins. -
For native testing, use the
cpp-unit-testandxctestplugins.
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 |
|---|---|
|
|
|
|
|
|
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.
See Custom Collection and Map Types for details.
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.
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:
ext.foo = "hello"
println(foo) // Resolved through hierarchy — now deprecated
extra["foo"] = "hello"
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
fooorbar()in Groovy DSL -
Kotlin DSL property delegates such as
val foo: String by project -
Explicit API calls on the
Projectinstance 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.propertiesat the root, and read them in subprojects viaproviders.gradleProperty("name"). They become typedProvider<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(orparent.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:
tailrec fun Project.findExtraInHierarchy(name: String): Any? {
if (extra.has(name)) return extra[name]
val ancestor = parent ?: return null
return ancestor.findExtraInHierarchy(name)
}
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)
}
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()— callinggetProperties()or accessing thepropertiesproperty on aProjectinstance. -
Calling
getProperties()(or accessing thepropertiesproperty) 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:
|
Wire the provider as a task property for better Configuration Cache support:
If you need the value immediately:
|
Accessing a property with a default value (Kotlin, Java, or Groovy):
Accessing a property with a default value (Groovy, idiomatic):
|
Use
|
Kotlin property delegation:
|
Get the project property directly:
|
Forwarding properties as system properties:
|
Use
|
Token replacement in resource files (common in Minecraft mods, Spring Boot, etc.):
|
Build an explicit map of only the properties you need:
|
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, If you write |
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:
def someProject = project(":some-project")
dependencies {
implementation(someProject) // Deprecated
}
Instead, use the project() method on DependencyHandler or the createProjectDependency() method on DependencyFactory:
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:
repositories {
flatDir {
name = "libs"
dirs "libs1", "libs2"
}
}
repositories {
flatDir {
name = "libs"
dirs("libs1", "libs2")
}
}
For mavenCentral:
repositories {
mavenCentral {
name = "nonDefaultName"
}
}
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,.iwsfiles) -
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
TargetJdkenum and itstoVersion(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.
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
OSvariable check has been removed, since Gradle no longer supports non-NT-based versions of Windows. -
endlocalis 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 callinggradlew.bat. -
& CALLis 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 /bhas 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:
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:
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) |
|
|
2 |
|
|
3 (Lowest) |
|
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 withAbstractTestTask.addTestOutputListener(TestOutputListener) -
AbstractTestTask.beforeTest(Closure)can be replaced withAbstractTestTask.addTestListener(TestListener) -
AbstractTestTask.afterTest(Closure)can be replaced withAbstractTestTask.addTestListener(TestListener) -
AbstractTestTask.beforeSuite(Closure)can be replaced withAbstractTestTask.addTestListener(TestListener) -
AbstractTestTask.beforeSuite(Closure)can be replaced withAbstractTestTask.addTestListener(TestListener) -
Test.testFramework(Closure)can be replaced withTest.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 falsestatement. -
If you do NOT want to use the plugin: Delete the line entirely.
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:
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:
// DON'T do this
project.rootProject === project.parent
Avoid referential equality in Groovy:
// 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 > okmay now be reported simply as:ok. -
For TestNG versions from 6.9.10 up to (but not including) 6.9.13.3, the
IClassListenerAPI 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 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:
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"])
}
}
}
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:
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 |
Replaced |
Replaced |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
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:
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:
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,
ValidatePluginswill use it. -
Otherwise, it will fall back to the Java version used to run Gradle.
If you explicitly set a toolchain like this:
tasks.withType<ValidatePlugins>().configureEach {
javaLauncher.set(
project.javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(17))
}
)
}
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:
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"))
}
}
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:
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")
}
}
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:
val group = "org"
val artifactId = "foo"
val version = "1.0"
configurations.dependencyScope("implementation") {
dependencies.add(project.dependencyFactory.create(group, artifactId, version))
}
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
CommandLineArgumentProviderto add arguments viaJavaForkOptions.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.minHeapSizeandJavaForkOptions.maxHeapSize -
All other JVM arguments configured via
JavaForkOptions.jvmArgs -
The assertion and debug flags configured via
JavaForkOptions.enableAssertionsandJavaForkOptions.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:
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
}
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:
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:
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:
val myIntProp = 42
tasks.register<GradleBuild>("nestedBuild") {
startParameter.projectProperties.put("myIntProp", "$myIntProp") // Convert int to String
}
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:
-
Try running
gradle help --scanand view the deprecations view of the generated Build Scan.
This lets you see any deprecation warnings that apply to your build.
Alternatively, you can run
gradle help --warning-mode=allto see the deprecations in the console, though it may not report as much detailed information. -
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.
-
Run
gradle :wrapper --gradle-version 9.0.0to update the project to 9.0.0. -
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:
-
Delegate’s
invokeMethod, which chooses known methods and does no dynamic lookup. -
Owner’s
invokeMethod, which chooses known methods and does no dynamic lookup. -
Delegate’s
invokeMissingMethod, which does dynamic lookup including via properties. -
Owner’s
invokeMissingMethod, which does dynamic lookup including via properties.
In Groovy 4, the lookup order is:
-
Delegate’s
invokeMethod, which chooses known methods and does no dynamic lookup. -
Delegate’s
invokeMissingMethod, which does dynamic lookup including via properties. -
Owner’s
invokeMethod, which chooses known methods and does no dynamic lookup. -
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.
This is tracked by https://github.com/gradle/gradle/issues/32476.
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:
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:
plugins {
id("cpp-library")
}
model {
toolChains {
clang(Clang) {
eachPlatform {
cCompiler.executable = "clang"
cppCompiler.executable = "clang++"
}
}
}
}
This should become:
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.
See Including projects without an existing directory for more details.
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:
plugins {
id("jvm-toolchains")
}
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:
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()
}
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:
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.
plugins {
id("java-library")
}
// configure test dependencies
// ...
tasks.register<Test>("otherTest")
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:
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 })
}
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:
testing {
suites {
named<JvmTestSuite>("test") {
targets {
register("otherTest")
}
}
}
}
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:
tasks.withType<AbstractTestTask>().configureEach {
failOnNoDiscoveredTests = false
}
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 isfun <T> Provider<T>.some()you must qualify<T>as<T : Any>becauseTisn’t nullable onProvider<T>. -
The nullability of generic bounds is now handled strictly.
For example, you can’t use
Property<String?>anymore because theTinProperty<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
earplugin would skip buildingwarandjarartifacts. -
Applying the
warplugin would skip building thejar.
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
earplugin excludedjarandwarartifacts from archives. -
Applying the
warplugin excluded thejarartifact 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:
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)
}
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:
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)
}
}
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:
-
groovy: GroovySourceDirectorySet -
scala: ScalaSourceDirectorySet
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 |
|---|---|---|---|
|
|
|
Configure the |
|
|
|
Replaced by |
|
|
|
Configure the report task ( |
|
|
|
Configure the |
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,ClosureBackedActionThese classes used to provide utilities related to
groovy.lang.Closure. Plugins should avoid relying on Groovy specifics, such asClosure, 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 anActionas 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:
-
Try running
gradle help --scanand view the deprecations view of the generated Build Scan.
This lets you see any deprecation warnings that apply to your build.
Alternatively, you can run
gradle help --warning-mode=allto see the deprecations in the console, though it may not report as much detailed information. -
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.
-
Run
gradle :wrapper --gradle-version 8.14.5to update the project to 8.14.5. -
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:
-
Introduce a new method that starts with
getinstead ofiswhich 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.
-
-
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:
-
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
-
Request additional attributes to disambiguate which chain is selected (if they result in non-identical final attributes)
-
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.
-
isVerbose() is replaced by getOptions().isVerbose()
-
Calling setVerbose(boolean) with
trueis replaced by getOptions().verbose() -
Calling
setVerbose(false)did nothing.
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:
-
Using Daemon JVM criteria (an incubating feature)
-
Setting the
org.gradle.java.homeGradle property -
Using the ConfigurableLauncher#setJavaHome method on the Tooling API
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:
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
addLateroraddAllLateron 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
beforeResolvehook; 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:
On Configuration:
-
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:
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"))
}
}
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.DISABLEDorCleanup.ALWAYSwill 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)anddescription(String)methods have been replaced with theid(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.
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:
| Original Input | New Input | Reasoning |
|---|---|---|
|
|
The |
|
|
Without a scheme, the path is taken as-is, without decoding. |
|
Spaces are not valid in URLs. |
|
|
||
|
|
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:
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:
plugins {
id("java-library")
}
java {
registerFeature("feature") {
usingSourceSet(sourceSets["main"])
}
}
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:
plugins {
id("java-library")
}
sourceSets {
create("feature")
}
java {
registerFeature("feature") {
usingSourceSet(sourceSets["feature"])
}
}
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:
dependencies {
implementation("org:foo:1.0") {
artifact {
name = "notfoo"
}
}
}
dependencies {
implementation("org:foo:1.0") {
artifact {
name = "notfoo"
}
}
}
Instead, this dependency should be declared as:
dependencies {
implementation("org:notfoo:1.0")
}
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:
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:
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:
include("project-without-directory")
project(":project-without-directory").projectDir.mkdirs()
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.
plugins {
id("ear")
}
ear {
deploymentDescriptor {
version = "1.3"
withXml {
asElement()
}
}
}
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:
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:
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)
}
}
}
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:
// Returns a java.io.File
file("$buildDir/myOutput.txt")
// Returns a java.io.File
file("$buildDir/myOutput.txt")
Should be replaced by:
// 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 }
// 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:
// Returns a java.io.File
file("$buildDir/outputLocation")
// Returns a java.io.File
file("$buildDir/outputLocation")
Should be replaced by:
// 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 }
// 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:
dependencies {
implementation(module("org:foo:1.0") {
dependency("org:bar:1.0")
module("org:baz:1.0") {
dependency("com:example:1.0")
}
})
}
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:
@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)
}
}
}
}
}
}
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")
}
@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)
}
}
}
}
}
}
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:
plugins {
base
}
base {
archivesName.set("gradle")
distsDirectory.set(layout.buildDirectory.dir("custom-dist"))
libsDirectory.set(layout.buildDirectory.dir("custom-libs"))
}
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:
plugins {
application
}
applicationDefaultJvmArgs = listOf("-Dgreeting.language=en") // Accessing a convention
plugins {
id 'application'
}
applicationDefaultJvmArgs = ['-Dgreeting.language=en'] // Accessing a convention
This should be changed to use the application { } configuration block, backed by JavaApplication, instead:
plugins {
application
}
application {
applicationDefaultJvmArgs = listOf("-Dgreeting.language=en")
}
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:
plugins {
id("java")
}
configure<JavaPluginConvention> { // Accessing a convention
sourceCompatibility = JavaVersion.VERSION_18
}
plugins {
id 'java'
}
sourceCompatibility = 18 // Accessing a convention
This should be changed to use the java { } configuration block, backed by JavaPluginExtension, instead:
plugins {
id("java")
}
java {
sourceCompatibility = JavaVersion.VERSION_18
}
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:
plugins {
id("war")
}
configure<WarPluginConvention> { // Accessing a convention
webAppDirName = "src/main/webapp"
}
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.
plugins {
id("war")
}
tasks.war {
webAppDirectory.set(file("src/main/webapp"))
}
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:
plugins {
id("ear")
}
configure<EarPluginConvention> { // Accessing a convention
appDirName = "src/main/app"
}
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.
plugins {
id("ear")
}
tasks.ear {
appDirectory.set(file("src/main/app"))
}
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:
plugins {
`project-report`
}
configure<ProjectReportsPluginConvention> {
projectReportDirName = "custom" // Accessing a convention
}
plugins {
id 'project-report'
}
projectReportDirName = "custom" // Accessing a convention
Configure your report task instead:
plugins {
`project-report`
}
tasks.withType<HtmlDependencyReportTask>() {
projectReportDirectory.set(project.layout.buildDirectory.dir("reports/custom"))
}
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.
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.
In the absence of test suites, dependencies must be manually declared on the test runtime classpath:
-
If using JUnit 5, an explicit
runtimeOnlydependency onjunit-platform-launcheris required in addition to the existingimplementationdependency on the test engine. -
If using JUnit 4, only the existing
implementationdependency onjunit4 is required. -
If using JUnit 3, a test
runtimeOnlydependency onjunit4 is required in addition to acompileOnlydependency onjunit3.
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")
}
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:
beforeSettings {
caches {
// Disable cache marking for all caches
markingStrategy.set(MarkingStrategy.NONE)
}
}
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 |
|---|---|
|
|
|
|
|
|
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.
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:
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.
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 })
}
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:
-
Maven
-
Ivy
Previously, the following code did not generate warnings, but it created inconsistencies between published artifacts:
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() }
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:
if (GradleVersion.current() >= GradleVersion.version("8.0")) {
apply(from = "gradle8/cache-settings.gradle.kts")
}
if (GradleVersion.current() >= GradleVersion.version('8.0')) {
apply from: "gradle8/cache-settings.gradle"
}
beforeSettings {
caches {
cleanup.set(Cleanup.DEFAULT)
}
}
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:
-
Try running
gradle help --scanand view the deprecations view of the generated 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=allto see the deprecations in the console, though it may not report as much detailed information. -
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.
-
Run
gradle :wrapper --gradle-version 8.0.2to update the project to 8.0.2. -
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:
-
set the test framework before configuring options
-
migrate to the JVM Test Suite Plugin
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:
-
An input file collection that can’t be resolved.
-
An input or output file or directory that cannot be read. See Declaring input or output directories which contain unreadable content.
-
Using a
java.io.Fileas the@InputArtifactof an artifact transform. -
Using an input with an unknown implementation. See Cannot use an input with an unknown implementation.
-
Missing dependencies between tasks. See Implicit dependencies between tasks.
-
Converting files to a classpath where paths contain file separator.
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 |
|
|
|
|
|
|
|
|
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
includeBuild("nested")
includeBuild("nestedNested")
.
├── settings.gradle
└── nested
├── settings.gradle
└── nestedNested
└── settings.gradle
includeBuild("nested")
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:
-
Kotlin 1.5 language / standard library
-
Kotlin 1.6 language / standard library
-
Kotlin 1.7 language / standard library
-
Kotlin 1.8 language / standard library
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
antlrconfiguration created by theAntlrPlugin -
The
zincconfiguration created by theScalaBasePlugin -
The
providedCompileandprovidedRuntimeconfigurations created by theWarPlugin
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.
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
Mapas a dependency declaration for Kotlin or Java. -
You cannot use a bundle as a dependency declaration directly (
implementation(libs.bundles.testing)). Useimplementation.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
-
Checkstyle has been updated to Checkstyle 8.45.1.
-
JaCoCo has been updated to 0.8.8.
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:
-
dockerStopis a finalizer ofdockerTestso it must be run afterdockerTest; -
removeContaineris a dependency ofdockerStopso it must be run beforedockerStop; -
createContainermust run afterremoveContainer;
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
Updates to bundled Gradle dependencies
-
Groovy has been updated to Groovy 3.0.11.
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
Potential breaking changes
Updates to default tool integration versions
-
PMD has been updated to PMD 6.39.0.
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.
ConfigurableReport#setDestination(org.gradle.api.provider.Provider<java.io.File>) has been deprecated
ConfigurableReport#setDestination(org.gradle.api.provider.Provider<java.io.File>) is now deprecated and scheduled for removal in Gradle 8.0.
Use Report#getOutputLocation().set(…) instead.
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:
-
Interface TaskExecutionListener
-
Interface TaskActionListener
-
Registering TaskExecutionListener, TaskActionListener, TestListener, TestOutputListener via Gradle.addListener()
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:
-
Method Gradle.buildFinished()
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
-
Kotlin has been updated to Kotlin 1.5.31.
-
Groovy has been updated to Groovy 3.0.9.
-
Ant has been updated to Ant 1.10.11 to fix CVE-2021-36373 and CVE-2021-36374.
-
Commons compress has been updated to Commons-compress 1.21 to fix CVE-2021-35515, CVE-2021-35516, CVE-2021-35517 and CVE-2021-36090.
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
-
Groovy has been updated to Groovy 3.0.8.
-
Kotlin has been updated to Kotlin 1.5.21.
Updates to default tool integration versions
-
PMD has been updated to PMD 6.36.0.
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
Updates to default tool integration versions
-
JaCoCo has been updated to 0.8.7.
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.
-
GradleVersion -
Path -
Configurable
-
VersionNumber -
TextUtil -
WrapUtil -
RelativePathUtil -
DistributionLocator -
SingleMessageLogger -
ConfigureUtil
ConfigureUtil is being removed without a replacement.
Plugins can avoid the need for using ConfigureUtil.
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
-
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:
-
groovy: GroovySourceDirectorySet -
antlr: AntlrSourceDirectorySet -
scala: ScalaSourceDirectorySet
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-filefor specifying a custom settings file location -
-b,--build-filefor 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").
Deprecated properties in compile task
-
The JavaCompile.destinationDir property has been deprecated. Use the JavaCompile.destinationDirectory property instead.
-
The GroovyCompile.destinationDir property has been deprecated. Use the GroovyCompile.destinationDirectory property instead.
-
The ScalaCompile.destinationDir property has been deprecated. Use the ScalaCompile.destinationDirectory property instead.
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:
-
org.gradle.api.internal.HasConvention(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:
-
Migrate plugin configuration: gradle/gradle#16900.
-
Migrate custom source sets: gradle/gradle#17149.
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
-
groovy: GroovySourceDirectorySet -
antlr: AntlrSourceDirectorySet -
scala: ScalaSourceDirectorySet
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:
| 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.
| 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:
| OS | Architecture |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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.
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.
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 ( |
Plugin |
Used to extend Gradle’s capabilities (like the |
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)
-
Gradle directory to store wrapper files and more
-
Gradle wrapper scripts - THIS IS A GRADLE PROJECT!
-
Gradle settings file to define a root project name and subprojects
-
Gradle build scripts of the two subprojects -
subproject-aandsubproject-b -
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.
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
Next Step: Learn about the Gradle Wrapper >>
Wrapper Basics
The recommended way to execute any Gradle build is with the Gradle Wrapper.
The wrapper script invokes a declared version of Gradle, downloading it beforehand if necessary.
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:
-
Automatically downloads and uses a specific Gradle version.
-
Standardizes a project on a given Gradle version.
-
Provisions the same Gradle version for different users and environments (IDEs, CI servers…).
-
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:
-
Using a system-installed Gradle distribution — by running the
gradlecommand. -
Using the Gradle Wrapper — by running the
gradleworgradlew.batscript 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.
-
Using a system-installed Gradle distribution:
$ gradle build -
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)
-
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. -
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). -
gradlew: This is a shell script (Unix-based systems) that acts as a wrapper aroundgradle-wrapper.jar. It is used to execute Gradle tasks on Unix-based systems without needing to manually install Gradle. -
gradlew.bat: This is a batch script (Windows) that serves the same purpose asgradlewbut 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. |
Next Step: Learn about the Gradle CLI >>
Command-Line Interface Basics
The command-line interface is the primary method of interacting with Gradle outside the IDE.
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
Next Step: Learn about the Settings file >>
Settings File Basics
The settings file (settings.gradle(.kts)) is the entry point of every Gradle project.
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:
rootProject.name = "root-project" // (1)
include("sub-project-a") // (2)
include("sub-project-b")
include("sub-project-c")
-
Define the project name.
-
Add subprojects.
rootProject.name = 'root-project' // (1)
include('sub-project-a') // (2)
include('sub-project-b')
include('sub-project-c')
-
Define the project name.
-
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)
-
The
settings.gradle(.kts)file. -
The three subprojects, each with their own
build.gradle(.kts)file.
Next Step: Learn about the Build scripts >>
Build File Basics
Generally, a build script (build.gradle(.kts)) details build configuration, tasks, and plugins.
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:
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"
}
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'
}
-
Add plugins.
-
Add dependencies.
-
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.
plugins { // (1)
// Apply the application plugin to add support for building a CLI application in Java.
application
}
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:
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)
}
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.
application { // (3)
// Define the main class for the application.
mainClass = "org.example.App"
}
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)
Next Step: Learn about Dependency Management >>
Dependencies and Dependency Management Basics
Gradle has built-in support for dependency management.
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:
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)
}
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)
}
-
Applies the Java Library plugin.
-
Adds a dependency on Google’s Guava library used in production code.
-
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:
-
implementationis used for dependencies needed to compile and run your production code. -
apiis 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:
-
[versions]to declare the version numbers that plugins and libraries will reference. -
[libraries]to define the libraries used in the build files. -
[bundles]to define a set of dependencies. -
[plugins]to define plugins.
Here’s an example:
[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:
dependencies {
implementation(libs.guava)
api(libs.juneau.marshall)
}
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.
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.
Next Step: Learn about Incremental Builds and Build Caching >>
Incremental Builds and Build Caching Basic
Gradle uses two main features to reduce build time: incremental builds and build caching.
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 |
|---|---|
|
The task’s inputs and outputs haven’t changed since the last run, so it was skipped. |
|
The task was skipped and its outputs were restored from the local or remote build cache. |
|
The task had no source files to process (e.g., no Java files to compile), so it was 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. |
|
|
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.
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
compileJavaortest) -
Add new configurations (like
implementationorruntimeOnly) -
Contribute DSL elements (like
application {}orpublishing {})
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, andjar. - Google Services Plugin (
com.google.gms.google-services) -
Configures Firebase and Google APIs in Android builds. Adds DSL like
googleServices {}and tasks likegenerateReleaseAssets. - 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-libraryplugin, which adds tasks for compiling Java, running tests, and packaging libraries. -
Apply the community-maintained
spotlessplugin (version6.25.0), which adds code formatting tasks and integrates tools likektlint,prettier, andgoogle-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.
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.
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.
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.
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)
-
Root Directory - Gradle Project
-
Build Script - Project-level configuration
-
Settings File - Project inclusion and build identity
-
Gradle Files - Wrapper executables and version pinning
-
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
-
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.
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)
└── ⋮
-
Root Project
-
Project-specific cache directory generated by Gradle.
-
Version-specific caches (e.g., to support incremental builds).
-
The build directory of this project.
-
Contains the JAR file and configuration of the Gradle Wrapper.
-
Project-specific Gradle configuration properties.
-
Scripts for executing builds using the Gradle Wrapper.
-
The project’s settings file where the list of subprojects is defined.
-
Usually, a project is organized into one or multiple subprojects.
-
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)
-
Gradle User Home
-
Global cache directory (for everything that is not project-specific).
-
Version-specific caches (e.g., to support incremental builds).
-
Shared caches (e.g., for artifacts of dependencies).
-
Registry and logs of the Gradle Daemon.
-
Global initialization scripts.
-
JDKs downloaded by the toolchain support.
-
Distributions downloaded by the Gradle Wrapper.
-
Global Gradle configuration properties.
Next Step: Learn how to structure Multi-Project Builds >>
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.
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:
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)
-
The
settings.gradle(.kts)file should include all subprojects. -
Each subproject should have its own
build.gradle(.kts)file.
In this example, the root settings file will look as follows:
include("sub-project-1", "sub-project-2", "sub-project-3")
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:
-
Multi-Project Builds using
buildSrc- wherebuildSrcis a subproject-like directory at the Gradle project root containing shared build logic. -
Composite Builds including
build-logic- a build that includes other builds wherebuild-logicis a build directory at the Gradle project root containing reusable build logic.
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)
-
Gradle recognized
buildSrcfolder -
Contains common build logic from
sub-project-1,sub-project-2andsub-project-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)
-
Separate Gradle build called
build-logic -
Contains common build logic from
sub-project-1,sub-project-2andsub-project-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:
include("sub-project-1", "sub-project-2", "sub-project-3")
includeBuild("build-logic")
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)
-
Separate Gradle build called
build-logic -
Separate Gradle build called
project-1with 2 of its own subproject -
Separate Gradle build called
project-2with 1 of its own subproject -
Separate Gradle build called
project-3with 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.
Next Step: Learn about the Gradle Build Lifecycle >>
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.
Build Phases
A Gradle build has three distinct phases.
Gradle runs these phases in order:
| Phase 1. Initialization | Phase 2. Configuration | Phase 3. Execution |
|---|---|---|
- Detects the settings file |
- Evaluates the build files of every project participating in the build |
- Schedules and executes the selected tasks |
The following example shows which parts of settings and build files correspond to various build phases:
rootProject.name = "basic"
println("This is executed during the initialization phase.")
// 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.")
}
}
rootProject.name = 'basic'
println 'This is executed during the initialization phase.'
// 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: buildHtml → assembleDocs → createDocs.
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:
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).
Next Step: Learn how to write Build Scripts >>
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.
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.
|
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:
-
Statements: Top-level expressions that execute immediately during the initialization (for settings scripts) or configuration (for build scripts) phase.
-
Blocks: Nested sections (Groovy closures or Kotlin lambdas) passed to configuration methods. These blocks apply settings to Gradle objects like
project,pluginManagement,dependencyResolutionManagement,repositories, ordependencies.
Examples of common blocks include:
plugins {
id("java")
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
implementation(project(":shared"))
}
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
thisobject 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:
-
Local Variables
-
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.
val dest = "dest"
tasks.register<Copy>("copy") {
from("source")
into(dest)
}
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:
-
extraproperty in Kotlin.
-
extproperty in Groovy.
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) }
}
}
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:
-
Code is evaluated immediately in order.
-
Statements outside of configuration blocks execute eagerly.
-
Properties and logic should be deferred using
Provideror 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:
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"))
}
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')
}
-
Apply plugins to the build.
-
Define the locations where dependencies can be found.
-
Add dependencies.
-
Set properties.
-
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:
plugins { // (1)
id("application")
}
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:
repositories { // (2)
mavenCentral()
}
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.
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")
}
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:
application { // (4)
mainClass = "com.example.Main"
}
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:
tasks.register<Zip>("zip-reports") {
from("Reports/")
include("*")
archiveFileName.set("Reports.zip")
destinationDirectory.set(file("/dir"))
}
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:
tasks.named<Test>("test") { // (5)
useJUnitPlatform()
}
tasks.named('test', Test) { // (5)
useJUnitPlatform()
}
The example below configures the Javadoc task to automatically generate HTML documentation from Java code:
tasks.named<Javadoc>("javadoc").configure {
exclude("app/Internal*.java")
exclude("app/internal/*")
}
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:
println(name)
println(project.name)
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
nameto theProjectobject. -
In Kotlin, the build script is compiled as an extension of the
Projecttype, 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
nameare dynamically delegated to theSettingsobject. -
In Kotlin, the script is compiled as an extension of the
Settingsclass, sonameandpluginManagement {}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().
Next Step: Learn about Gradle Managed Types >>
Gradle Managed Types
Gradle Managed Types are the building blocks for writing modern, efficient, and cache-friendly build logic.
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>:
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
PropertyorProvider) 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 |
|---|---|---|
|
Lazy scalar value (e.g., a version number) |
A version string |
|
Lazy list of values (e.g., compiler args) |
A list of compiler args |
|
Lazy map of values (e.g., environment vars) |
A map of Maven Pom properties |
|
Lazy reference to a single file (input or output) |
A single file input/output |
|
Lazy reference to a directory (input or output) |
A destination directory |
|
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.
val version = project.version.toString() // evaluated now
val file = File("build/output.txt") // evaluated now
myTask.outputFile.set(file)
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.
val outputFile: RegularFileProperty = project.objects.fileProperty()
outputFile.set(layout.buildDirectory.file("output.txt")) // evaluated later
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:
// Not Configuration Cache compatible
tasks.register("printVersion") {
doLast {
val eager_version = project.version.toString()
println("Version is $eager_version")
}
}
// 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>:
// 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()}")
}
}
// 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()}"
}
}
Next Step: Learn about Declaring and Managing Dependencies >>
Declaring and Managing Dependencies
Gradle provides a rich and flexible model for declaring dependencies, managing versions, and resolving conflicts across builds.
Declare Dependencies
The dependencies{} block is where you declare the external libraries, internal modules, or files your project needs to compile, run, or test:
dependencies {
implementation("com.google.guava:guava:30.0-jre")
runtimeOnly("org.apache.commons:commons-lang3:3.14.0")
}
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:
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")
}
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:
[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:
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)
}
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:
dependencies {
implementation("org.apache.httpcomponents:httpclient:4.5.4")
implementation("commons-codec:commons-codec") {
version {
strictly("1.9")
}
}
}
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:
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")
}
}
}
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:
dependencies {
implementation("jaxen:jaxen:1.1.6") // Transitive dependency that brings XPath functionality
implementation("org.jdom:jdom2:2.0.6") // Also offers XPath functionality
}
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:
dependencies {
implementation("jaxen:jaxen:1.1.6") {
capabilities {
requireCapability("xml:xpath-support")
}
}
implementation("org.jdom:jdom2:2.0.6")
}
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:
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")
}
}
}
}
}
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.
Next Step: Learn about Creating and Registering Tasks >>
Creating and Registering Tasks
The work that Gradle can do on a project is defined by one or more tasks.
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:
tasks.register("hello") {
doLast {
println("Hello world!")
}
}
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:
tasks.register<Copy>("copyTask") {
from("source")
into("target")
include("*.war")
}
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.
tasks.register<Delete>("removeOutput") {
delete(layout.buildDirectory.file("outputs/1.txt"))
}
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):
// 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."
}
// 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:
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())
}
}
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:
abstract class GreetingTask : DefaultTask() {
@TaskAction
fun greet() {
println("hello from GreetingTask")
}
}
// Create a task using the task type
tasks.register<GreetingTask>("hello")
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 {}:
tasks.register("hello") {
doLast {
println("Hello world!")
}
}
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.
-
The group acts as a category for the task. When you run
./gradlew tasks, Gradle clusters all tasks with the same group name together. -
The description is a short summary explaining what the task actually does.
Task Dependencies
You can declare tasks that depend on other tasks:
tasks.register("hello") {
doLast {
println("Hello world!")
}
}
tasks.register("intro") {
dependsOn("hello")
doLast {
println("I'm 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:
tasks.register("taskX") {
dependsOn("taskY")
doLast {
println("taskX")
}
}
tasks.register("taskY") {
doLast {
println("taskY")
}
}
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:
tasks.register("hello") {
group = "Custom"
description = "A lovely greeting task."
doLast {
println("Hello world!")
}
dependsOn(tasks.assemble)
}
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:
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")
}
}
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:
-
Actionable tasks have some action(s) attached to do work in your build:
compileJava. -
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
groupordescription). -
Execution Block (
doLastor@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.
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:
-
Core plugins - Gradle develops and maintains a set of Core Plugins.
-
Community plugins - Gradle plugins shared in a remote repository such as Maven or the Gradle Plugin Portal.
-
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:
plugins {
id("java")
}
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:
plugins {
id("com.gradleup.shadow") version "8.3.4"
}
plugins {
id 'com.gradleup.shadow' version '8.3.4'
}
And the location of the plugin must be specified in the settings file as needed:
pluginManagement { // (1)
repositories {
gradlePluginPortal()
}
}
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: |
|---|---|---|---|
Apply a plugin to a project. |
|
||
Apply a plugin to multiple projects. |
The |
|
|
Apply a plugin to multiple projects. |
|
||
Apply a plugin to multiple projects. |
A convention plugin in the |
|
|
Apply a plugin needed for the build script itself. |
|
||
Apply a script plugins. Applying a plugin when type-safe accessors are not available. |
The legacy |
|
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:
plugins {
java
}
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:
plugins {
id("org.springframework.boot") version "3.3.1"
}
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.
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)
}
-
for core Gradle plugins or plugins already available to the build script
-
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:
include("hello-a")
include("hello-b")
include("goodbye-c")
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")
}
plugins {
id("com.example.hello")
}
plugins {
id("com.example.hello")
}
plugins {
id("com.example.goodbye")
}
include 'hello-a'
include 'hello-b'
include 'goodbye-c'
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')
}
plugins {
id 'com.example.hello'
}
plugins {
id 'com.example.hello'
}
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:
rootProject.name = "multi-project-build"
include("domain", "infra", "http")
plugins {
id("com.gradleup.shadow") version "8.3.4" apply false
id("io.ratpack.ratpack-java") version "2.0.0-rc-1" apply false
}
plugins {
`java-library`
}
dependencies {
api("javax.measure:unit-api:1.0")
implementation("tec.units:unit-ri:1.0.3")
}
plugins {
`java-library`
id("com.gradleup.shadow")
}
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":
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("myPlugins") {
id = "my-plugin"
implementationClass = "my.MyPlugin"
}
}
}
plugins {
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
myPlugins {
id = 'my-plugin'
implementationClass = 'my.MyPlugin'
}
}
}
The plugin can then be applied by ID:
plugins {
id("my-plugin")
}
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:
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)
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:
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
println("Plugin ${this.javaClass.simpleName} applied on ${project.name}")
}
}
apply<MyPlugin>()
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:
pluginManagement {
plugins {
}
resolutionStrategy {
}
repositories {
}
}
rootProject.name = "plugin-management"
pluginManagement {
plugins {
}
resolutionStrategy {
}
repositories {
}
}
rootProject.name = 'plugin-management'
The block can also be used in Initialization Script:
settingsEvaluated {
pluginManagement {
plugins {
}
resolutionStrategy {
}
repositories {
}
}
}
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{}:
pluginManagement {
repositories {
maven(url = file("./maven-repo"))
gradlePluginPortal()
ivy(url = file("./ivy-repo"))
}
}
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:
pluginManagement {
val helloPluginVersion = providers.gradleProperty("helloPluginVersion").get()
plugins {
id("com.example.hello") version "${helloPluginVersion}"
}
}
plugins {
id("com.example.hello")
}
helloPluginVersion=1.0.0
pluginManagement {
plugins {
id 'com.example.hello' version "${helloPluginVersion}"
}
}
plugins {
id 'com.example.hello'
}
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:
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")
}
}
}
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.
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"))
}
}
}
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):
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.
apply(plugin = "java")
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:
apply<JavaPlugin>()
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:
apply(from = "other.gradle.kts")
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:
[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:
plugins {
`java-library`
alias(libs.plugins.versions)
}
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:
plugins {
id("org.springframework.boot") version "3.3.1"
}
plugins {
id 'org.springframework.boot' version '3.3.1'
}
When you apply a plugin, it typically does one or both of the following:
-
Adds tasks to your build - Plugins often expose tasks that perform a specific action.
For example, the Maven Publish Plugin adds a
publishToMavenLocaltask, which uploads artifacts to a local Maven repository. -
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:
tasks.withType<Test>().configureEach {
useJUnitPlatform {
includeEngines("junit-vintage")
// excludeEngines("junit-jupiter")
}
}
tasks.withType(Test).configureEach {
useJUnitPlatform {
includeEngines 'junit-vintage'
// excludeEngines 'junit-jupiter'
}
}
And Java options using the java {} block:
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
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.
1. Init Plugins
-
Applied globally to all builds. Loaded from
~/.gradle/init.gradle[.kts]or passed via the--init-scriptcommand-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.
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 |
|
Global |
|
Settings Plugin |
|
Build Layout |
|
Project Plugin |
|
Per-Project |
|
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.
1. Precompiled Script Plugins
-
Written as
.gradle.ktsor.gradlefiles. -
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.
Next Step: Learn about Pre-Compiled Script Plugins >>
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:
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")
// ...
}
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:
plugins {
`java-library-convention`
}
plugins {
id 'java-library-convention'
}
Structuring Precompiled Script Plugins
Precompiled script plugins are typically placed in either:
-
A dedicated
buildSrcdirectory in your build, or -
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:
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
}
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:
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()
}
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:
plugins {
id("myproject.java-conventions")
}
dependencies {
testImplementation("junit:junit:4.13")
}
plugins {
id 'myproject.java-conventions'
}
dependencies {
testImplementation 'junit:junit:4.13'
}
Next Step: Learn about Binary Plugins >>
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:
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)
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)
-
Extend the
org.gradle.api.Plugininterface. -
Override the
applymethod.
1. Extend the org.gradle.api.Plugin interface
Create a class that extends the Plugin interface:
abstract class SamplePlugin : Plugin<Project> {
}
class SamplePlugin implements Plugin<Project> {
}
2. Override the apply method
Add tasks and other logic in the apply() method:
override fun apply() {
}
void apply(Project project) {
}
Let’s take a look at a more realistic example next.
Next Step: Learn how to develop Binary Plugins >>
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:
| 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:
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"
}
}
}
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'
}
}
}
-
java-gradle-plugin- Sets up various configurations you need to do plugin development in Java -
junittesting framework - A popular Java testing framework -
Plugin configuration - Sets an id of
org.example.filesizediffwhich we can use to reference the plugin
1. Extension
The extension defines the plugin’s configurable inputs, two file properties in this case:
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:
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:
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:
-
Creates an extension named
filesizediff: This allows users to configure the plugin using adiff {}block in their build script. The configuration values are stored in an instance ofFileSizeDiffExtension. -
Registers a task of type
FileSizeDiffTask: The task is given the namefileSizeDiff, and its properties (file1andfile2) are mapped from the corresponding properties in thediffextension. 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:
plugins {
id("org.example.filesizediff")
}
diff {
file1 = file("a.txt")
file2 = file("b.txt")
}
plugins {
id("org.example.filesizediff")
}
diff {
file1 = file('a.txt')
file2 = file('b.txt')
}
Next Step: Learn how to test Binary Plugins >>
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:
-
Unit Tests
-
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:
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:
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:
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()
}
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:
rootProject.name = "consumer"
includeBuild("plugin")
rootProject.name = 'consumer'
includeBuild("plugin")
Then in the build file of the consumer project, you apply the plugin and test it:
plugins {
id("org.example.filesizediff")
}
diff {
file1 = file("a.txt")
file2 = file("b.txt")
}
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
Next Step: Learn how to publish Binary Plugins >>
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:
group = "org.example" // (3)
version = "1.0.0"
gradlePlugin { // (3)
plugins {
create("filesizediff") {
id = "org.example.filesizediff"
implementationClass = "org.example.FileSizeDiffPlugin"
}
}
}
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:
publishing {
repositories {
maven {
url = uri("${layout.projectDirectory}/publish")
}
}
}
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.
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 |
|---|---|---|
General |
8.14 |
|
General |
8.14 |
|
General |
8.14 |
|
General |
8.14 |
|
General |
9.0.0 |
|
General |
9.2.0 |
|
General |
9.2.0 |
|
General |
9.6.0 |
|
General |
9.7.0 |
|
Structuring Builds |
9.0.0 |
|
Structuring Builds |
9.0.0 |
|
Structuring Builds |
9.0.0 |
|
Structuring Builds |
9.1.0 |
|
Structuring Builds |
9.3.0 |
|
Dependencies |
8.14 |
|
Dependencies |
9.0.0 |
|
Dependencies |
9.0.0 |
|
Dependencies |
9.0.0 |
|
Dependencies |
9.0.0 |
|
Dependencies |
9.0.0 |
|
Dependencies |
9.1.0 |
|
Dependencies |
9.2.0 |
|
Always Declare Attributes on Consumable and Resolvable Configurations |
Dependencies |
9.7.0 |
Task |
8.14 |
|
Favor @CacheableTask and @DisableCachingByDefault over cacheIf(Spec) and doNotCacheIf(String, Spec) |
Task |
8.14 |
Task |
9.0.0 |
|
Task |
9.1.0 |
|
Task |
9.1.0 |
|
Task |
9.1.0 |
|
Prefer |
Task |
9.2.0 |
Task |
9.3.0 |
|
Task |
9.7.0 |
|
Task |
9.7.0 |
|
Task |
9.7.0 |
|
Performance |
9.0.0 |
|
Performance |
9.1.0 |
|
Performance |
9.1.0 |
|
Performance |
9.0.0 |
|
Performance |
9.4.0 |
|
Security |
9.1.0 |
|
Security |
9.3.0 |
|
Security |
9.7.0 |
|
Security |
9.7.0 |
|
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>
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
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)
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)
-
Declare a Repository: To use the legacy plugin application syntax, you need to explicitly tell Gradle where to find a plugin.
-
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.
-
Apply a Core Plugin: This is very similar using either method.
-
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
plugins {
id("java") // (1)
id("com.google.protobuf").version("0.9.4") // (2)
}
plugins {
id("java") // (1)
id("com.google.protobuf").version("0.9.4") // (2)
}
-
Apply a Core Plugin: This is very similar using either method.
-
Apply a Third-Party Plugin: You specify the version using method chaining in the
pluginsblock 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:
plugins {
id("pluginA")
id("pluginB")
}
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:
plugins {
id("pluginA")
id("pluginB")
}
plugins {
id("pluginC")
id("pluginD")
}
plugins {
id("pluginA")
id("pluginB")
}
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:
plugins {
id("my-plugin")
id("plugin-i-depend-on")
}
plugins {
id("my-plugin")
id("plugin-i-depend-on")
}
or
plugins {
id("plugin-i-depend-on")
id("my-plugin")
}
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:
// Ensure required plugin is applied
project.pluginManager.apply("com.example.required-plugin")
// 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 {}:
// 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 -> { } }
}
// 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:
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")
}
}
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:
plugins {
id("myplugin")
}
// Assumes 'java' plugin is present
extensions.getByType<org.gradle.api.plugins.JavaPluginExtension>().apply {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}
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:
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))
}
}
}
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:
pluginManager.withPlugin("java") {
extensions.configure<org.gradle.api.plugins.JavaPluginExtension> {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}
}
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:
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))
}
}
}
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
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")
}
}
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}")
}
}
-
Casting to
AttributeContainerInternaland usingtoMap()should be avoided as it relies on an internal API.
Do This Instead
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")
}
}
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")
}
}
-
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
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")
}
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
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
// Left empty
// 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
rootProject.name = "my-example-project"
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:
> 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
# This file is located in /app
propertyA=fixedValue
propertyB=someValue
// 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")
}
}
// 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"
}
}
# This file is located in /util
propertyA=fixedValue
propertyB=otherValue
// 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")
}
}
// 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"
}
}
-
Register a task that uses the value of properties in each subproject.
-
The task reads properties, which are supplied by the project-local
app/gradle.propertiesfile.propertyAdoes not vary between subprojects. -
'util’s print task reads the properties which are supplied by
util/gradle.properties.propertyBvaries 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
# This file is located in the root of the build
propertyA=fixedValue
propertyB=someValue
import org.gradle.api.provider.Property
interface ProjectProperties { // (1)
val propertyA: Property<String>
val propertyB: Property<String>
}
import org.gradle.api.provider.Property
interface ProjectProperties { // (1)
Property<String> getPropertyA()
Property<String> getPropertyB()
}
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()}")
}
}
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()}")
}
}
// This file is located in /app
plugins { // (4)
id("project-properties")
}
myProperties { // (5)
propertyA = providers.gradleProperty("propertyA")
propertyB = providers.gradleProperty("propertyB")
}
// This file is located in /app
plugins { // (4)
id "project-properties"
}
myProperties { // (5)
propertyA = providers.gradleProperty("propertyA")
propertyB = providers.gradleProperty("propertyB")
}
// This file is located in /util
plugins {
id("project-properties")
}
myProperties {
propertyA = providers.gradleProperty("propertyA")
propertyB = "otherValue" // (6)
}
// This file is located in /util
plugins {
id "project-properties"
}
myProperties {
propertyA = providers.gradleProperty("propertyA")
propertyB = "otherValue" // (6)
}
-
Define a simple extension type in
buildSrcto hold property values. -
Register that property in a convention plugin.
-
Register tasks using property values in the convention plugin.
-
Apply the convention plugin in each subproject.
-
Set the extension’s property values in each subproject’s build script. This uses the values defined in the root
gradle.propertiesfile. The task reads values from the extension, not directly from the project properties. -
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
afterEvaluatecallbacks execute in registration order. If two plugins or scripts both useafterEvaluate, 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
afterEvaluateare 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.
afterEvaluatecallbacks 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:
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)
}
}
}
}
}
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)
}
}
}
}
}
-
The plugin’s
afterEvaluateruns before anyafterEvaluateregistered later in the build script — ordering depends on registration order. -
getOrElsereads the property’s current value immediately. If the value is set in a laterafterEvaluate, this will never see it. -
Registering a task inside
afterEvaluatedefeats task configuration avoidance. -
Checking plugin presence inside
afterEvaluateassumes all plugins have been applied before this callback runs.
The build script applies the plugin and sets the extension value in its own afterEvaluate:
plugins {
id("java-library")
id("app-info-plugin")
}
afterEvaluate {
the<AppInfoExtension>().appName.set("my-app") // (1)
}
plugins {
id 'java-library'
id 'app-info-plugin'
}
afterEvaluate {
appInfo { // (1)
appName.set('my-app')
}
}
-
This
afterEvaluateruns after the plugin’s — by the time it sets the name, the plugin has already captured the default value.
Running printAppInfo outputs unnamed — not 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():
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)
}
}
}
}
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)
}
}
}
}
-
convention()provides a default value that is used only if no explicit value is set viaset(). -
The value is resolved at execution time via
get()— configuration order does not matter. -
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:
plugins {
id("java-library")
id("app-info-plugin")
}
appInfo {
appName.set("my-app") // (1)
}
plugins {
id 'java-library'
id 'app-info-plugin'
}
appInfo {
appName.set('my-app') // (1)
}
-
The value is set during normal configuration. Because the plugin wires the
Propertylazily, 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:
-
Check which Gradle version introduced it.
-
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
├── 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
├── 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
include("app") // (1)
include("app") // (1)
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"
}
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"
}
-
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.
-
As there is only a single project in this build, the
applicationplugin must be applied here. This means that theapplicationplugin will affect all source files in the build, even those which have no need for it. -
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
├── 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
├── 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
include("app") // (1)
include("util")
include("util-commons")
include("util-guava")
include("app") // (1)
include("util")
include("util-commons")
include("util-guava")
// 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"
}
// 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"
}
// 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")
}
// 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"
}
// 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")
}
// 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"
}
-
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.
-
The
applicationplugin is only applied to theappproject, which is the only project that needs it. -
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.
-
Each project only adds the specific plugins it needs.
-
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
├── 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
├── 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
plugins { // (1)
`java-library`
}
plugins {
id 'java-library' // (1)
}
-
The
java-libraryplugin is applied to the root project, as there are Java source files are in the root project.
Do This Instead
├── 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
├── 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
include("core") // (1)
include("core") // (1)
// This is the build.gradle.kts file for the core module
plugins { // (2)
`java-library`
}
// This is the build.gradle file for the core module
plugins { // (2)
id 'java-library'
}
-
The root project exists only to configure the build, informing Gradle of a (sub)project named
core. -
The
java-libraryplugin is only applied to thecoreproject, 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
buildSrccauses 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
buildSrcproject automatically applies thejavaplugin, 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
// This file is located in /buildSrc
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "org.example.myplugin"
implementationClass = "org.example.MyPlugin"
}
}
}
// 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.
rootProject.name = "favor-composite-builds"
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
// This file is located in /build-logic/plugin
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "org.example.myplugin"
implementationClass = "org.example.MyPlugin"
}
}
}
// 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.
// This file is located in the root project
includeBuild("build-logic") // (1)
rootProject.name = "favor-composite-builds"
// This file is located in the root project
includeBuild("build-logic") // (1)
rootProject.name = "favor-composite-builds"
// This file is located in /build-logic
rootProject.name = "build-logic"
include("plugin") // (2)
// This file is located in /build-logic
rootProject.name = "build-logic"
include("plugin") // (2)
-
Composite builds must be explicitly included: Use the
includeBuildmethod to locate and include a build in order to use its products. -
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
-
A project named
applocated at the root of the build -
A directory named
substhat is not intended to represent a Gradle project, but is used to organize the build -
Another organizational directory not intended to represent a Gradle project
-
A Gradle project named
my-web-modulethat should be included in the build
include(":app") // (1)
include(":subs:web:my-web-module") // (2)
include(":app") // (1)
include(":subs:web:my-web-module") // (2)
-
Including the
appproject located at the root of the build requires no additional configuration -
Including a project named
:subs:my-web-modulelocated in a nested subdirectory causes Gradle to create empty projects for each element of the path
> 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
include(":app")
include(":my-web-module")
project(":my-web-module").projectDir = file("subs/web/my-web-module") // (1)
include(":app")
include(":my-web-module")
project(":my-web-module").projectDir = file("subs/web/my-web-module") // (1)
-
After including the
:subs:web:my-web-moduleproject, itsprojectDirproperty is set to the physical location of the project
> 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
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)
}
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)
}
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)
}
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)
}
-
Common compiler settings repeated across multiple projects.
-
Shared test configuration that must be maintained in multiple places.
-
Common dependencies that could be managed centrally.
-
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:
pluginManagement {
includeBuild("build-logic") // (1)
}
plugins {
`kotlin-dsl` // (2)
}
pluginManagement {
includeBuild("build-logic") // (1)
}
plugins {
id("groovy-gradle-plugin") // (2)
}
-
Include the
build-logicbuild, which defines convention plugins. -
Enable the use of Kotlin DSL in
build-logic.
Create convention plugins for each type of project in build-logic:
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"))
}
plugins { // (2)
id("my.base-java-library")
id("my.java-use-junit5")
}
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")
}
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"]
}
plugins { // (2)
id("my.base-java-library")
id("my.java-use-junit5")
}
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")
}
-
Default settings for a Java library plugin.
-
A convention plugin can apply other convention plugins.
-
JUnit 5 configuration moved to a convention plugin.
And apply these plugin in any build files to use the shared logic:
plugins {
id("my.java-library") // (6)
}
plugins {
id("my.java-library") // (6)
}
dependencies {
api("com.google.guava:guava:23.0") // (7)
}
plugins {
id("my.java-library") // (6)
}
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:
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")
}
}
}
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:
[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" }
plugins {
id("java-library")
alias(libs.plugins.versions)
}
dependencies {
api(libs.bundles.groovy)
testImplementation(libs.junit.jupiter)
implementation(libs.commons.lang3)
}
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:
-
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, uselog4j-api -
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, usejackson-databindorjackson-core-databind -
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, usearmeria-grpc -
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, usegoogle-java-format, notgoogle-javaorjava -
Omit redundant segments: If the group and artifact IDs are the same, avoid repeating them.
Example: For
io.ktor:ktor-client-core, usektor-client-core, notktor-ktor-client-core -
Convert internal dashes to camelCase: If the artifact ID contains dashes, convert them to camelCase for better readability in code.
Example:
spring-boot-starter-webbecomesspringBootStarterWeb -
Suffix plugin libraries with
-plugin: When referencing a plugin as a library (not in the[plugins]section), append-pluginto the name.Example: For
org.owasp:dependency-check-gradle, usedependency-check-plugin
Example
[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" }
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)
}
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:
buildscript {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("java")
}
repositories {
mavenCentral()
}
buildscript {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("java")
}
repositories {
mavenCentral()
}
Do This Instead
Instead, you should set them up in settings.gradle.kts like this:
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
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
plugins {
kotlin("jvm").version("2.4.0")
}
dependencies {
api(kotlin("stdlib")) // (1)
}
plugins {
id("org.jetbrains.kotlin.jvm") version "2.4.0"
}
dependencies {
api("org.jetbrains.kotlin:kotlin-stdlib:2.4.0") // (1)
}
-
stdlibis 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
plugins {
kotlin("jvm").version("2.4.0") // (1)
}
plugins {
id("org.jetbrains.kotlin.jvm") version "2.4.0" // (1)
}
-
stdlibdependency 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.,
compileOnlyandimplementation) can result in hard-to-diagnose classpath issues.
Example
Don’t Do This
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)
}
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)
}
-
Redundant dependency in
implementationscope.
Do This Instead
plugins {
`java-library`
}
dependencies {
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") // (1)
}
plugins {
id 'java-library'
}
dependencies {
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") // (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
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)
}
}
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)
}
}
-
Avoid the named argument notation when declaring dependencies
-
Other modifiers methods and constraints like
excludeare not included in this recommendation and can use named argument notation as needed
Do This Instead
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)
}
}
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)
}
}
-
Use the string notation instead when declaring dependencies
-
Other modifiers methods and constraints like
excludeare 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:
-
Performance, since you only query repositories for dependencies that should actually exist within them
-
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
-
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:
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}
Do This Instead
Use content filtering to ensure that the proper repositories are searched first for the expected artifacts:
dependencyResolutionManagement {
repositories {
google {
content {
// Use this repository for androidx and GMS dependencies
includeGroupByRegex("androidx.*")
includeGroup("com.google.gms")
}
}
// Specify the fallback repository last
mavenCentral()
}
}
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.
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()
}
}
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
modulefrom agroup, instead of excluding the entiregroup. -
Avoid global exclusions using
configurations.all { … }orconfigurations.configureEach { … }.
Example
Don’t Do This
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)
}
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)
}
-
The
commons-pool2dependency transitively includescglib:cglibandorg.ow2.asm:asm-utilas optional dependencies - we want to exclude both.hibernate-coretransitively optionally includescglib:cglib, and alsojavaassist:javassist- we want to exclude both. -
This excludes every module provided by the
cglibgroup from every dependency in theimplementationconfiguration. If other current or future dependencies in this project rely on different modules fromcglib, those dependencies may fail to resolve, leading to compilation or runtime errors. -
This excludes
org.ow2.asm:asm-utilfrom every dependency in theimplementationconfiguration. If future dependencies rely onorg.ow2.asm:asm-util, they may fail at compile or runtime because the module will be silently excluded. -
This excludes
javaassist:javassistfrom 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:
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 ...
}
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 ...
}
-
Exclusions are applied only to the dependency that actually transitively includes them.
-
All exclusions apply to a particular
moduleinstead of every module from a particulargroup. -
javaassist:javassistis only excluded from thehibernate-coredependency - 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):
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)
}
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
}
-
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):
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}")
}
}
}
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}")
}
}
}
-
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):
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)
}
}
}
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)
}
}
}
-
New Attribute type defined in producer: The consumable configuration defines a new
customattribute that contains project-specific variant identification information. The new Attribute is also added to the Gradle Attribute schema to ensure type-safety. -
Attributes declared in producer: The consumable configuration adds
Category=LIBRARY, andcustomattribute =my-custom-value. These attributes will be used to identify this variant during resolution.
In 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}")
}
}
}
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}")
}
}
}
-
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.
-
New Attribute type defined in consumer: The resolvable configuration defines the same new
customattribute that contains project-specific variant identification information. -
Attributes declared in consumer: The resolvable configuration is identified as
Category=LIBRARY, andcustomattribute =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:
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"))
}
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")
}
-
Task With Multiple Outputs:
helloWorldtask prints "Hello" to itsmessageFileand "World" to itsaudienceFile. -
Registering the Task:
helloWorldproduces "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:
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)
}
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)
}
-
Translation Task Setup:
translateBadrequireshelloWorldto run first to produce the message file otherwise it will fail with an error as the file does not exist. -
Explicit Task Dependency: Running
translateBadwill causehelloWorldto run first, but Gradle does not understand why.
Do This Instead
Instead, you should explicitly wire task inputs and outputs like this:
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)
}
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)
}
-
Register Implicit Task Dependency:
translateGoodrequires only one of the files that is produced byhelloWorld.
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:
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")
}
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")
}
-
Define a Task: The
BadCalculatorTasktype is deterministic and produces relocatable output, but is not annotated. -
Mark the Task Instance as Cacheable: This example shows how to mark a specific task instance as cacheable.
-
Forget to Mark a Task Instance as Cacheable: Unfortunately, the
addBad2instance of theBadCalculatorTasktype is not marked as cacheable, so it will not be cached, despite behaving the same asaddBad1.
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:
@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")
}
@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")
}
-
Annotate the Task Type: Applying the
@CacheableTaskto a task type informs Gradle that instances of this task should always be cached. -
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:
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)
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)
-
Using
Provider.get()in the task action -
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:
tasks.register<MyTask>("avoidThis") {
myInput = "currentEnvironment=${currentEnvironment.get()}" // (1)
myOutput = layout.buildDirectory.get().asFile.resolve("output-avoid.txt") // (2)
}
tasks.register("avoidThis", MyTask) {
myInput = "currentEnvironment=${currentEnvironment.get()}" // (1)
myOutput = new File(layout.buildDirectory.get().asFile, "output-avoid.txt") // (2)
}
-
Reading the value of
currentEnvironmentat configuration time: This value might change by the time the task start executing. -
Reading the value of
buildDirectoryat 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:
tasks.register<MyTask>("doThis") {
myInput = currentEnvironment.map { "currentEnvironment=$it" } // (1)
myOutput = layout.buildDirectory.file("output-do.txt") // (2)
}
tasks.register("doThis", MyTask) {
myInput = currentEnvironment.map { "currentEnvironment=$it" } // (1)
myOutput = layout.buildDirectory.file("output-do.txt") // (2)
}
-
Using
map()to transformcurrentEnvironment:maptransform runs only when the value is read. -
Using
file()to create a newProvider<RegularFile>: the value of thebuildDirectoryis 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:
tasks.register("generateDocs") {
// Build logic to generate documentation
}
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:
tasks.register("generateDocs") {
group = "documentation"
description = "Generates project documentation from source files."
// Build logic to generate documentation
}
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
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)
}
}
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)
}
}
-
isEmpty()causes resolution: Many seemingly harmless Collection API methods likeisEmpty()cause Gradle to resolve dependencies. -
Accessing files directly: Using
getFiles()to access the files in aConfigurationwill also cause Gradle to resolve the file collection. -
Adding a file via plus operator: Using the plus operator will force the
runtimeClasspathconfiguration to be resolved implicitly. The implementation ofConfigurationdoesn’t override the plus operator for regular files, therefore it falls back to using the eager API, which causes resolution. -
Be careful with indirect inputs: Some built-in tasks, for example subtypes of
AbstractCopyTasklikeZip, 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.
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))
}
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))
}
-
Add configurations to Task properties or Specs directly: This will defer resolution until the task is executed.
-
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
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)
}
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)
}
-
Add project dependency: The
:libproject must be built in order to resolve the runtime classpath successfully. -
Declare input property as Set of files: A simple
Setinput doesn’t track task dependencies. -
Dependency artifacts are used to calculate digest: Artifacts from the already resolved classpath are used to calculate the digest.
-
Resolve runtimeClasspath: The implicit task dependency on
:library:jartask is lost here when the configuration is resolved prior to task execution. Thelibproject will not be built when the:app:badClasspathPrintertask is run, leading to a failure incalculateDigestbecause thelib.jarfile will not exist.
Do This Instead
To avoid issues, always defer resolution to the execution phase by using lazy APIs like FileCollection.
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)
}
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)
}
-
Write to a file in the output directory: This is the same.
-
Declare input files property as ConfigurableFileCollection: This lazy collection type will track task dependencies.
-
Dependency artifacts are resolved to calculate digest: The classpath will be resolved at execution time to calculate the digest.
-
Configuration is passed to input property directly: Using
fromcauses the configuration to be lazily wired to the input property. The configuration will be resolved when necessary, preserving task dependencies. The output reveals that thelibproject is now built when the:app:goodClasspathPrintertask is run because of the implicit task dependency, and thelib.jarfile 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
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)
}
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)
}
-
The
AnimalSearchTasktask 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 isUP-TO-DATEor 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. -
The
copytask will move the exact samecandidates.txtto different destination directories, depending on if theuseAlternateInputproject property is set. -
The
searchtask is wired to use as input whatever the file thecopytask moved to itsdestinationDir. Despite the contents of the file being the same, when enabling the-PuseAlternateInputafter a successful build, thesearchtask will be out-of-date due to its different directory, and the search will be rerun.
Do this Instead
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE) // (1)
abstract val candidatesFile: RegularFileProperty
@InputFile
@PathSensitive(PathSensitivity.NONE) // (1)
abstract RegularFileProperty getCandidatesFile()
-
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 thesearchtask is rerun with-PuseAlternateInput, it remainsUP-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
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")
}
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")
}
-
Write to a file in the output directory: This task produces a single file in the
outputDirectory, named based on thetypeinput. -
Read a specific file in the input directory: This task only needs to read a single
a.txtfile in the input directory. -
Set output directory: Sets
outputDirectoryto a subdirectory inbuildDirectory. -
Set output directory: Same as above, using the same shared
greetingsdirectory. -
Wire
greeterAto consumer: Makes sure thatgreeterAruns and produces the output directory before it is used byconsumer.
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.
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)
}
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)
}
-
Set unique output directories: Each
GreetingTaskis assigned its own unique output directory based on thetypeinput.
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:
-
Tasks that produce single output files should make this clear from the type of their
@Outputproperties. -
Tasks that only consume single input files should make this clear from the type of their
@Inputproperties.
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")
}
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")
}
-
Write to a specific output file: This task produces a single file to a directly specified
outputFilewithout registering an entire output directory. -
Read a specific file: Unlike the previous example the input is a single directly specified
inputFilefile. -
Set output file: Sets
outputFileto a file that is inside asharedsubdirectory ofbuildDirectory. -
Wire
greeterAto consumer: Makes sure thatgreeterAproduces the output file before it is used byconsumerby 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:
-
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.
-
Task types - More robust than names in many cases, but can still change across plugin versions, depending on the plugin’s backwards compatibility policies.
-
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
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"
}
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"
}
-
Looking up
compileJavaby name: This relies on the hardcoded"compileJava"string. -
Using
tasks.namedto get thegeneratePomFileForMavenPublicationtask: This unnecessarily relies on this task’s name, even though it’s the onlyGenerateMavenPomtask 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:
tasks.named<JavaCompile>(JavaPlugin.COMPILE_JAVA_TASK_NAME) { // (1)
sourceCompatibility = "17"
targetCompatibility = "17"
}
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME) { // (1)
sourceCompatibility = "17"
targetCompatibility = "17"
}
-
Using
tasks.namedwithJavaPlugin.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:
java { // (1)
setSourceCompatibility(JavaVersion.VERSION_17)
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
pom.url = "sample.gradle.org" // (2)
}
}
}
java { // (1)
setSourceCompatibility(JavaVersion.VERSION_17)
}
publishing {
publications {
maven(MavenPublication) {
from(components.java)
pom.url = "sample.gradle.org" // (2)
}
}
}
-
Using Java DSL: Entirely avoids implementation details of
java-libraryplugin and setssourceCompatibilityfor allJavaCompiletasks. -
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
Projectinstance as an explicit task input. -
Use similar functionality already exposed through the
Taskobject (e.g.,Task.getLogger()instead ofProject.getLogger()). -
Capture
Projectvalues 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
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)
}
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
}
-
Reading
project.versionin a task action: Inside the task’s action, theProjectinstance is accessed to read theversionproperty.
There are two main problems with this setup:
-
Accessing the
Projectinstance during task execution will cause Configuration Cache failures. -
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.
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)
}
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)
}
-
Declare the version as an input property: This allows Gradle to track the version string for up-to-date checks.
-
Assign the version during configuration: It is safe to read
project.versionduring the configuration phase to assign it to a task input. -
Set the output location: Use
project.layoutduring 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:
-
Do not call
.get()insidemaporflatMap— the file doesn’t exist yet at configuration time, breaking lazy configuration and task dependency tracking -
Do not use standalone
provider {}to wrap task outputs —provider {}produces a disconnected provider with no task dependency information -
Use directly annotated abstract getters for task outputs —
flatMapcan only track dependencies through directly annotated abstract getters, not task outputs that exist only to define transforms of other properties. Getters that returnProviderinstead ofPropertyalso break tracking, even when annotated. -
Use
mapfor plain values,flatMapforProvider-typed outputs —flatMaprequires its closure to return aProvider; usemapto extractFileorStringvalues
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.
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)
})
}
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)
}
}
-
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.
Use the flatMap/map chain instead.
The map on a flatMap result is evaluated lazily at execution time, when the file exists:
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)
)
}
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)
}
-
Lazy wiring via
flatMap: extracts theoutputFileProvider while preserving the dependency ongenerator. -
Read content lazily: chaining
mapon theflatMapresult 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.
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()
})
}
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
}
}
-
Disconnected
provider {}block: has no link to the generator task; Gradle cannot infer thatconsumeNakeddepends ongeneratorfrom this provider alone.
Use flatMap to extract the Provider-typed output, then chain map to transform the value:
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)
)
}
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)
}
-
Lazy wiring via
flatMap: extracts theoutputFileProvider while preserving the dependency ongenerator. -
Read content lazily: chaining
mapon theflatMapresult 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.
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"))
}
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'))
}
-
Derived output property: when
outputFileis built via.map { … }rather than annotated directly,flatMapmay lose the task dependency.
As a workaround, use .map { it.property.get() } to preserve the dependency:
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"))
}
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'))
}
-
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:
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"))
}
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'))
}
-
Directly annotated abstract getter: with
@OutputFileon an abstractRegularFileProperty,flatMapcarries 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.
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() })
}
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 }
}
-
Use
mapfor non-Provider outputs: when the producer exposes a plainFileinstead of a Provider,mapis 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
-binreleases). -
The
-alldistribution 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:
# 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:
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:
# 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:
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
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)
}
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)
}
-
Performing heavy computation during configuration phase.
Do This Instead
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")
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)
-
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(distributionUrland optionallydistributionSha256Sum)
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
-
Inspect Build Files: Look at
build.gradle(.kts),settings.gradle(.kts), andgradle.propertiesfor suspicious dependencies, plugins, customexectasks, or obfuscated code.
-
Checksum Verification: Use checksum validation to ensure
gradle-wrapper.jarhasn’t been tampered with. -
Check the URL: Open
gradle/wrapper/gradle-wrapper.propertiesand ensure thedistributionUrlpoints 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, andgradle/wrapper/gradle-wrapper.jar, then rungradle wrapper(not./gradlew). Compare the regenerated files against the originals, any differences indicate the project’s wrapper may have been modified.
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
plugins {
`java-library` // (1)
}
tasks.named<Jar>("jar") {
isPreserveFileTimestamps = true // (2)
isReproducibleFileOrder = false // (3)
}
plugins {
id 'java-library' // (1)
}
tasks.named('jar', Jar) {
preserveFileTimestamps = true // (2)
reproducibleFileOrder = false // (3)
}
-
No
java { toolchain { … } }block is declared. Gradle has no default toolchain, so the JDK used for compilation falls back to whicheverJAVA_HOMEis active. Different machines (and even different JDK patch releases on the same machine) emit different bytecode. -
Sets
preserveFileTimestampstotrue, overriding the Gradle 9.0+ default offalse. 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. -
Sets
reproducibleFileOrdertofalse, overriding the Gradle 9.0+ default oftrue. Archive entries are ordered by the filesystem’s directory-iteration order, which varies by OS and filesystem.
Do This Instead
plugins {
`java-library`
}
java {
toolchain {
// Choose your project's required version
languageVersion = JavaLanguageVersion.of(21) // (1)
}
}
// (2)
plugins {
id 'java-library'
}
java {
toolchain {
// Choose your project's required version
languageVersion = JavaLanguageVersion.of(21) // (1)
}
}
// (2)
-
Pin the JDK used to compile so the build is decoupled from each developer’s local
JAVA_HOMEand from whichever JDK happens to be on the CI image; Gradle auto-provisions a matching JDK if one isn’t found. -
Leave archive defaults alone — Gradle 9.0+ sets
preserveFileTimestamps = falseandreproducibleFileOrder = trueon every AbstractArchiveTask (Jar,War,Ear,Zip,Tar). The performance cost of these defaults is negligible:reproducibleFileOrderadds 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 withtasks.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.
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)
}
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:
-
The Task is declared as cacheable: However, the task’s output differs depending on when it is run, so it should not be cached.
-
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.
-
Error in task property wiring: The
lastNameproperty is linked to thefirstNameproperty on the extension, which is likely a mistake. -
The wrong variable is assigned during task configuration: The
greetervariable from the buildscript is mistakenly assigned instead of the task’sgreetingproperty.
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:
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);
}
}
-
Todayis a proper@Input: This allows Gradle’sUP-TO-DATEchecking to properly consider it when rerunning the task.
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)
});
}
}
-
Corrected typo in assignment: The last name convention set to the value of the last name from the extension.
-
Todayis set to the same value on all tasks: Only callingInstant.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.
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)
}
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)
}
-
Define the new test suite: Creates a
functionalTestJVM Test Suite. -
Add TestKit dependency: This contains the
GradleRunnerclass we’ll use to write tests. -
Make the
java-gradle-pluginaware 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.
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:
-
testTaskRegistration: This test runs the
tasksreport and verifies the output. -
testTaskExecution: This test runs the custom task and verifies its output file.
-
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.
-
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--profileas the built-in Gradle option. -
In
gradle -- mytask --profile=value, Gradle passes--profileas 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
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:
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):
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
helptask. -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:5005by 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
trueand 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 istrue. -Dorg.gradle.debug.suspend=(true,false)-
A Gradle property that if set to
trueand debugging is enabled, the JVM running Gradle will suspend until a debugger is attached. Default istrue.
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
warnto report problems without failing the build.Set to
failto report problems and fail the build if there are any problems. --configure-on-demand,--no-configure-on-demandIncubating-
Toggles configure-on-demand. Only relevant projects are configured in this build run. Default is off.
--isolated-projects,--no-isolated-projectsIncubating-
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
normalorlow. Default is normal. --profile-
Generates a high-level performance report in the
layout.buildDirectory.dir("reports/profile")directory.--scanis preferred. --scan-
Generate a Build Scan with detailed performance diagnostics.
--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 --statusto list running and recently stopped Gradle daemons. It only displays daemons of the same Gradle version. --stop(Standalone command)-
Run
gradle --stopto 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
plainto 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
coloredto 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
richto 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
verboseto enable color and other rich output likerichwith output task names and outcomes at the lifecycle log level, (as is done by default in Gradle 3.5 and earlier).NoteWhen the NO_COLORenvironment 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-reportIncubating-
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
allto log all warnings.Set to
failto log all warnings and fail the build if there are any warnings.Set to
summaryto suppress all warnings and log a summary at the end of the build.Set to
noneto suppress all warnings, including the summary at the end of the build.In
allandfailmodes, 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.
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-graphSince 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
.gradledirectory 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
.gradlein 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-interactiveIncubating-
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:
-
Are consumed and interpreted by the tasks themselves;
-
Must be specified immediately after the task in the command-line;
-
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 messages |
|
Important information messages |
|
Warning messages |
|
Progress information messages |
|
Information messages |
|
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 |
|---|---|---|
|
|
QUIET and higher |
|
|
WARN and higher |
no logging options |
LIFECYCLE and higher |
|
|
|
INFO and higher |
|
|
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:
tasks.register("logtask") {
doLast {
logger.lifecycle("Lifecycle: Build progress info.")
logger.info("Info: Additional insights.")
logger.debug("Debug: Detailed troubleshooting info.")
}
}
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 |
|---|---|---|
|
|
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. |
|
|
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 |
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:
println("A message which is logged at QUIET level")
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:
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
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.
logger.info("A {} log message", "info")
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.
import org.slf4j.LoggerFactory
val slf4jLogger = LoggerFactory.getLogger("some-logger")
slf4jLogger.info("An info log message logged using SLF4j")
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.
logging.captureStandardOutput(LogLevel.INFO)
println("A message which is logged at INFO level")
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.
tasks.register("logInfo") {
logging.captureStandardOutput(LogLevel.INFO)
doFirst {
println("A task message which is logged at INFO level")
}
}
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:
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()
}
}
}
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.
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:
-
Adding the Wrapper - You set up a new Gradle project and add the Wrapper to it.
-
Using the Wrapper - You run a project with the Wrapper that already provides it.
-
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
-bindistribution contains only the runtime but no sample code and documentation. -
The Gradle version used for executing the build. By default, the
wrappertask 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 The |
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,
9resolves to the latest9.x.yrelease, and9.1resolves to the latest9.1.xrelease.The following labels are allowed:
--distribution-type-
The Gradle distribution type used for the Wrapper. Available options are
binandall. The default value isbin. --gradle-distribution-url-
The full URL pointing to the Gradle distribution ZIP file. This option makes
--gradle-versionand--distribution-typeobsolete, 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 is500milliseconds, 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.
tasks.wrapper {
distributionType = Wrapper.DistributionType.ALL
}
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 |
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 |
Uses the JVM that launches the wrapper script (e.g. |
Gradle Daemon JVM |
Long-lived process that executes the build and caches state between runs. |
Uses the JVM set by: |
*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):
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_OPTSandGRADLE_OPTSmatchorg.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=falseto the$GRADLE_USER_HOME/gradle.properties` file -
add the flag
-Dorg.gradle.daemon=falseto theGRADLE_OPTSenvironment 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:
-
Your environment (
JAVA_HOME) -
Your build script (if using toolchains)
-
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:
tasks.named<UpdateDaemonJvm>("updateDaemonJvm") {
languageVersion = JavaLanguageVersion.of(17)
}
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:
#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 |
AdoptOpenJDK |
|
ADOPTOPENJDK |
Amazon Corretto |
|
AMAZON |
Apple |
|
APPLE |
Azul Zulu |
|
AZUL |
BellSoft |
|
BELLSOFT |
GraalVM |
|
GRAAL_VM |
Hewlett Packard |
|
HEWLETT_PACKARD |
IBM |
|
IBM |
JetBrains |
|
JETBRAINS |
Microsoft |
|
MICROSOFT |
Oracle |
|
ORACLE |
SAP |
|
SAP |
Tencent |
|
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.
tasks.named<UpdateDaemonJvm>("updateDaemonJvm") {
val myPlatforms = mutableListOf(
BuildPlatformFactory.of(
org.gradle.platform.Architecture.AARCH64,
org.gradle.platform.OperatingSystem.MAC_OS
)
)
toolchainPlatforms.set(myPlatforms)
}
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:
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")
)
}
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:
tasks.named<UpdateDaemonJvm>("updateDaemonJvm") {
toolchainDownloadUrls.empty()
}
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
-XmxJVM argument) -
The minimum heap size (the
-XmsJVM argument) -
The boot classpath (the
-Xbootclasspathargument) -
The "assertion" status (the
-eaargument)
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:
-
Finishes the currently running build.
-
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):
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:
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)
-
Registry and logs of the Gradle Daemon.
-
Global initialization scripts.
-
JDKs downloaded by the toolchain support.
-
Distributions downloaded by the Gradle Wrapper.
-
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)
└── ⋮
-
Project-specific cache directory generated by Gradle.
-
The build directory of this project into which Gradle generates all build artifacts.
-
Contains the JAR file and configuration of the Gradle Wrapper.
-
Project-specific Gradle configuration properties.
-
Scripts for executing builds using the Gradle Wrapper.
-
The project’s settings file where the list of subprojects is defined.
-
Usually, a project is organized into one or multiple subprojects.
-
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 |
|---|---|
|
JARs that Gradle generates on first use of the Gradle version. |
|
Compiled Kotlin DSL scripts and accessors. |
|
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 |
|---|---|
|
Local Build Cache entries: task outputs keyed by input hashes. |
|
Instrumented JARs produced by Gradle’s classpath instrumentation. |
|
Downloaded module dependencies (POMs, JARs, metadata). See Dependency caching. |
|
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 |
|---|---|
|
Version-specific build state for incremental build (snapshots of task inputs and outputs from the previous build). |
|
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-9ormodules-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:
-
Released wrapper distributions: Distributions and related version-specific caches corresponding to released versions (e.g.,
4.6.2or8.0).Default retention for unused versions is 30 days.
-
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.
-
Downloaded resources: Shared caches downloaded from a remote repository (e.g., cached dependencies).
Default retention for unused resources is 30 days.
-
Created resources: Shared caches that Gradle creates during a build (e.g., artifact transforms).
Default retention for unused resources is 7 days.
-
Build cache: The local build cache (e.g., build-cache-1).
Default retention for unused build cache entries is 7 days.
-
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:
beforeSettings {
caches {
releasedWrappers.setRemoveUnusedEntriesAfterDays(45)
snapshotWrappers.setRemoveUnusedEntriesAfterDays(10)
downloadedResources.setRemoveUnusedEntriesAfterDays(45)
createdResources.setRemoveUnusedEntriesAfterDays(10)
buildCache.setRemoveUnusedEntriesAfterDays(5)
daemonLogs.setRemoveUnusedEntriesAfterDays(14)
}
}
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:
-
DEFAULT: Cleanup is performed periodically in the background (currently once every 24 hours).
-
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.
-
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:
beforeSettings {
caches {
cleanup = Cleanup.DISABLED
}
}
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:
if (GradleVersion.current() >= GradleVersion.version("8.0")) {
apply(from = "gradle8/cache-settings.init.gradle.kts")
}
if (GradleVersion.current() >= GradleVersion.version('8.0')) {
apply from: "gradle8/cache-settings.init.gradle"
}
Version-compliant cache configuration script:
beforeSettings {
caches {
releasedWrappers { setRemoveUnusedEntriesAfterDays(45) }
snapshotWrappers { setRemoveUnusedEntriesAfterDays(10) }
downloadedResources { setRemoveUnusedEntriesAfterDays(45) }
createdResources { setRemoveUnusedEntriesAfterDays(10) }
buildCache { setRemoveUnusedEntriesAfterDays(5) }
}
}
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:
beforeSettings {
caches {
// Disable cache marking for all caches
markingStrategy = MarkingStrategy.NONE
}
}
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 |
|---|---|---|
Flags that configure build behavior and Gradle features |
|
|
Properties specific to your Gradle project |
|
|
Properties that are passed to the Gradle runtime (JVM) |
|
|
Properties that configure Gradle settings and the Java process that executes your build |
|
|
Properties that configure build behavior based on the environment |
|
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 |
Flags have precedence over properties and environment variables |
|
2 |
> Project Root Dir |
Stored in a |
|
3 |
> |
Stored in a |
|
4 |
> Environment |
Sourced by the environment that executes Gradle |
Here are all possible configurations of specifying the JDK installation directory in order of priority:
-
Command Line
$ ./gradlew exampleTask -Dorg.gradle.java.home=/path/to/your/java/home --scan -
Gradle Properties File
gradle.propertiesorg.gradle.java.home=/path/to/your/java/home -
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
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 |
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:
-
Command Line: You can add project properties directly to your Project object via the
-Pcommand line option.$ ./gradlew build -PmyProperty='Hi, world' -
System Property: Gradle creates specially-named system properties for project properties which you can set using the
-Dcommand line flag orgradle.propertiesfile. For the project propertymyProperty, the system property created is calledorg.gradle.project.myProperty.$ ./gradlew build -Dorg.gradle.project.myProperty='Hi, world'gradle.propertiesorg.gradle.project.myProperty='Hi, world' -
Gradle Properties File: You can also set project properties in
gradle.propertiesfiles.gradle.propertiesmyProperty='Hi, world'The
gradle.propertiesfile 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.
-
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 amyPropertyproperty on your project object, with the value ofHi, 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
|
|
For other ways to access properties, see the following methods on the Project object:
-
findProperty("name")— returns the value ornull, 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:
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")
}
}
}
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:
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:
-
Command Line: Using the
-Dcommand-line option, you can pass a system property to the JVM, which runs Gradle. The-Doption of thegradlecommand has the same effect as the-Doption of thejavacommand.$ ./gradlew build -Dgradle.wrapperUser=myuser -
Gradle Properties File: You can also set system properties in
gradle.propertiesfiles with the prefixsystemProp.gradle.propertiessystemProp.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_HOMEdirectory. 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:
-
Command Line: Using the
-Dcommand-line option, you can pass a Gradle property:$ ./gradlew build -Dorg.gradle.caching.debug=false -
Gradle Properties File: Place these settings into a
gradle.propertiesfile and commit it to your version control system.gradle.propertiesorg.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 |
2 |
|
|
Stored in a |
3 |
|
Project Root Dir |
Stored in a |
4 |
|
|
Stored in a |
|
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
truethe 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=5005to 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
jdkorjrelocation; 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_HOMEor the path tojava) 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
lifecyclelevel. org.gradle.parallel=(true,false)-
When configured, Gradle will fork up to
org.gradle.workers.maxJVMs 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
.gradlein the root project directory."Default is
.gradle. org.gradle.problems.report=(true|false)-
Enable (
true) or disable (false) the generation ofbuild/reports/problems-report.html.trueis 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.parallelin 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
trueon operating systems where Gradle supports this feature. org.gradle.warning.mode=(all,fail,summary,none)-
When set to
all,summary, ornone, 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/binto yourPATHfor 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_OPTStypically doesn’t affect the daemon, you can use it to pass arguments to the daemon by setting theorg.gradle.jvmargssystem 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_HOMEdirectory for Gradle to store its global configuration properties, initialization scripts, caches, log files and more.Defaults to
USER_HOME/.gradleif 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.homeor 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 |
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:
rootProject.name = "basic"
println("This is executed during the initialization phase.")
// 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.")
}
}
rootProject.name = 'basic'
println 'This is executed during the initialization phase.'
// 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 |
|
Global environment setup (e.g., enterprise repos). |
|
2 |
init script or |
Runs before the settings file is even parsed. |
||
3 |
|
|
Must be first. Defines plugin repos and version rules. |
|
4 |
|
|
Applies plugins to the |
|
5 |
Settings Script Body |
|
Evaluates |
|
6 |
init script or |
Settings are fully processed; Project objects are created. |
||
7 |
init script or |
All project instances exist but aren’t configured yet. |
||
Configuration |
8 |
init script or |
Not recommended. Fires immediately before each project starts evaluating. |
|
9 |
init script or parent project’s |
Runs before a specific project’s build script is evaluated. |
||
10 |
|
|
Sets the classpath for the build script itself. |
|
11 |
|
|
Applies plugins and adds DSL/Tasks. |
|
12 |
Build Script Body |
|
Registers tasks ( |
|
13 |
|
Not recommended. Runs after a specific project is evaluated; useful for reacting to another project’s configuration. |
||
14 |
init script or root |
Fires immediately after each project finishes evaluating. |
||
15 |
init script or root |
All project scripts have run; final chance to tweak the graph. |
||
16 |
Task Graph Construction |
Internal ( |
Gradle calculates the DAG based on requested tasks. |
|
Execution |
17a |
Task Input Snapshotting |
Internal ( |
Evaluated per task to determine whether it should be skipped or executed. |
17b |
Internal ( |
Constructs a dependency graph based on declared dependencies. |
||
17c |
Internal ( |
Dependencies are downloaded from repositories. |
||
18 |
Task Execution |
Console / CLI ( |
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: build → assemble → createDocs.
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:
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 |
|---|---|---|
Early wiring from an init script: global logging/metrics, tweaking |
||
Adjusting repositories, enabling build scans, reading environment variables, or changing build layout. |
||
Applying configuration to every project (for example, adding a plugin to all subprojects). |
||
Applying defaults, logging project configuration start. |
||
Validating configuration or detecting missing plugins. |
||
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.
// 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
}
}
// 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.
// 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()
}
}
// 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.
// 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
}
}
// 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.
// 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")
)
}
// 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.
// 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}")
}
// 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.
// 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))
}
}
}
}
// 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? |
|---|---|---|
Fires when projects are loaded/evaluated. |
❌ |
|
Fires when the execution of an operation progresses. |
✅ |
|
(Partially Deprecated) |
Fires when a task has started or completed its action. |
❌ |
(Deprecated) |
Fires before and after each task executes. |
❌ |
(Deprecated) |
Fires when the task execution graph has been populated. |
❌ |
Fires when an operation completes. |
✅ |
|
Fires before and after dependency resolution. |
❌ |
|
Fires before and after each project is evaluated. |
❌ |
|
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:
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.")
}
}
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:
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)
}
}
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:
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")
}
}
}
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:
-
Profile with a Build Scan
-
Local profile reports
-
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:
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.
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.
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.
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:
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.
Changes to symbolic links
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-fsflag -
set
org.gradle.vfs.watch=falseingradle.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.
Symlinks
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 stateduring 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 fileserror 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)
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 ofvoid.
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.
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)
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>) {
...
}
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:
-
They are implemented as methods with specific signatures.
-
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:
-
It must have at least one argument.
-
The last argument must be of type
groovy.lang.Closureor org.gradle.api.Action.
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
Actionarguments, 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
Closurearguments, 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
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
-
The embedded Kotlin compiler supports the platforms and architectures listed in the Kotlin/Native target support documentation.
-
Familiarity with Kotlin syntax and basic language features is recommended. Refer to the Kotlin documentation and Kotlin Koans to learn the basics.
-
Using the
plugins {}block to declare Gradle plugins is highly recommended as it significantly improves the editing experience.
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:
-
Import the whole build again:
-
Reload script dependencies when editing a build script:
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
.gradlefile name extension. -
Kotlin DSL script files use the
.gradle.ktsfile 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
Settingsobject) using the pattern*.settings.gradle.kts. This includes script plugins applied from settings scripts. -
Name initialization scripts using the pattern
*.init.gradle.ktsor simplyinit.gradle.kts.
Implicit imports
All Kotlin DSL build scripts come with implicit imports, including:
-
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:
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 |
|
Project extensions and conventions, and extensions on them |
|
Extensions on the |
|
Elements in the |
|
Elements in project-extension containers |
Source sets contributed by the Java Plugin that are added to the |
For the main project settings script and precompiled settings script plugins:
| Type-safe model accessors | Example |
|---|---|
Project extensions and conventions, contributed by |
|
|
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:
// 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:
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()
}
}
-
Uses type-safe accessors for the
api,implementationandtestImplementationdependency configurations contributed by the Java Library Plugin -
Uses an accessor to configure the
sourceSetsproject extension -
Uses an accessor to configure the
mainsource set -
Uses an accessor to configure the
javasource for themainsource set -
Uses an accessor to configure the
testtask
|
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 |
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:
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:
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:
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.
The following sample demonstrates how to reference and configure artifact configurations without type-safe accessors:
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 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:
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:
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.
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:
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 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:
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:
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")
}
-
Gets a reference of type
Taskto the existing task namedcheck -
Registers a new untyped task named
myTask1 -
Gets a reference to the existing task named
compileJavaof typeJavaCompile -
Registers a new task named
myCopy1of typeCopy -
Gets a reference to the existing (untyped) task named
assembleand configures it — you can only configure properties and methods that are available onTaskwith this syntax -
Registers a new untyped task named
myTask2and configures it — you can only configure properties and methods that are available onTaskin this case -
Gets a reference to the existing task named
testof typeTestand configures it — in this case you have access to the properties and methods of the specified type -
Registers a new task named
myCopy2of typeCopyand 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:
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:
extra["myNewProperty"] = "initial value" // (1)
val myExtraProperty = extra["myNewProperty"] as String // (2)
val myExtraNullableProperty = extra["myNullableProperty"] as String? // (3)
-
Creates a new extra property called
myNewPropertyin the current context (the project in this case) and initializes it with the value"initial value" -
Access an existing extra property from the current context (the project in this case)
-
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:
val myNewProperty = rootProject.extra["myNewProperty"] as String // (1)
-
Reads the root project’s
myNewPropertyextra 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:
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)
}
}
-
Creates a new
reportTypeextra property on thetesttask -
Reads the
testtask’sreportTypeextra property to configure thearchiveTestReportstask
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)orproperty = value -
Accessing the value:
property.get()
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) }
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 { … }
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:
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")
}
-
Set value with the
.set()method -
Set value with lazy property assignment using the
=operator -
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, andgradleKotlinDsl()dependencies to thecompileOnlyandtestImplementationconfigurations, 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:
-
Registers the SAM-with-receiver Kotlin compiler plugin.
-
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:
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.
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.
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
invokedynamicJVM 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.
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:
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:
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.
To call a Kotlin function that has named arguments from Groovy, just use a normal method call with positional parameters:
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.
To call a Groovy function with named arguments from Kotlin, you need to pass a Map<String, ?>, as shown in this example:
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:
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:
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:
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:
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)
}
-
The receiver is a GroovyObject and provides Kotlin helpers
-
The
GroovyObjectAPI is available -
Invoke the
methodNamemethod, passing some parameters -
Configure the
blockNameproperty, maps to aClosuretaking method invocation -
Invoke
anothermethod taking named arguments, maps to a Groovy named argumentsMap<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:
native { // (1)
dynamic {
groovy as Usual
}
}
plugins {
id("dynamic-groovy-plugin") version "1.0" // (2)
}
apply(from = "dynamic-groovy-plugin-configuration.gradle") // (3)
-
The Groovy script uses dynamic Groovy to configure plugin
-
The Kotlin build script requests and applies the plugin
-
The Kotlin build script applies the Groovy script
Troubleshooting
The IDE support is provided by two components:
-
Kotlin Plugin (used by IntelliJ IDEA/Android Studio).
-
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 tasksto gather more details. -
Check the logs in one of these locations:
-
$HOME/Library/Logs/gradle-kotlin-dslon macOS -
$HOME/.gradle-kotlin-dsl/logon Linux -
$HOME/AppData/Local/gradle-kotlin-dsl/logon 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.scriptCompilationAvoidancesystem property tofalse. -
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.
|
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)
}
-
Property assignment
-
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
Settingsaccording 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:
plugins {
java
jacoco
`maven-publish`
id("org.springframework.boot") version "3.4.4"
}
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:
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")
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 The declarative nature of the |
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.
plugins {
jacoco
}
jacoco {
toolVersion = "0.8.1"
}
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:
apply(plugin = "checkstyle")
configure<CheckstyleExtension> {
maxErrors = 10
}
apply plugin: "checkstyle"
checkstyle {
maxErrors = 10
}
Again, we strongly recommend that you apply plugins declaratively via the plugins {} block.
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.
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
Taskinstances 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.
tasks.jar {
archiveFileName = "foo.jar"
}
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:
tasks.named<Jar>("jar") {
archiveFileName = "foo.jar"
}
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:
tasks.getByName<Jar>("jar") {
archiveFileName = "foo.jar"
}
tasks.getByName('jar') {
archiveFileName = 'foo.jar'
}
Working with containers in the Gradle Kotlin DSL is documented in detail here.
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:
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")
}
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:
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")
}
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(…):
task("greeting") {
doLast { println("Hello, World!") }
}
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:
tasks.register("greeting") {
doLast { println("Hello, World!") }
}
tasks.register('greeting') {
doLast { println('Hello, World!') }
}
tasks.create("greeting") {
doLast { println("Hello, World!") }
}
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:
tasks.register<Zip>("docZip") {
archiveFileName = "doc.zip"
from("doc")
}
tasks.register('docZip', Zip) {
archiveFileName = 'doc.zip'
from 'doc'
}
tasks.create<Zip>("docZip") {
archiveFileName = "doc.zip"
from("doc")
}
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:
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")
}
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.
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:
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")
}
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 thetestImplementationconfiguration and to which we add a different dependency
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")
}
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:
// 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:
-
The Writing Build Scripts chapter demonstrates the use of
apply(from = "")to modularize build scripts. -
The Multi-project Builds chapter demonstrates various multi-project build structures.
-
The Developing Custom Gradle Plugins and Gradle Kotlin DSL Primer chapters demonstrate how to develop custom Gradle plugins.
-
The Composing builds chapter demonstrates how to use Composite Builds.
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