Maven 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 Apache Maven 3.10.0 and the Maven Wrapper plugin and scripts 3.3.4 (the latest on Maven Central on 2026-10-16). Every example was run on JDK 26 on 2026-10-16, on macOS, in a scratch workspace that mimics the tidepool build. Maven 3.9.16 and Maven 4.0.0-rc-7 (a release candidate) were run too where the text says so. Maven distributions were downloaded from Maven Central, or from a small HTTP server on localhost that served an already downloaded copy when an example needed to repeat a download or to show authentication. The wrapper’s cache was redirected with MAVEN_USER_HOME into the scratch workspace, so ~/.m2/wrapper was not touched. Paths were shortened to <work> and <tmp>, and the lines Loaded N auto-discovered prefixes ... that Maven 3.10 prints while resolving were left out. Everything that was not run is labelled "not run here".

Maven is stable and available on most systems, but the version on a given machine is whatever somebody installed. The Maven Wrapper moves the choice of Maven from the machine to the project. The project carries two small scripts, mvnw for Unix shells and mvnw.cmd for Windows, and a properties file that names the Maven distribution. A developer or a CI job runs ./mvnw clean install instead of mvn clean install, and if the named distribution is not on the machine yet, the script downloads and unpacks it first and then runs it. Every later call reuses the unpacked copy. The wrapper page says the wrapper should work with any Maven 3.x version or later, and that the deprecated, end-of-life Maven 2.x is not supported. How to install Maven itself is on Installation; the options of the mvn command that mvnw forwards are on Running Maven from the Command Line; the files under .mvn that sit beside the wrapper are on Configuring Maven.

What the wrapper does for a team

A build that depends on "whatever Maven is installed" has the same weakness as one that depends on "whatever JDK is installed": two machines can produce different results from the same sources. The version of Maven decides which lifecycle bindings apply when a POM names no plugin version, which settings are honoured, which breaking changes of a release apply (see Maven Versions & Release History) and, in the future, whether the build runs on Maven 3 or Maven 4. With the wrapper:

  • a new colleague needs a JDK and nothing else: no "install Maven X, put it on the PATH" step in the README;

  • CI and developers run the same Maven, and the version changes in one reviewed commit that edits one line of maven-wrapper.properties;

  • an old branch builds with the Maven it was written for, even after the team moved to a newer one;

  • the distribution can be pinned by checksum, which is the supply-chain control covered at the end of this page.

The wrapper does not remove the need for a JDK (the Maven scripts need one, as described on Installation) and it cannot be created without a Maven (or an IDE feature that runs the same plugin): mvn wrapper:wrapper needs an installed Maven once. The equivalent in Gradle is gradlew with the version in gradle-wrapper.properties (see Gradle Wrapper); Ant has no wrapper.

Creating the wrapper

The Apache Maven Wrapper plugin has a single goal that matters, wrapper:wrapper. It is an aggregator goal that unpacks the wrapper files into the project directory. Because the plugin is resolved by its prefix, no POM entry is needed. In an empty directory with a minimal pom.xml, with Maven 3.10.0:

mvn wrapper:wrapper
[INFO] --- wrapper:3.3.4:wrapper (default-cli) @ tidepool-wrapper-demo ---
[INFO] Unpacked only-script type wrapper distribution org.apache.maven.wrapper:maven-wrapper-distribution:zip:only-script:3.3.4
[INFO] Configuring .mvn/wrapper/maven-wrapper.properties to use Maven 3.10.0 and download from https://repo.maven.apache.org/maven2
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

The goal wrote three files:

.mvn/wrapper/maven-wrapper.properties
mvnw
mvnw.cmd
$ cat .mvn/wrapper/maven-wrapper.properties
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip

Older tutorials create the wrapper with mvn -N io.takari:maven:wrapper. That Takari plugin has had no release on Maven Central since 0.7.7 (2019-12-04); today the Apache plugin and its wrapper prefix are the way. By default the plugin writes the version of the Maven that is running it. The same command with the Maven 3.9.16 on the PATH wrote apache-maven/3.9.16/apache-maven-3.9.16-bin.zip, and with Maven 4.0.0-rc-7 it wrote the 4.0.0-rc-7 URL. The plugin itself is a Maven plugin, so the "Loaded prefixes" and download lines of any plugin appear on the first run. Maven 4.0.0-rc-7 also printed a warning in an empty directory, because there was no .mvn directory yet to mark the project root: [WARNING] Unable to find the root directory. Create a .mvn directory in the root directory or add the root="true" attribute on the root project's model to identify it.

