Monday, April 18, 2011

How to set up an Apache Ant macrodef

Note: this was originally posted December 29, 2010 on my old blog.

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:

<target name="mxmlc" description="Compile the flex application">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here)"/>
    </exec>
</target>

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:

<target name="mxmlc" description="Compile the flex application">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for main.mxml)"/>
    </exec>
</target>

<target name="mxmlc2" description="Compile the flex module">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for module1.mxml)"/>
    </exec>
</target>

All is good, right?  It's working, time to move on.  Well, the happy devs working on the Flex application have added 2 more modules - what to do now?  You could go with this:

<target name="mxmlc" description="Compile the flex application">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for main.mxml)"/>
    </exec>
</target>

<target name="mxmlc2" description="Compile flex module 1">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for module1.mxml)"/>
    </exec>
</target>

<target name="mxmlc3" description="Compile flex module 2">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for module2.mxml)"/>
    </exec>
</target>

<target name="mxmlc4" description="Compile flex module 3">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for module3.mxml)"/>
    </exec>
</target>

But that's pretty ugly - you could at least combine them into a single target, like this:

<target name="mxmlc" description="Compile the flex application">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for main.mxml)"/>
    </exec>
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for module1.mxml)"/>
    </exec>
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for module2.mxml)"/>
    </exec>
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for module3.mxml)"/>
    </exec>
</target>

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).

<target name="mxmlc" description="Compile the flex application">
    <echo message="Compiling main.xml"/>
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">

        <arg line="(a very long list of arguments here for main.mxml)"/>
    </exec>
    <move file="main.swf" todir="${dir.output}"/>
    <echo message="Compiling module1.xml"/>
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for module1.mxml)"/>
    </exec>
    <move file="module1.swf" todir="${dir.output}"/>
    <echo message="Compiling module2.xml"/>
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for module2.mxml)"/>
    </exec>
    <move file="module2.swf" todir="${dir.output}"/>
    <echo message="Compiling module3.xml"/>
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="(a very long list of arguments here for module3.mxml)"/>
    </exec>
    <move file="module3.swf" todir="${dir.output}"/>
</target>

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):

<property name="license.flex3"                value="redacted-number-here"/>
<property name="dir.war"                      value="some directory path"/>
<property name="dir.libs"                     value="some other directory path"/>
<target name="mxmlc" description="Compiles the flex application">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">

        <arg line="-creator 'ACME Corp.' -publisher 'ACME Corp.' -title AnvilApp -description http://acme.foo -optimize=true -as3 -target-player=9.0.124 -warnings=false -debug=false -license=flexbuilder3,${license.flex3} -sp ${dir.war} -library-path+=${dir.libs}  --file-specs ${dir.war}/AnvilApp.mxml -output ${dir.war}/AnvilApp.swf"/>
    </exec>
</target>

Refactored, I organize the properties like this (your mileage may vary):

<property name="dir.war"                      value="some directory path"/>
<property name="dir.libs"                     value="some other directory path"/>
<property name="flex.meta.1"                  value="-creator 'ACME Corp.' -publisher 'ACME Corp.'"/>
<property name="flex.meta.2"                  value="-title AnvilApp -description http://acme.foo"/>
<property name="flex.meta.props"              value="${flex.meta.1} ${flex.meta.2}"/>
<property name="debug.options"                value="-debug=false"/>
<property name="common.flex"                  value="-optimize=true -as3 -target-player=9.0.124 -warnings=false"/>
<property name="license.flex3"                value="redacted-number-here"/>
<property name="common.fixed"                 value="-license=flexbuilder3,${license.flex3} -sp ${dir.war} -library-path+=${dir.libs}"/>
<property name="flex.common.args"             value="${flex.meta.props} ${debug.options} ${common.flex} ${common.fixed}"/>

Which then lets the flex compilation call look like this:

<target name="mxmlc" description="Compiles the flex module">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">

        <arg line="${flex.common.args} --file-specs ${dir.war}/AnvilApp.mxml -output ${dir.war}/AnvilApp.swf"/>
    </exec>
</target>

Let's convert this mxmlc target into a macrodef.  If you're feeling cautious, just make a copy of the target, and then modify the copy until it works properly.
First change "target" to "macrodef".

<!--this is used to compile our internal flex modules-->
<macrodef name="mxmlc" description="For building the flex modules during flex compilation">
    <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">
        <arg line="${flex.common.args} --file-specs ${dir.war}/AnvilApp.mxml -output ${dir.war}/AnvilApp.swf"/>
    </exec>
</macrodef>

Wrap all the tasks (in this case, there's just an exec task) inside a sequential task.

<!--this is used to compile our internal flex modules-->
<macrodef name="mxmlc" description="For building the flex modules during flex compilation">
    <sequential>
        <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">

            <arg line="${flex.common.args} --file-specs ${dir.war}/AnvilApp.mxml -output ${dir.war}/AnvilApp.swf"/>
        </exec>
    </sequential>

</macrodef>

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:

<!--this is used to compile our internal flex modules-->
<macrodef name="mxmlc" description="For building the flex modules during flex compilation">
    <attribute name="mxml.args"   default="${flex.common.args}"/>
    <attribute name="sourcePath"/>
    <attribute name="destPath"/>

    <sequential>
        <exec executable="${flex.sdk}/bin/mxmlc" failonerror="true">

            <arg line="@{mxml.args} --file-specs @{sourcePath} -output @{destPath}"/>
        </exec>
    </sequential>

</macrodef>

Now this can be invoked like so:

<target name="test-macro">
    <mxmlc sourcePath="${dir.war}/AnvilApp.mxml" destPath="${dir.war}/AnvilApp.swf"/>
</target>

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:

<target name="test-macro">
    <mxmlc mxml.args="${flex.common.args} ${additional.args}" sourcePath="${dir.war}/AnvilApp.mxml" destPath="${dir.war}/AnvilApp.swf"/>
</target>

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:

<target name="compileFlexApp">
    <mxmlc sourcePath="${dir.war}/AnvilApp.mxml" destPath="${dir.war}/AnvilApp.swf"/>
    <mxmlc sourcePath="${dir.war}/module1.mxml" destPath="${dir.war}/module1.swf"/>
    <mxmlc sourcePath="${dir.war}/module2.mxml" destPath="${dir.war}/module2.swf"/>
    <mxmlc sourcePath="${dir.war}/module3.mxml" destPath="${dir.war}/module3.swf"/>
</target>

Need more Ant goodness?  I'm going through Ant in Action where I've picked up quite a few tips.

Currently
Ant in Action: Covers Ant 1.7 (Manning)
By Steve Loughran, Erik Hatcher
see related

1 comment:

  1. Fantastic article. I didn't understand the reason to use macrodef until now

    ReplyDelete