Maven Profiles and “The Maven Way”

Stephen Connolly's picture

Maven Profiles

When people first come to Maven, one of the most confusing things to grasp is when exactly should you use profiles.

Profiles provide this almost magical ability to morph the effective pom.xml and so it can be tempting to use them to completely tweak the results of your build, because in essence each active profile gets to alter almost the entire model.

But, as we shall see, that way madness lies. Some of the things that profiles let you do have good and proper use-cases, so it is not a case that profiles provide too much… more a case that in providing the necessary flexibilities it is a side-effect that evil pom.xml abuse becomes possible.

Before we go any farther I should really take the time to explain how the “Maven way” sees an “ideal build” working

The ideal build

When I was growing up there was a game called “Top Trumps” where you had a deck of cards with details of different cars, engine size, maximum RPM, top speed, etc. The idea was that you would compare cards based on a criteria that you chose, and the best match won. My reason for bringing this up is that there was no “ideal” car. One car might be a great car when comparing top speed, but if your opponent chose to compare on number of passengers you were dead in the water.

So when we are discussing an ideal build, we need to pick the category that matters most to us. This does not mean that the other categories are not important to us, more that they are not our primary priority.

Maven’s primary priority is to ensure that the build is reproducible. There are different levels of reproducibility:

  1. If I check out a tag from source control on Monday and do a build, and then I checkout that same tag again on Tuesday and run the build again, I should end up with the same (or equivalent, e.g. timestamps may be embedded in the binaries) artifacts

  2. If I check out a tag from source control and you checkout the same tag, and we both have the same versions of the same build toolchains and the same operating systems on our machines, then we should end up with the same (or equivalent) artifacts.

  3. If I check out a tag from source control and you checkout the same tag, and we both have the same build toolchains (but may have different versions and/or different operating systems), then we should end up with the same (or equivalent) artifacts.

  4. If I check out a tag from source control and you checkout the same tag, and we both have similar build toolchains (I might have gcc you might have cc, or I might have the JDK’s javac and you are using Eclipse’s java compiler, etc) then we should end up the same (or equivalent) artifacts.

Now not every build toolchain will allow the same level of reproducibility. For example, if you are building platform dependent native binaries then you may only be able to build ARM binaries on an ARM architecture. Certainly if your build too runs tests, then you will only be able to run tests of those ARM binaries on a system with ARM architecture.

Now as we encounter different build toolchains it starts to become apparent that sometimes we just need to get those build toolchains onto the machine that is doing the build. So Maven says, in the interests of a build being reproducible, an ideal Maven build will actually set up all the build toolchains you need for you as part of the build.

In other words, in the ideal world, when my laptop dies, all I should need to do is: get a new one; install Maven; checkout my source code; and build. Maven will go to the internet and download all the required build toolchains, install them, and then finish the build… producing the exact same build artifacts as my old laptop produced.

The “real” build

That is the ideal build.

The real world is messier, but try to keep that vision in your mind when you think about Maven.

