Build Environment & Properties

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 2026-10-07, the current release on 2026-10-16). Every example was run through the Gradle 9.8.1 wrapper on JDK 26 (the JVM of the daemon, unless a section says it switched to another JDK) on 2026-10-16, on macOS, in scratch projects that mimic the tidepool build, and each example that differs by language was run in the Kotlin DSL and in the Groovy DSL. The examples use the configuration cache (--configuration-cache) unless the text says otherwise, and each one says whether it is compatible with it. In the scratch projects the wrapper pointed at a local copy of the same gradle-9.8.1-bin.zip instead of the services.gradle.org URL, to avoid a second download. Every run had its own Gradle User Home (GRADLE_USER_HOME pointing into the scratch directory), so the ~/.gradle of the machine was neither read nor changed, and the long scratch path is shortened to <work>. Gradle 8.14 was not available here: what differs in it is described from the official documentation and release notes in a section of its own and is labelled "not run here". Everything else that was not run is labelled "not run here" too.

Almost every Gradle behaviour that is not written in the build scripts is configured from outside them: how much memory the daemon gets, which JDK runs it, whether the build cache is on, what version a CI run is stamped with, which password publishes the artifacts. Gradle offers four channels for this, they interact, and the rules are spread over several pages of the manual. This page puts them in one place, shows which channel wins when two disagree by running the builds, and lists every org.gradle.* property. The commands behind the options are on Command-Line Interface; the daemon that the JVM settings apply to is on Gradle Daemon; what the Gradle User Home contains is on Directories & Logging; the scripts that read these values are on Settings & Build Scripts; and the wrapper that picks the Gradle version is on Gradle Wrapper.

Four kinds of settings and why they are kept apart

A Gradle build is configured by values of four different kinds, and mixing them up is the root of most surprises on this topic.

Kind What it is Set with Read with

Project property

Data for the build scripts: a version, a flag, a credential. Visible on the Project object.

-Pname=value, ORG_GRADLE_PROJECT_name, -Dorg.gradle.project.name, gradle.properties

providers.gradleProperty("name"), findProperty("name"), property("name"), extra

System property

A property of the JVM that runs the build, as for any Java program.

-Dname=value, systemProp.name=value in gradle.properties, GRADLE_OPTS, JAVA_OPTS

providers.systemProperty("name"), System.getProperty("name")

Gradle property

A setting of Gradle itself, named org.gradle.*: parallelism, caching, the daemon’s JVM, logging.

-Dorg.gradle.name=value (or a dedicated option such as --parallel), gradle.properties

Not meant to be read by build logic

Environment variable

A variable of the process that starts Gradle.

The shell or the CI job

providers.environmentVariable("NAME")

The separation is deliberate. Project properties are build inputs and should be small, typed on the way in and declared; Gradle properties change how the tool behaves and the manual says they should not be read in build scripts; system properties belong to the JVM; environment variables are the lowest-common-denominator channel for CI and containers. Equivalents exist in the other two tools of this section, and the map is in the next table.

Need Ant Maven Gradle

A value for the build from the command line

-Dname=value, immutable once set

-Dname=value (a user property)

-Pname=value (project property) or -Dname=value (system property)

Shared defaults in the repository

a .properties file loaded with the property task

.mvn/maven.config, <properties> in the POM

gradle.properties in the root directory

Per-user or per-machine settings

ANT_OPTS, ANT_ARGS, files read by the launcher script

~/.m2/settings.xml, MAVEN_OPTS, MAVEN_ARGS

gradle.properties in the Gradle User Home, GRADLE_OPTS

JVM options of the build tool

ANT_OPTS

.mvn/jvm.config, MAVEN_OPTS

org.gradle.jvmargs for the daemon, GRADLE_OPTS for the client

Credentials

properties files outside the repository

<server> in settings.xml, optionally encrypted

credentials(PasswordCredentials) with properties in the Gradle User Home or the environment

The Ant side is on Properties and Running Ant; the Maven side is on Configuring Maven and settings.xml & Password Encryption. The three tools are compared on Comparing Ant, Maven & Gradle.

Where the values live: the gradle.properties files

A gradle.properties file is the common home of the first three kinds in the table above. The same file can hold all of them, told apart by their names:

# Gradle properties: configure Gradle itself
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx1g -Dfile.encoding=UTF-8

# System properties: the prefix is stripped and the JVM property is set
systemProp.https.protocols=TLSv1.2,TLSv1.3

# Project properties: everything else
kotlin.code.style=official
tidepool.release.channel=stable

(This file is an illustration of the three name shapes; the runs below use smaller files that are shown in each transcript.) Gradle looks for the file in three places:

Place Meaning Typical content

The root directory of the build

Committed with the project, shared by every developer and CI

org.gradle.* flags that every build needs (parallel, caching, daemon memory), project properties such as the group and version

The Gradle User Home (GRADLE_USER_HOME, ~/.gradle by default)

Per user or per machine, never committed

A personal JDK path, a proxy, credentials, a bigger heap on a big machine

The Gradle installation directory (GRADLE_HOME)

Per installation, rarely used

Defaults for everybody who runs that installation

With the wrapper there is no separate installation: the "installation directory" is the unpacked distribution under GRADLE_USER_HOME/wrapper/dists/gradle-9.8.1-bin/<hash>/gradle-9.8.1, and a gradle.properties placed there is read too (the first transcript below does exactly that). When the same name appears in more than one place, the Gradle User Home wins over the project, and the project wins over the installation. The manual’s table for this has two rows that are easy to misread, so the order was verified by running the builds in the sections below; it also matches the upgrade note of Gradle 9.4.0, which says the precedence of system properties passed to the wrapper was corrected so that the command line wins over the Gradle User Home, which wins over the project directory.

The file format has two classic traps

A gradle.properties file is a Java properties file, and two of its rules bite. The first is the character set: the bytes of the file are read as ISO-8859-1, so a UTF-8 file with accented letters turns into mojibake, and the fix is a Unicode escape. The second is the backslash, which is an escape character, so a Windows path needs doubled backslashes (or forward slashes). The probe is a task that prints one property; the first run has the UTF-8 file, the second one has escapes.

Kotlin DSL: build.gradle.kts
tasks.register("pitfalls") {
    val label = providers.gradleProperty("label").orElse("<not set>")
    val verbose = providers.gradleProperty("verbose").map { it.toBoolean() }.orElse(false)
    doLast {
        println("label   = ${label.get()}")
        println("verbose = ${verbose.get()}")
    }
}
Groovy DSL: build.gradle
tasks.register('pitfalls') {
    def label = providers.gradleProperty('label').orElse('<not set>')
    def verbose = providers.gradleProperty('verbose').map { it.toBoolean() }.orElse(false)
    doLast {
        println "label   = ${label.get()}"
        println "verbose = ${verbose.get()}"
    }
}
$ cat gradle.properties
flag=false
label=Café São
verbose=false

$ ./gradlew pitfalls -q
label   = Café São
verbose = false

$ cat gradle.properties
flag=false
label=Caf\u00e9 S\u00e3o
verbose=false

$ ./gradlew pitfalls -q
label   = Café São
verbose = false

$ cat gradle.properties
label=C:\temp\new

$ ./gradlew pitfalls -q | od -c | head -3
0000000    l   a   b   e   l               =       C   :  \t   e   m   p
0000020   \n   e   w  \n   v   e   r   b   o   s   e       =       f   a
0000040    l   s   e  \n

$ cat gradle.properties
label=C:\\temp\\new

$ ./gradlew pitfalls -q
label   = C:\temp\new
verbose = false

The Groovy build printed the same lines. The third file has C:\temp\new written with single backslashes, and the od -c output shows what Gradle made of it: \t became a tab and \n became a newline, so the value is C:, a tab, emp, a newline and ew. With doubled backslashes the value is intact. Properties that hold paths on Windows are best written with forward slashes, which Java accepts everywhere. Both builds are compatible with the configuration cache (they read the properties lazily with providers.gradleProperty).

What the best practices say about the files

The manual’s best practices give three rules. Set the build flags that everybody needs in the gradle.properties of the root project and commit it, instead of relying on command-line options for every run: options on the command line are for short-term testing and are easy to forget or to apply inconsistently. Do not put a gradle.properties in a subproject: the manual says the support is inconsistent, that Gradle itself and popular plugins do not reliably handle it, and that values get scattered; use the subproject’s build script or a convention plugin instead (see Convention Plugins & Sharing Build Logic). And properties set this way are not inherited across build boundaries when composite builds are used (see Composite Builds), so an included build needs its own gradle.properties.

Project properties

Project properties are the data channel. A project property is a string that the build scripts can read, and there are four ways to set one, from the highest precedence to the lowest:

Precedence Way Example

1

Command line

-PmyProperty=value

2

System property with the org.gradle.project. prefix, on the command line

-Dorg.gradle.project.myProperty=value

3

Environment variable with the ORG_GRADLE_PROJECT_ prefix

ORG_GRADLE_PROJECT_myProperty=value

4

A gradle.properties file: Gradle User Home, then the project directories, then the installation directory

myProperty=value

Reading a property

The recommended way to read one is providers.gradleProperty("name"). It returns a lazy Provider<String>, it is compatible with the configuration cache, and it looks only in the build-level sources of the table above: -P, the org.gradle.project.* system properties, ORG_GRADLE_PROJECT_* and the gradle.properties files of the Gradle User Home, the build root and the installation. The older dynamic API (findProperty, property, hasProperty) searches the project model instead, in this order: the project’s own properties, its extra properties (where Gradle puts every Gradle property that is not a project property), an extension of that name, a task of that name, and, deprecated in Gradle 9.6, the extra properties and extensions of the parent projects. findProperty returns null when nothing matches, property throws and hasProperty returns false. The probe has two tasks' worth of reads: one through the provider and one through findProperty.

Kotlin DSL: build.gradle.kts
// Project property, read lazily: the recommended way
val viaProvider = providers.gradleProperty("myProperty").orElse("<not set>")
// Project property, read through the dynamic Project lookup while configuring
val viaFind = findProperty("myProperty")?.toString() ?: "<not set>"

tasks.register("showProperty") {
    val provider = viaProvider
    val found = viaFind
    doLast {
        println("providers.gradleProperty(\"myProperty\") = ${provider.get()}")
        println("project.findProperty(\"myProperty\")    = $found")
    }
}
Groovy DSL: build.gradle
// Project property, read lazily: the recommended way
def viaProvider = providers.gradleProperty('myProperty').orElse('<not set>')
// Project property, read through the dynamic Project lookup while configuring
def viaFind = findProperty('myProperty')?.toString() ?: '<not set>'

tasks.register('showProperty') {
    def provider = viaProvider
    def found = viaFind
    doLast {
        println "providers.gradleProperty(\"myProperty\") = ${provider.get()}"
        println "project.findProperty(\"myProperty\")    = ${found}"
    }
}

Both are compatible with the configuration cache: the task captures a Provider and a String, not the Project. To see the precedence, the property myProperty is defined in one more place at each step, from the lowest source to the highest. The Gradle User Home is the guh directory of the scratch project (GRADLE_USER_HOME), and <installation> is the unpacked 9.8.1 distribution under its wrapper/dists.

$ ./gradlew showProperty --configuration-cache -q
providers.gradleProperty("myProperty") = <not set>
project.findProperty("myProperty")    = <not set>

$ cat <installation>/gradle.properties
myProperty=installation

$ ./gradlew showProperty --configuration-cache -q
providers.gradleProperty("myProperty") = installation
project.findProperty("myProperty")    = installation

$ cat gradle.properties
myProperty=project

$ ./gradlew showProperty --configuration-cache -q
providers.gradleProperty("myProperty") = project
project.findProperty("myProperty")    = project

$ cat $GRADLE_USER_HOME/gradle.properties
myProperty=user-home

$ ./gradlew showProperty --configuration-cache -q
providers.gradleProperty("myProperty") = user-home
project.findProperty("myProperty")    = user-home

$ ORG_GRADLE_PROJECT_myProperty=env ./gradlew showProperty --configuration-cache -q
providers.gradleProperty("myProperty") = env
project.findProperty("myProperty")    = env

$ ORG_GRADLE_PROJECT_myProperty=env ./gradlew showProperty --configuration-cache -q -Dorg.gradle.project.myProperty=sysprop
providers.gradleProperty("myProperty") = sysprop
project.findProperty("myProperty")    = sysprop

$ ORG_GRADLE_PROJECT_myProperty=env ./gradlew showProperty --configuration-cache -q -Dorg.gradle.project.myProperty=sysprop -PmyProperty=cli
providers.gradleProperty("myProperty") = cli
project.findProperty("myProperty")    = cli

Each step shows the new source winning over everything defined before it: the installation file, then the project file, then the Gradle User Home file, then the environment variable, then -Dorg.gradle.project.myProperty and finally -P. The Groovy DSL project printed identical lines at every step. The file location that the manual calls "current project directory" matters only for subprojects, which the multi-project example further down shows. Because the configuration cache was on in every run, the transcript also shows that a changed property is noticed: each run printed a new value, because providers.gradleProperty is tracked as an input of the cache entry.

Properties are strings, and absent is not empty

Whatever the source, a project property is a String. Three consequences:

  • A flag has to be converted: providers.gradleProperty("verbose").map { it.toBoolean() }.orElse(false). The conversion is done once, at the edge.

  • A property given as -Plabel with no value is the empty string, not "absent": isPresent is true and the value is empty.

  • In the Groovy DSL, if (verbose) is true for the string "false", because any non-empty string is true in Groovy. This is the classic bug of builds that gate behaviour on a property.

The probe was extended with a task that findProperty`s `verbose while gradle.properties says verbose=false (Groovy DSL only, since that is where the truthiness rule applies), and pitfalls was run with -Plabel and no value:

Kotlin DSL: build.gradle.kts
tasks.register("pitfalls") {
    val label = providers.gradleProperty("label").orElse("<not set>")
    val verbose = providers.gradleProperty("verbose").map { it.toBoolean() }.orElse(false)
    doLast {
        println("label   = ${label.get()}")
        println("verbose = ${verbose.get()}")
    }
}
Groovy DSL: build.gradle
tasks.register('pitfalls') {
    def label = providers.gradleProperty('label').orElse('<not set>')
    def verbose = providers.gradleProperty('verbose').map { it.toBoolean() }.orElse(false)
    doLast {
        println "label   = ${label.get()}"
        println "verbose = ${verbose.get()}"
    }
}

// The classic Groovy trap: a Gradle property is always a String
tasks.register('truthy') {
    def verbose = findProperty('verbose')
    doLast {
        println "verbose is ${verbose.getClass().simpleName} '${verbose}'"
        if (verbose) { println "if (verbose) is true even though the file says false" }
        println "'false'.toBoolean() = ${verbose.toBoolean()}"
    }
}
$ cat gradle.properties
verbose=false

$ ./gradlew truthy -q
verbose is String 'false'
if (verbose) is true even though the file says false
'false'.toBoolean() = false
$ ./gradlew pitfalls -q -Plabel
label   =
verbose = false

A missing property

property("name") throws when no source defines the name, and the failure happens while the build is configured. The requireFlag task of the probe reads project.property("flag") with no flag defined anywhere:

