Monday, March 30, 2009

Exposing SVN version and build time with Ant

As a developer, I find that an application's most useful version number is its SVN version; once I have that number, I can head over to the repository and figure out exactly what code is running. This is extremely useful both during development (Did I upload the correct .war to my staging server?) and for ongoing maintenance (What version is running on this server that needs to be updated?)

Fortunately, it's pretty easy to configure Ant to automatically inject the SVN version into your application every time you build it. Let's say that you have a particular file (/WEB-INF/jsp/admin.jsp) that should contain the SVN version number.

We take advantage of svntask, a nice simple library that lets Ant use SVN. Download/unzip the project files, place them somewhere convenient in your project, and add the following to your Ant build file: (modifying the fileset directory to where you put the svntask libraries)
<target name="version">
<typedef resource="com/googlecode/svntask/svntask.xml">
<classpath>
<fileset dir="${lib.dir}/buildtime">
<include name="svnkit.jar" />
<include name="svntask.jar"/>
</fileset>
</classpath>
</typedef>

<svn>
<info path="." revisionProperty="revisionVersion" />
</svn>

<property name="svn.version" value="${revisionVersion}" />
</target>

This stores the SVN version number of your project root in the "svn.version" Ant property. Now, it's just a matter of copying the correct file from the source web directory to the target web directory before you package your .war file: (The following will require some modification depending on how you normally package your wars.) We use Ant's copy/filter tasks to replace all occurances of the token "@version@" in admin.jsp with the SVN version number.
<target name="war.pre" depends="version">
<copy file="${src.web.dir}/WEB-INF/jsp/admin.jsp"
tofile="${target.war.expanded.dir}/WEB-INF/jsp/admin.jsp"
overwrite="true">
<filterset>
<filter token="version" value="${svn.version}" />
</filterset>
</copy>
</target>

Now, the following snippit in admin.jsp
<div id="footer">
<p>Build Number @version@</p>
</div>

becomes
<div id="footer">
<p>Build Number 1692</p>
</div>

This is good enough for production releases, where the package application will (or should) be compiled straight from the repository. However, during development, I often won't be checking in changes between builds. So, to tell those builds apart, I find it useful to include the build timestamp as well, which I can get using Ant's tstamp task. This isn't a perfect solution, but it's enough to give me some reassurance that I didn't accidentally deploy an old build to my local server.
<target name="war.pre" depends="version">
<tstamp>
<format property="war.tstamp" pattern="yyyy-MM-dd HH:mm:ss z" />
</tstamp>
<copy file="${src.web.dir}/WEB-INF/jsp/admin.jsp"
tofile="${target.war.expanded.dir}/WEB-INF/jsp/admin.jsp"
overwrite="true">
<filterset>
<filter token="version" value="${svn.version}" />
<filter token="time" value="${war.tstamp}" />
</filterset>
</copy>
</target>

Now, inside of admin.jsp, I can include the following snippit
<div id="footer">
<p>Build Number @version@ (@time@)</p>
</div>

and Ant will transform this into
<div id="footer">
<p>Build Number 1692 (2009-03-30 11:20:15 PDT)</p>
</div>

which is exactly what I want.

2 comments:

a said...

Here is another tip.

Put this inside your <jar></jar> in ant:

<manifest>
<attribute name="Specification-Version" value="${version}" />
<attribute name="Implementation-Version" value="${TODAY}" />
<attribute name="Main-Class" value="com.foo.Version" />
</manifest>

package.com.foo;

public class Version
{
/** */
public static String getSpecification()
{
Package pkg = Version.class.getPackage();
return (pkg == null) ? null : pkg.getSpecificationVersion();
}

/** */
public static String getImplementation()
{
Package pkg = Version.class.getPackage();
return (pkg == null) ? null : pkg.getImplementationVersion();
}

/**
* A simple main method that prints the version and exits
*/
public static void main(String[] args)
{
System.out.println("Version: " + getSpecification());
System.out.println("Implementation: " + getImplementation());
}
}

Then, you can do this:

java -jar my.jar

No need to put version numbers in jar files ever again. =)

You also get to easily do this:

/** @return all the version numbers */
public Package[] getVersions()
{

Package[] packages = Package.getPackages();
return packages;
}

Then in a jsp page:

<table>
<tr>
<th>Package</th>
<th>Version</th>
<th>Implementation</th>
</tr>
<c:forEach var="pkg" items="${backend.versions}">
<tr>
<td><c:out value="${pkg.name}"/></td>
<td><c:out value="${pkg.specificationVersion}"/></td>
<td><c:out value="${pkg.implementationVersion}"/></td>
</tr>
</c:forEach>
</table>

To get something like this:

http://subethamail.org/se/version.jsp


-jon (author of svntask and appreciative of the nice comments about his project)

Luke said...

FYI, you'll likely want to use committedRevisionProperty rather than revisionProperty. The latter gets the number of the revision you have selected (normally HEAD, so the revision of HEAD across the repository), while the former gets the number of the revision last committed. That is generally more useful.

I.e., if you are building project A, which was committed at r1, but project B has been added to the repository at r2, HEAD (and thus revisionProperty) will be "2". committedRevisionProperty will be 1.