Object-Oriented Ant Build Files

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

PresetDefs 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.

PresetDefs 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.1 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 AntCalls 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>

attributes and elements 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. elements 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 PresetDefs, MacroDefs, Targetss, 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 imports a file called standard.ant2 located here, which contains the above examples and a few other targets, paths, 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 imported 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.

  1. Note that the naming convention of prefixing “def.” to the PresetDef is mine and not a requirement of Ant.[back]
  2. My naming convention for Ant includes is to use “ant” as the file extension to differentiate between including build.xml files and included *.ant files[back]

2 Responses to “Object-Oriented Ant Build Files”

  1. Vladimir says:

    An excellent post, thank you!

    I’ve been doing Ant builds for years and years and to my surprise, I’ve never used PresetDef (I used MacroDef instead, which is a bit too bulky for the purpose).

  2. I know quite a few shops who’ve ended up doing what you describe (including the company I work for 😉 and it turns out to work quite well. I’d make a few tweaks, though.

    In many cases I’ve found that you need a project wide build.properties file, which should be under revision control and an additional (optional as well) properties file that applies to the local environment of the development box, so my standard.ant file has a second task loading local.properties. Yes, those tasks are part of standard.ant in my case.

    Then I don’t use a prefix like “dev.” but rather use a different XML namespace with the help of ‘s and ‘s uri attributes.

    Finally I tend to use a more complex setup of targets in standard.ant to allow customization via target overrides (in particular I have a bunch of empty targets that only collect dependencies and work as “extension points”). Explaining this in a comment is not really possible, I should write a blog entry about it …

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 License.