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).

flowchart TD A["./gradlew"] --> B["Find java: JAVA_HOME, else PATH"] B --> C["java -jar gradle/wrapper/gradle-wrapper.jar"] C --> D["Read gradle-wrapper.properties"] D --> E{"Distribution already unpacked?"} E -- yes --> H["Start the Gradle launcher of that distribution"] E -- no --> F["Download distributionUrl with credentials and retries"] F --> G["Check distributionSha256Sum, then unzip"] G --> H H --> I["Connect to or start a daemon and run the build"]

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

--gradle-version

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 latest, release-candidate, release-milestone, release-nightly and nightly are accepted too.

--distribution-type

bin (default) or all; see bin or all.

--gradle-distribution-url

The complete URL of a distribution ZIP. It makes --gradle-version and --distribution-type obsolete, because the URL already says both, and it is the way to host the distribution in a company network. The URL is validated.

--gradle-distribution-sha256-sum

The SHA-256 the downloaded ZIP must have.

--network-timeout

The timeout in milliseconds for the wrapper’s network operations; the default is 10000.

--validate-url, --no-validate-url

Validate (the default) or skip the validation of the distribution URL.

--retries, --retry-back-off-ms

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/)

9.8.1

distributions/gradle-9.8.1-bin.zip

9

distributions/gradle-9.8.1-bin.zip (the latest 9.x release)

9.7

distributions/gradle-9.7.1-bin.zip (the latest 9.7.x release)

latest

distributions/gradle-9.8.1-bin.zip

release-milestone

distributions/gradle-9.9.0-milestone-2-bin.zip

nightly

distributions-snapshots/gradle-9.9.0-20261015004339+0000-bin.zip

8.14

distributions/gradle-8.14-bin.zip (an exact version: the 8.14 release, not 8.14.6)

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

gradle-9.8.1-bin.zip

151,620,101 bytes (about 145 MiB)

gradle-9.8.1-all.zip

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

gradle/wrapper/gradle-wrapper.jar

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).

gradle/wrapper/gradle-wrapper.properties

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 gradle.properties.

gradlew, gradlew.bat

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

distributionBase

GRADLE_USER_HOME

Whether the unpacked distribution is stored in the Gradle User Home or in the project directory.

distributionPath

wrapper/dists

The directory, relative to distributionBase, where distributions are unpacked.

distributionUrl

the services.gradle.org URL of the chosen version and type

Where to download the ZIP from. http, https and file URLs work; credentials may be embedded (see Authenticated downloads).

distributionSha256Sum

absent

The SHA-256 the ZIP must have. Verified only when the distribution is downloaded.

networkTimeout

10000

Milliseconds to wait on network operations.

validateDistributionUrl

true

Whether the wrapper task validates the URL when it writes it. A property of the task more than of the runtime.

retries, retryBackOffMs

0, 500

Download retries and the initial back-off in milliseconds, which doubles after each failure. Since 9.5.

zipStoreBase, zipStorePath

GRADLE_USER_HOME, wrapper/dists

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.

