Thursday, April 19, 2007

Antwrap

Recently, I wrote a library called Antwrap that can be used to invoke Ant tasks from a Ruby script. My original motivation for writing it was to use Rake as a build tool on a Java project. While Ant provides an abundance of useful tasks, I've always found its XML syntax makes it hard to write concise, fine-grained tasks (the original author of Ant states as much). Rake is an internal DSL written in Ruby(which is concise), so you can define your own abstractions in the build script and you have a huge selection of libraries at your disposal.

I wanted to use Rake as a build tool but there was an obvious hurdle to clear; all those Java specific tasks that we take for granted in Ant aren't available in the Ruby libraries. For example, let's say a Rake script needs to kick off a Java process. The core Ruby libraries make this possible:

system("java", "-client -jar lib/foobar.jar")
But the system call is not specific to the Java executable so passing in JVM and Program options can be awkward. In Ant, we would use the java task:
<java classpath="lib/foobar.jar" classname="foo.bar.FooBar" fork="true">

<jvmarg value="client"/>
<arg value="argOne"/>
<arg value="argTwo"/>
</java>
This is the equivalent using Antwrap:
@ant = AntProject.new(:ant_home => "/Users/caleb/tools/apache-ant-1.7.0")

@ant.java(:classpath => 'lib/foobar.jar', :classname => 'foo.bar.FooBar',
:fork => 'true'){
jvmarg(:value => 'client')
arg(:value => 'argOne')
arg(:value => 'argTwo')
}
The Task attributes are passed to the method as a hash. What would normally be child elements in XML are passed to the method via a block. The AntProject instance doesn't actually have a java method defined. Rather, its method_missing method is invoked and ends up instantiating the appropriate Ant task for you. This is one example of Ruby's powerful meta-programming features; they make it easy to provide rich semantics (as opposed to invoking @ant.executeTask('java')).

Here is a more complicated example using the javac task. This simply illustrates that all of the normal Ant tasks are at your disposal, including Ant Properties and Refs:

@ant = AntProject.new(:ant_home => "/Users/caleb/tools/apache-ant-1.7.0")
@ant.property(:name => 'common.dir', :value => @current_dir)

@ant.path(:id => "other.class.path"){
pathelement(:location => "classes")
pathelement(:location => "config")
}

@ant.path(:id => "common.class.path"){
fileset(:dir => "${common.dir}/lib"){
include(:name => "**/*.jar")
}
pathelement(:location => "${common.classes}")
}

@ant.javac(:srcdir => "test", :destdir => "classes"){
classpath(:refid => "common.class.path")
classpath(:refid => "other.class.path")
}
The transition from an existing Ant build file to a Rake file is relatively easy. There is a handy conversion script available. So this:
<target name="clean" depends="init">

<delete dir="classes" failonerror="false"/>
<delete dir="${distro.dir}"/>
<delete file="${outputjar}"/>

<delete file="${output.dir}"/>
</target>
Becomes the Rake equivalent:
task :clean => [:init] do
@ant.delete(:dir => "classes", :failonerror => "false")
@ant.delete(:dir => "${distro.dir}")
@ant.delete(:file => "${outputjar}")
@ant.delete(:file => "${output.dir}")
end

Antwrap runs on the native Ruby and the JRuby interpreter. If running on the native Ruby interpreter, Antwrap depends on the Ruby Java Bridge (RJB) Gem which invokes Java classes via the Java Native Interface (JNI). Antwrap is currently being used for Ant tasks in the Raven (a.k.a. don't call me Maven) project. Raven is a JRuby implementation of the Rake tool that provides all kinds of utilities for a Java project.

In the long-term, I see Ruby/Rake scripts complementing a tool like Maven on Java projects. Ruby for fine-grained, project specific tasks and Maven for coarse-grained, boiler-plate tasks (compilation, unit-tests, packaging, documentation) that you need on most Java projects.

Other resources:
Martin Fowler on JRake
Groovy Antbuilder