Ant BuildListener interface is used to add a hooks feature that invokes a Groovy script mapped to build events.
One way of managing or customizing a complex Ant build is to use hook scripts that are executed at defined points in the Ant build life cycle. A use case for this is the reuse of build scripts in multiple build environments, like a build server vs a local workstation build, another is to add extra auditing.
Approach
We create a listener that take the project name and the current running target’s name to locate a matching hook file. This file contains a Groovy script to be invoked. Further, we allow a global hook script folder that contains hook scripts that would be applied if there are no matching hook scripts in a project level folder, named after the Ant build script project name.
Of course, this is Groovy specific, but it would be very easy to make this into a more generic hook invocation facility.
Similar ideas
Version control systems use the concept of hook scripts. Before a commit, for example, a hook script can ensure that the commit log contains the correct metadata. In AOP we can advise at ‘before’ and ‘after’ pointcuts.
Ant listeners
An Ant listener will be alerted to:
- build started
- build finished
- target started
- target finished
- task started
- task finished
- message logged
In this demo we use only the target events.
Implementation
In the Ant script below, we install a listener before any ant targets are invoked. A scriptdef defines the listener as a script file.
Demo build script
<project name="demo1" default="compile" basedir="."> <path id="libs"> <fileset dir="lib"> <include name="groovy-all-1.8.6.jar" /> </fileset> </path> <!-- Groovy library --> <taskdef name="groovy" classname="org.codehaus.groovy.ant.Groovy" classpathref="libs"/> <!-- sets a BuildListener to the project --> <scriptdef name="set-listener" language="Groovy" classpathref="libs" src="src/main/groovy/com/octodecillion/ant/Hook.groovy"> </scriptdef> <!-- install the listener --> <set-listener/> <target name="compile"> <echo>Hello compile world!</echo> </target> </project>
Now we want to use the following actual Groovy hook scripts. The start target hook is defined at the project level, and the finished target hook is defined at the root level.
demo1/compile_targetStarted.groovy
println "hook: {project=${event.project.name},target=${event.target.name},when=pre,event=$event}"
root/compile_targetFinished.groovy
println "hook: root,{target=${event.target.name},when=post,event=$event}"
Listener implementation
The actual listener implementation is shown below. It uses the build event object to find the project name and target name. Then it searches for the matching hook script,
In the example, the hooks/demo1 folder contains file: compile_targetStarted.groovy. Thus, when the Ant runtime invokes the targetStarted listener method on the ‘compile’ target, the listener will invoke the targetStarted hook script.
package com.octodecillion.ant import org.apache.tools.ant.BuildEvent import org.apache.tools.ant.Project import org.apache.tools.ant.SubBuildListener; import static groovy.io.FileType.FILES /** * Ant build listener that invokes groovy hook scripts. * * @author josef betancourt * */ class HookListener implements SubBuildListener { def project Map rootHooks = [:] Map projectHooks = [:] String TARGETHOOK = "target" enum When{ STARTED('Started'),FINISHED('Finished') String name When(s) {this.name = s} } /** */ def HookListener(project){ this.project = project // cache the root hooks new File("hooks/root").eachFileMatch FILES, ~/.*\.groovy/, { file -> rootHooks.put(getBaseName(file.name), file.text) } // cache the project hooks new File("hooks/$project.name").eachFileMatch FILES, ~/.*\.groovy/, { file -> projectHooks.put(getBaseName(file.name), file.text) } } @Override public void targetFinished(BuildEvent event) { evokeTargetHook(event, When.FINISHED) } @Override public void targetStarted(BuildEvent event) { evokeTargetHook(event, When.STARTED) } /** Invoke the 'started' or 'finished' root or target hook script */ def evokeTargetHook(BuildEvent event, When when){ def b = new Binding() b.event=event def shell = new GroovyShell(b) def hookName = "${event.target.name}_${TARGETHOOK}${when.name}" def stored = projectHooks[hookName] if(stored){ shell.evaluate(stored) }else{ // use the cached root hooks if found def hook = rootHooks[hookName] if(hook){ shell.evaluate(hook) } } } /** */ private String getBaseName(fileName){ fileName.replaceFirst(~/\.[^\.]+$/, '') } /** */ private String getSuffix(fileName){ def parts = fileName.split("\\.") parts.size() > 0 ? parts[parts.size()-1] : '' } //@formatter:off @Override public void subBuildFinished(BuildEvent event) {} @Override public void subBuildStarted(BuildEvent event) {} @Override public void buildFinished(BuildEvent event) {} @Override public void buildStarted(BuildEvent event) {} @Override public void messageLogged(BuildEvent event) {} @Override public void taskFinished(BuildEvent event) {} @Override public void taskStarted(BuildEvent event) {} //@formatter:on } // wire in the listener def listener = new HookListener(project) listener.project = project project.addBuildListener(listener) // end Script
Output
C:\Users\jbetancourt\workspace-4.3\AntAroundAdvice>ant Buildfile: C:\Users\jbetancourt\workspace-4.3\AntAroundAdvice\build.xml compile: hook: {project=demo1,target=compile,when=pre,event=org.apache.tools.ant.BuildEvent} [echo] Hello compile world! hook: root,{target=compile,when=post,event=org.apache.tools.ant.BuildEvent} BUILD SUCCESSFUL Total time: 1 second
Around Advice
While possibly useful, a further powerful potential is to allow the bypass of an invocation of a target altogether. One way of doing this is to use a library like XMLTask to modify the build script. This would have to be done before the script is parsed by Ant of course.
Alternative
One issue with described approach is that the target Ant build scripts must be modified (very minor) to hook in the hooks feature. Using the Ant API itself to add targets and change the target dependency chains might be possible. So is changing Ant itself using AspectJ is also possible.
Of course if very complex scenarios are necessary to manage legacy builds, it may be time to replace Ant with something like Gradle.
Project Layout
The project layout used to the test this hook approach is shown below:
| .classpath | .gitignore | .project | build.xml | tree.txt | +---.settings | org.eclipse.jdt.core.prefs | org.eclipse.jdt.groovy.core.prefs | +---docs | .gitignore | Backup of docs.wbk | docs.docx | +---hooks | +---demo1 | | compile_targetStarted.groovy | | | \---root | compile_targetFinished.groovy | +---lib | ant-antlr.jar | ant.jar | groovy-all-1.8.6.jar | groovy-all-2.2.0-rc-1.jar | +---src | \---main | \---groovy | \---com | \---octodecillion | \---ant | Hook.groovy |
Further reading
- ABuilds use of Ant hooks So far, the only Ant hook mention I’ve located
2 thoughts on “Ant hooks using Groovy scripts via Scriptdef”