|
Top 15 Ant Best Practices
by Eric M. Burke, coauthor of Java Extreme Programming Cookbook
12/17/2003
|
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.
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}"/>
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
Eric M. Burke
is an O'Reilly author and a principal
software engineer with Object Computing, Inc. in St.
Louis, MO.
O'Reilly & Associates published Java Extreme Programming Cookbook in March 2003.
Chapter 4, "JUnit," is available free online.
You can also look at the Table of Contents, the
Index, and the full description of
the book.
For more information, or to order the book,
click here.
Return to ONJava.com.