Ant hooks using Groovy scripts via Scriptdef

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, _listenerMethod[Started | Finished].groovy, in the folder that matches the project name “demo1” here. If a hook script is not found, it searches for a matching hook script in the root folder. The found script is then evaluated.

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

Similar Posts:

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.

2 thoughts on “Ant hooks using Groovy scripts via Scriptdef”

Leave a Reply

Your email address will not be published. Required fields are marked *