Installing Third-Party JARs & Relocation
|
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 with the Apache Maven Install Plugin 3.2.0 and Deploy Plugin 3.2.0. Every example
was run on JDK 26 on 2026-10-16. Maven 3.9.16 (Install and Deploy Plugin 3.1.4) and Maven 4.0.0-rc-7 (a release candidate) were run
where the page says so. Paths in the console output were shortened to ./, the working directory of the experiment, and from every
listing the Scanning for projects line, the banner and separator lines, blank [INFO] lines, the Total time and Finished at
lines, the standard Help 1 trailer of a failed build and the Loaded N auto-discovered prefixes line that Maven 3.10 prints
while resolving were left out. Nothing was published anywhere except to file:// repositories inside the scratch directory. A
command against a real remote repository is marked "not run here".
Sooner or later a build needs a JAR that no public repository has: a vendor SDK with a closed licence, a driver that cannot be
redistributed, a legacy library nobody publishes any more. Maven resolves dependencies by coordinates, so the job is always the
same: give the file coordinates and put it, with a POM, at the right place in a repository. This page shows the tools Maven has
for that, in the order you should consider them, and then the other side of the coin: telling consumers that an artifact moved.
The repositories themselves are on
Repositories, Mirrors & Repository Managers
and the everyday install and deploy phases on
Install, Deploy & Distribution Management.
Which way to put a JAR into a build
| Approach | Good for | Cost |
|---|---|---|
A repository manager (Nexus, Artifactory, Reposilite) with |
A team or a CI server. The JAR is uploaded once and every developer and build resolves it like any other artifact. |
You need the manager and credentials. |
|
One developer, a quick experiment, a CI job that installs the JAR before the build. |
The JAR exists only on that machine; everybody repeats the step. |
A project-local repository ( |
A small project that must build from a plain clone, with the JAR committed next to the code. |
A binary in version control, and a repository declaration in the POM. |
|
Nothing. It is deprecated, and dependent projects cannot resolve it. |
Warnings on every build; see below. |
A JAR that must be redistributed is a licensing question before it is a Maven question. Check the vendor’s terms before you upload it to a shared repository.
The vendor JAR used in this page
The experiments use a stand-in for a vendor library: one class in the package acme.gauge, built into
acme-gauge-sdk-2.4.jar, plus a sources JAR. It has no pom.xml inside, like most vendor JARs. These files, the consumer class and its POM below are this page’s own lab setup, not examples from the Maven documentation.
javac -d classes src/acme/gauge/TideGauge.java
jar --create --file acme-gauge-sdk-2.4.jar -C classes .
jar --create --file acme-gauge-sdk-2.4-sources.jar -C src .
META-INF/
META-INF/MANIFEST.MF
acme/
acme/gauge/
acme/gauge/TideGauge.class
(That is the content of acme-gauge-sdk-2.4.jar.) The consuming project is the tidepool ingest module, io.tidepool:tidepool-ingest,
with one class that calls the vendor library:
package io.tidepool.ingest;
import acme.gauge.TideGauge;
public final class GaugeFeed {
public static String latest() {
return TideGauge.reading("Bilbao");
}
}
Its POM declares the dependency the normal way, with coordinates of your choosing (the vendor’s name is a fine groupId, you pick the rest) and nothing else special:
<dependencies>
<dependency>
<groupId>com.acme.gauge</groupId>
<artifactId>acme-gauge-sdk</artifactId>
<version>2.4</version>
</dependency>
</dependencies>
To keep ~/.m2 clean every experiment used a throw-away local repository, set once through the MAVEN_ARGS environment variable
(Maven 3.9 and later; see Configuring Maven):
export MAVEN_ARGS="-B -ntp -Dmaven.repo.local=$PWD/local-repo"
The failure you are fixing
With nothing installed, the build cannot resolve the dependency. On Maven 3.10.0:
$ mvn compile
[INFO] Building tidepool-ingest 1.0.0-SNAPSHOT
[WARNING] The POM for com.acme.gauge:acme-gauge-sdk:jar:2.4 is missing, no dependency information available
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal on project tidepool-ingest: Could not resolve dependencies for project io.tidepool:tidepool-ingest:jar:1.0.0-SNAPSHOT
[ERROR] dependency: com.acme.gauge:acme-gauge-sdk:jar:2.4 (compile)
[ERROR] prefixes: Path com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar NOT allowed from central
Maven 3.10 loads the list of path prefixes that exist on Central (the Loaded ... auto-discovered prefixes line that was filtered out)
and does not even ask Central for a path outside it, so the message is prefixes: Path ... NOT allowed from central. Maven 4.0.0-rc-7
gives the same reason and adds an explanation with the properties -Daether.remoteRepositoryFilter.prefixes=false and
-Daether.remoteRepositoryFilter.groupId=false, which switch the filtering off for repository managers that do not publish prefix
files. Maven 3.9.16 does not filter and asks Central, so the first failure is:
$ mvn compile
[INFO] Building tidepool-ingest 1.0.0-SNAPSHOT
[WARNING] The POM for com.acme.gauge:acme-gauge-sdk:jar:2.4 is missing, no dependency information available
[INFO] BUILD FAILURE
[ERROR] Failed to execute goal on project tidepool-ingest: Could not resolve dependencies for project io.tidepool:tidepool-ingest:jar:1.0.0-SNAPSHOT
[ERROR] dependency: com.acme.gauge:acme-gauge-sdk:jar:2.4 (compile)
[ERROR] Could not find artifact com.acme.gauge:acme-gauge-sdk:jar:2.4 in central (https://repo.maven.apache.org/maven2)
A second attempt in the same local repository does not ask again and reports the cached failure: ... was not found in
https://repo.maven.apache.org/maven2 during a previous attempt. This failure was cached in the local repository and resolution is
not reattempted until the update interval of central has elapsed or updates are forced. The cached failure matters later. Maven writes small .lastUpdated marker files in the local repository for a lookup that failed,
and does not retry until the update interval has passed or you force it with -U. Installing the file afterwards still works, as
the next section shows.
install:install-file
install:install-file copies one file into the local repository at the path its coordinates dictate. It needs no project, so
Maven prints Maven Stub Project (No POM) as the project name.
mvn install:install-file -Dfile=vendor/acme-gauge-sdk-2.4.jar -DgroupId=com.acme.gauge -DartifactId=acme-gauge-sdk -Dversion=2.4 -Dpackaging=jar
$ mvn install:install-file -Dfile=vendor/acme-gauge-sdk-2.4.jar -DgroupId=com.acme.gauge -DartifactId=acme-gauge-sdk -Dversion=2.4 -Dpackaging=jar
[INFO] --- install:3.2.0:install-file (default-cli) @ standalone-pom ---
[INFO] Ignoring any POM embedded in acme-gauge-sdk-2.4.jar: complete coordinates were supplied, so a minimal POM will be generated instead. Use -DpomFile to install a specific POM.
[INFO] Installing ./vendor/acme-gauge-sdk-2.4.jar to ./local-repo/com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar
[INFO] Installing /tmp/mvninstall15916797829772843492.pom to ./local-repo/com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom
[INFO] BUILD SUCCESS
The Ignoring any POM embedded line is printed by Install Plugin 3.2.0 (Maven 3.9.16 runs 3.1.4 and does not print it): when you
supply all four coordinates the plugin does not read a POM from inside the JAR and writes a minimal one instead. The temporary
file /tmp/mvninstall...pom is that generated POM. This is what is in the local repository afterwards:
com/acme/gauge/acme-gauge-sdk/2.4/_remote.repositories
com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar
com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar.lastUpdated
com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom
com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom.lastUpdated
com/acme/gauge/acme-gauge-sdk/maven-metadata-local.xml
The two .lastUpdated files are the cache of the failed lookup from the previous section, and are harmless. The generated POM is
the minimum Maven needs:
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme.gauge</groupId>
<artifactId>acme-gauge-sdk</artifactId>
<version>2.4</version>
<description>POM was created from install:install-file</description>
</project>
A POM like this says nothing about the library’s own dependencies. If the vendor JAR needs other libraries, nothing brings them
in; see the -DpomFile form below. Two more files are written. _remote.repositories records where each file came from, and the
empty name after > means "installed locally, not downloaded":
#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice.
#Fri Oct 16 14:39:53 CEST 2026
acme-gauge-sdk-2.4.jar>=
acme-gauge-sdk-2.4.pom>=
(The timestamp comment line was removed.) maven-metadata-local.xml lists the versions installed locally under the artifactId; the repository files themselves are described on
Repositories, Mirrors & Repository Managers.
With the JAR installed, the consumer builds:
$ mvn compile
[INFO] Building tidepool-ingest 1.0.0-SNAPSHOT
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-ingest ---
[INFO] skip non existing resourceDirectory ./consumer/src/main/resources
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-ingest ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 1 source file with javac [debug release 21] to target/classes
[INFO] BUILD SUCCESS
and dependency:tree shows the new node:
$ mvn dependency:tree
[INFO] Building tidepool-ingest 1.0.0-SNAPSHOT
[INFO] --- dependency:3.11.0:tree (default-cli) @ tidepool-ingest ---
[INFO] io.tidepool:tidepool-ingest:jar:1.0.0-SNAPSHOT
[INFO] \- com.acme.gauge:acme-gauge-sdk:jar:2.4:compile
[INFO] BUILD SUCCESS
The four coordinates are required
Unless the JAR carries its own POM (see below), groupId, artifactId, version and packaging must all be given. Leaving
groupId, version and packaging out fails before anything is written:
$ mvn install:install-file -Dfile=vendor/acme-gauge-sdk-2.4.jar -DartifactId=oops
[INFO] --- install:3.2.0:install-file (default-cli) @ standalone-pom ---
[INFO] pom.xml not found in acme-gauge-sdk-2.4.jar
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-install-plugin:3.2.0:install-file (default-cli) on project standalone-pom: The artifact information is incomplete: 'groupId', 'artifactId', 'version' and 'packaging' are required. -> [Help 1]
A real POM, a sources JAR and a Javadoc JAR
If you have the vendor’s POM, or write one that lists the library’s dependencies, pass it with -DpomFile. The coordinates then
come from the POM, and -Dsources and -Djavadoc install the companion JARs with the sources and javadoc classifiers, which
is what an IDE looks for. Version 2.5 of the stand-in has this POM, which declares one dependency on Apache Commons CSV:
<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>
<groupId>com.acme.gauge</groupId>
<artifactId>acme-gauge-sdk</artifactId>
<version>2.5</version>
<name>ACME Gauge SDK</name>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
</dependencies>
</project>
mvn install:install-file -Dfile=vendor/acme-gauge-sdk-2.5.jar -DpomFile=vendor/acme-gauge-sdk-2.5.pom -Dsources=vendor/acme-gauge-sdk-2.5-sources.jar
$ mvn install:install-file -Dfile=vendor/acme-gauge-sdk-2.5.jar -DpomFile=vendor/acme-gauge-sdk-2.5.pom -Dsources=vendor/acme-gauge-sdk-2.5-sources.jar
[INFO] --- install:3.2.0:install-file (default-cli) @ standalone-pom ---
[INFO] Installing ./vendor/acme-gauge-sdk-2.5.jar to ./local-repo/com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5.jar
[INFO] Installing ./vendor/acme-gauge-sdk-2.5.pom to ./local-repo/com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5.pom
[INFO] Installing ./vendor/acme-gauge-sdk-2.5-sources.jar to ./local-repo/com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5-sources.jar
[INFO] BUILD SUCCESS
com/acme/gauge/acme-gauge-sdk/2.5/_remote.repositories
com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5-sources.jar
com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5.jar
com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5.pom
(That second listing is the content of the version directory.) Pointing the consumer at version 2.5 shows what the real POM buys: the vendor’s own dependency, and its transitive ones, arrive with it.
$ mvn dependency:tree
[INFO] Building tidepool-ingest 1.0.0-SNAPSHOT
[INFO] --- dependency:3.11.0:tree (default-cli) @ tidepool-ingest ---
[INFO] io.tidepool:tidepool-ingest:jar:1.0.0-SNAPSHOT
[INFO] \- com.acme.gauge:acme-gauge-sdk:jar:2.5: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] BUILD SUCCESS
A JAR built by Maven carries its own POM
A JAR produced by Maven contains its POM under META-INF/maven/<groupId>/<artifactId>/. Packaging the consumer module shows it:
META-INF/maven/
META-INF/maven/io.tidepool/
META-INF/maven/io.tidepool/tidepool-ingest/
META-INF/maven/io.tidepool/tidepool-ingest/pom.xml
META-INF/maven/io.tidepool/tidepool-ingest/pom.properties
For such a JAR, -Dfile alone is enough. Install Plugin 3.2.0 reads the embedded POM, takes the coordinates from it and installs it
as the POM (this run used another throw-away local repository):
$ mvn install:install-file -Dfile=consumer/target/tidepool-ingest-1.0.0-SNAPSHOT.jar -Dmaven.repo.local=$PWD/local-repo-embedded
[INFO] --- install:3.2.0:install-file (default-cli) @ standalone-pom ---
[INFO] Loading META-INF/maven/io.tidepool/tidepool-ingest/pom.xml from tidepool-ingest-1.0.0-SNAPSHOT.jar
[INFO] Using coordinates io.tidepool:tidepool-ingest:1.0.0-SNAPSHOT:jar from JAR embedded POM META-INF/maven/io.tidepool/tidepool-ingest/pom.xml
[INFO] Using JAR embedded POM as pomFile
[INFO] Installing ./consumer/target/tidepool-ingest-1.0.0-SNAPSHOT.jar to ./local-repo-embedded/io/tidepool/tidepool-ingest/1.0.0-SNAPSHOT/tidepool-ingest-1.0.0-SNAPSHOT.jar
[INFO] Installing /tmp/tidepool-ingest-1.0.0-SNAPSHOT4352212170762538594.pom to ./local-repo-embedded/io/tidepool/tidepool-ingest/1.0.0-SNAPSHOT/tidepool-ingest-1.0.0-SNAPSHOT.pom
[INFO] BUILD SUCCESS
io/tidepool/tidepool-ingest/1.0.0-SNAPSHOT/_remote.repositories
io/tidepool/tidepool-ingest/1.0.0-SNAPSHOT/maven-metadata-local.xml
io/tidepool/tidepool-ingest/1.0.0-SNAPSHOT/tidepool-ingest-1.0.0-SNAPSHOT.jar
io/tidepool/tidepool-ingest/1.0.0-SNAPSHOT/tidepool-ingest-1.0.0-SNAPSHOT.pom
io/tidepool/tidepool-ingest/maven-metadata-local.xml
The Maven guide describes the same feature for Install Plugin 2.5 and writes the plugin coordinates and version out in full on the
command line; with a current Maven the short prefix install: is enough, and the plugin version comes from the default binding
(see "Which versions do what" below).
The parameters that matter
The plugin’s own reference lists them all; the Since column is from the Install Plugin documentation.
| Parameter | Since | Meaning |
|---|---|---|
|
Required. The file to install. |
|
|
The coordinates. Read from |
|
|
2.1 |
An existing POM to install next to the file. |
|
2.1 |
Generate a minimal POM when none is supplied. Defaults to true if there is no POM in the local repository yet. |
|
2.2 |
For example |
|
2.2 |
Install into this directory instead of the configured local repository. |
|
2.3 |
Companion JARs installed with the |
|
3.1.3 |
Override the file extension that the packaging would choose. |
A project-local repository
-DlocalRepositoryPath points the install at a directory of your choice. The result is a complete Maven repository layout that
you can commit. Here it is project-repo/:
mvn install:install-file -Dfile=vendor/acme-gauge-sdk-2.4.jar -DgroupId=com.acme.gauge -DartifactId=acme-gauge-sdk -Dversion=2.4 -Dpackaging=jar -DlocalRepositoryPath=project-repo
$ mvn install:install-file -Dfile=vendor/acme-gauge-sdk-2.4.jar -DgroupId=com.acme.gauge -DartifactId=acme-gauge-sdk -Dversion=2.4 -Dpackaging=jar -DlocalRepositoryPath=project-repo
[INFO] --- install:3.2.0:install-file (default-cli) @ standalone-pom ---
[INFO] Ignoring any POM embedded in acme-gauge-sdk-2.4.jar: complete coordinates were supplied, so a minimal POM will be generated instead. Use -DpomFile to install a specific POM.
[INFO] Installing ./vendor/acme-gauge-sdk-2.4.jar to ./project-repo/com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar
[INFO] Installing /tmp/mvninstall1516254730076339048.pom to ./project-repo/com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom
[INFO] BUILD SUCCESS
./com/acme/gauge/acme-gauge-sdk/2.4/_remote.repositories
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom
./com/acme/gauge/acme-gauge-sdk/maven-metadata-local.xml
The consumer then declares that directory as a repository, using the ${project.basedir} property so the path does not depend on
where you start Maven:
<repositories>
<repository>
<id>project-libs</id>
<url>file://${project.basedir}/../project-repo</url>
</repository>
</repositories>
A build with a brand-new local repository resolves the JAR from there and copies it into the local repository:
$ mvn compile
[INFO] Building tidepool-ingest 1.0.0-SNAPSHOT
[WARNING] Could not validate integrity of download from file://./project/../project-repo/com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom: no checksums available
[WARNING] Could not validate integrity of download from file://./project/../project-repo/com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar: no checksums available
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-ingest ---
[INFO] skip non existing resourceDirectory ./project/src/main/resources
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-ingest ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 1 source file with javac [debug release 21] to target/classes
[INFO] BUILD SUCCESS
com/acme/gauge/acme-gauge-sdk/2.4/_remote.repositories
com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar
com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom
The two warnings say the repository has no checksum files, because install-file does not write any. Maven still uses the files.
deploy-file (next) does write checksums, so a repository created with it does not warn. If you use this approach, commit the
repository directory and give the repository an id of its own: Maven remembers which repository each downloaded file came from.
deploy:deploy-file
deploy:deploy-file uploads a file to a remote repository and updates the repository metadata. It is the right tool when more than
one person or machine needs the JAR. It does not install into the local repository: after all the deploys below, the local
repository of the experiment contained no version 2.6, which only deploy-file ever touched. (The Maven guide says the JAR goes to
the local and the remote repository; with Deploy Plugin 3.2.0 it goes only to the remote one.)
The required parameters are the coordinates plus the repositoryId, which must match a <server> id in settings.xml when the
repository needs credentials, and the url. The experiments deploy to a directory with a file:// URL, which needs no credentials:
mvn deploy:deploy-file -Dfile=vendor/acme-gauge-sdk-2.4.jar -DgroupId=com.acme.gauge -DartifactId=acme-gauge-sdk -Dversion=2.4 -Dpackaging=jar -DrepositoryId=company-releases -Durl=file://$PWD/remote-repo
$ mvn deploy:deploy-file -Dfile=vendor/acme-gauge-sdk-2.4.jar -DgroupId=com.acme.gauge -DartifactId=acme-gauge-sdk -Dversion=2.4 -Dpackaging=jar -DrepositoryId=company-releases -Durl=file://$PWD/remote-repo
[INFO] --- deploy:3.2.0:deploy-file (default-cli) @ standalone-pom ---
[INFO] pom.xml not found in acme-gauge-sdk-2.4.jar
[INFO] BUILD SUCCESS
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar.md5
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar.sha1
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom.md5
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom.sha1
./com/acme/gauge/acme-gauge-sdk/maven-metadata.xml
./com/acme/gauge/acme-gauge-sdk/maven-metadata.xml.md5
./com/acme/gauge/acme-gauge-sdk/maven-metadata.xml.sha1
Unlike install-file, the upload adds .md5 and .sha1 checksums for every file, and a maven-metadata.xml at the artifact level.
Generic POM, custom POM
By default deploy-file generates a generic POM and uploads it, as the listing shows. Set -DgeneratePom=false when the file you
upload is not the main artifact (for instance a sources JAR, see below) or when the repository already holds a POM. When a POM exists for the
JAR, pass -DpomFile; then the coordinates and packaging are read from it and -DgroupId and the others are not needed. -Dsources
and -Djavadoc work as in install-file:
mvn deploy:deploy-file -Dfile=vendor/acme-gauge-sdk-2.5.jar -DpomFile=vendor/acme-gauge-sdk-2.5.pom -Dsources=vendor/acme-gauge-sdk-2.5-sources.jar -DrepositoryId=company-releases -Durl=file://$PWD/remote-repo
$ mvn deploy:deploy-file -Dfile=vendor/acme-gauge-sdk-2.5.jar -DpomFile=vendor/acme-gauge-sdk-2.5.pom -Dsources=vendor/acme-gauge-sdk-2.5-sources.jar -DrepositoryId=company-releases -Durl=file://$PWD/remote-repo
[INFO] --- deploy:3.2.0:deploy-file (default-cli) @ standalone-pom ---
[INFO] BUILD SUCCESS
Deploying a sources JAR on its own
The Maven guide deploys a separate sources JAR with packaging set to java-source and generatePom set to false. That still
works and gives the -sources.jar file name without a second POM:
mvn deploy:deploy-file -Dfile=vendor/acme-gauge-sdk-2.4-sources.jar -DgroupId=com.acme.gauge -DartifactId=acme-gauge-sdk -Dversion=2.4 -Dpackaging=java-source -DgeneratePom=false -DrepositoryId=company-releases -Durl=file://$PWD/remote-repo
$ mvn deploy:deploy-file -Dfile=vendor/acme-gauge-sdk-2.4-sources.jar -DgroupId=com.acme.gauge -DartifactId=acme-gauge-sdk -Dversion=2.4 -Dpackaging=java-source -DgeneratePom=false -DrepositoryId=company-releases -Durl=file://$PWD/remote-repo
[INFO] --- deploy:3.2.0:deploy-file (default-cli) @ standalone-pom ---
[INFO] pom.xml not found in acme-gauge-sdk-2.4-sources.jar
[INFO] BUILD SUCCESS
After the three deploys the repository (checksum files not listed) holds:
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4-sources.jar
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.jar
./com/acme/gauge/acme-gauge-sdk/2.4/acme-gauge-sdk-2.4.pom
./com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5-sources.jar
./com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5.jar
./com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5.pom
./com/acme/gauge/acme-gauge-sdk/maven-metadata.xml
The pom.xml not found in ... line is only the plugin reporting that the file has no embedded POM; with generatePom=false no POM is
written for the sources JAR.
Source: Maven: Deploying Source Jars
Several files in one deploy
-Dfiles, -Dclassifiers and -Dtypes are comma-separated lists that deploy side artifacts of the same coordinates in one
call. The three lists must have the same number of entries:
mvn deploy:deploy-file -Dfile=vendor/acme-gauge-sdk-2.4.jar -DgroupId=com.acme.gauge -DartifactId=acme-gauge-sdk -Dversion=2.6 -Dpackaging=jar -Dfiles=vendor/acme-gauge-sdk-2.4-sources.jar -Dclassifiers=sources -Dtypes=jar -DrepositoryId=company-releases -Durl=file://$PWD/remote-repo
./com/acme/gauge/acme-gauge-sdk/2.6/acme-gauge-sdk-2.6-sources.jar
./com/acme/gauge/acme-gauge-sdk/2.6/acme-gauge-sdk-2.6.jar
./com/acme/gauge/acme-gauge-sdk/2.6/acme-gauge-sdk-2.6.pom
(The listing is the content of the 2.6 directory without checksum files.)
Using the remote repository
A consumer adds the repository to its POM, or better to a profile or to settings.xml, so the URL stays out of the project. For
the file:// repository:
<repositories>
<repository>
<id>company-releases</id>
<url>file://${project.basedir}/../remote-repo</url>
</repository>
</repositories>
A build with a new local repository resolves version 2.5 and its transitive dependencies. On Maven 3.10.0 the run printed nine
warnings of the form ... was downloaded from https://repo.maven.apache.org/maven2 while a cached not-found from a previous attempt
suppresses re-checking file://.../remote-repo/; expiring the cached not-found so that repository is checked again on the next
resolution, one for each POM or JAR of the Commons CSV subtree that was downloaded from Central. They are filtered out
of the listing below; they appeared on the first resolution only, and Maven 3.9.16 printed none for the same project.
$ mvn compile
[INFO] Building tidepool-ingest 1.0.0-SNAPSHOT
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-ingest ---
[INFO] skip non existing resourceDirectory ./remote-user/src/main/resources
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-ingest ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 1 source file with javac [debug release 21] to target/classes
[INFO] BUILD SUCCESS
com/acme/gauge/acme-gauge-sdk/2.5/_remote.repositories
com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5.jar
com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5.jar.sha1
com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5.pom
com/acme/gauge/acme-gauge-sdk/2.5/acme-gauge-sdk-2.5.pom.sha1
(The listing shows only the vendor artifact in the new local repository.)
A real remote repository
For a repository manager the command differs only in the -Durl and in the credentials, which come from a <server> entry in
settings.xml whose id equals -DrepositoryId. Environment variables keep the secrets out of the file:
<settings>
<servers>
<server>
<id>company-releases</id>
<username>${env.COMPANY_REPO_USER}</username>
<password>${env.COMPANY_REPO_TOKEN}</password>
</server>
</servers>
</settings>
mvn deploy:deploy-file -Dfile=vendor/acme-gauge-sdk-2.4.jar -DgroupId=com.acme.gauge -DartifactId=acme-gauge-sdk -Dversion=2.4 -Dpackaging=jar -DrepositoryId=company-releases -Durl=http://localhost:8081/repository/company-releases/
That command was not run here: there was no repository manager to receive it. Passwords can also be encrypted; see settings.xml & Password Encryption.
The Maven guide says a Wagon provider (wagon-ftp, wagon-file) must be copied to ${maven.home}/lib first. That was true for Maven 2
and early Maven 3. Since Maven 3.9.0 the default transport is the native HTTP transport, and file:// worked above with nothing
added. A protocol such as scp: or ftp: still needs a Wagon provider, which is added as a build extension rather than by copying
into lib (not run here; see Maven Extensions).
Source: Maven: Deploying Source Jars
Attaching extra artifacts with build-helper (third party)
install-file and deploy-file handle files that already exist. When a project’s own build produces an extra file that must be
installed and deployed with it (a data file, a generated schema, a native library), the usual tool is the attach-artifact goal of the
MojoHaus Build Helper Plugin. It is not an Apache Maven plugin, it is a third-party plugin that is used widely. It registers
the file as an attached artifact with a classifier, and the standard install and deploy goals do the rest. This example, version 3.6.2 of the
plugin, attaches the tidepool station list as seed data to a POM-packaged project:
<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>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-stations</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.6.2</version>
<executions>
<execution>
<id>attach-stations</id>
<phase>package</phase>
<goals>
<goal>attach-artifact</goal>
</goals>
<configuration>
<artifacts>
<artifact>
<file>data/stations.csv</file>
<type>csv</type>
<classifier>seed</classifier>
</artifact>
</artifacts>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
mvn deploy -DaltDeploymentRepository=company-releases::file://$PWD/remote-repo
$ mvn deploy -DaltDeploymentRepository=company-releases::file://$PWD/remote-repo
[INFO] Building tidepool-stations 1.0.0
[INFO] --- build-helper:3.6.2:attach-artifact (attach-stations) @ tidepool-stations ---
[INFO] --- install:3.2.0:install (default-install) @ tidepool-stations ---
[INFO] Installing ./attach/pom.xml to ./local-repo/io/tidepool/tidepool-stations/1.0.0/tidepool-stations-1.0.0.pom
[INFO] Installing ./attach/data/stations.csv to ./local-repo/io/tidepool/tidepool-stations/1.0.0/tidepool-stations-1.0.0-seed.csv
[INFO] --- deploy:3.2.0:deploy (default-deploy) @ tidepool-stations ---
[INFO] Using alternate deployment repository company-releases::file://./remote-repo
[INFO] Skipping already-deployed io.tidepool:tidepool-stations:1.0.0
[INFO] BUILD SUCCESS
io/tidepool/tidepool-stations/1.0.0/tidepool-stations-1.0.0-seed.csv
io/tidepool/tidepool-stations/1.0.0/tidepool-stations-1.0.0.pom
io/tidepool/tidepool-stations/maven-metadata.xml
The install goal put the POM and the -seed.csv file into the local repository, and the repository on disk holds the same
two files (checksum files not listed). The Skipping already-deployed line of the Deploy Plugin is worded confusingly for this POM-packaged
project, but the listing shows what reached the repository. A consumer asks for the file with <type>csv</type> and
<classifier>seed</classifier> on the dependency (not run here). The plugin’s version and other goals are on the
plugins page; the
packaging and attaching of JARs is on
Packaging JARs, WARs & Manifests.
Why not the system scope
A tempting shortcut is to leave the JAR in the project, in lib/, and refer to it with the system scope and a systemPath:
<dependency>
<groupId>com.acme.gauge</groupId>
<artifactId>acme-gauge-sdk</artifactId>
<version>2.4</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/acme-gauge-sdk-2.4.jar</systemPath>
</dependency>
It compiles, but every build says it should not. Maven 3.10.0:
$ mvn compile
[WARNING] Some problems were encountered while building the effective model for io.tidepool:tidepool-ingest:jar:1.0.0-SNAPSHOT
[WARNING] 'dependencies.dependency.scope' for com.acme.gauge:acme-gauge-sdk:jar declares usage of deprecated 'system' scope @ line 21, column 14
[WARNING] 'dependencies.dependency.systemPath' for com.acme.gauge:acme-gauge-sdk:jar should not point at files within the project directory, ${project.basedir}/lib/acme-gauge-sdk-2.4.jar will be unresolvable by dependent projects @ line 22, column 19
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[INFO] Building tidepool-ingest 1.0.0-SNAPSHOT
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-ingest ---
[INFO] skip non existing resourceDirectory ./syscope/src/main/resources
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-ingest ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 1 source file with javac [debug release 21] to target/classes
[INFO] BUILD SUCCESS
Maven 3.9.16 on the same project prints only the systemPath warning:
$ mvn compile
[WARNING] Some problems were encountered while building the effective model for io.tidepool:tidepool-ingest:jar:1.0.0-SNAPSHOT
[WARNING] 'dependencies.dependency.systemPath' for com.acme.gauge:acme-gauge-sdk:jar should not point at files within the project directory, ${project.basedir}/lib/acme-gauge-sdk-2.4.jar will be unresolvable by dependent projects @ line 22, column 19
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[INFO] Building tidepool-ingest 1.0.0-SNAPSHOT
[INFO] --- resources:3.4.0:resources (default-resources) @ tidepool-ingest ---
[INFO] skip non existing resourceDirectory ./syscope/src/main/resources
[INFO] --- compiler:3.15.0:compile (default-compile) @ tidepool-ingest ---
[INFO] Nothing to compile - all classes are up to date.
[INFO] BUILD SUCCESS
Maven 4.0.0-rc-7 reports 2 problems were encountered while building the effective model and gives the details with -e. In all three
the build succeeds today and the text says future versions might refuse the project. The two complaints are the whole case against
the shortcut: the scope itself is flagged as deprecated by Maven 3.10, and a systemPath inside the project directory is "unresolvable
by dependent projects": the JAR is never published, so any module or application that depends on your artifact does not get
the JAR, nor a way to find it. A repository (project-local, or remote) has none of these problems.
Relocation
Sometimes the problem is the opposite one: the artifact is on a repository, but under the wrong name. A project changes its
groupId, its artifactId or both, and consumers still ask for the old coordinates. Published repository content must never change,
so Maven’s answer is a relocation: a POM published at the old coordinates that contains no code and says "go there
instead". The element is <distributionManagement><relocation>, described on
POM Fundamentals. It may hold groupId, artifactId,
version and a message; every element you leave out keeps the old value, so a project that only changed its groupId writes
only the groupId.
A real relocation on Maven Central
Apache Ant published its releases up to 1.6.5 as ant:ant and, from 1.7.0, as org.apache.ant:ant, and left one relocation POM at the
old coordinates. This is that file, fetched from Central:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
<distributionManagement>
<relocation>
<groupId>org.apache.ant</groupId>
</relocation>
</distributionManagement>
</project>
A project that still depends on ant:ant:1.7.0 builds, with a warning (Maven 3.10.0):
$ mvn dependency:tree
[INFO] Building relocation-demo 1.0.0-SNAPSHOT
[WARNING] The artifact ant:ant:jar:1.7.0 has been relocated to org.apache.ant:ant:jar:1.7.0
[INFO] --- dependency:3.11.0:tree (default-cli) @ relocation-demo ---
[WARNING] The artifact ant:ant:jar:1.7.0 has been relocated to org.apache.ant:ant:jar:1.7.0
[INFO] io.tidepool:relocation-demo:jar:1.0.0-SNAPSHOT
[INFO] \- org.apache.ant:ant:jar:1.7.0:compile
[INFO] \- org.apache.ant:ant-launcher:jar:1.7.0:compile
[INFO] BUILD SUCCESS
The tree already shows the new coordinates, org.apache.ant:ant, and the dependency of the real artifact (ant-launcher). Maven 3.9.16
and Maven 4.0.0-rc-7 printed the same warning and the same tree (the 3.9.16 run used Dependency Plugin 3.7.0, the other two 3.11.0).
The warning appears twice in the output, before and after the plugin header.
Relocating your own artifact
The tidepool project decides that its modules move from io.tidepool to io.tidepool.core. The new artifact is deployed under the
new groupId, as usual. Under the old coordinates goes a minimal POM, relocation.pom: coordinates, the relocation, and a message
that tells the consumer what to do:
<?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>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0.0</version>
<distributionManagement>
<relocation>
<groupId>io.tidepool.core</groupId>
<message>The tidepool modules moved to io.tidepool.core; update your dependency.</message>
</relocation>
</distributionManagement>
</project>
Both are deployed with deploy-file. The relocation POM is itself the file to upload, so -Dpackaging=pom is needed; without it the
plugin would take its extension from the default packaging and name it .jar.
mvn deploy:deploy-file -Dfile=reloc/tidepool-model-1.0.0.jar -DgroupId=io.tidepool.core -DartifactId=tidepool-model -Dversion=1.0.0 -Dpackaging=jar -DrepositoryId=company-releases -Durl=file://$PWD/reloc/repo
mvn deploy:deploy-file -Dfile=reloc/relocation.pom -DpomFile=reloc/relocation.pom -Dpackaging=pom -DrepositoryId=company-releases -Durl=file://$PWD/reloc/repo
$ mvn deploy:deploy-file -Dfile=reloc/relocation.pom -DpomFile=reloc/relocation.pom -Dpackaging=pom -DrepositoryId=company-releases -Durl=file://$PWD/reloc/repo
[INFO] --- deploy:3.2.0:deploy-file (default-cli) @ standalone-pom ---
[INFO] BUILD SUCCESS
(That is the output of the second command.) The repository now has the JAR only under the new groupId, and only a POM under the old one:
./io/tidepool/core/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
./io/tidepool/core/tidepool-model/1.0.0/tidepool-model-1.0.0.pom
./io/tidepool/core/tidepool-model/maven-metadata.xml
./io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom
./io/tidepool/tidepool-model/maven-metadata.xml
(Checksum files are not listed.) A project that depends on the old coordinates is redirected:
$ mvn dependency:tree
[INFO] Building tidepool-app 1.0.0-SNAPSHOT
[WARNING] The artifact io.tidepool:tidepool-model:jar:1.0.0 has been relocated to io.tidepool.core:tidepool-model:jar:1.0.0: The tidepool modules moved to io.tidepool.core; update your dependency.
[INFO] --- dependency:3.11.0:tree (default-cli) @ tidepool-app ---
[WARNING] The artifact io.tidepool:tidepool-model:jar:1.0.0 has been relocated to io.tidepool.core:tidepool-model:jar:1.0.0: The tidepool modules moved to io.tidepool.core; update your dependency.
[INFO] io.tidepool:tidepool-app:jar:1.0.0-SNAPSHOT
[INFO] \- io.tidepool.core:tidepool-model:jar:1.0.0:compile
[INFO] BUILD SUCCESS
The warning carries the message, and the tree names the new coordinates. Maven 3.9.16 and Maven 4.0.0-rc-7 printed the same warning and tree. Nothing in the consumer’s POM changed: relocation is a courtesy, not a rewrite, and the warning stays until the consumer edits its dependency.
Source: Maven POM reference: Relocation
The procedure for a real project
The Maven guide to relocation describes the steps for an artifact that has already been published to a repository under the old name. In outline:
-
Copy all files of the old artifact to the new location (the new
groupIdpath), and change the groupId inside the copied POMs. -
At the old location, replace the POM of every released version by a minimal relocation POM. Back the old POMs up first.
-
If the repository uses checksums or signatures, recreate them for the POMs you changed. If it syncs to Central, start the sync.
-
At the next release, publish two POMs: the real one under the new groupId and a relocation POM under the old groupId for the new version, because Maven does not download a POM again once it has it, and users of the previous version would otherwise never see the move. From the release after that, publish only under the new groupId.
The guide itself points out that Central content should not be changed once published, which is why Ant left the old POMs alone
and published one relocation POM for 1.7.0 only. It also carries a note that a rework of the guide is in progress; treat the steps as
the long-standing practice and check them against your repository manager’s documentation. A repository manager
(Nexus, Artifactory) may let you do the same through its UI. The relocation itself was run here only on a file:// repository.
Source: Maven POM reference: Relocation
Which versions do what
What the experiments showed on each Maven. Default plugin versions are the ones the lifecycle binds; install: and deploy: on the
command line without a version use the same.
| Observation | Maven 3.9.16 | Maven 3.10.0 | Maven 4.0.0-rc-7 |
|---|---|---|---|
Install and Deploy Plugin used by |
3.1.4 |
3.2.0 |
3.2.0 |
|
no |
yes |
yes |
|
yes (the guide documents it since Install Plugin 2.5) |
yes |
yes |
Missing artifact is reported as |
|
|
the same, plus a hint to switch the filtering off |
|
warns about |
warns the scope is deprecated and about |
2 model problems, details with |
Relocation warning and resolved tree |
same |
same |
same |
Maven 4 is a release candidate, so the last column may change before the final release.
Comparable tools in Ant, Ivy and Gradle
None of these rows was run for this page; they are named from the official documentation.
| Need | Maven | Gradle | Ant with Ivy |
|---|---|---|---|
A JAR that is not on Central |
|
a file dependency, a |
a JAR in |
Publish to a repository |
|
the |
|
Redirect a renamed module |
|
not covered in this section |
not covered in this section |
Details: Gradle: Repositories & Content Filtering, Gradle: Publishing, Ivy: Settings & Resolvers, Ivy: Publishing & Multi-Project, and the tool-neutral Repositories & Publishing Across Tools.
Source: Gradle: Declaring repositories
Source: Ivy: resolvers
Where the books are outdated
The Maven 2 era books show Maven 2.2 and a repository workflow of that time. Today:
-
Archiva, the repository manager those books use for deploying a shared JAR, was retired in 2024 (see the Apache Attic page); the same
deploy:deploy-fileworkflow works against Nexus, Artifactory or Reposilite. -
A
deploy-fileneedingwagon-ftp,wagon-sshorwagon-filein${maven.home}/libis Maven 2 practice. Native HTTP transport has been the default since Maven 3.9.0, andfile://repositories need nothing. -
mvn install:install-fileexamples that name a plugin version such as2.5.2in full are obsolete: the version is bound by the lifecycle, 3.2.0 on Maven 3.10.0, and-Dsourcesand-Djavadocmake a separate call for the companion JARs unnecessary. -
The system scope was shown as a convenience; Maven 3.10 flags it as deprecated.
Summary of pitfalls
-
install-filewithout a POM writes a POM with no dependencies; the library’s own dependencies are not resolved. Use-DpomFile. -
A JAR installed only on your machine breaks everybody else’s build and CI. Deploy it to a repository.
-
deploy-filedoes not install into the local repository; run both when you need both. -
-DrepositoryIdmust equal a<server>id insettings.xml, or Maven sends no credentials. -
A sources JAR deployed with
generatePomleft at its default makes the plugin generate a POM withpackagingjava-sourceat the coordinates of the main artifact, which would replace its real POM; use-DgeneratePom=false. -
<scope>system</scope>is deprecated and never reaches dependent projects. -
A failed lookup is cached in the local repository (
.lastUpdatedfiles);-Uforces the update. -
A relocation POM must be published for every old version you want to redirect, and it is only a hint: consumers keep the warning until they edit their dependency.
-
Do not change a published JAR or POM; relocate and release again.