Ant is still the most common build scripting tool in the Java world. Commercial IDE and application vendors have adopted Ant and provide many extensions for their tools that can help with scriptable deployment. Newer approaches to scripting deployments (such as Maven, Buildr and Gradle) build on the rich ecosystem of ant tasks that already exist. As a result of this legacy, we will spend some time discussing Ant. However that isn’t to say we recommend using Ant – you may prefer a more powerful tool like Buildr, given a choice.
The following code example (thanks to Julian Simpson for helping out with this) shows an example Ant build script for the commit stage. This script can be run immediately before committing any change to the version control system, and as the first stage in the deployment pipeline by your CI server. The structure embodied in this collection of build targets and their dependencies is sufficient for even very large complex projects. The only difference between the use of this script in a single module project and a complex enterprise system build is the length of the list of components defined at various points in the script.
By default if you simply type ant at the command line, this script will execute the commit-build target. This will clean all output directories, ensuring that the build is not compromised by any leftover artifacts from a previous build. It will then perform code analysis, which is often composed of several steps. As a minimum, the code analysis will include some static analysis to enforce code style and consistency. This target can be implemented with a suitable third-party software such as Lint, CheckStyle, FindBugs, jsLint and so on, depending on the technology of our build. If the build fails compilation or code analysis it exits immediately, recording the failure.
An important strategy for keeping our builds easy to maintain is to do any real work in macros or targets that hide the detail of the task. This is good software design, but is often not done in build scripts that (for some reason) are often allowed to degenerate into tangled, badly written, monolithic blocks of code. In our example targets like junit-and-coverage, assemble-jar, assemble-webapp, and compile-module represent such lower-level macros. In our goal to achieve tidy, maintainable, re-usable modular code in our build scripts we also separate out all paths. This starts out as simply being tidy, but quickly means that you can reuse modules on different scenarios. So we can use the compile-module macro to compile production code, test code and acceptance test code by changing the source path, the classpath and the output directory. This is obvious to any software developer, except that most of the build files that we have seen don’t do it and so are complex, difficult to maintain, and fragile.
<?xml version="1.0" encoding="utf-8"?> <project name="demo" basedir="." default="commit-build"> <property name="test.results.dir" location="target/reports"/> <property name="project.dir" location="." /> <target name="-init"> <mkdir dir="target"/> </target> <path id="main.classpath"> <fileset dir="lib/build" includes="*.jar"/> </path> <path id="test.classpath"> <fileset dir="lib/test" includes="*.jar"/> </path> <target name="commit-build" depends="clean, code-analysis, unit-test, package" description ="Performs a full clean build of the whole project and runs all commit tests"/> <target name="clean" description="Clears all output directories so that build cannot be broken by code from previous builds"> <delete dir="target" includeemptydirs="true"/> </target> <macrodef name="build-component"> <attribute name="part"/> <sequential> <mkdir dir="target/@{part}-classes"/> <javac srcdir="${project.dir}/src/@{part}/java" destdir="target/@{part}-classes"> <classpath refid="@{part}.classpath"/> </javac> <copy todir="target/@{part}-classes" description="copy configuration"> <filterset> <filter token="ENV" value="${environment}" description="Replace token ENV in configuration files with value of property environment"/> </filterset> <fileset dir="src/@{part}/config/"/> </copy> </sequential> </macrodef> <target name="compile" depends="-init"> <build-component part="main"/> </target> <target name="compile-tests" depends="-init"> <build-component part="test"/> </target> <target name="unit-test" depends="compile, compile-tests"> <junit failureproperty="test.failure"> <classpath> <path refid="test.classpath"/> <pathelement path="target/classes"/> </classpath> <batchtest todir="${test.results.dir}"> <fileset dir="${test.results.dir}" includes="target/test-classes/**/*Test.class" excludes = "**/acceptance/**/*" /> </batchtest> </junit> </target> <target name="package" depends="compile"> <jar jarfile="target/demo.jar" basedir="target/main-classes"/> <apply executable="md5sum" output="target/md5sums.txt"> <fileset dir="target" includes="*.jar"/> </apply> </target> <target name="code-analysis" depends="compile" description="Performs mandatory static analysis on the project."> <!--this will depend on your code analysis tools--> </target> </project>
Although the preceding example is an Ant script, the strategy, the overall design principles, and even the dependencies between the tasks remain valid whatever scripting language you use. That is because this script models the process of the commit stage, rather than simply acting as a place-holder for a collection of tasks.
We have similar scripts for each stage in our development pipeline. Each one focussed on the tasks specific to that stage in the deployment pipeline. We also separate tasks that are specific to our CI environment from the core scripts that do the actual work. Whatever CI system you use will run the appropriate script for that stage. For example, the script that we have shown above is triggered in response to a commit to the version control system. You want to use the same script within your CI environment that you use locally on your machine. However when running on a CI environment, you want your CI server to save test reports and binaries and make them available to users, and to later stages in your build process. Modern CI tools can be configured to do this for you, including tasks like saving assembled artifacts to a binary repository.
Most stages in the deployment pipeline that follow a successful commit stage depend upon the application being deployed. It is vital that this deployment is automated too. Although this is not an explicit deployment pipeline stage, we usually create a separate script for deployment that is used by the stage-level scripts. This script needs to be flexible, in that it needs to be capable of deploying the application successfully into a variety of different environments, from development to production. This flexibility is important because it means that releases will use exactly the same script that you have tested countless times on previous deployments to other environments.
The deployment script should cover the case of upgrading your application as well as installing it from scratch. That means, for example, that it should shut down previously running versions of the application before deploying, and it should be able to create any database from scratch as well as upgrading an existing one.