Kotlin DSL: build.gradle.kts
tasks.register("requireFlag") {
    // property(name) fails when the property does not exist anywhere
    val flag = project.property("flag").toString()
    doLast { println("flag = $flag") }
}
Groovy DSL: build.gradle
tasks.register('requireFlag') {
    // property(name) fails when the property does not exist anywhere
    def flag = project.property('flag').toString()
    doLast { println "flag = ${flag}" }
}
$ ./gradlew requireFlag -q 2>&1 | head -7

FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring root project 'pitfalls'.
> Could not create task ':requireFlag'.
   > Could not get unknown property 'flag' for root project 'pitfalls' of type org.gradle.api.Project.

(The Groovy DSL project failed the same way: its message lines are identical, and its failure output adds a * Where: block with the build file and line, whose path is the scratch path.) The same read through the provider is lazy and has a natural default, which is why providers.gradleProperty("flag").orElse("default") is the safe form and the build never fails on a missing optional property.

A property that must be there: gating a release on CI

The manual’s example gates a release task on a property that only the CI sets. It also shows the three equivalent ways to supply it:

Kotlin DSL: build.gradle.kts
tasks.register("performRelease") {
    val isCI = providers.gradleProperty("isCI")
    doLast {
        if (isCI.isPresent) {
            println("Performing release actions")
        } else {
            throw InvalidUserDataException("Cannot perform release outside of CI")
        }
    }
}
Groovy DSL: build.gradle
tasks.register('performRelease') {
    def isCI = providers.gradleProperty('isCI')
    doLast {
        if (isCI.present) {
            println "Performing release actions"
        } else {
            throw new InvalidUserDataException("Cannot perform release outside of CI")
        }
    }
}
$ ./gradlew performRelease -q 2>&1 | head -6

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':performRelease' (registered in build file 'build.gradle.kts').
> Cannot perform release outside of CI

$ ./gradlew performRelease -q -PisCI=true
Performing release actions

$ ORG_GRADLE_PROJECT_isCI=true ./gradlew performRelease -q
Performing release actions

The failure is a custom exception from the task action, shown by Gradle as the cause of the failed task. In the Groovy run the same lines were printed, preceded by the * Where: block. On CI the environment variable is the natural channel because it is not written to a file and needs no change to the command line.

Subprojects, extra properties and the deprecated parent lookup

A multi-project build adds two questions: which gradle.properties files count, and what a subproject sees of its parent. The probe build has a root project, an app and a lib. The root gradle.properties defines shared, onlyRoot and a system property; app/gradle.properties redefines shared and defines another system property; the root build script puts fromRootExt in its extra properties (extra["fromRootExt"] in the Kotlin DSL, ext.fromRootExt in the Groovy DSL). The app and lib scripts only read:

Kotlin DSL: app/build.gradle.kts
tasks.register("whoAmI") {
    val viaProvider = providers.gradleProperty("shared").orElse("<not set>")
    val viaFind = findProperty("shared")?.toString() ?: "<not set>"
    val onlyRoot = findProperty("onlyRoot")?.toString() ?: "<not set>"
    val fromExt = findProperty("fromRootExt")?.toString() ?: "<not set>"
    val root = providers.systemProperty("tide.root").orElse("<not set>")
    val sub = providers.systemProperty("tide.sub").orElse("<not set>")
    doLast {
        println("app: providers.gradleProperty(shared) = ${viaProvider.get()}")
        println("app: findProperty(shared)             = $viaFind")
        println("app: findProperty(onlyRoot)           = $onlyRoot")
        println("app: findProperty(fromRootExt)        = $fromExt")
        println("app: systemProp tide.root             = ${root.get()}")
        println("app: systemProp tide.sub              = ${sub.get()}")
    }
}
Groovy DSL: app/build.gradle
tasks.register('whoAmI') {
    def viaProvider = providers.gradleProperty('shared').orElse('<not set>')
    def viaFind = findProperty('shared')?.toString() ?: '<not set>'
    def onlyRoot = findProperty('onlyRoot')?.toString() ?: '<not set>'
    def fromExt = findProperty('fromRootExt')?.toString() ?: '<not set>'
    def root = providers.systemProperty('tide.root').orElse('<not set>')
    def sub = providers.systemProperty('tide.sub').orElse('<not set>')
    doLast {
        println "app: providers.gradleProperty(shared) = ${viaProvider.get()}"
        println "app: findProperty(shared)             = ${viaFind}"
        println "app: findProperty(onlyRoot)           = ${onlyRoot}"
        println "app: findProperty(fromRootExt)        = ${fromExt}"
        println "app: systemProp tide.root             = ${root.get()}"
        println "app: systemProp tide.sub              = ${sub.get()}"
    }
}
$ cat gradle.properties
shared=from-root-file
onlyRoot=root-only
systemProp.tide.root=root-system-property

$ cat app/gradle.properties
shared=from-app-file
systemProp.tide.sub=sub-system-property

$ ./gradlew :app:whoAmI :lib:whoAmI -q
app: providers.gradleProperty(shared) = from-root-file
app: findProperty(shared)             = from-app-file
app: findProperty(onlyRoot)           = root-only
app: findProperty(fromRootExt)        = root-ext-value
app: systemProp tide.root             = root-system-property
app: systemProp tide.sub              = <not set>
lib: findProperty(shared) = from-root-file

$ ./gradlew :app:whoAmI --warning-mode all --console=plain 2>&1 | grep "^Implicit"
Implicit lookup of properties in parent projects has been deprecated. This will fail with an error in Gradle 10. Property 'fromRootExt' was not declared in project ':app' and was resolved from root project 'multi'. This lookup was initiated by 'findProperty()'. Consult the upgrading guide for further information: https://docs.gradle.org/9.8.1/userguide/upgrading_version_9.html#deprecated_implicit_lookup_in_parent_projects

The Groovy DSL project printed identical lines (its deprecation line is the same; only the stack frame under it differs). What the transcript shows:

  • providers.gradleProperty ignores the gradle.properties of the subproject, as the upgrade guide says, and returns the root value; findProperty in app finds the subproject file first, and in lib, which has no file of its own, finds the root file. Two ways to read the same name gave two answers, which is the practical reason the manual advises against subproject gradle.properties.

  • The systemProp. line of the subproject file was ignored: only the root project’s gradle.properties is checked for systemProp. entries.

  • findProperty("fromRootExt") in app resolved a value that the root project declared in its extra properties. That implicit walk up the parent projects is deprecated since Gradle 9.6 and removed in Gradle 10; the build still passed and printed the warning, which is the first line of the second command. Reading a property defined by gradle.properties (onlyRoot) did not warn, because it is a build-level property.

The migration the guide recommends is to put build-wide values in the root gradle.properties and read them with providers.gradleProperty, to extract shared logic into a convention plugin, and, if a value really must be read from another project, to say so explicitly (rootProject.extra, or the parent’s extra). The multi-project build itself is on Multi Project Builds. The extra properties (extra["name"] in the Kotlin DSL, ext.name in the Groovy DSL) are also the local, typed place to keep computed values, and are covered on Settings & Build Scripts. The example is not compatible with Isolated Projects (cross-project reads are what that feature forbids), and it is compatible with the plain configuration cache.

The Kotlin delegate by project is deprecated

For years the idiom of the Kotlin DSL was val name: String by project. Gradle 9.6 deprecated the property delegates (by project, by settings, by extra) together with the container delegates such as by registering, and scheduled their removal for Gradle 10. The probe has a child project that reads a property the root declared, through the delegate in the Kotlin DSL and through a bare name in the Groovy DSL (also a parent lookup):

Kotlin DSL: child/build.gradle.kts
// Deprecated since Gradle 9.6: property delegate, and it also walks up to the parent project
val greeting: String? by project

