Maven Packaging: JARs, WARs & Manifests
|
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, maven-jar-plugin 3.5.1 (which uses Maven Archiver 3.6.6),
maven-war-plugin 3.5.1 and maven-artifact-plugin 3.7.0. Every example was run on JDK 26 on 2026-10-16, in a scratch copy
of the tidepool project (a parent POM and the tidepool-model, tidepool-engine and tidepool-cli modules, all
compiled with maven.compiler.release 21 and every plugin version pinned). Where the behaviour differs, the same command was also run on
Maven 3.9.16 and on Maven 4.0.0-rc-7 (a release candidate, not a final release). Paths in the console output were shortened to
/work/packaging/..., and long console output was filtered with grep, which is said each time.
Packaging is the last step that is specific to the kind of project: the package phase of the
build lifecycle turns target/classes into a file
that can be installed, deployed or run. For <packaging>jar</packaging> (the default) the jar:jar goal does it, for
war the war:war goal. Both plugins build the archive with the same component, Maven Archiver, and so share one
<archive> configuration element. A JAR that bundles all its dependencies (a "fat" or "uber" JAR) is a separate topic, covered
in Uber JARs with Shade & Assembly.
This page covers the plain archives and what is inside their manifests. How a plugin is declared, pinned and configured is in
Plugins & Their Configuration, and how the
archive then gets into a repository is in
Install, Deploy & Distribution Management.
What the jar plugin produces by default
A module with no packaging configuration at all gets a JAR containing the compiled classes and the main resources, a manifest, and
a copy of the POM. Building tidepool-engine with mvn package and listing the result with unzip -l shows it:
$ mvn -q package -DskipTests -pl tidepool-engine -am
$ unzip -l tidepool-engine/target/tidepool-engine-1.0.0-SNAPSHOT.jar
Archive: tidepool-engine/target/tidepool-engine-1.0.0-SNAPSHOT.jar
Length Date Time Name
--------- ---------- ----- ----
0 02-01-1980 00:00 META-INF/
99 02-01-1980 00:00 META-INF/MANIFEST.MF
0 02-01-1980 00:00 io/
0 02-01-1980 00:00 io/tidepool/
0 02-01-1980 00:00 io/tidepool/engine/
0 02-01-1980 00:00 META-INF/maven/
0 02-01-1980 00:00 META-INF/maven/io.tidepool/
0 02-01-1980 00:00 META-INF/maven/io.tidepool/tidepool-engine/
2530 02-01-1980 00:00 io/tidepool/engine/StationReader.class
1775 02-01-1980 00:00 io/tidepool/engine/TideEngine.class
732 02-01-1980 00:00 META-INF/maven/io.tidepool/tidepool-engine/pom.xml
70 02-01-1980 00:00 META-INF/maven/io.tidepool/tidepool-engine/pom.properties
--------- -------
5206 12 files
Source: Apache Maven JAR Plugin
The entries are, in order of interest:
-
META-INF/MANIFEST.MFis the manifest, described in the next section. The default one has four lines and noMain-Class, sojava -jarrefuses the file:no main manifest attribute, in tidepool-engine/target/tidepool-engine-1.0.0-SNAPSHOT.jar(run here, exit status 1). -
META-INF/maven/<groupId>/<artifactId>/pom.xmlandpom.propertiesare the "Maven descriptor".pom.propertiesholds the three linesartifactId,groupIdandversion, and tools such as IDEs use the pair to recognise a JAR as a Maven artifact. They can be switched off, see below. -
The date column shows 1980-02-01, which is not a bug but the reproducible-build timestamp, explained in Reproducible builds.
The dates in the listing are the same on Maven 3.10.0 and on 4.0.0-rc-7, but not on 3.9.16, where each entry carries the time of the build.
How the jar is attached and when it is rebuilt
jar:jar is bound to package for the jar packaging (see
Build Lifecycle); its execution id is default-jar. The
file is written to ${project.build.directory} as <artifactId>-<version>[-<classifier>].jar and attached to the project as
its main artifact, which is what install and deploy publish. The parameters that matter most:
| Parameter | Meaning | Default and user property |
|---|---|---|
|
The directory packaged into the JAR. |
|
|
Fileset patterns, relative to |
Everything (but see |
|
Whether editor and VCS litter ( |
|
|
Attaches the file as a supplemental artifact ( |
none |
|
Do not create an archive when there is nothing to put in it. |
|
|
Rebuild even when the inputs look unchanged. Needed when another plugin post-processes the JAR, such as the shade plugin. |
|
|
Whether the JAR is attached to the project. |
|
|
If |
|
|
Timestamp for every entry of the archive. |
|
|
The Maven Archiver configuration, next section. |
none |
Source: jar:jar
The list above was read from mvn jar:help -Ddetail -Dgoal=jar, which prints every parameter of the installed version and is the
quickest way to check what a given plugin release accepts. The older useDefaultManifestFile parameter is only there to fail
the build if used; the plugin’s manifest is configured with archive.
The archive element: Maven Archiver
The jar, war, ejb and ear plugins, and the assembly plugin, all accept an <archive> child in their configuration and
hand it to Maven Archiver, a shared component with its own versioning. Its elements are:
| Element | Effect |
|---|---|
|
Include |
|
Compress the archive (default |
|
Recreate the archive every time (default |
|
Create an |
|
Replace the generated |
|
A manifest file of your own, merged with the generated one. |
|
Settings for the main section of |
|
Free key/value pairs added to the main section. |
|
Named sections of the manifest, each with its own |
Source: Maven Archiver: archive
The <manifest> element controls the generated main-section attributes:
| Element | Effect |
|---|---|
|
The |
|
Create a |
|
Text put in front of every |
|
|
|
The expression used when the layout is |
|
For snapshots, use the timestamped file name instead of |
|
Create an |
|
The |
|
The default |
|
|
|
|
|
|
Source: Maven Archiver: manifest
The Implementation-* and Specification-* entries are off by default since maven-jar-plugin 2.1; the official manifest page
says so and that they must be asked for explicitly. Two of the defaults are worth knowing: Java-Version reports the
release the code was compiled for (here 21, from maven.compiler.release), while Build-Jdk-Spec reports the JDK that ran
the build (26), and the Created-By value is chosen by the plugin, so a JAR says Maven JAR Plugin 3.5.1 and a WAR says
Maven WAR Plugin 3.5.1.
Source: Manifest customization
The official "Guide to Working with Manifests" is only a pointer to the Maven Archiver page above, and the "Guide to
Configuring Archive Plugins" contains the addMavenDescriptor recipe.
Source: Guide to Working with Manifests
A runnable JAR with a lib directory
The most common use of the manifest is a JAR that java -jar can start. tidepool-cli has a Main class, and its
dependencies (tidepool-engine, tidepool-model, Commons CSV with its two transitive libraries, and picocli) have to be found at
run time. The manifest can name them through Class-Path, as paths relative to the location of the JAR, so the
dependencies have to be copied next to it. The dependency:copy-dependencies goal of the dependency plugin does that.
<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>io.tidepool.cli.Main</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<Built-By>tidepool-build</Built-By>
<Automatic-Module-Name>io.tidepool.cli</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.11.0</version>
<executions>
<execution>
<id>copy-runtime-libs</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
The jar plugin’s version comes from pluginManagement in the parent POM. The dependency plugin runs in package after the
jar plugin, because both are bound to the same phase and a plugin bound by the packaging runs before one added in the POM. Building
the whole reactor and running the result:
$ mvn -B package -DskipTests | grep -E "Building jar|copy-dependencies|BUILD"
[INFO] Building jar: /work/packaging/tidepool-model/target/tidepool-model-1.0.0-SNAPSHOT.jar
[INFO] Building jar: /work/packaging/tidepool-model/target/tidepool-model-1.0.0-SNAPSHOT-tests.jar
[INFO] Building jar: /work/packaging/tidepool-engine/target/tidepool-engine-1.0.0-SNAPSHOT.jar
[INFO] Building jar: /work/packaging/tidepool-cli/target/tidepool-cli-1.0.0-SNAPSHOT.jar
[INFO] --- dependency:3.11.0:copy-dependencies (copy-runtime-libs) @ tidepool-cli ---
[INFO] BUILD SUCCESS
$ ls tidepool-cli/target/lib
commons-codec-1.19.0.jar
commons-csv-1.14.1.jar
commons-io-2.20.0.jar
picocli-4.7.7.jar
tidepool-engine-1.0.0-SNAPSHOT.jar
tidepool-model-1.0.0-SNAPSHOT.jar
$ unzip -p tidepool-cli/target/tidepool-cli-1.0.0-SNAPSHOT.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.1
Java-Version: 21
Build-Jdk-Spec: 26
Class-Path: lib/tidepool-engine-1.0.0-SNAPSHOT.jar lib/tidepool-model-1.
0.0-SNAPSHOT.jar lib/commons-csv-1.14.1.jar lib/commons-io-2.20.0.jar l
ib/commons-codec-1.19.0.jar lib/picocli-4.7.7.jar
Implementation-Title: tidepool-cli
Implementation-Version: 1.0.0-SNAPSHOT
Main-Class: io.tidepool.cli.Main
Automatic-Module-Name: io.tidepool.cli
Built-By: tidepool-build
$ java -jar tidepool-cli/target/tidepool-cli-1.0.0-SNAPSHOT.jar data/stations.csv SF
San Francisco: -0,869 m
Source: Maven Archiver: manifest
A few details of that output:
-
The manifest specification limits a line to 72 bytes, so the long
Class-Pathis wrapped, and each continuation line begins with a single space. Concatenate the lines after removing that space to read the value. -
The
Class-Pathorder is the order of the plugin’s dependency list. Maven 3.10 changed how the resolver flattens the dependency graph to breadth-first, but the change showed up only in plugin class realms, not in the project’s own compile classpath (see Dependencies, Scopes & Mediation), and the order written to this manifest was identical when the same project was built with 3.9.16. -
Built-ByandAutomatic-Module-Nameare ordinarymanifestEntries; any name can be added.Automatic-Module-Namegives a stable module name to a library JAR that has nomodule-info.class, and is the usual reason to touchmanifestEntriesin a library. -
The number decoration (
-0,869with a comma) is the JVM’s default locale at run time, not part of the packaging.
The Class-Path entries are resolved relative to the JAR, not to the current directory. Copying the JAR alone to another directory
and starting it there fails with Exception in thread "main" java.lang.NoClassDefFoundError: picocli/CommandLine, and
starting it from another directory (java -jar with an absolute path to the original) still works. This is why a runnable JAR of this
kind is distributed as a directory, usually zipped by the assembly plugin (see
Uber JARs with Shade & Assembly); one
file with everything inside is the shade or assembly approach on that page.
Other Class-Path layouts
classpathLayoutType changes what each Class-Path entry looks like. repository writes the path the artifact has in a Maven
repository, which is useful when the JAR is started from a directory that is a copy of a repository (an application server’s
lib/ is not that). Building a copy of the CLI with this configuration (and with addMavenDescriptor set to false) gave:
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifest>
<addClasspath>true</addClasspath>
<classpathLayoutType>repository</classpathLayoutType>
<mainClass>io.tidepool.cli.Main</mainClass>
</manifest>
</archive>
$ unzip -p target/tidepool-cli-repo-1.0.0-SNAPSHOT.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.1
Java-Version: 21
Build-Jdk-Spec: 26
Class-Path: io/tidepool/tidepool-engine/1.0.0-SNAPSHOT/tidepool-engine-1
.0.0-SNAPSHOT.jar io/tidepool/tidepool-model/1.0.0-SNAPSHOT/tidepool-mo
del-1.0.0-SNAPSHOT.jar org/apache/commons/commons-csv/1.14.1/commons-cs
v-1.14.1.jar commons-io/commons-io/2.20.0/commons-io-2.20.0.jar commons
-codec/commons-codec/1.19.0/commons-codec-1.19.0.jar info/picocli/picoc
li/4.7.7/picocli-4.7.7.jar
Main-Class: io.tidepool.cli.Main
unzip -l on that JAR listed no entry containing maven, which is the effect of addMavenDescriptor set to
false: the JAR has no META-INF/maven directory at all. The guide to archive configuration shows the same switch on the WAR
plugin, and it applies to every plugin that takes <archive>.
With custom the layout is an expression evaluated against the artifact. The following was run, together with a second set of
manifest features (an extra manifest file, a named section and the default specification and build-environment entries):
<archive>
<manifestFile>src/main/manifest/extra.mf</manifestFile>
<manifest>
<addClasspath>true</addClasspath>
<classpathLayoutType>custom</classpathLayoutType>
<customClasspathLayout>lib/${artifact.artifactId}.${artifact.extension}</customClasspathLayout>
<mainClass>io.tidepool.cli.Main</mainClass>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
<addBuildEnvironmentEntries>true</addBuildEnvironmentEntries>
</manifest>
<manifestEntries>
<Tidepool-Edition>enterprise</Tidepool-Edition>
</manifestEntries>
<manifestSections>
<manifestSection>
<name>io/tidepool/cli/</name>
<manifestEntries>
<Sealed>true</Sealed>
</manifestEntries>
</manifestSection>
</manifestSections>
</archive>
where src/main/manifest/extra.mf contains the two lines Sealed: true and Tidepool-Edition: community:
$ unzip -p target/tidepool-cli-sections-1.0.0-SNAPSHOT.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.1
Java-Version: 21
Build-Jdk-Spec: 26
Build-Tool: Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f547
2)
Build-Jdk: 26 (Oracle Corporation)
Build-Os: Mac OS X (27.0.1; aarch64)
Class-Path: lib/tidepool-engine.jar lib/tidepool-model.jar lib/commons-c
sv.jar lib/commons-io.jar lib/commons-codec.jar lib/picocli.jar
Specification-Title: tidepool-cli-sections
Specification-Version: 1.0
Main-Class: io.tidepool.cli.Main
Tidepool-Edition: community
Sealed: true
Name: io/tidepool/cli/
Sealed: true
The result shows how the pieces combine. Every main-section attribute comes from the union of the generated defaults, the
manifest element, manifestEntries and the file. When the same key is set in two places, here Tidepool-Edition, the value
in the manifest file is the one in the JAR; the enterprise value in the POM was dropped. Do not rely on the merge rule, give each key one
home. A manifestSection writes a separate block after a blank line whose first line is Name:, used for per-package attributes such as Sealed. The
Build-* lines record the machine and tool that built the file, so they make a build less reproducible, and the plugin
leaves them off unless addBuildEnvironmentEntries is turned on (the Specification-Version is the major and minor
version only).
Classifier JARs, includes and excludes
A second JAR from the same module is produced by adding an execution of jar:jar with a classifier. This one contains only the
station reader:
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<id>reader-only</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classifier>reader</classifier>
<includes>
<include>io/tidepool/engine/StationReader*.class</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
$ mvn -B package -DskipTests | grep -E "Building jar|BUILD"
[INFO] Building jar: /work/packaging/v3/target/tidepool-engine-slim-1.0.0-SNAPSHOT.jar
[INFO] Building jar: /work/packaging/v3/target/tidepool-engine-slim-1.0.0-SNAPSHOT-reader.jar
[INFO] BUILD SUCCESS
$ unzip -l target/tidepool-engine-slim-1.0.0-SNAPSHOT-reader.jar | grep class
2530 02-01-1980 00:00 io/tidepool/engine/StationReader.class
The classic mistake is to forget the classifier. A second jar:jar execution without it would try to replace the main
artifact, and the build stops:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-jar-plugin:3.5.1:jar (reader-only) on project tidepool-engine-slim: You have to use a classifier to attach supplemental artifacts to the project instead of replacing them. -> [Help 1]
A consumer asks for such a JAR with <classifier>reader</classifier> in its dependency. The conventional classifiers (sources,
javadoc, tests) are produced by the source, javadoc and jar plugins in the same way (see
Publishing to Maven Central, which needs the first two).
Test JARs
Test classes, helpers and fixtures are not part of the main artifact, but another module may need them. The jar plugin’s
test-jar goal packages target/test-classes into a JAR with the classifier tests. The plugin documentation calls the
approach below the preferred way, as opposed to a second execution with a classifier.
<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
$ mvn -B verify -DskipTests -pl tidepool-model,tidepool-engine | grep -E "jar:|Building jar|BUILD"
[INFO] --- jar:3.5.1:jar (default-jar) @ tidepool-model ---
[INFO] Building jar: /work/packaging/tidepool-model/target/tidepool-model-1.0.0-SNAPSHOT.jar
[INFO] --- jar:3.5.1:test-jar (default) @ tidepool-model ---
[INFO] Building jar: /work/packaging/tidepool-model/target/tidepool-model-1.0.0-SNAPSHOT-tests.jar
[INFO] --- jar:3.5.1:jar (default-jar) @ tidepool-engine ---
[INFO] Building jar: /work/packaging/tidepool-engine/target/tidepool-engine-1.0.0-SNAPSHOT.jar
[INFO] BUILD SUCCESS
$ mvn -B dependency:tree -pl tidepool-engine | grep -E '^\[INFO\] (\+|\||\\)|tidepool-engine:jar'
[INFO] io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
[INFO] +- io.tidepool:tidepool-model:jar:1.0.0-SNAPSHOT:compile
[INFO] +- org.apache.commons:commons-csv:jar:1.14.1:compile
[INFO] | +- commons-io:commons-io:jar:2.20.0:compile
[INFO] | \- commons-codec:commons-codec:jar:1.19.0:compile
[INFO] \- io.tidepool:tidepool-model:test-jar:tests:1.0.0-SNAPSHOT:test
The test-jar goal is bound to package by default, takes its output from testClassesDirectory
(${project.build.testOutputDirectory}) and has the classifier tests. Its dependency is declared with type test-jar (or
equivalently <classifier>tests</classifier>) and almost always test scope, because production code that needs a test JAR is a sign that
the helper belongs in a module of its own. In a reactor build the dependency resolved even when only the test phase was
requested (mvn clean test -pl tidepool-model,tidepool-engine ran the model’s HeightTest successfully on both 3.10.0 and
3.9.16), because the reactor then points at target/test-classes. Installing or using the module outside the reactor needs
the real JAR, i.e. package at least. The test JAR is also an artifact to install and deploy, so it lands in the repository
next to the main one, which is the case in the file repository used for the reproducibility check below (...-tests.jar).
WAR files
A <packaging>war</packaging> module is a web application. The war plugin lays out an exploded directory (WEB-INF/classes,
WEB-INF/lib, the contents of src/main/webapp) under target/<finalName> and zips it. The war packaging’s other
lifecycle bindings are in Build Lifecycle. A small module
tidepool-web was used to try it: one servlet (compiled against jakarta.servlet-api 6.1.0 in provided scope, so the
container supplies it), a static page, and a dependency on tidepool-engine.
war in the scratch copy)<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>tidepool-web</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>tidepool</finalName>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<packagingExcludes>static/notes.txt,WEB-INF/lib/commons-codec-*.jar</packagingExcludes>
<webResources>
<resource>
<directory>extra</directory>
<targetPath>WEB-INF</targetPath>
<includes>
<include>BUILD-INFO.txt</include>
</includes>
</resource>
</webResources>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifestEntries>
<Tidepool-Edition>web</Tidepool-Edition>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
$ mvn -B package | grep -E "war:|Building war|Assembling|Copying webapp|BUILD"
[INFO] --- war:3.5.1:war (default-war) @ tidepool-web ---
[INFO] Assembling webapp [tidepool-web] in [/work/packaging/war/target/tidepool]
[INFO] Copying webapp webResources [/work/packaging/war/extra] to [/work/packaging/war/target/tidepool]
[INFO] Copying webapp resources [/work/packaging/war/src/main/webapp]
[INFO] Building war: /work/packaging/war/target/tidepool.war
[INFO] BUILD SUCCESS
$ unzip -l war/target/tidepool.war
Archive: war/target/tidepool.war
Length Date Time Name
--------- ---------- ----- ----
0 02-01-1980 00:00 META-INF/
122 02-01-1980 00:00 META-INF/MANIFEST.MF
0 02-01-1980 00:00 WEB-INF/
0 02-01-1980 00:00 WEB-INF/classes/
0 02-01-1980 00:00 WEB-INF/classes/io/
0 02-01-1980 00:00 WEB-INF/classes/io/tidepool/
0 02-01-1980 00:00 WEB-INF/classes/io/tidepool/web/
0 02-01-1980 00:00 WEB-INF/lib/
0 02-01-1980 00:00 static/
24 02-01-1980 00:00 WEB-INF/BUILD-INFO.txt
1568 02-01-1980 00:00 WEB-INF/classes/io/tidepool/web/StationsServlet.class
60964 02-01-1980 00:00 WEB-INF/lib/commons-csv-1.14.1.jar
563971 02-01-1980 00:00 WEB-INF/lib/commons-io-2.20.0.jar
4396 02-01-1980 00:00 WEB-INF/lib/tidepool-engine-1.0.0-SNAPSHOT.jar
5524 02-01-1980 00:00 WEB-INF/lib/tidepool-model-1.0.0-SNAPSHOT.jar
59 02-01-1980 00:00 index.html
34 02-01-1980 00:00 static/site.css
--------- -------
17 files
$ unzip -p war/target/tidepool.war META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven WAR Plugin 3.5.1
Java-Version: 21
Build-Jdk-Spec: 26
Tidepool-Edition: web
Source: Apache Maven WAR Plugin
What the configuration did:
-
finalNameis a build-level setting, not a war plugin one, and renamestidepool-web-1.0.0-SNAPSHOT.wartotidepool.war. A war with a fixed name is what a servlet container deploys under a stable context path. The artifact installed and deployed to a repository is still named by the coordinates, so the file name is only about the file intarget. -
packagingExcludesremovedstatic/notes.txt(a file insrc/main/webappthat must not ship) and the Commons Codec JAR fromWEB-INF/lib(it is a transitive dependency that the container is assumed to provide). The war plugin documents this as the way to include or exclude files after the webapp has been assembled;warSourceExcludesdoes the equivalent on the source directory before assembly. The user propertymaven.war.packagingExcludessets the same list on the command line. -
webResourcesadds a directory that is not undersrc/main/webapp, with an optionaltargetPath,includes,excludesandfiltering. The example copies one file intoWEB-INF. -
The
providedservlet API is not inWEB-INF/lib, as it must not be: the container supplies it. -
The servlet class is in
WEB-INF/classes, because a module withwarpackaging compiles like any Java module; only the last step differs.
web.xml and the Servlet API
Since the Servlet 3.0 specification a web.xml is optional, because servlets can be declared with annotations (the example uses
@WebServlet). The war plugin’s failOnMissingWebXml parameter is documented as defaulting to false since 3.1.0 if the project
depends on the Servlet 3.0 API or newer, and the example above worked with the line removed, because
jakarta.servlet-api is on its classpath. A module with no servlet API and no src/main/webapp/WEB-INF/web.xml, which is the
case of a module that only holds static files, fails:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-war-plugin:3.5.1:war (default-war) on project tidepool-static: Error assembling WAR: webxml attribute is required (or pre-existing WEB-INF/web.xml if executing in update mode) -> [Help 1]
The cure is <failOnMissingWebXml>false</failOnMissingWebXml>, as in the tidepool-web configuration above. Setting it
explicitly is also the safer habit, because the default depends on dependency detection.
Other war parameters
The goals of the war plugin are war (the one bound to package), exploded and inplace (assemble the web application as a directory
for a quick local run) and manifest. The parameters, taken from mvn war:help -Ddetail, include:
| Parameter | Meaning |
|---|---|
|
The web source directory (default |
|
Use a |
|
Additional directories merged into the archive, optionally filtered. |
|
Merge the contents of another WAR (an overlay, a dependency of type |
|
Filter the assembled files before they are zipped. |
|
Rename the JARs in |
|
Put |
|
Resource filtering of web resources (see Resources & Filtering). |
|
Recompress the JARs and zips copied into the archive (default |
|
Skip the goal (user property |
|
Attach the WAR with a classifier instead of as the main artifact. |
|
Maven Archiver configuration, as above. |
Source: war:war
The war plugin documents a manifest Class-Path recipe of its own: with addClasspath set a WAR gets the entries of its dependencies,
which duplicates WEB-INF/lib, so the recipe controls which dependencies appear in the manifest and which in the directory.
Source: WAR manifest customization
Spring Boot and Quarkus applications are packaged by their own plugins (see Uber JARs with Shade & Assembly for the general approach and Quarkus build tooling for one framework’s); the classic WAR is still the deployable for servlet containers.
Reproducible builds
A build is reproducible when the same sources, build environment and build instructions give bit-by-bit identical files.
For packaging the main enemy is time: ZIP entries carry a modification time. The plugins that write archives therefore accept a
single outputTimestamp, which the whole build sets through the project.build.outputTimestamp property, as an ISO 8601
date-time (2026-10-16T00:00:00Z) or as epoch seconds. With it set the jar, war, source, javadoc, assembly and shade plugins
write that value into every entry.
The default since Maven 3.10
Before Maven 3.10 a project opted in by defining the property itself. Since Maven 3.10.0 (and 4.0.0-beta-5) the super POM
defines it, so that every project is in the reproducible mode without touching its POM, with the fixed value
1980-02-01T00:00:00Z. The same lookup in the three Maven versions:
The command help:evaluate -Dexpression=project.build.outputTimestamp -q -DforceStdout -pl tidepool-engine printed, in each
Maven:
| Maven | Output |
|---|---|
3.9.16 |
|
3.10.0 |
|
4.0.0-rc-7 |
|
The effect shows in the SHA-256 of the engine JAR, built twice with three seconds between the builds and no property in any POM (the first 16 hexadecimal digits are shown):
== 3.10.0
acc73b37f6487efd
acc73b37f6487efd
== 3.9.16
15f1cd5b90e6af4c
5fe2087cb7ea21b4
Maven 3.10 produced the same file twice, and 3.9.16 produced two different ones. On 3.9.16 the property can be given on
the command line, -Dproject.build.outputTimestamp=2026-10-16T00:00:00Z, and then both builds were identical
(009dc46da9c5e874, entries dated 10-16-2026 00:00). In your POM, set it in <properties>, so that the value is visible and the
build behaves the same on 3.9:
<properties>
<project.build.outputTimestamp>2026-10-16T00:00:00Z</project.build.outputTimestamp>
</properties>
The official guide gives two ways to keep this value current: the release plugin (3.0.0-M1 or later) updates it in the same
commit that sets the release version, and the versions plugin’s versions:set updates it from release 2.9.0. A fixed value
in a parent POM, as here, is perfect for a snapshot; a release should carry a timestamp from its own release. Some projects
use the last Git commit time (${git.commit.time}) for it. See
Releasing.
To opt out, the guide says any non-numeric single-character value disables the mode. Tried here with
-Dproject.build.outputTimestamp=x on 3.10.0: two builds three seconds apart gave different SHA-256 values (7d52307fcb1cbf10
and 4996fb6b4aa5f420) and entries dated with the minute of the build.
Checking it with artifact:compare
Equal hashes between two local builds prove little. The artifact plugin’s compare goal compares the artifacts of the current
build with a reference build, which is either the one in the local repository or the one in a repository named with
-Dreference.repo. It exits with an error if the files differ, and writes the result to target/<artifactId>-<version>.buildcompare.
To try it without a remote server, a standalone copy of tidepool-model with the release version 1.0.0 (snapshots get
timestamped names, which would not compare) was deployed to a file repository with
mvn clean deploy -DaltDeploymentRepository=ref::file:///work/packaging/refrepo; the copy of the artifacts that the deploy put
into the local repository was then deleted, so that the reference is read from the file repository and the comparison cannot
use it. The rebuild:
$ mvn -B clean verify artifact:compare -Dreference.repo=ref::file:///work/packaging/refrepo | grep -E "artifact:|Checking|Comparing|reference build|rebuild comparison|saved to|BUILD|WARN|ERROR"
[INFO] --- artifact:3.7.0:compare (default-cli) @ tidepool-model ---
[WARNING] No source information available in buildinfo for rebuilders...
[INFO] Checking against reference build from local repository or ref::file:///work/packaging/refrepo...
[INFO] Comparing against 2 reference files from ref (file:///work/packaging/refrepo, default, releases+snapshots)
[INFO] reference build java.version: 26 (from MANIFEST.MF Build-Jdk-Spec)
[INFO] reference build os.name: Unix (from pom.properties newline)
[INFO] [Reproducible Builds] rebuild comparison result: 2 files match
[INFO] saved to target/tidepool-model-1.0.0.buildcompare
[INFO] BUILD SUCCESS
Rebuilding with a different timestamp (-Dproject.build.outputTimestamp=2026-10-17T00:00:00Z) is what a non-reproducible
build looks like:
[ERROR] sha512 mismatch tidepool-model-1.0.0.jar: investigate with diffoscope target/reference/io.tidepool/tidepool-model-1.0.0.jar target/tidepool-model-1.0.0.jar
[ERROR] [Reproducible Builds] rebuild comparison result: 1 files match, 1 differ
[ERROR] saved to target/tidepool-model-1.0.0.buildcompare
[ERROR] [Reproducible Builds] to analyze the differences, see diffoscope instructions in target/tidepool-model-1.0.0.buildcompare
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-artifact-plugin:3.7.0:compare (default-cli) on project tidepool-model: Rebuilt artifacts are different from reference -> [Help 1]
$ cat target/tidepool-model-1.0.0.buildcompare
version=1.0.0
ok=1
ko=1
ignored=0
okFiles="tidepool-model-1.0.0.pom"
koFiles="tidepool-model-1.0.0.jar"
ignoredFiles=""
reference_java_version="26 (from MANIFEST.MF Build-Jdk-Spec)"
reference_os_name="Unix (from pom.properties newline)"
# diffoscope target/reference/io.tidepool/tidepool-model-1.0.0.jar target/tidepool-model-1.0.0.jar
The POM matched, the JAR did not, and the file ends with the diffoscope command that would show where the two JARs differ
(diffoscope is not installed here, so that step was not run). The guide’s own quick test is mvn clean install followed by
mvn clean verify artifact:compare, and for a staged release mvn verify artifact:compare -Dreference.repo=<staging URL>.
The guide warns that a local comparison does not prove independence from the environment (user name, directory, JDK version
and the line endings that differ between Windows and Unix); a real check rebuilds on a different machine. The goal reads the JDK version and the operating system family
of the reference build from its manifest and pom.properties (the two reference build lines above), and the rebuild writes its own
target/*.buildinfo file.
Source: artifact:compare
The companion goal artifact:check-buildplan lists plugins in the build that are known not to be reproducible. For the standalone project:
$ mvn -B clean package artifact:check-buildplan | grep -E "check-buildplan|No known|BUILD"
[INFO] --- artifact:3.7.0:check-buildplan (default-cli) @ tidepool-model ---
[INFO] No known issue in 6 plugins
[INFO] BUILD SUCCESS
Source: artifact:check-buildplan
What else breaks reproducibility
Two elements of the archive configuration of this page make a JAR depend on the environment: addBuildEnvironmentEntries
(operating system, JDK and Maven version) and a Built-By/Build-Time entry computed at build time. Dependency version ranges (see
Versions, Snapshots & Ranges) and unpinned plugin
versions change the content with no change in your project: an unpinned jar or war plugin gets the version in the running Maven’s
lifecycle bindings, which changes with every Maven upgrade, so pin every plugin (see Enforcer and
Security & Reproducible Builds for the
checks and the supply-chain side). The JDK is also a factor: even with the same release, different major JDK versions generate
different bytecode, and the guide advises recording it.
Changes between Maven versions
| Version | What changes for packaging |
|---|---|
3.9.16 |
|
3.10.0 |
|
4.0.0-rc-7 (release candidate) |
Same default timestamp (checked with the lookup above and the entry dates of the engine JAR). Maven 4 also introduces new dependency
types for JARs, |
The jar plugin has pre-releases that are not used here: 4.0.0-beta-1 of maven-jar-plugin and 4.0.0-beta-5 of Maven Archiver
exist on Central, but the stable versions named at the top of the page are the ones that were run.
Where older material is outdated
The Maven 2 book that informed these pages (Maven 2.2) has little to say on packaging beyond the lifecycle, but a few things that circulate with it are out of date:
-
Plugin versions were commonly left out of the POM. Maven 3 warns about it, and an unpinned
maven-jar-pluginormaven-war-plugintakes the version in the running Maven’s lifecycle bindings, which changes from one Maven release to the next; declare them with a version (directly or inpluginManagement). -
A manifest
Class-Pathplus alib/directory is documented for the oldmaven-assembly-plugin, but for an executable JAR the assembly plugin’s descriptor and the shade plugin have taken over (see Uber JARs with Shade & Assembly). -
The
jar.useDefaultManifestFileparameter and themanifestFile-only recipes of old posts: the former now only breaks the build, and the manifest is configured witharchive. -
A
web.xmlis not required for a servlet 3.0 WAR, and thejavax.servletcoordinates were replaced byjakarta.servletin Servlet 5 and later. -
Reproducibility was not a concept in Maven 2. The timestamp in the archives was the build time.
Equivalents in Ant and Gradle
| Maven | Ant | Gradle |
|---|---|---|
|
The |
The |
|
|
An entry |
|
The |
Write |
Test JAR with the |
A second |
The |
Classifier JAR |
Another |
A second |
WAR from |
The |
The |
|
Set the |
Archive tasks with |
The cross-tool picture is in Comparing Ant, Maven and Gradle.