Why -source and -target are not a convention

Convention over configuration is a simple concept. Maven incorporates this concept by providing sensible default behavior for projects. The Compiler Plugin is used to compile the sources of your project and provides some defaults for source/target levels for Java source code.

You may think that source/target levels is a part of this ‘convention over configuration’ concept. But actually they are not, so keep reading to learn why.

What problems to expect when building something that small as Kewin Sawicki‘s TimeAgo library with Maven? No, I’m not kidding since I’ve expected no problems at all but… Unfortunately, build of the current master branch fails:

[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Compilation failure

/org.github.timeago/src/main/java/org/github/timeago/TimeAgo.java:[129,23] cannot find symbol
symbol  : method format(java.lang.String,long)
location: class java.text.MessageFormat
...
[INFO] ------------------------------------------------------------------------

There are 5 such problems related to java.text.MessageFormat‘s method format(String pattern, Object... arguments). This method uses the varargs feature introduced in Java5, as well as the method itself, so the problem is obvious: Maven tries to compile TimeAgo with pre-Java5 JDK.

How is it posible?! It was taken for granted that Maven allows to avoid such problems but, for some reason, I can’t build it as is.

Here we have a problem, that Martin Fawler explained some time ago:

One of the prevailing assumptions that fans of Continuous Integration have is that builds should be reproducible. By this we mean that at any point you should be able to take some older version of the system that you are working on and build it from source in exactly the same way as you did then.

Let’s digg into some details and try to find out why it’s unreproducible and how easy it will be to find a configuration that ‘just works’.

Problem: unreproducible build

So, right now I have 3 different JDK installed:

  • usr/lib/jvm/java-6-openjdk
  • usr/lib/jvm/java-1.5.0-sun-1.5.0.22
  • usr/lib/jvm/java-6-sun-1.6.0.24

Lets add 4 versions of Maven2 to the mix:

  • Apache Maven 2.1.0 (r755702; 2009-03-18 21:10:27+0200)
  • Apache Maven 2.2.1 (rdebian-4)
  • Apache Maven 3.0.2 (r1056850; 2011-01-09 02:58:10+0200)
  • Apache Maven 3.0.4-SNAPSHOT (rNON-CANONICAL_2011-04-06_23-20_mn; 2011-04-06 23:20:51+0300)

and try to build TimeAgo with mvn clean compile using bash-matrix-project😉 approach described earlier:

 
Apache Maven 2.1.0 (r755702; 2009-03-18 21:10:27+0200) FAIL
Apache Maven 2.2.1 (rdebian-4) FAIL
Apache Maven 3.0.2 (r1056850; 2011-01-09 02:58:10+0200) SUCCESSFUL
Apache Maven 3.0.4-SNAPSHOT (rNON-CANONICAL_2011-04-06_23-20_mn; 2011-04-06 23:20:51+0300) SUCCESSFUL

As build fails for Maven 2.x but succeeds for Maven 3.x, regardless of JDK been used, definetely, it’s not reproducible. Let’s try to find the reason for such strange behavior.

Why?

Fire build again but this time we’ll set source/target levels explicitly, via command-line options -Dmaven.compiler.source=1.5 and -Dmaven.compiler.target=1.5 and it succeeds for all environments.

#1: missing source/target levels in POM

Looking at the project’s POM, we see nothing related to compiler plugin’s settings. This way build depends on default values for source and target levels implemented in currently effective version of the compiler plugin. I strongly disagree with comments like

Maven has a strong preference for «convention over configuration» which means that requiring the source and target entries in the pom is not really reasonable IMO. Instead, the defaults should be documented better, and perhaps the error message could be adjusted to point to a FAQ entry that explains things for new users.

on MCOMPILER-57. Comments on MCOMPILER-80 like

Using default source/target levels in a build tool is not a good idea since it makes both the success of the build and its artifacts dependent on the environment in which it was run, contrary to the goals of a reproducible build. A given module or tree of modules will have a certain source level it requires in order to compile, so increments to this in the POM should be coversioned with source code changes to use new language features. Target level (usually but not necessarily the same as source level) is even more important to specify explicitly, because it is not obvious when you get it wrong — until your bytecode fails to load on an older customer JVM which you had intended to still support.

or this one

Just that, the source/target levels should be decided by its owners/developers, NOT BY Maven. It’s the project owner/developer’s responsibility to decided the expected source/target levels and set the values in the project POM. Thus to have reproducible builds across computers/jdks.

reflect a reality much better.

Maven2 help plug-in is not that helpful

I tryed to inspect the project’s effective POM with maven-help-plugin, as you may find answers like this. For Maven 2.x it’s quite small, due to some bug in Maven2 and not in plugin:

[preserved_text e28c4bc0cef72ff1780a91c079421884 /]

After this change, your build will fail if some plugins are missing valid versions, with a clear reason:

[INFO] ------------------------------------------------------------------------
[INFO] Building Time Ago
[INFO]    task-segment: [test-compile]
[INFO] ------------------------------------------------------------------------
[INFO] [enforcer:enforce {execution: enforce-plugin-versions}]
[WARNING] Rule 0: org.apache.maven.plugins.enforcer.RequirePluginVersions failed with message:
Some plugins are missing valid versions:(LATEST RELEASE SNAPSHOT are not allowed )
org.apache.maven.plugins:maven-clean-plugin.    The version currently in use is 2.3
org.apache.maven.plugins:maven-resources-plugin.        The version currently in use is 2.3
org.apache.maven.plugins:maven-deploy-plugin.   The version currently in use is 2.5
org.apache.maven.plugins:maven-compiler-plugin.         The version currently in use is 2.0.2
org.apache.maven.plugins:maven-install-plugin.  The version currently in use is 2.3.1
org.apache.maven.plugins:maven-surefire-plugin.         The version currently in use is 2.7.2
org.apache.maven.plugins:maven-site-plugin.     The version currently in use is 2.0
org.apache.maven.plugins:maven-jar-plugin.      The version currently in use is 2.2

[INFO] ------------------------------------------------------------------------
[ERROR] BUILD ERROR
[INFO] ------------------------------------------------------------------------
[INFO] Some Enforcer rules have failed. Look above for specific messages explaining why the rule failed.

However, you still have to be careful when choosing versions. Using wrong version, you may, by accident, narrow down a range of the tools available to your consumers.

For instance, one of the real projects I know about, is a Flex application that requires some specific version of a plugin to be used and that very version works with an exact version of Maven 2. So using any plugins that require anything higher than Maven 2.0.8, IIRC, is a ‘no-no’ for this project.

Recent changes in Jenkins is another example. It started using com.cloudbees.maven-license-plugin to check license headers in source files. Initially this plugin required Maven 3.x and introducing this dependency to Jenkins made it impossible to build Jenkins with Maven 2.x for no apparent reason. Fixing this problem, in this particular case, was easy for two reasons:

But this not true for most projects. I would rather be more consvervative in such situations and not use the latest (but not always greatest) versions without clear benefits.

Summary

One apple a day… When it refers to the Maven compiler plugin, you:

  • must explicitly define source and target levels
  • should specify version of plugin (but please, be sane)

These small changes will make your builds much more stable and resistant to changes in environments while obeying its contracts like ‘I’m Java5-compatible’.