Developer

Ant makes branding Java JARs simple and foolproof

There are various ways to specify release designations in builds, but most approaches are less than reliable. Java developers can tap into the power of Ant to simplify the task and make sure that all JARs get branded with a release number.


To developers, version 1.2 follows version 1.1 unless a backward-incompatible change was made, in which case they go with version number 2.0. Because mid-development version number shifts can wreak havoc on schedules, bug-tracking systems, and source repository tags, development teams have long been augmenting their version numbers with internal identification systems, such as release, revision, milestone, or build numbers. These office-use-only designations seldom show up on data sheets, Web sites, and CDs as version numbers do, but, often, they're more useful than the official version numbers when answering the all important "what's different in this build?" question raised by a newly discovered bug.

Various systems exist for specifying release designations in builds. Some shops hard code the value in their source and rely on developers to remember to update the value with each modification they make. Other projects rely on the token replacement systems supported by many source control systems that can be used to insert a file's version number into a string constant. Still others manually create a small text file within each archive that can be programmatically queried.

However, each of these systems has its problems. Developers forget to increment hard-coded build numbers—especially for the sort of "quick, little fixes" so likely to introduce bugs. Source control version numbers are file-based and reflect only the version information of the single file from which they're drawn. Text files can fall away as JARs are repackaged, optimized, obfuscated, or merged.

A better method for branding builds with release identifiers relies on using the token filters provided by the Ant build system. When copying files from one location to another, the Ant copy task can be instructed to replace tokens of the form @TEXT@ with arbitrary strings. Using this feature and some other Ant build file trickery, we can make sure that all JARs get branded with a release number without making daily development compilations any more cumbersome.

The source file
Let's begin by taking a look at the Java source file for our sample application, MyApp.java (Listing A). Within this class file, you'll see a few static fields and methods. The first static field, called RELEASE, has a value of "@RELEASE@". This is the token we'll be replacing later using the Ant copy filters. For now, however, we'll just leave it with its "@RELEASE@" value.

The first of the two static methods, getVersionString(), simply concatenates the values of some of the other static fields and then optionally appends the value of RELEASE, unless that value is the string @RELEASE@. In that case, it's not appended since it contains no usable build identification information. If RELEASE has been changed in the source file before compilation, that value is appended to the returned version string.

It's important to note that the constant string we use to compare against the value of RELEASE is split into two strings, which are concatenated at compile time. This prevents the Ant token replacement filter from replacing the @RELEASE@ constant in addition to the static field assignment statement.

To developers, version 1.2 follows version 1.1 unless a backward-incompatible change was made, in which case they go with version number 2.0. Because mid-development version number shifts can wreak havoc on schedules, bug-tracking systems, and source repository tags, development teams have long been augmenting their version numbers with internal identification systems, such as release, revision, milestone, or build numbers. These office-use-only designations seldom show up on data sheets, Web sites, and CDs as version numbers do, but, often, they're more useful than the official version numbers when answering the all important "what's different in this build?" question raised by a newly discovered bug.

Various systems exist for specifying release designations in builds. Some shops hard code the value in their source and rely on developers to remember to update the value with each modification they make. Other projects rely on the token replacement systems supported by many source control systems that can be used to insert a file's version number into a string constant. Still others manually create a small text file within each archive that can be programmatically queried.

However, each of these systems has its problems. Developers forget to increment hard-coded build numbers—especially for the sort of "quick, little fixes" so likely to introduce bugs. Source control version numbers are file-based and reflect only the version information of the single file from which they're drawn. Text files can fall away as JARs are repackaged, optimized, obfuscated, or merged.

A better method for branding builds with release identifiers relies on using the token filters provided by the Ant build system. When copying files from one location to another, the Ant copy task can be instructed to replace tokens of the form @TEXT@ with arbitrary strings. Using this feature and some other Ant build file trickery, we can make sure that all JARs get branded with a release number without making daily development compilations any more cumbersome.

The source file
Let's begin by taking a look at the Java source file for our sample application, MyApp.java (Listing A). Within this class file, you'll see a few static fields and methods. The first static field, called RELEASE, has a value of "@RELEASE@". This is the token we'll be replacing later using the Ant copy filters. For now, however, we'll just leave it with its "@RELEASE@" value.

The first of the two static methods, getVersionString(), simply concatenates the values of some of the other static fields and then optionally appends the value of RELEASE, unless that value is the string @RELEASE@. In that case, it's not appended since it contains no usable build identification information. If RELEASE has been changed in the source file before compilation, that value is appended to the returned version string.

It's important to note that the constant string we use to compare against the value of RELEASE is split into two strings, which are concatenated at compile time. This prevents the Ant token replacement filter from replacing the @RELEASE@ constant in addition to the static field assignment statement.

