Download dependencies with Ant and Ivy

In a previous article I stepped through development of an Ant script to build a target JAR file and run unit tests against it. Where we left off, the project depended on the developer manually populating a library directory with JUnit and its dependency. We will now eliminate that manual step by using Apache Ivy.

Ivy is a dependency manager which is produced by the same folks who maintain Ant. With relatively small additions to our project, Ivy can manage those dependencies for us. In this article I will show how to update the Ant build.xml file from the previous article to download those dependencies automatically, even downloading Ivy itself.

The first addition to our project is an ivy.xml file to declare our minimal dependencies:

<ivy-module version="2.0">
    <info organisation="" module="" />
    <configurations>
        <conf name="binaries" />
    </configurations>
    <dependencies>
        <dependency org="junit" name="junit" rev="4.12" conf="binaries->default" />
    </dependencies>
</ivy-module>

This essentially says “I need the binaries for JUnit 4.12, as well as anything it needs.” It is the simplest ivy.xml file that we can use. Ivy can also install the sources of those dependencies for you, to use for documentation or debugging. Examples of that will be easy to find after you’ve grokked this simpler example and have it working in your project.

Besides creating ivy.xml (above), build.xml needs to be changed to download Ivy and the dependencies specified in ivy.xml when missing or out of date. The more interesting aspects of this are:

  • Define properties which specify the Ivy version and where it is installed.
  • Add or change build targets to represent the new steps in building the project from scratch.
  • Download Ivy itself.

These properties are added to set the desired Ivy version and install location:

<!-- Ivy-related settings -->
<property name="ivy.install.version" value="2.4.0" />
<condition property="ivy.home" value="${env.IVY_HOME}">
    <isset property="env.IVY_HOME" />
</condition>
<property name="ivy.home" value="${user.home}/.ant" />
<property name="ivy.jar.dir" value="${ivy.home}/lib" />
<property name="ivy.jar.file" value="${ivy.jar.dir}/ivy.jar" />

Here is an outline of the new targets and dependency changes:

<!-- the existing "build" target has a new dependency -->
<target name="build" depends="get-dependencies">
</target>

<!-- new target which cleans the Ivy cache as well as existing build objects -->
<target name="clean-all" depends="clean" description="clean ivy cache">
</target>

<!-- new target to get dependencies, after first obtaining Ivy -->
<target name="get-dependencies" depends="init-ivy">
</target>

<!-- Try to load Ivy from either the local lib dir or Ant's lib dir -->
<target name="init-ivy" depends="download-ivy">
</target>

<!-- new target to download Ivy if necessary, considering desired
     version, file timestamp, and whether the 'net can be reached -->
<target name="download-ivy" unless="offline">
</target>

Our initial clean target only deleted the build directory. Now it also deletes the lib directory:

<target name="clean">
    <delete dir="${build.dir}" />
    <delete dir="${lib.dir}" />
</target>

Here’s the new clean-all target:

<target name="clean-all" depends="clean" description="clean ivy cache">
    <ivy:cleancache />
</target>

The get-dependencies, init-ivy, and download-ivy targets are new:

<target name="get-dependencies" depends="init-ivy">
    <ivy:retrieve conf="binaries" pattern="lib/[conf]/[artifact](-[classifier]).[ext]" />
</target>

<target name="init-ivy" depends="download-ivy">
    <path id="ivy.lib.path">
        <fileset dir="${ivy.jar.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="download-ivy" unless="offline">
    <mkdir dir="${ivy.jar.dir}" />
    <get src="https://repo1.maven.org/maven2/org/apache/ivy/ivy/${ivy.install.version}/ivy-${ivy.install.version}.jar"
         dest="${ivy.jar.file}" usetimestamp="true" />
</target>

One final tweak: The ivy namespace, used in the get-dependencies and clean-all targets, needs to be declared for the project:

<project xmlns:ivy="antlib:org.apache.ivy.ant" default="test">
</project>

As a recap, here’s the final build.xml, built step by step across these two blog posts:

