Minimal Builds with Ant


For a build tool to support the concept of minimal, or incremental, builds, it must be able to identify the dependency relationships that exist between the various project artifacts that make up the application.

Prior to Ant, most Java developers used Make tools for creating build files. Make tools employ declarative programming language semantics for defining dependency rules between build artifacts. With this approach, the Make tool infers the build tasks to perform following a change to a particular source file or component.

Declarative programming is described in Chapter 10.


The inference capabilities of the Make tool support a minimal build approach but at the expense of build-file complexity. Ant uses a simpler syntax for its build files than Make, but does not intrinsically support a declarative approach to the build process.

The Importance of Build Dependencies

To appreciate the impact of build dependencies, let's consider the example build.xml in Listing 12-1. In this example, the build file instructs Ant as to the build order of each of the targets.

Listing 12-1. Ant Example build.xml
 <project name="ant-build" default="compile">   <target name="compile"           description="Compile all Java source">     <javac srcDir="."/>   </target>   <target name="clean"           description="Removes all class files">     <delete>       <fileset dir="." includes="*.class"/>     </delete>   </target>   <target name="build"           depends="clean, compile"           description="Rebuilds all source"/> </project> 

The example build file in Listing 12-1 contains three build targets: compile, clean, and build. The compile target is set as the default for the project and is run whenever Ant is executed unless an alternative target, such as ant build, is explicitly specified.

Running the build file for the first time with the default compile target results in the compilation of all Java source in the base directory. For this example, assume we have a single HelloWorld.java file.

Running the build file a second time is distinctly quicker, as Ant determines all class files are up-to-date and no compilation is necessary. How is this possible given the build file provided no dependency information between HelloWorld.java and HelloWorld.class?

The secret lies in the <javac> task, used to compile the Java source. This task has built-in dependency rules and knows to associate *.java and *.class files. Therefore, for Java compilations, Ant supports the minimal build approach we require for fast builds. Unfortunately, Ant is not as knowledgeable about other file types. This is a problem, especially for J2EE builds that rely on many custom build steps.

Listing 12-2 revises the previous example to illustrate the problem.

Listing 12-2. Build File with Dependent Target
 <project name="ant-build" default="compile">   <target name="generate"           description="Long running build task">     <ejbdoclet>       .       .       .     </ejbdoclet>   </target>   <target name="compile"           depends="generate"           description="Compile all Java source">     <javac srcDir="."/>   </target>   <target name="clean"           description="Removes all class files">     <delete>       <fileset dir="." includes="*.class"/>     </delete>   </target>   <target name="build"           depends="clean, compile"           description="Rebuilds all source"/> </project> 

A new target, <generate>, has been added to the build. This target invokes a code generator, for example, XDoclet, which processes those files containing annotations.

Before the <compile> target can begin, the <generate> target must have completed. This dependency between the two targets is expressed using the special Ant attribute depends. With this dependency defined, Ant always runs the <generate> target ahead of <compile>.

The relationship specified between the two targets is procedural: Ant does not check file timestamps to determine if the <generate> target must be run.

Assume our example project comprised 10,000 source files, with 5,000 of them marked up with XDoclet-style annotations or attributes. Modifying an annotated file requires running XDoclet to pick up the change. However, if a file without annotations is changed, then we can safely skip the <generate> task and run only <compile>. Because the <compile> target uses the <javac> task, only the affected source file is compiled.

Tip

Virus checkers slow down builds. Turning your virus checker off noticeably speeds up the build process. Unfortunately, the threat to unprotected machines from malicious viruses means companies tend to insist all machines run virus protection software at all times.

If you can't turn off your virus checker, another option is to change the setting of the virus software so it ignores all files written to build directories. Most virus checkers are configurable to enable the exclusion of certain files and directories from the scanning process. Refer to the manual of your particular virus-protection software for more information.


Ideally, we want our code generator, be it XDoclet or some other tool, to have the same functionality as the <javac> task. If only one of our 5,000 annotated files is modified, then the code generator should process only a single file.

With this approach, the steps of the build process are as follows:

1.

Developer modifies a single annotate file.

2.

XDoclet generates new program source from only the file that has changed.

3.

<javac> task compiles only the files generated by XDoclet.