The Build file
Now, we turn our attention to the Build.xml file (Listing B), which employs a few nonobvious techniques. One of the goals is to prevent the developer from having to specify release information for every compilation. Most compilations aren't going to be released anywhere, and developers forced to provide a release name for every test compilation would probably end up scripting their builds with a fixed build identifier value, which would defeat the whole point of build identifiers.

To achieve this, we've separated targets for the creation of the classes and any JARs. The target called classes initially sets the property srcdir to the value of the previously defined sources property. Thus, when the classes target is run directly, the Java sources files used for compilation will be those found in the src directory. Compiling off the sources that are edited by the developers allows for the use of IDEs and other applications, which extract filenames from compilation error messages and open them for correction in the editor application.

However, when compiling sources files into classes to be included in a JAR, we don't want to compile the classes in the src directory. Instead, we must make a copy of the files—and perform token replacement in the process. The creation and specification of the alternate source directory is performed in the pre-jar target. This target sets the srcdir property not to the value of sources, as the classes target did, but to a subdirectory of the newly created jar directory.

The pre-jar target then copies the source code hierarchy from the src directory to a location inside the jar directory. During this copy, a filterset is used to replace all instances of the string @RELEASE@ with the value of the release property. The classes target will attempt to set the value of srcdir after it's been set to the filtered source directory in the pre-jar target. Fortunately, Ant's policy of never allowing for the redefinition of a property prevents the original value from being overwritten.

The pre-jar target handles the filtering. But it doesn't make sure that the release property, whose value will be used as a replacement for the @RELEASE token, has an assigned value. That check is handled by an ensure-release target, which just pops up a failure message explaining how to specify a release identifier if one wasn't provided on the command line.

Putting it all together
The invocation of the pre-jar and ensure-release targets is automatically handled by the dependency chain of the jar target. Specifically, the jar target makes sure that before its tasks are executed, both pre-jar and classes have been run. In turn, pre-jar first calls the ensure-release target to verify that a value has been set for the release identifier. The result is a build system that can be run using the classes target for day-to-day developer compilation and testing or can be run with the jar target to produce a JAR that's branded with a build identifier.

Alternate setups with branding and nonbranding JAR targets, default build identifier values, and the creation of manifest files are, of course, also possible. This implementation was arranged for the clarity of the examples. Real-world build file scenarios are generally more complex, but, so long as a duplication of the javac task invocation can be avoided, most setups should be straightforward.

The Build file
Now, we turn our attention to the Build.xml file (Listing B), which employs a few nonobvious techniques. One of the goals is to prevent the developer from having to specify release information for every compilation. Most compilations aren't going to be released anywhere, and developers forced to provide a release name for every test compilation would probably end up scripting their builds with a fixed build identifier value, which would defeat the whole point of build identifiers.

To achieve this, we've separated targets for the creation of the classes and any JARs. The target called classes initially sets the property srcdir to the value of the previously defined sources property. Thus, when the classes target is run directly, the Java sources files used for compilation will be those found in the src directory. Compiling off the sources that are edited by the developers allows for the use of IDEs and other applications, which extract filenames from compilation error messages and open them for correction in the editor application.

However, when compiling sources files into classes to be included in a JAR, we don't want to compile the classes in the src directory. Instead, we must make a copy of the files—and perform token replacement in the process. The creation and specification of the alternate source directory is performed in the pre-jar target. This target sets the srcdir property not to the value of sources, as the classes target did, but to a subdirectory of the newly created jar directory.

The pre-jar target then copies the source code hierarchy from the src directory to a location inside the jar directory. During this copy, a filterset is used to replace all instances of the string @RELEASE@ with the value of the release property. The classes target will attempt to set the value of srcdir after it's been set to the filtered source directory in the pre-jar target. Fortunately, Ant's policy of never allowing for the redefinition of a property prevents the original value from being overwritten.

The pre-jar target handles the filtering. But it doesn't make sure that the release property, whose value will be used as a replacement for the @RELEASE token, has an assigned value. That check is handled by an ensure-release target, which just pops up a failure message explaining how to specify a release identifier if one wasn't provided on the command line.

Putting it all together
The invocation of the pre-jar and ensure-release targets is automatically handled by the dependency chain of the jar target. Specifically, the jar target makes sure that before its tasks are executed, both pre-jar and classes have been run. In turn, pre-jar first calls the ensure-release target to verify that a value has been set for the release identifier. The result is a build system that can be run using the classes target for day-to-day developer compilation and testing or can be run with the jar target to produce a JAR that's branded with a build identifier.

Alternate setups with branding and nonbranding JAR targets, default build identifier values, and the creation of manifest files are, of course, also possible. This implementation was arranged for the clarity of the examples. Real-world build file scenarios are generally more complex, but, so long as a duplication of the javac task invocation can be avoided, most setups should be straightforward.

Editor's Picks