The wrapper has the same shape on Maven 3 and Maven 4: the wrapper’s version is independent of the Maven it launches, so one wrapper (3.3.4) can launch Maven 3.9.16, 3.10.0 or 4.0.0-rc-7. Run it from the root of the project (in a multi-module build, the directory with the aggregator POM), so that .mvn sits where Maven looks for it.

The goal’s parameters

mvn wrapper:help -Ddetail=true -Dgoal=wrapper lists the parameters. All of them are optional; the right-hand column is the property to put on the command line.

Parameter User property Meaning

mavenVersion

maven

The Maven version to require. Defaults to the version of the Maven running the goal; any release above 2.0.9 is accepted.

distributionType

type

only-script, script, bin or source (see The four distribution types). If -Dtype is not given, the type already in maven-wrapper.properties is kept; for a new wrapper it is only-script.

distributionUrl

distributionUrl

The exact URL of the distribution (since plugin 3.3.0). If absent, it is built from the version and the repository URL.

distributionSha256Sum

distributionSha256Sum

The expected SHA-256 of the distribution (since 3.2.0).

wrapperSha256Sum

wrapperSha256Sum

The expected SHA-256 of maven-wrapper.jar (since 3.2.0; only the jar-based types use it).

mvndVersion

mvnd

The Maven Daemon version to require instead of Maven (since 3.2.0; see Using Maven Daemon).

includeDebugScript

includeDebug

Add the mvnwDebug and mvnwDebug.cmd scripts (default false).

alwaysDownload

alwaysDownload

Write alwaysDownload=true into the properties file (since 3.2.0; default false).

alwaysUnpack

alwaysUnpack

Write alwaysUnpack=true into the properties file (since 3.2.0; default false).

What mvnw does

The scripts are short enough to read, and reading mvnw (295 lines for the default type) is the best way to understand the behaviour that follows. The flow in mvnw, as run here, is:

flowchart TD run["./mvnw args"] --> props["read distributionUrl and distributionSha256Sum from .mvn/wrapper/maven-wrapper.properties"] props --> repo["replace the repository part of the URL when MVNW_REPOURL is set"] repo --> home["MAVEN_HOME = MAVEN_USER_HOME/wrapper/dists/name/hash of the URL"] home --> exists{"MAVEN_HOME already there?"} exists -->|"yes"| exec["exec MAVEN_HOME/bin/mvn with the arguments"] exists -->|"no"| dl["download with wget, else curl, else a Java Downloader (always Java when MVNW_USERNAME is set)"] dl --> sum{"distributionSha256Sum set?"} sum -->|"no"| unpack["unpack into a temporary directory and move it to MAVEN_HOME"] sum -->|"yes"| cmp{"SHA-256 matches?"} cmp -->|"yes"| unpack cmp -->|"no"| fail["print an error and exit 1"] unpack --> exec

Step by step:

  1. mvnw is a POSIX sh script (set -euf, so an unset variable or a failing command stops it). It reads distributionUrl and distributionSha256Sum from .mvn/wrapper/maven-wrapper.properties next to the script, not from the current directory. That is why ../mvnw works from a module directory.

  2. The cache directory is ${MAVEN_USER_HOME:-$HOME/.m2}/wrapper/dists/<name>/<hash>. <name> is the distribution file name without -bin.zip, and <hash> is the hexadecimal Java String.hashCode() of the full URL. For the URL above that is dae9a06d, which was checked with an independent computation. The consequence is that the same Maven version fetched through two different URLs (Maven Central and an internal proxy) is stored twice: here the Central URL gave dae9a06d and the localhost one e371c5c2.

  3. If that directory exists, the script exec`s `bin/mvn in it, and nothing else happens.

  4. Otherwise it downloads the archive to a temporary directory. It tries wget, then curl (curl -f -L), and if neither exists it compiles and runs a tiny Downloader.java with the JDK (so javac must be on the PATH or under JAVA_HOME). When MVNW_USERNAME is set, wget and curl are skipped and the Java downloader is used, because it carries the credentials. If unzip is missing, it asks for the .tar.gz of the same distribution instead.

  5. If distributionSha256Sum is set, the archive is checked with sha256sum or shasum -a 256 before unpacking.

  6. The unpacked directory is moved to the cache, the original URL is written beside it in a file mvnw.url, and mvnw replaces itself with mvn.

