Gradle Wrapper
|
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) and its wrapper. Gradle 8.14.6, the last 8.x release,
differs in a few places; those are described from the official release notes and labelled "not run here", because Gradle 8.14 was not available. Every
other example was run on JDK 26 on 2026-10-16, on macOS, in scratch directories that mimic the tidepool build, with the Gradle 9.8.1 wrapper and the
installed Gradle 9.8.1 of this series. Two practical deviations explain the output you will see. First, the wrapper properties shown on this page are
the official ones, with the services.gradle.org URL; in the scratch directories that had to run ./gradlew, distributionUrl pointed at a local
copy of the same gradle-9.8.1-bin.zip (a file: URL) to avoid a second download, and that is why the first-run output shows a file: URL. Second, the
long absolute path of the scratch directory is shortened to <scratch>. For the authenticated and retried downloads the same zip was served by a tiny
HTTP server on localhost, so no credential or token left the machine, and the values used (ci-reader, demo-password, demo-token) are
placeholders. The Gradle User Home was redirected into the scratch directory for the runs that show downloads, so ~/.gradle was not touched.
Everything that was not run is labelled "not run here".
The wrapper is the way to run a Gradle build: a script in the project that starts "the Gradle this project asks for", downloading it first if needed.
How to install Gradle itself, which you need once to create the wrapper, is on
Installation; the options that ./gradlew forwards to Gradle are on
Command-Line Interface; the Gradle User Home that holds the cached
distributions is on Directories & Logging; and the core vocabulary
(build, project, task) is on Core Concepts.
What the wrapper does for a team
The Gradle documentation recommends running every build through the wrapper. The wrapper is not a separate download: it is generated by the built-in wrapper
task and consists of two scripts (gradlew for Unix shells, gradlew.bat for Windows), a small JAR with the download code and a properties file that names the
Gradle distribution. A developer or a CI job runs ./gradlew build instead of gradle build; if the distribution named in the properties file is not in the
Gradle User Home yet, the JAR downloads and unpacks it, and every later call reuses the unpacked copy. The benefits the manual lists are:
-
the project is standardised on one Gradle version, which makes builds more reliable and robust;
-
moving a project to another Gradle version is one change to the wrapper definition, reviewed like any other commit;
-
IDEs and CI servers get the same Gradle as developers do, with the same one-line change.
Two more consequences matter in practice. An old branch builds with the Gradle it was written for, even after the team moved on, which is how the
removals of Gradle 9 (see
Versions, Compatibility & Release History) stay
a decision per branch instead of a surprise. And a new colleague needs a JDK and nothing else: there is no "install Gradle X and put it on the PATH" step in the
README. The wrapper does not remove the need for a JDK, and it cannot be created without a Gradle: the first gradle wrapper needs an installed Gradle (or
gradle init, which also generates the wrapper) once.
The same idea exists in Maven as the Maven Wrapper (mvnw, see Maven Wrapper); Ant has
no wrapper, its installation is a directory you unpack (see Ant Installation). The
comparison table at the end of this page lines up the three.
How a wrapper call runs
./gradlew is a shell script. It finds a Java (JAVA_HOME if set, otherwise java on the PATH), then starts it with the wrapper JAR. The JAR reads
gradle/wrapper/gradle-wrapper.properties, looks for the distribution in the Gradle User Home, downloads and verifies it if absent, and finally starts the
Gradle launcher of that distribution, which connects to a daemon or starts one (see
Gradle Daemon).
The script itself is short (248 lines for Unix, 112 for Windows in 9.8.1). Its documented knobs are environment variables: JAVA_HOME selects the Java
(a wrong value stops the script before anything else, see Error messages), and JAVA_OPTS and GRADLE_OPTS add JVM options, appended to the default
-Xmx64m -Xms64m of the small wrapper JVM. The variables Gradle itself reads are on
Build Environment & Properties. The JAR is 48,749 bytes in 9.8.1 and
contains the classes of org.gradle.wrapper (GradleWrapperMain, Download, Install, WrapperExecutor and a few helpers). The wrapper clients still run on
a very old JVM (JVM 8 for the wrapper, see the compatibility page), while Gradle 9 needs a JVM 17 or newer to run the build; with a JDK 17 in JAVA_HOME the same wrapper reported:
Launcher JVM: 17.0.14 (JetBrains s.r.o. 17.0.14+1-b1367.22)
Daemon JVM: /Users/.../jbr-17.0.14/Contents/Home (no Daemon JVM specified, using current Java home)
(The home directory is replaced by ... in the second line.) Which JVM runs the daemon, and how a project can declare it, is covered with the daemon and the
toolchains.
Adding the wrapper
Every Gradle build has a built-in wrapper task in the "Build Setup" group. Run it on the root project (the colon prefix targets the root project, the only
place where the task is relevant, and avoids configuring other projects with configure on demand or Isolated Projects). In an empty project, with the installed Gradle 9.8.1:
gradle :wrapper
> Task :wrapper
BUILD SUCCESSFUL in 341ms
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html
The task wrote four files; the listing also shows the settings.gradle.kts that already existed:
./gradle/wrapper/gradle-wrapper.jar
./gradle/wrapper/gradle-wrapper.properties
./gradlew
./gradlew.bat
./settings.gradle.kts
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
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
By default the task writes the version of the Gradle that runs it and the -bin distribution. gradle init generates the same files as part of a new project (the
generated .gitattributes and .gitignore are described under Files to commit), and the Gradle properties file carries no distributionSha256Sum unless you
ask for one (see Verifying the distribution and the wrapper JAR). In a multi-project build the wrapper files live next to the root settings.gradle(.kts), so
that ../../gradlew tasks from a subproject directory finds the same scripts; the scripts find their own JAR, so they can be called from any directory.
The options of the wrapper task
gradle help --task wrapper lists what the task accepts from the command line:
gradle -q help --task wrapper
Detailed task information for wrapper
Path
:wrapper (registered by plugin 'org.gradle.wrapper')
Type
Wrapper (org.gradle.api.tasks.wrapper.Wrapper)
Options
--distribution-type The type of the Gradle distribution to be used by the wrapper.
Available values are:
ALL
BIN
--gradle-distribution-sha256-sum The SHA-256 hash sum of the gradle distribution.
--gradle-distribution-url The URL to download the Gradle distribution from.
--gradle-version The version of the Gradle distribution required by the wrapper. The following labels are allowed: latest, release-candidate, release-milestone, release-nightly, and nightly.
--network-timeout Timeout in ms to use when the wrapper is performing network operations.
--retries The number of download retries.
--retry-back-off-ms The initial back off in milliseconds between retries (doubles on each failure).
--validate-url Sets task to validate the configured distribution url.
--no-validate-url Disables option --validate-url.
--rerun Causes the task to be re-run even if up-to-date.
Description
Generates Gradle wrapper files.
Group
Build Setup
| Option | Meaning |
|---|---|
|
The Gradle version the wrapper downloads and runs. The resulting URL is validated before it is written. Since Gradle 9.0 a major or a minor version alone is accepted (see below). The labels |
|
|
|
The complete URL of a distribution ZIP. It makes |
|
The SHA-256 the downloaded ZIP must have. |
|
The timeout in milliseconds for the wrapper’s network operations; the default is 10000. |
|
Validate (the default) or skip the validation of the distribution URL. |
|
The number of download retries (default 0, disabled) and the initial back-off (default 500 ms, doubling after each failure). Since Gradle 9.5. |
The validation sends a HEAD request for an https URL and checks that the file exists for a file: URL. It exists since Gradle 8.2; before it, a typo in the
version surfaced as an I/O exception in the middle of the next build. With the options combined, to fix the version, the checksum, a longer timeout and retries:
gradle :wrapper --gradle-version 9.8.1 \
--gradle-distribution-sha256-sum dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f \
--network-timeout 30000 --retries 3 --retry-back-off-ms 1000
> Task :wrapper
BUILD SUCCESSFUL in 729ms
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=30000
retries=3
retryBackOffMs=1000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
What --gradle-version accepts
Since Gradle 9.0 the version can be a full version, a minor version or a major version, and the wrapper task resolves it with Gradle’s version service
(https://services.gradle.org/versions/9 lists the 9.x releases). Earlier Gradle versions did not follow semantic versioning and could misread a partial
version, so this works only when the Gradle that runs the task is 9.0 or newer. These are the results on 2026-10-16, each from a run of
gradle :wrapper --gradle-version <value> in an empty project with Gradle 9.8.1; only the resulting distributionUrl is shown:
| Value | Resulting distributionUrl (after https://services.gradle.org/) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Not every value succeeds. These are the real messages, abridged to the line that names the problem:
$ gradle :wrapper --gradle-version release-candidate
> There is currently no version information available for 'release-candidate'.
$ gradle :wrapper --gradle-version 8
> '8' is not a valid Gradle version string (examples: '9.0.0', '9.1.0-rc-1')
> Specify a valid Gradle release listed on https://gradle.org/releases/.
> Use one of the following dynamic version specifications: 'latest', 'release-candidate', 'release-milestone', 'release-nightly', 'nightly'.
$ gradle :wrapper --gradle-version 9.99.9
> Test of distribution url https://services.gradle.org/distributions/gradle-9.99.9-bin.zip failed. Please check the values set with --gradle-distribution-url and --gradle-version.
$ gradle :wrapper --gradle-distribution-url http://localhost:8080/gradlew/invalid
> Test of distribution url http://localhost:8080/gradlew/invalid failed. Please check the values set with --gradle-distribution-url and --gradle-version.
The last command succeeds with --no-validate-url, and then writes validateDistributionUrl=false next to the unchecked URL, so the failure moves to the first
./gradlew call of the next developer. Use --no-validate-url only for a URL the machine running the task cannot reach but the developers can.
In gradle-wrapper.properties the version is always written as X.Y.Z; partial versions are only for the command line. (This is a rule since 9.0.0 and is quoted
from the manual.)
bin or all
The -bin distribution holds the runtime only; -all adds the Gradle source code and documentation. The manual and the performance best practices recommend
-bin for most builds: it is smaller to download and faster to cache on CI. The sizes of the two 9.8.1 files, from the Content-Length of an HTTP HEAD request on 2026-10-16
(neither was downloaded for this comparison):
| File | Size |
|---|---|
|
151,620,101 bytes (about 145 MiB) |
|
264,641,377 bytes (about 252 MiB) |
The reason for -all is to have the Gradle source code and documentation next to the runtime, for example for an IDE; how IDEs use the wrapper is on
CI and IDE Integration. With --distribution-type all the URL becomes:
$ gradle :wrapper --gradle-version 9.8.1 --distribution-type all
...
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-all.zip
Files to commit
.
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
The same layout applies to the Groovy DSL, with settings.gradle and build.gradle; gradlew.bat was not run here, because the runs were on macOS. All four wrapper files go into version control. The JAR is a binary and
small (48 KB), and committing it is the expected practice: without it the first command of a fresh clone has nothing to run. The manual notes that some
organisations do not allow binary files in version control and that there is no workaround then. A wrapper that is not committed is just a script nobody can use,
so the alternative is to generate it in CI before the build, which loses the benefit of reviewing it.
| File | Purpose |
|---|---|
|
The code that downloads, verifies and unpacks the distribution. Executed on every developer machine and build server, so it deserves a check (see Verifying the distribution and the wrapper JAR). |
|
The runtime configuration of the wrapper: which distribution, where it is stored, timeouts, retries, checksum. More generic settings, such as a proxy, do not belong here but in |
|
The Unix shell script and the Windows batch file that start the JAR. |
Line endings matter for the two scripts: a gradlew saved with CRLF endings fails on Unix, and a gradlew.bat with LF endings is unreliable on Windows. gradle init
therefore writes a .gitattributes (since Gradle 6.0, which added it to make sure the batch scripts keep their line endings). The file generated by 9.8.1 is:
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf
# Binary files should be left untouched
*.jar binary
The .gitignore that gradle init generates lists .gradle, build and .kotlin and does not ignore *.jar, so the wrapper JAR is not excluded by default. If your own
.gitignore has a rule such as *.jar, add an exception for gradle/wrapper/gradle-wrapper.jar; the symptom of forgetting it is a clone whose ./gradlew stops at once with Java’s own message, which does not mention the wrapper:
Error: Unable to access jarfile <scratch>/work/g7-gradle-wrapper/nojar/gradle/wrapper/gradle-wrapper.jar
(That was a copy of the skeleton without the JAR; the exit status was 1.)
Using the wrapper
Using the wrapper is running the build with ./gradlew on macOS and Linux, or gradlew.bat on Windows, instead of gradle; the arguments are the same. Call it from the
root, or with the right relative path from a subproject (../gradlew tasks). Any installed Gradle is ignored. On the first call with an empty Gradle User Home, in a copy of the
tidepool skeleton whose distributionUrl points at the local copy of the 9.8.1 zip:
./gradlew --version
Fetching distribution.
Downloading file:<scratch>/tools/gradle-9.8.1-bin.zip
..............10%..............20%...............30%..............40%...............50%..............60%...............70%..............80%...............90%..............100%
Welcome to Gradle 9.8.1!
Here are the highlights of this release:
- Java 27 support
- Maven mirror settings reuse
- Linked problem locations in build output
For more details see https://docs.gradle.org/9.8.1/release-notes.html
------------------------------------------------------------
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
With a real distribution URL the second line is Downloading https://services.gradle.org/distributions/gradle-9.8.1-bin.zip. The progress bar is a percentage
since Gradle 6.0, and since the same release the wrapper reinstalls a distribution it finds invalid instead of failing and requiring manual intervention. The second call prints only the
--version block, because the distribution is already unpacked. Both calls used a fresh Gradle User Home, whose wrapper directory afterwards looked like this:
wrapper
wrapper/dists
wrapper/dists/gradle-9.8.1-bin
wrapper/dists/gradle-9.8.1-bin/b059gjp0p6cuxvj40w8csa0e5
wrapper/dists/gradle-9.8.1-bin/b059gjp0p6cuxvj40w8csa0e5/gradle-9.8.1
wrapper/dists/gradle-9.8.1-bin/b059gjp0p6cuxvj40w8csa0e5/gradle-9.8.1-bin.zip.lck
wrapper/dists/gradle-9.8.1-bin/b059gjp0p6cuxvj40w8csa0e5/gradle-9.8.1-bin.zip.ok
The layout is wrapper/dists/<distribution name>/<hash derived from the distribution URL>/: the unpacked gradle-9.8.1 directory plus a lock file and a .ok marker that says the unpacking
finished. A different URL for the same version (a company mirror, -all instead of -bin) has a different directory, so both can coexist. The download itself is
removed after unpacking in these runs. The same Gradle User Home also holds the dependency cache and the daemon registry, which is why CI caches wrapper/dists along with it
(see CI and IDE Integration). GRADLE_USER_HOME moves the whole directory; it is how the scratch
runs of this page kept the real one untouched.
The keys of gradle-wrapper.properties
All keys, with the values 9.8.1 writes. distributionBase and zipStoreBase accept GRADLE_USER_HOME (the default) or PROJECT, the two values of the task’s PathBase.
| Key | Default written | Meaning |
|---|---|---|
|
|
Whether the unpacked distribution is stored in the Gradle User Home or in the project directory. |
|
|
The directory, relative to |
|
the |
Where to download the ZIP from. |
|
absent |
The SHA-256 the ZIP must have. Verified only when the distribution is downloaded. |
|
|
Milliseconds to wait on network operations. |
|
|
Whether the |
|
|
Download retries and the initial back-off in milliseconds, which doubles after each failure. Since 9.5. |
|
|
Where the downloaded ZIP is stored before unpacking. |
The PROJECT value stores the distribution in the repository, which would make a download unnecessary (the DSL reference describes exactly this use, for custom snapshots). It works; with
distributionBase=PROJECT, distributionPath=.gradle-dist and the two zip-store keys set the same way, a fresh Gradle User Home stayed empty (only native) and the project
directory received:
.gradle-dist
.gradle-dist/gradle-9.8.1-bin
.gradle-dist/gradle-9.8.1-bin/b059gjp0p6cuxvj40w8csa0e5
.gradle-dist/gradle-9.8.1-bin/b059gjp0p6cuxvj40w8csa0e5/gradle-9.8.1
.gradle-dist/gradle-9.8.1-bin/b059gjp0p6cuxvj40w8csa0e5/gradle-9.8.1-bin.zip.lck
.gradle-dist/gradle-9.8.1-bin/b059gjp0p6cuxvj40w8csa0e5/gradle-9.8.1-bin.zip.ok
That directory is about 150 MB; committing it is rarely a good idea, and the default is the right choice for almost every project. Properties that older Gradle versions wrote
(urlRoot, distributionName, distributionVersion, distributionClassifier) were removed in Gradle 3.0.
Upgrading the wrapper
Upgrading Gradle is running the wrapper task again with the new version. The manual recommends the task over editing the file, because the task also refreshes the scripts, which
sometimes change between Gradle versions. For 9.8.1 the release notes give the command as:
./gradlew :wrapper --gradle-version=9.8.1 && ./gradlew :wrapper
The first call runs the task with the old Gradle and rewrites the properties; the second call runs with the new Gradle (downloaded at that moment) and so also rewrites
gradle-wrapper.jar and the scripts with the new Gradle’s versions. One run keeps the JAR untouched, which is usually fine because a new Gradle runs with older wrapper files, but the second
run brings everything up to date. After upgrading, ./gradlew --version confirms the version; commit the four wrapper files together.
Only 9.8.1 could be used for the runs, so the upgrade was exercised as a switch to 9.7.1 (the mechanics are the same, only the direction differs, and nothing was downloaded; both URLs were
validated with a HEAD request). The project had a distributionSha256Sum, the situation of every hardened project, and the first call stopped:
$ ./gradlew :wrapper --gradle-version 9.7.1
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :wrapper FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':wrapper' (registered by plugin 'org.gradle.wrapper').
> gradle-wrapper.properties contains distributionSha256Sum property, but the wrapper configuration does not have one. Specify one in the wrapper task configuration or with the --gradle-distribution-sha256-sum task option. You can find checksums for all Gradle releases at https://gradle.org/release-checksums
* 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 10s
1 actionable task: 1 executed
[exit 1]
That is the task protecting you from a stale checksum: the old sum cannot be right for a new version, so the task refuses to write an unchecked URL beside it. The SHA-256 of 9.7.1 is published in
a small .sha256 file next to the distribution (gradle-9.7.1-bin.zip.sha256), which was fetched for this run (about 64 bytes, not the distribution). With the sum:
$ ./gradlew :wrapper --gradle-version 9.7.1 --gradle-distribution-sha256-sum acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a
> Task :wrapper
BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html
distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
The last two lines are the matching lines of the properties file afterwards. The JAR was byte-for-byte the 9.8.1 one (3b8a25775a69158b5ad2b1d17a80a88dc7b40a352a73eb27d06c612a7ce68e98) because it was written by the Gradle that
ran the task, which is why a second call under the new Gradle is part of the official command. The second call, which would download 9.7.1, was not run here.
One more observation, easy to miss in review. When the version that is requested is the version of the Gradle that runs the task, an existing sum is carried over, which is the behaviour the manual describes for a version that does not change. With a properties file for 9.7.1 (URL and sum), running the installed 9.8.1 as gradle :wrapper --gradle-version 9.8.1
succeeded, and the only change in the file was the URL:
4c4
< distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
---
> distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
The 9.7.1 sum stayed next to the 9.8.1 URL, which fails at the first ./gradlew with the checksum error shown under Error messages. So always pass --gradle-distribution-sha256-sum when the version changes, and read the diff of
gradle-wrapper.properties before committing.
Editing distributionUrl by hand also works, and so does editing it with a pull-request bot. The caveat of the manual applies: run the wrapper task afterwards to refresh the scripts and JAR.
Configuring the wrapper task in the build
The options above are properties of the Wrapper task, so you can set them once in the build instead of retyping them. The manual’s example fixes the distribution type to all. Here is the
same idea with the properties a hardened project cares about. The sum is that of gradle-9.8.1-all.zip, published by Gradle.
tasks.wrapper {
gradleVersion = "9.8.1"
distributionType = Wrapper.DistributionType.ALL
distributionSha256Sum = "5ceda8d2425eb2af196dec31b6fc1f38af2fb638fd621f86fa685805f2262224"
networkTimeout = 30000
retries = 3
retryBackOffMs = 1000
}
tasks.named('wrapper') {
gradleVersion = '9.8.1'
distributionType = Wrapper.DistributionType.ALL
distributionSha256Sum = '5ceda8d2425eb2af196dec31b6fc1f38af2fb638fd621f86fa685805f2262224'
networkTimeout = 30000
retries = 3
retryBackOffMs = 1000
}
Both versions were run (in two copies of the skeleton, one with build.gradle.kts and one with build.gradle) with gradle :wrapper --configuration-cache; the output was identical in the two:
Calculating task graph as no cached configuration is available for tasks: :wrapper
> Task :wrapper
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
Configuration cache entry stored.
and they wrote the same properties file, with the all URL, the sum, and the three numbers:
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=5ceda8d2425eb2af196dec31b6fc1f38af2fb638fd621f86fa685805f2262224
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-all.zip
networkTimeout=30000
retries=3
retryBackOffMs=1000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
A second call printed Reusing configuration cache., > Task :wrapper UP-TO-DATE and Configuration cache entry reused., so the configuration is configuration-cache compatible. With these properties in the build,
./gradlew :wrapper alone is the whole upgrade command: change gradleVersion and the sum in one commit, and run the task. The properties distributionSha256Sum, retries and retryBackOffMs
worked in the build script of 9.8.1 as shown, although the DSL reference page does not list them among its properties on 2026-10-16; the command-line options always do.
Where the distribution comes from
The default is services.gradle.org. Reasons to change it are a build network that blocks that host, a repository manager that mirrors it, or a build that must not depend on the
public internet at all. In each case the change is the distributionUrl:
-
--gradle-distribution-url https://repo.example.com/gradle/gradle-9.8.1-bin.zip(or the same URL in the properties file) points at an internal server; the manual says this is useful to host the distribution inside the company network, and any HTTP or HTTPS server that serves the ZIP works, for example a generic or raw repository of a repository manager (not run here); -
a
file:URL works too, which is how the scratch runs of this page avoided a second download of a 150 MB file; it is also how an air-gapped build can use a distribution kept on a shared disk; -
a proxy is configured outside the wrapper properties, with the proxy system properties in
gradle.properties, as described in the networking chapter.
Unlike the Maven Wrapper, the Gradle documentation describes no MVNW_REPOURL-style variable that rewrites the host; the URL is the single source (see the comparison table). The company URL is still checked by
distributionSha256Sum, and the sum is the one published by Gradle: it identifies the content, not where it came from.
Authenticated downloads
The wrapper can download from a server that wants HTTP Basic credentials or, since Gradle 9.4, a static Bearer token. Both are system properties, read from the Gradle User Home’s
gradle.properties (or any other way of setting system properties); Basic credentials can also be embedded in the URL. The manual states the rules:
-
credentials in system properties win over credentials embedded in
distributionUrl; -
a
wrapperTokenwins overwrapperUserandwrapperPasswordand over credentials in the URL; -
the credentials and the token are sent in clear text, so use
httpsonly, never plainhttp(the 9.4 release notes say both schemes are supported and strongly recommend HTTPS); -
prefix the property with the host name, dots replaced by underscores and lower-cased (
your.example.combecomesyour_example_com), so that the credentials are not offered to a different server, for example one reached through a redirect or a changed URL; the manual strongly recommends this for tokens.
# GRADLE_USER_HOME/gradle.properties on a developer machine or CI agent, not in the repository
systemProp.gradle.your_example_com.wrapperUser=${PLACEHOLDER_USER}
systemProp.gradle.your_example_com.wrapperPassword=${PLACEHOLDER_PASSWORD}
# or, since 9.4, a Bearer token
systemProp.gradle.your_example_com.wrapperToken=${PLACEHOLDER_TOKEN}
(The ${PLACEHOLDER_USER} values are not interpolated by Gradle; they stand for a value your secret store provides when the file is generated.) A real HTTPS server was not available, so the following was
run against a server on localhost serving the same ZIP over plain http, with the host key localhost. The server, a few lines of Python 3, answers 401 unless the right header arrives, and
logs the scheme of the Authorization header (never its value):
#!/usr/bin/env python3
import argparse, base64
from http.server import BaseHTTPRequestHandler, HTTPServer
p = argparse.ArgumentParser()
p.add_argument("--file", required=True)
p.add_argument("--port", type=int, default=18099)
p.add_argument("--basic") # user:password
p.add_argument("--bearer") # token
p.add_argument("--fail-first", type=int, default=0)
a = p.parse_args()
count = {"n": 0}
class H(BaseHTTPRequestHandler):
def log_message(self, *args): pass
def do_GET(self):
count["n"] += 1
auth = self.headers.get("Authorization", "")
scheme = auth.split(" ")[0] if auth else "none"
if count["n"] <= a.fail_first:
code = 503
elif a.bearer and auth == "Bearer " + a.bearer:
code = 200
elif a.basic and auth == "Basic " + base64.b64encode(a.basic.encode()).decode():
code = 200
elif not a.bearer and not a.basic:
code = 200
else:
code = 401
print(f"request {count['n']}: GET {self.path} Authorization={scheme} -> {code}", flush=True)
if code != 200:
self.send_response(code)
if code == 401:
self.send_header("WWW-Authenticate", 'Basic realm="gradle"')
self.send_header("Content-Length", "0")
self.end_headers()
return
data = open(a.file, "rb").read()
self.send_response(200)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
HTTPServer(("127.0.0.1", a.port), H).serve_forever()
Each case below used an empty Gradle User Home, a copy of the skeleton with distributionUrl=http\://localhost\:18099/gradle-9.8.1-bin.zip, and
./gradlew --version. Without credentials (the server started with --basic ci-reader:demo-password):
request 1: GET /gradle-9.8.1-bin.zip Authorization=none -> 401
Fetching distribution.
Downloading http://localhost:18099/gradle-9.8.1-bin.zip
Attempt 1/1 failed. Reason: Server returned HTTP response code: 401 for URL: http://localhost:18099/gradle-9.8.1-bin.zip
Exception in thread "main" java.io.IOException: Server returned HTTP response code: 401 for URL: http://localhost:18099/gradle-9.8.1-bin.zip
at org.gradle.wrapper.Download.download(SourceFile:1)
...
(The frames after Download.download are left out.) With Basic credentials for the host in the Gradle User Home:
systemProp.gradle.localhost.wrapperUser=ci-reader
systemProp.gradle.localhost.wrapperPassword=demo-password
request 1: GET /gradle-9.8.1-bin.zip Authorization=Basic -> 200
Fetching distribution.
Downloading http://localhost:18099/gradle-9.8.1-bin.zip
WARNING Using HTTP Basic Authentication over an insecure connection to download the Gradle distribution. Please consider using HTTPS.
Welcome to Gradle 9.8.1!
The client prints the warning every time it sends credentials over http. With the credentials in the URL (http\://ci-reader\:demo-password@localhost\:18099/gradle-9.8.1-bin.zip in the
properties file) the server again saw Basic and the wrapper masked the user information in its own message as Downloading http://***@localhost:18099/gradle-9.8.1-bin.zip; the file, however, is committed to
version control and holds the password in plain text, which the manual reserves for controlled environments. With the unprefixed properties systemProp.gradle.wrapperUser and
systemProp.gradle.wrapperPassword the request also carried Basic and succeeded: the unprefixed form is sent to whatever host the URL names, which is the leak the host prefix prevents.
With a Bearer token:
systemProp.gradle.localhost.wrapperToken=demo-token
request 1: GET /gradle-9.8.1-bin.zip Authorization=Bearer -> 200
Fetching distribution.
Downloading http://localhost:18099/gradle-9.8.1-bin.zip
WARNING Using HTTP Bearer Token Authentication over an insecure connection to download the Gradle distribution. Please consider using HTTPS.
Welcome to Gradle 9.8.1!
With the token and the user and password for the same host all set, the server again saw only Bearer, as documented. With a token configured for a different host
(systemProp.gradle.other_example_com.wrapperToken), the request to localhost carried no Authorization header at all (Authorization=none -> 401), so the token was not leaked. And since these are
ordinary system properties, the same token worked when supplied as GRADLE_OPTS="-Dgradle.localhost.wrapperToken=demo-token" in the environment instead of a file, which is the form a CI job uses with a secret variable.
That is the safe place for the value: never in gradle-wrapper.properties, which is in the repository (see
Build Environment & Properties for keeping secrets out of the project, and the Maven equivalent,
MVNW_USERNAME and MVNW_PASSWORD, on Maven Wrapper).
Basic and Bearer authentication over HTTPS, and with a real repository manager, were not run here. Bearer authentication needs the wrapper of Gradle 9.4 or newer; on Gradle 8.14 only the Basic credentials exist (not run here, from the 9.4.0 release notes).
Retries
Since Gradle 9.5 the wrapper can retry the download. It is off by default, to preserve the behaviour of earlier versions. The two properties are in gradle-wrapper.properties:
retries=3
retryBackOffMs=1000
retries is the maximum number of extra attempts, retryBackOffMs the initial delay, doubled after each failure. The manual’s snippet shows an explanation after a # on the same line, which
does not work in a .properties file (see below); keep comments on their own lines. With the server answering 503 for the first two requests and a one-line change to
retries=3 and retryBackOffMs=200 (200 ms to keep the demo short):
request 1: GET /gradle-9.8.1-bin.zip Authorization=none -> 503
request 2: GET /gradle-9.8.1-bin.zip Authorization=none -> 503
request 3: GET /gradle-9.8.1-bin.zip Authorization=none -> 200
Fetching distribution (retrying 3 times, with an initial back off of 200 ms).
Downloading http://localhost:18099/gradle-9.8.1-bin.zip
Attempt 1/4 failed. Reason: Server returned HTTP response code: 503 for URL: http://localhost:18099/gradle-9.8.1-bin.zip
Downloading http://localhost:18099/gradle-9.8.1-bin.zip
Attempt 2/4 failed. Reason: Server returned HTTP response code: 503 for URL: http://localhost:18099/gradle-9.8.1-bin.zip
Downloading http://localhost:18099/gradle-9.8.1-bin.zip
Welcome to Gradle 9.8.1!
With the default retries=0 the first 503 ended the call (Attempt 1/1 failed), and with retries=1 the third request never came: both attempts failed with the same exception
(Attempt 2/2 failed). Retries cover transient server and network problems; they are no remedy for a wrong URL or credentials, which fail the same way every time.
A classic bug follows from the properties format. Java properties files only know comments at the start of a line, so a trailing # comment becomes part of the value. With retries=3 # max attempts
the wrapper did not even start:
Exception in thread "main" java.lang.RuntimeException: Could not load wrapper properties from '<scratch>/work/g7-gradle-wrapper/auth/retry-comment/proj/gradle/wrapper/gradle-wrapper.properties'.
at org.gradle.wrapper.WrapperExecutor.<init>(SourceFile:55)
...
Caused by: java.lang.NumberFormatException: For input string: "3 # max attempts"
The manual’s snippet puts such a comment after retryBackOffMs=1000 and the 9.5.0 release notes after both lines; copied literally they hit the same problem (only the retries case was run here). The retry feature does not exist in Gradle 8.14 (not run here); a wrapper
JAR from 8.14 would not use the keys.
Verifying the distribution and the wrapper JAR
The wrapper downloads a program and the project then executes it, with a second binary (the JAR) committed to the repository. Gradle offers two independent checks.
The distribution: distributionSha256Sum
Add the SHA-256 of the ZIP to the properties file, with --gradle-distribution-sha256-sum or the property. The sums are published as a .sha256 file next to every distribution
(https://services.gradle.org/distributions/gradle-9.8.1-bin.zip.sha256) and on the release-checksums page. The 9.8.1 -bin value is
dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f, which is also the SHA-256 of the local copy of the ZIP used for these runs (shasum -a 256 on the file printed the same value).
If the sum does not match, the wrapper stops before unpacking anything. With the first characters of the sum changed to 0000aaaa, in an empty Gradle User Home:
Fetching distribution.
Downloading file:<scratch>/tools/gradle-9.8.1-bin.zip
..............10%..............20%...............30%..............40%...............50%..............60%...............70%..............80%...............90%..............100%
Exception in thread "main" java.lang.RuntimeException: Verification of Gradle distribution failed!
Your Gradle distribution may have been tampered with.
Confirm that the 'distributionSha256Sum' property in your gradle-wrapper.properties file is correct and you are downloading the wrapper from a trusted source.
Distribution Url: file:<scratch>/tools/gradle-9.8.1-bin.zip
Download Location: <scratch>/work/g7-gradle-wrapper/guh4/wrapper/dists/gradle-9.8.1-bin/b059gjp0p6cuxvj40w8csa0e5/gradle-9.8.1-bin.zip
Expected checksum: '0000aaaaf8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f'
Actual checksum: 'dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f'
Visit https://gradle.org/release-checksums/ to verify the checksums of official distributions. If your build uses a custom distribution, see with its provider.
at org.gradle.wrapper.Install.verifyDownloadChecksum(SourceFile:2)
...
(The frames after verifyDownloadChecksum are left out.) Two limits follow from how it works. The check happens only when the distribution is downloaded: with the same wrong sum in a project whose
distribution was already unpacked in the Gradle User Home, ./gradlew --version simply worked, so the sum protects a first download (a new machine, a clean CI cache) and not an existing cache. And it
protects against a corrupted or substituted download, not against someone who changes both the URL and the sum in the same pull request; the properties file deserves the review of code. On a CI server with a
persistent cache the sum is rarely evaluated, which is why a clean-cache build from time to time is worth having.
The wrapper JAR
The JAR is the more dangerous file, because it is a binary that nobody reads and it is committed: a pull request that only "upgrades Gradle" could replace it. Gradle publishes the checksums of all
wrapper JARs (except those of 3.3 to 4.0.2, which did not generate reproducible JARs), so the check is a comparison. The manual’s commands use sha256sum --check; this machine also has shasum. Both were run
in gradle/wrapper of a skeleton with the JAR generated by 9.8.1:
curl --location --output gradle-wrapper.jar.sha256 \
https://services.gradle.org/distributions/gradle-9.8.1-wrapper.jar.sha256
echo " gradle-wrapper.jar" >> gradle-wrapper.jar.sha256
sha256sum --check gradle-wrapper.jar.sha256
gradle-wrapper.jar: OK
The downloaded .sha256 file holds the hash alone, without a newline; the echo appends the file name. The result is one space between hash and name, which sha256sum here accepts. shasum -a 256 --check rejected
that line with "no properly formatted SHA checksum lines found", because it wants two spaces (or a space and an asterisk); the equivalent that worked:
printf '%s %s\n' "$(curl -sL https://services.gradle.org/distributions/gradle-9.8.1-wrapper.jar.sha256)" gradle-wrapper.jar | shasum -a 256 --check -
gradle-wrapper.jar: OK
When one byte of a copy of the JAR was changed, the same check reported the failure and exited non-zero:
sha256sum: WARNING: 1 computed checksum did NOT match
gradle-wrapper-tampered.jar: FAILED
The SHA-256 of the 9.8.1 JAR is 3b8a25775a69158b5ad2b1d17a80a88dc7b40a352a73eb27d06c612a7ce68e98 and appeared on the release-checksums page. If the number of a JAR is not on that page, it may come from a milestone,
release candidate or nightly build, or from Gradle 3.3 to 4.0.2; the manual says to treat such a file as untrustworthy until proven otherwise. A JAR that was generated by another version than the one named in the
properties file is not an attack by itself: run the wrapper task again to refresh it.
Gradle also publishes a PGP signature for every distribution artifact, including the JAR (gradle-9.8.1-wrapper.jar.asc, which exists: the request returned HTTP 200). The procedure is to import the Gradle key
(the fingerprint is on the Gradle Keys page, with the <FINGERPRINT> placeholder in the manual’s command), download the .asc and run gpg --verify gradle-wrapper.jar.asc gradle-wrapper.jar; a success shows a "Good
signature" line. GnuPG 2.2.41 was installed, but the key import and the verification were not run here.
On GitHub the check does not have to be manual. The gradle/actions/setup-gradle action validates every gradle-wrapper.jar of the repository against the known checksums as part of each build
(wrapper validation is built into the action since v4), and a standalone wrapper-validation action does only that. The workflow and its caching inputs are on
CI and IDE Integration; the wider picture, with the tools' own distributions, is on
Supply-Chain Security for JVM Builds. The Gradle 9.3 and 9.6 releases added and refreshed best-practice entries
for exactly these two checks.
Error messages
| Message (abridged) | Cause and fix |
|---|---|
|
|
|
|
|
The server wants credentials the wrapper did not send. The host in the property key must match the host of the URL, in lower case with underscores for dots. |
|
A transient server problem with retries off. Set |
|
A value in |
|
The |
|
The URL does not exist or is not reachable from the machine that runs the task. Fix the version or URL, or |
|
A major or minor version only is not accepted below 9.0, and |
Every message in the table was produced by a run on this page.
Release history of the wrapper
| Release | Change to the wrapper |
|---|---|
2.4 |
|
3.0 |
The old properties |
6.0 |
Download progress as a percentage; the wrapper reinstalls an invalid distribution; |
8.2 |
The |
9.0 |
|
9.3 |
|
9.4 |
Bearer token authentication for the download, and per-host credentials for Basic and Bearer. |
9.5 |
Automatic retries ( |
9.6 |
|
Gradle 8.14.6, the last 8.x release, has none of the 9.x rows (not run here); its upgrade command in the release notes is
./gradlew wrapper --gradle-version=8.14.6 && ./gradlew wrapper. Since 9.0, wrapper clients still start on JVM 8 while the build itself needs a JVM 17 or newer; see the compatibility page.
The wrapper in Gradle, Maven and Ant
| Gradle 9.8.1 | Maven 3.10.0 | Ant 1.10.18 | |
|---|---|---|---|
Creating it |
|
|
none: there is no wrapper, the distribution is unpacked per machine |
Files |
|
|
- |
Pinning a version |
|
|
- |
Checksum |
|
|
- |
Cache |
|
|
- |
Credentials |
|
|
- |
Mirror |
edit |
|
- |
The cross-tool view of repositories, mirrors and credentials is on Repositories & Publishing Across Tools, and the choice of tool on Comparing Ant, Maven & Gradle.
Where older material is outdated
The book Gradle in Action (Gradle 1.7) introduces the wrapper in its chapter 3. It is still the right idea, but its mechanics have changed:
-
The wrapper task no longer has to be declared in the build script with a
gradleVersion; since Gradle 2.4gradle wrapper --gradle-versionworks in any build, and the task is built in. -
The current wrapper supports a checksum, credentials, a timeout, retries and URL validation. A detail to know: the DSL reference still lists an
httpdefault fordistributionUrl, while 9.8.1 wrotehttpsin every run on this page. -
Gradle 1.x release numbers (
1.7) are gone: Gradle follows semantic versioning since 9.0, and the wrapper accepts9or9.8on the command line. -
The wrapper JAR can be verified against published checksums and PGP signatures (the checksums exist for all releases except 3.3 to 4.0.2), which are newer than the book’s Gradle 1.7.
The corrections for the rest of that chapter, such as the Java plugin and its configurations, are on Java Projects.
Summary
-
gradle :wrapperwritesgradlew,gradlew.bat,gradle/wrapper/gradle-wrapper.jarandgradle-wrapper.properties; commit all four, including the JAR. -
--gradle-versiontakes a full version, since 9.0 a major or minor version, or a label;--distribution-type bin(default) is smaller thanall(151,620,101 bytes against 264,641,377 for 9.8.1). -
The distribution is cached in
GRADLE_USER_HOME/wrapper/dists/<name>/<hash of the URL>;distributionBase=PROJECTstores it in the project instead. -
Upgrade with
./gradlew :wrapper --gradle-version <new> --gradle-distribution-sha256-sum <sum>, then run./gradlew :wrapperagain, and read the diff of the properties file: an old sum can survive next to a new URL. -
distributionSha256Sumis checked only on download; verify the JAR with the published SHA-256 (sha256sum --check) or letsetup-gradledo it on every build. -
Basic and, since 9.4, Bearer credentials are system properties prefixed with the host (
systemProp.gradle.<host>.wrapperToken), never in the repository; retries (9.5) need numeric values without trailing comments. -
Configure the
wrappertask in the build (tasks.wrapper { ... }) to make./gradlew :wrapperthe whole upgrade command; the example is configuration-cache compatible.