Java Build System

Required Software

In order to use the described build system for Java you need the following software on your computer:

  • Java compiler
  • Apache ant build tool
  • JUnit unit testing library
  • RSB as an exemplary dependency

Brief Overview

We will demonstrate how to set up a Java project using the build tool Apache Ant, which is a standard tool in the Java world. ant is configured through an XML file in the project root directory, called build.xml.

In this tutorial we will build a library with some reusable functions, which results in a jar file in Java. Additionally, we will create an executable class which uses the functions provided in the library. As java does not distinguish between libraries and executables and each jar can contain as many executable classes as desired, we will only build a single jar file containing all of theses classes.

We assume knowledge of the Java language, the concept of jar files and the classpath for finding these files. Additionally, you should have a very basic understanding of how to use the apache ant command line program. In case you need advice on this, please consult e.g. Tutorial: Hello World with Apache Ant.

Folder Layout

The folder layout of our tutorial project will look as follows.

  • build-system-essentials-java: top-level folder of our project
    • build.xml: Build declarations and dependency handling
    • README.txt, COPYING.txt, …: Files with additional textual information like license, contact information etc.
    • src: Contains the source code of our project using the typical Java convention of a folder per package and a file per class.
    • test: Contain the unit tests for the project in a parallel hierarchy to the src folder.

Declaring an Ant Project and Finding Upstream Dependencies

See also

Ant online documentation
Verbose explanation of every ant feature and task.

We start by declaring the basic project in the build.xml:

1
2
3
4
5
6
7
<project name="build-system-essentials" default="dist">

    <property name="version" value="0.1.0" />
    <property name="jarname" value="${ant.project.name}-${version}.jar" />
    <description>
        An example project for ant.
    </description>

The project is declared with the project tag and needs a name. Additionally, we declare the default target ant shall execute if a target is not explicitly specified on the command line. For this project, it makes sense to call dist, which will generate a jar-file with our code. Afterwards, we declare some basic information about the project, namely version, a name for the generated jar-file and a longer description about it.

All property definitions like the ones above can be changed by the caller of ant using -D flags on the command line. This is almost always necessary, since ant has no built-in mechanism for finding dependencies such as executable programs or jar-files automatically. Therefore, the user needs to pass in all required locations if the defaults provided in the build.xml do not match the system’s configuration. As this usually results in a lengthy command line, we will describe a second way of defining these properties using a configuration file:

1
    <property file="build.properties" />

With this declaration, ant parses a the file build.properties which consists or key=value definitions. ant properties set in this way are persistent. A user of our project would typically create such a build.properties file, specify all customizations in that file, and then just call ant without specifying any properties.

Note

The property file definition in build.xml has to precede all properties that should be changeable, because:

Properties are immutable: whoever sets a property first freezes it for the rest of the build

Ant online documentation

The ant command line flags always precede everything and hence can still be used to override settings from build.properties.

Now that it is possible to modify properties in a persistent manner, we can start with defining some of them for locations of external libraries:

1
2
3
4
    <property name="protobuf.lib" location="/usr/share/java/protobuf-java.jar" />
    <property name="rsb.lib" location="/usr/share/java/rsb.jar" />
    <property name="junit.lib" location="/usr/share/java/junit4.jar" />
    <property name="hamcrest.lib" location="/usr/share/java/hamcrest-core.jar" />

The defaults we give here are valid for a recent Ubuntu GNU/Linux system.

Note

Hamcrest is a dependency of JUnit in the version installed by Ubuntu GNU/Linux. Other distributions may include a standalone jar of JUnit where hamcrest is include.

After fixing the locations of external libraries we declare several locations for our own source code and the folders where compilations results will go to:

1
2
3
4
5
6
7
    <property name="dir.src" location="src" />
    <property name="dir.test" location="test" />
    <property name="dir.build.src" location="build/src" />
    <property name="dir.build.test" location="build/test" />
    <property name="dir.build.dist" location="dist" />
    <property name="dir.report.unittests.xml" location="testreports" />
    <property name="dir.doc" location="doc" />

As explained above, production source code and test source code are separated in different folders. It is also a good idea to separate the compilation results of the build process in different folders so that the final jar-file of the production code can easily be generated by globbing over the generated *.class files. Therefore, dir.build.src and dir.build.test are separated folders. We furthermore define where the jar-file will be generated, test reports in XML format for automatic parsing are created, and where the API documentation is generated. All location definitions in ant are relative to the location of the build.xml in the filesystem.

Finally, we define the installation locations for our project:

1
2
3
    <property name="install.prefix" location="/usr" />
    <property name="install.dir.jar" location="${install.prefix}/share/java" />
    <property name="install.dir.license" location="${install.prefix}/share/doc/${ant.project.name}" />

As mentioned before, ant does not provide any dependency checking. Even if a library location is given explicitly and does not point to a valid jar-file, e.g. for RSB, there will be no warning about this. Instead, compilation will fail and it may not be easy to spot what went wrong. Therefore, it is advisable to check at least the minimal dependencies manually:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    <fail message="Google Protocol Buffers library not found at '${protobuf.lib}'. Please set protobuf.lib.">
        <condition>
            <not>
                <available file="${protobuf.lib}" />
            </not>
        </condition>
    </fail>
    <fail message="rsb not found at '${rsb.lib}'. Please set rsb.lib.">
        <condition>
            <not>
                <available file="${rsb.lib}" />
            </not>
        </condition>
    </fail>

If all dependencies are available, we need to construct classpaths from the collected file locations for the compiler invocation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    <path id="classpath.src">
        <pathelement path="${classpath}" />
        <pathelement location="${protobuf.lib}/" />
        <pathelement location="${rsb.lib}/" />
    </path>
    <path id="classpath.test">
        <path refid="classpath.src" />
        <pathelement location="${junit.lib}" />
        <pathelement location="${hamcrest.lib}" />
        <pathelement location="${dir.build.src}" />
        <pathelement location="${dir.build.test}" />
    </path>