The first call here downloaded and unpacked the Maven 3.10.0 zip from Maven Central; the second only started Maven:

$ time ./mvnw -v          # first call, empty MAVEN_USER_HOME
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)
Maven home: <work>/muh/wrapper/dists/apache-maven-3.10.0/dae9a06d
Java version: 26, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home
...
./mvnw -v  0.45s user 0.08s system 45% cpu 1.151 total

$ time ./mvnw -v          # second call
...
./mvnw -v  0.38s user 0.06s system 134% cpu 0.325 total

(The timings are those of this machine over a fast connection; the download is about 10 MB.) The cache contains an ordinary Maven home:

$ ls <work>/muh/wrapper/dists/apache-maven-3.10.0/dae9a06d
LICENSE  NOTICE  README.txt  bin  boot  conf  lib  mvnw.url
$ cat <work>/muh/wrapper/dists/apache-maven-3.10.0/dae9a06d/mvnw.url
https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip

That means the Maven conf/settings.xml of the downloaded installation is used as the global settings file of the build, not the one of any other Maven installed on the machine. A project that needs a global setting must put it in the project’s .mvn/maven.config (-s, -gs) or in the user settings.

After the first call, any build runs as usual. In the demo project (Maven 3.10.0 pinned by the wrapper):

$ ./mvnw -ntp -q help:evaluate -Dexpression=maven.version -DforceStdout
3.10.0

Verbose mode

MVNW_VERBOSE=true makes mvnw say what it is doing. By default the script is quiet and prints nothing of its own. With MVNW_VERBOSE=true, an empty cache and curl as the downloader, the first call printed (the unzip listing of about 130 lines was cut at the ellipsis):

$ MVNW_VERBOSE=true ./mvnw -v
Couldn't find MAVEN_HOME, downloading and installing it ...
Downloading from: https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip
Downloading to: <tmp>/apache-maven-3.10.0-bin.zip
Found curl ... using curl
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
...
Archive:  <tmp>/apache-maven-3.10.0-bin.zip
...
Found extracted Maven distribution directory: apache-maven-3.10.0
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)

The second call, with the cache present, printed found existing MAVEN_HOME at <work>/muh/wrapper/dists/apache-maven-3.10.0/dae9a06d. The value MVNW_VERBOSE=debug runs the script under set -x (not run here). The variable is removed from the environment before mvn starts, so it does not reach Maven or its plugins.

The four distribution types

The type is the shape of the wrapper (how it downloads Maven), not of Maven. The plugin documents four; only-script exists since plugin 3.2.0 and has been the default for new wrappers since 3.3.0 (help:describe shows Default: bin on 3.2.0 and Default: only-script on 3.3.0). They were each created with mvn wrapper:wrapper -Dtype=<type> and the files were listed:

Type Files created How Maven is fetched Binary in the repository

only-script (default)

mvnw (11 790 bytes), mvnw.cmd (8 481), .mvn/wrapper/maven-wrapper.properties

The script downloads the Maven archive itself with wget, curl or Java (a PowerShell script on Windows), then runs the real mvn.

no

script

mvnw (11 336), mvnw.cmd (7 903), the properties file, with an extra wrapperUrl line

On first use the script downloads maven-wrapper.jar with wget or curl (PowerShell on Windows) into .mvn/wrapper, and the jar downloads Maven. The jar was there after the first ./mvnw.

no (the downloaded jar shows up as an untracked file)

bin

the script files plus .mvn/wrapper/maven-wrapper.jar (63 093 bytes)

The committed jar downloads and runs Maven, so neither wget nor curl is needed.

yes, the jar

source

the script files plus .mvn/wrapper/MavenWrapperDownloader.java (3 751 bytes)

The script can compile and run the Java file to fetch maven-wrapper.jar, which then downloads Maven; needs a JDK, not a JRE. As with script, the jar appeared in .mvn/wrapper after the first run.

no, but a source file (the downloaded jar is untracked)

For the jar-based types the properties file also holds wrapperUrl:

$ cat .mvn/wrapper/maven-wrapper.properties        # created with -Dtype=bin
wrapperVersion=3.3.4
distributionType=bin
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar

All four launched Maven 3.10.0 successfully when MVNW_REPOURL pointed at the local server (the script, bin and source runs printed [WARNING] Using an insecure connection to download the Maven distribution. Please consider using HTTPS. because the URL was http; the only-script run did not warn). Two differences between the types were visible in the cache and are worth knowing:

  • only-script stores the distribution as wrapper/dists/apache-maven-3.10.0/<hash>, while the jar-based types store it as wrapper/dists/apache-maven-3.10.0-bin/<hash>/apache-maven-3.10.0. A machine that has used both has two copies.

  • With the jar-based types the extra link in the chain (the jar) is another thing to trust: the jar can be pinned with wrapperSha256Sum (see Verifying checksums).

Which type to choose (this is my recommendation, not a rule of the documentation): only-script for nearly every project, because there is nothing binary to review and the Maven distribution is the only download. bin fits a machine without wget and curl (a minimal container, say) that has Java and a repository that accepts a small binary; source is the compromise for a repository policy that forbids binary files on a machine that has a JDK but no wget or curl. With script and source, the jar that appears in .mvn/wrapper after the first run is a download, not a project file: add .mvn/wrapper/maven-wrapper.jar to .gitignore or commit it on purpose, so that it does not drift in and out of pull requests.

Changing the type of an existing wrapper

When wrapper:wrapper runs in a project that already has maven-wrapper.properties, the existing type is kept, so that updating the wrapper does not change the type by accident. Re-running the goal on the bin wrapper above with only -Dmaven=3.9.16 left distributionType=bin in place and changed the URL. Passing -Dtype changes the type:

mvn wrapper:wrapper -Dtype=only-script

That run deleted .mvn/wrapper/maven-wrapper.jar, left only maven-wrapper.properties in .mvn/wrapper, and wrote the Maven version of the running Maven (3.10.0 here) back into distributionUrl, which brings the next section’s trap.

Choosing and upgrading the Maven version

-Dmaven=<version> sets the version, for a release or a snapshot. To move the demo project from 3.10.0 to 3.9.16, and then to the Maven 4 release candidate, then to compare what changed in the working tree:

mvn wrapper:wrapper -Dmaven=3.9.16
git diff --stat
./mvnw -v
mvn wrapper:wrapper -Dmaven=4.0.0-rc-7
 .mvn/wrapper/maven-wrapper.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
Apache Maven 3.9.16 (2bdd9fddda4b155ebf8000e807eb73fd829a51d5)

(the second git diff line, for 4.0.0-rc-7, differs only in the URL, and ./mvnw -v then reported Apache Maven 4.0.0-rc-7.) Only the properties file changed: the scripts were byte-identical because the wrapper version was the same, so an upgrade of Maven is a one-line, easy to review commit. You can edit the line by hand with the same result, as the page also documents, including snapshots served by the Apache snapshot repository (not run here):

distributionUrl=https://repository.apache.org/content/repositories/snapshots/org/apache/maven/apache-maven/4.1.0-SNAPSHOT/apache-maven-4.1.0-20250710.120440-1-bin.zip

Three traps in this workflow:

  • mvn wrapper:wrapper without -Dmaven is not a no-op. The default is the running Maven. With Maven 3.9.16 installed on a developer’s machine, running the goal "to refresh the scripts" on a project pinned to 3.10.0 writes 3.9.16 into the properties file. The run above that changed the type did exactly this. Always give -Dmaven= explicitly, or review the diff.

  • Different URLs mean different cache entries. Because the cache directory contains the hash of the URL, two developers using different MVNW_REPOURL values cache the same Maven twice. That costs disk, not correctness.

  • The wrapper’s version and Maven’s version are separate. Upgrading the plugin (wrapperVersion=3.3.4) refreshes the scripts; upgrading Maven changes distributionUrl. Do the latter to adopt a Maven release, the former only to get bug fixes of the scripts. A wrapper pinned to Maven 4.0.0-rc-7 still runs with the 3.x plugin, as above.

Moving the build itself to a new Maven line (3.9 to 3.10, or 3 to 4) is the subject of Maven Versions & Release History and Migrating to Maven 4; the wrapper only changes which Maven answers.

maven-wrapper.properties reference

Every key of the file that the scripts and the plugin read or write:

Key Meaning

wrapperVersion