<project xmlns:ivy="antlib:org.apache.ivy.ant" default="test">
    <!-- input directories and files -->
    <property name="app.src.dir" value="sources" />
    <property name="tests.src.dir" value="tests" />
    <property name="lib.dir" value="lib" />

    <!-- output directories and files -->
    <property name="build.dir" value="build" />
    <property name="app.classes.dir" value="${build.dir}/application/classes" />
    <property name="app.jar.dir" value="${build.dir}/application/jar" />
    <property name="app.jar.name" value="Trx.jar" />
    <property name="app.jar.pathname" value="${app.jar.dir}/${app.jar.name}" />
    <property name="app.jar.main-class" value="org.emptyhammock.trx.TrxEdit" />
    <property name="tests.classes.dir" value="${build.dir}/tests/classes" />
    <property name="install.dir" value="${user.home}/bin" />

    <!-- Ivy-related settings -->
    <property name="ivy.install.version" value="2.4.0" />
    <condition property="ivy.home" value="${env.IVY_HOME}">
        <isset property="env.IVY_HOME" />
    </condition>
    <property name="ivy.home" value="${user.home}/.ant" />
    <property name="ivy.jar.dir" value="${ivy.home}/lib" />
    <property name="ivy.jar.file" value="${ivy.jar.dir}/ivy.jar" />

    <presetdef name="project.javac">
        <javac debug="on" includeantruntime="false" />
    </presetdef>

    <target name="build" depends="get-dependencies">
        <mkdir dir="${app.classes.dir}" />
        <mkdir dir="${app.jar.dir}" />
        <mkdir dir="${tests.classes.dir}" />

        <project.javac srcdir="${app.src.dir}" destdir="${app.classes.dir}" />
        <jar destfile="${app.jar.pathname}" basedir="${app.classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="${app.jar.main-class}" />
            </manifest>
        </jar>

        <project.javac srcdir="${tests.src.dir}" destdir="${tests.classes.dir}">
            <classpath>
                <fileset dir="${lib.dir}" includes="**/*.jar" />
                <path location="${app.jar.pathname}" />
            </classpath>
        </project.javac>
    </target>

    <target name="clean">
        <delete dir="${build.dir}" />
        <delete dir="${lib.dir}" />
    </target>

    <target name="clean-all" depends="clean" description="clean ivy cache">
        <ivy:cleancache />
    </target>

    <target name="test" depends="build">
        <junit printsummary="on" haltonfailure="yes" fork="true">
            <classpath>
                <fileset dir="${lib.dir}" includes="**/*.jar" />
                <path location="${app.jar.pathname}"/>
                <pathelement location="${tests.classes.dir}" />
            </classpath>
            <formatter type="brief" usefile="false" />
            <batchtest>
                <fileset dir="${tests.src.dir}" includes="**/*Test.java" />
            </batchtest>
        </junit>
    </target>

    <target name="install" depends="build">
        <copy file="${app.jar.pathname}" todir="${install.dir}" />
    </target>

    <target name="get-dependencies" depends="init-ivy">
        <ivy:retrieve conf="binaries" pattern="lib/[conf]/[artifact](-[classifier]).[ext]" />
    </target>

    <target name="init-ivy" depends="download-ivy">
        <path id="ivy.lib.path">
            <fileset dir="${ivy.jar.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="download-ivy" unless="offline">
        <mkdir dir="${ivy.jar.dir}" />
        <get src="https://repo1.maven.org/maven2/org/apache/ivy/ivy/${ivy.install.version}/ivy-${ivy.install.version}.jar"
             dest="${ivy.jar.file}" usetimestamp="true" />
    </target>

</project>

Here’s a run-through of the build, including dependency download. In this case, ivy.jar is already cached under $HOME/.ant.

$ ant clean-all
Buildfile: /home/trawick/git/jaccounts/build.xml

clean:
   [delete] Deleting directory /home/trawick/git/jaccounts/build

clean-all:
[ivy:cleancache] :: Apache Ivy 2.4.0 - 20141213170938 :: http://ant.apache.org/ivy/ ::
[ivy:cleancache] :: loading settings :: url = jar:file:/home/trawick/.ant/lib/ivy.jar!/org/apache/ivy/core/settings/ivysettings.xml

BUILD SUCCESSFUL
Total time: 0 seconds
$ ant build
Buildfile: /home/trawick/git/jaccounts/build.xml

download-ivy:
      [get] Getting: https://repo1.maven.org/maven2/org/apache/ivy/ivy/2.4.0/ivy-2.4.0.jar
      [get] To: /home/trawick/.ant/lib/ivy.jar
      [get] Not modified - so not downloaded

init-ivy:

get-dependencies:
[ivy:retrieve] :: Apache Ivy 2.4.0 - 20141213170938 :: http://ant.apache.org/ivy/ ::
[ivy:retrieve] :: loading settings :: url = jar:file:/home/trawick/.ant/lib/ivy.jar!/org/apache/ivy/core/settings/ivysettings.xml
[ivy:retrieve] :: resolving dependencies :: #;working@trawick-ideapad
[ivy:retrieve]      confs: [binaries]
[ivy:retrieve]      found junit#junit;4.12 in public
[ivy:retrieve]      found org.hamcrest#hamcrest-core;1.3 in public
[ivy:retrieve] downloading https://repo1.maven.org/maven2/junit/junit/4.12/junit-4.12.jar ...
[ivy:retrieve] ............................... (307kB)
[ivy:retrieve] .. (0kB)
[ivy:retrieve]      [SUCCESSFUL ] junit#junit;4.12!junit.jar (662ms)
[ivy:retrieve] downloading https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar ...
[ivy:retrieve] .............. (43kB)
[ivy:retrieve] .. (0kB)
[ivy:retrieve]      [SUCCESSFUL ] org.hamcrest#hamcrest-core;1.3!hamcrest-core.jar (487ms)
[ivy:retrieve] :: resolution report :: resolve 2791ms :: artifacts dl 1153ms
    ---------------------------------------------------------------------
    |                  |            modules            ||   artifacts   |
    |       conf       | number| search|dwnlded|evicted|| number|dwnlded|
    ---------------------------------------------------------------------
    |     binaries     |   2   |   2   |   2   |   0   ||   2   |   2   |
    ---------------------------------------------------------------------
[ivy:retrieve] :: retrieving :: #
[ivy:retrieve]      confs: [binaries]
[ivy:retrieve]      2 artifacts copied, 0 already retrieved (351kB/14ms)

build:
    [mkdir] Created dir: /home/trawick/git/jaccounts/build/application/classes
    [mkdir] Created dir: /home/trawick/git/jaccounts/build/application/jar
    [mkdir] Created dir: /home/trawick/git/jaccounts/build/tests/classes
[project.javac] Compiling 16 source files to /home/trawick/git/jaccounts/build/application/classes
      [jar] Building jar: /home/trawick/git/jaccounts/build/application/jar/Trx.jar
[project.javac] Compiling 7 source files to /home/trawick/git/jaccounts/build/tests/classes

BUILD SUCCESSFUL
Total time: 6 seconds

References: