ANT's finally a real build tool

Java Development News:

ANT's finally a real build tool

By Mike Spille

12 Apr 2004 | TheServerSide.com

Like many other people, I've had a love-hate relationship with ANT for sometime now. On the plus side, ANT did away with a whole lotta nastiness that was associated with Make files. The tabs, the colons, the incompatibilities across operating systems, the embedded shell script hacks - all that is gone now. But unfortunately, ANT didn't exactly usher in a golden age of build systems. It's got its own quirks and flaws and design oddities that we've had to live with for some time - the endless unreadable XML, the lack of scripting capabilities (sorry, an ANT task written in Java isn't a script :-), and of course the lack of modular constructs. On the latter, the few anemic nods to modularity have basically sucked, and has lead over and over again to people creating many big build.xml files that are mostly identical, and with little chance for creating corporate/departmental/project build standards beyond offering a reference build.xml file - and instructing developers how to do cut and paste on their OS of choice.

If you're one of those feature-matrix comparison people, and you put ANT and Make (or some other build tool of choice) next to each other, I'd say for most people ANT would win out. ANT just has more plusses on its side than Make does. But there's more to life than soulless feature matrixes and comparisons based only on quantity (ha ha, I have 15 more features than you - I win!). Quality plays a part, and so does what I call "must-haves". Any application in any domain must have the minimum feature set of "must-haves" to be considered seriously in the domain, and build tools are no exception. The problem historically with ANT is that while it's got 20 thousand bazillion features, it's oddly neglected some core must-haves that have crippled it since its inception.

The biggie for me has always been ANT's complete ignorance on the subject of modular build systems. On the various projects I've worked on over the years, invariably one of the tasks that gets tackled early on is the subject of the build system. Specifically - what's our build structure look like, how do we handle dev vs. QA vs. production builds, and above all how do we standardize the build environment? Stories of how this has been done over the years, and the various levels of success ranging from elegant and slick build system up to massive behemoths that ultimately collapsed in on themselves from their own weight, are beyond the scope of this entry, but the fundamental point remains: how do we make builds consistent, and make it easy for new projects to spin up a standard build easily and in a manner that leverages what's already been done in the past?

On that score, ANT has provided two mechanisms: first, the ability to load property files so that ANT "variables" can be set based on the environment is being run in (e.g. you could use different property files for different environments/releases, and with per-developer overrides as well). This gave you a certain amount of configuration variability so you don't have to hard code things in your build files, and it's nice. The second part is the <ant> task, which lets you create a build hierarchy - where you have a master builder (no jokes, please) at the top of a hierarchy which goes down through your project tree and trundles through building stuff as it goes along. This is also nice when you need to "build the world" but don't want a world-size build.xml file.

But, for reasons I've never understood, ANT just stopped there. We got props files and the <ant> task, and the mythical ANT developer stood back and stretched and said "Well, modularities done!". The problem is, while these facilties are nice, they're just not enough if you really want to standardize your build across many projects. Property files are limited in what they let you specify, so you can get configuration variability on a subset of build concepts. And the <ant> task only goes top-down. If you're a typical developer 3 levels deep in the hierarchy trying to build your own little widget, this doesn't help you at all. The end result is that despite the facilities ANT gives you, you still end up with a bunch of build.xml files that keep defining the same targets over and over again in the same way. You've still got a culture of cut'n'paste build creations. Because of this, even though ANT is pretty cool in a number of ways, I could never consider it a real build tool. For all the nice features, it had some holes in its design that you could drive the planet Jupiter through (with room for a couple of moons to spare).

Praise the Lord, ANT 1.6 Is Here!

Finally, after years of employing various ANT hacks to try to achieve some kind of reasonable modularity (antmerge being my favorite), the ANT guys have finally turned their heads to a slight angle from where it was originally pointing. And one of them said, "Hey, Joe, lookitthat! There's a freakin' hole the size of Jupiter over there in our design!!". And Joe said "Shit, you're right - and I think you could fit a couple of moons through there too - at least Europa and maybe Io too". And now, after all these years, we finally have the <import> task. And I can finaly call ANT a real build tool (and Maven can go play in its own cacca for all I care).

In a nutshell, the <import> task lets you reference other build files. This means that you can create common centralized libraries of build files that other people can use on their own projects - all without copy and paste. And believe it or not, the semantics all make sense too. You can provide default tasks and properties, and the importer can override tasks and properties to customize behaviors on a case by case basis if it's required. The end result is that individual project build files are smaller and easier to understand, and common behavior can be achieved across an entire large system in a natural and non-cut-n-pasty manner (I don't know about you, but I always found pasties rather unnatural).

To illustrate the basic idea, I'll reference the simple build refactoring I've done in my own Pyrasun Libraries work. I now have two whole projects to manage (PyraLog and Pyrasun EmberIO libraries - do I rock or what?), and it's a nice small test bed to consider factoring commonalities up out of the individual build files and into a common Pyrasun build file.

Figure 1 below shows my very modest common build file, build_common.xml:

build_common.xml - The common Pyrasun build file
<project basedir="." name="build_common">

<!-- ========== Executable Targets ======================================== -->

  <target description="Initialize environment" name="init" depends="project_init">

    <!-- read properties from the build.properties, if any -->

    <property name="build.home" value="build"/>

    <property name="build.compiler" value="javac1.4" />

    <property name="test.build.home" value="build/test"/>

    <property name="dist.home" value="dist"/>
    <property name="source.home" value="src/main"/>
    <property name="source.test" value="src/test"/>

    <property name="compile.debug" value="true"/>
    <property name="compile.deprecation" value="true"/>
    <property name="compile.optimize" value="true"/>

    <property file="${basedir}/build.properties"/>

    <path id="compile.classpath">
      <pathelement location="${build.home}/classes"/>
      <fileset dir="./lib">
        <include name="*.jar"/>
      </fileset>

    </path>

    <path id="javadoc.path">
      <pathelement path="${source.home}"/>
    </path>
  </target>

  <target description="Project-level prepare phase" name="project_prepare" />

  <target depends="init,project_prepare" description="Prepare build directory" name="prepare">
    <mkdir dir="${build.home}"/>
    <mkdir dir="${build.home}/classes"/>
    <mkdir dir="${test.build.home}/classes"/>
  </target>

  <target depends="prepare" description="Compile source" name="compile">
    <javac debug="${compile.debug}" deprecation="${compile.deprecation}"
              destdir="${build.home}/classes" target="1.4" source="1.4"
              optimize="${compile.optimize}" srcdir="${source.home}">
      <classpath refid="compile.classpath"/>
    </javac>
  </target>

  <target description="Project-level prepare phase" name="project_clean" />

  <target depends="init,project_clean" description="Wipeout all generated files"
             name="clean">
    <delete dir="${build.home}"/>
    <delete dir="${dist.home}"/>
  </target>

  <target depends="clean,compile" description="Clean and compile all components"
             name="all"/>

  <target depends="compile" description="Create component Javadoc documentation"
             name="javadoc">
    <mkdir dir="docs/api"/>
    <javadoc author="true"
             bottom="Pyrasun Java Libraries - Pyrasun Logging"
             destdir="docs"
             source="1.4"
             doctitle="${component.title}"
             packagenames="pyrasun.*"
             access="public"
             sourcepathref="javadoc.path" version="true"
             windowtitle="${component.title} (Version ${component.version})"/>
  </target>

  <target depends="compile" name="build-test">
    <javac debug="${compile.debug}" deprecation="${compile.deprecation}"
              destdir="${test.build.home}/classes" target="1.4" source="1.4"
              optimize="${compile.optimize}" srcdir="${source.test}">
      <classpath refid="compile.classpath"/>
    </javac>
  </target>

  <target depends="compile" description="Create binary distribution" name="jar">
    <mkdir dir="${dist.home}"/>
    <mkdir dir="${build.home}/src"/>

    <copy file="LICENSE" todir="${build.home}/classes"/>

    <jar basedir="${build.home}/classes"
            jarfile="${dist.home}/${component.name}-${component.version}.jar" >
      <include name="**/*"/>
    </jar>
  </target>

</project>

As I mentioned, this common build file is pretty simple. It defines a few basic properties, sets up a few default paths, and then defines prepare, init, clean, compile, jar, and javadoc tasks. Not exactly rocket science, right? Ah, but here's the payoff - here's the build.xml for the EmberIO library:

build.xml - EmberIO build file
<project basedir="." default="compile" name="EmberIO">

<!-- ========== Executable Targets ======================================== -->

  <target description="Initialize environment" name="project_init">

    <property name="component.name" value="EmberIO"/>
    <property name="component.package" value="pyrasun"/>
    <property name="component.title" value="Pyrasun EmberIO"/>
    <property name="component.version" value="0.1_Alpha"/>
  </target>

  <import file="../build_common.xml" />

</project>
Yeah, that's right - that's the whole enchillada. In this case EmberIO is vanilla enough that all I need to do is define a few properties, and then include build_common.xml, and I'm done - I auto-inherit all of the default tasks and those work just fine in this case.

Some things to note from this example:
  • I'm initializing properties in a target named "project_init". This is utilizing a two-level task hierarchy that the build_common.xml defines. At the top level, build_common.xml defines the default common tasks (init, clean, prepare, compile, etc). But it makes these tasks dependent on project-level tasks. So "prepare" is dependent on "project_prepare", "init" is dependent on "project_init", etc. These per-project tasks are defined as empty within build_common.xml. This means a per-project build.xml can define these tasks on a case by case basis and they'll automatically be called and included in the dependency list. In the case of EmberIO's build.xml, it uses "project_init" to define project properties.
  • The common_build.xml file reads in build.properties if it's present. This illustrates a subtle point of the <import> task - references in imported build files are made relative to the importer, not to the location of importee. So the reference to load build.properties in common_build.xml will not be made in the base directory of common_build.xml, but instead in the base directory of build.xml. The ANT docs on this are a little fuzzy at this point in time if you want to read relative to the importee - for now they recommend absolute paths if you don't want to be relative to the importer :-(
At the basic level, that's all there is to it. Obviously there are intricacies if you're creating build files with more complexity than this - you'll have to pay careful attention to paths and filesets, partition off per-project properties and tasks a bit more carefully, and be careful how overrides work. But hopefully the simple examples here are enough to convey the gist of it all - and to show that ANT finally can be taken seriously for large-scale build tasks. And with any luck, maybe the <import> task will be the final nail in Maven's coffin!!


About the author

Mike Spille mike@krisnmike.com
Blog: http://jroller.com/page/pyrasun

Mike Spille is an enterprise developer who has been living in the development world for a long time. He has written very interesting articles on topics such as distributed transactions.

Related Resources