The version of the wrapper scripts that wrote the file (3.3.4 here). Informational.

distributionType

only-script, script, bin or source (see The four distribution types). The plugin reads it to preserve the type; mvnw does not need it.

distributionUrl

The URL of the Maven archive. The script requires it to end in -bin.zip (or to be a maven-mvnd-* file name); http://127.0.0.1:8765/maven.zip stopped with distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found 'http://127.0.0.1:8765/maven.zip'. May carry credentials as https://user:password@host/... (see Downloading with credentials).

distributionSha256Sum

Lower-case hexadecimal SHA-256 of the archive; if present the script verifies it (see Verifying checksums).

wrapperUrl

Where the jar-based types download maven-wrapper.jar from.

wrapperSha256Sum

Lower-case hexadecimal SHA-256 of maven-wrapper.jar; verified before the jar is used.

alwaysDownload

For the jar-based types, download Maven on every run. The bin wrapper downloaded the zip on both of two consecutive runs with alwaysDownload=true, and once only without it. The only-script scripts ignore the key.

alwaysUnpack

For the jar-based types, unpack on every run. The only-script scripts ignore the key.

The properties file is read by a shell loop, so keep it to simple key=value lines with no quoting. The script trims spaces, tabs, carriage returns and line feeds from the values (a comment in the script cites MWRAPPER-139 for files that went through Windows tools).

Verifying checksums

A wrapper downloads and runs a program, so the download is the weak point. The page recommends pinning both the jar and the distribution by SHA-256, in the properties file, "using only small caps" hexadecimal: wrapperSha256Sum for maven-wrapper.jar and distributionSha256Sum for the Maven archive. The plugin can write them for you. The SHA-256 below was computed from the apache-maven-3.10.0-bin.zip whose SHA-512 matches the apache-maven-3.10.0-bin.zip.sha512 published on Maven Central (Central does not publish a .sha256 file for it: the request returned 404, so the SHA-256 has to be computed from a download that was verified against the SHA-512 first, or taken from a source you trust):

mvn wrapper:wrapper -DdistributionSha256Sum=1f6d9909266510f039f59aa0e13dcd2c66da85f043e56276e41f2918f8bddaff
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip
distributionSha256Sum=1f6d9909266510f039f59aa0e13dcd2c66da85f043e56276e41f2918f8bddaff

With the right value the wrapper started Maven 3.10.0. With the value changed to 64 zeros, in an empty cache, mvnw stopped before unpacking anything and exited with status 1:

Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised.
If you updated your Maven version, you need to update the specified distributionSha256Sum property.

For a bin wrapper, the same plugin call with -DwrapperSha256Sum=<sha256 of maven-wrapper.jar> wrote wrapperSha256Sum. After one byte was appended to the committed jar, the next ./mvnw refused to run it:

Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.
Investigate or delete <work>/ckbin/.mvn/wrapper/maven-wrapper.jar to attempt a clean download.
If you updated your Maven version, you need to update the specified wrapperSha256Sum property.

Two limits to keep in mind. The checksum protects against a corrupted or substituted download, not against a malicious change of the properties file, so changes to .mvn/wrapper/ deserve the same code review as pom.xml. And the check needs sha256sum or shasum on the PATH; without either, the script prints an error asking you to install one or to remove the property. The broader picture, with signatures, Gradle’s wrapper checksum and wrapper-jar validation in CI, is on Supply Chain Security, and the verification of a manually downloaded Maven (SHA-512 and PGP) is on Installation.

Where the distribution comes from

By default the URL points at Maven Central. Behind a firewall, or with a policy that every artifact comes through a repository manager, there are three ways to change it. Nexus, Artifactory and Reposilite were not installed here, so what follows was run against a plain HTTP server on localhost that serves the Central directory layout (org/apache/maven/apache-maven/3.10.0/...), which is what a repository manager proxying Central serves, and the repository-manager specifics are not run here. Repository managers and mirrors in general are on Repositories, Mirrors & Repository Managers.

Edit distributionUrl

The most explicit way is the property itself, either by hand or with -DdistributionUrl=... (plugin 3.3.0 and later). Every developer and CI job then downloads from that address.

MVNW_REPOURL

The page’s second option leaves the committed URL on Maven Central and sets an environment variable on each machine. The script replaces everything before /org/apache/maven/ in distributionUrl with the variable’s value (for a maven-mvnd-* file, everything before /maven/mvnd/):

$ MVNW_REPOURL=http://127.0.0.1:8765 ./mvnw -v
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)
...

When the variable is set while the plugin creates the wrapper, the plugin writes the rewritten URL into the properties file, which is then no longer the Central one:

$ MVNW_REPOURL=http://127.0.0.1:8765 mvn wrapper:wrapper
[INFO] Configuring .mvn/wrapper/maven-wrapper.properties to use Maven 3.10.0 and download from http://127.0.0.1:8765
$ grep distributionUrl .mvn/wrapper/maven-wrapper.properties
distributionUrl=http://127.0.0.1:8765/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip

Do not commit such a file if the address means something only on your network. The variable is the better choice when the project is also built outside the organisation. The mvnw script unsets MVNW_REPOURL, MVNW_USERNAME, MVNW_PASSWORD and MVNW_VERBOSE before it starts Maven.

A mirror in settings.xml

The page also says that if MVNW_REPOURL is unset but a mirror URL is configured in settings.xml, the plugin uses it when it creates the wrapper. In the runs here, only a mirror whose mirrorOf is * was used (run with -o, so that nothing was contacted; the host repo.example.org is a placeholder and was never reached):

<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0">
  <mirrors>
    <mirror>
      <id>central</id>
      <mirrorOf>*</mirrorOf>
      <url>https://repo.example.org/central-proxy</url>
    </mirror>
  </mirrors>
</settings>
$ mvn -o -s settings.xml wrapper:wrapper
[INFO] Configuring .mvn/wrapper/maven-wrapper.properties to use Maven 3.10.0 and download from https://repo.example.org/central-proxy

A mirror with <mirrorOf>central</mirrorOf> or <mirrorOf>external:*</mirrorOf> left the Central address in place. Since the file is written once and then committed, check it after creating the wrapper on a machine that has such a settings file. Mirrors and the settings.xml format are on settings.xml & Password Encryption.

Downloading with credentials

Two ways exist for a distribution server that wants Basic authentication: the environment variables MVNW_USERNAME and MVNW_PASSWORD, or user:password@ in the distributionUrl. A throwaway server on localhost that returns 401 unless the right credentials are sent was used (the credentials below are placeholders that exist only for this test):

$ MVNW_REPOURL=http://127.0.0.1:8766 ./mvnw -v
curl: Failed to fetch http://127.0.0.1:8766/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip

$ MVNW_USERNAME=ci-reader MVNW_PASSWORD=wrong MVNW_REPOURL=http://127.0.0.1:8766 ./mvnw -v
Exception in thread "main" java.net.ProtocolException: Server redirected too many times (20)
	at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1713)
	...

$ MVNW_USERNAME=ci-reader MVNW_PASSWORD=changeit-example MVNW_REPOURL=http://127.0.0.1:8766 ./mvnw -v
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)

The first message is the one to recognise: without the credentials curl -f reports only that it failed to fetch (the server’s 401 is not shown), and with MVNW_USERNAME set the Java downloader is used, so a rejected password shows a Java exception whose text ("redirected too many times") is about the repeated authentication retry. The user:password@ form, set in the properties file, also worked in the run (with curl):

distributionUrl=http://ci-reader:changeit-example@127.0.0.1:8766/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip

The second form commits a secret to the repository and shows it to everyone who can read it. Prefer the environment variables, taken from the CI system’s secret store, with no real value in any file. The settings-based credentials of settings.xml are not used by the wrapper script, which does not read settings.xml at download time.

Where the distribution is stored

The base directory is ~/.m2 unless MAVEN_USER_HOME is set; the distribution is under wrapper/dists. MAVEN_USER_HOME was set in every example here, so that nothing was written to the real ~/.m2. Setting it is also the usual way to put the distributions on a CI cache volume, a read-only home directory or a shared location:

export MAVEN_USER_HOME=/var/cache/maven-user-home
./mvnw -v

The variable moves only the wrapper’s cache; it does not move the local repository (~/.m2/repository), which settings.xml or -Dmaven.repo.local control (see Configuring Maven). Deleting the wrapper/dists directory is always safe: the next call downloads again.

A failed download can leave an empty directory for the version in the cache (for example wrapper/dists/apache-maven-9.9.9/); it does no harm, because the existence test is on the hash-named subdirectory.

