I wrote my first build script in my second year of college. It was a shell script that compiled a few Java files and copied the output into a folder. It worked. For that project, at least.

Three years later, working on a team project with five other developers, I realised that script was useless. It assumed a specific directory layout. It assumed a specific version of the JDK. It assumed nobody had moved the source files. It assumed the build machine had the same libraries installed as my laptop. It assumed a lot of things, and every assumption was a potential failure point.

So I rewrote it. And then I rewrote it again. And each time, I learned something about what makes a build script actually useful rather than just technically correct.

Clean first, always

The first rule is simple: your build should start by wiping the previous output. If you do not do this, you will occasionally get a build that looks successful but contains classes from the last build mixed in with the new ones. The symptoms are strange — a method you deleted suddenly works again, a class that should not exist is being loaded, errors that make no sense until you clean and rebuild.

In Ant, this is one target:

<target name="clean">
    <delete dir="${build.dir}"/>
    <delete dir="${dist.dir}"/>
</target>

Make it the default dependency. Every build target should depend on clean, or at least on a target that depends on clean. It costs two seconds. It saves twenty minutes of debugging.

Make it run anywhere

If your build script only works on your machine, it is not a build script. It is a personal ritual.

The moment a second developer joins the project, you need a shared build process. The moment a build server enters the picture, you need it to be automated and repeatable. The moment someone switches operating systems, you need it to be portable.

This is the main reason to use Ant over raw shell scripts. Ant handles the platform differences for you — file separators, path conventions, command variations — so you write your build once and it works everywhere. If you are stuck with shell scripts, at least use variables for paths and JDK locations so you can adjust them per machine without rewriting the whole script.

Fetch before you build

A build should pull the latest code from version control before it starts compiling. If it does not, you are building against stale source, and the result is meaningless.

In Ant, you can integrate this with the Subversion task:

<target name="update">
    <svn username="${svn.user}" password="${svn.password}">
        <checkout url="${svn.url}" destPath="${src.dir}"/>
    </svn>
</target>

Or if you prefer to keep source and build separate, use svn update on your source directory as a target that compile depends on. Either way, the build should never succeed against old code.

Give people options

Not every build is the same. Sometimes you just want to compile. Sometimes you need to deploy only the changed files. Sometimes you need to restart the server without rebuilding anything because you changed a configuration file, not the code.

A good build script gives you targets for each of these scenarios:

<target name="deploy-html" depends="compile">
    <copy todir="${deploy.dir}/web">
        <fileset dir="${src.dir}/web"/>
    </copy>
</target>

<target name="deploy-classes" depends="compile">
    <copy todir="${deploy.dir}/WEB-INF/classes">
        <fileset dir="${build.dir}"/>
    </copy>
</target>

<target name="deploy-config" depends="compile">
    <copy todir="${deploy.dir}/WEB-INF/classes">
        <fileset dir="${src.dir}/config"/>
    </copy>
</target>

<target name="restart-server">
    <exec executable="${server.restart.script}"/>
</target>

Now a developer can run ant deploy-config and restart the server without waiting for a full rebuild. That is not a luxury — it is a daily time saver.

Automate the server restart

This one is small but worth calling out separately. If your build deploys to a local or remote application server, and that server needs a restart to pick up changes, automate it. Do not make the developer remember to stop and start the server manually.

The exec task in Ant can run any shell command, so you can restart Tomcat, JBoss, or whatever you are using directly from the build. On a local machine it might be as simple as running a shell script. On a remote server, you can use SSH through Ant’s <sshexec> task.

Check for stale references

One of the most annoying problems in a team project is when someone deletes a class or renames a method, but other files still reference the old name. The code compiles on their machine because they have the old file cached somewhere, but the build fails for everyone else.

You can catch this by enabling strict compilation flags. In Ant, set failonerror="true" and use the -Xlint flag with javac:

<target name="compile" depends="clean, update">
    <mkdir dir="${build.dir}"/>
    <javac srcdir="${src.dir}" destdir="${build.dir}"
           failonerror="true"
           deprecation="true">
        <compilerarg value="-Xlint:all"/>
    </javac>
</target>

This will flag unused imports, deprecated APIs, unchecked casts, and anything else the compiler can detect. It forces the team to keep the code clean, and it turns the build script into a quality gate rather than just a compilation tool.

Keep it fast

A build that takes five minutes will get ignored. A build that takes thirty seconds will get run before every commit.

There are ways to speed things up. Incremental compilation — only recompiling files that have changed — is one, though it introduces the stale-class problem I mentioned earlier, so use it carefully. Another is parallel compilation. Ant supports running multiple tasks in parallel with the parallel task, which can help if you have separate compilation steps for different modules.

But the biggest speed gain comes from not doing unnecessary work. If a target does not need to run, skip it. Use the unless and if attributes to conditionally execute targets based on properties:

<target name="full-build" depends="compile, test, package"/>

<target name="quick-build" depends="compile"
        if="skip.tests"/>

The script is a living thing

Your build script will change. The project structure will change. You will add new frameworks, new tests, new deployment targets. The script that worked for a ten-person project will not work for a hundred. The script that worked for a local deployment will not work for a multi-server setup.

That is fine. The point is not to write a perfect build script on the first try. The point is to write one that works today, make it portable enough that it can work tomorrow, and keep improving it as the project grows.

The best build scripts are the ones nobody notices. You do not think about them until they break, and when they do break, you know exactly where to look because you wrote them and you understand them.