Saturday, September 23, 2006

Moving from Ant to Maven2

My build system at work is Ant. I haven't used Maven2 in real-world (commercial) environments before, but I have used it enough in my personal projects to convince me that this would be a good move to make, and eliminate quite a bit of build maintenance drudgery that we currently take for granted.

The main benefits of using Maven2 as a build system are standardized directory structure and targets (or goals in Maven), no more JAR files in CVS, automatically building the project descriptor files for various IDEs (Eclipse, IDEA) from the POM, support for project documentation site building, the APT wiki-style documentation language which can be automatically translated to HTML in the project website, built in support for generating various kinds of project reports, and many others. Disadvantages include limited support for building targets that deviate from the norm, limited support for multi-environment deployment, and a steep learning curve (which I am not totally over yet).

Anyway, the first application to be built with Maven2 would be a small webapp, which depended for most of its functionality on Java code written in another web application, which was built with Ant. Both systems are expected have pretty rapid code churn, so it is important that the new webapp be able to pick up the latest JAR files from the older application as they are built.

Fortunately, the older webapp is being continuously integrated using Anthill. So it should be possible to get the latest JAR file from Anthill, since Anthill is configured to rebuild every half an hour if it detects changes in the version control system. This article describes the infrastructure I had to set up to get this working.

Creating a local repository

Since the JAR file from the older webapp is an internal JAR file, we cannot upload to the ibiblio or codehaus repositories, so we need our own repository. For this I chose maven-proxy, a simple web application which exposes a remote file based repository over HTTP, as well as forwards requests it cannot server from its local repository to upstream servers such as ibiblio or codehaus.

I put maven-proxy on the same host, and in the same Tomcat instance on which Anthill was running. The local repository was set up to be a sub-directory maven-repository under the tomcat user's home directory.

The maven-proxy distribution comes as a WAR (Web ARchive) file, which one needs to drop into the webapps directory. However, the first time it explodes, the web.xml is incomplete and the maven-proxy.properties file is non-existent, so it will throw errors and refuse to function. You need to supply the maven-proxy.properties (see the Configuration page for examples), and point the "maven-proxy.properties" configuration parameter to the location of your properties file, as shown below:

1
2
3
4
5
  <context-param>
    <param-name>maven-proxy.properties</param-name>
    <param-value>/opt/tomcat/webapps/maven-proxy/WEB-INF/classes/maven-proxy.properties</param-value>
    <description>Controls where maven-proxy grabs its properties from</description>
  </context-param>

Once this is done, you can access the repository with an URL like this:

1
http://build.mycompany.com:8080/maven-proxy

Under the maven-repository are two other subdirectories, internal and external. The internal subdirectory is for JAR files generated by modules written within the company, and the external subdirectory are for JAR files that are written by external third parties, but may not be available at ibiblio for whatever reason. I had thought of having a third subdirectory central which would really be the cache for the ibiblio and codehaus stuff, but that did not work, because I would have to declare this repository to be a mirror in settings.xml which would not be available in the project CVS, so it would not be suitable for team work. So JAR files such as commons-lang, etc would still be pulled directly from ibiblio and its mirrors.

Getting Anthill to write to the repository

The second part was to add functionality to the build.xml file so it would add the JAR file it built to the local repository directory. Recall that I set up the repository on the same machine as the build machine and owned by the same tomcat user that Anthill is also running under, so there are no permission issues.

To do this, I added an antcall into the target that generated the JAR file.

1
2
3
4
  <target name="jar" depends="compile,build.properties">
    ...
    <antcall target="copy-jar-file-to-maven-repository" />
  </target>

This target will first check to see if it is being run by Anthill, in which case a deployDir property would be set, to tell Anthill where to write the JAR after its done building. So we test for the property, and if we are running on Anthill, then we also copy to the maven-repository/internal directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  <!-- Check if we are running on AntHill -->
  <target name="check-if-anthill">
    <condition property="is.anthill" value="true">
      <not>
        <equals arg1="${deployDir}" arg2=""/>
      </not>
    </condition>
  </target>

  <!-- If we are running on AntHill, then execute this target to copy jar
       to the internal maven repository -->
  <target name="copy-jar-file-to-maven-repository" depends="check-if-anthill" if="is.anthill">
    <property name="mavenRepo" value="${tomcatHome}/maven-repository/internal" />
    <property name="jarLocation" value="${mavenRepo}/com/mycompany/oldapp/SNAPSHOT" />
    <mkdir dir="${jarLocation}" />
    <copy file="${build}/${jar.name}" tofile="${jarLocation}/oldapp-SNAPSHOT.jar" />
    <exec dir="${jarLocation}" executable="/usr/bin/md5sum" os="Linux" output="${jarLocation}/oldapp-SNAPSHOT.jar.md5">
      <arg value="oldapp-SNAPSHOT.jar" />
    </exec>
    <copy file="${basedir}/bin/pom-template.xml" tofile="${jarLocation}/oldapp-SNAPSHOT.pom" />
    <exec dir="${jarLocation}" executable="/usr/bin/md5sum" os="Linux" output="${jarLocation}/oldapp-SNAPSHOT.pom.md5">
      <arg value="oldapp-SNAPSHOT.pom" />
    </exec>
  </target>

