Gradle Best Practices & Troubleshooting

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Gradle 9.8.1 (released on 7 October 2026), with Gradle 8.14.6, the last 8.x release, mentioned where it differs. Every example marked as run was executed with the 9.8.1 wrapper on 2026-10-16, on macOS (Apple silicon), in scratch copies of the tidepool build (a root project with the subprojects tidepool-model, tidepool-engine and tidepool-cli, a version catalog, and repositories declared in settings.gradle.kts), once with *.gradle.kts files and once with *.gradle files. The Gradle daemon ran on Oracle JDK 26 (java version "26"), the default JDK of the machine, and the code compiles with --release 21. The gradle.properties of the examples sets org.gradle.jvmargs=-Xmx512m -Dfile.encoding=UTF-8 -Duser.language=en -Duser.country=US, so that messages come out in English instead of the machine’s Spanish, and org.gradle.configuration-cache=true: the examples are compatible with the configuration cache (a second run reused the entry) except the one that fails on purpose, which is said where it happens. Runs that need to show a problem the cache would hide use --no-configuration-cache, and say so. The normal runs used the shared Gradle User Home; only the debugger run used a Gradle User Home created for it. The scratch directory is shown as <work>. Gradle 8.14.x was not available here, so what is said about it comes from the official release notes and upgrade guides and is labelled "not run here". Also not run here: publishing a Build Scan, a daemon that cannot connect because of network address translation, refreshing an IDE, attaching a real debugger, and the wrapper validation action for GitHub Actions.

This is a synthesis page: it collects the advice that Gradle publishes as its best practices and its troubleshooting guide, points to the page of this site that explains each topic in depth instead of repeating it, and backs the advice that can be checked with a real run. The first half is about how to write a build that is easy to keep healthy; the second half is about what to do when it is not. For the other two tools, the equivalents are Maven Troubleshooting & Best Practices and, for Ant, the diagnostics options in Running Ant; a table near the end compares them. The cross-tool background is in Build Automation Fundamentals and in Comparing Ant, Maven and Gradle.

The official best-practices index

Gradle keeps a list of recommendations, the Best Practices for Gradle, split into pages by theme (general, structuring builds, dependencies, tasks, performance, security and testing). Each practice has an explanation, a "Don’t do this" and a "Do this instead" example (in both DSLs), references and tags, and the index records the Gradle version in which the practice was added to the list. That column is not the version in which a feature appeared; it tells you which advice is new to you if you last read the list some releases ago. The oldest entries are marked 8.14; newer entries are added in nearly every minor release (the latest at the time of writing is marked 9.8.0), so rereading the index after each upgrade is cheap and worth doing. The index counts 47 practices on 2026-10-16.

The tables below reproduce the index (title, section and release) and add the page of this site that covers the topic. The practices are advice, not rules: several of them (for instance "Avoid afterEvaluate") have narrow, documented exceptions, which the official page lists.

Table 1. General practices of the official index
Practice Added in Detailed page

Use Kotlin DSL

8.14

Kotlin DSL, Groovy DSL & Migrating to Kotlin

Use the Latest Minor Version of Gradle

8.14

Upgrading Gradle, Versions, Compatibility & Release History

Apply Plugins Using the plugins Block

8.14

Plugins Overview

Do Not Use Internal APIs

8.14

Binary Plugins

Set build flags in gradle.properties

9.0.0

Build Environment & Properties

Name Your Root Project

9.2.0

Settings & Build Scripts

Do not use gradle.properties in subprojects

9.2.0

Build Environment & Properties

Avoid afterEvaluate

9.6.0

Build Lifecycle, Lazy Configuration & Providers

Consider use of @Incubating APIs carefully

9.7.0

Versions, Compatibility & Release History

Obtain Loggers via Logging.getLogger(Class) outside of Tasks

9.8.0

Directories & Logging, Writing Custom Tasks

Table 2. Structuring builds practices of the official index
Practice Added in Detailed page

Modularize Your Builds

9.0.0

Multi-Project Builds

Do Not Put Source Files in the Root Project

9.0.0

Multi-Project Builds

Favor build-logic Composite Builds for Build Logic

9.0.0

Convention Plugins & Sharing Build Logic, Composite Builds

Avoid Unintentionally Creating Empty Projects

9.1.0

Multi-Project Builds, Settings & Build Scripts

Use Convention Plugins

9.3.0

Convention Plugins & Sharing Build Logic

Table 3. Dependencies practices of the official index
Practice Added in Detailed page

Single GAV String

8.14

Declaring Dependencies & Configurations

Use Version Catalogs to Centralize Dependency Versions

9.0.0

Version Catalogs

Name Version Catalog Entries Appropriately

9.0.0

Version Catalogs

Set up your Dependency Repositories in the Settings file

9.0.0

Repositories & Content Filtering

Don’t Explicitly Depend on the Kotlin Standard Library

9.0.0

Declaring Dependencies & Configurations

Avoid Redundant Dependency Declarations

9.0.0

Declaring Dependencies & Configurations

Use Content Filtering with multiple Repositories

9.1.0

Repositories & Content Filtering

Apply Exclusions Narrowly

9.2.0

Dependency Resolution, Conflicts & Capabilities

Always Declare Attributes on Consumable and Resolvable Configurations

9.7.0

Variant-Aware Resolution & Module Metadata

Table 4. Tasks practices of the official index
Practice Added in Detailed page

Avoid dependsOn

8.14

Tasks

Favor @CacheableTask and @DisableCachingByDefault over cacheIf(Spec) and doNotCacheIf(String, Spec)

