Before Ant, building and deploying Java applications required a hodgepodge of platform-specific scripts, makefiles, proprietary IDEs, or manual processes. Now, nearly every open source Java project uses Ant. A great number of companies use Ant for internal projects as well. The widespread use of Ant in these projects has naturally led to an increased need for a set of well-established best practices.
This article summarizes several of my favorite Ant tips or best practices. Many were inspired by mistakes made on previous projects, or from horror stories relayed to me from other developers. One person told me of a project where XDoclet-generated code was placed into a version- control tool that locks files. When a developer changes a source file, they must remember to manually check out and lock all of the files that will be regenerated. They must then manually run the code generator, and only then can they tell Ant to compile the code. Here are some problems with this approach:
- Generated code should not be stored in version control.
- Ant (or XDoclet, in this case) should automatically determine which files will be affected by the next build. Programmers should not have to figure this out manually.
- The Ant buildfile should define correct target dependencies so that programmers do not have to invoke targets in a particular order in order to get a good build.
Related Reading Java Extreme Programming Cookbook |
When I start any new project, I begin by creating the Ant buildfile. Ant defines the build process and is used by every programmer on the team throughout the day. All of the tips in this article assume the Ant buildfile is an important artifact that must be written with care, maintained in version control, and refactored periodically. So here now are my top 15 Ant best practices.
1. Adopt Consistent Style Conventions
Ant users either love the XML buildfile syntax or hate it. Rather than jump into the middle of this fascinating debate, let's look at a few simple ways to keep the XML buildfile clean.
First and foremost, spend time formatting your XML so it looks visually appealing. Ant works with ugly or pretty XML, but ugly XML is hard to read. Provided you leave a blank line between targets, indent consistently, and avoid exceeding 90 or so columns of text, XML is surprisingly readable. Throw in a good editor or IDE that syntax highlights the XML, and you should not have any trouble getting by.
Also pick meaningful, human-readable names for targets and properties. For example, dir.reports is a better name than rpts. The specific naming convention is not important -- just come up with something and stick to it.
2. Put build.xml in the Project Root Directory
The Ant buildfile can reside anywhere, but putting build.xml in the top-level project directory keeps things simple and clean. This is the most common convention, and programmers expect to find build.xml in this location. Having the buildfile at the top directory also makes it conceptually easy to see how relative paths point to different directories in your project tree. Here is a typical project layout:
[root dir]
| build.xml
+--src
+--lib (contains 3rd party JARs)
+--build (generated by the build)
+--dist (generated by the build)
When build.xml is in the top directory, you can compile code from the command line without changing your working directory, provided you are somewhere within the project directory tree. Just type this: ant -find compile
. The -find
argument tells Ant to search ancestor directories until it locates the buildfile.
3. Prefer a Single Buildfile
Some people prefer to break up large projects into several small buildfiles, each of which is responsible for a small portion of the overall build. This is strictly a matter of opinion, but beware that breaking up the build often makes it harder to wrap your head around the overall process. Be careful not to over-engineer a clever hierarchy of buildfiles when a single file can usually do the job.
Even if your project is divided into many different buildfiles, programmers expect to find a master build.xml in the project root directory. Make sure this buildfile is available, even if it only delegates actual work to subordinate builds.
4. Provide Good Help
Strive to make the buildfile self-documenting. Adding target descriptions is the easiest way to accomplish this. When you type ant -projecthelp
, you see a listing of each target containing a description. For example, you might define a target like this:
<target name="compile"
description="Compiles code, output goes to the build dir.">
The simple rule is to include descriptions for all of the targets you wish programmers to invoke from the command line. Internal targets should not include description attributes. Internal targets may include targets that perform intermediate processing, such as generating code or creating output directories.
Another way to provide help is to include XML comments in the buildfile. Or, define a target named help
that prints detailed usage information when programmers type ant help
.
<target name="help"
description="Display detailed usage information">
<echo>Detailed help...</echo>
</target>
5. Provide a Clean Target
Every buildfile should include a target that removes all generated files and directories, bringing everything back to its original, pristine state. All files remaining after a clean should be those found in version control. For example:
<target name="clean"
description="Destroys all generated files and dirs.">
<delete dir="${dir.build}"/>
<delete dir="${dir.dist}"/>
</target>
Do not automatically invoke clean
, unless perhaps you have some special target for generating a full release. When programmers are merely compiling or performing other tasks, they do not want the buildfile to perform a full cleanup before proceeding. This is both annoying and counterproductive. Trust programmers to decide when they are ready to clean all files.
6. Manage Dependencies Using Ant
Suppose your application consists of a Swing GUI, a web interface, an EJB tier, and shared utility code. In large systems, you need to clearly define which Java packages belong to which layer of the system. Otherwise, you end up being forced to compile hundreds or thousands of files each time you change something. Poor dependency management leads to overly complex, brittle systems. Changing the layout of a GUI panel should not cause you to recompile your servlets and EJBs.
As systems get bigger, it is easy to inadvertently introduce server-side code that depends on client-side code, or vice versa. This is because the typical IDE project compiles everything using a monolithic classpath. Ant lets you control the build more effectively.
Design your Ant buildfile to compile large projects in stages. First, compile shared utility code. Place the results into a JAR file. Then, compile a higher level portion of the project. When you compile higher-level code, compile against the JAR file(s) created in the first step. Repeat this process until you reach the highest level of the system.
Building in stages enforces dependency management. If you are working on a low-level framework Java class and accidentally refer to a higher-level GUI panel, the code will not compile. This is due to the fact that when the buildfile compiles the low-level framework, it does not include the high-level GUI panel code in the source path.
7. Define and Reuse Paths
A buildfile is often easier to understand if paths are defined once in a central location, and then reused throughout the buildfile. Here is an example that shows this in action.
<project name="sample" default="compile" basedir=".">
<path id="classpath.common">
<pathelement location="${jdom.jar.withpath}"/>
...etc
</path>
<path id="classpath.client">
<pathelement location="${guistuff.jar.withpath}"/>
<pathelement location="${another.jar.withpath}"/>
<!-- reuse the common classpath -->
<path refid="classpath.common"/>
</path>
<target name="compile.common" depends="prepare">
<javac destdir="${dir.build}" srcdir="${dir.src}">
<classpath refid="classpath.common"/>
<include name="com/oreilly/common/**"/>
</javac>
</target>
</project>
Techniques like this always gain more value as the project grows and the build gets progressively more complex. You will probably have to define different paths to compile each tier of the application, as well as paths to run unit tests, run the application, run XDoclet, generate JavaDocs, etc. This modular path approach is preferable to a gigantic path for everything. Failure to modularize makes it easy to lose track of dependencies.
8. Define Proper Target Dependencies
Suppose the dist
target depends on the jar
target, which depends on compile
, which depends on prepare
. Ant buildfiles ultimately define a dependency graph, which must be carefully defined and maintained.
Periodically review the dependencies to ensure that your builds do the right amount of work. Large buildfiles tend to degrade over time as more targets are added, so you end up with unnecessary dependencies that cause your builds to work too hard. For example, you might find yourself regenerating the EJB code when the programmer actually only wanted to compile some GUI code that does not use EJB at all.
Omitting dependencies in an effort to "optimize" the build is another common mistake. This is a mistake because it forces programmers to remember to invoke a series of targets in a particular order in order to get a decent build. A better solution exists: provide public targets (those with descriptions) that contain correct dependencies, and another set of "expert" targets that let you manually execute individual build steps. These steps do not guarantee a complete build, but let expert users bypass steps during quick and dirty coding sessions.
9. Use Properties for Configurability
Any piece of information that needs to be configured, or that might change, should be defined as an Ant property. The same is true for values that are used in more than one place in the buildfile. Properties should be defined either at the top of a buildfile or in a standalone properties file for maximum flexibility. Here is how properties look when defined in the buildfile:
<project name="sample" default="compile" basedir=".">
<property name="dir.build" value="build"/>
<property name="dir.src" value="src"/>
<property name="jdom.home" value="../java-tools/jdom-b8"/>
<property name="jdom.jar" value="jdom.jar"/>
<property name="jdom.jar.withpath"
value="${jdom.home}/build/${jdom.jar}"/>
etc...
</project>
Or, you can use a properties file:
<project name="sample" default="compile" basedir=".">
<property file="sample.properties"/>
etc...
</project>
And in sample.properties:
dir.build=build
dir.src=src
jdom.home=../java-tools/jdom-b8
jdom.jar=jdom.jar
jdom.jar.withpath=${jdom.home}/build/${jdom.jar}
Having a separate file for properties is beneficial because it explicitly defines the configurable portion of the build. You can provide a different version of this properties file for different platforms, or for developers working on different operating systems.
10. Keep the Build Process Self-Contained
To the greatest extent possible, do not refer to external paths and libraries. Above all, do not rely on the programmer's CLASSPATH
setting. Instead, use relative paths throughout your buildfile and define your own paths. If you refer to an explicit path such as C:\java\tools, other developers will not be able to use your buildfile because they are highly unlikely to use the same directory structure.
If you are deploying an open source project, provide a distribution that includes all JAR files necessary to compile your code, subject to licensing restrictions, of course. For internal projects, dependent JAR files should be managed under version control and checked out to a well-known location.
When you do have to refer to external paths, define the paths as properties. This lets programmers override those settings to conform to their own machines. You can also refer to environment variables using this syntax:
<property environment="env"/>
<property name="dir.jboss" value="${env.JBOSS_HOME}"/>
Related Reading |
11. Use Version Control
The buildfile is an important artifact that should be versioned, just like source code. When you tag or label your code, apply the same tag or label to your buildfile. This lets you go back to a previous release and build the software using the buildfile as it was back then.
In addition to the buildfile, you should maintain third-party JAR files in version control. Again, this makes it possible to recreate previous releases of your software. This also makes it easier to ensure that all developers have the same JAR files, because they can check them out of version control to a path relative to the buildfile.
Generally, avoid storing build output in version control. Provided that your source code is versioned properly, you should be able to recreate any previous release through the build process.
12. Use Ant as the Least Common Denominator
Suppose your team uses an IDE. Why bother with Ant when programmers can click the lightning bolt icon to rebuild the whole application?
The problem with IDEs is one of consistency and reproducibility across a team of members. IDEs are almost always designed for individual programmer productivity, not for consistent builds across a team of developers. Typical IDEs require each programmer to define his or her own project file. Programmers may have different directory structures, may use different versions of various libraries, or may be working on different platforms. This leads to situations where code that compiles fine for Bob may not build properly for Sally.
Regardless of what IDE your team uses, set up an Ant buildfile that all programmers use. Make a rule that programmers perform an Ant build before checking new code into version control. This ensures that code is always built from the same Ant buildfile. When problems arise, perform a clean build using the project-standard Ant buildfile, not someone's particular IDE.
Programmers should be free to use whatever IDE or editor they are comfortable with. Use Ant as a common baseline to ensure the code is always buildable.
13. Use zipfileset
People often use Ant to create WAR, JAR, ZIP, and EAR files. These files generally require a particular internal directory structure, one that usually does not match the directory structure of your source code and build environment.
An extremely common practice is to write an Ant target that copies a bunch of files to a temporary holding area using the desired directory structure, and then create the archive from there. This is not the most efficient approach. Using a zipfileset
is a better solution. This lets you select files from any location and place them in the archive file using a different directory structure. Here is a small example:
<ear earfile="${dir.dist.server}/payroll.ear"
appxml="${dir.resources}/application.xml">
<fileset dir="${dir.build}" includes="commonServer.jar"/>
<fileset dir="${dir.build}">
<include name="payroll-ejb.jar"/>
</fileset>
<zipfileset dir="${dir.build}" prefix="lib">
<include name="hr.jar"/>
<include name="billing.jar"/>
</zipfileset>
<fileset dir=".">
<include name="lib/jdom.jar"/>
<include name="lib/log4j.jar"/>
<include name="lib/ojdbc14.jar"/>
</fileset>
<zipfileset dir="${dir.generated.src}" prefix="META-INF">
<include name="jboss-app.xml"/>
</zipfileset>
</ear>
In this example, all JAR files are placed in the lib directory of the EAR. The hr.jar
and billing.jar files are copied from our build directory, therefore we use zipfileset
to move them to the lib directory inside of the EAR. The prefix attribute specifies the destination directory in the EAR.
14. Perform the Clean Build Test
Assuming your buildfile has clean
and compile
targets, perform the following test. First, type ant clean
. Second, type ant compile
. Third, type ant compile
again. The third step should do absolutely nothing. If files compile a second time, something is wrong with your buildfile.
A buildfile should perform work only when input files change with respect to corresponding output files. A build process that compiles, copies, or performs some other work when it is not necessary to perform the work is inefficient. Even small inefficiencies become big problems when a project grows in size.
15. Avoid Platform-Specific Ant Wrappers
For whatever reason, some people like to ship their products with simple batch files or scripts called something like compile. Look inside of the script and you will find the following:
ant compile
Developers are familiar with Ant and are perfectly capable of typing ant compile
. Do not include a platform-specific script that does nothing but invoke Ant. Your script becomes one more little annoyance for people to study and understand when they are looking at your tool for the first time. It is also likely that you will fail to provide scripts for every operating system out there, which will really annoy some users.
Summary
Too many companies rely on manual processes and ad hoc procedures for compiling code and creating software distributions. Teams that do not use a defined build process with Ant or similar tools spend an amazing amount of time tracking down problems when code compiles for some developers but fails for others.
Creating and maintaining build scripts is not glamorous work, but is necessary. A well-crafted Ant buildfile lets you focus on what you enjoy most -- writing code!
References
- Ant
- AntGraph: A tool for visualizing Ant dependencies
- Ant: The Definitive Guide, O'Reilly
- Java Extreme Programming Cookbook, O'Reilly