Java Build System¶
Tip
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 handlingREADME.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
The ant command line flags always precede everything and hence can still be used to override settings frombuild.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