Maven Resolver Ant Tasks

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 the Maven Resolver Ant Tasks 2.0.0, released on 6 August 2026, running on Apache Ant 1.10.18 with JDK 26 on macOS. Every example was run on 2026-10-16 in a scratch directory (the tidepool toolkit described on the section landing page), and the one example that matters for the Java floor was run again on JDK 17. Output is shown as Ant printed it; the scratch directory is shortened to . and Ant’s own directory to $ANT_HOME. Lines of download progress that do not matter for the point being made are left out, and the text says so each time. Maven Central and the project pages were reached on the same day.

The Maven Resolver Ant Tasks are the Apache Maven project’s answer to the question "how does an Ant build use Maven repositories?". They put the library that Maven itself uses to find and download artifacts, Maven Resolver, behind a few Ant tasks and types: resolve collects and downloads dependencies and turns them into paths, file sets or properties, install copies built artifacts into a local repository, and deploy uploads them to a remote one. The same coordinates, the same settings.xml, the same mirror and proxy rules and the same repository layout as Maven apply, so an Ant build can consume what Maven builds publish and the other way round. The sibling page Ivy Overview describes the other way to manage dependencies in Ant; the table near the end of this page helps to choose between them. Dependency concepts such as scopes and mediation are explained in Dependency Management Fundamentals.

What the project is and what 2.0.0 changed

The tasks live in their own repository, apache/maven-resolver-ant-tasks, and are released separately from Maven and from Maven Resolver. The releases published on Maven Central are 1.2.0, 1.2.1, 1.3.0, 1.3.1, 1.4.0, 1.5.0, 1.5.1, 1.5.2, 1.6.0, 1.6.1 and 2.0.0. The ones that matter:

Release Published What changed (from the GitHub release notes)

1.5.2

February 2025

Managed dependencies no longer default their scope to compile; Resolver 1.9.22, Maven 3.9.9, Ant 1.10.15.

1.6.0

August 2025

Dependency management through the dependencyManagement type (BOM import); developers and scm in createPom; the usage.md guide and the example projects.

1.6.1

July 2026

A null pointer when deploy ran without a registered POM was fixed.

2.0.0

6 August 2026

"Upgrade to Resolver 2 (and Maven 3.10)". This is the line the page is about.

The POM of 2.0.0 says what the jar is built from: Maven Resolver 2.0.21, Maven 3.10.0-rc-1 for the settings and model builders, Ant 1.10.17 as the provided Ant API, and Java 8 as the minimum (the classes have class-file version 52). The site text still says the tasks are for "Apache Ant 1.7+". That is the declared compatibility; what was run for this page is Ant 1.10.18, on JDK 26 and on JDK 17, and both worked. Older Ant lines and older JDKs were not tried.

Getting the JAR

The artifact is org.apache.maven.resolver:maven-resolver-ant-tasks:2.0.0. Maven Central holds two JARs under it, and choosing the right one is the first stumbling block:

File Size Use

maven-resolver-ant-tasks-2.0.0-uber.jar

4,917,168 bytes

The one to use. It bundles Maven Resolver, the Maven model and settings builders, the Apache HTTP client, SLF4J and the other libraries the tasks need, so the build file needs this single JAR.

maven-resolver-ant-tasks-2.0.0.jar

130,194 bytes

Only the tasks themselves. Its dependencies are listed in the POM, but Ant has no way to download them, so it is useful only to a build that already has them on a classpath (for example one assembled by Maven).

The uber JAR is a classifier of the main artifact, so every Maven-style tool can fetch it by naming the classifier. The dependency:get goal of the Maven Dependency Plugin does it from the command line; the local repository was redirected to a scratch directory so that the run did not touch ~/.m2.

mvn -B -ntp -q -Dmaven.repo.local=./repo \
  org.apache.maven.plugins:maven-dependency-plugin:3.11.0:get \
  -Dartifact=org.apache.maven.resolver:maven-resolver-ant-tasks:2.0.0:jar:uber
ls -l repo/org/apache/maven/resolver/maven-resolver-ant-tasks/2.0.0/ | awk 'NR>1 {print $5, $9}'
316 _remote.repositories
4917168 maven-resolver-ant-tasks-2.0.0-uber.jar
32 maven-resolver-ant-tasks-2.0.0-uber.jar.md5
40 maven-resolver-ant-tasks-2.0.0-uber.jar.sha1
14667 maven-resolver-ant-tasks-2.0.0.pom
32 maven-resolver-ant-tasks-2.0.0.pom.md5
40 maven-resolver-ant-tasks-2.0.0.pom.sha1

Downloading from Central is not by itself a trust decision. The directory on Central also holds .sha1, .md5 and .asc (PGP signature) files for every JAR. Comparing the published SHA-1 with the local one takes two lines, and both printed the same digest:

curl -s https://repo1.maven.org/maven2/org/apache/maven/resolver/maven-resolver-ant-tasks/2.0.0/maven-resolver-ant-tasks-2.0.0-uber.jar.sha1
shasum lib/maven-resolver-ant-tasks-2.0.0-uber.jar
4ed6bce0d80a1a7fd5f25d80c1bdc5463ef137b0
4ed6bce0d80a1a7fd5f25d80c1bdc5463ef137b0  lib/maven-resolver-ant-tasks-2.0.0-uber.jar

A checksum fetched from the same server as the file only detects a damaged download. Verifying the .asc signature against the Maven project’s published keys is the stronger check; it was not run here. The page Supply-Chain Security explains the trade-offs, and the Ant-side download tasks are on Network & Remote Tasks.

Maven Central also has a CycloneDX software bill of materials for the release (maven-resolver-ant-tasks-2.0.0-cyclonedx.json and .xml).

Making Ant load the tasks

The tasks are an antlib: a JAR with a descriptor, org/apache/maven/resolver/ant/antlib.xml, that lists the task and type names. Antlibs are used through an XML namespace whose URI starts with antlib:; for this one it is antlib:org.apache.maven.resolver.ant. The prefix is free; this page uses resolver:, matching the log lines Ant prints. There are two ways to make Ant find the JAR, and both were run.

Declare it in the build file. The official page recommends copying the JAR into the project’s lib directory and loading it with taskdef, so the build carries its own copy and needs no Ant installation changes:

resolve-basic.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="resolve-basic" default="resolve" basedir="."
         xmlns:resolver="antlib:org.apache.maven.resolver.ant">
  <taskdef uri="antlib:org.apache.maven.resolver.ant"
           resource="org/apache/maven/resolver/ant/antlib.xml"
           classpath="lib/maven-resolver-ant-tasks-2.0.0-uber.jar"/>

  <resolver:localrepo dir="build/m2"/>

  <target name="resolve">
    <resolver:resolve>
      <dependencies>
        <dependency coords="org.apache.commons:commons-csv:1.14.1"/>
      </dependencies>
      <path refid="compile.classpath" classpath="compile"/>
      <files refid="lib.files" dir="build/lib" layout="{artifactId}-{version}.{extension}"/>
      <properties prefix="dep."/>
    </resolver:resolve>
    <echo>path   : ${toString:compile.classpath}</echo>
    <echo>dep.org.apache.commons:commons-csv:jar = ${dep.org.apache.commons:commons-csv:jar}</echo>
  </target>
</project>

Put it on Ant’s own classpath. With the JAR in -lib, in ~/.ant/lib or in $ANT_HOME/lib, the taskdef line is unnecessary, because Ant looks up an antlib: namespace on its classpath. The same build file without the taskdef failed without the library and worked with it:

$ ant -f resolve-basic-nodef.xml
Buildfile: ./resolve-basic-nodef.xml

BUILD FAILED
./resolve-basic-nodef.xml:5: Problem: failed to create task or type antlib:org.apache.maven.resolver.ant:localrepo
Cause: The name is undefined.
Action: Check the spelling.
Action: Check that any custom tasks/types have been declared.
Action: Check that any <presetdef>/<macrodef> declarations have taken place.
No types or tasks have been defined in this namespace yet

This appears to be an antlib declaration.
Action: Check that the implementing library exists in one of:
        -$ANT_HOME/lib
        -/Users/me/.ant/lib
        -a directory added on the command line with the -lib argument


Total time: 0 seconds

$ ant -lib lib -f resolve-basic-nodef.xml
Buildfile: ./resolve-basic-nodef.xml

resolve:
[resolver:resolve] Resolving artifacts
     [echo] path   : ./build/m2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar:…
     [echo] dep.org.apache.commons:commons-csv:jar = ./build/m2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar

BUILD SUCCESSFUL
Total time: 0 seconds

Both -lib lib (a directory) and -lib lib/maven-resolver-ant-tasks-2.0.0-uber.jar (the file) worked. The three places for libraries and the rule to never rely on CLASSPATH are explained on Installation, and the mechanics of namespaces on Antlibs & Namespaces. The taskdef form is the better default for a shared project, because a new developer needs no setup beyond the repository; the -lib form suits a CI image or a team that standardises its Ant installation. Two details were noticed in the first run:

  • With ant -v, a line Could not load definitions from resource org/apache/maven/resolver/ant/antlib.xml. It could not be found. appears next to the successful load by taskdef. It is noise from Ant’s attempt to resolve the namespace through its own classpath first, and the build continues.

  • The project’s usage.md warns that loading the antlib resource does not work from Groovy’s AntBuilder; there the types and tasks have to be defined one by one with their implementation class names. That guide is the source of this statement; it was not run here.

What the antlib defines

The descriptor inside the JAR defines these names. Notice that pom is declared as a task although the site documents it as a type, and that createPom is not described on the site’s main page at all (it is in the usage.md guide and the API documentation):

Kind Names

Tasks

resolve, install, deploy, createPom, pom

Types

authentication, proxy, mirror, localrepo, remoterepo, remoterepos, dependency, dependencies, artifact, artifacts, settings, dependencyManagement

The nested elements of a task are matched by Ant’s introspection on the local element name, not on the namespace, so inside a <resolver:resolve> a nested <dependencies> needs no prefix (a prefixed <resolver:dependencies> works too, and a top-level definition outside a task needs the prefix). All examples below use the unprefixed form for nested elements.

A first resolve

The build file above asks for one dependency, commons-csv 1.14.1, with a local repository inside the project. This is the first run, with an empty local repository, so every file is downloaded (the progress lines are shown once here):

ant -f resolve-basic.xml
Buildfile: ./resolve-basic.xml

resolve:
[resolver:resolve] Downloading from central https://repo.maven.apache.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom
[resolver:resolve] Downloaded from central https://repo.maven.apache.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom (22 KB at 648.4 KB/sec)
[resolver:resolve] Downloading from central https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/85/commons-parent-85.pom
[resolver:resolve] Downloaded from central https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/85/commons-parent-85.pom (77 KB at 2185.7 KB/sec)
…
[resolver:resolve] Resolving artifacts
[resolver:resolve] Downloading from central https://repo.maven.apache.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar
[resolver:resolve] Downloaded from central https://repo.maven.apache.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar (60 KB at 773.2 KB/sec)
…
     [echo] path   : ./build/m2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar:./build/m2/commons-io/commons-io/2.20.0/commons-io-2.20.0.jar:./build/m2/commons-codec/commons-codec/1.19.0/commons-codec-1.19.0.jar
     [echo] dep.org.apache.commons:commons-csv:jar = ./build/m2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar

BUILD SUCCESSFUL
Total time: 1 second

A second run finds everything in the local repository and prints only Resolving artifacts before the two echo lines. Four things in this output are worth reading slowly:

  1. The POMs come first. Maven Resolver reads commons-csv-1.14.1.pom and its parents (commons-parent, apache) to learn the dependencies of commons-csv before it knows which JARs to download. The Resolving artifacts line marks the switch from graph building to downloading.

  2. Transitive dependencies are followed. The one declared dependency produced three JARs, because commons-csv depends on commons-io and commons-codec. With ant -v the task also prints the dependency graph, and the repository it uses:

    [resolver:resolve] Using local repository ./build/m2 (enhanced)
    Using remote repository central (https://repo.maven.apache.org/maven2/, default, releases)
    Collecting dependencies
    [resolver:resolve] Resolved Dependency Graph:
    [resolver:resolve]    org.apache.commons:commons-csv:jar:1.14.1:compile
    [resolver:resolve]       commons-io:commons-io:jar:2.20.0:compile
    [resolver:resolve]       commons-codec:commons-codec:jar:1.19.0:compile
    [resolver:resolve] Resolving artifacts
  3. Three outputs were requested, and each is a different Ant object. <path refid="compile.classpath"> defines an Ant path, usable as classpathref in javac and java. <files refid="lib.files" dir="build/lib" layout="…"> copies the files into build/lib under names built from the layout placeholders and also defines a resource collection with that id. <properties prefix="dep."> sets one property per artifact, named after the prefix and the artifact’s groupId:artifactId:extension, whose value is the path of the file in the local repository (note the colons in the property names; ${dep.org.apache.commons:commons-csv:jar} works in a build file, but the name cannot be built with a macro attribute without care). The build/lib directory ended with the three expected files, commons-codec-1.19.0.jar, commons-csv-1.14.1.jar and commons-io-2.20.0.jar.

  4. The path points into the local repository, not into build/lib. The classpath uses the cache files directly; the copies in build/lib exist for tasks that want a directory (packaging a WAR, an application distribution).

flowchart LR A[settings.xml and build file] --> B[Local repository and remote repositories] B --> C[Collect: read POMs and build the graph] C --> D[Mediate: nearest wins, exclusions, BOM] D --> E[Download missing JARs] E --> F[path] E --> G[files] E --> H[properties]

Settings, repositories and mirrors

Where the settings come from

The tasks read the same settings.xml as Maven. By default the user file is ${user.home}/.m2/settings.xml; for the global file the site lists ${ant.home}/etc/settings.xml and ${maven.home}/conf/settings.xml. The <settings> type changes both locations. The global-file search order is taken from the site and was not run here. With no settings file and no localrepo, the local repository was the Maven default, ~/.m2/repository: the first, throw-away experiment of this page resolved commons-csv-1.14.1.jar to /Users/…/.m2/repository/org/apache/commons/commons-csv/1.14.1/…. That sharing is usually what a developer wants, because Maven and Ant builds reuse each other’s downloads, and sometimes what a CI job does not want, so a project-local <localrepo dir="build/m2"/> (as used in this page) is the way to isolate it.

A settings file can supply the local repository, mirrors, servers (credentials) and proxies. This settings.xml was used in the next example; the credentials are placeholders that read environment variables, and the example does not contact any server that needs them, so credential handling was not run here:

settings/settings.xml
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0">
  <localRepository>${user.dir}/build/m2-from-settings</localRepository>
  <mirrors>
    <mirror>
      <id>tidepool-mirror</id>
      <mirrorOf>central</mirrorOf>
      <url>https://repo.maven.apache.org/maven2/</url>
    </mirror>
  </mirrors>
  <servers>
    <server>
      <id>internal</id>
      <username>${env.REPO_USER}</username>
      <password>${env.REPO_TOKEN}</password>
    </server>
  </servers>
</settings>
settings-mirror.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="settings-mirror" default="resolve" basedir="."
         xmlns:resolver="antlib:org.apache.maven.resolver.ant">
  <taskdef uri="antlib:org.apache.maven.resolver.ant"
           resource="org/apache/maven/resolver/ant/antlib.xml"
           classpath="lib/maven-resolver-ant-tasks-2.0.0-uber.jar"/>
  <resolver:settings file="settings/settings.xml"/>

  <target name="resolve">
    <resolver:resolve>
      <dependencies>
        <dependency coords="info.picocli:picocli:4.7.7"/>
      </dependencies>
      <path refid="cp" classpath="compile"/>
    </resolver:resolve>
    <echo>${toString:cp}</echo>
  </target>
</project>
Buildfile: ./settings-mirror.xml

resolve:
[resolver:resolve] Downloading from tidepool-mirror https://repo.maven.apache.org/maven2/info/picocli/picocli/4.7.7/picocli-4.7.7.pom
[resolver:resolve] Downloaded from tidepool-mirror https://repo.maven.apache.org/maven2/info/picocli/picocli/4.7.7/picocli-4.7.7.pom (2 KB at 8.8 KB/sec)
[resolver:resolve] Resolving artifacts
[resolver:resolve] Downloading from tidepool-mirror https://repo.maven.apache.org/maven2/info/picocli/picocli/4.7.7/picocli-4.7.7.jar
[resolver:resolve] Downloaded from tidepool-mirror https://repo.maven.apache.org/maven2/info/picocli/picocli/4.7.7/picocli-4.7.7.jar (408 KB at 3315.9 KB/sec)
     [echo] ./build/m2-from-settings/info/picocli/picocli/4.7.7/picocli-4.7.7.jar

Three effects of the settings file are visible: the JAR landed in the localRepository named there (with ${user.dir} expanded to Ant’s working directory), the repository that Maven’s central id stands for was replaced by the mirror (Downloading from tidepool-mirror), and the build file itself did nothing about repositories. In real use the mirror’s URL would be an internal repository manager and the central URL would never be contacted directly; here the mirror points back to Central so that the example runs anywhere. Mirrors, servers, encrypted passwords and profiles are described for Maven on settings.xml & Password Encryption and Repositories, Mirrors & Repository Managers. The uber JAR bundles Maven’s settings decrypter and plexus-sec-dispatcher, so encrypted passwords in settings.xml are expected to work as in Maven; that was not tested.

The repository and network types

These types can be declared at the project level (then they apply to every task) or nested in a task. Attribute names come from the site and from the classes in the JAR.

Element Attributes and notes

localrepo

dir. Only one local repository can be in use at a time. Its default is Maven’s.

remoterepo

id, url, type (default default), releases and snapshots (booleans, or nested <releases enabled updates checksums/> and <snapshots .../>), updates (always, daily, never or an interval), checksums (fail, warn, ignore), a nested <authentication> or authref. The site says only file:, http: and https: URLs are supported.

remoterepos

A named group of <remoterepo> elements (and nested <remoterepos>), usable "in every place that is legal for a remote repository". A refid reuses a definition.

mirror

id, url, type, mirrorOf (the same patterns as Maven), a nested authentication.

proxy

host, port, type, nonProxyHosts, a nested authentication. There can be several; the first whose nonProxyHosts does not match the host is used. Not run here (it needs a proxy).

authentication

username, password, privateKeyFile, passphrase, servers (the repository ids it applies to; without it the authentication must be referenced explicitly). Not run here.

settings

file, globalfile.

The project-level declaration of two repositories and a group, taken from the consuming build that appears later in this page, shows the syntax:

<resolver:localrepo dir="build/m2-consumer"/>
<resolver:remoterepo id="tidepool.local" url="file://${basedir}/build/repo" snapshots="false" checksums="fail"/>
<resolver:remoterepos id="all">
  <remoterepo refid="tidepool.local"/>
  <remoterepo id="central" url="https://repo.maven.apache.org/maven2/" snapshots="false"/>
</resolver:remoterepos>

Which repositories does resolve use? The task uses the repositories nested in it or referenced with remoteReposRef; if it has none, it uses the group with the id resolver.repositories, which by default contains only Central. Declaring your own <remoterepos id="resolver.repositories"> replaces that default for every resolve in the build:

<resolver:remoterepos id="resolver.repositories">
  <remoterepo id="tidepool.local" url="file://${basedir}/build/repo"/>
  <remoterepo id="central" url="https://repo.maven.apache.org/maven2/"/>
</resolver:remoterepos>

Run with ant -v, the task then reported Using remote repository tidepool.local (file://./build/repo, default, releases) followed by Using remote repository central, and resolved io.tidepool:tidepool-engine:1.0.0 from the first.

Explicit repositories do not stop Central from being used for transitive dependencies. In the checksum experiment below, resolve was given only one repository, a file repository holding only the tidepool artifacts. The tidepool POM was read from it, but commons-csv and its parents came from Central:

[resolver:resolve] Downloaded from bad file://./build/repo-bad/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom
[resolver:resolve] Downloaded from central https://repo.maven.apache.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom
…

Maven works the same way: the repositories declared inside a dependency’s POM, including the central repository of the Super POM that every POM inherits, are added when that dependency’s own dependencies are collected. A build that must not contact Central has to say so with a mirror (<mirrorOf>*</mirrorOf>) in the settings file, not only with its own repository list. For a build that has no mirror a safe check is an offline run, below.

Offline mode

Setting the property resolver.offline to true suppresses all network traffic: only artifacts and metadata already in the local repository are used. It can be set in the build file or, as here, on the command line. With an empty local repository the build fails, with a message that names the cause; with a populated one it succeeds:

rm -rf build/m2
ant -Dresolver.offline=true -f resolve-basic.xml      # cold
ant -f resolve-basic.xml                              # fill the cache
ant -Dresolver.offline=true -f resolve-basic.xml      # warm
Buildfile: ./resolve-basic.xml

resolve:
[resolver:resolve] The POM for org.apache.commons:commons-csv:jar:1.14.1 is missing, no dependency information available
[resolver:resolve] Resolving artifacts

BUILD FAILED
./resolve-basic.xml:11: Could not resolve artifacts: The following artifacts could not be resolved: org.apache.commons:commons-csv:jar:1.14.1 (absent): Cannot access central (https://repo.maven.apache.org/maven2/) in offline mode and the artifact org.apache.commons:commons-csv:jar:1.14.1 has not been downloaded from it before.

Total time: 0 seconds

The warm run prints the same Resolving artifacts and echo lines as an online run, with no download lines. That makes resolver.offline the cheap way to prove that a build can run from its cache, which is what a CI job with a restored cache and no network access needs. Gradle’s --offline and Maven’s -o are the same idea in the other two tools; see Repositories & Publishing Across Tools.

Declaring dependencies

Dependencies are Ant types, so they can be declared once, given an id and reused by several resolve tasks. A <dependency> has two spellings that mean the same:

<dependency coords="org.apache.commons:commons-csv:1.14.1"/>
<dependency groupId="org.apache.commons" artifactId="commons-csv" version="1.14.1"
            classifier="" type="jar" scope="compile">
  <exclusion groupId="commons-codec" artifactId="commons-codec"/>
</dependency>

The coords format is groupId:artifactId:version[[:type[:classifier]]:scope]: the type and classifier are optional, the scope is last. The attributes are groupId, artifactId, version, classifier, type, scope and systemPath (for system scope dependencies), and nested <exclusion> elements take groupId, artifactId, classifier, extension or coords (g:a). A <dependencies> element groups dependencies, global <exclusion> elements and other <dependencies> groups (refid), and can be filled from a POM or from a text file. The three sources of dependencies can be mixed.

From a POM

If the project already has a pom.xml, the pom task registers it and <dependencies pomRef="…"/> takes its dependencies, after property interpolation. This is the cheapest bridge between a Maven project and an Ant script that must build with the same dependencies, and the POM can be as small as the one used here:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-engine</artifactId>
  <version>1.0.0</version>
  <name>Tidepool engine</name>
  <properties>
    <csv.version>1.14.1</csv.version>
    <release.target>17</release.target>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-csv</artifactId>
      <version>${csv.version}</version>
    </dependency>
    <dependency>
      <groupId>info.picocli</groupId>
      <artifactId>picocli</artifactId>
      <version>4.7.7</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>6.1.3</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
pom-based.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="pom-based" default="paths" basedir="."
         xmlns:resolver="antlib:org.apache.maven.resolver.ant">
  <taskdef uri="antlib:org.apache.maven.resolver.ant"
           resource="org/apache/maven/resolver/ant/antlib.xml"
           classpath="lib/maven-resolver-ant-tasks-2.0.0-uber.jar"/>

  <resolver:localrepo dir="build/m2"/>
  <resolver:pom file="pom.xml" id="pom"/>

  <target name="paths">
    <echo>pom.groupId    = ${pom.groupId}</echo>
    <echo>pom.artifactId = ${pom.artifactId}</echo>
    <echo>pom.version    = ${pom.version}</echo>
    <echo>pom.name       = ${pom.name}</echo>
    <echo>pom.properties.csv.version = ${pom.properties.csv.version}</echo>

    <resolver:resolve>
      <dependencies pomRef="pom"/>
      <path refid="cp.compile" classpath="compile"/>
      <path refid="cp.runtime" classpath="runtime"/>
      <path refid="cp.test" classpath="test"/>
    </resolver:resolve>
    <pathconvert property="c.compile" refid="cp.compile" pathsep=" "><mapper type="flatten"/></pathconvert>
    <pathconvert property="c.runtime" refid="cp.runtime" pathsep=" "><mapper type="flatten"/></pathconvert>
    <pathconvert property="c.test" refid="cp.test" pathsep=" "><mapper type="flatten"/></pathconvert>
    <echo>compile: ${c.compile}</echo>
    <echo>runtime: ${c.runtime}</echo>
    <echo>test   : ${c.test}</echo>
  </target>
</project>
Buildfile: ./pom-based.xml

paths:
     [echo] pom.groupId    = io.tidepool
     [echo] pom.artifactId = tidepool-engine
     [echo] pom.version    = 1.0.0
     [echo] pom.name       = Tidepool engine
     [echo] pom.properties.csv.version = 1.14.1
[resolver:resolve] Resolving artifacts
     [echo] compile: commons-csv-1.14.1.jar commons-io-2.20.0.jar commons-codec-1.19.0.jar picocli-4.7.7.jar
     [echo] runtime: commons-csv-1.14.1.jar commons-io-2.20.0.jar commons-codec-1.19.0.jar
     [echo] test   : commons-csv-1.14.1.jar commons-io-2.20.0.jar commons-codec-1.19.0.jar picocli-4.7.7.jar junit-jupiter-api-6.1.3.jar opentest4j-1.3.0.jar junit-platform-commons-6.1.3.jar apiguardian-api-1.1.2.jar jspecify-1.0.0.jar

BUILD SUCCESSFUL

(The first run of this build printed the usual download lines before Resolving artifacts; they are left out.)

This run answers most questions about the pom type and about scopes:

  • Properties. A POM registered from a file makes its effective model available as Ant properties, prefixed with the id of the <pom> element (pom. when it has none): ${pom.groupId}, ${pom.artifactId}, ${pom.version}, ${pom.name}, and the POM’s own <properties> as ${pom.properties.csv.version}. The build file never repeats the version, which is why a build that publishes (below) can name its JAR from pom.artifactId and pom.version. A <pom> can also be declared without a file, with groupId, artifactId, version and packaging, or with coords="g:a:v"; then there is no POM file to install, and install and deploy refuse to run (see the errors table).

  • The default POM. A <pom> built from a file becomes the default for install, deploy and resolve when it has no id, and also when it has one but no default POM exists yet (Pom.execute() in 2.0.0); a later <pom> with an id does not replace an existing default, a later one without an id does. The log says Using default POM (…) when that happens. The default id is resolver.pom, a constant in the JAR.

  • Scope filters. The classpath shortcut on a <path>, <files> or <properties> target is shorthand for a list of scopes. The site defines classpath="compile" as provided,system,compile. In the run above compile includes the provided library picocli and leaves out junit-jupiter-api; runtime drops picocli because provided is not on the runtime classpath; and test has everything. provided follows Maven: it is on the compile and test classpaths, and not on the runtime one.

  • Transitive scope. The transitive dependencies of junit-jupiter-api (opentest4j, junit-platform-commons, apiguardian-api and jspecify) arrived with the test scope of the dependency that brought them.

From a plain text file, scope filters and exclusions

For an Ant-only project a POM is overhead. A dependency can come from a file with one dependency per line, in the coords format; everything after a hash is a comment, and an empty line is skipped:

dependencies.txt
# Tidepool engine dependencies, one per line: groupId:artifactId:version[:type[:classifier]]:scope
org.apache.commons:commons-csv:1.14.1:compile
info.picocli:picocli:4.7.7:provided   # supplied by the launcher
org.junit.jupiter:junit-jupiter-api:6.1.3:test

The scope filters can name scopes directly, with ! (or -) to exclude one, and exclusions can be attached to one dependency or to the whole group. This build file shows both, and a macro that prints a path in a readable way:

scopes-and-exclusions.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="scopes-and-exclusions" default="all" basedir="."
         xmlns:resolver="antlib:org.apache.maven.resolver.ant">
  <taskdef uri="antlib:org.apache.maven.resolver.ant"
           resource="org/apache/maven/resolver/ant/antlib.xml"
           classpath="lib/maven-resolver-ant-tasks-2.0.0-uber.jar"/>
  <resolver:localrepo dir="build/m2"/>

  <macrodef name="show">
    <attribute name="label"/>
    <attribute name="refid"/>
    <sequential>
      <pathconvert property="out.@{label}" refid="@{refid}" pathsep=" "><mapper type="flatten"/></pathconvert>
      <echo>@{label}: ${out.@{label}}</echo>
    </sequential>
  </macrodef>

  <target name="file">
    <resolver:resolve>
      <dependencies file="dependencies.txt"/>
      <path refid="p.compile" classpath="compile"/>
      <path refid="p.only.test" scopes="test"/>
      <path refid="p.no.provided" scopes="compile,runtime,!provided"/>
      <properties prefix="provided." scopes="provided"/>
    </resolver:resolve>
    <show label="compile" refid="p.compile"/>
    <show label="test-only" refid="p.only.test"/>
    <show label="no-provided" refid="p.no.provided"/>
    <echo>provided.info.picocli:picocli:jar = ${provided.info.picocli:picocli:jar}</echo>
  </target>

  <target name="exclusions">
    <resolver:resolve>
      <dependencies>
        <dependency coords="org.apache.commons:commons-csv:1.14.1">
          <exclusion groupId="commons-codec" artifactId="commons-codec"/>
        </dependency>
        <exclusion coords="commons-io:commons-io"/>
      </dependencies>
      <path refid="p.excl" classpath="runtime"/>
    </resolver:resolve>
    <show label="after exclusions" refid="p.excl"/>
  </target>

  <target name="all" depends="file,exclusions"/>
</project>
Buildfile: ./scopes-and-exclusions.xml

file:
[resolver:resolve] Resolving artifacts
     [echo] compile: commons-csv-1.14.1.jar commons-io-2.20.0.jar commons-codec-1.19.0.jar picocli-4.7.7.jar
     [echo] test-only: junit-jupiter-api-6.1.3.jar opentest4j-1.3.0.jar junit-platform-commons-6.1.3.jar apiguardian-api-1.1.2.jar jspecify-1.0.0.jar
     [echo] no-provided: commons-csv-1.14.1.jar commons-io-2.20.0.jar commons-codec-1.19.0.jar
     [echo] provided.info.picocli:picocli:jar = ./build/m2/info/picocli/picocli/4.7.7/picocli-4.7.7.jar

exclusions:
[resolver:resolve] Resolving artifacts
     [echo] after exclusions: commons-csv-1.14.1.jar

all:

BUILD SUCCESSFUL

scopes="test" selects only the test-scoped artifacts (not the compile ones, which classpath="test" would include), and scopes="compile,runtime,!provided" states the set directly. Both the per-dependency exclusion (commons-codec) and the global one (commons-io) removed their artifact from the graph, leaving only commons-csv. The <properties scopes="…"> form is the cheapest way to get the path of one specific file, such as a provided launcher JAR for a java task.

Versions without a version: BOMs with dependencyManagement

Since 1.6.0 the dependencyManagement type brings Maven’s dependency management to an Ant build: a BOM imported with type="pom" scope="import" supplies the versions that dependency elements leave out. It is combined with the dependencies by a registered POM, which createPom produces (next section). The 2.0.0 run shows both sides: the dependency junit-jupiter-api has no version attribute, and the JUnit BOM 6.1.3 provided it.

The management only takes effect when the task reads it from a registered POM. An unversioned dependency in a plain <dependencies> block, with no management behind it, fails before anything is downloaded:

### ant noversion
BUILD FAILED
./errors.xml:14: You must specify the 'version' for a dependency

The same failure appeared in the createPom build below when its last resolve was pointed at the dependency group (dependenciesRef) instead of at the registered POM; with the POM, the unversioned dependency resolved. For the Maven-side mechanics see Dependency Management & BOMs.

The resolve task in detail

<resolve> has one required input, <dependencies> (nested, or dependenciesRef, or the default POM) and any number of outputs.

Element or attribute Meaning

<dependencies>, dependenciesRef

The dependencies to resolve. At most one per task. Without one, the default POM is used; with neither, the build fails with No <dependencies> set for resolution.

<remoterepo>, <remoterepos>, remoteReposRef

Repositories to use for this task instead of resolver.repositories.

failOnMissingAttachments

Fail when a requested sources or javadoc attachment does not exist (see below). The default is false. It is an attribute of the task only; <properties> and <files> take attachments but not this flag.

<path refid classpath scopes>

Defines an Ant path with the resolved JARs.

<files refid dir layout attachments classpath scopes>

Defines a resource collection with refid, and copies the files to dir when it is given. layout is allowed only with dir; its placeholders are {groupId}, {groupIdDirs} (the group with slashes), {artifactId}, {version}, {baseVersion} (the -SNAPSHOT form), {extension} and {classifier}. attachments is sources or javadoc (a single value; a comma-separated list is rejected).

<properties prefix attachments classpath scopes>

Sets one property per artifact, prefix first, then groupId:artifactId:extension.

Each of the three targets can appear more than once in the same task, which is how the examples above produced the compile, runtime and test paths from one dependency graph and one set of downloads.

Attachments: sources and Javadoc

attachments="sources" or "javadoc" asks for the -sources or -javadoc classifier of every resolved artifact. That is how an Ant build feeds an IDE or a documentation task. Many artifacts do not publish both, so the behaviour for the missing ones is configurable. ant-launcher 1.10.15 has sources but no Javadoc on Central:

attachments.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="attachments" default="sources" basedir="."
         xmlns:resolver="antlib:org.apache.maven.resolver.ant">
  <taskdef uri="antlib:org.apache.maven.resolver.ant"
           resource="org/apache/maven/resolver/ant/antlib.xml"
           classpath="lib/maven-resolver-ant-tasks-2.0.0-uber.jar"/>
  <resolver:localrepo dir="build/m2"/>

  <target name="sources">
    <resolver:resolve failOnMissingAttachments="${strict}">
      <dependencies>
        <dependency coords="org.apache.ant:ant-launcher:1.10.15"/>
      </dependencies>
      <files dir="build/attachments" attachments="sources" layout="{artifactId}-{version}-{classifier}.{extension}"/>
      <files dir="build/attachments" attachments="javadoc" layout="{artifactId}-{version}-{classifier}.{extension}"/>
    </resolver:resolve>
  </target>
</project>
$ ant -q -Dstrict=false -f attachments.xml
BUILD SUCCESSFUL
Total time: 0 seconds
$ ls build/attachments
ant-launcher-1.10.15-sources.jar

$ ant -q -Dstrict=true -f attachments.xml
BUILD FAILED
./attachments.xml:10: Could not resolve artifacts: The following artifacts could not be resolved: org.apache.ant:ant-launcher:jar:javadoc:1.10.15 (absent): Could not find artifact org.apache.ant:ant-launcher:jar:javadoc:1.10.15 in central (https://repo.maven.apache.org/maven2/)

Without the attribute the behaviour was the same as false: the build succeeded and only the sources JAR was copied.

Checksum and update policies

A repository’s checksums policy decides what happens when the checksum next to a downloaded file does not match: fail, warn or ignore. To see all three, a copy of the repository that the publishing build below creates was damaged on purpose (a wrong .sha1 for the JAR and no .md5), and the same resolve was run against it:

checksums.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="chk" default="go" basedir="."
         xmlns:resolver="antlib:org.apache.maven.resolver.ant">
  <taskdef uri="antlib:org.apache.maven.resolver.ant"
           resource="org/apache/maven/resolver/ant/antlib.xml"
           classpath="lib/maven-resolver-ant-tasks-2.0.0-uber.jar"/>
  <resolver:localrepo dir="build/m2-chk"/>
  <target name="go">
    <resolver:resolve>
      <dependencies><dependency coords="io.tidepool:tidepool-engine:1.0.0:runtime"/></dependencies>
      <remoterepo id="bad" url="file://${basedir}/build/repo-bad" checksums="${policy}"/>
      <path refid="p"/>
    </resolver:resolve>
    <echo>${toString:p}</echo>
  </target>
</project>
$ ant -q -Dpolicy=fail -f checksums.xml
[resolver:resolve] Checksum validation failed, expected 'deadbeef' (REMOTE_EXTERNAL) but is actually '244aba0fdd648a120aa1101817b095247e3251d7' for file://./build/repo-bad/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar

BUILD FAILED
./checksums.xml:9: Could not resolve artifacts: The following artifacts could not be resolved: io.tidepool:tidepool-engine:jar:1.0.0 (absent): Could not transfer artifact io.tidepool:tidepool-engine:jar:1.0.0 from/to bad (file://./build/repo-bad): Checksum validation failed, expected 'deadbeef' (REMOTE_EXTERNAL) but is actually '244aba0fdd648a120aa1101817b095247e3251d7'

$ ant -q -Dpolicy=warn -f checksums.xml
[resolver:resolve] Checksum validation failed, expected 'deadbeef' (REMOTE_EXTERNAL) but is actually '244aba0fdd648a120aa1101817b095247e3251d7' for file://./build/repo-bad/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
[resolver:resolve] Checksum validation failed, expected 'deadbeef' (REMOTE_EXTERNAL) but is actually '244aba0fdd648a120aa1101817b095247e3251d7' for file://./build/repo-bad/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
     [echo] ./build/m2-chk/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar:…

$ ant -q -Dpolicy=ignore -f checksums.xml
     [echo] ./build/m2-chk/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar:…

With warn the build continues and the warning is printed twice (as observed); with ignore nothing is printed. Use fail for anything that is not a throw-away experiment. Notice that these checksums protect against corruption in transit, not against a malicious repository, which can serve a matching pair. The updates attribute (always, daily, never, or an interval in minutes) controls how often Resolver re-checks remote metadata, mainly for SNAPSHOT versions; it was not exercised here.

Publishing: install and deploy

Where resolve reads, install and deploy write. Both need to know what is being published (the artifacts) and as what (the POM that gives group, artifact and version). The artifacts are <artifact file="…" type="…" classifier="…"/> elements, nested or grouped under an <artifacts id="…"> and referenced with artifactsRef. The POM is a <pom file="…"/> registered earlier, nested in the task, or referenced with pomRef. A <pom> that has a file is required: the site states "you need to set a POM that references a file", because the POM file itself is uploaded next to the artifacts.

publish.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="tidepool" default="install" basedir="."
         xmlns:resolver="antlib:org.apache.maven.resolver.ant">
  <taskdef uri="antlib:org.apache.maven.resolver.ant"
           resource="org/apache/maven/resolver/ant/antlib.xml"
           classpath="lib/maven-resolver-ant-tasks-2.0.0-uber.jar"/>

  <property name="build.dir" location="build"/>
  <resolver:pom file="pom.xml" id="pom"/>

  <target name="package">
    <mkdir dir="${build.dir}/classes"/>
    <javac srcdir="src/main/java" destdir="${build.dir}/classes" release="17" includeantruntime="false"/>
    <jar destfile="${build.dir}/${pom.artifactId}-${pom.version}.jar" basedir="${build.dir}/classes"/>
    <jar destfile="${build.dir}/${pom.artifactId}-${pom.version}-sources.jar" basedir="src/main/java"/>
  </target>

  <target name="install" depends="package">
    <resolver:localrepo dir="${build.dir}/m2-install"/>
    <resolver:install>
      <artifact file="${build.dir}/${pom.artifactId}-${pom.version}.jar"/>
      <artifact file="${build.dir}/${pom.artifactId}-${pom.version}-sources.jar" classifier="sources"/>
    </resolver:install>
  </target>

  <target name="deploy" depends="package">
    <resolver:localrepo dir="${build.dir}/m2-install"/>
    <resolver:remoterepo id="local.release" url="file://${build.dir}/repo"/>
    <resolver:deploy>
      <remoterepo refid="local.release"/>
      <artifact file="${build.dir}/${pom.artifactId}-${pom.version}.jar"/>
      <artifact file="${build.dir}/${pom.artifactId}-${pom.version}-sources.jar" classifier="sources"/>
    </resolver:deploy>
  </target>
</project>

The POM is the one shown earlier, and the Java source is a single record io.tidepool.Station. The pom.artifactId and pom.version properties come from the registered POM.

install

install copies the artifacts into the local repository in the Maven layout, which is the repository that Maven and Gradle’s mavenLocal() read when they use the same directory (the ~/.m2/repository default); here it is build/m2-install.

ant -f publish.xml install
Buildfile: ./publish.xml

package:
    [mkdir] Created dir: ./build/classes
    [javac] Compiling 1 source file to ./build/classes
      [jar] Building jar: ./build/tidepool-engine-1.0.0.jar
      [jar] Building jar: ./build/tidepool-engine-1.0.0-sources.jar

install:
[resolver:install] Using default POM (io.tidepool:tidepool-engine:1.0.0)
[resolver:install] Installing ./pom.xml to ./build/m2-install/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom
[resolver:install] Installing ./build/tidepool-engine-1.0.0.jar to ./build/m2-install/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
[resolver:install] Installing ./build/tidepool-engine-1.0.0-sources.jar to ./build/m2-install/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0-sources.jar
[resolver:install] Installing io.tidepool:tidepool-engine/maven-metadata.xml to ./build/m2-install/io/tidepool/tidepool-engine/maven-metadata-local.xml

BUILD SUCCESSFUL
Total time: 0 seconds

Two points deserve notice. The file name of the installed POM is derived from the coordinates (tidepool-engine-1.0.0.pom) and its content is the source pom.xml copied as it is, including ${csv.version}; consumers that read it let Maven’s model builder interpolate it. (The next section shows a generated POM that is complete without interpolation.) And the installed directory holds more than the artifacts: _remote.repositories, which records where each file came from, maven-metadata-local.xml, and under the repository root .meta/resolver-status.properties and a resolver-status.properties, which Resolver uses to remember failed lookups.

deploy

deploy uploads to the remote repository nested in it, and (for a SNAPSHOT version) to a separate snapshotrepo. The checksum files and the repository metadata are generated by the task. For this page the only target is a file:// URL under build; publishing to a real server or to Maven Central was not run here, and credentials for one would go in an <authentication> element with placeholders such as ${env.REPO_TOKEN}, never in the file.

ant -f publish.xml deploy
find build/repo -type f | sort
Buildfile: ./publish.xml

package:

deploy:
[resolver:deploy] Using default POM (io.tidepool:tidepool-engine:1.0.0)
[resolver:deploy] Uploading to local.release file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom
[resolver:deploy] Uploaded to local.release file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom (936 B at 304.7 KB/sec)
[resolver:deploy] Uploading to local.release file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
[resolver:deploy] Uploading to local.release file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0-sources.jar
[resolver:deploy] Uploaded to local.release file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0-sources.jar (725 B at 708.0 KB/sec)
[resolver:deploy] Uploaded to local.release file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar (2 KB at 1249.0 KB/sec)
[resolver:deploy] Downloading from local.release file://./build/repo/io/tidepool/tidepool-engine/maven-metadata.xml
[resolver:deploy] Uploading to local.release file://./build/repo/io/tidepool/tidepool-engine/maven-metadata.xml
[resolver:deploy] Uploaded to local.release file://./build/repo/io/tidepool/tidepool-engine/maven-metadata.xml (306 B)

BUILD SUCCESSFUL
Total time: 0 seconds
build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0-sources.jar
build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0-sources.jar.md5
build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0-sources.jar.sha1
build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar.md5
build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar.sha1
build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom
build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom.md5
build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom.sha1
build/repo/io/tidepool/tidepool-engine/maven-metadata.xml
build/repo/io/tidepool/tidepool-engine/maven-metadata.xml.md5
build/repo/io/tidepool/tidepool-engine/maven-metadata.xml.sha1

The layout is the standard Maven repository layout, with .md5 and .sha1 checksum files next to every file and the artifact-level maven-metadata.xml (in the tidepool-engine directory, not the group’s) that lists the released versions. What is not produced is PGP signatures (.asc) or the other checks that Maven Central demands, so this task alone cannot publish to Central; the Maven side of that is on Publishing to Maven Central, and the layout is drawn on Repositories & Publishing Across Tools. Only file:, http: and https: repositories are supported; a ftp:// URL failed, as shown in the errors table.

Releases and SNAPSHOTs

deploy sends a SNAPSHOT version to the <snapshotrepo> and a release version to the <remoterepo>, as Maven’s distributionManagement does. A SNAPSHOT upload is timestamped, and a second upload does not replace the first. The deploy below uses the POM snapshot-pom.xml, which has the version 1.1.0-SNAPSHOT:

snapshots.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="snapshots" default="deploy" basedir="."
         xmlns:resolver="antlib:org.apache.maven.resolver.ant">
  <taskdef uri="antlib:org.apache.maven.resolver.ant"
           resource="org/apache/maven/resolver/ant/antlib.xml"
           classpath="lib/maven-resolver-ant-tasks-2.0.0-uber.jar"/>
  <property name="build.dir" location="build"/>
  <resolver:localrepo dir="build/m2-snap"/>
  <resolver:pom file="snapshot-pom.xml"/>

  <target name="deploy">
    <mkdir dir="${build.dir}/classes"/>
    <jar destfile="${build.dir}/tidepool-model-1.1.0-SNAPSHOT.jar" basedir="${build.dir}/classes" whenempty="create"/>
    <resolver:remoterepo id="releases" url="file://${build.dir}/repo-snapshots/releases"/>
    <resolver:remoterepo id="snapshots" url="file://${build.dir}/repo-snapshots/snapshots"/>
    <resolver:deploy>
      <remoterepo refid="releases"/>
      <snapshotrepo refid="snapshots"/>
      <artifact file="${build.dir}/tidepool-model-1.1.0-SNAPSHOT.jar"/>
    </resolver:deploy>
  </target>
</project>
Buildfile: ./snapshots.xml

deploy:
    [mkdir] Created dir: ./build/classes
      [jar] JARs are never empty, they contain at least a manifest file
      [jar] Building MANIFEST-only jar: ./build/tidepool-model-1.1.0-SNAPSHOT.jar
[resolver:deploy] Using default POM (io.tidepool:tidepool-model:1.1.0-SNAPSHOT)
[resolver:deploy] Downloading from snapshots file://./build/repo-snapshots/snapshots/io/tidepool/tidepool-model/1.1.0-SNAPSHOT/maven-metadata.xml
[resolver:deploy] Uploading to snapshots file://./build/repo-snapshots/snapshots/io/tidepool/tidepool-model/1.1.0-SNAPSHOT/tidepool-model-1.1.0-20261017.100531-1.pom
[resolver:deploy] Uploaded to snapshots file://./build/repo-snapshots/snapshots/io/tidepool/tidepool-model/1.1.0-SNAPSHOT/tidepool-model-1.1.0-20261017.100531-1.pom (250 B at 122.1 KB/sec)
[resolver:deploy] Uploading to snapshots file://./build/repo-snapshots/snapshots/io/tidepool/tidepool-model/1.1.0-SNAPSHOT/tidepool-model-1.1.0-20261017.100531-1.jar
[resolver:deploy] Uploaded to snapshots file://./build/repo-snapshots/snapshots/io/tidepool/tidepool-model/1.1.0-SNAPSHOT/tidepool-model-1.1.0-20261017.100531-1.jar (332 B at 324.2 KB/sec)
[resolver:deploy] Downloading from snapshots file://./build/repo-snapshots/snapshots/io/tidepool/tidepool-model/maven-metadata.xml
[resolver:deploy] Uploading to snapshots file://./build/repo-snapshots/snapshots/io/tidepool/tidepool-model/1.1.0-SNAPSHOT/maven-metadata.xml
[resolver:deploy] Uploaded to snapshots file://./build/repo-snapshots/snapshots/io/tidepool/tidepool-model/1.1.0-SNAPSHOT/maven-metadata.xml (775 B at 756.8 KB/sec)
[resolver:deploy] Uploading to snapshots file://./build/repo-snapshots/snapshots/io/tidepool/tidepool-model/maven-metadata.xml
[resolver:deploy] Uploaded to snapshots file://./build/repo-snapshots/snapshots/io/tidepool/tidepool-model/maven-metadata.xml (285 B)

BUILD SUCCESSFUL
Total time: 0 seconds

A second run, in the same second, produced tidepool-model-1.1.0-20261017.100531-2.jar (and its .pom) next to the -1 files. The release repository (repo-snapshots/releases) was not touched. (The run above printed Downloading from snapshots …/maven-metadata.xml for the not yet existing metadata, which is how Resolver reads the previous build number.)

Consuming what was published

The repository that deploy wrote can be used straight away as a remote repository by another build, even a Maven or Gradle one. This consumer lists it before Central, uses a different local repository (so nothing is taken from the earlier install), and asks for the sources of everything:

consume.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="consumer" default="consume" basedir="."
         xmlns:resolver="antlib:org.apache.maven.resolver.ant">
  <taskdef uri="antlib:org.apache.maven.resolver.ant"
           resource="org/apache/maven/resolver/ant/antlib.xml"
           classpath="lib/maven-resolver-ant-tasks-2.0.0-uber.jar"/>

  <resolver:localrepo dir="build/m2-consumer"/>
  <resolver:remoterepo id="tidepool.local" url="file://${basedir}/build/repo" snapshots="false" checksums="fail"/>
  <resolver:remoterepos id="all">
    <remoterepo refid="tidepool.local"/>
    <remoterepo id="central" url="https://repo.maven.apache.org/maven2/" snapshots="false"/>
  </resolver:remoterepos>

  <target name="consume">
    <resolver:resolve>
      <dependencies>
        <dependency coords="io.tidepool:tidepool-engine:1.0.0"/>
      </dependencies>
      <remoterepos refid="all"/>
      <path refid="cp" classpath="runtime"/>
      <files dir="build/consumed-sources" attachments="sources" layout="{artifactId}-{classifier}.{extension}"/>
    </resolver:resolve>
    <pathconvert property="cp.flat" refid="cp" pathsep=" "><mapper type="flatten"/></pathconvert>
    <echo>runtime classpath: ${cp.flat}</echo>
  </target>
</project>
Buildfile: ./consume.xml

consume:
[resolver:resolve] Downloading from tidepool.local file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom
[resolver:resolve] Downloaded from tidepool.local file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom (936 B at 457.0 KB/sec)
[resolver:resolve] Downloading from tidepool.local file://./build/repo/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom
[resolver:resolve] Downloading from central https://repo.maven.apache.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom
[resolver:resolve] Downloaded from central https://repo.maven.apache.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom (22 KB at 578.3 KB/sec)
…
[resolver:resolve] Resolving artifacts
[resolver:resolve] Downloading from tidepool.local file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
[resolver:resolve] Downloaded from tidepool.local file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar (2 KB at 1249.0 KB/sec)
…
[resolver:resolve] Downloaded from tidepool.local file://./build/repo/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0-sources.jar (725 B at 708.0 KB/sec)
…
     [echo] runtime classpath: tidepool-engine-1.0.0.jar commons-csv-1.14.1.jar commons-io-2.20.0.jar commons-codec-1.19.0.jar

BUILD SUCCESSFUL
Total time: 1 second

The tidepool artifact came from the file repository, and commons-csv and the other third-party libraries from Central (each lookup tries the repositories in order and the first that has the file wins, so the log shows a Downloading from tidepool.local attempt first for every file). The directory build/consumed-sources held tidepool-engine-sources.jar and the three sources JARs of the libraries. The same repository can be read from Maven with a <repository> element and from Gradle with maven { url = uri("…/build/repo") }; this is the cross-tool round trip described on Repositories & Publishing Across Tools.

Generating a POM with createPom

A published artifact needs a POM so that consumers can resolve its dependencies, and an Ant-only project does not have one. The createPom task writes one from the Ant-side description: group, artifact and version, name and description, the dependencies and dependencyManagement types by reference, and nested licenses, developers, repositories and scm elements. It also registers the POM it wrote as the default one, unless skipPomRegistration is true. The task is not described on the site’s main page; its attributes (pomTarget, groupId, artifactId, version, name, description, dependenciesRef, dependencyManagementRef, skipPomRegistration) come from the usage guide and the API.

create-pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="tidepool" default="publish" basedir="."
         xmlns:resolver="antlib:org.apache.maven.resolver.ant">
  <taskdef uri="antlib:org.apache.maven.resolver.ant"
           resource="org/apache/maven/resolver/ant/antlib.xml"
           classpath="lib/maven-resolver-ant-tasks-2.0.0-uber.jar"/>

  <property name="groupId" value="io.tidepool"/>
  <property name="artifactId" value="tidepool-engine"/>
  <property name="version" value="1.0.0"/>
  <property name="build.dir" location="build"/>

  <resolver:localrepo dir="build/m2-pom"/>

  <resolver:dependencyManagement id="dm">
    <dependencies>
      <dependency groupId="org.junit" artifactId="junit-bom" version="6.1.3" type="pom" scope="import"/>
    </dependencies>
  </resolver:dependencyManagement>

  <resolver:dependencies id="deps">
    <dependency coords="org.apache.commons:commons-csv:1.14.1"/>
    <dependency groupId="org.junit.jupiter" artifactId="junit-jupiter-api" scope="test"/>
  </resolver:dependencies>

  <target name="pom">
    <mkdir dir="${build.dir}"/>
    <resolver:createPom pomTarget="${build.dir}/${artifactId}-${version}.pom"
                        groupId="${groupId}" artifactId="${artifactId}" version="${version}"
                        name="Tidepool engine" description="Tide prediction engine"
                        dependenciesRef="deps" dependencyManagementRef="dm">
      <licenses>
        <license>
          <name>Apache License, Version 2.0</name>
          <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
        </license>
      </licenses>
    </resolver:createPom>
    <echo message="registered POM: ${pom.groupId}:${pom.artifactId}:${pom.version}"/>
  </target>

  <target name="publish" depends="pom">
    <mkdir dir="${build.dir}/classes"/>
    <javac srcdir="src/main/java" destdir="${build.dir}/classes" release="17" includeantruntime="false"/>
    <jar destfile="${build.dir}/${artifactId}-${version}.jar" basedir="${build.dir}/classes"/>
    <resolver:install>
      <artifact file="${build.dir}/${artifactId}-${version}.jar"/>
    </resolver:install>
    <resolver:resolve>
      <path refid="cp.test" classpath="test"/>
    </resolver:resolve>
    <pathconvert property="cp.flat" refid="cp.test" pathsep=" "><mapper type="flatten"/></pathconvert>
    <echo>test classpath: ${cp.flat}</echo>
  </target>
</project>

The generated POM, which is what install then copied:

build/tidepool-engine-1.0.0.pom
<?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>io.tidepool</groupId>
  <artifactId>tidepool-engine</artifactId>
  <version>1.0.0</version>
  <name>Tidepool engine</name>
  <description>Tide prediction engine</description>
  <licenses>
    <license>
      <name>Apache License, Version 2.0</name>
      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
    </license>
  </licenses>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.junit</groupId>
        <artifactId>junit-bom</artifactId>
        <version>6.1.3</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-csv</artifactId>
      <version>1.14.1</version>
      <classifier></classifier>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

The empty <classifier></classifier> on the coords-declared dependency is how 2.0.0 wrote it (the dependency that was declared with attributes did not get one); Maven treats an empty classifier as none, and the POM was usable, but it is a blemish worth knowing if the file is checked in or diffed.

Buildfile: ./create-pom.xml

pom:
    [mkdir] Created dir: ./build
      [pom] Downloading from central https://repo.maven.apache.org/maven2/org/junit/junit-bom/6.1.3/junit-bom-6.1.3.pom
      [pom] Downloaded from central https://repo.maven.apache.org/maven2/org/junit/junit-bom/6.1.3/junit-bom-6.1.3.pom (6 KB at 160.3 KB/sec)
     [echo] registered POM: io.tidepool:tidepool-engine:1.0.0

publish:
    [mkdir] Created dir: ./build/classes
    [javac] Compiling 1 source file to ./build/classes
      [jar] Building jar: ./build/tidepool-engine-1.0.0.jar
[resolver:install] Using default POM (io.tidepool:tidepool-engine:1.0.0)
[resolver:install] Installing ./build/tidepool-engine-1.0.0.pom to ./build/m2-pom/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom
[resolver:install] Installing ./build/tidepool-engine-1.0.0.jar to ./build/m2-pom/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
[resolver:install] Installing io.tidepool:tidepool-engine/maven-metadata.xml to ./build/m2-pom/io/tidepool/tidepool-engine/maven-metadata-local.xml
[resolver:resolve] Using default pom for dependency resolution (io.tidepool:tidepool-engine:1.0.0 (org.apache.maven.resolver.internal.ant.types.Pom@12aa4996))
…
[resolver:resolve] Resolving artifacts
…
     [echo] test classpath: commons-csv-1.14.1.jar commons-io-2.20.0.jar commons-codec-1.19.0.jar junit-jupiter-api-6.1.3.jar opentest4j-1.3.0.jar junit-platform-commons-6.1.3.jar apiguardian-api-1.1.2.jar jspecify-1.0.0.jar

BUILD SUCCESSFUL
Total time: 1 second

The last resolve has no <dependencies> at all: it used "the default pom for dependency resolution", the one createPom had registered, with its imported BOM supplying the version of junit-jupiter-api. This is the whole round trip an Ant project needs to publish a library with correct metadata: dependencies declared once, a POM generated from them, the dependencies of the build resolved from the same declaration. A resolve that was given the unversioned dependency through dependenciesRef instead of the registered POM stopped with You must specify the 'version' for a dependency.

Errors and how to read them

Most failures are one of a few messages. Each of these was produced on purpose with a small build file (errors.xml):

Message Cause Fix

Could not find artifact org.apache.commons:commons-csv:jar:9.9.9 in central (https://repo.maven.apache.org/maven2/), preceded by The POM for … is missing, no dependency information available

The version does not exist in any repository that was searched. The wording is Maven Resolver’s, so a search for it finds Maven answers as well.

Fix the coordinate, or add the repository that holds the artifact.

Bad dependency coordinates 'commons-csv', expected format is <groupId>:<artifactId>:<version>[[:<type>[:<classifier>]]:<scope>]

coords with fewer than three parts.

Write g:a:v.

You must specify the 'version' for a dependency

A dependency without a version and without management.

Add the version, or register a POM or dependencyManagement that provides it.

Unknown scope 'bogus' for dependency

A misspelled scope. This is only a warning: the build still succeeded.

Use compile, provided, runtime, test or system.

You must specify the <pom file="..."> element to denote the descriptor for the artifacts

install or deploy without a registered POM that has a file (also when the POM was declared with coords only).

Register <pom file="pom.xml"/> or generate one with createPom.

No <dependencies> set for resolution

resolve with outputs but neither dependencies nor a default POM.

Add <dependencies>, dependenciesRef or register a POM.

Cannot access ftp://example.invalid/repo with type default using the available connector factories: BasicRepositoryConnectorFactory, then Could not collect dependencies

A repository URL with a protocol other than file:, http: or https:.

Use a supported protocol or put a repository manager in front.

Cannot access central (…) in offline mode and the artifact … has not been downloaded from it before

resolver.offline with a cold cache.

Run online once, or populate the local repository.

Checksum validation failed, expected '…' (REMOTE_EXTERNAL) but is actually '…'

A corrupted or tampered remote file with checksums="fail" or warn.

Fix the repository; do not weaken the policy.

Two empty cases are silent. A resolve with dependencies but without <path>, <files> or <properties> resolves and discards (it ran successfully and produced nothing, which is how a "pre-warm the cache" target can be written), and an install with a registered POM but no artifacts also ended with BUILD SUCCESSFUL.

Ivy or the Maven Resolver Ant Tasks?

Both tools solve "an Ant build needs libraries from Maven repositories". The difference is the model and, with it, the amount of build-file machinery.

Question Maven Resolver Ant Tasks Ivy 2.6.0

Dependency model

Maven’s: coordinates, scopes, nearest-wins mediation, exclusions, BOMs

Ivy’s: modules, arbitrary configurations and mappings between them, pluggable conflict managers and version matchers

Where dependencies are declared

In the build file, a text file or a pom.xml

In an ivy.xml, next to the build file

Repositories and credentials

The Maven settings.xml is read as it is; mirrors, proxies and servers are shared with Maven

ivysettings.xml; separate from Maven’s settings

Reports and caching

The Maven local repository (shared with Maven); no HTML report, but ant -v prints the resolved dependency graph

Its own cache and an HTML/XML resolution report; very flexible retrieve patterns

Publishing

install and deploy in the Maven layout, createPom for a POM

publish with any resolver, makepom for a POM

Setup

One JAR, a namespace and about three lines

One JAR, a namespace and usually an ivysettings.xml as well

Best when

The project is, or must interoperate with, a Maven project, or you want the least machinery

The build needs Ivy’s configurations, its dependency reports or non-Maven repositories

For a new Ant build that only consumes Maven Central, the Resolver Ant Tasks are the lower-ceremony choice, and the move from Ivy to them is walked through, with both versions run, in Migrating Between Tools. Ivy’s side is on Ivy: Settings & Resolvers, Ivy Ant Tasks and Ivy: Publishing & Multi-Project Builds.

The same thing in Maven and in Gradle

The tasks are Maven Resolver wrapped for Ant, so each has a direct Maven counterpart, and the Gradle equivalents are native features of that tool.

Resolver Ant Tasks Maven Gradle

resolve with a path

dependency:build-classpath, or the compile and test classpaths the plugins build themselves; see Dependencies, Scopes & Mediation

A resolvable configuration such as compileClasspath; see Declaring Dependencies & Configurations

resolve with files and a dir

dependency:copy-dependencies

A Sync or Copy task from a configuration

install, deploy

install:install-file, deploy:deploy-file, or the install and deploy phases; see Install, Deploy & Distribution Management

maven-publish with publishToMavenLocal and a file: repository

remoterepo, mirror, proxy, authentication

repositories, settings.xml mirrors, proxies and servers; see Repositories, Mirrors & Repository Managers

repositories { } and credentials; see Repositories & Content Filtering

dependencyManagement with a BOM

dependencyManagement with import scope; see Dependency Management & BOMs

platform() and enforcedPlatform()

resolver.offline

-o

--offline

createPom

The pom.xml itself

The pom block of maven-publish

The engine

Maven Resolver, described on Maven Resolver

Gradle’s own resolution engine

Where the books are outdated

Older Ant books and tutorials predate these tasks. They teach a lib/ directory with the JARs checked in, then the older Maven Ant Tasks (maven-ant-tasks, last release 2.1.3, project site last published in 2014) or Ivy. The Resolver Ant Tasks take over from the Maven Ant Tasks, and they are the route into Ant that the Maven project maintains today. Anything in an older book that says "Ant has no dependency management of its own, so check the JARs in" is still true of Ant’s core, but there are three maintained answers now: Ivy, the Resolver Ant Tasks, or a different tool (see Deprecations & Migration).

References