Ivy: Publishing & Multi-Project Builds
|
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 covers the producing side of Ivy: how a module becomes something other builds can depend on (deliver and publish),
how several modules that depend on each other are built in the right order (buildlist with subant), how the published
revision is chosen, how another project resolves what was published, and how to publish a POM beside the JAR so that Maven
builds can use it (makepom). It is written against Apache Ivy 2.6.0 running in Apache Ant 1.10.18. Every example was run on
2026-10-16 on JDK 26 (macOS), with the Maven 3.10.0 distribution for the interoperability check. The work happened in a scratch
directory with its own Ivy cache; paths in output were shortened to . and the machine name to host. Publishing goes only
to build/repo inside that directory, a file:// style repository, never to a real server. The consuming side (resolving,
retrieving, the cache) is on Ivy Ant Tasks; the
descriptors are on
Ivy: Module Descriptors &
Configurations; resolvers and their patterns are on
Ivy: Settings & Resolvers; version
constraints and conflicts on
Ivy: Conflicts & Versions. The repository
and publishing ideas shared by all three build tools are on
Repositories & Publishing Across
Tools.
The scenario: three modules, one repository
The tidepool project is split into three modules that depend on each other in a line:
.
├── build.xml root build: ordering and publishing every module
├── common.xml targets shared by the modules
├── ivysettings.xml the repositories
├── data/stations.csv
├── tidepool-model/
│ ├── build.xml ivy.xml src/main/java/...
├── tidepool-engine/
│ ├── build.xml ivy.xml src/main/java/...
├── tidepool-cli/
│ ├── build.xml ivy.xml src/main/java/...
└── build/repo/ the file repository that publish writes to
Each module has an ivy.xml with two configurations, compile and runtime (which extends compile), one published JAR,
and dependencies on its neighbour with a dynamic revision. The engine’s descriptor:
<ivy-module version="2.0">
<info organisation="io.tidepool" module="tidepool-engine" status="integration"/>
<configurations>
<conf name="compile"/>
<conf name="runtime" extends="compile"/>
</configurations>
<publications>
<artifact name="tidepool-engine" type="jar" conf="compile,runtime"/>
</publications>
<dependencies>
<dependency org="io.tidepool" name="tidepool-model" rev="latest.integration" conf="compile->compile;runtime->runtime"/>
<dependency org="org.apache.commons" name="commons-csv" rev="1.14.1" conf="compile->default"/>
</dependencies>
</ivy-module>
The descriptor has no revision in info: the revision is decided at publication time (below). The model has no
dependencies and the CLI depends on the engine and on picocli in the same way. The module’s build.xml only imports the shared
targets:
<project name="tidepool-engine" default="jar">
<import file="../common.xml"/>
</project>
<ivysettings>
<properties environment="env"/>
<property name="repo.dir" value="${ivy.settings.dir}/build/repo" override="false"/>
<settings defaultResolver="chain"/>
<caches defaultCacheDir="${ivy.settings.dir}/build/ivy-cache"/>
<resolvers>
<filesystem name="shared">
<ivy pattern="${repo.dir}/shared/[organisation]/[module]/[revision]/ivy.xml"/>
<artifact pattern="${repo.dir}/shared/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]"/>
</filesystem>
<filesystem name="m2" m2compatible="true">
<ivy pattern="${repo.dir}/m2/[organisation]/[module]/[revision]/[module]-[revision].pom"/>
<artifact pattern="${repo.dir}/m2/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]"/>
</filesystem>
<ibiblio name="central" m2compatible="true"/>
<chain name="chain">
<resolver ref="shared"/>
<resolver ref="central"/>
</chain>
</resolvers>
</ivysettings>
shared is an Ivy-layout repository on the local disk: one directory per organisation, module and revision, with the
ivy.xml next to the JAR. m2 is the same idea in Maven layout, used in the makepom section. Resolution looks in shared
first and then in Maven Central, which is where commons-csv and picocli come from. Because the repository is a plain
directory, "publish" in this page means "copy files into a directory tree", which is also what it means when that directory is
a network share or the working copy of a repository server; a remote protocol only changes the resolver.
<project name="common" xmlns:ivy="antlib:org.apache.ivy.ant">
<dirname property="root.dir" file="${ant.file.common}"/>
<property name="ivy.default.ivy.user.dir" location="${root.dir}/build/ivy-user"/>
<property name="build.dir" location="build"/>
<property name="classes.dir" location="${build.dir}/classes"/>
<property name="dist.dir" location="${build.dir}/dist"/>
<property name="status" value="integration"/>
<property name="version.prefix" value="1.0."/>
<property name="force" value="false"/>
<property name="refresh" value="false"/>
<property name="overwrite" value="false"/>
<ivy:settings file="${root.dir}/ivysettings.xml"/>
<target name="resolve" description="resolve the module's dependencies">
<ivy:resolve file="ivy.xml" log="download-only" refresh="${refresh}"/>
</target>
<target name="compile" depends="resolve" description="compile the module">
<ivy:cachepath pathid="compile.path" conf="compile"/>
<mkdir dir="${classes.dir}"/>
<javac srcdir="src/main/java" destdir="${classes.dir}" release="21" includeantruntime="false"
classpathref="compile.path"/>
</target>
<target name="jar" depends="compile" description="package the module">
<mkdir dir="${dist.dir}"/>
<jar destfile="${dist.dir}/${ivy.module}.jar" basedir="${classes.dir}"/>
</target>
<target name="newversion" depends="resolve" unless="ivy.new.revision">
<ivy:buildnumber organisation="${ivy.organisation}" module="${ivy.module}" revision="${version.prefix}"/>
</target>
<target name="publish" depends="jar, newversion" description="publish to the shared repository">
<ivy:publish resolver="shared" pubrevision="${ivy.new.revision}" status="${status}"
forcedeliver="${force}" overwrite="${overwrite}"
artifactspattern="${dist.dir}/[artifact].[ext]"/>
<echo>${ivy.module} published as ${ivy.new.revision}</echo>
</target>
<target name="clean"><delete dir="${build.dir}"/></target>
</project>
The shape follows the multi-project tutorial of the Ivy distribution: every module imports one common build file, modules
differ only in their ivy.xml, and the compile class path comes straight from the cache with cachepath. The properties
force, refresh and overwrite are switches that the examples flip on the command line (-Dforce=true); each is explained
where it matters. The newversion target skips itself when ivy.new.revision is already set, which is how a release manager
forces a revision with -Divy.new.revision=1.2.0.
Source: Ivy tutorial: multiple projects
One publication, step by step
For a single module the sequence that publish triggers, and the targets above implement, is:
buildnumber: choosing the revision
buildnumber (rules and attributes on Ivy Ant Tasks) asks
the repository for the newest revision that starts with the prefix 1.0. and proposes the next one. With an empty repository the
first proposal is 1.0.0; each later publication gets the next number (1.0.1, 1.0.2). Because the number comes from the
repository, two developers publishing to the same repository get different revisions, and no revision is written into any
source file. The tutorial puts a "target version" in a properties file and uses defaultBuildNumber and a separator for
revisions like 1.0-dev-b1.
deliver: the resolved descriptor
deliver takes the descriptor of the module and the last resolve, and writes a resolved copy: the revision and status are
filled in, variables are replaced, and every dynamic revision is replaced by the static one that the resolve found, with the
original constraint kept in revConstraint. A module that was resolved on Monday with latest.integration and published as
1.0.2 therefore says rev="1.0.2", and a consumer who resolves it next year still gets the same dependency. Nothing is
uploaded.
| Attribute | Effect |
|---|---|
|
Where the file is written; the default is |
|
The revision to put in the file. Default: |
|
Branch and publication date ( |
|
The status to write; default |
|
|
|
Replace the revision of forced dependencies by the one the resolver forced (since 2.2, default |
|
Write |
|
When the descriptor |
|
Configurations to include; wildcards allowed (since 2.0). Default all. |
|
Target to call for recursive delivery (below). |
Three deliveries of the engine, from the same resolve, on the same day, with deliverpattern writing the files to different
names:
<target name="deliver-static" depends="resolve">
<ivy:deliver deliverpattern="${build.dir}/delivered/static-[revision].xml"
pubrevision="1.1.0" status="milestone"/>
</target>
<target name="deliver-dynamic" depends="resolve">
<ivy:deliver deliverpattern="${build.dir}/delivered/dynamic-[revision].xml"
pubrevision="1.1.0" status="milestone" replacedynamicrev="false"/>
</target>
<target name="deliver-conf" depends="resolve">
<ivy:deliver deliverpattern="${build.dir}/delivered/compile-only-[revision].xml"
pubrevision="1.1.0" status="milestone" conf="compile"/>
</target>
deliver-static:
[ivy:deliver] :: delivering :: io.tidepool#tidepool-engine;working@host :: 1.1.0 :: milestone :: Fri Oct 16 12:23:21 CEST 2026
[ivy:deliver] delivering ivy file to ./tidepool-engine/build/delivered/static-1.1.0.xml
deliver-dynamic:
[ivy:deliver] :: delivering :: io.tidepool#tidepool-engine;working@host :: 1.1.0 :: milestone :: Fri Oct 16 12:23:21 CEST 2026
[ivy:deliver] delivering ivy file to ./tidepool-engine/build/delivered/dynamic-1.1.0.xml
deliver-conf:
[ivy:deliver] :: delivering :: io.tidepool#tidepool-engine;working@host :: 1.1.0 :: milestone :: Fri Oct 16 12:23:21 CEST 2026
[ivy:deliver] delivering ivy file to ./tidepool-engine/build/delivered/compile-only-1.1.0.xml
The relevant lines of the three files, side by side:
| File | info and artifact |
Dependency on the model |
|---|---|---|
|
|
|
|
same |
|
|
same |
|
Note the case of a milestone depending on an integration build: the delivered descriptor of a milestone still points to an integration revision of the model. That is the situation recursive delivery exists for.
Recursive delivery. With delivertarget, deliver calls the named target (through antcall) for each dependency whose
status is lower than the status being delivered, with the properties dependency.name, dependency.version (the resolved
revision), dependency.published.version and dependency.published.status. By default those last two are asked of the user
with input; setting recursive.delivery.status and recursive.delivery.version answers for everybody. The target is where a
build would deliver and publish the dependency (usually by calling the dependency’s own build file):
<target name="deliver-recursive" depends="resolve">
<property name="recursive.delivery.status" value="release"/>
<property name="recursive.delivery.version" value="2.0.0"/>
<ivy:deliver deliverpattern="${build.dir}/delivered/recursive-[revision].xml"
pubrevision="2.0.0" status="release" delivertarget="deliver-dependency"/>
</target>
<target name="deliver-dependency">
<echo>recursive delivery: ${dependency.name} ${dependency.version} becomes ${dependency.published.version} (${dependency.published.status})</echo>
</target>
deliver-recursive:
[ivy:deliver] :: delivering :: io.tidepool#tidepool-engine;working@host :: 2.0.0 :: release :: Fri Oct 16 12:23:21 CEST 2026
deliver-dependency:
[echo] recursive delivery: tidepool-model 1.0.2 becomes 2.0.0 (release)
[ivy:deliver] delivering ivy file to ./tidepool-engine/build/delivered/recursive-2.0.0.xml
The delivered engine now says <dependency org="io.tidepool" name="tidepool-model" rev="2.0.0" revConstraint="latest.integration" .../>.
The echo stands in for the work a real target does; that work (releasing model 2.0.0 first) was not run here, so
that revision does not exist in the repository. When status is integration no recursive delivery happens at all.
Delivery is usually implicit. publish delivers by itself when it has no delivered file, and the manual’s advice is to call
deliver yourself only when the delivered file must be edited or post-processed before upload (to add a source-control tag,
for example), or when a recursive delivery has to be followed by a second resolve against the delivered file.
Source: Ivy: deliver
publish: uploading
publish copies the artifacts listed in the descriptor’s publications from artifactspattern into the resolver’s
repository, together with the delivered ivy.xml, using the resolver named by resolver. It does not create the artifacts: a
missing JAR fails the build (haltonmissing). The pattern may contain the tokens of the artifact ([artifact], [ext],
[type], [conf]…) and must be the pattern under which the build left the files; further patterns can be given with nested
artifacts elements, and extra artifacts with nested artifact elements.
| Attribute | Effect |
|---|---|
|
Required. The resolver, by name in the settings, that does the writing. |
|
Where artifacts, and the Ivy file to publish, are found. The default of the second is the first. |
|
What to deliver, if a delivery is needed (as for |
|
Deliver again even when the delivered file exists. Default |
|
Update revision, branch, publication date and status of the existing delivered file instead of delivering again. |
|
Publish the Ivy file (default |
|
Replace files for a revision that already exists. Default |
|
As for |
|
Configurations to publish (wildcards since 2.2). |
|
What to do with an artifact that is not where the pattern says. Both default to |
|
Validate the descriptor; the settings instance to use. |
|
Recursive delivery, as above. |
The first run of the root target publish-all (below) from an empty repository, for the model:
publish:
[ivy:publish] :: delivering :: io.tidepool#tidepool-model;working@host :: 1.0.0 :: integration :: Fri Oct 16 12:23:11 CEST 2026
[ivy:publish] delivering ivy file to ./tidepool-model/build/dist/ivy.xml
[ivy:publish] :: publishing :: io.tidepool#tidepool-model
[ivy:publish] published tidepool-model to ./build/repo/shared/io.tidepool/tidepool-model/1.0.0.part/tidepool-model-1.0.0.jar
[ivy:publish] published ivy to ./build/repo/shared/io.tidepool/tidepool-model/1.0.0.part/ivy.xml
[ivy:publish] publish committed: moved ./build/repo/shared/io.tidepool/tidepool-model/1.0.0.part
[ivy:publish] to ./build/repo/shared/io.tidepool/tidepool-model/1.0.0
[echo] tidepool-model published as 1.0.0
The .part directory is the filesystem resolver’s atomic publish: the files are written into a directory next to the final
one and the directory is renamed when everything is in place, so that a consumer never sees half a publication. After the
three modules were published, the repository held, for each revision, the JAR and the ivy.xml plus .md5 and .sha1
checksum files of each (resolvers can check downloads against them; checksums and signatures across the three tools are on
Supply Chain Security):
build/repo/shared/io.tidepool/tidepool-model/1.0.0/ivy.xml
build/repo/shared/io.tidepool/tidepool-model/1.0.0/ivy.xml.md5
build/repo/shared/io.tidepool/tidepool-model/1.0.0/ivy.xml.sha1
build/repo/shared/io.tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
build/repo/shared/io.tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar.md5
build/repo/shared/io.tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar.sha1
...
The ivy.checksums property (default sha1,md5) selects the algorithms. SHA-1 and MD5 are weak by today’s standards, so
the checksums protect against truncated and corrupted transfers, not against tampering.
Source: Ivy: publish
The delivered file in the repository
The engine’s published ivy.xml (revision 1.0.0, before any other revision existed) is the delivered file:
<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0">
<info organisation="io.tidepool" module="tidepool-engine" revision="1.0.0" status="integration" publication="20261016121709"/>
<configurations>
<conf name="compile"/>
<conf name="runtime" extends="compile"/>
</configurations>
<publications>
<artifact name="tidepool-engine" type="jar" conf="compile,runtime"/>
</publications>
<dependencies>
<dependency org="io.tidepool" name="tidepool-model" rev="1.0.0" revConstraint="latest.integration" conf="compile->compile;runtime->runtime"/>
<dependency org="org.apache.commons" name="commons-csv" rev="1.14.1" conf="compile->default"/>
</dependencies>
</ivy-module>
This is what any other project sees when it depends on tidepool-engine;1.0.0: the model is pinned to 1.0.0, and the
original wish ("the latest integration build") is recorded for people.
Source: Ivy: deliver
Publishing the same module twice
Three things go wrong when a build is run a second time, and each produces an error that sends people to the manual.
A stale delivered file. publish delivers only if the delivered file does not exist yet. On the second publish-all, the
file from the first run is still in build/dist:
publish:
[ivy:publish] :: publishing :: io.tidepool#tidepool-model
BUILD FAILED
./build.xml:13: The following error occurred while executing this line:
./common.xml:35: impossible to publish artifacts for io.tidepool#tidepool-model;working@host: java.lang.IllegalArgumentException: cannot publish ./tidepool-model/build/dist/ivy.xml as 1.0.1: bad revision found in ivy file (Revision: 1.0.0). Use forcedeliver or update.
buildnumber proposed 1.0.1, the leftover file says 1.0.0. Either clean build/ first or set forcedeliver="true" (the
force property in common.xml, run as ant -Dforce=true publish-all), which delivers again every time. A tutorial build
file uses forcedeliver="true" in its local publish target for the same reason.
An existing revision. Publishing a revision that is already in the repository is refused, so that a published revision can be trusted not to change:
publish:
[ivy:publish] :: delivering :: io.tidepool#tidepool-model;working@host :: 1.0.2 :: integration :: Fri Oct 16 12:23:17 CEST 2026
[ivy:publish] delivering ivy file to ./tidepool-model/build/dist/ivy.xml
[ivy:publish] :: publishing :: io.tidepool#tidepool-model
BUILD FAILED
./common.xml:35: impossible to publish artifacts for io.tidepool#tidepool-model;working@host: java.io.IOException: file copy not done from ./tidepool-model/build/dist/tidepool-model.jar to ./build/repo/shared/io.tidepool/tidepool-model/1.0.2/tidepool-model-1.0.2.jar: destination already exists and overwrite is false
(That was ant -Dforce=true -Divy.new.revision=1.0.2 publish in tidepool-model after 1.0.2 had been published.) With
overwrite="true" (here -Doverwrite=true) the same command succeeds, and, as the output shows, the files go straight to
the final directory without the .part step:
[ivy:publish] :: publishing :: io.tidepool#tidepool-model
[ivy:publish] published tidepool-model to ./build/repo/shared/io.tidepool/tidepool-model/1.0.2/tidepool-model-1.0.2.jar
[ivy:publish] published ivy to ./build/repo/shared/io.tidepool/tidepool-model/1.0.2/ivy.xml
[echo] tidepool-model published as 1.0.2
Overwriting is meant for integration builds in a private repository. A consumer’s cache will not notice that a published
revision changed: it keeps the copy it downloaded unless the dependency is marked changing.
A stale dynamic revision. Ivy remembers what a dynamic revision resolved to for a time to live, by default
${ivy.cache.ttl.default}, which is 10s in the ivy.properties file inside the 2.6.0 JAR. A build that publishes the
model and then, seconds later, resolves latest.integration for the engine can therefore still see the old model. Running
publish-all right after another one:
$ grep tidepool- tidepool-engine/build/dist/ivy.xml tidepool-cli/build/dist/ivy.xml # after ant -Dforce=true publish-all
tidepool-engine/build/dist/ivy.xml: <dependency org="io.tidepool" name="tidepool-model" rev="1.0.0" revConstraint="latest.integration" ...
tidepool-cli/build/dist/ivy.xml: <dependency org="io.tidepool" name="tidepool-engine" rev="1.0.0" revConstraint="latest.integration" ...
The engine and the CLI published as 1.0.1 pin the model and the engine at 1.0.0, although 1.0.1 of each had just been
published, because their resolve reused the answer that was a second old. Running it again with the refresh switch
(resolve has refresh="true", which forces dynamic revisions to be recomputed) fixes it:
$ ant -Dforce=true -Drefresh=true publish-all
$ grep tidepool- tidepool-engine/build/dist/ivy.xml tidepool-cli/build/dist/ivy.xml
tidepool-engine/build/dist/ivy.xml: <dependency org="io.tidepool" name="tidepool-model" rev="1.0.2" revConstraint="latest.integration" ...
tidepool-cli/build/dist/ivy.xml: <dependency org="io.tidepool" name="tidepool-engine" rev="1.0.2" revConstraint="latest.integration" ...
A build server that publishes a whole chain should set refresh="true" on the resolve of the publishing build, or set a
ttl of 0ms for the organisation (the property ivy.cache.ttl.default in the settings does that for everything); the
ttl elements are described on
Ivy: Settings & Resolvers.
Several modules at once
buildlist
buildlist takes a fileset of build files, looks for an ivy.xml beside each one (the attribute ivyfilepath changes where),
reads the declared dependencies between them and returns an Ant path of the build files in dependency order: a module comes
after the modules it depends on, which is exactly the order a reactor-less build needs. It also sets ivy.sorted.modules, a
comma-separated list of the module names, which is handy in a log. With revisions in the info elements, the revisions are
taken into account (a dependency on a revision that does not match is reported and treated as a different module); with none,
as here, they are ignored.
| Attribute or element | Effect |
|---|---|
|
Required. The id of the path to create. |
|
Relative path from the build file to its descriptor; default |
|
Only the modules that the named module(s) depend on, directly or transitively (comma-separated names since 2.0); and whether to leave the roots out. |
|
Only the modules that depend on the named module(s) (since 1.4.1), and whether to leave them out. |
|
Restrict the result to direct dependencies of the roots, or direct dependents of the leaves. |
|
Most dependent module first. |
|
Start the list at the named module, followed by everything after it (since 2.0), for resuming a failed build. |
|
For build files with no descriptor: |
|
Stop on an invalid descriptor. Default |
nested |
Name modules with |
Circular dependencies are tolerated: the modules in a cycle are kept together and placed so that every outside dependency comes first. The task works on the descriptors only, without resolving anything.
The root build.xml, which every example uses to order and run the modules:
<project name="tidepool" default="publish-all" xmlns:ivy="antlib:org.apache.ivy.ant">
<property name="ivy.default.ivy.user.dir" location="build/ivy-user"/>
<ivy:settings file="ivysettings.xml"/>
<target name="build-list" description="order the modules by their dependencies">
<ivy:buildlist reference="build-path">
<fileset dir="." includes="tidepool-*/build.xml"/>
</ivy:buildlist>
<echo>build order: ${ivy.sorted.modules}</echo>
</target>
<target name="publish-all" depends="build-list" description="build and publish every module in order">
<subant target="publish" buildpathref="build-path" inheritall="false"/>
</target>
<target name="clean-all" depends="build-list">
<subant target="clean" buildpathref="build-path" inheritall="false"/>
</target>
</project>
$ ant -lib lib/ivy-2.6.0.jar build-list
Buildfile: ./build.xml
build-list:
[ivy:buildlist] :: Apache Ivy 2.6.0 - 20260712075753 :: https://ant.apache.org/ivy/ ::
[ivy:buildlist] :: loading settings :: file = ./ivysettings.xml
[echo] build order: io.tidepool#tidepool-model, io.tidepool#tidepool-engine, io.tidepool#tidepool-cli
The fileset delivers tidepool-cli, tidepool-engine, tidepool-model (alphabetical order); the descriptors turn that into
model, engine, CLI. The subant task then runs the publish target in each build file of the path, in that order, in a
separate Ant project each (inheritall="false" keeps the root’s properties out, so each module picks up its own). The same
list, with the options from the table, on the same three modules:
<ivy:buildlist reference="p.root" root="tidepool-engine"><fileset dir="." includes="tidepool-*/build.xml"/></ivy:buildlist>
<echo>root=engine : ${ivy.sorted.modules}</echo>
[echo] default : io.tidepool#tidepool-model, io.tidepool#tidepool-engine, io.tidepool#tidepool-cli
[echo] reverse : io.tidepool#tidepool-cli, io.tidepool#tidepool-engine, io.tidepool#tidepool-model
[echo] root=engine : io.tidepool#tidepool-model, io.tidepool#tidepool-engine
[echo] root=engine, excl. : io.tidepool#tidepool-model
[echo] leaf=model : io.tidepool#tidepool-model, io.tidepool#tidepool-engine, io.tidepool#tidepool-cli
[echo] leaf=model, direct : io.tidepool#tidepool-model, io.tidepool#tidepool-engine
[echo] restartFrom=engine : io.tidepool#tidepool-engine, io.tidepool#tidepool-cli
[echo] nested root=cli : io.tidepool#tidepool-model, io.tidepool#tidepool-engine, io.tidepool#tidepool-cli
In build terms: root answers "what must I build to build this module?", leaf answers "what must I rebuild when this
module changes?" (the model’s change reaches the engine and the CLI, and with onlydirectdep only the engine), reverse is
the order for a clean-up or an uninstall, restartFrom resumes a failed publish-all at the module that failed. The
ivy.sorted.modules property contains only modules that have a descriptor.
Build files without a descriptor. A directory with a build.xml but no ivy.xml (documentation, say) is placed according
to onMissingDescriptor. In a scratch directory with alpha, beta, gamma (alpha and beta depend on each other, gamma on
beta) and docs (no descriptor), the paths came out as:
[echo] head : docs, beta, alpha, gamma
[echo] tail : beta, alpha, gamma, docs
[echo] skip : beta, alpha, gamma
and with onMissingDescriptor="fail":
BUILD FAILED
./circular/build.xml:12: a module has no module descriptor and onMissingDescriptor=fail. Build file: ./circular/docs/build.xml. Expected descriptor: ./circular/docs/ivy.xml
The cycle alpha, beta stays together (beta, alpha), and gamma, which depends on it, comes after. The ordering tolerates a
cycle that resolution may not: whether a resolve of a circular dependency is allowed is decided by the
circularDependencyStrategy of the settings, covered on
Ivy: Conflicts & Versions.
Source: Ivy: buildlist
subant and import
Ordering is only half of a multi-module build; the other half is running something in each module. subant runs a target in
each build file of a path (or a fileset), once per file, in the order of the path; with genericantfile it runs the same
build file in each directory; failonerror="false" continues past a failing module. The multi-file chapter,
Multi-File Builds: import,
include & subant, describes import, include and subant in detail; the pattern used here (a common.xml imported by each
module, an ordered subant in the root) is the one of the Ivy tutorial, and the Maven and Gradle equivalents are the
reactor (see Comparing Ant, Maven & Gradle).
The root build declares its own ivy:settings, and each module’s common.xml declares one too: with
inheritall="false" every subant project loads its own instance. The manual warns that a settings instance
shared with a sub-build through a reference must be created by the same class loader, which is a risk when the Ivy antlib is
loaded by a typedef in each build; with -lib (as here) or Ivy in Ant’s lib directory there is a single class loader and
the problem does not arise.
Publishing the whole chain
The first ant publish-all, from an empty repository, built and published the model, then the engine, then the CLI, each
resolving what the previous one had just put in the repository. The engine’s resolve, as it appears in the log (the CLI’s is
alike):
resolve:
[ivy:resolve] :: loading settings :: file = ./ivysettings.xml
[ivy:resolve] downloading ./build/repo/shared/io.tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar ...
[ivy:resolve] .. (3kB)
[ivy:resolve] .. (0kB)
[ivy:resolve] [SUCCESSFUL ] io.tidepool#tidepool-model;1.0.0!tidepool-model.jar (2ms)
[ivy:resolve] downloading https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar ...
...
The "download" of a local file is the copy from the file repository into the cache. Run without the -Dforce=true switch the
second publish-all fails as shown earlier; run with it (and -Drefresh=true when the chain was published a few seconds
before) it produces 1.0.1 or 1.0.2 for all three. The result after three rounds (all using the same repository) is one
directory per module and revision under build/repo/shared/io.tidepool, with the CLI’s ivy.xml pointing at the engine
revision that was current when it was delivered.
Using what was published
A project that is not part of the build uses the published CLI like any other library. Its descriptor asks for the newest integration build:
<ivy-module version="2.0">
<info organisation="io.tidepool.demo" module="consumer"/>
<configurations>
<conf name="runtime"/>
</configurations>
<dependencies>
<dependency org="io.tidepool" name="tidepool-cli" rev="latest.integration" conf="runtime->runtime"/>
</dependencies>
</ivy-module>
with its own settings: a chain of the file repository and Maven Central, and its own empty cache:
<ivysettings>
<settings defaultResolver="shared"/>
<caches defaultCacheDir="${cache.dir}"/>
<resolvers>
<chain name="shared">
<filesystem name="local-repo">
<ivy pattern="${ivy.settings.dir}/../build/repo/shared/[organisation]/[module]/[revision]/ivy.xml"/>
<artifact pattern="${ivy.settings.dir}/../build/repo/shared/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]"/>
</filesystem>
<ibiblio name="central" m2compatible="true"/>
</chain>
</resolvers>
</ivysettings>
<project name="consumer" default="run" xmlns:ivy="antlib:org.apache.ivy.ant">
<property name="ivy.default.ivy.user.dir" location="../build/ivy-user"/>
<property name="cache.dir" location="build/consumer-cache"/>
<property name="data.file" location="../data/stations.csv"/>
<ivy:settings file="consumer-settings.xml"/>
<target name="resolve">
<ivy:resolve file="ivy.xml"/>
</target>
<target name="run" depends="resolve">
<ivy:cachepath pathid="run.path" conf="runtime"/>
<java classname="io.tidepool.cli.Main" classpathref="run.path" fork="true" failonerror="true">
<arg value="${data.file}"/>
<arg value="SF"/>
</java>
</target>
</project>
$ ant -lib lib/ivy-2.6.0.jar run
resolve:
[ivy:resolve] :: Apache Ivy 2.6.0 - 20260712075753 :: https://ant.apache.org/ivy/ ::
[ivy:resolve] :: loading settings :: file = ./consumer/consumer-settings.xml
[ivy:resolve] :: resolving dependencies :: io.tidepool.demo#consumer;working@host
[ivy:resolve] confs: [runtime]
[ivy:resolve] found io.tidepool#tidepool-cli;1.0.2 in local-repo
[ivy:resolve] [1.0.2] io.tidepool#tidepool-cli;latest.integration
[ivy:resolve] found io.tidepool#tidepool-engine;1.0.2 in local-repo
[ivy:resolve] found io.tidepool#tidepool-model;1.0.2 in local-repo
[ivy:resolve] found org.apache.commons#commons-csv;1.14.1 in central
[ivy:resolve] found commons-io#commons-io;2.20.0 in central
[ivy:resolve] found commons-codec#commons-codec;1.19.0 in central
[ivy:resolve] found info.picocli#picocli;4.7.7 in central
...
[ivy:resolve] :: resolution report :: resolve 1876ms :: artifacts dl 573ms
---------------------------------------------------------------------
| | modules || artifacts |
| conf | number| search|dwnlded|evicted|| number|dwnlded|
---------------------------------------------------------------------
| runtime | 7 | 7 | 7 | 0 || 7 | 7 |
---------------------------------------------------------------------
run:
[java] San Francisco: -0,869 m
Seven modules were resolved from two repositories: the three tidepool modules from the file repository and Commons CSV,
Commons IO, Commons Codec and picocli from Maven Central; the last three come in through Commons CSV’s own POM. The program
printed a tide height (the decimal comma is the formatting of the machine’s Spanish locale). The point of the run is the
found ... in local-repo lines: the engine and the model were not declared by the consumer, they came in transitively through
the published ivy.xml files, and at the revisions that were pinned when they were delivered.
Source: Ivy documentation: resolvers and Ivy: retrieve
Integration versions and statuses
The Ivy documentation distinguishes two ways of dealing with integration builds: publish them all under the same, special
revision (the "snapshot" convention, which the dependency must flag with changing), or give each a new revision, from a build
number or a timestamp, and let consumers ask for latest.integration. Ivy recommends the second way because a revision then
identifies exactly one build, which keeps even integration builds reproducible and lets an integration build be promoted to a
milestone or a release. This page does the second way.
A module has a status (integration, milestone, release by default), set in the descriptor and overridden by
publish/deliver. A dependency written as latest.<status> means "the newest revision whose status is at least that
good". With the repository holding 1.0.0, 1.0.1 and 1.0.2 (integration), 1.1.0 published with
-Dstatus=milestone and 1.1.1 published as an integration build:
<project name="statuses" default="all" xmlns:ivy="antlib:org.apache.ivy.ant">
<property name="ivy.default.ivy.user.dir" location="build/ivy-user"/>
<property name="ivy.cache.ttl.default" value="0ms"/>
<ivy:settings file="ivysettings.xml"/>
<macrodef name="latest">
<attribute name="constraint"/>
<sequential>
<ivy:findrevision organisation="io.tidepool" module="tidepool-model" revision="@{constraint}" property="found.@{constraint}"/>
<echo>@{constraint} -> ${found.@{constraint}}</echo>
</sequential>
</macrodef>
<target name="all">
<latest constraint="latest.integration"/>
<latest constraint="latest.milestone"/>
<latest constraint="latest.release"/>
<latest constraint="1.0.+"/>
<latest constraint="[1.0,1.1)"/>
</target>
</project>
[echo] latest.integration -> 1.1.1
[echo] latest.milestone -> 1.1.0
[echo] latest.release -> ${found.latest.release}
[echo] 1.0.+ -> 1.0.2
[echo] [1.0,1.1) -> 1.0.2
latest.integration takes the newest build whatever its status; latest.milestone skips 1.1.1 because its status is lower
than milestone; latest.release finds nothing, as no revision has status release, and the property is left unset (Ant
shows the unexpanded ${found.latest.release}). A latest.release dependency on a module that was never released therefore
fails to resolve rather than falling back to something less stable. The last two lines are a sub-revision and a range, from
the matchers described on Ivy: Conflicts &
Versions. The ivy.cache.ttl.default of 0ms in the build file is there because of the stale-answer behaviour shown earlier.
A milestone is published from the same publish target with a different status and revision:
$ ant -Dforce=true -Divy.new.revision=1.1.0 -Dstatus=milestone publish
publish:
[ivy:publish] :: delivering :: io.tidepool#tidepool-model;working@host :: 1.1.0 :: milestone :: Fri Oct 16 12:23:27 CEST 2026
...
[echo] tidepool-model published as 1.1.0
Promoting an integration build to a better status is not something Ivy does by itself: the best-practices page says that Ivy is a dependency manager, not a build tool, and that publishing only distinctly named versions, replacing constraints with static revisions at publication, and using recursive delivery is what gives reproducible builds. It also points out the cost of this approach, a lot of intermediate revisions that a cleaning script has to remove from the repository.
Looking into a repository
Two tasks work on the repository rather than on a module, and need a resolver that can list its contents. An Ivy-layout
file repository can; a Maven-layout one gives unreliable answers (see the note on listmodules on
Ivy Ant Tasks).
<project name="repo-tools" default="all" xmlns:ivy="antlib:org.apache.ivy.ant">
<property name="ivy.default.ivy.user.dir" location="build/ivy-user"/>
<ivy:settings file="ivysettings.xml"/>
<target name="listmodules">
<ivy:listmodules organisation="io.tidepool" module="tidepool-.*" revision="1\.0\..*" resolver="shared"
property="found.[module].[revision]" value="[organisation]"/>
<echoproperties prefix="found."/>
</target>
<target name="repreport">
<ivy:repreport organisation="io.tidepool" module="tidepool-.*" revision="1.0.+"
todir="build/repreport" dot="true" xml="true" graph="false"/>
<loadfile srcfile="build/repreport/ivy-repository-report.dot" property="dot"/>
<echo>${dot}</echo>
</target>
</project>
listmodules:
[echoproperties] found.tidepool-cli.1.0.0=io.tidepool
[echoproperties] found.tidepool-cli.1.0.1=io.tidepool
[echoproperties] found.tidepool-cli.1.0.2=io.tidepool
[echoproperties] found.tidepool-engine.1.0.0=io.tidepool
[echoproperties] found.tidepool-engine.1.0.1=io.tidepool
[echoproperties] found.tidepool-engine.1.0.2=io.tidepool
[echoproperties] found.tidepool-model.1.0.0=io.tidepool
[echoproperties] found.tidepool-model.1.0.1=io.tidepool
[echoproperties] found.tidepool-model.1.0.2=io.tidepool
listmodules found the nine 1.0.x revisions (the 1.1.x ones were excluded by the revision pattern) with the default
exactOrRegexp matcher; property and value are patterns built from [organisation], [module], [branch] and
[revision]. repreport resolves the matching modules and everything they depend on, Maven Central included, and writes a
report of the dependencies among them, as XML by default, GraphML (graph) or Graphviz DOT (dot). The DOT output for the
1.0.+ modules, after running the task:
/* * directed graph dot input file. * * generated by ivy report */
digraph G {"tidepool-cli""picocli""tidepool-engine""commons-csv""commons-codec""commons-io""tidepool-model""tidepool-cli" -> "picocli";"tidepool-cli" -> "tidepool-engine";"tidepool-engine" -> "commons-csv";"commons-csv" -> "commons-codec";"commons-csv" -> "commons-io";"tidepool-engine" -> "tidepool-model";}
(The whitespace is as Ivy wrote it; Graphviz does not mind.) The revision of repreport is a constraint, not a pattern:
my first attempt with the regular expression 1\.0\.2 was taken literally and reported the modules "not found".
Source: Ivy: listmodules
Source: Ivy: repreport
Maven interoperability: makepom
Maven cannot read an ivy.xml. To let a Maven (or Gradle) project depend on a module that Ivy publishes, publish a POM next
to the JAR in a Maven-layout repository. makepom converts a descriptor into a POM:
| Attribute or element | Effect |
|---|---|
|
Required. The descriptor to convert and the POM to write. |
|
Configurations to include (wildcards, since 2.2). Default all. |
|
The artifact the POM describes: its |
|
A POM template (since 2.2). Lines with |
|
The |
nested |
|
nested |
Extra dependencies to add: |
Not everything in an Ivy descriptor has a POM equivalent (the manual says so), and the conversion is therefore best done on
the delivered descriptor, where revisions are static. The module-level target, which delivers, converts and publishes the JAR
and the POM to the m2 resolver:
<target name="publish-m2" depends="jar" description="publish a POM and the JAR in Maven layout">
<property name="m2.revision" value="1.0.2"/>
<ivy:deliver deliverpattern="${dist.dir}/ivy-[revision].xml" pubrevision="${m2.revision}" status="release"/>
<ivy:resolve file="${dist.dir}/ivy-${m2.revision}.xml" log="quiet"/>
<ivy:makepom ivyfile="${dist.dir}/ivy-${m2.revision}.xml" pomfile="${dist.dir}/${ivy.module}.pom"
description="Tidepool ${ivy.module}">
<mapping conf="compile" scope="compile"/>
<mapping conf="runtime" scope="runtime"/>
</ivy:makepom>
<ivy:publish resolver="m2" pubrevision="${m2.revision}" publishivy="false"
artifactspattern="${dist.dir}/[artifact].[ext]">
<artifact name="${ivy.module}" ext="pom" type="pom"/>
</ivy:publish>
</target>
Three points about this target. The ivy:resolve of the delivered file is not decoration: without it publish does not know
the module at the new revision and fails, with publishivy="false", either with Ivy file not found in cache for ...;1.0.2!
(when the revision attribute is given) or by trying to move a directory called working@host. publishivy="false" is what
keeps the Ivy descriptor out of the Maven repository, where the POM takes its place at
[artifact]-[revision].pom; the nested artifact adds the POM to the list of files to publish. And status="release" in
the delivery is a statement about the publication, not a check that the dependencies are releases.
Run for the three modules from the root (the target publish-m2-all is a subant of publish-m2 over the ordered path, with
-Dm2.revision=1.0.2):
publish-m2:
[ivy:deliver] :: delivering :: io.tidepool#tidepool-model;working@host :: 1.0.2 :: release :: Fri Oct 16 12:22:12 CEST 2026
[ivy:deliver] delivering ivy file to ./tidepool-model/build/dist/ivy-1.0.2.xml
[ivy:publish] :: publishing :: io.tidepool#tidepool-model
[ivy:publish] published tidepool-model to ./build/repo/m2/io/tidepool/tidepool-model/1.0.2.part/tidepool-model-1.0.2.jar
[ivy:publish] published tidepool-model to ./build/repo/m2/io/tidepool/tidepool-model/1.0.2.part/tidepool-model-1.0.2.pom
[ivy:publish] publish committed: moved ./build/repo/m2/io/tidepool/tidepool-model/1.0.2.part
[ivy:publish] to ./build/repo/m2/io/tidepool/tidepool-model/1.0.2
The repository build/repo/m2 then holds io/tidepool/<module>/1.0.2/<module>-1.0.2.jar and .pom (plus checksums) for the
three modules: the group io.tidepool became directories because the resolver is m2compatible. The generated POM of the
engine:
<?xml version="1.0" encoding="UTF-8"?>
<!--
Apache Maven 2 POM generated by Apache Ivy
https://ant.apache.org/ivy/
Apache Ivy version: 2.6.0 20260712075753
-->
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
<packaging>jar</packaging>
<version>1.0.2</version>
<description>Tidepool tidepool-engine</description>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
Both the Ivy dependency on the model (mapped from compile->compile;runtime->runtime) and the one on Commons CSV became
compile dependencies, because the engine’s compile configuration is where both are declared and the engine’s runtime
adds none of its own. Information that exists in an ivy.xml but not in a POM (a configuration with no scope, revConstraint,
conflict managers) is lost, which is why the POM "may not resolve to the exact same dependencies".
Does Maven accept it? A Maven project outside Ivy’s world declares the file repository and the CLI:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool.demo</groupId>
<artifactId>maven-consumer</artifactId>
<version>1.0.0</version>
<repositories>
<repository>
<id>ivy-published</id>
<url>file://${project.basedir}/../build/repo/m2</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-cli</artifactId>
<version>1.0.2</version>
</dependency>
</dependencies>
</project>
$ mvn -B -Dmaven.repo.local=./repo dependency:tree
...
[INFO] --- dependency:3.11.0:tree (default-cli) @ maven-consumer ---
[INFO] io.tidepool.demo:maven-consumer:jar:1.0.0
[INFO] \- io.tidepool:tidepool-cli:jar:1.0.2:compile
[INFO] +- io.tidepool:tidepool-engine:jar:1.0.2:compile
[INFO] | +- io.tidepool:tidepool-model:jar:1.0.2: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] \- info.picocli:picocli:jar:4.7.7:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
Maven 3.10.0 resolved the whole chain, the three modules from the Ivy-published repository and the rest from Maven Central
through the POMs. (The download and "cached not-found" warning lines that Maven printed on its first run against an empty local
repository were left out.) An earlier attempt, made when the CLI’s POM named an engine revision that had never been published
in the Maven repository, printed The POM for io.tidepool:tidepool-engine:jar:1.0.1 is missing, no dependency information
available and dropped the transitive dependencies without failing the build: a POM that points to something absent
degrades silently, so publish the whole chain to the Maven repository at consistent revisions, as publish-m2-all does.
Going the other way, from a POM to a descriptor, is convertpom, described on
Ivy Ant Tasks. If your goal is to publish Maven-style
artifacts from Ant and nothing more, the
Maven Resolver Ant Tasks have install and
deploy that write a POM themselves, with no descriptor conversion.
Source: Ivy: makepom
Publishing in the other tools
| Idea | Ivy | Maven | Gradle |
|---|---|---|---|
Revision of a build |
|
|
|
Resolved metadata |
|
the POM as written, with versions normally fixed in it |
the generated POM and module metadata; |
Upload |
|
|
|
Several modules |
|
the reactor orders modules by their dependencies |
multi-project build, ordered by task dependencies |
Maven POM from the native metadata |
|
native |
the |
The details of each column are on the Maven and Gradle pages of this section; the shared concepts (a repository as a directory tree, checksums, promotion) are on Repositories & Publishing Across Tools.
Where older material is outdated
-
The Ivy tutorials publish into
${ivy.local.default.root}and${ivy.shared.default.root}directories configured through Ivy’s default settings (alocaland asharedresolver in a~/.ivy2layout). The default settings of 2.6.0 work that way; the examples here use explicit settings, which is what a real build should do. -
Tutorials use the class
latest.integrationfor everything. Do so only with a repository you control and a cachettlyou understand, as shown above. -
Older articles recommend
ivy:installto "publish" a module from one repository to another; it is a copy of a resolved module with its dependencies (see Ivy Ant Tasks), not a publication of your build. -
Examples that publish to a repository over
sftporsshuse credentials in the settings; such publication was not run here, and a modern setup uses a repository manager over HTTPS with a token from the environment, never a literal password. The same applies to Maven Central:makepomproduces a POM that is not enough for Central (no licence, SCM, developers, signatures); publishing there is not run here and is covered for Maven on Repositories & Publishing Across Tools.
Common problems
-
bad revision found in ivy file ... Use forcedeliver or update: a delivered file from an earlier build is still in the output directory. Clean, or useforcedeliver="true", orupdate="true"to refresh only revision, status and date. -
destination already exists and overwrite is false: the revision is already published. Compute a new revision or, for integration builds only, useoverwrite="true". -
A module pins an old dependency right after publishing it: the dynamic-revision TTL, see the section above.
-
The order from
buildlistis not what you expect: the order comes from theivy.xmlfiles only; a dependency declared only in thebuild.xmlis invisible. Checkivy.sorted.modules. -
publishfails withIvy file not found in cache ...whenpublishivy="false": resolve the delivered file first, as thepublish-m2target does. -
Maven shows no transitive dependencies of an Ivy-published module: the POM is missing for some revision; see the Maven check above.
References
-
Apache Ivy documentation: deliver
-
Apache Ivy documentation: publish
-
Apache Ivy documentation: buildlist
-
Apache Ivy documentation: buildnumber
-
Apache Ivy documentation: makepom
-
Apache Ivy documentation: listmodules
-
Apache Ivy documentation: repreport
-
Apache Ivy documentation: findrevision
-
Apache Ivy documentation: Tutorial: using Ivy in multiple projects environment
-
Apache Ivy documentation: Best practices
-
Apache Ivy documentation: ttl
-
Apache Ant manual: subant
-
Apache Maven: Introduction to the POM