Ant on the make

This article was originally published in VSJ, which is now part of Developer Fusion.
For decades, software developers have been using a magical incantation composed of ad-hoc shell scripts, Windows batch files, scripts for "make" tools, and assorted custom utilities to build, maintain, and deploy complex software projects. Just in time for the age of "write-once, run anywhere software" enabled by the Java platform, a new OS-independent build/maintenance/deploy scripting tool called Ant is taking the software development world by storm. In this article, we will get acquainted with Ant, and learn how to use it to manage software projects.

The Legacy Batch File

To quickly understand what Ant can do for us, we will start with a small build automation problem, one that occurs almost daily in a developer's life. We will tackle it with a Windows batch file solution, and then see how the same problem can be solved with Ant. Then we will extend our reach and show the additional flexibility and features that an Ant-based solution provides.

DiagramConsider a simple Java development tree, with two source files in different packages as in Figure 1.

What we want to do is to compile both the com.vsj.ant.Test class (in the com.vsj.ant package) and com.vsj.bee.Bumble class (in the com.vsj.bee package), and then place them into a single JAR file called testlib.jar. We want to place the resulting JAR file into the lib directory. Of course, in a production situation, we may have many more source files in each package and many more packages – however, the approach to automate the build will be the same.

Using a batch file, included as buildit.bat in the source distribution, we can automate this. Here is what buildit.bat file looks like:

mkdir build\classes
mkdir lib
javac .\src\com\vsj\ant\*.java 
	.\src\com\vsj\bee\*.java
	-d .\build\classes
jar cvf .\lib\testlib.jar -C
	.\build\classes .
The batch file creates the lib directory for the output, and a build\classes directory for the generated class files during the compile. Note that we specify each of the packages to build explicitly in the javac invocation. Since the compiled output (class files) are essentially merged into the common output tree below build\classes (com.vsj.*), there is no need to specify each package when using the jar command.

Try running the batch file. Check under build\classes to verify that the class files are created together with the package directory tree. Check the lib directory for testlib.jar file. Perform a "jar tvf testlib.jar" on the file to ensue the two expected binaries are indeed archived within the JAR bundle. Finally, delete the lib\testlib.jar file to get ready for the next test.

This buildit.bat batch file approach works well on Windows-based machines, and as long as the project stays simple, it is also be quite speedy on most machines. For larger projects, more complex projects, or projects that requires a build mechanism that works across UNIX, Windows, and MacOS – we will need to look to other build tools such as Ant.

Ant's XML core

Take a look at the Ant build script (called a "buildfile" in Ant-speak), equivalent to buildit.bat, in the build_01.xml file. This is listed below:
<project name="Test" default="lib">

<target name="compile">
<mkdir dir="./build/classes"/>
<javac srcdir="./src" destdir="./build/classes"/>
</target>

<target name="lib">
<mkdir dir="./lib"/>
<jar jarfile="./lib/testlib.jar" basedir="./build/classes"/>
</target>

</project>
Firstly there are a few things to notice about this file:
  • an Ant buildfile is an XML file, which gives it a regular structure, and more importantly make it very easy to parse, validate, and even to generate
  • the document element (root) is <project> and it contains a number of <target> elements inside
  • a <target> element can contain many Ant tasks. Each Ant task is itself an XML element. For example, we can see <mkdir> task for making directory, <javac> task for compiling Java source files, <jar> task for creating JAR archive, etc.
  • paths are separated by the "/" forward slash, even on Windows-based systems. This is OK as ant will translate to the native separator

Invoking Ant

When you invoke Ant on the command line without any argument, it will look for a buildfile called build.xml to execute by default. If you try that now, you'll get a message similar to:
Buildfile: build.xml does not exist!
Build failed
To use our own buildfile, we need to use the -f (or the long form: -buildfile) command switch to specify that we want ant to execute the build_01.xml script. From the code directory of the source distribution, here is the command line:
ant -f build_01.xml
If you execute the above command, here is the output you will get:
Buildfile: build_01.xml
lib:
	[jar] Building jar: G:\simple\lib\testlib.jar

BUILD SUCCESSFUL
Now, try to execute the same command again. This time, the build succeeded but it did nothing. Why?

The buildfile did nothing this time because the resulting testlib.jar is already up to date. This is the exact desirable behaviour that we want! With buildit.bat, the major shortcoming is the fact that it will always recompile and re-archive – even if testlib.jar is up to date.

Ant projects and tasks

