Wednesday, August 02, 2006

Using Ant with Maven 1 and Maven 2

I've been using Maven 1 for well over a year now. Not long after I had started using it and generally liking it, the Maven guys decided to drop support for it and came out with 2.0. Everything changed again. This time the directory layout conventions are different (which completes negates the benefits of having adopted a convention in the first place), and it's more difficult now to just use bits of Ant when you need to. I do not look so kindly on such capriciousness; and there are other complaints about Maven besides. Finally, I decided that although like most developers I get more excited about by build tools than I probably should, in the end, it's just a way to build code. Whether it is Ant or Maven, it's really just overhead; at some point, after having constructed beautiful and clean works of code, you need to actually build it and publish an artifact that others can use. And so, in order to lessen this overhead and get some real work done, I decided to move to the more established, more well-known Ant.

However, I now have quite a few projects using Maven 1. And eventually, I'll probably have to co-exist with Maven 2 projects. What's more, I think the best part of Maven is the dependency management; and it gets better in Maven 2 with support for transitive dependencies. Is there a way to have my cake and eat it too? Indeed there is. In a moment of brilliance, the Maven guys decided to take their golden dependency management code and make it available as Ant tasks. Turns out this stuff actually works. I've been able to
  • use Maven2 to manage dependencies for my Ant builds, with all of their transistive goodness
  • publish Maven2 artifacts to Maven2 repos
  • publish Maven1 artifacts to Maven1 repos
  • use either Maven1 or Maven2 artifacts
  • treat my Maven repository as both a Maven1 and a Maven2 repository
Getting Started

The documentation on the Antlib for Maven 2.0 page is decent. Installing the Ant tasks is fairly straightforward. You add an XML namespace declaration.

<project name="ExpBuilder" basedir="." artifact="urn:maven-artifact-ant">
...
</project>



Then what I wanted to do was to completely bootstrap the maven dependency by downloading it with the task. For this, I add the following to my "init" target:


<mkdir dir="${lib}/maven" />
<get src="${maven.artifact.ant.url}"
dest="${lib}/maven/maven-artifact-ant-dep.jar" usetimestamp="true">

<typedef resource="org/apache/maven/artifact/ant/antlib.xml"
uri="urn:maven-artifact-ant">
<classpath>
<pathelement location="${lib}/maven/maven-artifact-ant-dep.jar" />
</classpath>
</typedef>


Where ${maven.artifact.ant.url} is defined in the build.properties file and could be a remote url or a local "file:///" url. Some people instead check this jar into CVS along with the code and use Maven to download the other dependencies, but I wanted something completely bootstrapped.

Defining Maven Repos

Here's what I used, again, in my "init" method, to define the needed maven repos. Here's what I have:



<artifact:remoterepository id="extreme.http.maven1"
url="http://www.extreme.indiana.edu/dist/java-repository" layout="legacy" />
<artifact:remoterepository id="extreme.http.maven2"
url="http://www.extreme.indiana.edu/dist/java-repository" />
<artifact:remoterepository id="extreme.scp.maven1"
url="scp://rainier.extreme.indiana.edu/l/extreme/java/repository" layout="legacy" />
<artifact:remoterepository id="extreme.scpexe.maven1"
url="scpexe://rainier.extreme.indiana.edu/l/extreme/java/repository" layout="legacy" />
<artifact:remoterepository id="extreme.scp.maven2"
url="scp://rainier.extreme.indiana.edu/l/extreme/java/repository" />
<artifact:remoterepository id="extreme.scpexe.maven2"
url="scpexe://rainier.extreme.indiana.edu/l/extreme/java/repository" />



Basically I have one maven repository that I define 6 different mappings for. This allows me to treat the same Maven repository as a Maven 1 and a Maven 2 repository. The difference between Maven 1 and Maven 2 repository descriptions is in the layout="legacy" attribute. I started by using the scp:// style remote repository deployment, but found that the scpexe:// style actually works better since it uses my local ssh environment, i.e., it uses my ssh-agent and I don't have to type in my password several times just to publish a single artifact.

Defining Dependencies

This part is where Maven 2 comes in. See the getting started guide for help getting going with Maven 2 if you're unfamiliar with it. Basically I created a very simple pom.xml file that contains only the basic info like groupId, artifactId, version, and then a list of dependencies. The Maven Ant tasks provide a way to read in and reference your POM:

<artifact:pom id="maven.project" file="pom.xml" />
That's it!

Pulling in the Dependencies