Kotlin DSL
tasks.wrapper {
    gradleVersion = "9.8.1"
    distributionType = Wrapper.DistributionType.ALL
    distributionSha256Sum = "5ceda8d2425eb2af196dec31b6fc1f38af2fb638fd621f86fa685805f2262224"
    networkTimeout = 30000
    retries = 3
    retryBackOffMs = 1000
}
Groovy DSL
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 wrapperToken wins over wrapperUser and wrapperPassword and over credentials in the URL;

  • the credentials and the token are sent in clear text, so use https only, never plain http (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.com becomes your_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

ERROR: JAVA_HOME is set to an invalid directory: /nonexistent

JAVA_HOME names a directory without bin/java. The script exits with status 1 before starting anything. Fix or unset JAVA_HOME.

Verification of Gradle distribution failed! with Expected checksum and Actual checksum

distributionSha256Sum does not match the downloaded ZIP: a stale sum after a version change, a tampered file or a mirror that serves something else. Compare with the release-checksums page.

Server returned HTTP response code: 401 for URL: ...

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.

Server returned HTTP response code: 503 ... and Attempt 1/1 failed

A transient server problem with retries off. Set retries (9.5 or later).

Could not load wrapper properties from ... with NumberFormatException

A value in gradle-wrapper.properties is not a number, usually a trailing # comment on a numeric line.

gradle-wrapper.properties contains distributionSha256Sum property, but the wrapper configuration does not have one.

The wrapper task found a sum in the file but was not given one. Pass --gradle-distribution-sha256-sum or configure the task.

Test of distribution url ... failed. Please check the values set with --gradle-distribution-url and --gradle-version.

The URL does not exist or is not reachable from the machine that runs the task. Fix the version or URL, or --no-validate-url.

'8' is not a valid Gradle version string

A major or minor version only is not accepted below 9.0, and 8 is not a full version. Use 8.14.6 or a label.

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

gradle wrapper --gradle-version and --gradle-distribution-url work from the command line; before, the build script had to declare a wrapper task with gradleVersion.

3.0

The old properties urlRoot, distributionName, distributionVersion and distributionClassifier were removed.

6.0

Download progress as a percentage; the wrapper reinstalls an invalid distribution; gradle init writes .gitattributes.

8.2

The wrapper task validates the distribution URL before writing it.

9.0

--gradle-version accepts a major or minor version alone (the properties file always has X.Y.Z).

9.3

Wrapper.getAvailableDistributionTypes deprecated; a best-practice entry for validating the wrapper JAR checksum.

9.4

Bearer token authentication for the download, and per-host credentials for Basic and Bearer.

9.5

Automatic retries (retries, retryBackOffMs), off by default.

9.6

getNetworkTimeout() of the Wrapper task promoted to stable; the wrapper validation guidance was updated.

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

gradle :wrapper (built in); gradle init too

mvn wrapper:wrapper (the wrapper plugin, 3.3.4)

none: there is no wrapper, the distribution is unpacked per machine

Files

gradlew, gradlew.bat, gradle/wrapper/gradle-wrapper.{jar,properties}

mvnw, mvnw.cmd, .mvn/wrapper/maven-wrapper.properties (a JAR only for some types)

-

Pinning a version

distributionUrl in the properties file, rewritten by the task

distributionUrl in the properties file

-

Checksum

distributionSha256Sum (distribution only); the JAR is checked by comparison or in CI

distributionSha256Sum and wrapperSha256Sum

-

Cache

GRADLE_USER_HOME/wrapper/dists/<name>/<hash>

MAVEN_USER_HOME/wrapper/dists (default ~/.m2)

-

Credentials

systemProp.gradle.<host>.wrapperUser, .wrapperPassword, .wrapperToken

MVNW_USERNAME and MVNW_PASSWORD

-

Mirror

edit distributionUrl or use --gradle-distribution-url

MVNW_REPOURL, or edit distributionUrl

-

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.4 gradle wrapper --gradle-version works 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 http default for distributionUrl, while 9.8.1 wrote https in every run on this page.

  • Gradle 1.x release numbers (1.7) are gone: Gradle follows semantic versioning since 9.0, and the wrapper accepts 9 or 9.8 on 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 :wrapper writes gradlew, gradlew.bat, gradle/wrapper/gradle-wrapper.jar and gradle-wrapper.properties; commit all four, including the JAR.

  • --gradle-version takes a full version, since 9.0 a major or minor version, or a label; --distribution-type bin (default) is smaller than all (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=PROJECT stores it in the project instead.

  • Upgrade with ./gradlew :wrapper --gradle-version <new> --gradle-distribution-sha256-sum <sum>, then run ./gradlew :wrapper again, and read the diff of the properties file: an old sum can survive next to a new URL.

  • distributionSha256Sum is checked only on download; verify the JAR with the published SHA-256 (sha256sum --check) or let setup-gradle do 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 wrapper task in the build (tasks.wrapper { ... }) to make ./gradlew :wrapper the whole upgrade command; the example is configuration-cache compatible.