In the real world we have to make compromises with respect to build reproducibility. That does not mean it is wrong to make those compromises. More that we need to be aware of the compromises we are making and our reasons for making them. Some examples of the kind of compromises I have seen are:

  • Your build needs to work on Java 5 and Java 6 JDKs. Normally that involves ensuring that your production code only uses the Java 5 API and none of the new features involved in the Java 6 API. But you can have issues with things. For example, Java 6 repeats the hell of XML api integration into the standard classpath only with JAXB. So if you need to use a different version of JAXB from that bundled in Java 6 you need to use the endorsed mechanism to allow your unit tests to run on all versions of Java 6 (versions after 1.6.0_13 may not require as much of the dancing games). If you are lucky enough to just need the standard JAXB that ships with the JVM, then you hit a legitimate use of profiles, namely adding the JAXB dependency with <scope>test</scope> when Maven is running on Java 1.5. The evil temptation is to do the same for the runtime classpath… but the correct Maven way is to produce two separate artifacts, one for use on Java 1.6 and the other for use on Java 1.5. Usually you will have these artifacts as separate Maven modules.

  • Your database access unit tests are not true unit tests in that they need a database in order to function. To keep with the spirit of the Maven way, we would use a pure Java database such as Derby or HyperSQL. Each test would fire up an isolated in-memory instance of the database for use in that test case (or perhaps set of test cases if they can clean up correctly). Butwe have a problem, because some of the SQL DDL is specific to a particular SQL vendor (MySQL, Oracle, PostgreSQL, Microsoft SQL Server, etc). Now we have to compromise. Some of the compromises that people make in this case are:

    • Require that the developer has the database server installed on their machine using a specific port and username/password. This adds to the steps required to get up and running.

    • Use a shared database instance on a remote server. This requires that the tests do not mind running at the same time when multiple developers run the tests, or else you need to put some locking logic into your tests, or have the developers co-ordinate when they will run the test suite. Also what happens when the developer is running over VPN. Not a great solution for open source projects either, as the database server now becomes a security risk (as the server connection details including username/password has to be baked into the pom.xml or some other file in the source tree. Never mind the performance hit that these tests will incur.

    • Use a profile or system property to enable the tests. Typically a system property is a good way to do this. Have the system property be the JDBC URI for the database server, or maybe a trio of system properties. The tests can then use, for example, JUnit 4 style assumesto skip the database tests when there is no database URI provided, e.g.

      @Test public void newAccountsGet10Credits() throws Exception {
      String jdbcUri = System.getProperty("database.uri");
      assumeThat(jdbcUri, is(not(nullValue())));

      }

    • Write or find a Maven plugin that will create configure a database instance for the tests and set a system property with the connection details for that configured instance, and then tear down the instance afterwards. This would still require the developer to have the database software installed on their machine, but they probably don’t need to keep the database server running all the time, and they can work on different branches at the same time because the plugin would give concurrent sessions different database instances. And if you have crafted it correctly, you might have the tests skip when the developer doesn’t have the database software installed on their machine.

    • Write or find a Maven plugin that will install the database software for the developer, configure it with a database instance for the tests, etc. This may only work if the developer is on a limited number of operating systems and/or CPU architectures.

  • Your integration tests need the application to be deployed on a specific application server. Normally you can get away with either using Jetty and the jetty-maven-plugin or Tomcat and the tomcat-maven-plugin to run your web application. But if your application uses application server specific features, you may have to resort to some of the same type of tricks used for the database issue.

  • Your application code requires some specific build tools to generate some of the build artifacts. For example, maybe you need the the thrift compiler to generate the source code for your remoting API, or maybe you need the WiX toolchain to generate the Microsoft Windows installer for your product. Some solutions I have seen to these issues include:

    • All the previous tricks as for the application server and database server.

    • Making the parts of the build that require this separate build tool into independently released projects. This allows developers to work on the parts of the project that don’t need the toolchain. You might even set up a continuous integration server (such as Jenkins) which has the toolchain configured and a build job to release a new version of the artifacts.

The take-home message

Don’t worry if you have to make compromises to the Maven way to make your build work. Just try to make as few as possible and try to keep them as close to the Maven way as you can.

What might appear strange, is that keeping close to the Maven way actually makes it easier to move away from Maven if you need to. For example if your unit tests skip when the database is not available, if you change to a different build tool, those tests will continue to skip until you get that build tool set up to provide the database connection information.

The Maven way is really about making your build as easy as possible, with the fewest hacks. My observation is that people who don’t like Maven typically seem to love leaving lots of little hacks in their build files. To each their own!

—Stephen Connolly

CloudBees

www.cloudbees.com

Stephen Connolly has nearly 20 years experience in software development. He is involved in a number of open source projects, including Jenkins. Stephen was one of the first non-Sun committers to the Jenkins project and developed the weather icons. Stephen lives in Dublin, Ireland - where the weather icons are particularly useful. Follow Stephen on Twitter and on his blog.