We already know that a buildfile describes a project, composed of one or more targets. Each target is composed of tasks. The buildfile describes the parameterisation of the tasks, as well as how they interrelate with one another. For example, for a <javac> compilation task, we specify the source files that need to be compiled as well as the destination directory where the compiled class files should be placed. Parameterisation of the tasks element is performed through XML attributes or XML nested sub-elements.

Each task in a buildfile is actually implemented by a Java class (indeed you can create your own task by supplying a class that implements the org.apache.tools.ant Task interface – but this is beyond the scope of this introductory article). Some core tasks are supplied with Ant, and many optional tasks can be obtained through the web. Together, the available library of tasks should cover most of your Java project build and management needs. The table below describes a few of the most commonly used core tasks. Consult the Ant documentation for many more tasks that you can use, as well as details of the attributes or sub-elements that these tasks can contain.

Common core tasks
Task Description
<javac> Compiles java code
<jar> Creates a JAR archive
<copy> Copies a file or a set of files
<mkdir> Creates a directory
<delete> Deletes a file, directory, or set of files

As an example, take a look at the <javac> task in the build_01.xml file:

<javac srcdir="./src" destdir="./build/classes"/>

The attribute srcdir specifies the directory where the source tree is located, while destdir specifies where the output class tree should be created. Note that the <javac> task, before triggering the Java compiler, will check the output class tree to see if it is already up to date (i.e. no compilation required). Since the <javac> task will recursively descend a source tree looking for Java source to compile, there is no need to explicitly specify the com.vsj.ant and com.vsj.bee packages (as was the case with the buildit.bat file).

Managing Dependencies with Ant

To see build_01.xml work, we need to delete the testlib.jar file, the lib directory, the build\classes directory and all the directories and files below the build\classes directory. Do this now.

Run the build_01.xml buildfile again, note that this time you get the message:

Buildfile: build_01.xml
lib:
	[mkdir] Created dir: G:\simple\lib
BUILD FAILED
file:G:/simple/build_01.xml:10: G:\simple\build\classes not found.
Total time: 1 second
The buildfile doesn't do quite what we expected. If we examine the first line of the project element in build_01.xml, we will see:
<project name="Test" default="lib">
Here, the default attribute is used to specify the default target of the build file: lib. This means that when Ant is invoked without a specific target, the target called lib will be executed. The lib target is actually a <jar> task:
<target name="lib">
<mkdir dir="./lib"/>
<jar jarfile="./lib/testlib.jar" basedir="./build/classes"/>
</target>
Since we've just deleted the build\classes directory, the basedir attribute of the <jar> task is pointing to nothing. This is the reason why we got the build failure message.

To fix this, we must first execute the compile target. We can specify one or more targets to build in a build file by listing them on the command line. Try:

ant -f build_01.xml compile
You should see the successful compile:
Buildfile: build_01.xml
compile:
	[mkdir] Created dir: G:\simple\build\classes
	[javac] Compiling 2 source files to G:\simple\build\classes

BUILD SUCCESSFUL
Total time: 2 seconds
Now, if you try the command again:
ant -f build_01.xml
...the default lib target is executed and since the classes files are in place, the testlib.jar archive is successfully created.

It would be nice if we could tell Ant to execute the compile target if the classes files are not already in place. We can do exactly this, with the depends attribute of a target.

Specifying Ant tasks dependencies

In the build_02.xml file we have modified the original build_01.xml file to:
  1. Specify that the lib target depends on the compile target.
  2. Use a number of <property> tasks to set up relative paths, making relocation of the project simpler.
  3. Added a new target called clean to clean up any generated output.
Here is the content of the build_02.xml file:
<project name="Test" default="lib" basedir=".">

<property name="out" location="${basedir}/build/classes"/>
<property name="archive" location="${basedir}/lib"/>

<target name="compile">
<mkdir dir="${out}"/>
<javac srcdir="${basedir}/src" destdir="${out}"/>
</target>

<target name="lib" depends="compile" >
<mkdir dir="${archive}"/>
<jar jarfile="${archive}/testlib.jar" basedir="${out}"/>
</target>

<target name="clean" description="clean up" >
	<delete dir="${out}"/>
	<delete dir="${archive}"/>
</target>

</project>
The addition of the basedir attribute to the <project> element will set a property called ${basedir} that we can use within the script. The lib target now has a depends attribute, and it is set equal to compile – this is how you can specify interdependency between the targets. We've created two additional properties, called out and archive, that are created using <property> tasks independent of all targets. These properties are referenced within the targets via ${out} and ${archive}. Compare this with the original build_01.xml and you will see that we can easily relocate this version of the buildfile by simply changing the basedir attribute of the <project> element.

Last but not least, note the use of the <delete> task in the clean target to remove the two directories generated during the build.

We can try this file out by first removing the two generated directories via the command:

ant -f build_02.xml clean
Check to see that the two directories are indeed removed. Then try a build:
ant -f build_02.xml
Check to see that the classes are generated and the JAR file contains both expected classes. Notice that we did not have to execute the compile target separately since Ant now knows that the lib target depends on the compile target.

Now, with everything up to date, try a build again:

ant -f build_02.xml
The build succeeds this time, but did nothing because everything is up to date, and nothing needs to be done.

By modifying build_02.xml, you can readily create Ant build files suitable for most small or experimental projects.

Combining Multiple Source Trees

The build file, build_03.xml, shows one way of compiling and combining mutually exclusive source trees into a single JAR file. In this case, we have a class called NewProduct in the com.superco.product package. Let us assume that the source tree for this package is managed by another division, and is rooted in the ${basedir}/altsrc directory.

To include this new package into the generated JAR library, we only have to make a very minor change.

In the compile target of build_03.xml, we added a new <javac> task that will compile the additional source tree:

<target name="compile">
<mkdir dir="${out}"/>
<javac srcdir="${basedir}/src" destdir="${out}">
<javac srcdir="${basedir}/altsrc" destdir="${out}"/>
</target>
Since both <javac> have sent output to the same ${out} tree, we have no need to change the parameterisation of the <jar> task.

To try this out, first clean up the generated directories:

ant -f build_03.xml clean
Then execute the default lib target to create the JAR file:
ant -f build_03.xml
Change to the lib directory and see the content of the JAR file via:
jar tvf testlib.jar
While this works we are starting the Java compiler twice by having two instances of <javac> in the same target, which can be quite inefficient if we add many more external packages. This can be avoided by using a technique we cover next.

Copying Dependent Binaries From Other Projects

Most large projects are composed of many smaller sub-projects. The current thinking in software development methodologies often prescribes a loosely coupled approach to managing sub-projects. Because of this, there is often a need to 'clone' or completely copy another project's tree of binaries to merge with our own. This enables us to better control the project evolution by synchronising with the external project only at fixed points during the development cycle.

Let's see how this can be done. Assume that we have a third party tree of binary class files located in ${basedir}/refbin. This is the class tree for the com.maxco.util package.

Now, to reflect real world practice, the members of this team have left the class files in the source tree – that is, the tree contains both .java and .class files! Instead of complaining to the team, we decided to copy only the class files over to our generated binary tree. To do this, we need to work with an Ant object known as a Fileset.

Working with Ant filesets

Many Ant tasks can take filesets for parameterisation. For an example, we will be using the <copy> task in our case. This task can copy files and directories from a specified source to a destination. We will use the <copy> task in the build_04.xml buildfile. If you examine build_04.xml, you will find a brand new target called clonelib:
<target name="clonelib">
<copy todir="${out}">
	<fileset dir="${extern}">
	<exclude name="**/*.java"/>
	</fileset>
</copy>
</target>
This target contains a single <copy> task. The destdir attribute is set to the same ${out} where our compile target output will go to. The parameter is a <fileset> sub-element. The <fileset> element specify dir to be ${extern}, which is set to ${basedir}/refbin by the property:
<property name="extern" location="${basedir}/refbin"/>
This means that every single file in the ${basedir}/refbin directory, or any of it's subdirectory, will be copied. However, since we use an <exclude> element, this <fileset> will exclude any .java file found at any level. The "**/" wildcard is used to match any subdirectory level. Therefore, this <copy> element will only copy the .class files to our ${out} directory.

Also in build_04.xml, we improved the multiple source tree compilation (the compile target) by using a nested <src> element without using multiple <javac> tasks:

<target name="compile">
<mkdir dir="${out}"/>
<javac destdir="${out}">
	<src path="${basedir}/src"/>
	<src path="${basedir}/altsrc"/>
</javac>
</target>
This ensures that the <javac> task is only invoked once to compile all the source trees we require (obviously this can be extended beyond the two source trees that we have in this example).

Last but not least, the lib target now depends on more than just the compile target. It also depends on the clonelib target. We add this dependency to the build_04.xml file:

<target name="lib" depends="compile,clonelib" >
<mkdir dir="${archive}"/>
<jar jarfile="${archive}/testlib.jar" basedir="${out}"/>
</target>
Since the <copy> task will, by default, check to see if the files in the todir attribute is up to date before performing its task – the default action for the entire project will be "do nothing" if the JAR file is completely up to date.

To try out the build_04.xml file, first clean up the generated directories:

ant -f build_04.xml clean
Now, build the entire project using the default target:
ant -f build_04.xml
You should see the copying of the class file in the new <copy> task. Check the generated JAR file using:
jar tvf testlib.jar
Verify that this new library combines the com.vsj.ant.Test, com.vsj.bee.Bumble with the new source tree class com.superco.product.NewProduct, as well as the reference binary com.maxco.util.Gizmo. Verify as well that the .java class did not get copied into the ${out} tree of binaries.

Versatile Ant

Ant is not just another feeble build-tool. One of its major strengths is its extensibility – it is quite easy to write a new task for Ant. Ant's feature set is never complete since new tasks are created by developers and enthusiasts every single day. Indeed, one can create Ant tasks to perform almost anything that Java code can do – there is already a wealth of optional tasks that have been created that take advantage of it. A couple of the most common advanced Ant deployments include:
  • software unit testing in conjunction with JUnit (JUnit task)
  • web applications build-test-deploy integration with Tomcat (task included with Tomcat 4.1.x)
Some useful Ant tasks that you may want to explore include:
  • Cab – Like <jar> task, but creates Microsoft CAB files for use with Internet Explorer.
  • Zip – Creates archives in zip file format.
  • Tar – Creates a tar archive.
  • EJB – Vendor specific EJB development tasks.
  • Ant – Runs another buildfile. Useful for hierarchy of sub-projects.
  • AntCall – Executes a specific target in the current buildfile.
  • FTP – Sends, receives and delete files from a remote FTP server.
  • get – Retrieves a file from any remote URL.
  • Mail – Sends email via SMTP protocol.

Other handy tips

Finally, we will cover a few useful tips that you may find helpful when using Ant.

Specifying Command Line Properties
You can use the -D switch to provide properties directly to an Ant buildfile. Properties specified in this way will take precedence over those that are specified in the buildfile. For example, to include a different external binary directory for build_04.xml, we can specify:

ant -f build_04.xml -Dextern=./newref

Customised Property File
By specifying -propertyfile <filename> at the command line, you can supply a set of overriding properties for your buildfile. For example, a file called build.properties may contain:

out=build/classes
archive=lib
extern=refbin
And we can use the following command line to customise our build:
ant -f build_04.xml -propertyfile build.properties

Providing Project Help Including
If you supply a description attribute to a target the -projecthelp command argument will print that description. This is useful as help text for someone using your buildfile. For example, we can add a description for the clonelib target:

<target name="clonelib" description=
"Copy from the reference tree of class files">
Try it out using build_05.xml:
ant -f build_05.xml -projecthelp

Adding External Buildfile Segments
You can use XML's external entity declaration to include XML segments in your buildfile. For example, if we keep our <property> tasks in a separate XML file called properties.xml:

<property name="out" location="${basedir}/build/classes"/>
<property name="archive" location="${basedir}/lib"/>
<property name="extern" location="${basedir}/refbin"/>
...then we can declare the external entity at the beginning of the buildfile as:
<!DOCTYPE project [
	<!ENTITY externalProperties SYSTEM "properties.xml">
]>
In the XML document itself, we can reference this external entity where we want the segment to be inserted:
<project name="Test" default="lib" basedir=".">
&externalProperties;
...
See build_05.xml for an example of the use of external entity references.

Conclusions

Ant is a Java-friendly software build tool that is easily extensible. It works on all platforms on which Java runs. In this article, we have discovered how to use Ant to automate the process of software build and maintenance.


Sing Li is a consultant, trainer and freelance writer specialising in Java, web applications, distributed computing, and peer to peer technologies. His recent publications include Early Adopter JXTA, Professional JINI and Professional Apache Tomcat, which are all published by Wrox Press.


Downloading and installing Ant

The latest version of Ant can be downloaded from jakarta.apache.org/ant.

Installation of Ant requires that you place the <ant installation directory>/bin directory into your PATH environment variable. You should also set the ANT_HOME environment variable to where you've installed Ant.

Additional Ant tasks and tools are available from jakarta.apache.org/ant/external.html or other vendor/public sources. Optional tasks exist as JAR files and should be placed into the <ant installation directory>/lib directory for Ant to load automatically.

You might also like...

Comments

About the author

Sing Li United States

Sing Li has been writing software, and writing about software for twenty plus years. His specialities include scalable distributed computing systems and peer-to-peer technologies. He now spends ...

Interested in writing for us? Find out more.

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“The first 90% of the code accounts for the first 90% of the development time. The remaining 10% of the code accounts for the other 90% of the development time.” - Tom Cargill