Background
Recently I’ve been working on an implementation of hybris, a Java e-commerce package whose build system is based on Ant. The Maven Ant Tasks plugin is available for importing dependencies, but I wasn’t happy with the default tasks that wrap this plugin.
They make no attempt to purge old dependencies that have been removed from the POM. The build script keeps your Eclipse projects up to date with most changes, but does not update classpath settings when dependencies change. Most importantly, the Maven plugin uses the excludeTransitive=true option, so Maven will not import the dependencies of your dependencies.
Fortunately, every step in the build process includes a “callback”, which is a hook where you can inject your own customizations. I decided to forgo the default task for updating Maven dependencies, and do it myself in one of the extension’s callbacks.
Cleaning the extension’s /lib directory was easy, as was setting excludeTransitive=false to resolve the full dependency hierarchy. However, overwriting the Eclipse project’s .classpath file was a bit more tricky than I expected. Basically, I needed to:
- Write to .classpath the fixed information that never changes
- Iterate through each dependency in the extension’s /lib directory, and append a line in .classpath for it
- Append the closing </classpath> tag at the bottom of .classpath
Eek! It’s been awhile since I’ve made heavy use of Ant, and I suddenly remembered that it doesn’t really have general loops or iterator constructs. The open-source ant-contrib bundle comes installed with hybris, and it provides useful tasks such as <for> and <foreach>. However, it is poorly-documented, and hasn’t been updated in forever (much like Ant!). Also, the property scoping rules are very quirky, and it looks like I would have to create a completely separate Ant task just for the code within my loop.
There has to be an easier way to put a simple loop within an Ant script, right?
JavaScript in Ant?
I have been primarily living in the Maven world for some time now. I knew that ad-hoc scripting is a standard feature in more modern build systems, such as Gradle (Groovy), Buildr (JRuby), and sbt (Scala). However, after a little research I was shocked to discover that good-old Ant was there a decade ago!
With the <script> task, you can drop free-form code directly into an Ant task anywhere you like. All JSR-223 languages are available, such as Groovy, JRuby, Jython, and more. However, if you don’t want to add any further dependencies to your build classpath, then JavaScript is already baked-in to Ant out of the box. It’s been there this entire time! Who knew?
Say what you will about JavaScript, but it is certainly up to the challenge of a basic “for each” loop. So to update my Eclipse .classpath file, I added a code snippet like this to the relevant hybris Ant callback:
... <script type="text/javascript" language="javascript">// <![CDATA[ importClass(java.io.File) // Properties set by " <property>" elements in the surrounding Ant task var libdir = new File( project.getProperty("libdir") ) var classpathfile = new File( project.getProperty("classpathfile") ) // STEP 1: OVERWRITE ".classpath" AND GET THE FILE STARTED echo = project.createTask("echo"); echo.setFile(classpathfile); echo.setAppend(false); echo.setMessage( '<classpath>\n\t Blah blah blah... fixed items that don't change'"/>' ); echo.perform(); // STEP 2: ITERATE THROUGH THE DEPENDENCIES AND APPEND A CLASSPATH ENTRY FOR EACH for each (var jarFile in libdir.listFiles()) { if (jarFile.getName().toLowerCase().endsWith(".jar")) { echo = project.createTask("echo") echo.setFile(classpathfile) echo.setAppend(true) echo.setMessage( '\n\t<classpathentry exported="true" kind="lib" path="lib/' + jarFile.getName() + '"/>' ) echo.perform() } } // STEP 3: WRAP UP THE ".classpath" FILE echo = project.createTask("echo") echo.setFile(classpathfile) echo.setAppend(false) echo.setMessage( '\n</classpath>\n' ) echo.perform() classpathfile.close() // ]]></script> ...
A few things to notice:
- The entire Java API is accessible to JavaScript. On line 4, I am importing the java.io.File class to handle my file I/O.
- JavaScript can access any Ant properties set in the surrounding task context, with project.getProperty(“<name>”). See lines 7-8.
- Scripts can access and create Ant tasks themselves, with project.createTask(“<name>”). See lines 11-15.
- Ant tasks are basically treated as POJO’s, and setter methods are available to set their attributes. Here, echo.setAppend(true) is equivalent to the XML tag <echo append=”true”>.
- After setting any necessary attributes, the perform() method actually invokes the Ant task.
This experience has caused me to take a fresh look at Ant. I still prefer Maven for most greenfield projects, and in a special case I might look for an excuse to gain more experience with Gradle. However, if you are inheriting someone else’s build scripts, or just like the maturity of Ant and how well it integrates with things, then script injection can be a powerful reward to make Ant worth your while.