logo

ShrimpWorks

// why am I so n00b?

So far, we’ve covered the basics of creating a re-distributable .jar package suitable for use as a library, and building a Jar file which can be run by a user or server process.

A major part of any non-trivial application these days is the inclusion and re-use of 3rd party libraries which implement functionality your applications require. When a project starts, it’s probably easy enough to manually drop the odd jar library into a lib directory and forget about it, but maintaining a large application which depends on many libraries, which in turn depend on additional libraries for their own functionality, it can quickly turn into a nightmare to manage.

To solve this problem, many dependency management tools have been introduced, most notably, Apache Maven. Maven however, is so much more than just a dependency management tool, and is actually intended to manage your entire project structure. I believe however, the combination of Ant and Ivy provides far more flexibility, extensibility and control over your build and dependency management processes.

So, let’s integrate Apache Ivy into our Ant script as we left it in part 2.

For starters, the code for this part is available in GitHub once again. I’ll walk through the diff of what’s changed since part 2.

As mentioned in my post on FindBugs integration with Ant, I’m a strong believer in the idea that a developer (or user) should be able to check out or download a project’s code and be able to build and start working on the code with minimal time and effort investment. This is especially useful in a work environment where you have many developers working on projects, and time spent flapping around with setting up build environments has a direct impact on timelines and team output.

To this end, our Ant script will be responsible for downloading and configuring Ivy as part of the regular build process, meaning a new developer or user simply has to get your code and have Ant installed - all additional features and capabilities of the build process should be “self maintaining”. I believe this is another testament to Ant’s flexibility.

Let’s go through the changes to our build.xml file since part 2:

build.xml changes:

<project name="hello-world" default="dist" basedir="." xmlns:ivy="antlib:org.apache.ivy.ant">

Start by adding the ivy XML namespace.

<property name="lib.dir" location="lib"/>

Since we’re going to be incorporating additional libraries into our project, this is a new configuration option which we’ll use to point to where we want them stored. This will map to the project-root/lib directory.

<path id="default.classpath">
    <fileset dir="${lib.dir}" includes="default/*.jar"/>
    <pathelement path="${build.src.dir}"/>
</path>

Again, since we’re incorporating additional libraries, the new lib directory needs to be added to the class-path so dependencies can be made use of at compile time. The new Fileset tag is pointing out the location of the “default” dependencies (more on what “default” refers to later - see the explanation on the addition of the ivy.xml file), and is including all .jar packages in that location.

<path id="dist.classpath">
    <fileset dir="${dist.dir}" includes="lib/*.jar"/>
</path>

Defining another new class-path. As you’ll see later on in the updated dist build target, this is used to add libraries to our application’s manifest file (the manifest includes a list of all packages and paths to include in the class-path).

<!-- Ivy configuration -->
<property name="ivy.install.version" value="2.4.0" />
<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" />

As the comment tag suggests, these are a couple of options used for configuring Ivy usage.

Firstly, the version is defined as it’s own variable. Later, this will be built into a download URL, but defining it as a separate value makes it easier to configure.

Then, the ivy.home property sets where the base path to where we want Ivy to live. I’ve set this to the .ant directory in the user’s home directory.

ivy.jar.dir is where Ivy will be downloaded to, and ivy.jar.file is the path to the actual ivy.jar file. Also note how the paths are built up from various properties, so it can be quite easily customised.

Finally, ivy.lib.dir references the library directory, and is used as the root target directory for dependencies Ivy downloads.

<!-- Ivy download -->
<target name="ivy-download">
    <mkdir dir="${ivy.jar.dir}"/>
    <get src="http://repo2.maven.org/maven2/org/apache/ivy/ivy/${ivy.install.version}/ivy-${ivy.install.version}.jar" 
         dest="${ivy.jar.file}" usetimestamp="true"/>
</target>

Now that everything is set up and ready, Ivy itself can be downloaded. The download operation is defined as it’s own build target, so it can be performed independently of everything else in the script (this is very useful for testing), as well as be used as a dependency by targets which actually use Ivy.

It starts by creating the target directory where the Ivy library will be downloaded to, and then using Ant’s get task to perform a download of the desired Ivy version, to the previously configured download location. The usetimestamp attribute of the get task will skip re-downloading the library on every build if it hasn’t been modified on the remote server.

<!-- Ivy initialisation -->
<target name="ivy-init" depends="ivy-download">
    <path id="ivy.lib.path" path="${ivy.jar.file}"/>
    <taskdef resource="org/apache/ivy/ant/antlib.xml"
             uri="antlib:org.apache.ivy.ant" classpathref="ivy.lib.path"/>
</target>

Again, a small target dedicated to a single function. This time, we need to make Ant aware that an Ivy task is available, and this is done using the taskdef task. Another path is defined here as well, used to inform Ant where it may find the task that is being defined.

<!-- Ivy dependency resolution -->
<target name="ivy-resolve" depends="ivy-init" description="retrieve dependencies with ivy">
    <ivy:retrieve pattern="${lib.dir}/[conf]/[artifact]-[revision](-[classifier]).[ext]" />
</target>

At last, we can use Ivy to download some dependencies! The pattern attribute in the retrieve task tells Ivy where it should download artefacts (“libraries” and associated source and javadoc packages) to. You can find out more about patterns in the Ivy reference.

