Skip to main content

Using JavaScript to make Apache Ant less painful

Posted by emcmanus on September 20, 2010 at 10:14 AM PDT

 I feel a bit guilty saying bad things about Apache Ant. It's free, it's available everywhere, and a lot of volunteers have put a lot of work into making it what it is. You can very quickly and easily make a build file for simple Java projects. But. It seems to have been more accreted than designed, and if you try to use the core system to accomplish anything outside the most basic stuff you can look forward to hours staring alternately at those horribly illegible XML files and that huge and frustratingly disorganized online manual. So I was pleasantly surprised recently to realize that you can use JavaScript directly inside your build.xml files to avoid having to fight with Ant's enormous but haphazard collection of concepts and tags.

It could well be that I am the last person in the world to learn this and that everyone else has been using JavaScript to work around Ant's deficiencies since 2001. Perhaps not, though, because it used to be the case that if you wanted to use a scripting language with Ant then you needed to go off and get the relevant jars and put them where Ant could find them. What's changed is that you can now pretty much assume that you will have at least JDK 6 everywhere (it came out in December 2006), which means you automatically have a JavaScript engine. If Ant is running on JDK 6, it can find and use this engine without any configuration.

This is not the only solution when you run up against the limits of what you can straightforwardly do with Ant. Several other approaches exist. One is to use XSLT to generate your build.xml files. I think that should only ever be the very last of last resorts. Imagine someone coming upon your system for the first time and trying to understand how it works. Where do they start? How do they find out what will actually happen when the system is built? Where do they start if they need to change something?  If there's one thing I've learnt over the years it's that an extra degree of "meta-ness" dramatically increases the difficulty of learning and using a system. Certainly it can also make it more powerful, but in this case there are less meta-ish alternatives that are no less powerful.

A second approach is to write your own Ant tasks using Java code. Aside from the constant nagging question "why I am having to do this?" as you do so, the result is again a build system that is hard to understand. Part of your build file will be devoted to compiling the custom tasks; and of course the source of those tasks, showing what they actually do, is off in some other files somewhere.

A third approach is to use ant-contrib to add to Ant things that should already be there, in particular the and especially constructs. (I'm sure I'm not alone in finding it completely inexplicable that Ant still does not come with these tasks as standard.) The right way to use ant-contrib is not to plonk it in your Ant install directory, because then you're using a non-standard Ant and your projects will not work if you transfer them to another machine. Instead, you should copy ant-contrib-1.0b3.jar (or whatever the version is) into the lib directory of your project, and explicitly activate it in your build.xml with a line like this:

<taskdef resource="net/sf/antcontrib/antlib.xml" classpath="${basedir}/lib/ant-contrib-1.0b3.jar" />

Using ant-contrib is actually a fine solution to many of the most annoying Ant deficiencies, and you may prefer it to the final approach, which is my subject here: using JavaScript.

Using JavaScript in Ant tasks

Here is a complete Ant project that says hello using JavaScript:

    <?xml version="1.0" encoding="UTF-8"?>
    <project name="hello" default="default">
        <target name="default">
            <script language="javascript">println("hello, world")</script>
        </target>
    </project>
   

Now, the first thing to say is that I am living dangerously here. If ever my script contained a character with meaning to XML (basically < and &) then the whole Ant build would fail with a mysterious parse error. So to avoid that danger, I need to add some extra XML gobbledygook (you can tell I love XML very very much):

        <script language="javascript"><![CDATA[
            println("hello, world")
        ]]></script>
   

Obviously I could have achieved as much with the task, so let's look at a real problem.

Checking classpath dependencies

Probably Zig and Zag and Dustin the first time I realized that I didn't like Ant very much was when I tried to solve the following problem. I was working on a big application that had been split into several Ant projects. One of these projects (call it Big) depended on a number of other projects (let's say Zig and Zag). We were using NetBeans, which is quite good at managing inter-project dependencies. It automatically set things up so when I built Big it would first build Zig and Zag. The problem is that Big needed the resulting Zig.jar and Zag.jar, and it used them in a very expensive processing step that would take about 30 seconds. So I really wanted to ensure that that processing step would only happen if a new Zig.jar or Zag.jar had been built. If building Zig and Zag had made no change, because Zig.jar and Zag.jar already existed, then I didn't want to reconstruct Big for nothing.

