Monday, April 18, 2011

How to set up an Apache Ant macrodef

Overview

This blog post will go over how to set up a somewhat trivial macrodef in Apache Ant. Ant basics are assumed.

What is a macrodef?

A macrodef allows you to squeeze off a group of commonly used tasks into it's own logical grouping. To use a programming analogy, it's like creating a method (or function, or procedure, or whatever) to hold your commonly-used tasks together. If you are doing the same sequence of tasks over and over, and especially if they only differ with respect to a few different parameters, then that sequence of tasks would be a great candidate for wrapping up into a macrodef.

Why use a macrodef?

Because, as a developer/build engineer/software architect/rational human, you are pretty dang tired of copy-and-paste programming.

Who should use it?

Anyone who uses Ant, has a lot of miscellaneous targets more complex than "hello world", and wants to keep their sanity amidst a wall of XML. Treat your script like a program - just think of macrodefs as custom functions that you can re-use.

How to get started?

In this example, I have a target which uses the Flex command-line compiler to compile a Flex module:

Fairly simple, right? But look what happens next - the Flex application got so big, everyone thought it would be a good idea to modularize the application, so instead of one Flex app, we have a small Flex app and a Flex module. So the Ant build gets updated like so:

Still no really compelling reason to learn how to use a macrodef, right? Let's keep growing this thing: we want to echo out a simple message so the build output records which compilation unit it is currently working on. While we're at it, the Flex devs re-organized where all the compiled output is placed (they're "agile" in more than one way).

Our trusty friend copy-n-paste saves the day, yet again! This is obviously the Right Way to do it. Right? All of you experienced developers should be shaking your head so quickly your hair straightens itself. Let's refactor this before it grows any more obscene.

(Granted, yes the move task can do a fileset, but for the sake of my contrived example let's just roll with it. The point is that you don't want to repeat yourself.)
The first thing to do is find out which parts are exactly the same. It turns out the arguments to the mxmlc compiler are almost all the same, except for the source input and the output name. For the hopelessly curious, the actual arg line looks like this (bolded for visual effect):

At this point, it should work like the original target. Invoke it simply like this:

<target name="test-macro"> <mxmlc/></target>

The name of the task is defined by the name attribute set in the macrodef, and becomes the name of a custom task. Let's parameterize the macrodef for use with all the modules. We use the dollar sign and curly braces for properties, but inside macrodefs we use the at-sign instead of dollar signs, and they're called attributes instead of properties, like so:

Now the input and output files are specified as arguments to a custom task. Note how the attribute names are used to specify the actual values passed in with the call to mxmlc. Note also that the flex arguments are set as a default value for the mxml.args attribute; this means that the values can be overridden during the call, like so:

In that example I added some additional args, whatever they might be, in addition to the previously defined flex.common.args property.
Now we can finally add more modules easily with much less copy-n-paste: