Engine Yard Blog RSS Feed

Recently, I landed a new library for JRuby that will be part of JRuby 1.5. Before I start I want to conjure the image you see below this text: that's Right! Mr. Potato Head: a role model for us all. Something that delights us for hours (or at least, probably did, at one point in our lives), is flexible, and is not only a toy, but also a starchy food product.

Bear with me for a second, and excuse what must have just sounded like a wee bit of lunacy. The the truth is, we live in a world where, as programmers, we construct Mr. Potato Heads every day. We're confronted with making software where not only are we required to stick various elements together, but also to arrange them in the most pleasing way possible. Software design is really just _like _decorating a potato. The potato of this blog entry will be build software.

Build Tools

In the Java world, Ant is the 800 pound potato of build tools. It's in virtually every Java build environment on the planet. To date, I have only really known one person who really really liked Ant.

For the most part, I think people respect Ant as a tool which is a bit syntactically gross, but which gets the job done reliably. It is also lamented for having little support for imperative programming constructs. This seems to have been by design, but it doesn't seem to make very many programmers happy.

In the Ruby world we have Rake. Rake, by contrast, has a much nicer syntax than Ant. It also allows any construct that the Ruby language allows, since Rake is just a DSL-like API for building software running in a Ruby interpreter. On the other hand, if you have the need to perform common tasks in the Java world, then it lacks a bunch of standardized cross-platform tasks that Ant contains. You find yourself breaking out into lots of icky shell commands (javac -classpath #{my_files}) and this works great until you try and build something on, gasp, Windows.

A pragmatic (but not so great) reality is that most Java shops may warm to switching to a different build technology, but are unlikely to switch over projects en masse. Even if they were enough in love with Rake to switch, they'd need to hobble together replacements for tasks that they take for granted in Ant. That is... until now.

Use Cases

JRuby's Rake and Ant integration handles the following use cases:

  • Call any ant task or type from within Rake
  • Allow Rake to be invoked from within Ant
  • Allow Rake tasks to be imported as callable Ant targets
  • Allow Ant to be invoked from Rake
  • Allow Ant targets to be called as Rake tasks from within Rake

Let's break these down one at a time...

Call Any Ant Task or Type From Within Rake

The truth is, Ant is really just a built-in library in JRuby. You could just write a script and not use it in Rake:

    require 'ant'

    ant do
      build_dir = "java_build" # Regular Ruby variables interact fine

      # But defining and consuming Ant properties is fine
      property :name => "src.dir", :value => "java_src"

      path(:id => "project.class.path") do
        pathelement :location => "classes"
      end

      mkdir :dir => build_dir

      javac(:destdir => build_dir) do
        classpath :refid => "project.class.path"
        src { pathelement :location => "${src.dir}" }
      end

      jar :destfile => "simple_compile.jar", :basedir => build_dir
    end

This example constructs an instance of an Ant project, then makes a directory, compiles some Java source, and finally, generates a jar file of the results. All of these are just Ant tasks. They'll work on any platform. Sweet, I say! This does, however, lack dependency management. So let's use Rake to do it instead:

    require 'ant'

    build_dir = "java_build"
    file build_dir

    task :setup => build_dir do
      ant.property :name => "src.dir", :value => "java_src"
      ant.path(:id => "project.class.path") do
        pathelement :location => "classes"
      end
    end

    task :compile => :setup do
      ant.javac(:destdir => build_dir) do
        classpath :refid => "project.class.path"
        src { pathelement :location => "${src.dir}" }
      end
    end

    task :jar => :compile do
      ant.jar :destfile => "simple_compile.jar", :basedir => build_dir
    end

    task :default => :jar

[Quick note: since we can combine the best of both worlds, you don't need to ever set an Ant property if you don't want to. Just use a Ruby variable or constant. Different folks for different strokes...]

This snippet shows how easy it is to consume Ant tasks in a Rakefile. Really, JRuby's Ant library is just a straight-forward set of APIs that map clearly to Ant's original syntax. Looking up how to do something is a very straight-forward task.

The other benefit mentioned above is that Rake can use imperative programming in it. Let's consider a snippet like this in Ant:

    <java classname="${mainclass}">
      <arg value="--command"/>
      <arg value="maybe_install_gems"/>
      <arg value="--no-ri"/>
      <arg value="--no-rdoc"/>
      <arg value="--env-shebang"/>
    </java>

It can use imperative conveniences:

    command = "--command may_install_gems --no-ri --no-rdoc --env-shebang"
    ant.java :classname => "${mainclass}" do
      command.split(/\s+/).each { |value| arg :value => value }
    end

So if you're a Rake user already and you need to do Java things, then using this support should be a pretty simple decision. Heck, there are many other optional Ant tasks that may be useful even if you're not doing Java things.

Allow Rake to be Invoked From Within Ant

If you have the ability to write the equivalent of an Ant project using Rake, then you may want to make part of your project driven by Rake. However, if you do this, you may _still _need to call it from within Ant. We can do this with the new 'Rake' task.

If we pretend the previous Rakefile existed parallel to a build.xml file we have then in that Ant's build.xml file we could have:

    <?xml version="1.0" encoding="UTF-8"?>
    <project name="foobar" default="default" basedir=".">
      <description>Builds, tests, and runs the project foobar.</description>

      <target name="load-rake-task">
        <taskdef name="rake" classname="org.jruby.ant.Rake"/>
      </target>

      <target name="default" depends="load-rake-task">
        <rake task="jar"/>
      </target>

      ...
    </project>

This Ant script's 'default' target will load our Rake task and then call Rake (file defaults to 'Rakefile'), and more specifically, call the task 'jar' (task defaults to 'default'). There are a couple of cool scenarios to consider here:

1. Try Rake by only dipping your toes in the water

This strategy is great if you like Rake but are worried you don't have enough influence to get your development team to switch their entire build suite. You can just stick a Rakefile off to the side for some new functionality and let your teammates evaluate how much they like it. If they do, then switch the rest later... or don't. The idea that most Java shops will big-bang change their build software is extremely unlikely. An incremental strategy is your best bet.

2. Easier integration with Java tools

Even if you're totally sold on Rake, you still need to know that software like NetBeans expects to see a build.xml file, so it can interact with your project. Having a small shim like the one above makes you play nice with any tools that_ expect_ Ant.

This is the simplest way to call Rake from Ant, but the next option may suit your needs better...

Allow Rake Tasks to be Imported as Callable Ant Targets

The big missing feature of the Rake task in the script above is that it's only one-way. You can call Rake, but then the Rakefile you call has no meaningful interaction with the Ant side of things. Sure it can call Ant tasks, but it can't see properties or Ant targets that are defined in the calling build.xml file.

To have better interoperability we have another Ant task: RakeImport. RakeImport will require the specified Rakefile and then register all of its tasks with Ant dependency management system. Let's look at a simple example:

    <?xml version="1.0" encoding="UTF-8"?>

    <project name="foobar" default="top-level" basedir=".">
        <description>Builds, tests, and runs the project foobar.</description>

        <taskdef name="rakeimport" classname="org.jruby.ant.RakeImport"/>
        <rakeimport/>

        <target name="top-level" depends="its_in_rake" />

        <target name="its_in_ant">
          <echo message="ant: its_in_ant"/>
        </target>
    </project>

In Ant, we specify that we want to use RakeImport and then immediately call it. This loads the following Rakefile and registers all of its tasks with Ant:

    task :its_in_rake => [:setup, :its_in_ant]  do
      puts "it's in Rake"
    end

    task :setup do
      puts "setup in Rake"
    end

Now let's run 'ant top-level'; we now see the following output:

    Buildfile: build.xml
    [rakeimport] (in /Users/enebo/work/akakamiari/samples/rake_import_example2)

    setup:
    setup in Rake

    its_in_ant:
         [echo] ant: its_in_ant

    its_in_rake:
    it's in Rake

    top-level:

    BUILD SUCCESSFUL
    Total time: 7 seconds

This output shows that it's executing both Ant targets and Rake tasks in the desired order. itsinant executes as a dependency of itsinrake which in turn executes because it is a dependency of the Ant target 'top-level'.

The scenarios for this level of integration:

1. Choosing the best tool

Since Rake gives a full imperative programming environment there are some things that are trivial to do in Rake which are cumbersome (or impossible without writing a custom Ant task) in Ant. You can move that stuff into Rake, but still continue using Ant for everything else.

2. Toe wet... time to go waist deep

In the previous section we used the Rake task to demonstrate to your development group that Rake is useful. This allows you to start depending on the capabilities of Rake more by being able to inject Rake tasks into the Ant dependency graph. Your group is still using Ant as the main build tool, but you're delegating more of the build to Rake.

Allow Ant to be Invoked From Rake

Let's look at things from the other side of the coin. If you're already a Rake user, but you need to interact with existing Ant build files, we've also got solutions for you. The first method allows you to just call Ant from within Rake:

    task :call_ant do
      ant '-f my_build.xml my_target1'
    end

Alternatively, you can also supply arguments as a list:

    args = ['-f', 'my_build.xml', 'my_target1']
    task :call_ant do
      ant args
    end

Believe it or not, this doesn't just execute ant; it loads it into your JRuby environment. This is nice because it doesn't spawn a second JVM to run Ant.

As I mentioned at the beginning of this section, you may want to just call into Ant, but not have any more interaction than that. That's ok, but if you want more....

Allow Ant Targets to be Called as Rake Tasks From Within Rake

'ant_import' beats 'ant' in flexibility because it ends up registering all of Ant's top-level targets with Rake's dependency management system. Once you import an Ant file into Rake, you can call the Ant tasks as if they were ordinary Rake tasks. A simple example:

    task :ant_import do
      ant_import
    end

    task :compile => [:ant_import, :its_in_ant_setup] do
      # Do some compilation
    end

This example will load antimport when the compile task is executed which will in turn load the build.xml file in the current directory and load all of its Ant targets. I did not antimport at the top-level of the Rakefile to show that you may only want to load the Ant targets if you are actually planning on using them.

Details Details

This code has just landed in JRuby trunk. All of the examples listed here should work, but this code is brand new. It's a work in progress that will firm up by the time JRuby 1.5 is out. So this means two things for the reader:

1. You can help find problems and help improve the library

In other words, if this interests you, then you can get involved early and help address problems or enhance the library. Odds are you can make the difference between this integration being great or merely good.

Getting started note: the current practice for running the examples above is to copy jruby-complete.jar into $ANT_HOME/lib. You can manually set your classpath to include jruby-complete.jar as well, but I found it less error prone to just copy the jar.

2. If you don't like unfinished software then wait for 1.5

Some people don't like the hassles or have the time to play with unfinished software. If you're in this position, then don't worry. We'll get things working nicely for 1.5. We don't want an early test run to ruin anyone's expectations.

Conclusion

Going back to our original Mr. Potato Head theme, I was hoping to show that sometimes combining things together really is the best strategy. In theory, it's nice to start over and make something pure and wonderfully homogenous. In practice, there are always pesky real-life details that get in the way. Why rewrite everything when you can just attach the new technology on the side? If you do have the long term goal of replacing an older technology, why not do it incrementally? Every big-bang rewrite project I have ever been in has basically failed in a significant way. Change things one piece at a time and your chances of success are much higher.

Our new Rake/Ant integration is meant to allow incremental change, or at least to allow the best features of each tool to be used together. It embodies the reality that both tools may be necessary and that replacing one for the other probably won't be a 100% solution.

As always, questions and comments are welcome!


Tagged:

comments powered by Disqus