The obvious way to solve this problem would be to insert a simple check in Big's build.xml that would test whether Big.jar was more recent than Zig.jar and Zag.jar. I didn't much like that, though. What if I added a new dependency, on project Zog? Would I have to remember to add Zog to the check? Surely I could write things in a way that didn't require me to list the upstream jars explicitly. Surely I could derive that list from something that NetBeans would set automatically when the project dependencies changed. Surely.

Let's state the problem a bit more formally. I have a property ${javac.classpath} that contains a list of jars and directories separated by File.pathSeparator. I want to set a property ${jar.up.to.date} only if Big.jar is more recent than every jar mentioned in ${javac.classpath}. I think it should be straightforward to do that in Ant, and it might even be that there is a straightforward way to do it, but even after spending hours puzzling over filesets and selectors and filterchains and resources and patternsets I never succeeded in finding one. Something is wrong when you have to spend endless time trying to see if it is possible to fit the random available jigsaw pieces together to make up the picture you want. I basically ended up writing ${javac.classpath} to a file and performing a series of transformations on that file so that it ended up being the required definition, which I could read in using . Ugh.

Here's how I could solve that problem with the construct which ought to be in Ant but is at least in ant-contrib:

    <for list="${javac.classpath}" delimiter="${path.separator}" param="jar">
        <sequential>
            <condition property="jar.out.of.date">
                <and>
                    <matches string="@{jar}" pattern=".*\.jar" />
                    <not>
                        <and>
                            <available file="@{jar}" />
                            <uptodate srcfile="@{jar}" targetfile="${dist.jar}" />
                        </and>
                    </not>
                </and>
            </condition>
        </sequential>
    </for>
   

(It's easier to compute "not up to date" and use "unless=" here; each loop iteration gets a chance to set jar.out.of.date and it only remains unset if no iteration sets it. Once it's set a property stays set, so there's no easy way to achieve the opposite effect, though of course I could then set jar.up.to.date based on jar.out.of.date.)

But maybe I don't feel like fighting against Ant and XML today, searching through the manual to find the magic and and tags I need. What would I write if I used JavaScript instead?

    <script language="javascript"><![CDATA[
        importClass(java.io.File)
        var base = new File(basedir)
        var targetJarName = project.getProperty("dist.jar")
        var targetJar = new File(base, targetJarName)
        var classpath = project.getProperty("javac.classpath")
        var parts = classpath.split(File.pathSeparator)
        var uptodate = targetJar.exists()
        for each (var part in parts) {
            if (part.endsWith(".jar")) {
                var jar = new File(base, part)
                uptodate &= jar.exists() && jar.lastModified() < targetJar.lastModified()
            }
        }
        if (uptodate) {
            project.setProperty("jar.up.to.date", "true")
        }
    ]]></script>
   

That's arguably no simpler than the version, though it is vastly simpler than my solution with writing out files and ing them again. But the big win is really familiarity: every Java developer should know enough JavaScript syntax and enough of the basic Java APIs to understand what is going on here. Relatively few developers are so unfortunate as to have had to master enough Ant arcana to be able to plunge without fear into the version.

You do need to know a few basic things about the environment in which scripts execute:

  • The script variable project is set to Ant's Project object, and you can use it to get and set Ant properties with getProperty and setProperty (or, better, setNewProperty), as above, as well as executing other tasks and even adding new targets.
  • Every property, target, and resource collection (such as filesets and dirsets) whose name is a valid JavaScript identifier appears as a script variable of the same name. That's how we can reference ${basedir} as above.
  • As in the first example, println() is available if you need to debug your script.
  • You cannot assume that you are executing in any particular directory so in general you will need to make absolute File values using basedir as above, or project.getBaseDir() which is already a File.

(Incidentally I share Attila Szegedi's mystification at the absence of online Ant API docs at apache.org.)

You have the full power of the standard Java APIs available to you, but you also have access to Ant's APIs if you prefer to use Ant's internal logic to solve the problems it's best at, such as dependency checking. For example, another way to write the script above, which is more like the version, is this:

    <macrodef name="check-against-jar">
        <attribute name="jar" />
        <sequential>
            <condition property="jar.out.of.date">
                <not>
                    <and>
                        <available file="@{jar}" />
                        <uptodate srcfile="@{jar}" targetfile="${dist.jar}" />
                    </and>
                </not>
            </condition>
        </sequential>
    </macrodef>
    <script language="javascript"><![CDATA[
        importClass(java.io.File)
        var classpath = project.getProperty("javac.classpath")
        var parts = classpath.split(File.pathSeparator)
        for each (var part in parts) {
            if (part.endsWith(".jar")) {
                var checkAgainstJar = project.createTask("check-against-jar")
                // println(checkAgainstJar.getClass()) to discover that it's a MacroInstance
                checkAgainstJar.setDynamicAttribute("jar", part)
                checkAgainstJar.execute()
            }
        }
    ]]></script>
   

So now when I'm faced with a problem that doesn't seem to be easy to solve with plain Ant, I know I have two alternatives to banging my head against the wall of Ant's obtuseness: ant-contrib and Blue Meanies JavaScript.

But what do I know?

 I've hacked my fair share of buildfiles but I wouldn't consider that I'm any kind of expert on Ant. Am I being unduly mean in my opinions? Is it actually really easy to solve problems like mine but I am too dim to see? Have all the cool kids abandoned Ant long ago to move on to a shiny new alternative? Let me know in the comments.

Comments

dependset

I think you could have done everything you want with the dependset task. It will delete Big.jar if it is out of date. Then you just make another task to only build Big.jar if absent. You may have course have to adopt dependset for all your other dependencies... http://ant.apache.org/manual/Tasks/dependset.html Now, if you could just tell me how to run an LDAP query with javascript or with an ant task....

dependset

Greg, yes I could use <dependset>, but only after I had somehow converted my classpath into a <fileset>. <pathconvert> allows you to do the reverse conversion, but I don't *think* there's a non-eldritch way to do this one with the standard tasks. Though I could be mistaken.

As I mentioned, javascript code within an Ant file has full access to the Java APIs, so you should be able to do an LDAP query using JNDI (javax.naming.*).

the best approach is to do

the best approach is to do the reverse - compose/create your classpath as an ant path/fileset when you need it in semi-colon form convert it to a property using property/refid, as per http://www.jguru.com/faq/view.jsp?EID=471917

An RFE for NetBeans

Well, yes, but the problem in this case was that I was working from a property ${javac.classpath} that is maintained by NetBeans on the basis of what you configure in its configuration dialogues. So I guess we could say that NetBeans should define a fileset or maybe filelist and derive the ${javac.classpath} property from it. Except I'm not sure that fileset and/or filelist have the required semantics. It's a mess.

<path>

You can create a path from a semi-colon separated property very easily.
Actually no need for a strict "fileset" these days with ant. It now uses "Resources" which are more generic. e.g.

<dependset>
<sources>
<path path="build/file1.txt;build/file2.txt" />
</sources>
<targets>
<file file="build/out.jar" />
</targets>
</dependset>

<path> indeed!

Well, hey, that's good to know! So I guess I need a different example of why you might need ant-contrib or JavaScript. Although given the amount of rooting around I did without finding your solution, I think I'll still be inclined to jump into JavaScript at the slightest provocation.

Thanks,

Éamonn 

Gradle

For our latest project, we moved from Ant to Gradle.

Scons

Yes, that is more sensible. There's scons, too, and maybe others in the same mold. Anything (even home-grown bash scripts) but ant.

too kind

I think, if anything, that you are being too kind. Once you acknowledge that some things are better done in script than in XML, why not bite the bullet and write configuration scripts rather than use ant? What need do you have of ant at all? The declarative approach to building projects never struck me as bringing anything to the table: a script can do everything an ant file can and is usually much more readable. Another cargo cult, if you ask me.