tasks.register("greet") {
    val text = greeting
    doLast { println("greeting = $text") }
}
Groovy DSL: child/build.gradle
// A bare name is resolved dynamically, walking up to the parent project: deprecated since Gradle 9.6
tasks.register('greet') {
    def text = greeting
    doLast { println "greeting = ${text}" }
}
$ ./gradlew :child:greet -q
greeting = hello from the root

$ ./gradlew :child:greet --warning-mode all --console=plain 2>&1 | grep -E "^(The .val name|Implicit)"
The 'val name: Type? by project' property delegate syntax has been deprecated. This is scheduled to be removed in Gradle 10. Use 'val property = project.findProperty(name)' instead. Consult the upgrading guide for further information: https://docs.gradle.org/9.8.1/userguide/upgrading_version_9.html#kotlin_dsl_delegated_properties
Implicit lookup of properties in parent projects has been deprecated. This will fail with an error in Gradle 10. Property 'greeting' was not declared in project ':child' and was resolved from root project 'delegate'. This lookup was initiated by 'val ... by project'. Consult the upgrading guide for further information: https://docs.gradle.org/9.8.1/userguide/upgrading_version_9.html#deprecated_implicit_lookup_in_parent_projects

The Groovy DSL run printed the same value and, with --warning-mode all, the second warning only. The replacement for a property that must exist is project.property("name"); for one that may be missing, providers.gradleProperty("name") or findProperty("name"). When a group of properties has to be forwarded (for example every property that starts with test. to the test task as system properties), the upgrade guide recommends providers.gradlePropertiesPrefixedBy("test."), which returns a map provider. That API was not run here.

System properties

System properties are properties of the JVM that runs the build. You set them with -Dname=value on the command line, with systemProp.name=value in a gradle.properties file, or as -D options in GRADLE_OPTS or JAVA_OPTS. The command-line form has the same effect as -D on the java command, and the build reads the value with providers.systemProperty("name") (lazy, tracked by the configuration cache) or System.getProperty("name"). The probe task prints tide.region:

Kotlin DSL: build.gradle.kts
tasks.register("showSystemProperty") {
    val region = providers.systemProperty("tide.region").orElse("<not set>")
    val opts = providers.systemProperty("probe.opts").orElse("<not set>")
    doLast {
        println("tide.region = ${region.get()}")
        println("probe.opts  = ${opts.get()}")
    }
}
Groovy DSL: build.gradle
tasks.register('showSystemProperty') {
    def region = providers.systemProperty('tide.region').orElse('<not set>')
    def opts = providers.systemProperty('probe.opts').orElse('<not set>')
    doLast {
        println "tide.region = ${region.get()}"
        println "probe.opts  = ${opts.get()}"
    }
}
$ ./gradlew showSystemProperty -q --configuration-cache
tide.region = <not set>
probe.opts  = <not set>

$ ./gradlew showSystemProperty -q --configuration-cache -Dtide.region=Atlantic
tide.region = Atlantic
probe.opts  = <not set>

$ cat gradle.properties
systemProp.tide.region=from-file

$ ./gradlew showSystemProperty -q --configuration-cache
tide.region = from-file
probe.opts  = <not set>

$ ./gradlew showSystemProperty -q --configuration-cache -Dtide.region=Atlantic
tide.region = Atlantic
probe.opts  = <not set>

$ cat $GRADLE_USER_HOME/gradle.properties
systemProp.tide.region=from-user-home

$ ./gradlew showSystemProperty -q --configuration-cache
tide.region = from-user-home
probe.opts  = <not set>

The -D option beats the file, and the Gradle User Home file wins over the project file (in the last step both files existed, with from-file in the project and from-user-home in the user home). The order of system properties is therefore: command line, Gradle User Home gradle.properties, project gradle.properties. The Groovy DSL project printed identical lines, and both builds are compatible with the configuration cache. The manual’s own examples of useful system properties are gradle.wrapperUser and gradle.wrapperPassword (credentials to download the distribution, see Gradle Wrapper), gradle.user.home (see below) and https.protocols (for example TLSv1.2,TLSv1.3). In a multi-project build only the root project’s gradle.properties is checked for systemProp. entries, as the previous section showed.

Gradle properties: the org.gradle.* settings

The org.gradle.* properties configure Gradle itself. You set them with -Dorg.gradle.name=value on the command line or in a gradle.properties file, and most also have a dedicated command-line option (--parallel, --max-workers, --warning-mode). The manual’s precedence for them is: the command line first, then the gradle.properties of the Gradle User Home, then the one in the build root directory, then the installation directory. The dedicated option beats the equivalent -D. The manual also says their values should not be retrieved in build scripts, because they are settings of the tool, not inputs of the build. To observe them anyway, the probe reads Gradle’s startParameter once, for this demonstration:

Kotlin DSL: build.gradle.kts
// Reads Gradle's own settings: for the demonstration only, see the text
val workers = gradle.startParameter.maxWorkerCount
val parallel = gradle.startParameter.isParallelProjectExecutionEnabled
val caching = gradle.startParameter.isBuildCacheEnabled
val logLevel = gradle.startParameter.logLevel
val warningMode = gradle.startParameter.warningMode

tasks.register("showGradleProperties") {
    val summary = "workers.max=$workers, parallel=$parallel, caching=$caching, " +
        "logging.level=$logLevel, warning.mode=$warningMode"
    doLast { println(summary) }
}
Groovy DSL: build.gradle
// Reads Gradle's own settings: for the demonstration only, see the text
def workers = gradle.startParameter.maxWorkerCount
def parallel = gradle.startParameter.parallelProjectExecutionEnabled
def caching = gradle.startParameter.buildCacheEnabled
def logLevel = gradle.startParameter.logLevel
def warningMode = gradle.startParameter.warningMode

tasks.register('showGradleProperties') {
    def summary = "workers.max=${workers}, parallel=${parallel}, caching=${caching}, " +
        "logging.level=${logLevel}, warning.mode=${warningMode}"
    doLast { println summary }
}
$ ./gradlew showGradleProperties --no-configuration-cache --console=plain | grep '^workers'
workers.max=10, parallel=false, caching=false, logging.level=LIFECYCLE, warning.mode=Summary

$ cat gradle.properties
org.gradle.workers.max=3
org.gradle.caching=true

$ ./gradlew showGradleProperties --no-configuration-cache --console=plain | grep '^workers'
workers.max=3, parallel=false, caching=true, logging.level=LIFECYCLE, warning.mode=Summary

$ cat $GRADLE_USER_HOME/gradle.properties
org.gradle.workers.max=2
org.gradle.parallel=true

$ ./gradlew showGradleProperties --no-configuration-cache --console=plain | grep '^workers'
workers.max=2, parallel=true, caching=true, logging.level=LIFECYCLE, warning.mode=Summary

$ ./gradlew showGradleProperties --no-configuration-cache --console=plain -Dorg.gradle.workers.max=5 -Dorg.gradle.logging.level=info -Dorg.gradle.warning.mode=all | grep '^workers'
workers.max=5, parallel=true, caching=true, logging.level=INFO, warning.mode=All

$ ./gradlew showGradleProperties --no-configuration-cache --console=plain --max-workers=6 --no-parallel -Dorg.gradle.workers.max=5 | grep '^workers'
workers.max=6, parallel=false, caching=true, logging.level=LIFECYCLE, warning.mode=Summary

The five runs show: the defaults (10 is the number of CPU cores of this machine, the default for org.gradle.workers.max), the project file, the Gradle User Home file winning on the keys they share (workers.max=2 over 3; caching still comes from the project file and parallel from the user-home file, which shows that the files are merged key by key), -D over both, and finally the dedicated options: --max-workers=6 beat -Dorg.gradle.workers.max=5, and --no-parallel beat org.gradle.parallel=true. The Groovy DSL project printed identical lines. These runs used --no-configuration-cache, on purpose: a build that reads startParameter while configuring is not a good citizen. Run with the cache on, it stored an entry, and then did not notice that org.gradle.workers.max had changed:

$ cat gradle.properties
org.gradle.workers.max=3

$ ./gradlew showGradleProperties --configuration-cache --console=plain | grep -E '^workers|Reusing|entry'
workers.max=3, parallel=false, caching=false, logging.level=LIFECYCLE, warning.mode=Summary
Configuration cache entry stored.

$ cat gradle.properties
org.gradle.workers.max=2

$ ./gradlew showGradleProperties --configuration-cache --console=plain | grep -E '^workers|Reusing|entry'
Reusing configuration cache.
workers.max=3, parallel=false, caching=false, logging.level=LIFECYCLE, warning.mode=Summary
Configuration cache entry reused.

The second run reused the cache entry and printed the old value 3 although gradle.properties now says 2. This is why build logic should not copy Gradle’s own settings into its tasks; the settings take effect where Gradle reads them. (The cached value is the one captured at configuration time; the real setting of the second run was 2.)

The org.gradle.* reference

The reference below is the manual’s list for 9.8.1, with the command-line option that does the same where there is one (from ./gradlew --help of 9.8.1). The column "Run here" says which properties were exercised by a run on this page; the others are described from the manual and are labelled "not run here". "Incubating" means the feature may still change.

Property Values Default Effect Option / run here

org.gradle.caching

true, false

false

Reuse task outputs from earlier builds (see Build Cache).

--build-cache, --no-build-cache; run

org.gradle.caching.debug

true, false

false

Log the input hashes and the cache key of each task.

not run here

org.gradle.configuration-cache

true, false

false

Reuse the configuration of earlier builds (see Configuration Cache & Isolated Projects).

--configuration-cache, --no-configuration-cache; run through the option

org.gradle.configureondemand

true, false

false

Configure only the projects that are needed (incubating).

--configure-on-demand; not run here

org.gradle.console

auto, plain, colored, rich, verbose

depends on how Gradle is invoked

Console colouring and verbosity.

--console; run through the option

org.gradle.console.interactive

true, false

true

false stops Gradle prompting the user and uses default answers; useful for CI, scripts and AI agents (incubating).

--non-interactive, 9.6; not run here

org.gradle.continue

true, false

false

Continue running tasks after a task failure.

--continue, --no-continue; not run here

org.gradle.daemon

true, false

true

Use the Gradle daemon.

--daemon, --no-daemon; run

org.gradle.daemon.idletimeout

milliseconds

10800000 (3 hours)

Idle time after which the daemon terminates itself.

not run here

org.gradle.debug

true, false

false

Run the build with remote debugging on port 5005 (equivalent to adding the jdwp agent) and suspend until a debugger attaches.

not run here

org.gradle.java.home

path of a JDK or JRE

derived from JAVA_HOME or the java on the path

JVM that runs the build (the daemon). Does not change the JVM of the client.

run

org.gradle.java.installations.auto-detect

true, false

true

Detect the JDKs installed on the machine when locating toolchains (see Toolchains).

not run here

org.gradle.java.installations.auto-download

true, false

true

Download JDKs automatically when locating toolchains.

not run here

org.gradle.java.installations.paths

comma-separated list of directories

empty

Extra directories that hold JDK installations for toolchains.

not run here

org.gradle.java.installations.fromEnv

comma-separated list of variable names

empty

Environment variables that point to JDK installations for toolchains.

not run here

org.gradle.jvmargs

JVM arguments

-Xmx512m plus a metaspace limit, as the manual states; the observed daemon showed -Xmx512m

JVM arguments of the daemon. Does not change the client JVM.

run

org.gradle.logging.level

quiet, warn, lifecycle, info, debug

lifecycle

Log level; values are not case-sensitive.

-q, -w, -i, -d; run

org.gradle.parallel

true, false

false

Run projects in parallel with up to org.gradle.workers.max workers.

--parallel, --no-parallel; run

org.gradle.priority

low, normal

normal

Scheduling priority of the daemon and the processes it starts.

--priority; not run here

org.gradle.projectcachedir

directory

.gradle in the root project directory

Project-specific cache directory.

--project-cache-dir; not run here

org.gradle.problems.report

true, false

true

Generate build/reports/problems-report.html from the Problems API.

--problems-report, --no-problems-report (incubating); the report was generated by the runs above, for example the Problems report is available at line

org.gradle.tooling.parallel

true, false

the value of org.gradle.parallel (since 9.4.0)

Build Tooling API models (for IDE sync) in parallel; set it explicitly, because relying on the default is deprecated since 9.8.

not run here

org.gradle.isolated-projects

true, false

false

Isolated Projects (since 9.7.0, incubating): configure projects in parallel; implies the configuration cache.

--isolated-projects; not run here

org.gradle.isolated-projects.diagnostics

true, false

false

Configure projects sequentially to report all violations deterministically (since 9.7.0). Requires Isolated Projects.

not run here

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

true, false

false

Report violations without failing, to time a parallel build before fixing them (since 9.7.0); outputs may be incorrect.

not run here

org.gradle.vfs.verbose

true, false

false

Verbose logging when watching the file system.

not run here

org.gradle.vfs.watch

true, false

true where supported

Watch the file system and reuse what was collected between builds.

--watch-fs, --no-watch-fs; not run here

org.gradle.warning.mode

all, fail, summary, none

summary

How warnings (deprecations) are shown.

--warning-mode; run

org.gradle.workers.max

number

the number of CPU processors

Maximum number of workers.

--max-workers; run

org.gradle.mirror.maven.settings

true, false

false

Reuse the mirrors of Maven’s settings.xml (since 9.8.0, incubating); see below.

not run here

The Isolated Projects properties were renamed in Gradle 9.7: the old names org.gradle.unsafe.isolated-projects* are deprecated and still accepted as aliases, and when both names are set an explicit false in either disables the feature. For the full list of options of the command line see Command-Line Interface.

Reusing Maven mirrors (Gradle 9.8, incubating)

Organisations that already point Maven at an internal repository manager through a <mirror> in settings.xml can ask Gradle to honour the same mirrors with org.gradle.mirror.maven.settings=true. It is a Gradle property, so it is read from gradle.properties or from -Dorg.gradle.mirror.maven.settings=true, and -P has no effect. Gradle then reads the mirrors from the user and the global settings.xml, matches each HTTP or HTTPS Maven repository against them by repository name (Maven Central matches the name central, the Gradle Plugin Portal the name Gradle Central Plugin Repository), takes the credentials from the <server> entry with the mirror’s id, and resolves from the mirror. Publishing is not affected. Ivy repositories, mavenLocal, flat directories and S3 or GCS repositories are not mirrored. This was not run here, because the mirrors are read from the real ~/.m2/settings.xml of the user, which the examples of this section never touch. The Maven side of the same file is on Repositories, Mirrors & Repository Managers, and the repository declarations are on the Gradle repositories page of this section.

The JVM that runs Gradle

Three JVMs can be involved in a build, and the settings above address different ones. The client is the small JVM started by gradlew or gradle; it parses the command line, finds or starts a daemon and relays the console. The daemon is a separate, long-lived JVM that runs the build. Toolchains (see Toolchains) are a third, independent choice: the JDK that compiles and tests the code, which need not be the one that runs Gradle.

Setting Applies to Notes

JAVA_HOME

the client, and the daemon unless it is overridden

Must be a JDK 17 or newer for Gradle 9

org.gradle.java.home

the daemon only

Command line or gradle.properties; a JDK is safer than a JRE

Daemon JVM criteria (gradle/gradle-daemon-jvm.properties)

the daemon only

Stable since Gradle 9.2; described on Gradle Daemon

org.gradle.jvmargs

the daemon only

Heap and any other JVM option of the daemon

GRADLE_OPTS, JAVA_OPTS

the client; a -D system property given there still reaches the build, including -Dorg.gradle.jvmargs

Rarely needed

JAVA_HOME and org.gradle.java.home

With JAVA_HOME pointing at a JDK 21 (a JetBrains Runtime 21.0.10 on this machine) the daemon ran on 21; -Dorg.gradle.java.home on the command line and the same property in gradle.properties both beat JAVA_HOME. The probe prints the version of the JVM the build runs in:

Kotlin DSL: build.gradle.kts
tasks.register("showJvm") {
    doLast {
        val args = java.lang.management.ManagementFactory.getRuntimeMXBean().inputArguments
        println("java.version   = ${System.getProperty("java.version")}")
        println("max heap (MiB) = ${Runtime.getRuntime().maxMemory() / 1024 / 1024}")
        println("daemon -Xmx    = ${args.filter { it.startsWith("-Xmx") }}")
    }
}
Groovy DSL: build.gradle
tasks.register('showJvm') {
    doLast {
        def args = java.lang.management.ManagementFactory.runtimeMXBean.inputArguments
        println "java.version   = ${System.getProperty('java.version')}"
        println "max heap (MiB) = ${Runtime.runtime.maxMemory().intdiv(1024 * 1024)}"
        println "daemon -Xmx    = ${args.findAll { it.startsWith('-Xmx') }}"
    }
}
$ JAVA_HOME=$JDK21 ./gradlew showJvm -q
java.version   = 21.0.10
max heap (MiB) = 512
daemon -Xmx    = [-Xmx512m]

$ JAVA_HOME=$JDK21 ./gradlew showJvm -q -Dorg.gradle.java.home=$JDK17
java.version   = 17.0.14
max heap (MiB) = 512
daemon -Xmx    = [-Xmx512m]

$ cat gradle.properties
org.gradle.java.home=<JDK 17 home>

$ ./gradlew showJvm -q
java.version   = 17.0.14
max heap (MiB) = 512
daemon -Xmx    = [-Xmx512m]

$ JAVA_HOME=$JDK21 ./gradlew showJvm -q
java.version   = 17.0.14
max heap (MiB) = 512
daemon -Xmx    = [-Xmx512m]

$ cat gradle.properties
org.gradle.java.home=/no/such/jdk

$ ./gradlew showJvm -q 2>&1 | head -5

FAILURE: Build failed with an exception.

* What went wrong:
Value '/no/such/jdk' given for org.gradle.java.home Gradle property is invalid (Java home supplied is invalid)

In the transcript $JDK21 and $JDK17 are the home directories of two JDKs installed on the machine (a gradle.properties shows <JDK 17 home> in place of the path). The Groovy DSL project printed identical lines, and both builds are compatible with the configuration cache. Notice that a wrong org.gradle.java.home fails fast with a clear message. A JVM older than 17 is not accepted by Gradle 9 (the minimum moved from 8 to 17 in Gradle 9.0, see Versions, Compatibility & Release History); the older JDKs are reached through toolchains. That rejection was not run here, because no JDK older than 17 is used on this page.

org.gradle.jvmargs, GRADLE_OPTS and JAVA_OPTS

The manual is explicit that org.gradle.jvmargs configures the daemon and not the client, and that GRADLE_OPTS configures the client. The daemon’s heap is therefore a gradle.properties matter. Two runs of the probe above show it, and a third changes org.gradle.jvmargs for one run, which makes the daemon incompatible with the running one so that Gradle starts a new daemon:

$ ./gradlew showJvm -q
java.version   = 26
max heap (MiB) = 512
daemon -Xmx    = [-Xmx512m]

$ cat gradle.properties
org.gradle.jvmargs=-Xmx700m

$ ./gradlew showJvm -q
java.version   = 26
max heap (MiB) = 704
daemon -Xmx    = [-Xmx700m]

$ ./gradlew showJvm --console=plain -Dorg.gradle.jvmargs=-Xmx800m 2>&1 | grep -E "Starting|max heap|daemon -Xmx"
Starting a Gradle Daemon, 2 incompatible and 50 stopped Daemons could not be reused, use --status for details
max heap (MiB) = 800
daemon -Xmx    = [-Xmx800m]

The default daemon heap was 512 MiB. org.gradle.jvmargs=-Xmx700m gave 704 MiB as seen by the JVM, and -Dorg.gradle.jvmargs=-Xmx800m on the command line gave a third daemon (the message lists how many daemons could not be reused; the counts depend on the history of the Gradle User Home, which here had many runs behind it). The Groovy DSL project printed the same values. Memory and the reuse rules are on Gradle Daemon. Now GRADLE_OPTS: a heap option does nothing for the build, while a -D system property reaches it. JAVA_OPTS behaves the same way:

$ GRADLE_OPTS="-Dprobe.opts=gradle_opts -Xmx64m" ./gradlew showSystemProperty showJvm -q --configuration-cache
tide.region = <not set>
probe.opts  = gradle_opts
java.version   = 26
max heap (MiB) = 512
daemon -Xmx    = [-Xmx512m]

$ JAVA_OPTS="-Dprobe.opts=java_opts" ./gradlew showSystemProperty -q --configuration-cache
tide.region = <not set>
probe.opts  = java_opts

The -Xmx64m of GRADLE_OPTS left the daemon at 512 MiB, and -Dprobe.opts=gradle_opts arrived as a system property of the build. The launcher script builds the client command line from its defaults, then JAVA_OPTS, then GRADLE_OPTS (see Installation). The Groovy DSL project printed identical lines. The manual points out that GRADLE_OPTS can still carry daemon options if it sets the org.gradle.jvmargs system property, which is the way to do it from an environment variable:

$ GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx600m" ./gradlew showJvm -q
java.version   = 26
max heap (MiB) = 600
daemon -Xmx    = [-Xmx600m]

The daemon got 600 MiB. The Groovy DSL project printed the same lines. For a project the recommended place for daemon options is still org.gradle.jvmargs in gradle.properties, which also keeps them in version control.

Turning the daemon off

org.gradle.daemon=false (or --no-daemon) runs the build in a single-use daemon process that Gradle forks and stops at the end of the build. The message explains it:

$ cat gradle.properties
org.gradle.daemon=false

$ ./gradlew showJvm --console=plain 2>&1 | head -4
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.8.1/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build

> Task :showJvm

Environment variables

The manual lists the environment variables that the gradle command understands. Gradle also turns variables named ORG_GRADLE_PROJECT_* into project properties, as shown above.

Variable Purpose Run here

GRADLE_HOME

Installation directory of Gradle; can be used to select a local Gradle instead of the wrapper. A gradle.properties in it is read as the lowest-precedence file.

the file, yes

JAVA_HOME

JDK of the client VM, which is also the daemon’s unless org.gradle.java.home or the daemon JVM criteria say otherwise.

yes

JAVA_OPTS

JVM options and custom settings for the JVM; the manual’s example passes memory and -Dfile.encoding options.

yes

GRADLE_OPTS

JVM arguments of the Gradle client VM. The client is lightweight, so changing its options is uncommon; the daemon’s options are set with the org.gradle.jvmargs property.

yes

GRADLE_USER_HOME

The Gradle User Home: global properties, init scripts, caches, logs. Defaults to USER_HOME/.gradle.

yes

GRADLE_DAEMON_BIND_ADDRESS

Address or host name to which the local sockets between client and daemon are bound; set it when automatic detection fails (for example with several network interfaces).

