Coming Up for Air

Dependency Management with Ant and Ivy

One of my long-standing complaints with Ant is that project dependency management is non-existent in the core Ant distribution. Many will quickly point to the Maven Ant tasks, but I’ve never been really fond of them for one reason or another. The other advice I often get is to use Ivy, but even after several attempts, I had never gotten Ivy to work. With the recent release of 2.0 beta 1, though, I thought I’d try again, and I’m glad I did. Not only have I gotten it to work for me, but I was also able to successfully configure custom resolvers. Below is what I had to do to migrate the Mojarra Scales dependency management to Ivy.

Using Ivy requires, at least in the way I’m using it, two new files, ivy.xml, which defines my dependencies, and ivysettings.xml, which configures my custom resolvers. The usage from my Ant build file is actually pretty simple, so we’ll start there:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<target name="-download-ivy" unless="skip.download">
    <!-- download Ivy from web site so that it can be
        used even without any special installation -->
    <echo message="installing ivy..."/>
    <get src="http://repo1.maven.org/maven2/org/apache/ivy/ivy/${ivy.install.version}/ivy-${ivy.install.version}.jar"
         dest="${ivy.jar.file}" usetimestamp="true"/>
</target>
<target name="-install-ivy" depends="-download-ivy"
    description="--> install ivy">
    <path id="ivy.lib.path">
        <fileset dir="${lib.build.dir}" includes="*.jar"/>
    </path>
    <taskdef resource="org/apache/ivy/ant/antlib.xml"
        uri="antlib:org.apache.ivy.ant" classpathref="ivy.lib.path"/>
</target>
<target name="update" depends="prepare,-install-ivy"
    description="Download project dependencies">
    <!-- edited for brevity -->
    <ivy:settings file="ivysettings.xml" />
    <ivy:retrieve pattern="lib/[conf]/[artifact]-[revision].[ext]" />
    <!-- edited for brevity -->
</target>

You will also need to define these properties somewhere:

ivy.install.version=2.0.0-beta1
ivy.jar.file=${lib.build.dir}/ivy.jar

But what’s going on here? The -download-ivy target, simply downloads the Ivy jar and installs it in the target directory, lib/build in Scales' case. Next, -install-ivy tells Ant how to find the Ivy tasks. Finally, in the update target, we tell Ivy where to find the settings (which we’ll look at in a moment), and then we tell Ivy to download the dependencies.

But what’s that pattern all about? This, I think, is a pretty interesting aspect of Ivy. Unlike Maven (and the related Ant tasks), the files aren’t just downloaded into a local repo and left, exposed to the build system through some sort of path or fileset reference. Ivy does download and cache the artifacts, but it also copies those to a directory in one’s project (at least, that’s how I have it set up ;). The pattern tells Ivy to copy the files a directory named after the configuration (more on that in a moment) under the lib/ directory, using an artifact-version.ext file name structure. It is then up to me to create a classpath or fileset reference from those files, and I think I like it that way. So, what’s this "configuration" I mentioned? For that, let’s turn our eyes to ivy.xml.

The ivy.xml for Scales looks like this at the moment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<ivy-module version="1.0">
    <info organisation="org" module="standalone" revision="working"/>
    <configurations defaultconfmapping="runtime->*">
        <conf name="build"/>
        <conf name="javascript"/>
        <conf name="applet" />
        <conf name="compile" extends="applet" />
        <conf name="test" extends="compile"/>
    </configurations>
    <dependencies>
        <dependency org="com.sun.wts.tools.mri" name="maven-repository-importer" rev="1.2" conf="build->*>"/>
        <dependency org="commons-httpclient" name="commons-httpclient" rev="3.1" conf="applet->*"/>
        <dependency org="commons-fileupload" name="commons-fileupload" rev="1.1.1" conf="applet->*"/>
        <dependency org="commons-codec" name="commons-codec" rev="1.3" conf="applet->*"/>
        <dependency org="commons-io" name="commons-io" rev="1.2" conf="applet->*"/>
        <dependency org="commons-logging" name="commons-logging" rev="1.1" conf="compile->*"/>
        <dependency org="commons-lang" name="commons-lang" rev="2.3" conf="compile->*"/>
        <dependency org="commons-collections" name="commons-collections" rev="3.1" conf="compile->*"/>
        <dependency org="com.sun.facelets" name="jsf-facelets" rev="1.1.14" conf="compile->*"/>
        <dependency org="com.sun.jsftemplating" name="jsftemplating" rev="1.2-SNAPSHOT" conf="compile->*"/>
        <dependency org="com.sun.jsftemplating" name="jsftemplating-dynafaces-0.1" rev="1.2-SNAPSHOT" conf="compile->*"/>
        <dependency org="javax.faces" name="jsf-api" rev="1.2_07" conf="compile->*"/>
        <dependency org="javax.servlet.jsp" name="jsp-api" rev="2.1" conf="compile->*"/>
        <dependency org="javax.el" name="el-api" rev="1.0" conf="compile->*"/>
        <dependency org="taglibrarydoc" name="tlddoc" rev="1.3" conf="compile->*"/>
        <dependency org="velocity" name="velocity" rev="1.4" conf="compile->*"/>
        <dependency org="findbugs" name="findbugs" rev="1.0.0" conf="test->*"/>
        <dependency org="findbugs" name="findbugs-ant" rev="1.0.0" conf="test->*"/>
        <dependency org="yui" name="yui" rev="2.4.1" conf="javascript->*"/>
    </dependencies>
