Why you should use the Maven Ant Tasks instead of Maven or Ivy
March 8, 2009 23 Comments
Like many others I really like Ant and I intend to keep using it until something better comes along. And I strongly feel that when you try to use *only* Maven for everything, you give up control over a few things.
Are there things that Ant does better than Maven? Oh yes, IMHO. Here are a few examples of typical Java web-app build related stuff that turn out to be “edge cases” in Maven meaning that you have to struggle, read up on obscure plugins or resort to ugly hacks in order to get things to work.
Replacing text in files only for development mode
<target name="jetty-setup-dev" unless="production.mode"> <echo>making webapp changes for development mode...</echo> <replace file="target/${war.name}/WEB-INF/web.xml" token="org.apache.wicket.protocol.http.WicketFilter" value="info.jtrac.wicket.devmode.ReloadingWicketFilter"/> </target>
The example above shows how I switch to a custom Wicket filter in web.xml that supports hot-class-reloading and debug logging – but only in development mode. The Ant replaceregexp task can give you even more regex driven find-and-replace goodness.
Capturing input from the user
<target name="confirm"> <input message="Are you sure?" validargs="y,n" addproperty="input"/> <condition property="abort"> <equals arg1="n" arg2="${input}"/> </condition> <fail if="abort">User aborted.</fail> </target>
All the best trying to do that in Maven. Oh and using the maven-antrun-plugin doesn’t count – that’s cheating ;)
ZIP a bunch of files from where *you* want into what *you* want
<target name="dist-jtrac-src"> <mkdir dir="target"/> <zip destfile="target/jtrac-src.zip"> <zipfileset dir="nbproject" prefix="jtrac/nbproject"/> <zipfileset dir="etc" prefix="jtrac/etc"/> <zipfileset dir="src" prefix="jtrac/src"/> <zipfileset dir="." includes="*.*" prefix="jtrac"/> </zip> </target>
Maven apologists will promptly point you to the great Maven Assembly Plugin. Sorry, no thanks. I sincerely tried to understand how to use *that* piece of work a few times in the past but gave up in despair.
Execute an arbitrary Java class with control over everything (args, classpath, etc.)
<target name="hbm-export" depends="compile"> <input message="Hibernate Dialect:" addproperty="dialect"/> <java classname="org.hibernate.tool.hbm2ddl.SchemaExport" fork="true"> <classpath> <path refid="test.classpath"/> <path path="target/classes"/> </classpath> <jvmarg value="-Dhibernate.dialect=${dialect}"/> <arg value="--text"/> <arg value="--delimiter=;"/> <arg value="--output=target/schema.sql"/> <arg value="src/main/resources/*.hbm.xml"/> </java> </target>
This is a target I keep around to forward-engineer the DDL from my Hibernate entities on demand, for any database of the user’s choosing – just enter the dialect at the prompt.
There are times when even Ant may not have all the answers and a technique I use (or misuse) effectively is to use a custom Java class to extend Ant. So I don’t even need to learn how to create custom Ant tasks and mess around with the Ant classpath ensuring that the build remains “xcopy” friendly. Now, the “Maven way” to handle custom stuff is to write a custom plugin. Which is the easiest thing in the world right? Heh – don’t get me started.
There are quite a few more examples I can think of but that’s not the main point of this blog post. Hopefully I have made a case for “why Ant” above. A deal-clincher for me is the fact that all popular IDE-s have excellent support for Ant. NetBeans (which I use) takes it to a whole different level of tight integration – for e.g. you can map Ant targets to toolbars, shortcuts and things like that. Yes, IDE support for Maven is getting there but not as good as Ant IMO.
But am I saying that you shouldn’t use Maven at all? Not really. Let me explain.
For Java projects, the essential one thing that Maven does well and Ant cannot – is dependency management. So IMO the way to go is to use *both* Maven and Ant. The problem is that Maven obviously goes out of the way to *not* use Ant.
Fortunately, there *is* a way to get the information about dependencies from your Maven “pom.xml” project file across to your Ant “build.xml” file. I am referring to the Maven Ant Tasks. The thing is – there is very little documentation out there on how to use this. My conspiracy theory is that the Maven guys don’t want you to know about the existence of the Maven Ant Tasks. This is backed up by the fact that the two options for using Ant mentioned here in the official Maven FAQ cunningly avoid linking to the actual Maven Ant Tasks! (update: more than half a year after this blog post was published, the Maven FAQ was updated with a new entry “How can I use Maven features in an Ant build?” :)
I’ve explored custom plugin based approaches for bridging Maven and Ant with some success, and I’ve gotten some good feedback and even patches for the NetBeans specific integration that I tried for. I wasn’t aware of the Maven Ant Tasks until recently because as I said, the Maven guys have done a good job of making it hard to find. So I hope this post helps improve things on this front.
How to use the Maven Ant Tasks
I’ll use my Seam / JSF vs Wicket comparison project as a reference. First create a standard pom.xml like you would normally as in this example, just focus on dependencies.
This is the magic snippet that goes into build.xml that establishes the link between Maven and Ant:
<target name="mvn-init" unless="compile.classpath" xmlns:artifact="urn:maven-artifact-ant"> <typedef resource="org/apache/maven/artifact/ant/antlib.xml" uri="urn:maven-artifact-ant" classpath="lib/maven-ant-tasks.jar"/> <condition property="maven.repo.local" value="${maven.repo.local}" else="${user.home}/.m2/repository"> <isset property="maven.repo.local"/> </condition> <echo>maven.repo.local=${maven.repo.local}</echo> <artifact:localRepository id="local.repository" path="${maven.repo.local}"/> <artifact:pom file="pom.xml" id="maven.project"/> <artifact:dependencies pathId="compile.classpath" filesetId="compile.fileset" useScope="compile"> <pom refid="maven.project"/> <localRepository refid="local.repository"/> </artifact:dependencies> <artifact:dependencies pathId="test.classpath" filesetId="test.fileset" useScope="test"> <pom refid="maven.project"/> <localRepository refid="local.repository"/> </artifact:dependencies> <artifact:dependencies pathId="runtime.classpath" filesetId="runtime.fileset" useScope="runtime"> <pom refid="maven.project"/> <localRepository refid="local.repository"/> </artifact:dependencies> </target>
- You need just the maven-ant-tasks JAR in “lib” as per line #03, if you are picky about that, you can even consider downloading it automatically as part of the build. You can find details on how to do that in this blog post on how to use the Maven Ant Tasks instead of Ivy by Josh Suereth (who deserves credit for some of the ideas in this post).
- I prefer bootstrapping the Maven integration within a target instead of globally for the build so that targets that don’t depend on Maven can run faster. Also, I don’t have to “pollute” the entire build.xml with the XML namespace stuff, it is isolated to just within this one target.
- The unless=”compile.classpath” in line #01 is a nice way to ensure that the bootstrapping happens only once even when Ant executes multiple targets that depend on each other.
- Lines #04 – 06 allows the user to override the default local repository location by having a “maven.repo.local” property entry set for e.g. in “build.properties”. You can even use relative paths here – so all your dreams of having Maven builds that work even when you copy the project folders across to some other machine – can come true ;)
- The rest intializes classpath and fileset variables for compile, test and runtime scopes – which should be sufficient for most WAR projects.
- Something that really surprised the heck out of me is that you don’t need Maven installed for all this to work! You are getting all the benefits from a single teeny tiny JAR file. Spread the word, people!
Now you can write your compile target like this, note the ‘classpathref” on line #03:
<target name="compile" depends="mvn-init"> <mkdir dir="target/classes"/> <javac srcdir="src/main/java" destdir="target/classes" classpathref="compile.classpath"/> <copy todir="target/classes"> <fileset dir="src/main/java" excludes="**/*.java"/> </copy> <copy todir="target/classes" failonerror="false"> <fileset dir="src/main/resources"/> </copy> </target>
I normally use Jetty for development (like this) so I don’t need to assemble a WAR most of the time. People who haven’t tried this really don’t know what they are missing, huge time saver – not to mention tight IDE integration for things like debugging. But when the time comes to build a WAR this is how you could do it (note the reference to the Maven fileset at line #11):
<macrodef name="war-exploded"> <attribute name="deployDir" default="target/${war.name}"/> <sequential> <copy todir="@{deployDir}"> <fileset dir="src/main/webapp"/> </copy> <copy todir="@{deployDir}/WEB-INF/classes"> <fileset dir="target/classes"/> </copy> <copy todir="@{deployDir}/WEB-INF/lib" flatten="true"> <fileset refid="runtime.fileset"/> </copy> </sequential> </macrodef> <target name="war-exploded" depends="compile"> <war-exploded/> </target> <target name="war" depends="war-exploded"> <zip destfile="target/${war.name}.war" basedir="target/${war.name}"/> </target>
Note that I don’t even use the Ant “war” task, I just ZIP ;) Maven evangelists would have you believe that Ant leads to un-maintainable, verbose build scripts that are hard to re-use across projects. I call bullsh*t. Look at the Ant “macrodef” usage above which means that I can reuse the WAR routine and do something like this for Tomcat later:
<target name="tomcat-war-exploded" depends="war-exploded"> <war-exploded deployDir="${tomcat.home}/webapps/${war.name}"/> </target>
What about Ivy?
Whoa, I never thought that this would turn out to be such a long post, let me end by addressing the question: should projects use Ivy instead?
My opinion is NO. Actually, the trigger for this blog post is that I recently joined a project-team already using Ivy and I’m having a hard time adjusting. Anyway, trying to be objective, here are my reasons:
- One of the good things about Maven is that it enforces the closest thing we have in the Java world to a standard directory layout structure. I find it hilarious that Ivy falls into the same trap as Maven in that they can’t say good things about Maven just like the Maven guys avoid saying good things about Ant. So you won’t find any recommendations to use a standard project structure in the Ivy “best practices” documentation ;) Ivy projects tend to use ad-hoc directory structures, this is what I have observed.
- Maven obviously has been around longer than Ivy, this translates into more documentation, more users who are used to working (and struggling ;) with it etc.
- IDE project descriptor creation support: For example, Ivy has no official way to create your Eclipse project descriptor files for you. This itself would be a reason for me to avoid Ivy. There are some ways to manage your Eclipse classpath like IvyDE which honestly doesn’t have much documentation and something like this and this Google code project which don’t look too stable. If you have a Maven pom.xml and Maven installed, just do “mvn eclipse:eclipse” and you can even add “-DdownloadSources=true” – and you can ensure that even things like the “output folders” like “target/classes” and “target/test-classes” – are standard across your team.
- Needless to say, using the Maven Ant Tasks approach gives you a smooth migration path to Maven. If you do it right, nothing prevents Maven die-hards on your project team from using only the pom.xml and avoiding Ant completely. This is how I have set up the JTrac build for the last 3 years.
- Maven is positioned as a “project management and comprehension” tool (whatever that means ;) and there is certainly a good ecosystem of plugins that for e.g. can generate your project web-site, run Checkstyle reports, etc.
- As someone already using Maven for a while, I found it really unsettling that having to use Ivy for another project was resulting in a ton of JAR duplication on my hard disk. And why on earth did the Ivy guys decide to re-invent the structure in which JAR files are stored in the local repo? So you can’t reuse your Maven repo or copy stuff around.
- Something that the Ivy guys would hate to admit is that ultimately you end up downloading JARs from the Maven public repositories only. So all that talk of doing a better job than Maven kind of sounds flat. People are quick to blame Maven when resolving of dependencies fails to work as expected but this is usually the fault of whoever setup the metadata in the repositories. There’s not much that Ivy can do to solve that problem.
- I think Maven multi-project support where you keep common stuff in a parent POM and have projects depend on each other is better than how you would do it in Ivy (although I haven’t explored this fully yet).
- The only thing that appears to be compelling about Ivy is that doing “exclusions” is far less verbose than how you would have it in Maven. But if the Maven guys just fix this one issue …
As an example of the kind of thinking that Ivy encourages which I don’t agree with is this quote from a blog post by Dan Allen (Seam committer and author of “Seam In Action”):
But the most absolutely vital feature of Ivy, and what makes it infinitely more useful than Maven 2, is the fact that you can disable transitive dependencies.
I’m outspoken about my position on transitive dependencies. I see them as both evil and a silly device designed for novices (and people with way too much time on their hands). It makes your build non-reproducible and unstable and in the end causes you more work than the work you were attempting to eliminate by moving to Maven 2. This “feature” really is Maven’s boat anchor. Trust me on this one, it’s trivial to define which libraries your application depends on. There really aren’t that many! And you can put all the worrying aside about exclusions.
All I’ll say is that if you disable transitive dependencies, then what is the point. You might as well write a script that downloads a bunch of JAR files listed in a properties file. And about the statement that “there really aren’t that many!” – I really don’t agree. Take Seam itself as an example of a framework that has so many dependencies (and sub dependencies) and expecting developers to keep track of the sub-dependencies (instead of the project-owners who should be updating the Maven repository metadata correctly) – just seems wrong to me.
Did I miss any reasons for not using Ivy? Do let me know in the comments!
recent comments