antbuildautomation

Apache Ant: Automating Java Builds Before Maven Existed

Before Maven, before Gradle, there was Ant. XML build files felt like progress in 1999. Here is what a real Ant build looked like for a production Java project.

·3 min read

Apache Ant: Automating Java Builds Before Maven Existed

Before Ant, Java projects were built with shell scripts or Makefiles. Makefiles were designed for C — they operated on files and modification times and required understanding make's dependency rules. Shell scripts were platform-specific. On Windows, the build worked differently than on Unix. Ant solved this with a platform-neutral, XML-based build tool designed specifically for Java.

Ant 1.0 shipped in 2000. By 2001 it was the standard. At Motorola we were using a pre-release version from 1999 and writing build.xml files that looked almost identical to what the release would standardise.

The Build File Structure

An Ant build file was an XML document with a root <project> element, property definitions, and a set of named <target> elements:

<?xml version="1.0" encoding="UTF-8"?>
<project name="nms-core" default="build" basedir=".">

  <!-- ── Properties ──────────────────────────────────────── -->
  <property name="src.dir"     value="src"/>
  <property name="build.dir"   value="build"/>
  <property name="dist.dir"    value="dist"/>
  <property name="docs.dir"    value="docs"/>
  <property name="lib.dir"     value="lib"/>
  <property name="version"     value="1.0.0"/>

  <!-- classpath for compilation -->
  <path id="compile.classpath">
    <fileset dir="${lib.dir}">
      <include name="**/*.jar"/>
    </fileset>
  </path>

  <!-- ── Targets ─────────────────────────────────────────── -->
  <target name="init"
          description="Create build directories">
    <mkdir dir="${build.dir}/classes"/>
    <mkdir dir="${dist.dir}"/>
    <mkdir dir="${docs.dir}"/>
  </target>

  <target name="compile"
          depends="init"
          description="Compile source files">
    <javac srcdir="${src.dir}"
           destdir="${build.dir}/classes"
           classpathref="compile.classpath"
           debug="true"
           deprecation="true"
           target="1.2">
      <include name="**/*.java"/>
    </javac>
  </target>

  <target name="test"
          depends="compile"
          description="Run unit tests">
    <junit printsummary="yes" haltonfailure="yes">
      <classpath>
        <path refid="compile.classpath"/>
        <pathelement location="${build.dir}/classes"/>
      </classpath>
      <formatter type="plain"/>
      <batchtest todir="${build.dir}/test-results">
        <fileset dir="${src.dir}">
          <include name="**/*Test.java"/>
        </fileset>
      </batchtest>
    </junit>
  </target>

  <target name="jar"
          depends="compile"
          description="Package into a JAR">
    <jar destfile="${dist.dir}/nms-core-${version}.jar"
         basedir="${build.dir}/classes">
      <manifest>
        <attribute name="Main-Class"
                   value="com.motorola.nms.Main"/>
        <attribute name="Class-Path"
                   value="lib/snmp4j-1.0.jar lib/log4j-1.0.jar"/>
        <attribute name="Implementation-Version"
                   value="${version}"/>
      </manifest>
    </jar>
  </target>

  <target name="javadoc"
          depends="compile"
          description="Generate API documentation">
    <javadoc sourcepath="${src.dir}"
             destdir="${docs.dir}"
             classpathref="compile.classpath"
             author="true"
             version="true"
             use="true"
             windowtitle="NMS Core API ${version}">
      <doctitle><![CDATA[<h1>NMS Core ${version}</h1>]]></doctitle>
    </javadoc>
  </target>

  <target name="build"
          depends="compile,test,jar"
          description="Full build: compile, test, package"/>

  <target name="dist"
          depends="build,javadoc"
          description="Distribution build including documentation"/>

  <target name="deploy"
          depends="jar"
          description="Copy JAR to application server">
    <copy file="${dist.dir}/nms-core-${version}.jar"
          todir="/opt/motorola/nms/lib"/>
    <copy todir="/opt/motorola/nms/lib">
      <fileset dir="${lib.dir}">
        <include name="**/*.jar"/>
      </fileset>
    </copy>
  </target>

  <target name="clean"
          description="Remove all build artefacts">
    <delete dir="${build.dir}"/>
    <delete dir="${dist.dir}"/>
    <delete dir="${docs.dir}"/>
  </target>

</project>

Running Targets

# Default target (build: compile → test → jar)
ant

# Specific target
ant test

# Override a property at the command line
ant jar -Dversion=1.0.1

# List available targets
ant -projecthelp

Multi-Module Builds

Our NMS had three modules: nms-core, nms-snmp, and nms-ui. We used a master build file at the root:

<project name="nms" default="build" basedir=".">

  <target name="build">
    <ant dir="nms-snmp"  target="jar"  inheritAll="false"/>
    <ant dir="nms-core"  target="jar"  inheritAll="false"/>
    <ant dir="nms-ui"    target="jar"  inheritAll="false"/>
  </target>

  <target name="clean">
    <ant dir="nms-snmp" target="clean" inheritAll="false"/>
    <ant dir="nms-core" target="clean" inheritAll="false"/>
    <ant dir="nms-ui"   target="clean" inheritAll="false"/>
  </target>

</project>

inheritAll="false" prevented the parent's properties from leaking into child builds, which caused confusing overrides.

What Ant Got Right and Wrong

Right: Platform independence. The same build.xml ran on Solaris, Windows NT, and Linux without modification. This was genuinely valuable in a mixed-OS team. Ant's built-in tasks (<javac>, <jar>, <copy>, <junit>) covered 90% of build needs.

Wrong: Ant had no understanding of dependencies between projects. In our three-module build, changing nms-snmp required remembering to rebuild nms-core afterwards. Maven fixed this in 2004 with a dependency management model. Ant builds also became large XML documents that were hard to navigate and refactor.

The other fundamental problem: Ant was imperative. You described how to build. Maven and Gradle shifted to declarative models where you describe what to build and the tool works out the how. That proved to be a better abstraction.