Apache Ant is a good tool. This is a collection of simple steps to make your ant scripts easier to develop and maintain.

Aspiratonal Ant

Building Better Builds

More than a year ago I read the article Top 15 Ant Best Practices by Eric Burke, and I came across a reference to it in someone's blog the other day. So I thought I would reread it.

To my surprise I realised that my use of Apache Ant had changed significantly over that year. And I thought that the advice given might no longer be appropriate. To sum up the change in my use of Ant I would describe it as switching from writing single objective scripts towards multi-objective reusable scripts.

Home · Contact · Blog · General Interest · Software · JHosts · Gos4j · © Hugh Reid

The Day After

The day after the article was published Apache released version 1.6. This release added a number of new features, that may have appeared subtle, but they enabled better use of some of ant's more powerful tasks.

The really useful features added were:

  • Macros
  • AntLib
  • Imports
  • Namespaces
  • SubAnt

Or try:
Apache Ant
multi-objective reusable scripts
Ant macros
visitor pattern
limitations of ant
Related Pages

Don't Use A Single Build File

It seems quite crazy to me now to suggest using a single build file. It is still good advice for ant 1.5.4, but now that ant has the ability to import other ant scripts in a sensible way (you have always been able to use XML system references) then you should make use of that asset to structure your builds.

When you import a file in ant, the imported file project defintion and its properties are inherited from the current file. This means that separating the concerns of different aspects of the build becomes easier to do. Files become less monlithic in nature and you start to get areas of possible re-use.

Put build.xml Files Everywhere

Instead of having one entry level into the build you can have lots. You can still use the ant -find option if you need to, but wherever you run the build from, you are running it in context.

This is how you put the file imports to best use. The trick is to put the more generic build scripts in the parent directories, and so by just running ant locally you run the context based target.

If your project file structure follows something like Sun's Blueprints then you will have about 4 levels of directory with several separately built artefacts at each level. Each artefact build file should set the properties unique to that artefact and then import the shared build files from the parent directory.

Externalise Paths

It is important to separate the concerns of the build from the concerns of the software being built. The dependencies of the software being built should not be written into the build files. So the suggestion is to use the includesfile part of the path-like elements; which is a neat trick to get the software build paths into the build structure.

<classpath>
    <fileset dir="${lib}" includesfile="jars.txt" />
</classpath>
			

One side effect of this is that it makes it easier to identify when the dependency tree has changed. The following section shows how this can be realised.

<uptodate property="no.jar.change" targetfile="${artefact}">
    <srcfiles file="jars.txt" />
    <srcfiles dir="${lib}" includesfile="jars.txt" />
</uptodate>
			

Record Your Output

When developing a complex build there are always problems. Ant tasks are quite forgiving when it comes to missing resources and nonsensical operations. As a result finding the source of a missing jar can be quite a long process.

The first thing you do is set the -verbose option. But to make that useful you need to always record the output. To do this use the record task like this:

<record name="${ant.file}.log" loglevel="verbose" />
			

Use Macros

Ant macros are very useful, they represent a simple way of implementing a visitor pattern style build.

Firstly you should create the macro definition. This can be done in a high level build file or included from a kind of macro library.

<macrodef name="mCompile">
    <attribute name="srcdir" default="src" />
    <attribute name="destdir" default="classes" />
    <attribute name="classpathref" default="${java.class.path}" />
    <sequential>
	<javac srcdir="@{srcdir}"
		destdir="@{destdir}"
		classpathref="@{classpathref}"
		target="1.3"
		source="1.3"
		debug="on"
		failonerror="no" />
    </sequential>
</macrodef>
			

Notice that a naming convention is used to tell build developers that it is a macro definition. Also the parameters are defaulted to the standard locations used by the project, but please note that you may not want to do this to force build developers to be explicit about their parameters. The task PreSetDef is a more specialised, but less flexible, version of the same functionality.

And then you use it in the various places you need to compile things.

<mCompile classpathref="my.class.path" />
			

That may seem like a lot of work to just write a javac line. But consider what happens to a large system build when you move up a version of Java and perhaps want to use the -Xlint features of Java1.5. With the macro you can make sweeping changes to all common actions in your system (which can be dangerous), but can also permit each project to have its own defined version and then to reuse the core build scripts.

Avoid Custom Tasks

You can do almost everything with the Core Ant tasks.

It is all too easy for experienced Java programmers to get frustrated by the limitations of ant and then dive in and write their own task. I prefer to avoid this wherever possible, I would much rather that the enthusiasts at Apache maintain my tasks rather than my company's developers. But it is true that the first way you might imagine the build working is normally impossible to achieve with ant. But if you approach it from the point of view of what ant can do then you can almost always achieve what you need.

Use Extension Points

Build files that import other build files can override the build resources provided. This allows you to design in extension points for variations in build functionality. Consider the following build snippet:

<target name="generate-war" depends="...">
    <antcall target="pre-generate-war" />
    <war ... />
</target>

<target name="pre-generate-war" />
			

Just running this as it is will do nothing except execute the war task. But if you include this in a project specific build then you have the opportunity to override the pre-generate-war target and add functionality without having to have some conditional switch.

<import file="../app-build.xml"/>

<!-- This is an override -->
<target name="pre-generate-war">
    <copy ... />
</target>
			

You should ensure that the default version is blank and has no dependencies, that way the overriden version does not have to emulate any existing actions. In a multi level hierarchical build you should consider having an extension point in the overriden version to allow further extension at the next level. And incidentally, you can reverse the inheritance remationship by having the extension points used by the leaf build.

As a rule of thumb, wherever you have a sequence of tasks in a target you should provide and extension point prior to each step. Because ant builds in dependency order post step extension points are normally superfluous.

Summary

  • Reuse targets through macros and imported build files.
  • Separate the concerns of the build from those of software dependency.
  • Design your build to be extended and maintained.
  • Don't bother writing your own ant tasks.
  • Record output by default.

Specialist Ant Search