Ant Best Practices: The Clean Test

I'll tell you what should happen. Nothing. Diddly. Well, no compilation. If no compilation happened, should it bother packaging up artefacts or anything? I don't think so. If you want to run tests, so be it; you might have an unreliable test. But for the most part, nothing should happen if nothing changed. This is the clean test.

Why? Think of it like this. If you change a single CSS file on your project, do you want to go and recompile all your code and run your tests? Hold that thought.

On the first and largest project that I ever did at ThoughtWorks, the fast build was 45 minutes. Don't even go there with the build that did automated functional tests. One evening I was working late with one of the performance testers. The CruiseControl build was firing all the time. We realised that every time he did a performance test run, he checked in the results into the VCS alongside the code. That triggered a build. Whoops. We didn't always have thatcapacity to spare. I have since sat there on many a project and watched builds trigger because a developer had to make the tiniest change. It's dull to sit through a tedious CI build, just because the build didn't discriminate between the files that had changed.

This stuff can make a difference. When your project has grown to a larger size, rest assured that it will be hard to change these things. So how do you convince Ant to be less zealous? Start by making sure that your default target doesn't depend on your 'clean' target. When you refactor code and move classes around, you might need to execute the clean target. But treat that as the exception and not the rule.

That's fine until you start packaging up the code in zip or jar files. What happens then? The uptodate task is your friend. This little guy will take a look at the sources and output of a task, and tell you if you should bother running it. Take a look:


<project default="archive">
<property name="build.directory" location="build" />
<property name="source.directory" location="src" />
<property name="archive" location="${build.directory}/stooges.zip" />
<fileset dir="${source.directory}" includes="*.xml" id="archive.files"/>
<target name="clean">
<delete>
<fileset dir="${build.directory}" includes="**/*"/>  </delete>
</target>
<target name="check-archive">
<echo message="Checking zip file time stamp" />
<uptodate property="-archive.is.unchanged" targetfile="${archive}">
<srcfiles refid="archive.files"/>
</uptodate>
</target>
<target name="archive" unless="-archive.is.unchanged" depends="check-archive">
<echo message="Making a zip file"/>
<zip file="${archive}" >
<fileset refid="archive.files" />
</zip>
</target>
</project>

The first time that you run this build, it does what you expect: makes a zipfile. The clever bit is what it does the second time you run it. It doesn't bother. Here's the first run:

Detected Java version: 1.6 in: /usr/lib/jvm/java-6-sun-1.6.0.06/jre
Detected OS: Linux
parsing buildfile /home/jsimpson/Documents/workspace/playpen/code/up2date.build.xml with URI = file:/home/jsimpson/Documents/workspace/playpen/code/up2date.build.xml
Project base dir set to: /home/jsimpson/Documents/workspace/playpen/code

[antlib:org.apache.tools.ant] Could not load definitions from resource org/apache/tools/ant/antlib.xml. It could not be found.
Build sequence for target(s) `archive' is [check-archive, archive]
Complete build sequence is [check-archive, archive, clean, ]

check-archive:
[echo] Checking zip file time stamp
[uptodate] The targetfile "/home/jsimpson/Documents/workspace/playpen/code/build/stooges.zip" does not exist.

archive:
[echo] Making a zip file
[zip] Building zip: /home/jsimpson/Documents/workspace/playpen/code/build/stooges.zip
[zip] adding entry iggy.xml
[zip] adding entry dave.xml

BUILD SUCCESSFUL
Total time: 1 second

You can tell that it was checking the lie of the land before it executed the main task. You might like the output from the second task:


Detected Java version: 1.6 in: /usr/lib/jvm/java-6-sun-1.6.0.06/jre
Detected OS: Linux
parsing buildfile /home/jsimpson/Documents/workspace/playpen/code/up2date.build.xml with URI = file:/home/jsimpson/Documents/workspace/playpen/code/up2date.build.xml
Project base dir set to: /home/jsimpson/Documents/workspace/playpen/code
[antlib:org.apache.tools.ant] Could not load definitions from resource org/apache/tools/ant/antlib.xml. It could not be found.
Build sequence for target(s) `archive' is [check-archive, archive]
Complete build sequence is [check-archive, archive, clean, ]

check-archive:
[echo] Checking zip file time stamp
[uptodate] iggy.xml omitted as /home/jsimpson/Documents/workspace/playpen/code/build/stooges.zip is up to date.
[uptodate] dave.xml omitted as /home/jsimpson/Documents/workspace/playpen/code/build/stooges.zip is up to date.
[uptodate] No sources found.
[uptodate] File "/home/jsimpson/Documents/workspace/playpen/code/build/stooges.zip" is up-to-date.

archive:
Skipped because property '-archive.is.unchanged' set.

BUILD SUCCESSFUL
Total time: 1 second

There you have it. It didn't bother creating the zip file because the sources hadn't changed. That's useful. But what about the duplication?

Admittedly you do need to define the sources and target of the task twice. This could lead to duplication. If you look at the example, you'll see how I got around that:

  • the target of the zip task is a property. So that's fine.
  • the sources of the zip task are all types of FileSet. So I defined the FileSet once and passed the reference to both tasks.

Everyone's build should undertake the Clean Test.

DevOps New Zealand