We define two separate classpaths for the compilation of the production source code and the unit tests. This has the advantage that unit tests accidentally leaked into the production tree will cause a compilation error. As visible in line 7, it is possible to use an existing classpath and extend it to form a new one, in this case the test classpath. Please note that for compiling the unit tests the compilation results of the production source code must be available to the compiler. This is the reason why dir.build.src is in the test classpath. Additionally, to actually execute the unit tests, also the compiled unit tests (*.class files) need to be available. Therefore, also dir.build.test is listed here.

Building Production Source Code

In order to build a jar-file, we will first make sure that the folders where build artifacts will be generated in exists. For this purpose we define a special init target:

1
2
3
4
5
6
7
    <target name="init" description="generates folders for build artifacts">
        <mkdir dir="${dir.build.src}" />
        <mkdir dir="${dir.build.test}" />
        <mkdir dir="${dir.build.dist}" />
        <mkdir dir="${dir.report.unittests.xml}" />
        <mkdir dir="${dir.doc}" />
    </target>

As ant does not automatically provide a clean target, it is also a good habit to define such a target, symmetrical to the init target:

1
2
3
4
5
6
7
    <target name="clean" description="cleans up build artifacts">
        <delete dir="${dir.build.src}" />
        <delete dir="${dir.build.test}" />
        <delete dir="${dir.build.dist}" />
        <delete dir="${dir.report.unittests.xml}" />
        <delete dir="${dir.doc}" />
    </target>

Having ensured that all necessary folders exist, we can define a target to compile the production source code:

1
2
3
4
5
6
    <target name="compile" depends="init" description="compiles the source tree">
        <javac srcdir="${dir.src}" destdir="${dir.build.src}" debug="on" deprecation="on" includeAntRuntime="false">
            <compilerarg value="-Xlint" />
            <classpath refid="classpath.src" />
        </javac>
    </target>

Our target compile depends on init to have folders available. Inside compile we invoke the Java compiler using the javac ant task. javac uses the production source code found in ${dir.test} and generates class files in ${dir.build.test}. Furthermore, we instruct the compiler to include debug information in the generated classes for easy debugging, enable warnings for the use of deprecated functions, and prevent ant from providing its libraries to the compiler as recommended in the documentation. With these instructions we are able to compile our production source code.

Building Unit Tests

In order to build the unit tests we will add a compile-tests target comparable to the compile target:

1
2
3
4
5
    <target name="compile-tests" depends="compile" description="compiles the unit tests">
        <javac srcdir="${dir.test}" destdir="${dir.build.test}" debug="on" deprecation="on" includeAntRuntime="false">
            <classpath refid="classpath.test" />
        </javac>
    </target>

Instead of depending on init, this target also requires that the production source code was compiled before and hence depends on compile.

Afterwards, we need to define another target which actually executes the compiled unit tests:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    <target name="test" depends="compile-tests" description="Runs the unit tests">
        <junit printsummary="yes" haltonfailure="false">
            <formatter type="xml" />
            <formatter type="plain" usefile="false" />
            <classpath refid="classpath.test" />
            <assertions>
                <enable />
            </assertions>
            <batchtest fork="yes" todir="${dir.report.unittests.xml}">
                <fileset dir="${dir.test}">
                    <include name="**/*Test*.java" />
                </fileset>
            </batchtest>
        </junit>
    </target>

ant provides a junit task for this purpose. We instruct the task to output test results as XML files for automatic processing, e.g. by a continuous integration server, and as output on the console for manual checks. After setting the test classpath, we instruct the task that during the execution of the unit tests, Java assertions shall be turned on, so that assertion errors will get visible with the unit tests. Finally, we instruct the task to execute all tests found in files containing the pattern *Test*.java in their name, inside the test folder.

Now, unit tests can be executed using ant test.

Exposing the Project to Downstream Projects

First, we have to build a jar-file from the compiled classes using the jar task provided by ant:

1
2
3
4
5
6
7
8
9
    <target name="dist" depends="compile" description="generate the distribution">
        <jar jarfile="${dir.build.dist}/${jarname}" basedir="${dir.build.src}">
            <manifest>
                <attribute name="Implementation-Vendor" value="CoR-Lab Bielefeld University" />
                <attribute name="Implementation-Title" value="${ant.project.name}" />
                <attribute name="Implementation-Version" value="${version}" />
            </manifest>
        </jar>
    </target>

Afterwards, we can install the jar-file and the license:

1
2
3
    <property name="install.prefix" location="/usr" />
    <property name="install.dir.jar" location="${install.prefix}/share/java" />
    <property name="install.dir.license" location="${install.prefix}/share/doc/${ant.project.name}" />

Note

Installing the copyright follows the conventions of Debian-based Linux distributions like Ubuntu GNU/Linux.

Finally, we build the API documentation using javadoc, so that downstream developers have a human-readable reference:

1
2
3
4
5
6
7
8
    <target name="doc">
        <javadoc destdir="${dir.doc}" author="true" version="true" use="true" windowtitle="${ant.project.name}">
            <fileset dir="${dir.src}" defaultexcludes="yes">
                <include name="**/*.java" />
            </fileset>
            <classpath refid="classpath.src" />
        </javadoc>
    </target>

How to Build and Use the Project

cd path/to/build-system-essentials-java

# fill build.properties with the required variables

ant         # build
ant test    # run tests
ant doc     # build documentation
ant dist    # create a jar file in dist/ subdirectory
ant install # install the jar file and license
ant clean   # delete generated files