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 deploy:deploy-file

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.

install:install-file into ~/.m2

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 (install:install-file with -DlocalRepositoryPath, plus a file:// <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.

<scope>system</scope> with <systemPath>

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

file

Required. The file to install.

groupId, artifactId, version, packaging

The coordinates. Read from -DpomFile or from the embedded POM when not given.

pomFile

2.1

An existing POM to install next to the file.

generatePom

2.1

Generate a minimal POM when none is supplied. Defaults to true if there is no POM in the local repository yet.

classifier

2.2

For example sources or javadoc; empty means the main artifact.

localRepositoryPath

2.2

Install into this directory instead of the configured local repository.

sources, javadoc

2.3

Companion JARs installed with the sources and javadoc classifiers.

extension

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.

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

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.

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:

  1. Copy all files of the old artifact to the new location (the new groupId path), and change the groupId inside the copied POMs.

  2. At the old location, replace the POM of every released version by a minimal relocation POM. Back the old POMs up first.

  3. If the repository uses checksums or signatures, recreate them for the POMs you changed. If it syncs to Central, start the sync.

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

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 install-file and deploy-file

3.1.4

3.2.0

3.2.0

Ignoring any POM embedded in ... message when all coordinates are given

no

yes

yes

install-file -Dfile=x.jar of a Maven-built JAR reads the embedded POM

yes (the guide documents it since Install Plugin 2.5)

yes

yes

Missing artifact is reported as

Could not find artifact ... in central

prefixes: Path ... NOT allowed from central

the same, plus a hint to switch the filtering off

system scope

warns about systemPath only

warns the scope is deprecated and about systemPath

2 model problems, details with -e

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

install:install-file, deploy:deploy-file

a file dependency, a flatDir repository, or a repository declared in the repositories block

a JAR in lib/ on the classpath, or an Ivy filesystem resolver

Publish to a repository

deploy:deploy-file, deploy

the maven-publish plugin

ivy:publish

Redirect a renamed module

<relocation> in a POM

not covered in this section

not covered in this section

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-file workflow works against Nexus, Artifactory or Reposilite.

  • A deploy-file needing wagon-ftp, wagon-ssh or wagon-file in ${maven.home}/lib is Maven 2 practice. Native HTTP transport has been the default since Maven 3.9.0, and file:// repositories need nothing.

  • mvn install:install-file examples that name a plugin version such as 2.5.2 in full are obsolete: the version is bound by the lifecycle, 3.2.0 on Maven 3.10.0, and -Dsources and -Djavadoc make 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-file without 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-file does not install into the local repository; run both when you need both.

  • -DrepositoryId must equal a <server> id in settings.xml, or Maven sends no credentials.

  • A sources JAR deployed with generatePom left at its default makes the plugin generate a POM with packaging java-source at 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 (.lastUpdated files); -U forces 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.