dcsimg

Building an Enterprise with an Ant

The title of this column, Java Matters, has a dual meaning. In one sense, it's about the technical matters that concern Java when trying to use it under Linux. The other way of looking at it is to acknowledge that Java and Linux are intertwined such that whatever happens in one community is reflected in the other -- hence Java matters to Linux, and vice versa. This column represents an attempt to bridge the gap between Java and Linux by providing practices for using them together.

The title of this column, Java Matters, has a dual meaning. In one sense, it’s about the technical matters that concern Java when trying to use it under Linux. The other way of looking at it is to acknowledge that Java and Linux are intertwined such that whatever happens in one community is reflected in the other — hence Java matters to Linux, and vice versa. This column represents an attempt to bridge the gap between Java and Linux by providing practices for using them together.

Let’s start by talking about build tools, because every Java project needs one in order to create libraries and applications from source code. On Unix, the traditional build tool has been make, but because the commands that it calls might not be available on other operating systems, it is essentially non-portable.

The new kid on the block is called Ant, a sub-project of the Jakarta Project (http://www.jakarta.apache.org). The goal of this project is to provide commercial-quality server solutions that are based on Java.

Ant is a build system based on both Java and XML that’s, “…kind of like make without make‘s wrinkles,” as it is described on the Web site. Originally designed and built by James Duncan Davidson for building the Tomcat servlet/JSP engine, Ant has grown in recent years to be the build tool of choice for many Java projects.

Ant is controlled through the use of XML tags. These tags let you define targets, which are either things to be built (such as a library or application) or tasks to perform (such as building documentation or cleaning up). They also let you specify dependencies (library X consists of files A, B, and C; therefore, if file A changes, then X must be rebuilt).

The tags are processed by an XML parser included with Ant that will make calls to Java methods to perform whatever task needs to be performed. If Ant can’t do whatever it is you need done, you can extend it by adding Java classes and creating new XML tags to access them. This abstraction of tasks away from specific commands into XML tags and Java classes means Ant is not limited to any one operating system. Any system with a JVM can run Ant.

Ant works best with a JDK conforming to the Java 2 Platform, Standard Edition (J2SE), although it will run on a 1.1.x version of the JDK. You can find the Java 2 Platform reference implementations at http://java.sun.com/java2 and the latest release of Ant at http://jakarta.apache.org/ant/. As of this writing, Ant version 1.4.1 had just been released.

The documentation resides in the docs/ subdirectory on the Web site and includes a comprehensive user’s guide and links to various other Ant user resources. The rest of this column assumes you have a passing familiarity with Ant.

By the way, you should know what properties, patternsets, filesets, classpaths, and tasks are. (All of these topics are fully covered in the Ant documentation.) Knowing the basics will help you understand the different strategies for building a project that will be discussed later on.

Managing the Beast

When starting a new project, the first step should be to design a workable directory structure (see the Repository Layout Conventions sidebar). You would then write your code along with its test cases, compile the code, compile and run the tests, jar the compiled source, create javadocs for it, and finally, create a distribution.

If we examine all the commands executed in a single step (for instance, the compile step), we’d see that the only difference in each command is the name of the file being compiled. Therefore, we’ll create a generic build target. For every file we want to compile, we’ll “call” the target with the file name. We’ll do this for many other targets as well.

To provide a concrete example, let’s assume we work for “Your Company Here, Inc.” We’re just starting work on the “New Killer Product.” Our build file will not only implement the idea of generic targets, but we’ll also see to it that we can use this same paradigm to help us build the “Second Killer Product.”

The Sample Repository

The “Your Company Here, Inc.” repository would include the following directories and files:

  • At the root of the repository are the build.xml, common.properties, and nkp.properties files.
  • A src directory to hold all the module source code — it has three subdirectories: etc to hold configuration files, lib to hold third party libraries, and java for the Java source code.
  • A test directory to hold all the module test source code — it has three subdirectories: etc to hold configuration files, lib to hold third party libraries, and java for the test source code.
  • A docs directory to hold all the module documentation: Currently, it has only one subdirectory, nkp, for the “New Killer Product” module, with a README and RELEASE-NOTES file.

The build.xml and .properties Files

Lets begin with a quick look at how the build.xml file is set up. It assumes that you’re creating multiple modules from a single source base and that each of these modules has a short meaningful code name. This code name is used to identify targets that build the module and to name directories during the build processes. If your repository does not follow this pattern, you can still use the ideas presented here for certain repetitive tasks.

Only certain, relevant portions of the sample build.xml file will be shown for space reasons. You can download it as part of the complete repository for “Your Company Here” from http://www.linux-mag.com/downloads/ ant/enterprise_ant.tar.gz/.

At the start of the file (see Listing One, ), we define the name of our project and set the default target to help, which provides a basic help message. Typically, you don’t want to do anything potentially destructive by default, so it’s a good idea to make sure the user knows which target they want to execute.




Listing One: Initial Definitions


<project name=”enterprise_builds” default=”help”>
<target name=”help” description=”–> Build instructions.”>
<echo><![CDATA[
Usage: ant <target>

Use "ant -projecthelp" to get a list of available targets.

The following table lists the products that can be created from this build file.

Product Code Product Name
------------ ------------
nkp New Killer Product
]]>
</echo>
</target>

<!–
===================================================================
Set up miscellaneous global properties related to *all* builds
===================================================================
–>
<property name=”vendor” value=”Your Company Here, Inc.” />
<!–
===================================================================
Set up global properties related to the source tree
===================================================================
–>
<property name=”src.dir” value=”src” />
<property name=”src.java” value=”${src.dir}/java” />
<property name=”src.lib” value=”${src.dir}/lib” />
<property name=”src.etc” value=”${src.dir}/etc” />
<property name=”src.docs” value=”docs” />
<!–
===================================================================
Set up global properties related to the build area
===================================================================
–>
<property name=”build.base” value=”build” />
<!–
===================================================================
Set up global properties related to the distribution area
===================================================================
–>
<property name=”dist.base” value=”dist” />
<!–
===================================================================
Set up global properties related to the test area
===================================================================
–>
<property name=”test.src.dir” value=”test” />
<property name=”test.src.java” value=”${test.src.dir}/java” />
<property name=”test.src.etc” value=”${test.src.dir}/etc” />
<property name=”test.src.lib” value=”${test.src.dir}/lib” />

<!–
Rest of the file goes here
–>
</project>

The help message instructs the user to use ant -projecthelp to get a list of valid targets. Next is a number of global properties that are the same for every module build. This includes where the source, test, build, distribution, and documentation directories are located; these are all based on the suggested repository layout.

We will now need to skip to the prepare-nkp target in Listing Two. This is the point where we set up the remaining build properties by loading the module’s .properties file (in this case nkp.properties) and the common. properties file.




Listing Two: Preparing for the Building


<target name=”prepare-nkp”>
<tstamp>
<format property=”year” pattern=”yyyy” />
</tstamp>

<property file=”nkp.properties” />
<property file=”common.properties” />
<patternset id=”required_jars”>
<exclude name=”**” /> <!– we don’t want anything yet… –>
</patternset>

<patternset id=”required_test_jars”>
<exclude name=”**” />
</patternset>

<patternset id=”packages”>
<include name=”com/ych/nkp/*” />
<include name=”com/ych/nkp/actions/*” />
<exclude name=”**/*.swp” /> <!– Exclude GVIM .swp files –>
</patternset>

<patternset id=”test_packages”>
<include name=”com/ych/nkp/*” />
<include name=”com/ych/nkp/actions/*” />
<exclude name=”**/*.swp” /> <!– Exclude GVIM .swp files –>
</patternset>

<property name=”javadoc.packages” value=”com.ych.nkp.*” />

<path id=”classpath”>
<fileset dir=”${src.lib}”>
<patternset refid=”required_jars” />
</fileset>
</path>

<path id=”test_classpath”>
<fileset dir=”${test.src.lib}”>
<patternset refid=”required_test_jars” />
</fileset>
</path>
</target>

The nkp.properties file defines things like the module name, its full name, the module version, and some pertinent compiler options. The common. properties file defines a number of directory trees based on the module name defined in nkp.properties, including the module build, test, and distribution directories.

There are a number of patternsets and paths that must be defined in this target, as shown in Table One.




Table One: Required Elements in a Pre-module Target


ElementIDElementTypeDescription
required_jarspatternsetWhat third-party jars, relative to the src.lib directory, are required to build this module
required_test_jarspatternsetWhat third-party jars, relative to the test. src.lib directory, are required to build the test code
packagespatternsetThe packages and individual classes that make up the module
test_packagespatternsetThe packages and individual classes that make up the module test code
classpathpathThe classpath used to build the module source, derived from the required_jars patternset
test_classpath pathThe classpath used to build the module test source, derived from the test_packages patternset

Although what we’ve had to set up so far seems formidable, the payoff comes when we examine the build-nkp target. It’s as simple as what you can see in Figure One.




Figure One: The build-nkp Target


target name=”build-nkp” depends=”prepare-nkp,build”
description=”–> Compile the NKP source into the build directory.”>
</target>

The build-nkp target has an empty body because all of the work is performed by its dependent targets, prepare-nkp and build. We’ve already looked at prepare-nkp, so let’s examine the build target and its dependency, the prepare_src target, which are shown in Listing Three.




Listing Three: Reusable Prepare and Build Targets


<target name=”prepare_src”>
<mkdir dir=”${build.src}” />

<filter token=”VERSION” value=”${version}” />
<filter token=”NAME” value=”${Name}” />

<copy toDir=”${build.src}” includeEmptyDirs=”no” filtering=”on”>
<fileset dir=”${src.java}”>
<patternset refid=”packages” />
</fileset>
</copy>
</target>

<target name=”build” depends=”prepare_src”>
<mkdir dir=”${build.dir}” />
<mkdir dir=”${build.classes}” />
<mkdir dir=”${build.lib}” />

<javac srcdir=”${build.src}”
destdir=”${build.classes}”
debug=”${debug}”
deprecation=”${deprecation}”
optimize=”${optimize}”>
<classpath refid=”classpath” />
<patternset refid=”packages” />
</javac>

<filter token=”VENDOR” value=”${vendor}” />
<filter token=”VERSION” value=”${version}” />
<filter token=”DATE” value=”${TODAY}” />
<filter token=”TIME” value=”${TSTAMP}” />
<copy todir=”${build.etc}”
filtering=”on”>
<fileset dir=”${src.etc}”>
<include name=”**/${manifest}” />
</fileset>
</copy>
</target>

The prepare_src target copies source code from the src.java directory to the build.src directory based on the packages patternset, filtering for the @NAME@ and @VERSION@ tokens. Not only does this ensure we only compile the bare minimum of code required for the module and help reduce the size of the final jar, it also helps to track down obsolete references to classes in your source.

Once the prepare_src target completes, the build target gets to work. It does the actual work of compiling the module source based on the parameters, the classpath path element, and the packages patternset we set up in the prepare-nkp target.

It’s Not That Hard

There are a lot more targets defined in the build.xml file that aren’t covered here. The important thing to notice is that all the targets with nkp in their name can be reused for the “Second Killer Product.”

To sum up, if you want to build a new module, you need only to perform the following tasks:

  • First, create a new file, which you will name moduleid.properties,
  • Then add a prepare-moduleid target that defines the patternsets and paths as described above,
  • Then add a build-moduleid target that depends on prepare-moduleid and build,
  • Finally, add any remaining module-specific targets, following the pattern that the nkp module uses.

That’s all there is to it. The rest of the build process, including the creation of jars, javadocs, and tar.gz distribution files, follows the same model. This is a good build strategy that you may find to be useful in many of your own projects. Until next time, have fun using Ant.




Repository Layout Conventions

Getting a relatively large group of people to agree on a directory structure for a source repository can be more than a little difficult. As a matter of fact, it can be downright deadly. Builds and build directory structures are the kinds of things that flame wars are made of. However, if you try hard enough, you can probably get everyone to agree on a couple of key points:

  1. You need a directory for your source code. In the vast majority of cases, this is named src.
  2. You need a directory to compile your source into. It’s usually considered bad form to compile your class files in the same directories as your .java files. This could be named build, work, or classes; pick one and stick with it.
  3. You need a directory to create your module or project images. Again, it’s usually considered bad form to create your .jar files in the same directories as your .class files. This is often named dist, image, or proto.

No matter what your layout is, keep it logical and consistent. Be sure to document it in a top-level README — don’t leave your developers guessing as to where their class files will end up.



Glenn McAllister is a part-time committer on the Jakarta Ant project. He can be reached at glenn@somanetworks.com.

Comments are closed.