Running the wrapper

mvnw forwards all its arguments to mvn, so everything in Running Maven from the Command Line applies: ./mvnw -B -ntp verify, ./mvnw -pl tidepool-engine -am test, and so on. On Windows the equivalent is mvnw.cmd clean install. Notes from the runs:

  • The script sits at the project root, Maven finds .mvn by itself. In a two-module project (tidepool-parent and tidepool-model) with the wrapper at the root and an .mvn/maven.config containing -B and -Dstyle.color=never, ./mvnw validate built both modules from the root, and ../mvnw validate from tidepool-model built just that module. Running ./mvnw from the module failed because there is no mvnw in it:

    $ cd tidepool-model && ../mvnw -ntp validate
    [INFO] Building tidepool-model 1.0-SNAPSHOT
    [INFO] BUILD SUCCESS

    Running ./mvnw inside the module fails with the shell’s own "no such file or directory". A common convenience is a small shell function or alias that walks up the tree to the nearest mvnw; the wrapper itself does not do that.

  • The files in .mvn still apply. .mvn/maven.config, .mvn/jvm.config and .mvn/extensions.xml are read by the Maven that the wrapper started, exactly as with an installed Maven (see Configuring Maven and Extensions).

  • Debugging. mvn wrapper:wrapper -DincludeDebug=true added mvnwDebug and mvnwDebug.cmd beside the other two scripts. They start the same Maven with a debug agent, like the mvnDebug command of the distribution (not run here, because it waits for a debugger).

  • The Maven being used is the Maven of the properties file, whatever is installed. mvnw reads nothing from PATH except java/javac and the download tools, so which mvn and mvn -v can disagree with ./mvnw -v.

Using Maven Daemon

The Maven Daemon (mvnd) is a separate Apache project and is not part of Maven. The plugin can write a wrapper for it: -Dmvnd=<version> replaces the Maven version. It was run with the version 1.0.6, which exists on the Apache download server:

$ mvn wrapper:wrapper -Dmvnd=1.0.6
[INFO] Unpacked only-script type wrapper distribution org.apache.maven.wrapper:maven-wrapper-distribution:zip:only-script:3.3.4
[INFO] Configuring .mvn/wrapper/maven-wrapper.properties to use Maven 3.10.0 and download from https://repo.maven.apache.org/maven2
$ cat .mvn/wrapper/maven-wrapper.properties
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://archive.apache.org/dist/maven/mvnd/1.0.6/maven-mvnd-1.0.6-bin.zip

(The log line still says Maven 3.10.0, which is just the plugin’s message; the property is what counts.) The mvnw script recognises a maven-mvnd-*-bin.zip name. It then swaps -bin for a platform name (windows-amd64, darwin-amd64, darwin-aarch64 or linux-amd64, detected from uname), runs mvnd.sh instead of mvn, and uses /maven/mvnd/ as the pattern for MVNW_REPOURL. On this machine that would have fetched maven-mvnd-1.0.6-darwin-aarch64.zip, which exists on the server; the generic maven-mvnd-1.0.6-bin.zip returned 404. Running ./mvnw against it, and mvnd itself, are not run here: mvnd was not installed and was not downloaded. What it does and its caches are on Build Performance: Cache, Parallel Builds & mvnd.

Committing the wrapper

Commit mvnw, mvnw.cmd and .mvn/wrapper/maven-wrapper.properties, plus maven-wrapper.jar or MavenWrapperDownloader.java for the types that have them. Do not add .mvn/wrapper/ or mvnw to .gitignore, and keep the wrapper out of target. Three details avoid avoidable breakage:

  1. The executable bit. A Unix developer’s ./mvnw needs it, and a Windows developer’s git add may lose it. The 100755 mode of mvnw was visible in git ls-files -s here (macOS), and on Windows the bit can be set in the index: git update-index --chmod=+x mvnw (that command was not run, as no Windows machine was used).

  2. Line endings. mvnw is a shell script, so a CRLF checkout breaks it with errors like a bad interpreter, and mvnw.cmd should keep CRLF. In a repository with no .gitattributes, adding the generated files here printed warning: in the working copy of 'mvnw.cmd', CRLF will be replaced by LF the next time Git touches it (the warning depends on the core.autocrlf and core.safecrlf of the machine). A .gitattributes file pins them:

    /mvnw text eol=lf
    *.cmd text eol=crlf

    With it, git ls-files --eol showed i/lf w/lf attr/text eol=lf mvnw and i/lf w/crlf attr/text eol=crlf mvnw.cmd, and the warning was gone. (This is a common practice rather than an instruction from the Maven documentation.)

  3. Review changes in .mvn/wrapper. The distributionUrl decides which program every developer runs. A pull request that changes it, or the scripts, deserves a reviewer who knows what it does (see Verifying checksums).

A wrapper is also the practical answer to "which Maven does CI use": with ./mvnw in the pipeline the question has one answer, in the repository.

Wrapper on CI

On CI the wrapper replaces an "install Maven" step, and the download is the only cost; cache it so that it happens once per version and not once per job. The most recent documentation of actions/setup-java describes a separate cache entry for ~/.m2/wrapper/dists, keyed on .mvn/wrapper/maven-wrapper.properties, in addition to the local repository cache. A workflow for the demo project (not run here; written against the documentation of actions/setup-java and checked line by line; actions/checkout v7 and actions/setup-java v6 were the latest releases on 2026-10-16):

name: build
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-java@v6
        with:
          distribution: temurin
          java-version: 21
          cache: maven
      - run: ./mvnw -B -ntp verify

The scripts contain no JAVA_HOME default: mvnw uses the java from PATH or JAVA_HOME, which actions/setup-java sets. Other CI systems follow the same pattern: have a JDK, call ./mvnw -B -ntp, and cache wrapper/dists and the local repository. The broader CI guidance for the three tools is on CI & IDE Integration. A container image that already contains a Maven can skip the wrapper, but then the version is pinned by the image tag instead of the repository.

Error messages

All of these were produced by mvnw 3.3.4 with Maven 3.10.0 in the scratch workspace:

Message Cause What to do

./mvnw: line 117: ./.mvn/wrapper/maven-wrapper.properties: No such file or directory followed by cannot read distributionUrl property in ./.mvn/wrapper/maven-wrapper.properties

mvnw was copied without the .mvn/wrapper directory (or the properties file is not committed because .mvn is ignored)

Commit the properties file; check .gitignore for .mvn

curl: Failed to fetch <url> (or wget: Failed to fetch <url>)

A 404 (a version that does not exist: .../9.9.9/apache-maven-9.9.9-bin.zip), a 401/403, a proxy, or an unreachable host (http://127.0.0.1:1/... gave the same message)

Open the URL with curl -fIL <url>; fix the version, the credentials or the proxy variables

distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '...'

The URL does not end in -bin.zip

Use the file name from the repository (apache-maven-<version>-bin.zip)

Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised.

The downloaded archive differs from distributionSha256Sum

If you changed distributionUrl, update the sum with mvn wrapper:wrapper -DdistributionSha256Sum=...; otherwise investigate the source

Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.

maven-wrapper.jar differs from wrapperSha256Sum

Delete the jar to let the script download a clean one, or regenerate the wrapper

The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run.

Only printed on the Java-downloader path: neither wget nor curl was available (or MVNW_USERNAME was set) and there is no JDK

Install a JDK, or install curl; set JAVA_HOME to a JDK, not a JRE

The JAVA_HOME message comes from the script source (set_java_home) and was not triggered here. Everything else in the table was produced by a real run.

Summary

  • mvn wrapper:wrapper -Dmaven=<version> writes mvnw, mvnw.cmd and .mvn/wrapper/maven-wrapper.properties; always pass -Dmaven because the default is the running Maven.

  • The default only-script type needs no binary; script, bin and source use maven-wrapper.jar, and type is kept when the goal is re-run unless -Dtype changes it.

  • mvnw caches the unpacked Maven in MAVEN_USER_HOME/wrapper/dists/<name>/<hash of the URL> (default ~/.m2), so a later call just starts Maven.

  • Upgrade Maven by changing distributionUrl (one reviewed line); pin the download with distributionSha256Sum and, for the jar types, wrapperSha256Sum.

  • MVNW_VERBOSE=true explains the script; MVNW_REPOURL rewrites the repository part of the URL; MVNW_USERNAME and MVNW_PASSWORD supply Basic credentials.

  • Commit the wrapper, keep mvnw executable with LF endings and mvnw.cmd with CRLF, and review .mvn/wrapper changes like code.