</ivy-module>

Let’s take that one section at a time. The first element we see is info, and I’ll be honest here: I’m really not sure what that’s for at the moment. I think it may be related to publishing (or installing, in Maven parlance) project-specific artifacts to a repository somewhere, but don’t quote me on that. The next section is where we define "configurations," which seem to be roughly analagous to Maven’s scope’s, but completely configurable and arbitrary. I have defined five configurations:

  • build: Jars need only for the build environment

  • javascript: This is an unusual one, but I use this only for the YUI files (currently) to make extracting the files easier later in the build process

  • applet: Jars which need to be bundled for use with the applet

  • compile: Jars needed only for compilation, API classes, for example

  • test: Jars needed for testing

Take note of the extends attribute. It works just like you probably think it does: since compile extends applet, it will get all of the artifacts defined for it, as well as those defined for applet. Given how Ivy (is configured to) handle dependencies, that means you’ll get two copies of some of those artifacts, but I’m OK with that.

Next in the file is the list of dependencies, which is pretty straightforward. The org maps to Maven’s groupId, the name maps to artifactId, rev maps to version, and conf maps to scope in Maven terms. Please don’t ask what that odd "→*" is there for, as I don’t understand that fully yet. All I know is that it won’t work without it. :)

Next up is ivysettings.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<ivysettings>
    <settings defaultResolver="chained"/>
    <property name="java.net.maven.pattern" value="[organisation]/jars/[module]-[revision].[ext]"/>
    <resolvers>
        <chain name="chained" returnFirst="true">
            <ibiblio name="ibiblio" m2compatible="true"/>
            <ibiblio name="java-net-maven1" root="http://download.java.net/maven/1" pattern="${java.net.maven.pattern}" m2compatible="false"/>
            <ibiblio name="java-net-maven2" root="http://download.java.net/maven/2/" m2compatible="true"/>
            <url name="sourceforge">
                <artifact pattern="http://easynews.dl.sourceforge.net/sourceforge/[organization]/[module]_[revision].zip" />
                <artifact pattern="http://easynews.dl.sourceforge.net/sourceforge/[organization]/[module]-[revision].zip" />
            </url>
        </chain>
    </resolvers>
</ivysettings>

One of the great things about Ivy 2 is that is configured to look at Maven repos by default. For Scales, though, I need some artifacts from the java.net Maven repository. To enable that, I have to define a custom resolver, which we see in the resolvers/chain elements. The chain resolver instructs Ivy to try one resolver after another to find the current artifact, with the returnFirst attribute telling Ivy to bail out as soon as a resolver that locates the artifact. Inside the chain, I instruct Ivy to use the ibiblio repository first. I then configure both the Maven 1 and Maven 2 java.net repositories. To do that, I configure additional instances of the ibilio resolver, but I change the root URL for the repository. In the case of the Maven 1 repository, I describe the pattern needed to find the artifact, as well as telling the resolver that the repository is not Maven 2 compatible. Finally, I create a URL repository, called "sourceforge," which I will use to resolve my YUI dependency.

With all of that in place, I can issue an ant update from the command line, and sit back and watch Ivy checking the configured repositories for my dependencies. It may seem like a lot to configure for dependencies, but Ivy is certainly better than the homegrown dependency management schemes I’ve seen (and devised), and is certainly less intrusive than migrating wholesale to Maven. While I <i>am</i> coming around on Maven 2, this will be a great tool for those projects that I can’t (or won’t) migrate.

tags: Ant Scales

Quotes

Sample quote

Quote source