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.
Source: Maven: Maven 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.
Source: Maven Wrapper Plugin: Usage
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 |
|---|---|---|
|
|
The Maven version to require. Defaults to the version of the Maven running the goal; any release above 2.0.9 is accepted. |
|
|
|
|
|
The exact URL of the distribution (since plugin 3.3.0). If absent, it is built from the version and the repository URL. |
|
|
The expected SHA-256 of the distribution (since 3.2.0). |
|
|
The expected SHA-256 of |
|
|
The Maven Daemon version to require instead of Maven (since 3.2.0; see Using Maven Daemon). |
|
|
Add the |
|
|
Write |
|
|
Write |
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:
Step by step:
-
mvnwis a POSIXshscript (set -euf, so an unset variable or a failing command stops it). It readsdistributionUrlanddistributionSha256Sumfrom.mvn/wrapper/maven-wrapper.propertiesnext to the script, not from the current directory. That is why../mvnwworks from a module directory. -
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 JavaString.hashCode()of the full URL. For the URL above that isdae9a06d, 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 gavedae9a06dand thelocalhostonee371c5c2. -
If that directory exists, the script
exec`s `bin/mvnin it, and nothing else happens. -
Otherwise it downloads the archive to a temporary directory. It tries
wget, thencurl(curl -f -L), and if neither exists it compiles and runs a tinyDownloader.javawith the JDK (sojavacmust be on thePATHor underJAVA_HOME). WhenMVNW_USERNAMEis set,wgetandcurlare skipped and the Java downloader is used, because it carries the credentials. Ifunzipis missing, it asks for the.tar.gzof the same distribution instead. -
If
distributionSha256Sumis set, the archive is checked withsha256sumorshasum -a 256before unpacking. -
The unpacked directory is moved to the cache, the original URL is written beside it in a file
mvnw.url, andmvnwreplaces itself withmvn.
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
Source: Maven: Maven Wrapper
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 |
|---|---|---|---|
|
|
The script downloads the Maven archive itself with |
no |
|
|
On first use the script downloads |
no (the downloaded jar shows up as an untracked file) |
|
the |
The committed jar downloads and runs Maven, so neither |
yes, the jar |
|
the |
The script can compile and run the Java file to fetch |
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-scriptstores the distribution aswrapper/dists/apache-maven-3.10.0/<hash>, while the jar-based types store it aswrapper/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:wrapperwithout-Dmavenis 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 writes3.9.16into 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_REPOURLvalues 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 changesdistributionUrl. 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 |
|---|---|
|
The version of the wrapper scripts that wrote the file (3.3.4 here). Informational. |
|
|
|
The URL of the Maven archive. The script requires it to end in |
|
Lower-case hexadecimal SHA-256 of the archive; if present the script verifies it (see Verifying checksums). |
|
Where the jar-based types download |
|
Lower-case hexadecimal SHA-256 of |
|
For the jar-based types, download Maven on every run. The |
|
For the jar-based types, unpack on every run. The |
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
.mvnby itself. In a two-module project (tidepool-parentandtidepool-model) with the wrapper at the root and an.mvn/maven.configcontaining-Band-Dstyle.color=never,./mvnw validatebuilt both modules from the root, and../mvnw validatefromtidepool-modelbuilt just that module. Running./mvnwfrom the module failed because there is nomvnwin it:$ cd tidepool-model && ../mvnw -ntp validate [INFO] Building tidepool-model 1.0-SNAPSHOT [INFO] BUILD SUCCESSRunning
./mvnwinside 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 nearestmvnw; the wrapper itself does not do that. -
The files in
.mvnstill apply..mvn/maven.config,.mvn/jvm.configand.mvn/extensions.xmlare read by the Maven that the wrapper started, exactly as with an installed Maven (see Configuring Maven and Extensions). -
Debugging.
mvn wrapper:wrapper -DincludeDebug=trueaddedmvnwDebugandmvnwDebug.cmdbeside the other two scripts. They start the same Maven with a debug agent, like themvnDebugcommand 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.
mvnwreads nothing fromPATHexceptjava/javacand the download tools, sowhich mvnandmvn -vcan disagree with./mvnw -v.
Source: Maven: Maven Wrapper
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:
-
The executable bit. A Unix developer’s
./mvnwneeds it, and a Windows developer’sgit addmay lose it. The100755mode ofmvnwwas visible ingit ls-files -shere (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). -
Line endings.
mvnwis a shell script, so a CRLF checkout breaks it with errors like a bad interpreter, andmvnw.cmdshould keep CRLF. In a repository with no.gitattributes, adding the generated files here printedwarning: in the working copy of 'mvnw.cmd', CRLF will be replaced by LF the next time Git touches it(the warning depends on thecore.autocrlfandcore.safecrlfof the machine). A.gitattributesfile pins them:/mvnw text eol=lf *.cmd text eol=crlfWith it,
git ls-files --eolshowedi/lf w/lf attr/text eol=lf mvnwandi/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.) -
Review changes in
.mvn/wrapper. ThedistributionUrldecides 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 |
|---|---|---|
|
|
Commit the properties file; check |
|
A 404 (a version that does not exist: |
Open the URL with |
|
The URL does not end in |
Use the file name from the repository ( |
|
The downloaded archive differs from |
If you changed |
|
|
Delete the jar to let the script download a clean one, or regenerate the wrapper |
|
Only printed on the Java-downloader path: neither |
Install a JDK, or install |
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>writesmvnw,mvnw.cmdand.mvn/wrapper/maven-wrapper.properties; always pass-Dmavenbecause the default is the running Maven. -
The default
only-scripttype needs no binary;script,binandsourceusemaven-wrapper.jar, andtypeis kept when the goal is re-run unless-Dtypechanges it. -
mvnwcaches the unpacked Maven inMAVEN_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 withdistributionSha256Sumand, for the jar types,wrapperSha256Sum. -
MVNW_VERBOSE=trueexplains the script;MVNW_REPOURLrewrites the repository part of the URL;MVNW_USERNAMEandMVNW_PASSWORDsupply Basic credentials. -
Commit the wrapper, keep
mvnwexecutable with LF endings andmvnw.cmdwith CRLF, and review.mvn/wrapperchanges like code.