As it stands now, the <generate> target will pick up all 5,000 files, thereby resulting in the need for <javac> to undertake a significant recompilation effort.

This is a problem. Our build process does not support the concept of minimal builds, and our time-and-motion expert is far from pleased.

Using a Make tool, we could have defined build rules to instruct Make to run the code generator against modified files only. Unfortunately, Ant does not support this form of deterministic build process directly. More importantly, neither does the XDoclet Ant task.

Nevertheless, the authors of Ant recognized the importance of build dependencies and added support for this feature as a set of core tasks. We still have work to do if we are to make use of this functionality, as it is not default behavior. To understand how build dependencies can be enforced, let's leave XDoclet and consider another example.

Defining Build Dependencies in Ant

The build.xml file shown in Listing 12-3 demonstrates the use of the <uptodate> task to define a conditional build dependency between two targets. In this example, the dependency defines the need to package Java binaries into a single JAR file.

Listing 12-3. Ant Build File with Conditional Build
 <project name="depend-example" default="make"> <!-- Build file with dependency defined between      package and compile targets --> <property name="src.dir" location="src"/> <property name="bin.dir" location="bin"/> <property name="dist.dir" location="dist"/> <target name="compile"         description="Compiles all Java source">   <javac srcdir="${src.dir}"           destdir="${bin.dir}"/>   <!-- Check if files have been updated -->   <uptodate property="package.notRequired"             targetfile="${dist.dir}/app.jar">     <srcfiles dir="${bin.dir}" includes="**/*.class"/>   </uptodate> </target> <target name="package"         depends="compile"         unless="package.notRequired"         description="Produces JAR file">   <jar destfile="${dist.dir}/app.jar"         basedir="${bin.dir}"/> </target> <target name="clean"         description="Removes all class files">   <delete>     <fileset dir="${bin.dir}"/>     <fileset dir="${dist.dir}"/>   </delete> </target> <target name="make"         depends="package"         description="Incremental build"/> <target name="build"         depends="clean, make"         description="Rebuilds all source"/> </project> 

The build process shown in Listing 12-3 involves two steps. First, the compile target compiles all code in the src directory with <javac>, directing all output to the bin directory. Second, the package target collects the contents of the bin directory into a single JAR file, placing the archive in the dist directory ready for deployment.

Two top-level build targets are responsible for performing these steps:

  • The build target deletes all built files and compiles and packages the application from scratch.

  • The make target performs an incremental build and creates a distribution only if any source has been updated.

Packaging large numbers of files into JAR files is a time-consuming process, so it is of benefit to perform this task only when needed. We make the running of the package target conditional with the use of the unless attribute.

The unless attribute instructs Ant to skip the execution of the target if the associated property has been set to any value. Conversely, the if attribute of <target> instructs Ant to run the target if the property is set.

With properties and the if and unless attributes of the <target> task, we can control at build time which targets are run. From the example in Listing 12-3, the package target references the package.notRequired property to determine if it should generate a JAR file. This property is set as the final act of the compile target upon which the package target is dependent.

The task <uptodate> is used to set the package.notRequired property in the example. This conditional task sets the property identified with the property attribute if the target file is more up-to-date than the source. In this case, the <uptodate> task checks to see if any class files have been generated since the JAR file was last produced. If any class files prove to be more recent than the JAR file, then the package.notRequired property is left unset and the package target is allowed to execute.

The conditional support provided by Ant makes it is possible to create sophisticated build scripts that support incremental builds. However, adding conditional behavior to the build process can significantly increase its complexity. The example in Listing 12-3 covers a very basic case. A typical J2EE build involves many more targets, and adding dependency information therefore adds significant complexity to the build process.

To prevent additional build logic making build files unduly complex, good design dictates the build process be broken down into discrete blocks, or modules. With this approach, modules of the system can be built in isolation, and dependencies can be more easily defined between build targets.



    Rapid J2EE Development. An Adaptive Foundation for Enterprise Applications
    Rapid J2EEв„ў Development: An Adaptive Foundation for Enterprise Applications
    ISBN: 0131472208
    EAN: 2147483647
    Year: 2005
    Pages: 159
    Authors: Alan Monnox

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net