Publishing to Maven Repos with Ivy
Publishing to Maven repositories is a fact of life for projects in the Java community.
While I am not a fan of Maven itself, I readily admit that the standard shared repository/transitive-metadata approach they have driven has been a great thing for the community. While I was initially skeptical, downloading dependencies and keeping them out of source code repos is a good thing.
Using Ivy
That being said, I prefer to leverage Maven repositories from a safe, arms length distance via the excellent Ivy (and IvyDE) tool which, besides other features, can understand the Maven repository format both as a source for dependencies and destination for publishing projects.
While Ivy-to-Maven works well, it did take me a few tries to get it right, so here are the various settings/files that so far are doing the job.
Update 12/05/2013: I don’t know why I did this, but the example ivysettings.xml
, ivy.xml
, and build.xml
files below are actually from two separate projects of mine (Joist and Tessell). So, sorry for that being confusing, but they should still work. Note that you can check out either the Joist or Tessell builds for the latest Ivy incantations, in case these have drifted.
ivysettings.xml
This is the ivysettings.xml to Joist, with comments added.
<ivysettings>
<!--
This property is used later in the ivy.xml file to set
the project's revision. Unless overridden, it defaults
to the Maven SNAPSHOT convention, as that it works well
for publishing local test builds to ~/.m2/repository.
-->
<property name="revision" value="SNAPSHOT" override="false"/>
<!-- "default" is defined later in the file. -->
<settings defaultResolver="default"/>
<!-- Pulls in the "public" resolver for ibiblio-hosted jars. -->
<include url="${ivy.default.settings.dir}/ivysettings-public.xml"/>
<resolvers>
<!-- add any 3rd-party maven repos here... -->
<ibiblio name="joist" m2compatible="true" root="http://repo.joist.ws"/>
<!--
for *retrieving* artifacts for local testing builds,
we'll use maven's own .m2/repository.
-->
<ibiblio
name="local-m2"
m2compatible="true"
root="file://${user.home}/.m2/repository"/
changingPattern=".*SNAPSHOT">
<!--
for *publishing* artifacts for local testing builds,
as the previous ibiblio resolver does not support
publishing
-->
<filesystem name="local-m2-publish" m2compatible="true">
<artifact pattern="${user.home}/.m2/repository/[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]"/>
</filesystem>
<!--
for publishing release artifacts via an sshfs-mounted share
-->
<filesystem name="share-m2" m2compatible="true">
<artifact pattern="${user.home}/repo/[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]"/>
<filesystem>
<!-- strings the separate resolvers all together -->
<chain name="default" changingPattern=".*SNAPSHOT">
<resolver ref="public"/>
<resolver ref="joist"/>
<!--
Potential gotcha: you want your local-m2 to be last, or at least
after the public repo. Otherwise maven proper might partially
download a project into ~/.m2, but not include sources, and if
local-m2 is first, ivy will pull the artifacts out of ~/.m2 and
cache the partially-downloaded view of the repo (i.e. no sources).
If public is first, ivy will always try to get artifacts from their
canonical source.
-->
<resolver ref="local-m2"/>
</chain>
</resolvers>
</ivysettings>
The primary gotcha in this ivysettings.xml
was having to use two separate resolvers for the same ~/.m2/repository
. This is because:
-
Only the
ibiblio
resolver will parse poms and follow the pom’s transitive dependencies.(Technically the
filesystem
orurl
resolvers could retrieve jars with a Maven-ish artifact pattern, but the pom dependencies would be skipped.) -
The
ibiblio
resolver does not support publishing, so we have to fall back to thefilesystem
resolver with a Maven-ish artifact pattern to publish.
So, with the current Ivy 2.1.0 capabilities, it takes two resolvers for a single Maven repo if you want to both retrieve from and publish to it.
ivy.xml
This is the Tessell ivy.xml
, again with comments.
<ivy-module version="2.0" xmlns:m="http://ant.apache.org/ivy/maven">
<!--
We set the revision to the revision property from
ivysettings.xml, which defaults to SNAPSHOT. This
is overriden when publishing.
-->
<info organisation="org.tessell" module="tessell-dev" revision="${revision}"/>
<!--
I'm not a huge fan of a separate sources conf,
but that is how Ivy's ibiblio resolver converts
poms, so we'll stay consistent with that.
-->
<configurations>
<conf name="default"/>
<conf name="sources"/>
</configurations>
<publications>
<!--
We explicitly list a pom as an artifact of our
project. This way the Ivy publish task will
upload the pom to the maven repo, along with
the jars and sources.
-->
<artifact type="pom" ext="pom" conf="default"/>
<!--
This is the main jar, nothing special.
-->
<artifact type="jar" ext="jar" conf="default"/>
<!--
To publish sources to a maven repo, the
m:classifier="sources" is required.
-->
<artifact type="source" ext="jar" conf="sources" m:classifier="sources"/>
</publications>
<!--
defaultconf == we want the jars + sources for our dependencies
defaultconfmapping == unless specified otherwise, our confs map
to default for our dependencies
-->
<dependencies defaultconfmapping="sources->sources();%->default" defaultconf="default;sources">
<!--
tessell-user is published simultaneously with
tessell-dev, so depend on the same exact revision.
-->
<dependency org="org.tessell" name="tessell-user" rev="${revision}" conf="default"/>
<!-- other dependencies -->
<dependency org="com.google.gwt" name="gwt-dev" rev="2.1.0.M1" conf="default"/>
</dependencies>
</ivy-module>
build.xml
Finally, here is the Ivy-related part of the Tessell build.xml
:
<property name="ivy.jar.version" value="2.1.0"/>
<property name="ivy.jar.name" value="ivy-${ivy.jar.version}.jar"/>
<property name="ivy.home" value="${user.home}/.ivy2"/>
<available property="ivy.installed" file="${ivy.home}/${ivy.jar.name}"/>
<!--
this is called once and auto-installs the ivy jar into
~/.ivy2 so that users only have to have ant to build.
-->
<target name="ivy-install" unless="ivy.installed">
<mkdir dir="${ivy.home}"/>
<get src="http://repo1.maven.org/maven2/org/apache/ivy/ivy/${ivy.jar.version}/${ivy.jar.name}" dest="${ivy.home}/${ivy.jar.name}"/>
</target>
<!--
this is called automatically and just inits ivy
-->
<target name="ivy-init" depends="ivy-install">
<taskdef resource="org/apache/ivy/ant/antlib.xml" uri="antlib:org.apache.ivy.ant" classpath="${ivy.home}/${ivy.jar.name}"/>
<ivy:resolve/>
</target>
<!--
called by the user to download jars into bin/lib/
-->
<target name="ivy-retrieve" depends="ivy-init" description="downloads jars for the project">
<ivy:retrieve pattern="bin/lib/[conf]/[type]s/[artifact].[ext]" conf="*" type="*"/>
</target>
<!--
makes a pom for the project based off the ivy.xml file
-->
<target name="gen-pom" depends="ivy-init">
<ivy:makepom ivyfile="ivy.xml" pomfile="bin/poms/${ant.project.name}.pom">
<!--
Mapping confs to scopes is important, otherwise
unmapped confs are included as optional. If you
have private confs, the best option seems to
be marking them as provided or system. See
IVY-1201 for an ehancement request.
-->
<mapping conf="default" scope="compile"/>
</ivy:makepom>
</target>
<!--
publishes to ~/.m2/repository so that other testing
builds on our local machine can see it
-->
<target name="ivy-publish-local" depends="jar,ivy-init,gen-pom" description="publish jar/source to maven repo mounted at ~/.m2/repository">
<ivy:publish resolver="local-m2-publish" forcedeliver="true" overwrite="true" publishivy="false">
<artifacts pattern="bin/[type]s/[artifact].[ext]"/>
</ivy:publish>
</target>
<!--
pubishes to the ~/repo directory, which should be something
like an sshfs-mount of the public maven repository you are
publishing to
-->
<target name="ivy-publish-share" depends="jar,ivy-init,gen-pom" description="publish jar/source to maven repo mounted at ~/repo">
<ivy:publish resolver="share-m2" forcedeliver="true" overwrite="true" publishivy="false">
<artifacts pattern="bin/[type]s/[artifact].[ext]" />
</ivy:publish>
</target>
Making Poms
The build.xml
file above uses Ivy’s makepom
Ant task to convert our project’s ivy.xml
into a project.pom
before uploading it to the Maven repository.
This is generally acceptable, insofar as Ivy will get all of the dependencies translated correctly, but you’ll end up with a very minimal pom.
So if you want to add more elements to the pom, it is probably easiest to just hand-maintain a project.pom.template
and use a simple Ant filter to update the version on each build before being uploaded.
For example, tessell-dev-2.13.4.pom was built automatically by makepom
, while tessell-apt-2.6.3.pom was built from a hand-maintained template.
Local Testing Builds
So far I have not published SNAPSHOT
versions publicly, but they are very handy for sharing in-development changes between projects on your local machine. Just doing:
ant ivy-publish-local
Will publish a jar to ~/.m2
for other local projects to pull in your latest/uncommitted changes.
Since we’re going through the local-m2
<ibiblio/>
resolver, Ivy automatically handles latest-SNAPSHOT
checking and we don’t have to bother with any ~/.ivy2/cache
-busting tricks like I talked about in Ivy Is Useful.
Though if you’re using IvyDE and workspace resolution of dependencies, Eclipse should setup all of the cross-project references correctly and you won’t have to constantly publish SNAPSHOT
jars just for local consumption each time you make a change.
Doing Releases
With the above setup, I can now publish Tessell to the joist repo via:
ant -Drevision=x.y ivy-publish-share
It Works
While this looks like a lot of code, my inline comments added a lot—it is generally ~50 lines of ivysettings.xml
and ivy.xml
per-project and then ~50 lines of straight-forward build.xml
Ant code.
While this is more than, say, 10 LOC, it hasn’t been burdensome enough for me to investigate anything else, e.g. Buildr, or even near burdensome enough to consider using Maven itself.
I should probably throw the common files into a shared git repository and then git submodule them around instead of being reduced to copy/paste. But I haven’t gotten around to it yet, and it seems like each project always has some tweak here or there that would be annoying to generalize.
Nonetheless, this has been working well for me. If I made any errors or omissions, please let me know.
Update 08/03/2010: Add gotcha about the local-m2
resolver being chained after public
resolvers.
Update 01/13/2011: Add changingPattern="true"
to local-m2
and default
resolvers so that snapshots work without having Ant manually flushing the cache.
Update 12/05/2013: Fixed old “gwt-mpv” links to Tessell, as the project got renamed.