not run here

GRADLE_LIBS_REPO_OVERRIDE

Overrides Gradle’s default library repository, to point at an internal repository behind a firewall or proxy.

not run here

NO_COLOR

When present and not empty, disables colour output and keeps the other styling and the rich features (progress bars).

not run here

Choosing the Gradle User Home

GRADLE_USER_HOME is the usual way to move it; it can also be given as the system property gradle.user.home or as the option --gradle-user-home (-g). The probe prints the name of the directory Gradle chose:

Kotlin DSL: build.gradle.kts
tasks.register("showUserHome") {
    val home = gradle.gradleUserHomeDir.name
    doLast { println("Gradle user home directory name = $home") }
}
Groovy DSL: build.gradle
tasks.register('showUserHome') {
    def home = gradle.gradleUserHomeDir.name
    doLast { println "Gradle user home directory name = ${home}" }
}
$ ./gradlew showUserHome -q --configuration-cache
Gradle user home directory name = guh

$ ./gradlew showUserHome -q -Dgradle.user.home=$OTHER_HOME
Gradle user home directory name = other-home

$ ./gradlew showUserHome -q -g $OTHER_HOME
Gradle user home directory name = other-home

$OTHER_HOME is a second directory in the scratch workspace. The Groovy DSL project printed the same names, and both builds are compatible with the configuration cache. Moving the Gradle User Home is the standard recipe for CI jobs that cache the dependencies, and for the isolated runs of this page (see CI & IDE Integration).

Reading the environment in a build, and the configuration cache

A build script can read an environment variable with providers.environmentVariable("NAME") or with System.getenv("NAME"). Both are recorded as inputs of the configuration cache entry, so the cache stays correct. The difference is what happens when the value changes: a value read while the build is being configured invalidates the entry and the whole configuration runs again, while a provider that is only resolved when the task runs lets the same entry be reused. The probe registers one task of each kind (for brevity the dev stage is the default):

Kotlin DSL: build.gradle.kts
tasks.register("env") {
    // read while configuring, so the values are inputs of the configuration cache entry
    val stage = providers.environmentVariable("DEPLOY_STAGE").orElse("dev").get()
    val label = providers.gradleProperty("label").orElse("<not set>").get()
    doLast { println("stage=$stage label=$label") }
}

tasks.register("envLazy") {
    // read while executing: the cache entry is reused and the new value still arrives
    val stage = providers.environmentVariable("DEPLOY_STAGE").orElse("dev")
    val label = providers.gradleProperty("label").orElse("<not set>")
    doLast { println("stage=${stage.get()} label=${label.get()}") }
}
Groovy DSL: build.gradle
tasks.register('env') {
    // read while configuring, so the values are inputs of the configuration cache entry
    def stage = providers.environmentVariable('DEPLOY_STAGE').orElse('dev').get()
    def label = providers.gradleProperty('label').orElse('<not set>').get()
    doLast { println "stage=${stage} label=${label}" }
}

tasks.register('envLazy') {
    // read while executing: the cache entry is reused and the new value still arrives
    def stage = providers.environmentVariable('DEPLOY_STAGE').orElse('dev')
    def label = providers.gradleProperty('label').orElse('<not set>')
    doLast { println "stage=${stage.get()} label=${label.get()}" }
}
$ DEPLOY_STAGE=dev ./gradlew env --configuration-cache --console=plain 2>&1 | grep -E "stage=|Calculating|Reusing|entry"
Calculating task graph as no cached configuration is available for tasks: env
stage=dev label=<not set>
Configuration cache entry stored.

$ DEPLOY_STAGE=dev ./gradlew env --configuration-cache --console=plain 2>&1 | grep -E "stage=|Calculating|Reusing|entry"
Reusing configuration cache.
stage=dev label=<not set>
Configuration cache entry reused.

$ DEPLOY_STAGE=prod ./gradlew env --configuration-cache --console=plain 2>&1 | grep -E "stage=|Calculating|Reusing|entry"
Calculating task graph as configuration cache cannot be reused because environment variable 'DEPLOY_STAGE' has changed.
stage=prod label=<not set>
Configuration cache entry stored.

$ DEPLOY_STAGE=prod ./gradlew env -Plabel=x --configuration-cache --console=plain 2>&1 | grep -E "stage=|Calculating|Reusing|entry"
Calculating task graph as configuration cache cannot be reused because Gradle property 'label' has changed.
stage=prod label=x
Configuration cache entry stored.

$ DEPLOY_STAGE=dev ./gradlew envLazy --configuration-cache --console=plain 2>&1 | grep -E "stage=|Calculating|Reusing|entry"
Calculating task graph as no cached configuration is available for tasks: envLazy
stage=dev label=<not set>
Configuration cache entry stored.

$ DEPLOY_STAGE=prod ./gradlew envLazy --configuration-cache --console=plain 2>&1 | grep -E "stage=|Calculating|Reusing|entry"
Reusing configuration cache.
stage=prod label=<not set>
Configuration cache entry reused.

$ DEPLOY_STAGE=prod ./gradlew envLazy -Plabel=y --configuration-cache --console=plain 2>&1 | grep -E "stage=|Calculating|Reusing|entry"
Reusing configuration cache.
stage=prod label=y
Configuration cache entry reused.

The Groovy DSL project printed the same sequences. env reads while configuring: a new DEPLOY_STAGE or a new -Plabel was named as the reason the cache could not be reused, and the configuration ran again. envLazy reads when it executes: the entry made on the first run was reused with prod and with -Plabel=y, and the new values still arrived. Both forms are compatible with the configuration cache. Prefer the lazy form for values that change from run to run, such as a build number, which also keeps CI builds fast.

Keeping secrets out of the repository

Passwords and tokens must not be in the build scripts or in a gradle.properties that is committed. Gradle gives a mechanism that avoids it: a repository can declare which kind of credentials it needs and Gradle looks the values up in the Gradle properties, with the names derived from the repository’s name (<name>Username and <name>Password, and <name>AccessKey, <name>SecretKey and <name>SessionToken for AWS credentials). The lookup uses every project-property channel, so the same declaration works with a file in the Gradle User Home on a developer machine and with environment variables on CI. Credentials are required only when a task that needs them is part of the build, and Gradle checks for them before it runs any task. The example publishes with maven-publish to a repository named internal on localhost, where nothing listens, so no credential leaves the machine:

Kotlin DSL: build.gradle.kts
plugins {
    `java-library`
    `maven-publish`
}

group = "io.tidepool"
version = "1.0.0"

publishing {
    publications {
        create<MavenPublication>("lib") { from(components["java"]) }
    }
    repositories {
        maven {
            name = "internal"
            url = uri("http://localhost:8081/repository/releases/")
            isAllowInsecureProtocol = true
            // reads the Gradle properties internalUsername and internalPassword
            credentials(PasswordCredentials::class)
        }
    }
}
Groovy DSL: build.gradle
plugins {
    id 'java-library'
    id 'maven-publish'
}

group = 'io.tidepool'
version = '1.0.0'

publishing {
    publications {
        lib(MavenPublication) { from components.java }
    }
    repositories {
        maven {
            name = 'internal'
            url = 'http://localhost:8081/repository/releases/'
            allowInsecureProtocol = true
            // reads the Gradle properties internalUsername and internalPassword
            credentials(PasswordCredentials)
        }
    }
}
$ ./gradlew publishLibPublicationToInternalRepository -q 2>&1 | head -8

FAILURE: Build failed with an exception.

* What went wrong:
Credentials required for this build could not be resolved.
> The following Gradle properties are missing for 'internal' credentials:
    - internalUsername
    - internalPassword

$ ORG_GRADLE_PROJECT_internalUsername=deploy ORG_GRADLE_PROJECT_internalPassword='${PLACEHOLDER}' ./gradlew publishLibPublicationToInternalRepository -q 2>&1 | head -8

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':publishLibPublicationToInternalRepository' (registered by plugin 'org.gradle.maven-publish').
> Failed to publish publication 'lib' to repository 'internal'
   > Could not write to resource 'http://localhost:8081/repository/releases/io/tidepool/secrets/1.0.0/secrets-1.0.0.jar'.
      > Connect to localhost:8081 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused

The first run failed before any task ran, naming the two missing properties. In the second run the properties came from ORG_GRADLE_PROJECT_internalUsername and ORG_GRADLE_PROJECT_internalPassword (the password is a placeholder), the credentials were found, and the publish failed only because there is no server on localhost:8081. The Groovy DSL project produced the same two messages (with a * Where: block on the first). Both builds are compatible with the configuration cache. Publishing to a real repository, to Maven Central or to the Gradle Plugin Portal was not run here; the keys for the portal (gradle.publish.key and gradle.publish.secret) are Gradle properties too, and follow the same rules.

Where to put the values:

  • On a developer machine, in gradle.properties of the Gradle User Home, with permissions only for the user; never in the project directory, and never in a file that is committed (a .gitignore entry for a local gradle.properties does not protect a committed one).

  • On CI, in ORG_GRADLE_PROJECT_name variables set from the CI system’s secret store. The manual notes this is typically the preferred method for secrets in unattended builds.

  • Not with -P on the command line: the value is visible in the process list and in the shell history, and CI logs often echo command lines.

  • Not in build.gradle(.kts) and not in gradle.properties of the repository.

The Maven equivalent, with an encrypted master password, is on settings.xml & Password Encryption; the cross-tool view of repositories and publishing is on Repositories & Publishing Across Tools, and the supply-chain side (verifying what you download, protecting what you publish) is on Supply Chain Security.

Gradle 8.14 and Gradle 9.8.1 (not run here)

Gradle 8.14 was not available here, so this table comes from the official upgrade notes and release notes of 9.x and was not run. Where a row says that 8.14 "accepts" something, it means the deprecation or change appeared in the Gradle 9.x release named in the row.

Topic Gradle 8.14 (not run here) Gradle 9.8.1 (run)

JVM that runs Gradle

JAVA_HOME and org.gradle.java.home can be a JDK older than 17

Must be 17 or newer (the minimum changed in 9.0); older JDKs only through toolchains

val name: String by project and the other Kotlin property delegates

Accepted without a warning

Deprecated in 9.6, removal in Gradle 10

Property lookup in parent projects (findProperty, property, hasProperty, bare names)

Accepted without a warning

Deprecated in 9.6, an error in Gradle 10

System property priority for the wrapper

gradle.properties files could override -D options passed to the wrapper

Fixed in 9.4.0: the command line beats the Gradle User Home, which beats the project

org.gradle.tooling.parallel

Not available

Introduced in 9.4; leaving it unset while org.gradle.parallel is on is deprecated since 9.8

--non-interactive and org.gradle.console.interactive

Not available

--non-interactive since 9.6 (incubating)

Isolated Projects properties

Not available

org.gradle.isolated-projects* since 9.7 (the org.gradle.unsafe.* names are deprecated aliases)

org.gradle.mirror.maven.settings

Not available

Since 9.8 (incubating)

NO_COLOR

Not available

Honoured since 9.6

Where Gradle in Action (Gradle 1.7) is out of date

Gradle in Action (Muschko, 2014) was written for Gradle 1.7. Its chapters on installation, project properties and gradle.properties are still a good introduction to the vocabulary, but several practices changed:

The book’s practice Today Source

Install Gradle, set GRADLE_HOME and put it on the PATH

Run builds with the wrapper; GRADLE_HOME is needed only for a manual installation

Gradle Wrapper

The daemon is opt-in (--daemon)

On by default since Gradle 3.0

Gradle 3.0 release notes

A JDK 1.5 or newer runs Gradle

A JVM 17 or newer since Gradle 9.0; older JDKs through toolchains

Compatibility Matrix

Library versions as ext properties in the build script

Version catalogs (stable in 7.4); gradle.properties for build-wide values

Version catalogs

Reading ext properties of the parent project by their bare name in subprojects

Deprecated in 9.6, removed in Gradle 10; declare the property in the project or use providers.gradleProperty

Upgrading within Gradle 9.x.y

Common problems

Symptom Cause Fix

Could not get unknown property 'flag' for root project 'pitfalls' of type org.gradle.api.Project.

property("flag") or a bare name when nothing defines it

Define it in a gradle.properties or with -P, or use providers.gradleProperty("flag").orElse(...)

Value '/no/such/jdk' given for org.gradle.java.home Gradle property is invalid (Java home supplied is invalid)

The path is wrong (often a path committed from another machine)

Put machine-specific JDK paths in the Gradle User Home, not in the project

Credentials required for this build could not be resolved.

The repository asks for PasswordCredentials and the <name>Username and <name>Password properties are missing

Set them in the Gradle User Home or as ORG_GRADLE_PROJECT_name variables

label = Café São

UTF-8 characters in gradle.properties

Use Unicode escapes (\u00e9)

A path such as C:\temp\new has tabs and newlines in it

The backslash is an escape in properties files

Double the backslashes or use forward slashes

The Groovy build takes the true branch for verbose=false

The value is the non-empty string false

Convert it with toBoolean()

Implicit lookup of properties in parent projects has been deprecated.

findProperty, property, a bare name or by project resolved a value from a parent project

Declare the property in the project, or read the root’s with providers.gradleProperty

A systemProp. entry in a subproject is ignored

Only the root project’s gradle.properties is checked

Move it to the root file

The workers value printed by a cached build never changes

Gradle’s own settings were copied into the build at configuration time

Do not read org.gradle.* in build logic

To honour the JVM settings for this build a single-use Daemon process will be forked.

org.gradle.daemon=false (or the build needs JVM settings the running daemon lacks)

Remove the property, or accept the cost

Starting a Gradle Daemon, 2 incompatible and N stopped Daemons could not be reused

org.gradle.jvmargs or org.gradle.java.home changed

Keep the values stable per project, in gradle.properties

Summary

  • Gradle has four configuration channels: project properties (-P, ORG_GRADLE_PROJECT_*, gradle.properties), system properties (-D, systemProp.), Gradle properties (org.gradle.*) and environment variables. The command line wins, then the Gradle User Home file, the project file and the installation file.

  • The project gradle.properties is the committed place for flags every build needs; the Gradle User Home gradle.properties is the personal place for JDK paths, proxies and credentials; subproject gradle.properties files are discouraged.

  • Read project properties with providers.gradleProperty, which is lazy and compatible with the configuration cache. All property values are strings, an absent property is not an empty one, and property() fails on a missing name. The Kotlin by project delegate and the walk up to parent projects are deprecated since 9.6.

  • org.gradle.jvmargs and org.gradle.java.home configure the daemon; GRADLE_OPTS and JAVA_OPTS configure the client (their -D properties still reach the build); JAVA_HOME is the default for both. Gradle 9 needs a JDK 17 or newer.

  • GRADLE_USER_HOME, -Dgradle.user.home and -g move the Gradle User Home. An environment variable read with providers.environmentVariable does not invalidate the configuration cache when it changes only if the provider is resolved when the task runs; a value resolved while the build is configured (with get() in the task registration, or with System.getenv in the build script) is an input of the cache entry, and a new value forces a new configuration.

  • Secrets belong in the Gradle User Home or in ORG_GRADLE_PROJECT_* variables, declared in the build only as credentials(PasswordCredentials); not in the repository and not on the command line.