Setting up other JARs into the repository

Now came the one time task (or relatively infrequent) of putting JAR files, such as activation.jar, etc, which do not live in open repositories like ibiblio because Sun does not permit it, and commercial third party JARs from our vendors. I used a very minimal POM template file and a shell script to do this relatively painlessly. They are shown below.

1
2
3
4
5
6
7
8
9
<!-- A minimal POM template -->
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>_groupId_</groupId>
  <artifactId>_artifactId_</artifactId>
  <packaging>jar</packaging>
  <version>_version_</version>
  <dependencies />
</project>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash
EXTERNAL=/home/tomcat/maven-repository/external
JARHOME=/opt/tomcat/webapps/oldapp/WEB-INF/lib

# $1 = name of jar file (with jar ext)
# $2 = groupId
# $3 = artifactId
# $4 = version
function buildjar {
  todir=`echo $2 | sed -e 's/\./\//g'`
  echo "Building jar $1 to repo/$todir/$3/$3-$4.jar"
  cp $JARHOME/$1 $3-$4.jar
  md5sum $3-$4.jar > $3-$4.jar.md5
  cat pom_template.xml | sed s/_groupId_/$2/ | sed s/_artifactId_/$3/ | sed s/_version_/$4/ > $3-$4.pom
  md5sum $3-$4.pom > $3-$4.pom.md5
  mkdir -p $EXTERNAL/$todir/$3/$4
  cp $3-$4.* $EXTERNAL/$todir/$3/$4
  rm $3-$4.*
}

buildjar "activation.jar" "activation" "activation" "1.1"
...

Looking up the local repository from the POM

Finally, the moment of truth...getting all the JARs from our local repository. Actually, its not that momentous, I had been trying out the POM after every little change, so I was fairly confident that it will all work this time. Basically, I added the following two repositories into my pom.xml file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  <!-- Repository points to local proxy -->
  <repositories>
    <repository>
      <id>external</id>
      <name>Repository for non-company JARS not in ibiblio</name>      <url>http://build.mycompany.com:8081/maven-proxy/repository/external</url>
    </repository>
    <repository>
      <id>internal</id>
      <name>Company JAR repository</name>
      <url>http://build.mycompany.com:8081/maven-proxy/repository/internal</url>
      <layout>default</layout>
    </repository>
  </repositories>

and ran the command:

1
$ mvn clean compile test

which went through fine. The only other thing I wanted to do was to make my repository a copy of ibiblio, so each time I get a new JAR, I can write it to my local repository and never have to go looking at ibiblio again, unless its specified as a SNAPSHOT or I change the version. But that requires me to declare this repository to be a mirror, and the declaration can only happen in the settings.xml file, which lives in each developer's personal repository under ~/.m2. We may do that yet, but so far this setup seems to be holding up well.

When people think of moving to Maven2, they are overwhelmed at the sheer learning curve and complexity of the product. They are also overwhelmed at the amount of work it would take to convert their existing Ant installations to Maven2 installations. Often the first thought when you are confronted with a problem such as mine is to first convert the existing code to Maven2, then make it part of a multi-module build. A quicker alternative would be to move ahead with the approach described above.

4 comments (moderated to prevent spam):

Unknown said...

I know that Maven is now popular with developers. However as an end user, ant is good enough for me.
Reading through the article, Maven needs to have more knowledge and some thing which are not right yet. What I mean is that for a normal user, is it Maven really helpful?

Sujit Pal said...

Hi Sann, I would agree with you about Maven2's relative immaturity compared to Ant. Not sure what you mean by "normal user", I am assuming you mean a Java developer? If so, as a Java developer, there are some things I really like about Maven2 though:

* Dependency Management - you could probably use Ant with Ivy, but this is all built in with Maven.
* Archetypes - I like the fact that all apps look the same and is standards-based.
* Automatic building of IDE descriptors - When building a new project, I don't have to spend time "setting up" my project by pointing and clicking at JAR files.
* Jetty plugin for webapp development - this is so convenient its almost addictive.

However, I think its premise that "the pom.xml knows all" is coming back to bite it in the form of overcomplexity in implementing any but the simplest targets. Unlike Ant, where one can string a few tags (which correspond closely with OS level commands), you can build a new target, in Maven2 one has to write a plug-in, something that most average developers (including me) probably don't know how to or even want to do.

I recently read this blog post on a commenter's blog, which provides a more thorough coverage of this.

Unknown said...

I recently set up our own maven repo. Nexus offers a GUI to store jars and to setup the proxy, I recently installed it. You're able to specify your own repo within a settings.xml file where common first party artifacts go... http://nexus.sonatype.org/

Sujit Pal said...

Thanks Jon.