8.14

Build Cache, Writing Custom Tasks

Group and Describe custom Tasks

9.0.0

Tasks

Do not call get() on a Provider outside a Task action

9.1.0

Lazy Configuration & Providers

Don’t resolve Configurations before Task Execution

9.1.0

Declaring Dependencies & Configurations

Avoid using eager APIs on File Collections

9.1.0

Working with Files

Prefer @PathSensitivity.NONE for file inputs and @PathSensitivity.RELATIVE for directories

9.2.0

Incremental Build, Build Cache

Use unique output files and directories

9.3.0

Incremental Build

Don’t hardcode task names when referring to them

9.7.0

Tasks

Don’t access a Project instance during Task Execution

9.7.0

Configuration Cache & Isolated Projects

Wiring Task Outputs with map and flatMap

9.7.0

Lazy Configuration & Providers

Favor collection property types over a Property holding a collection

9.8.0

Lazy Configuration & Providers

Table 5. Performance practices of the official index
Practice Added in Detailed page

Enable UTF-8

9.0.0

Build Environment & Properties

Use the Build Cache

9.1.0

Build Cache

Use the Configuration Cache

9.1.0

Configuration Cache & Isolated Projects

Avoid Expensive Computations in Configuration Phase

9.0.0

Performance Profiling & Build Scans

Prefer the -bin Gradle Distribution

9.4.0

Gradle Wrapper

Table 6. Security practices of the official index
Practice Added in Detailed page

Validate the Gradle Distribution SHA-256 Checksum

9.1.0

Gradle Wrapper

Validate the Gradle Wrapper on every Upgrade

9.3.0

Gradle Wrapper

Do not Run ./gradlew on Untrusted Projects

9.7.0

Supply-Chain Security

Build Output Should Be Byte-for-Byte Reproducible

9.7.0

Publishing

Build your Published Artifacts Securely

9.8.0

Publishing

Table 7. Testing practices of the official index
Practice Added in Detailed page

Test your custom Task and Plugins with TestKit

9.4.0

Testing & Publishing Plugins

Source: Gradle: Best Practices for Gradle, index, and the pages best_practices_general, best_practices_structuring_builds, best_practices_dependencies, best_practices_tasks, best_practices_performance, best_practices_security and best_practices_testing under the Gradle User Manual

Reading the list as a few themes

Forty-seven items are easier to remember as six habits.

  • Say what you mean in one place. Name the root project, declare repositories once in settings.gradle(.kts), keep versions in a catalog, put build flags in the root gradle.properties, and use the plugins {} block. Everything that is declared twice drifts apart. The pages are Settings & Build Scripts, Repositories & Content Filtering and Version Catalogs.

  • Share logic through plugins, not through injection. A build-logic included build with convention plugins replaces allprojects {} and subprojects {} blocks and buildSrc; see Convention Plugins & Sharing Build Logic.

  • Be lazy. Register tasks instead of creating them, wire properties and providers instead of reading values early, and do not use afterEvaluate; see Lazy Configuration & Providers.

  • Declare what a task reads and writes. Inputs, outputs, path sensitivity and unique output directories are what make a task cacheable, up to date and safe to run in parallel; see Incremental Build and Build Cache.

  • Do not touch the live project at execution time. No Project, no resolved configurations, no get() on a provider outside an action. This is what makes a build compatible with the configuration cache.

  • Trust nothing you did not verify. Set distributionSha256Sum, validate the wrapper after each upgrade, do not run ./gradlew on a project you have not read, and make outputs reproducible; the cross-tool view is in Supply-Chain Security.

A starter configuration

The tidepool build used on this page follows the structural practices, which is why it is a good base for the experiments below. This is its settings.gradle.kts and gradle.properties.

Kotlin DSL: settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        mavenCentral()
    }
}

rootProject.name = "tidepool"

include("tidepool-model", "tidepool-engine", "tidepool-cli")
Groovy DSL: settings.gradle
dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        mavenCentral()
    }
}

rootProject.name = 'tidepool'

include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
gradle.properties (the same for both DSLs)
org.gradle.jvmargs=-Xmx512m -Dfile.encoding=UTF-8 -Duser.language=en -Duser.country=US
org.gradle.configuration-cache=true
group=io.tidepool
version=1.0.0
gradle/wrapper/gradle-wrapper.properties (the official download address)
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

What each line answers in the list of practices:

  • rootProject.name is set, so task paths and reports do not depend on the name of the directory (or of the CI workspace) the build runs in. The practice was added to the list in 9.2.0.

  • Repositories are declared once, in the settings file, with FAIL_ON_PROJECT_REPOS so that a project cannot add its own; this works with the content filtering practice of Repositories & Content Filtering.

  • Build flags live in the root gradle.properties, which is checked into version control, instead of in command-line options that people forget. The official page also says not to restate a property at its default value: a default can change in a later release through the normal deprecation cycle, and then a build that never set it follows the change while one that restated it does not. This is why org.gradle.configuration-cache=true is there (the configuration cache is recommended but still off by default in 9.8.1; the default is announced to change in Gradle 10) and why org.gradle.caching is not set to false.

  • -Dfile.encoding=UTF-8 follows the performance practice that asks for UTF-8, because a platform-dependent default encoding changes the inputs of tasks between machines and causes cache misses. The locale options are only there to keep this page in English; a real build does not need them.

  • The wrapper properties carry distributionSha256Sum and the -bin distribution, as the security and performance practices ask. The page Gradle Wrapper explains every key; the verification is shown below.

  • There is no gradle.properties in any subproject. The official page says that support for subproject property files is inconsistent (Gradle itself, the Android Gradle plugin and the Kotlin Gradle plugin do not handle it reliably) and that a value for one subproject belongs in its build script, and a value for several subprojects in a convention plugin.