Note that the “classifier” place-holder does not appear to be documented clearly anywhere, and refers to a Maven package classifier - for example “sources” or “javadocs”, which may accompany a library.

The end result of the retrieve task should end up (assuming some dependencies have been defined) in a structure something like project-root/lib/default/some_lib-1.0.jar.

<!-- Simple source build -->
<target name="build" depends="ivy-resolve" description="compile source">

Change to the original build target, set to depend on the previously described ivy-resolve task.

From this point, the target dependencies look something like the following:

  • build (compile .java sources) depends on:
  • ivy-resolve (download libraries which will be needed to compile sources), which depends on:
    • ivy-init (set up and define the Ivy tasks used to download), which in turn depends on:
    • ivy-download (download Ivy library itself) before anything else.
<!-- Build distribution -->
<target name="dist" depends="build" description="generate distribution">
    <mkdir dir="${dist.dir}/lib"/>

    <copy todir="${dist.dir}/lib">
        <fileset dir="${lib.dir}/default" includes="*.jar" erroronmissingdir="false">
            <exclude name="**/*-javadoc.jar"/>
            <exclude name="**/*-sources.jar"/>
        </fileset>
    </copy>

    <manifestclasspath property="dist.manifest.classpath" jarfile="${dist.dir}/${ant.project.name}.jar">
        <classpath refid="dist.classpath" />
    </manifestclasspath>

    <jar jarfile="${dist.dir}/${ant.project.name}.jar">
        <manifest>
            <attribute name="Main-Class" value="${main.class}"/>
            <attribute name="Class-Path" value="${dist.manifest.classpath}"/>
        </manifest>
        <fileset dir="${build.src.dir}" />
        <zipfileset dir="${src.dir}" excludes="**/*.java"/>
    </jar>
</target>

Here’s the whole revised dist target, which has grown substantially. A number of new elements have been added;

Firstly, the mkdir task has been updated to simply create the /lib directory within the already-configured ${dist.dir}. It makes parent directories as needed, so we don’t need a separate mkdir for each.

The addition of the copy task here copies all default libraries (that is, not ones used for testing, coverage, or other configurations - more on these kinds of configurations in another episode). It’s also explicitly excluding any javadoc or source packages from the distributable files - you typically don’t want to include these in your distribution.

Another new addition is the manifestclasspath, which is referencing the dist.classpath we defined earlier, to make sure that the contents of the dist/lib directory are included in the runtime class-path. This is referenced by a new attribute named Class-Path in the jar task’s manifest.

<delete dir="${lib.dir}"/>

Finally, since we created the lib directory as part of the build process, we need to be sure to clean it up as part of the clean target.

New file: ivy.xml:

At last, we arrive at the actual Ivy file itself. Since we haven’t explicitly declared an Ivy resolve task pointing to a specific Ivy file (via the file attribute on the resolve task), Ivy will by default use a file named ivy.xml, which is what we’re providing in this instance:

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.4">
    <info organisation="net.shrimpworks" module="${ant.project.name}"/>

    <configurations>
        <conf name="default" />
    </configurations>

    <dependencies>
        <!-- project dependencies -->
        <dependency org="org.slf4j" name="slf4j-api" rev="1.7.12" conf="default" />
        <dependency org="org.slf4j" name="slf4j-simple" rev="1.7.12" conf="default" />
    </dependencies>
</ivy-module>

Let’s run through the file briefly:

First up, info, which describes some simple properties of the project. This is not immediately useful to us, but will be for publishing artefacts, and more intricate dependency management (where the branch and status attributes prove particularly useful).

Configurations are one of the primary tools Ivy gives us for organising primarily the dependencies used in different types of builds (for example if we’re running unit tests, we require additional dependencies which we don’t want included as part of our standard build - configurations allow us to separate out these sorts of things), as well as artefact publishing. You can read a bit more about Configurations.

In this case, the inclusion of the configurations is not strictly required, since we’re just redefining the defalut configuration, which exists by default anyway. In another part of this series, it will get much more useful.

At last, what we’re actually after after all this, are the dependencies themselves. There are many attributes which can be defined per dependency within this collection. The key requirements are the org, name and rev.

Often when reviewing the usage documentation for a 3rd party project you want to use as a dependency, they will provide a Maven dependency, containing the fields groupId, artifactId and version, which map directly to Ivy’s org, name and rev.

In fact, by default Ivy will download dependencies from the Central Maven Repository, giving you immediate access to all the same content and libraries.

In my example, I’m downloading some simple SLF4J resources which my Main class requires to build.

In Summary

When you execute an ant build or ant dist now, the following actions are going to take place:

  1. Ant will download the Ivy package if it doesn’t exist or is out of date (ivy-download)
  2. Ivy’s Ant task will be registered and made available (ivy-init)
  3. Dependencies will be resolved and downloaded (ivy-resolve)
  4. Source code will be compiled, with the downloaded dependencies on the class-path (build)
  5. Compiled source and downloaded libraries will be packaged for distribution (dist)

This step turned out significantly chunkier than expected, but hopefully it makes sense and is digestible. What we have here is a fairly clean and simple Ivy implementation, and we’ll be building on top of this for the next edition of this epic saga. In the mean time, I highly recommend following some of the task and configuration elements I’ve linked throughout this document, there are many fun and interesting knobs and dials to play with.


Other parts in this series: