Powerful features in Ant
1.6 including presetdef
, macrodef
, and import
make it simple to keep common tasks in common locations and simplify the creation of a project’s build script. Further, although Ant scripts are not programs in the traditional sense, these features do allow quasi-OO techniques.
These aren’t new features – Ant 1.6 was released almost three years ago – but it’s always good to be reminded of cool features.
Build files for Java projects tend to contain the same core sets of actions:
- compile source classes
- compile test classes
- run unit tests
- build javadoc
- package files into JARs, WARs, EARs, etc.
I find that using Spring and Hibernate in many projects I also generate Hibernate *.hbm.xml files using XDoclet, and I’m also starting to formalize best practices for running test coverage and generating quality metrics as part of the continuous build process, so I’m adding the common elements of those tasks also.
PresetDef
PresetDef
s allow you to set default values and even default child elements in existing targets. For example, here’s my PresetDef
for the copy
target:
<presetdef name='def.copy'>
<copy preservelastmodified='true' includeEmptyDirs='false' />
</presetdef>
I can override either preservelastmodified
or includeEmptyDirs
when I call def.copy
, but if I leave those attributes out the default values are used. This is essentially like having a protected accessor in a base class.
PresetDef
s can contain default child elements too, for example:
<presetdef name='def.junit'>
<junit
fork='true'
haltonfailure='${test.haltonfailure}'
haltonerror='${test.haltonerror}'
printsummary='true'>
<formatter type='plain' usefile='false' />
<formatter type='xml' />
</junit>
</presetdef>
In this case I’m defaulting to fork the jvm for testing, I’m using build properties to specify whether to fail the build on error or failure, and I’m including two formatters, one to stdout and one to an XML file.
Declaring a PresetDef
creates a new pseudo target whose XML element name is the name
attribute. Here’s a simple target that’s used in conjunction with compiling Java source; typically you’ll want to copy all non-Java resources from the source path to the class path, e.g. image files, property files, etc. after compiling:
<target name='copy.resources' depends='prepare'>
<def.copy todir='${dir.classes.src}'>
<fileset dir='${dir.src}' excludes='**/*.java'/>
</def.copy>
</target>
In this example I’m using def.copy
primarily because I want to maintain the modification date of the files. This wouldn’t make sense if I were changing the file while copying it (e.g. if I were replacing tokens) but I do want to preserve dates in this case so I know that the correct versions of the files were copied.
MacroDef
The primary motivation for adding MacroDef
to Ant was to avoid the cost of AntCall
. For small projects you probably wouldn’t notice the time and resource costs of AntCall
s but for a large and complex project, reloading/reparsing the build script and reinitializing everything can slow down a build significantly. So MacroDef
was added to address the need to perform common sequences of tasks with parameter passing, similar to calling a method in a programming language, but in a lightweight fashion.
Here’s a sample MacroDef
for running JUnit tests. This one is fairly simple with just two subtasks – running the tests and generating HTML reports from the XML formatter output. There are two attributes, maxMemory
(with a typical default value) and report.dir
, and two required child elements filesets
and classpathElements
:
<macrodef name='unitTest'>
<attribute name='maxMemory' default='-Xmx256m'/>
<attribute name='report.dir' />
<element name='filesets' optional='false' />
<element name='classpathElements' optional='false' />
<sequential>
<def.junit>
<classpath>
<classpathElements />
</classpath>
<jvmarg value='@{maxMemory}' />
<batchtest todir='@{report.dir}'>
<filesets />
</batchtest>
</def.junit>
<junitreport todir='@{report.dir}'>
<fileset dir='@{report.dir}'>
<include name='TEST-*.xml' />
</fileset>
<report format='frames' todir='@{report.dir}' />
</junitreport>
</sequential>
</macrodef>
attribute
s and element
s allow simple and complex ‘parameter’ passing. An attribute
is a string parameter, like a property
, except that the syntax for resolving its value uses an ‘@’ instead of ‘$’ to avoid collisions with build properties. element
s allow you to pass entire XML elements (arbitrarily complex) into a MacroDef
. Note that Ant does validate the usage and the build will fail if non-optional child elements are missing.
Here’s a sample usage of this MacroDef
:
<target name='def.test'
depends='def.clean, xdoclet.hibernate, def.compile.tests'
description='run all unit tests'>
<delete dir='${dir.testresult}'/>
<mkdir dir='${dir.testresult}'/>
<unitTest report.dir='${dir.testresult}'>
<filesets>
<fileset dir='${dir.test}'>
<include name='**/*Test.java'/>
<exclude name='**/Base*Test.java'/>
</fileset>
</filesets>
<classpathElements>
<path refid='classpath.test'/>
<pathelement location='${dir.classes.test}'/>
</classpathElements>
</unitTest>
</target>
And to see how Ant inserts default values from the PresetDef
and parameters from the MacroDef
, here’s the equivalent Ant code not using PresetDef
or MacroDef
:
<target name='def.test'
depends='def.clean, xdoclet.hibernate, def.compile.tests'
description='run all unit tests'>
<delete dir='${dir.testresult}'/>
<mkdir dir='${dir.testresult}'/>
<junit
fork='true'
haltonfailure='${test.haltonfailure}'
haltonerror='${test.haltonerror}'
printsummary='true'>
<formatter type='plain' usefile='false' />
<formatter type='xml' />
<classpath>
<path refid='classpath.test'/>
<pathelement location='${dir.classes.test}'/>
</classpath>
<jvmarg value='-Xmx256m' />
<batchtest todir='${dir.testresult}'>
<fileset dir='${dir.test}'>
<include name='**/*Test.java'/>
<exclude name='**/Base*Test.java'/>
</fileset>
</batchtest>
</junit>
<junitreport todir='${dir.testresult}'>
<fileset dir='${dir.testresult}'>
<include name='TEST-*.xml' />
</fileset>
<report format='frames' todir='${dir.testresult}' />
</junitreport>
</target>
As with any code reuse strategy (e.g. base classes or JSP includes) the biggest benefit is centralization. If you copy and paste and need to make changes in the common code, you need to find all instances and make multiple identical changes, risking missing some. But with intelligent code reuse, when you change the code in the base class or included file, or for Ant build files in a PresetDef
or MacroDef
, and all users immediately see the changes.
Import
So you’ve created a bunch of useful PresetDef
s, MacroDef
s, Targets
s, etc. but how do you use them in your various build scripts? Ant build files are just XML, so the traditional way was to use entity includes. This basically worked but was very inflexible. In Ant 1.6 you can use import
to intelligently include a partial or complete build script. It’s intelligent inclusion because you can redefine (override) targets or other elements and you can even continue to use or call the overridden versions.
Here’s a complete sample build script for a project called ‘common’ that contains shared code common to many projects. It import
s a file called standard.ant
located here
, which contains the above examples and a few other target
s, path
s, and property
definitions:
<?xml version='1.0' encoding='UTF-8'?>
<project name='common' basedir='.' default='jar'>
<!-- optional properties file for developer overrides -->
<property file='build.properties' />
<import file='standard.ant' />
<target name='xdoclet.hibernate'
description='generates hibernate descriptors'
depends='def.compile.java'>
<xdocletHibernate>
<classpathElements>
<fileset dir='lib/xdoclet/'>
<include name='*.jar' />
</fileset>
<path refid='classpath.libdir' />
</classpathElements>
<filesets>
<fileset dir='${dir.src}' includes='**/model/**/*.java' />
</filesets>
</xdocletHibernate>
</target>
<target name='jar'
depends='def.clean, xdoclet.hibernate, def.compile.tests'>
<jar jarfile='build/common.jar'>
<fileset dir='${dir.classes.src}' />
<fileset dir='${dir.classes.test}' />
</jar>
</target>
<target name='init.dependencies' depends='check.javadoc.uptodate'/>
<target name='javadoc'
depends='prepare'
unless='javadoc.uptodate'
description='Build Javadoc'>
<delete dir='${dir.javadoc}' />
<mkdir dir='${dir.javadoc}' />
<def.javadoc doctitle='Common'/>
</target>
<target name='test' depends='def.test' description='Run unit tests' />
<target name='all' depends='jar, test, javadoc' description='Run unit tests' />
</project>
This is a pretty small build file considering what it does. If you run the all
target it will do the following:
- delete the previous build directory
- compile Java source
- compile test Java source
- generate Hibernate .hbm.xml descriptors for classes in any package whose name contains “model”
- build a JAR file with source and test classes (to include reusable test base classes), Hibernate descriptors, and all other non-Java resouces in the src and test directories
- run unit tests
- generate HTML reports from JUnit XML formatter output
- build Javadoc
This certainly isn’t all that apparent just by looking at the build script, but that’s a side effect of any code reuse.
Note that Ant properties cannot be changed once they’ve been set, so the statement order is important. First a build.properties
file is loaded (if it exists) to give the developer a chance to set custom values. Then standard.ant
is import
ed and any property values that weren’t set from the command line via -D
parameters or in build.properties
are set.
So you can see that it’s quite simple to extract common build functionality into libraries of reusable components to very quickly create build scripts with significant functionality, with only a minimum of customization. Even the sample build script that I’ve show could be a lot smaller if all projects have the same basic structure (one Java source directory in src/, one test directory in test/, JARs in lib/, etc.). You could easily push even more than I have into standard.ant
(and other included files) and have build scripts that contain little more than the title for your Javadoc and the name of your JAR file.
Powerful features in Ant 1.6 including presetdef, macrodef, and import make it simple to keep common tasks in common locations and simplify the creation of a project’s build script. Further,...