Practices you can check by running them

Four of the practices are claims that a few lines of build script and a command can confirm. Each of the examples below was added to a scratch copy of the tidepool build and run; the sources are the root build.gradle(.kts) and tidepool-model/build.gradle(.kts) of that copy.

Wire lazily instead of using afterEvaluate

The practice (added to the list in 9.6.0) says: do not use project.afterEvaluate {} to configure tasks, wire properties or react to plugin application; use lazy properties and pluginManager.withPlugin(). The official reasons are that the order of several afterEvaluate callbacks is the order of registration, so a callback can see stale configuration; that tasks configured inside it are touched eagerly even if they never run; and that it is incompatible with the configuration cache. It lists two narrow cases where it is still acceptable, fail-fast validation and reporting, and asks you to file a bug if the lazy APIs do not cover your case.

The reason people reach for afterEvaluate is that a value is set after the task that reads it has been defined. A Property solves that without a callback: the task holds the property, the value is read when the task runs, and it does not matter which line sets it.

Kotlin DSL: build.gradle.kts
val bannerText = objects.property<String>()
tasks.register("bannerLazy") {
    val text = bannerText
    doLast { println(text.get()) }
}
bannerText.set("set after the task was wired")
Groovy DSL: build.gradle
def bannerText = objects.property(String)
tasks.register('bannerLazy') {
    def text = bannerText
    doLast { println text.get() }
}
bannerText.set('set after the task was wired')

To react to another plugin without afterEvaluate, use pluginManager.withPlugin("java") { ... }, which runs immediately if the plugin is already applied and as soon as it is applied otherwise (the details are in Build Lifecycle and Lazy Configuration & Providers). A pitfall of the Groovy version: a plain bannerText = 'text' on a local def variable replaces the variable with a String (the build then fails with No signature of method: java.lang.String.get(), which is what the first attempt of this example did); a Property held in a local variable has to be set with set(). Task properties, in contrast, accept = in both DSLs.

The run (Kotlin DSL; the second run of the build, so the configuration cache entry was reused, which shows that the example is compatible with it; Groovy printed the same lines):

$ ./gradlew showVersionGood bannerLazy
Reusing configuration cache.

> Task :showVersionGood
version = 1.0.0

> Task :bannerLazy
set after the task was wired

BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
Configuration cache entry reused.

Do not use the Project at execution time

The practice "Don’t access a Project instance during Task Execution" (9.7.0) is the most common reason for a build to be rejected by the configuration cache. The project is a live object of the configuration phase, and a cached build does not have it. Here is the mistake, in a task that prints the project version in doLast, and the fix, which copies the value into a local variable while the task is being configured:

Kotlin DSL: build.gradle.kts
// Configuration cache trap: Project used while the task runs.
tasks.register("showVersionBad") {
    doLast { println("version = ${project.version}") }
}

// The fix: capture the value while the task is configured.
tasks.register("showVersionGood") {
    val version = project.version.toString()
    doLast { println("version = $version") }
}
Groovy DSL: build.gradle
// Configuration cache trap: Project used while the task runs.
tasks.register('showVersionBad') {
    doLast { println "version = ${project.version}" }
}

// The fix: capture the value while the task is configured.
tasks.register('showVersionGood') {
    def version = project.version.toString()
    doLast { println "version = ${version}" }
}

With the configuration cache on, the first one fails (the second one is the run shown in the previous section):

$ ./gradlew showVersionBad
Calculating task graph as no cached configuration is available for tasks: showVersionBad
> Task :showVersionBad FAILED

1 problem was found storing the configuration cache.
- Task `:showVersionBad` of type `org.gradle.api.DefaultTask`: invocation of 'Task.project' at execution time is unsupported with the configuration cache.
  See https://docs.gradle.org/9.8.1/userguide/configuration_cache_requirements.html#config_cache:requirements:use_project_during_execution

See the complete report at file://<work>/tidepool/build/reports/configuration-cache/6dc51gbm7he8rkshqkjyjihbj/efs7x0f6zwvrp07qhehjbow2n/configuration-cache-report.html

[Incubating] Problems report is available at: file://<work>/tidepool/build/reports/problems/problems-report.html

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':showVersionBad' (registered in build file 'build.gradle.kts').
> Invocation of 'Task.project' by task ':showVersionBad' at execution time is unsupported with the configuration cache.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.

BUILD FAILED in 1s
1 actionable task: 1 executed
Configuration cache entry discarded with 1 problem.

The Groovy run fails the same way. The differences are that the problem line names the build file and the line (Build file 'build.gradle': line 37: invocation of 'Task.project' ...), that the failure has a Where: section with Build file '<work>/tidepool/build.gradle' line: 37, and that the registration is reported as (registered in build file 'build.gradle'). That last parenthesis is the task provenance added to error messages in Gradle 9.5.0; the 8.14 output has no such text (not run here, from the release notes). The error message and the link to the explanation are the same in 8.14 (the configuration cache has been stable since 8.1). In a build that has just been taken to 9.x, --no-configuration-cache makes the failure disappear without fixing it, so the right reaction is to read the report whose location is printed, not to switch the cache off. The cache itself is the subject of Configuration Cache & Isolated Projects.

Reproducible archives

"Build output should be byte-for-byte reproducible" (9.7.0) is a security and a performance practice at once: if the same source produces the same bytes, anybody can check a binary against the source, and the build cache and the up-to-date checks work on stable inputs. The two usual causes of difference in a JAR are the timestamps of the entries and their order. Since Gradle 9.0.0 archive tasks are reproducible by default: timestamps are not preserved and the order of the entries is sorted. In Gradle 8.14 they were not (the 9.0.0 release notes list it as a change of default; not run here).

To see the effect, build the JAR of tidepool-model twice, two seconds apart, with a clean before each build, and compare the first 16 hex digits of the SHA-256 sum. Then do it with a switch that restores the old behaviour for archive tasks.

Kotlin DSL: tidepool-model/build.gradle.kts
// Opt out of the reproducible-archive defaults to see the difference.
if (providers.gradleProperty("nonRepro").isPresent) {
    tasks.withType<AbstractArchiveTask>().configureEach {
        isPreserveFileTimestamps = true
        isReproducibleFileOrder = false
    }
}
Groovy DSL: tidepool-model/build.gradle
// Opt out of the reproducible-archive defaults to see the difference.
if (providers.gradleProperty('nonRepro').present) {
    tasks.withType(AbstractArchiveTask).configureEach {
        preserveFileTimestamps = true
        reproducibleFileOrder = false
    }
}
$ for i in 1 2; do ./gradlew -q :tidepool-model:clean :tidepool-model:jar; shasum -a 256 tidepool-model/build/libs/tidepool-model-1.0.0.jar | cut -c1-16; sleep 2; done
875ec791c2960e71
875ec791c2960e71
$ for i in 1 2; do ./gradlew -q -PnonRepro :tidepool-model:clean :tidepool-model:jar; shasum -a 256 tidepool-model/build/libs/tidepool-model-1.0.0.jar | cut -c1-16; sleep 2; done
7230d90176fb608d
4f1a85a50f1d7522

The first pair of lines is identical with both DSLs (875ec791c2960e71 each time, in two separate builds in two projects with identical sources); with the switch on, the checksum changes on every build (the two Groovy runs gave 4bd70f84d92097fe and 4c0164d51b4e07f6). Two things follow. If a build must produce a checksum that someone else can reproduce, do not turn the archive options back on; and what remains of non-determinism is outside the archive settings: the JDK that compiled the classes (a toolchain pins it, see Gradle Toolchains), generated files that contain a timestamp or a host name, and the order in which a custom task writes its output. The same advice for Maven is in Maven Security & Reproducible Builds, and publishing is in Publishing.

Verify the wrapper

Two security practices concern the wrapper, which runs before any safeguard of your build: set distributionSha256Sum (added to the list in 9.1.0), and validate the wrapper on every upgrade (9.3.0). The JAR is a binary that is checked into the repository, so a change to it deserves the review you would give to any executable. Gradle publishes the checksum of the wrapper JAR and of each distribution next to the release (https://gradle.org/release-checksums/, and as plain text files at https://services.gradle.org/distributions/gradle-9.8.1-wrapper.jar.sha256 and ...-bin.zip.sha256). Compare the checksum of the checked-in JAR with the published one:

$ shasum -a 256 gradle/wrapper/gradle-wrapper.jar | cut -d' ' -f1
$ curl -sL https://services.gradle.org/distributions/gradle-9.8.1-wrapper.jar.sha256
3b8a25775a69158b5ad2b1d17a80a88dc7b40a352a73eb27d06c612a7ce68e98
3b8a25775a69158b5ad2b1d17a80a88dc7b40a352a73eb27d06c612a7ce68e98

The two values are equal, and equal to the one on the release-checksums page. The value of distributionSha256Sum in the starter configuration, dce76f55...c01f, is the one published for the -bin zip. The second check recommended by the official page is to regenerate the wrapper with a Gradle that you trust and compare the files. Here a Gradle 9.8.1 installation (not the project’s wrapper) regenerated the wrapper in an empty project with gradle wrapper --gradle-version 9.8.1 --gradle-distribution-sha256-sum <sum> --no-validate-url, and cmp found gradlew, gradlew.bat and gradle-wrapper.jar byte-identical to the ones in the project. The properties file differed only in distributionUrl, because the examples' wrapper downloads from a local file, not from services.gradle.org; the page shows the official address. In a pull request, a change to the JAR or to distributionUrl that has no matching Gradle upgrade is a red flag. For GitHub Actions the official setup action (v4 or newer) validates every gradle-wrapper.jar in the repository automatically (not run here); see CI & IDE Integration for the CI side.

The related practice is "Do not run ./gradlew on untrusted projects" (9.7.0): the wrapper script, the JAR and every build script execute arbitrary code, and opening a project in an IDE runs a Gradle sync that does the same. Read the build scripts and gradle-wrapper.properties first, make sure distributionUrl points to https://services.gradle.org/, regenerate the wrapper with a trusted installation, or work in a container or a virtual machine. The same applies to Maven’s wrapper and to Ant build files you downloaded; see Supply-Chain Security.

A note on adopting the list

Do not apply the list in one change. Adopt the cheap, mechanical items first (root project name, UTF-8, distributionSha256Sum, the plugins {} block), then the structural ones that touch many files (catalog, convention plugins), and use the deprecation warnings (--warning-mode all, see Command-Line Interface) and the configuration cache report to find the lazy-API and Project violations. The practice "Consider use of @Incubating APIs carefully" (9.7.0) cuts both ways: an incubating feature may change in a minor release, yet some of them, such as dependencyResolutionManagement.repositories, are recommended by the list itself. Check when a feature was introduced and how many open issues it has, as the official page suggests; the lifecycle labels are explained in Versions, Compatibility & Release History.

Troubleshooting

The Gradle troubleshooting guide is short; this section keeps its structure (installation, dependency resolution, slow builds, debugging build logic, tasks that run when they should be up to date, IDE integration, daemon connection) and adds the output you will actually see. Start by finding out where in the build the failure occurs, because the questions that follow differ.

flowchart TD A["The build fails"] --> B{"Does ./gradlew --version work?"} B -- no --> C["Installation: JAVA_HOME, execute permission, PATH, wrapper"] B -- yes --> D{"Does ./gradlew help work?"} D -- no --> E["A build script or the settings file is broken: read Where and What went wrong, add --stacktrace"] D -- yes --> F{"What does the failing task report?"} F -- "Could not resolve or Could not find" --> G["Dependency resolution: dependencyInsight, --refresh-dependencies, --offline"] F -- "A task runs when it should be UP-TO-DATE" --> H["--info prints why the task is not up to date"] F -- "The build is slow" --> I["--profile first, then a Build Scan"] F -- "A plugin or task misbehaves" --> J["Attach a debugger with org.gradle.debug"]

The reason help is a good probe is that it runs the initialization and configuration phases (every settings and build script) and then one trivial task. If help works, the scripts are fine and the problem is in the task that fails (see Build Lifecycle for the phases). If it fails, no task of yours has run yet.

Installation

./gradlew --version is the first thing to run (Kotlin DSL and Groovy DSL are irrelevant here). With the wrapper, this also downloads the distribution the first time.

$ ./gradlew --version

------------------------------------------------------------
Gradle 9.8.1
------------------------------------------------------------

Build time:    2026-10-07 16:57:46 UTC
Revision:      39daf39feb60a18b1b6ddccde53366522fef516f

Kotlin:        2.4.10
Groovy:        4.0.33
Ant:           Apache Ant(TM) version 1.10.17 compiled on April 6 2026
Launcher JVM:  26 (Oracle Corporation 26+35-2893)
Daemon JVM:    /Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home (no Daemon JVM specified, using current Java home)
OS:            Mac OS X 27.0.1 aarch64

Three failures at this point, from the guide:

  • Command not found: gradle. Gradle was installed but its bin directory is not on the PATH; or, for a project, use ./gradlew, which needs nothing installed (see Installation and Gradle Wrapper). Not run here.

  • JAVA_HOME is set to an invalid directory. The script checks the variable before starting anything:

    $ JAVA_HOME=/no/such/jdk ./gradlew --version
    
    ERROR: JAVA_HOME is set to an invalid directory: /no/such/jdk
    
    Please set the JAVA_HOME variable in your environment to match the
    location of your Java installation.

    Fix it by pointing JAVA_HOME at a JDK, or unsetting it. Gradle 9.8.1 needs Java 17 or newer to run the daemon (the wrapper client can still start on Java 8), but a project can still be compiled with other versions through toolchains, as in Gradle Toolchains.

  • Permission denied. The script has lost its executable bit (this happens when a file is committed from Windows or unpacked from a ZIP archive). On the system used here (zsh), with the bit removed by chmod -x gradlew:

    $ ./gradlew --version
    (eval):4: permission denied: ./gradlew

    The message belongs to the shell (zsh here; bash prints Permission denied). chmod +x gradlew fixes it, and git update-index --chmod=+x gradlew makes the bit permanent in the repository.

If --version works but every build fails with the same error, a configuration script is broken. ./gradlew help runs those scripts and no real task:

$ ./gradlew help
Calculating task graph as no cached configuration is available for tasks: help

> Task :help

Welcome to Gradle 9.8.1.

To run a build, run gradlew <task> ...

To see a list of available tasks, run gradlew tasks

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

To see a list of command-line options, run gradlew --help

For more detail on using Gradle, see https://docs.gradle.org/9.8.1/userguide/command_line_interface.html

For troubleshooting, visit https://help.gradle.org

BUILD SUCCESSFUL in 304ms
1 actionable task: 1 executed
Configuration cache entry stored.

A build script that does not work

Make a typing mistake in a project dependency (":tidepool-modle" instead of ":tidepool-model" in tidepool-engine/build.gradle.kts) and run help:

$ ./gradlew help
Calculating task graph as configuration cache cannot be reused because file 'tidepool-engine/build.gradle.kts' has changed.

FAILURE: Build failed with an exception.

* Where:
Build file '<work>/tidepool/tidepool-engine/build.gradle.kts' line: 6

* What went wrong:
Project with path ':tidepool-modle' could not be found.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.

BUILD FAILED in 13s
Configuration cache entry stored.

(One line, Starting a Gradle Daemon, 3 busy and 47 stopped Daemons could not be reused, was removed; it describes the other daemons of the machine.) Read it in this order: Where names the file and the line (the line of the project(...) call, not of the line that uses the result); What went wrong is the message; the Try: hints are generic. When the message is not enough, --stacktrace appends the exception and the full chain of calls (the first lines are shown here; the whole trace is about two hundred lines of internal frames):

$ ./gradlew help --stacktrace
...
* Exception is:
org.gradle.api.UnknownProjectException: Project with path ':tidepool-modle' could not be found.
	at org.gradle.api.internal.artifacts.DefaultProjectDependencyFactory.create(DefaultProjectDependencyFactory.java:73)
	at org.gradle.api.internal.artifacts.DefaultProjectDependencyFactory.create(DefaultProjectDependencyFactory.java:67)
	at org.gradle.api.internal.notations.ProjectDependencyFactory.create(ProjectDependencyFactory.java:41)
	at org.gradle.api.internal.artifacts.DefaultDependencyFactory.createProjectDependency(DefaultDependencyFactory.java:134)
...

The frames that matter are the first one from org.gradle.api (what failed) and the last ones before Program.execute (the build script frame, which names build.gradle.kts:6). Script compilation errors are worse: in the Kotlin DSL a typo in a block name (dependancies { ... }) reports Unresolved reference for the block and then for every call inside it, with the candidates of each overload, which can fill a screen. Look at the first error of the list only. The two DSLs are compared in Kotlin DSL and Groovy DSL & Migrating to Kotlin. In the Groovy DSL the same mistake gives the same Project with path ':tidepool-modle' could not be found message with Where: Build file '<work>/tidepool/tidepool-engine/build.gradle' line: 6, preceded by one more line in the What went wrong section, A problem occurred evaluating project ':tidepool-engine'.

Dependency resolution

Set the version of commons-csv in gradle/libs.versions.toml to 9.9.9, which does not exist, and compile. With the configuration cache on, the failure surfaces while Gradle stores the task graph and is wrapped in a long Configuration cache state could not be cached message that contains the same text; to read the real one, run with --no-configuration-cache:

$ ./gradlew :tidepool-engine:compileJava --no-configuration-cache
> Task :tidepool-model:compileJava UP-TO-DATE
> Task :tidepool-engine:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':tidepool-engine:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':tidepool-engine:compileClasspath'.
   > Could not find org.apache.commons:commons-csv:9.9.9.
     Searched in the following locations:
       - https://repo.maven.apache.org/maven2/org/apache/commons/commons-csv/9.9.9/commons-csv-9.9.9.pom
     Required by:
         project ':tidepool-engine'

* Try:
> If the artifact you are trying to retrieve can be found in the repository but without metadata in 'Maven POM' format, you need to adjust the 'metadataSources { ... }' of the repository declaration.
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.

BUILD FAILED in 2s
2 actionable tasks: 1 executed, 1 up-to-date

The message says what was asked (org.apache.commons:commons-csv:9.9.9), by whom (Required by: project ':tidepool-engine'), which configuration (compileClasspath), and where Gradle looked (a single .pom URL, because the build declares one repository). The causes are almost always one of: a typo in the version or the coordinates; a repository missing from dependencyResolutionManagement, or filtered out by content filtering (Repositories & Content Filtering); a snapshot that was never published; a proxy or credentials problem (Gradle prints the HTTP status in the message); or an artifact that exists without POM metadata, for which the Try: hint tells you to adjust metadataSources. The same build in offline mode shows the other side of the cache:

$ ./gradlew :tidepool-engine:compileJava --no-configuration-cache --offline
* What went wrong:
Execution failed for task ':tidepool-engine:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':tidepool-engine:compileClasspath'.
   > Could not resolve org.apache.commons:commons-csv:9.9.9.
     Required by:
         project ':tidepool-engine'
      > No cached version of org.apache.commons:commons-csv:9.9.9 available for offline mode.

* Try:

Offline mode uses only what is in the dependency cache; --refresh-dependencies forces the opposite (revalidate everything), and the cache, its expiry of dynamic versions and the lockfile and verification files are explained in Dependency Locking, Verification & Caching.

When the build resolves but picks a version you did not expect, ask Gradle why with dependencyInsight. This is the same build with the correct version (1.14.1):

$ ./gradlew :tidepool-engine:dependencyInsight --configuration runtimeClasspath --dependency commons-csv
> Task :tidepool-engine:dependencyInsight
org.apache.commons:commons-csv:1.14.1
  Variant runtime:
    | Attribute Name                 | Provided     | Requested    |
    |--------------------------------|--------------|--------------|
    | org.gradle.status              | release      |              |
    | org.gradle.category            | library      | library      |
    | org.gradle.libraryelements     | jar          | jar          |
    | org.gradle.usage               | java-runtime | java-runtime |
    | org.gradle.dependency.bundling |              | external     |
    | org.gradle.jvm.environment     |              | standard-jvm |
    | org.gradle.jvm.version         |              | 21           |

org.apache.commons:commons-csv:1.14.1
\--- runtimeClasspath

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

The first block shows the variant that was selected and the attributes the consumer requested (Java 21, jar, java-runtime) against what the module provided; the second is the reverse tree, from the dependency to the configuration that needs it. When there is a conflict, this report also lists the selection reasons (for example a conflict resolution, a constraint or a forced version), which is the quickest way to find the constraint, BOM, catalog or substitution rule that decided; the mechanisms are in Dependency Resolution, Conflicts & Capabilities, Resolution Rules & Substitution and Variant-Aware Resolution & Module Metadata. The official guide also points to the Dependencies view of a Build Scan, which offers the same information with a search box (publishing a scan was not run here). Maven’s equivalents (dependency:tree, -X) are in Maven Troubleshooting & Best Practices.

A task that runs when it should be up to date

A task is skipped (UP-TO-DATE) when the values of its inputs, the contents of its input files and the state of its outputs equal those of the last execution. When a task runs although you changed nothing, --info prints why it is not up to date. Two tasks demonstrate the usual causes. stampUnstable takes the current time as an input; stampStable takes the project version; noOutputs has an action and declares no output.

Kotlin DSL: build.gradle.kts
abstract class StampTask : DefaultTask() {
    @get:Input
    abstract val stamp: Property<String>

    @get:OutputFile
    abstract val stampFile: RegularFileProperty

    @TaskAction
    fun write() {
        stampFile.get().asFile.writeText(stamp.get())
    }
}

// Rerun trap 1: the input changes on every invocation.
tasks.register<StampTask>("stampUnstable") {
    stamp = System.currentTimeMillis().toString()
    stampFile = layout.buildDirectory.file("stamp-unstable.txt")
}

// The fix: derive the input from something that only changes when the project changes.
tasks.register<StampTask>("stampStable") {
    stamp = project.version.toString()
    stampFile = layout.buildDirectory.file("stamp-stable.txt")
}

// Rerun trap 2: an action but no declared outputs.
tasks.register("noOutputs") {
    doLast { println("working") }
}
Groovy DSL: build.gradle
abstract class StampTask extends DefaultTask {
    @Input
    abstract Property<String> getStamp()

    @OutputFile
    abstract RegularFileProperty getStampFile()

    @TaskAction
    void write() {
        stampFile.get().asFile.text = stamp.get()
    }
}

// Rerun trap 1: the input changes on every invocation.
tasks.register('stampUnstable', StampTask) {
    stamp = System.currentTimeMillis().toString()
    stampFile = layout.buildDirectory.file('stamp-unstable.txt')
}

// The fix: derive the input from something that only changes when the project changes.
tasks.register('stampStable', StampTask) {
    stamp = project.version.toString()
    stampFile = layout.buildDirectory.file('stamp-stable.txt')
}

// Rerun trap 2: an action but no declared outputs.
tasks.register('noOutputs') {
    doLast { println 'working' }
}

The build was run once to fill the outputs, and then again with --info, filtering the very long output down to the explanations. These runs used --no-configuration-cache: with the cache on, the value of System.currentTimeMillis() is computed once, at configuration time, and stored in the cache entry, so a cache hit would freeze the "unstable" input and hide the problem (which is a different, quieter trap).

$ ./gradlew stampUnstable stampStable noOutputs --no-configuration-cache --info | grep -A1 -E "is not up-to-date|^Skipping task" | grep -v -E "^--|^Resolve mutations"
Task ':stampUnstable' is not up-to-date because:
  Value of input property 'stamp' has changed for task ':stampUnstable'
Skipping task ':stampStable' as it is up-to-date.
Task ':noOutputs' is not up-to-date because:
  Task has not declared any outputs despite executing actions.

The output is identical with the Groovy DSL. The two reasons seen here are the most frequent in practice:

  • A value of an input property changed. Look for timestamps, random numbers, build numbers, host names or absolute paths among the inputs. Move such data out of the inputs, make it an output, or take it from a stable source. Absolute paths also defeat the build cache, which is why the list of practices asks for @PathSensitive declarations.

  • The task declares no output. A task with actions and no declared output is always considered out of date, because Gradle cannot know what the task changed. Declare the outputs (or, for a task that only reports, accept that it runs).

Other reasons are reported in the same place: an input file that was added, removed or changed; an output file that was changed or removed between runs (including by another task that writes to the same directory); and an explicit request to rerun (--rerun for one task, --rerun-tasks for all of them). The vocabulary and the inputs-and-outputs model are in Incremental Build; the meaning of the outcomes (UP-TO-DATE, FROM-CACHE, SKIPPED, NO-SOURCE) is in the command-line page. The Timeline view of a Build Scan gives the same explanation as a searchable page (not run here). A dry run shows what would run, in order, without running anything:

$ ./gradlew :tidepool-cli:jar -m
Calculating task graph as configuration cache cannot be reused because file 'build.gradle.kts' has changed.
:tidepool-model:compileJava SKIPPED
:tidepool-engine:compileJava SKIPPED
:tidepool-cli:compileJava SKIPPED
:tidepool-cli:processResources SKIPPED
:tidepool-cli:classes SKIPPED
:tidepool-cli:jar SKIPPED

BUILD SUCCESSFUL in 1s
Configuration cache entry stored.

Slow builds

The troubleshooting guide sends slow builds to the performance guide, which is the page Performance Profiling & Build Scans. A local first step that needs no account is --profile, which writes an HTML report with the time of each phase, each project and each task:

$ ./gradlew clean build --profile
...
Configuration cache entry stored.

See the profiling report at: file://<work>/tidepool/build/reports/profile/profile-2026-10-16-21-24-13.html
A fine-grained performance profile is available: use the --scan option.

The report is a file under build/reports/profile/. The scan option (--scan) is more detailed, but it publishes the build to an external service after you accept its terms of use, so it was not run here. Most of the time lost in practice comes from the practices of this page that are about performance: no configuration cache (configuration repeated on every invocation), no build cache, expensive computation in the configuration phase, dependency resolution during configuration (the practice "Don’t resolve Configurations before Task Execution"), and an undersized or oversized daemon heap (The Gradle Daemon). Build output as a stream of > Task ... lines is lifecycle logging; --console=verbose keeps outcomes such as UP-TO-DATE on the console in an interactive terminal.

Debugging build logic

Build logic (a script, buildSrc, a plugin of an included build) is code, and it can be debugged like code. Set the org.gradle.debug property to true and the build starts a JVM that suspends until a remote debugger attaches to port 5005 (org.gradle.debug.port changes the port; org.gradle.debug.host sets the interface, and * listens on all of them, which exposes the build to the network, so use it only when you must). The Kotlin DSL scripts can be debugged too, not only the plugin classes. This run used a Gradle User Home created for it (so that the shared daemons were not touched) and was ended after the line appeared, since no debugger was attached (an IDE attach was not run here):

$ ./gradlew help -Dorg.gradle.debug=true
...
Starting a Gradle Daemon (subsequent builds will be faster)
Listening for transport dt_socket at address: 5005

The JVM listens on 127.0.0.1:5005 only (confirmed with lsof in this run), and then waits. In an IDE, create a Remote JVM Debug configuration for that port and set breakpoints in the plugin or script source. The JVM stays suspended until a debugger attaches or you kill it (here the daemon and the client were ended by killing their processes).

For logs, raise the level (--info, --debug, --warning-mode all), and remember that the daemon writes its own log to $GRADLE_USER_HOME/daemon/<version>/daemon-<pid>.out.log. If the build fails before any task, with a message that a new daemon could not be connected to, the guide attributes it to network address translation (NAT) masquerading local connections as external ones, which Gradle refuses as a security precaution; the fix is in the network configuration, and the daemon log shows the detected network setup (not run here). Directories, log levels and the log format are in Directories & Logging.

Debugging IDE integration

Many rare IDE errors disappear when the IDE re-imports the build: in IntelliJ IDEA open the Gradle tool window and use the Refresh icon (the project must be linked to Gradle); in Eclipse with Buildship use the Refresh action of the "Gradle Tasks" view or the Gradle > Refresh Gradle Project command (both not run here). The IDE runs the same Gradle with the same wrapper, so a build that works on the command line and fails in the IDE usually differs in the JDK used for Gradle, in a Gradle User Home, or in the run configuration; see CI & IDE Integration and Init Scripts & the Tooling API.

Symptom, first command, where to read on

Symptom First thing to run Page

gradlew: command not found, Permission denied, JAVA_HOME is set to an invalid directory

./gradlew --version, chmod +x gradlew, check JAVA_HOME

Installation, Gradle Wrapper

Every build fails the same way, before any task

./gradlew help --stacktrace

Settings & Build Scripts, Build Lifecycle

Could not resolve ..., Could not find ...

--no-configuration-cache, dependencyInsight, --refresh-dependencies

Dependency Locking, Verification & Caching

The wrong version of a dependency wins

dependencyInsight --dependency <name>

Dependency Resolution, Conflicts & Capabilities

A task always runs

--info, then declare inputs and outputs

Incremental Build

invocation of 'Task.project' at execution time is unsupported

Read the configuration cache report; capture values at configuration time

Configuration Cache & Isolated Projects

The build is slow

--profile, then --scan

Performance Profiling & Build Scans

UnsupportedClassVersionError, wrong Java on the compile or test classpath

./gradlew javaToolchains, check the toolchain

Gradle Toolchains

A build breaks after an upgrade

--warning-mode all on the old version first

Upgrading Gradle

A plugin or task misbehaves

-Dorg.gradle.debug=true and a remote debugger

Writing Custom Tasks, Testing & Publishing Plugins

The same ideas in Ant and Maven

Need Gradle Maven Ant

Verbose output

--info, --debug, --stacktrace

-X, -e

-v, -d, -diagnostics

Dry run

-m (--dry-run)

none for the whole build (a plugin may offer one)

no dry-run option

Why did it run?

--info explains the up-to-date check

every goal runs; the incremental logic is in each plugin

a task decides by itself (timestamps in uptodate, copy, …​)

Why this dependency version?

dependencyInsight

dependency:tree -Dverbose

Ivy resolve output and reports, see the Ivy pages

Pin the toolchain

Java toolchains

maven-toolchains-plugin and maven.compiler.release

set JAVA_HOME

The Maven row is detailed in Maven Troubleshooting & Best Practices; the Ant options in Running Ant; the three tools side by side in Comparing Ant, Maven and Gradle.

What differs in Gradle 8.14

Not run here; the sources are the release notes and the upgrade guides.

  • The oldest entries of the best-practices list are marked 8.14; entries with later "added in" versions describe advice that the 8.14 documentation does not contain, though most of it applies to 8.x builds.

  • Archive tasks are reproducible by default only from 9.0.0 (the two properties above must be set by hand in 8.14).

  • Error messages gain the task provenance (registered in build file ...) in 9.5.0.

  • The Gradle daemon needs Java 8 to run in 8.14 and Java 17 in 9.0, so JAVA_HOME problems that mean "too old Java" are new with 9.

  • The configuration cache is stable since 8.1, so the Task.project failure above is the same in 8.14; what 9.x adds is the recommendation to use it by default (still not the default; announced for Gradle 10).

Where the book is outdated

The requester-provided book Gradle in Action (Muschko, 2014) was written for Gradle 1.7. Advice in it that the current best practices replace:

The book says Today

Share configuration with allprojects {} and subprojects {}

Convention plugins in buildSrc or, preferably, a build-logic included build (best practices for structuring builds)

Versions in ext properties

A version catalog (stable since 7.4)

Use the project freely at execution time; no configuration cache

The configuration cache is stable since 8.1; Task.project at execution time fails with it on

apply plugin: 'x'

The plugins {} block (general best practice)

sourceCompatibility and IDE settings for the JDK

Java toolchains

Groovy DSL only

Kotlin DSL is recommended for new builds and is the default of gradle init since 8.2

The daemon is opt-in (--daemon)

On by default since 3.0

Plugin tests with ProjectBuilder only

TestKit GradleRunner (a testing best practice)