Bending Maven with Ant

Need to bend Maven to your will wthout writing a maven plugin? Some hackery with Ant and Ant-Contrib‘s if task can solve many problems.

Lime Mojito’s approach to avoiding multiple build script technologies

We use maven at Lime Mojito for most of our builds due to the wealth of maven plugins available and “hardened” plugins that are suited to our “fail fast” approach to our builds. Checkstyle, Enforcer, Jacoco Coverage are a few of the very useful plugins we use. However, sometimes you need some custom build script and doing that in maven without using exec and a shell script can be tricky.

For more details see our post on Maintainable Builds with Maven to see how we keep our “application level” pom files so small.

We try and avoid having logic “spread” amongst different implementation technologies, and reduce cognitive load when maintining software by having the logic in one place. Usually this is a parent POM from a service’s perspective, but we try to avoid the “helper script” pattem as much as possible. We also strive to reduce the number of technologies in play so that maintaining services doesn’t require learning 47 different techologies to simply deploy a cloud service.

So how can you program Maven?

Not easily. Maven is “declarative” – you are meant to declare how the plugins are executed in order inside maven’s pom.xml for a source module. If you want to include control statements, conditionals, etc like a programming language maven is not the technology to do this in.

However, there is a maven plugin, ant-run, which allows us to embed Ant tasks and their “evil” logic companion, Ant Contrib, into our maven build.

Ant in Maven! Why would you do this?

Because maven is essentially an XML file. Ant instructions are also XML and embedding in the maven POM maintain a flow while editing. Property replacement between maven and ant is almost seamless, and this gives a good maintenance experience.

And yes, the drawback is that xml can become quite verbose. If our xml gets too big we consider it a “code smell” that we may need to write a proper maven plugin.

See our post on maintainable maven for tips on how we keep our service pom files so small.

Setting up the AntRun Maven plugin to use Ant Contrib.

We have this in our base pom.xml – see our Github Repository.

We configure the maven plugin, but add a dependency for the ant-contrib library. That library is older, and has a poor dependency with ant in it’s POM so we exclude the ant jar as below. Once enabled, we can add any ant-contrib task using the XML namespace antlib:net.sf.antcontrib.

For a quick tutorial on XML and namespaces, see W3 Schools here.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>3.1.0</version>
    <!--
      This allows us to use things like if in ant tasks.  We use ant to add some control flow to
      maven builds.

      <configuration>
          <target xmlns:ac="antlib:net.sf.antcontrib">
              <ac:if>
                  ...
  -->
    <dependencies>
        <dependency>
            <groupId>ant-contrib</groupId>
            <artifactId>ant-contrib</artifactId>
            <version>1.0b3</version>
            <exclusions>
                <exclusion>
                    <groupId>ant</groupId>
                    <artifactId>ant</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
</plugin>

Example one – Calculation to set a property in the pom.xml

Here we configure the ant plugin’s execution to calculate a property to be set in the maven POM.xml. This allows later tasks, and even other maven plugins, to use the set property to alter their execution. We use skip configurations a lot to separate our “full” builds from “fast builds” where a fast build might skip most checks and just produce a deliverable for manual debugging. This plugin’s execution runs in the Maven process-resources phase – before you write executions a solid understanding of the Maven Lifecycle is required.

Because the file links back to our parent pom, we do not need to repeat version, ant-contrib setup, etc. This example does not need ant-contrib.

The main trick is that we set exportAntProperties on the plugin execution so that properties we set in ant are set in the maven project object model. The Maven property <test.compose.location> is set in the <properties> section of the POM. It is replaced in the ant script before it is executed by ant seamlessly by the maven-antrun-plugin.

Note that the XML inside the <target> tag is Ant XML. We are using the condition task and an available task to see if a file exists. If it does then we set the property docker.compose.skip to true.

This example is in our java-development/pom.xml which is the base POM for all our various base POMs for jars, spring boot, Java AWS Lambdas, etc.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        <execution>
            <id>identify-docker-compose</id>
            <phase>process-resources</phase>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <exportAntProperties>true</exportAntProperties>
                <target>
                    <condition property="docker.compose.skip" else="false">
                        <or>
                            <equals arg1="${lime.fast-build}" arg2="true" />
                            <not>
                                <available file="${test.compose.location}" />
                            </not>
                        </or>
                    </condition>
                    <!--suppress UnresolvedMavenProperty -->
                    <echo level="info" message="docker.compose.skip is ${docker.compose.skip}" />
                </target>
            </configuration>
        </execution>
        ...

Example 2 – If we are not skipping, wait for Docker Compose up before integration test start

We use another plugin to manage docker compose before our integration test phase using failsafe for our Java integration tests. This older plugin was before docker had healthcheck support in compose files – we recommend this compose healthcheck approach in modern development. Our configuration of this plugin uses docker.compose.skip property to skip execution if set to true.

However we can specify a port in our Maven pom.xml and the build will wait until that port responds 200 on http://localhost. As ant-run is before the failsafe plugin in our declaration, its execution happens before the failsafe test run.

Note that the XML inside the <target> tag is Ant XML. The <ac:if> is using the namespace defined in the <target> element that tells ant to use the ant-contrib jar for the task. We are using the ant-contrib if task to only perform a waitFor if the docker.compose.skip property is set to false. This was performed earlier in the lifecucle by the example above.

This example is in our java-development/pom.xml which is the base POM for all our various base POMs for jars, spring boot, Java AWS Lambdas, etc.

<execution>
    <id>wait-for-docker</id>
    <phase>integration-test</phase>
    <goals>
        <goal>run</goal>
    </goals>
    <configuration>
        <target xmlns:ac="antlib:net.sf.antcontrib">
            <ac:if>
                <!--suppress UnresolvedMavenProperty -->
                <equals arg1="${docker.compose.skip}" arg2="false" />
                <then>
                    <echo level="info" message="Waiting for Docker on port ${docker.compose.port}" />
                    <waitfor maxWait="2" maxWaitUnit="minute">
                        <http url="http://localhost:${docker.compose.port}" />
                    </waitfor>
                </then>
            </ac:if>
        </target>
    </configuration>
</execution>

Conclusion

This aproach of ant hackery can produce small pieces of functionality in a maven build that can smooth the use of other plugins. Modern Ant has some support for if in a task dependency manner, but the older contrib tasks add a procedural approach that make the build cleaner in our opinion.

Why not Gradle? We have a lot of code in maven, and most of our projects fall into out standard deliverables of jars, Spring Boot jars or AWS Java Lambdas that are all easy to build in Maven. Our use of Java AWS CDK also uses maven so it ties nicely together from a limiting the number of technologies perspective. Given our service poms are so small due to our Maintainable Maven approach the benefits of Gradle seem small.

References

Mavenhttps://maven.apache.org
Maven AntRun Pluginhttps://maven.apache.org/plugins/maven-antrun-plugin/
Anthttps://ant.apache.org/manual/index.html
Ant Contrib taskshttps://ant-contrib.sourceforge.net/tasks/tasks/index.html
GitHub OSS standardshttps://github.com/LimeMojito/oss-maven-standards