One of the "gotchas" I discovered while putting this together was that using these tasks to retrieve artifacts from a Maven 1 repo is time consuming. This is because it tries to retrieve the .pom file for all artifacts, even if the jar in question is already in cache. Since my Maven 1 repository has a lot of artifacts in it that were deployed there with Maven and hence do not have .pom files, Maven spends a lot of extra time trying to download these files (on the order of a minute or two for about 20 dependencies). Hence, I decided to create an Ant target, called maven-dependencies, that would get the dependencies and copy them into ./lib directory. That way I only have to run it occasionally. The downside to this is that I have my compile target, for example, not depending on the maven-dependencies target, but it really should depend on it. Here it is:

<artifact:dependencies pathId="maven.compile.classpath"
filesetId="maven.compile.fileset" usescope="compile">
<remoteRepository refid="extreme.http.maven1" />
<remoteRepository refid="extreme.http.maven2" />
<pom refid="maven.project" />
</artifact:dependencies>

<artifact:dependencies pathId="maven.provided.classpath"
filesetId="maven.provided.fileset" usescope="provided">
<remoteRepository refid="extreme.http.maven1" />
<remoteRepository refid="extreme.http.maven2" />
<pom refid="maven.project" />
</artifact:dependencies>

<artifact:dependencies pathId="maven.runtime.classpath"
filesetId="maven.runtime.fileset" usescope="runtime">
<remoteRepository refid="extreme.http.maven1" />
<remoteRepository refid="extreme.http.maven2" />
<pom refid="maven.project" />
</artifact:dependencies>

<copy todir="${lib}">
<fileset refid="maven.compile.fileset" />
<mapper type="flatten" />
</copy>

<mkdir dir="${lib}/provided"/>
<copy todir="${lib}/provided">
<fileset refid="maven.provided.fileset" />
<mapper type="flatten" />
</copy>

<mkdir dir="${lib}/runtime" />
<copy todir="${lib}/runtime">
<fileset refid="maven.runtime.fileset" />
<mapper type="flatten" />
</copy>

Another gotcha is that although I have dependencies with scope "provided" (more on dependency scope in Maven 2), those dependencies don't get translated into a fileSet or path. The attribute "useScope" here means actually the set of dependencies needed at this scope. So for example my "compile" scoped set of dependencies includes my "provided" scoped dependencies and my "compile" scoped dependencies, since dependencies from both of those scopes are needed to compile my code. Likewise, my "runtime" scoped set of dependencies includes "compile" and "runtime" dependencies since those are needed at runtime (and "provided" dependencies are expected to be, well, provided). I use "./lib/*.jar" as my default classpath in the rest of the build script.

Deploying to Maven 1 and Maven 2 repositories

Again, I wanted to use the same repository for both Maven 1 and Maven 2 style artifacts. Deploying is pretty straightforward; here are my targets:

<target name="maven2-deploy" depends="jar">
<artifact:install-provider artifactId="wagon-ssh-external"
version="1.0-alpha-5" />
<artifact:deploy file="${build}/${maven.project.artifactId}-${maven.project.version}.jar">
<remoteRepository refid="extreme.scpexe.maven2" />
<pom refid="maven.project" />
</artifact:deploy>
</target>

<target name="maven1-deploy" depends="jar">
<artifact:install-provider artifactId="wagon-ssh-external"
version="1.0-alpha-5" />
<artifact:deploy file="${build}/${maven.project.artifactId}-${maven.project.version}.jar">
<remoteRepository refid="extreme.scpexe.maven1" />
<pom refid="maven.project" />
</artifact:deploy>
</target>


Again, I'm using scpexe as the install-provider because this picks up my ssh-agent environment on Mac and Linux workstations. On Windows you might want the scp install-provider, although it should be possible to configure scpexe install-provider to use certain executables, such as PuTTY.

Finally, let's build a portlet!

Building a portlet in Maven was pretty easy; just issue "maven war". Okay, well, not that easy, because it probably wouldn't pick up the right things, and if you support deployment to multiple portals, then that typically means you need to have multiple web.xml files. So, here's my war-gridsphere target for creating a portlet war for the GridSphere portal server.

<target name="war-gridsphere" depends="compile" description="Creates a web application resource file">
<war destfile="${build}/${maven.project.artifactId}.war"
webxml="${webapp}/WEB-INF/web.xml.gridsphere" compress="true">
<fileset dir="${webapp}" excludes="**/web.xml"/>
<fileset dir="${src.conf}">
<include name="*.properties"/>
</fileset>
<lib dir="${lib}/runtime" includes="*.jar"/>
<classes dir="${build}/classes"/>
</war>
</target>
Well, that wasn't so bad.

No comments: