Introduction

The groovy frontend is about bringing Groovy into Ant, so you can write your build file in Groovy rather than in XML. The language change so the build files are less overloaded of useless characters. The build file structure is kept very similar to the XML one. So the learning of that new language is pretty straight forward, the translation of an XML build file into a groovy one is quite simple.

Actually, behind the hood you will write XML, but the Groovy way. And we will get all the benefit of a scripting language as Groovy. Just to mention at least one feature: no more need of the ant contrib for the use of the if-then-else structure, you will just use the Groovy one just like you would do in Java.

Hello world

Let's see throw a very simple example how easy is to write a Groovy build script. So here is a simple hello world build file:

project.name = 'hello-world-project'
project.basedir = '.'
project.defaultTarget = 'sayhello'

target(name: 'sayhello', description: 'Say hello to the world') {
  echo(message: 'Hello world !')
}
Here is what it would have been written in XML:
<project name="myproject" basedir="." default="sayhello">

  <target name="sayhello" description="Say hello to the world">
    <echo message="Hello world !" />
  </target>

</project>

As you can see a Groovy build script do not start with some root node. It is actually just a simple Groovy script which has some predefined function and variables and with which is associated an Ant project instance.

So as attached variable there is the Ant project, on which we set its name, base directory and default target. And there is a special function (that doesn't appear to be so but it is) "target" which will actually define a target. The definition of the target take into parameter a name, it also accept a depends, just like in XML.

In the target we are entering into the XML mode, where you write XML with the Groovy MarkupBuilder. In this mode a function call is interpreted as a XML tag, the parameters of the function call the attributes of the XML tag, and the closure (the brackets) the content (data and sub tags) of the XML tag.

And to run that script, nothing different from the classical XML build file invokation. Just name your Groovy build file build.groovy file and launch ant in the folder you put your file in:

$ ls
build.groovy
$ ant -p
Buildfile: build.groovy

Main targets:

Other targets:

 sayhello  Say hello to the world
Default target: sayhello
$ ant
Buildfile: build.groovy

sayhello:
     [echo] Hello world !

BUILD SUCCESSFUL
Total time: 0 seconds

Strings in Groovy

In Groovy there are two kinds of strings. Some are simple quoted which are just basic Strings. Some are double quoted where Groovy is parsing and replacing property values (usually called GString).

The ones with are double quoted are interpreted so that we can put variable in place of tokens, like in XML build script. But the difference with XML is that while the XML script find the variables in the Ant properties and the Ant references, Groovy is searching in its own variable space. The Groovy frontend of Ant is making the Ant properties and the Ant references available to Groovy. So setting and getting variables in Groovy is settings and getting Ant property and Ant references. The Groovy strings can be used as in the XML scripts. See:

  property(name: 'myprop', value: 'myvalue')
  echo(message: "the value of myprop is: ${myprop}")
But as the string is interpreted by Groovy, properties with dots cannot be accessed. For instance this following piece of script will fail.
  property(name: 'myprop.value', value: 'myvalue')
  echo(message: "the value of myprop is: ${myprop.value}") // myprop doesn't exist for Groovy
In that case you should prefer using the Ant tokenizer by using simple string, simple quoted:
  property(name: 'myprop.value', value: 'myvalue')
  echo(message: 'the value of myprop is: ${myprop.value}')

Escaping $ is different in GString and in simple strings. In Ant (the simple string), you will use $$. Whereas in Groovy you should use \$\$ (it is doubled because it will be also interpreted by Ant).

So it is recommended to only use simple string. And if you do want to refer to local variables, then use GString or just use simple string concatenation:

  def localVar = 'foo'
  echo(message: "using GSTring to get localVar: ${localVar}")
  echo(message: 'using simple string concatenation to get localVar: ' + localVar)

Groovy variables

As in every Groovy script, we can define local variables, variables local to the environment they are being used. For instance we can do:

  def myvar = 'foo'
  echo(message: "the value of myvar is: ${myvar}")

There are also variables which are properties of the enclosing class. In the Groovy frontend they are managed in a custom way. It tries to map these properties to the Ant environment of properties and references. So when setting a Groovy you will actually set an Ant property (which are immutable):

  myprop = 'foo'
  myprop = 'bar'
  echo(message: 'the value of myprop will be foo: ${myprop}') 

When trying to read a variable, the Groovy frontend search for

When trying to set a value on a variable, the Groovy frontend will check that:

Invoke a task

Invoking a task is pretty straight forward as soon as you know how to write it in XML. Let's see some examples.

In XML in Groovy
A simple task with a simple attribute
  <touch file="temp.txt" />
  touch(file: 'temp.txt')
A task with some text as value
  <echo>
    echoing some text
  </echo>
  echo('echoing some text')
A task with an attribute and some text as value
  <echo file="log.txt">
    echoing some text
  </echo>
  echo(file: 'log.txt', 'echoing some text')
A task with sub element
  <copy todir="${basedir}/target">
    <fileset dir="${basedir}/src">
      <exclude name="**/*.java" />
    </filset>
  </copy>
  copy(todir: '${basedir}/target') {
    fileset(dir: '${basedir}/src') {
      exclude(name: '**/*.java')
    }
  }
A task mixing text value and sub element
  <fail>
    oups, it failed !
    <condition>
      <not>
        <isset property="thisdoesnotexist"/>
      </not>
    </condition>
  </fail>
  fail('oups, it failed !') {
    condition() {
      not() {
        isset(property: 'thisdoesnotexist')
      }
    }
  }

Task definition and namesapce

In Ant we can import new task and type definitions into a build script. These tasks and types can get into the default namespace, or in a custom one.

When these types and tasks are defined in the default namespace, as in XML, nothing particular to do to call the imported tasks. For instance in XML we write:

<project>

  <taskdef resource="org/apache/ivy/ant/antlib.xml" />

  <target name="buildlist">
    <buildlist reference="build-path" ivyfilepath="ivy/ivy.xml" reverse="true">
      <fileset dir="projects" includes="**/build.xml"/>
    </buildlist>
  </target>

</project>
In Groovy it is as simple:
taskdef(resource: 'org/apache/ivy/ant/antlib.xml')

target(name: 'buildlist') {
  buildlist(reference: 'build-path', ivyfilepath: 'ivy/ivy.xml', reverse: 'true') {
    fileset(dir: 'projects', includes: '**/build.xml')
  }
}

If the tasks or types are defined in a custom namespace, then in XML we need to define a XML namespace and the call of the imported tasks are in that XML namespace:

<project xmlns:ivy="org.apache.ivy.ant">

  <taskdef resource="org/apache/ivy/ant/antlib.xml" uri="org.apache.ivy.ant" />

  <target name="buildlist">
    <ivy:buildlist reference="build-path" ivyfilepath="ivy/ivy.xml" reverse="true">
      <fileset dir="projects" includes="**/build.xml"/>
    </ivy:buildlist>
  </target>

</project>
In Groovy here is a pure Groovy way of playing with namespace:
import groovy.xml.NamespaceBuilder

taskdef(resource: 'org/apache/ivy/ant/antlib.xml', uri: 'org.apache.ivy.ant')
def ns = NamespaceBuilder.newInstance([ 'ivy': 'org.apache.ivy.ant' ], project.references['groovyfront.builder'])

target(name: 'buildlist') {
  ns.'ivy:buildlist'(reference: 'build-path', ivyfilepath: 'ivy/ivy.xml', reverse: 'true') {
    fileset(dir: 'projects', includes: '**/build.xml')
  }
}
The Groovy frontend of Ant is providing easier way of playing with them:
taskdef(resource: 'org/apache/ivy/ant/antlib.xml', uri: 'org.apache.ivy.ant')
def ivy = groovyns(uri: 'org.apache.ivy.ant')

target(name: 'buildlist') {
  ivy.buildlist(reference: 'build-path', ivyfilepath: 'ivy/ivy.xml', reverse: 'true') {
    fileset(dir: 'projects', includes: '**/build.xml')
  }
}
You can also specify a prefix to the groovyns so that in the log you don't get the verbose uri log, just the prefix. Without the prefix you can get in the log something like this:
  [org.apache.ivy.ant:buildlist] ...
If you specify a prefix this way:
def ivy = groovyns(prefix: 'ivy', uri: 'org.apache.ivy.ant')
Then you get in the log:
  [ivy:buildlist] ...

Import / Include

import being a keyword in Groovy, the import Ant task has been renamed in the Grovvy frontend include. So include is just a direct alias of import, the functionality is exactly the same.

As this Groovy frontend is just a frontend above Ant, we can actually mix XML build files with Groovy ones. Only the language is different.

In XML in Groovy
Import of a build.xml file
  <import file="common/build.xml" />
  include(file: 'common/build.xml')
Import of a build.groovy file
  <import file="common/build.groovy" />
  include(file: 'common/build.groovy')

Bringing Groovy into Ant

As the script is expressed in the Groovy language, we can use some of the Groovy language feature into our build script.

if-then-else

A very basic feature that doesn't exist in Ant is branching. Well, there are some on targets (if, unless), and there are some in ant-contrib, but there are quite difficult or painful to write.

So here is the basic use case:

target(name: 'default') {
  condition(property: 'fileExist', value: 'true', else: 'false') {
    available(file: 'file.txt')
  }
  if (fileExist) {
    echo('file.txt exists')
  } else {
    echo('file.txt was not found')
  }
}
Note that the property has to be set for alternative value otherwise Groovy will complain that is it looking up for a property that doesn't exist. If no alternative value is provided, then the script should look like:
target(name: 'default') {
  condition(property: 'fileExist') {
    available(file: 'file.txt')
  }
  if (project.properties['fileExist'] != null && fileExist) {
    echo('file.txt exists')
  } else {
    echo('file.txt was not found')
  }
}

You can even put an if-then-else structure in a task definition:

target(name: 'default') {
  copy(todir: basedir) {
    fileset(dir: '${basedir}/target') {
      if (excludeJava) {
        exlude(name: '**/*.java')
      }
    }
  }
}

Loops

for, while, do-while

try-catch

TODO: should we document that ?

functions

In Groovy you can define function (closure with arguments). So we can define function that call ant task, latter to be called in some target.

def copyselected = { dir, todir, includes ->
  copy(todir: todir) {
    fileset(dir: dir) {
      include(name: includes)
    }
  }
}

target(name: 'copy-java') {
  copyselected('${basedir}/src', '${basedir}/target/sources', '**/*.java')  
}

target(name: 'copy-txt') {
  copyselected('${basedir}/src', '${basedir}/target/jar', '**/*.txt')  
}

But use these with caution, as local variables override ant tasks. If you define a local variable that has the same name of an ant task, then you won't be able to call the tasks. For instance this will fail:

def echo = 3
echo(message: 'calling this task will fail')

Crappy code allowed by Groovy

Properties can be set everywhere Groovy allow variable setting. And properties are global to the entire script while standard Groovy property are only affected to the local closure. This can be confusing. This kind of code should be avoided:

target(name: 'set-prop') {
  copy(todir: basedir) {
    fileset(dir: '${basedir}/target') {
      excludePattern = '**/*.java'
      exlude(name: '${excludePattern}')
    }
  }
}
And we should prefer the folowing code that does exactly the same as above:
target(name: 'set-prop') {
  excludePattern = '**/*.java'
  copy(todir: basedir) {
    fileset(dir: '${basedir}/target') {
      exlude(name: '${excludePattern}')
    }
  }
}