Whenever I teach a Maven training class someone invariably asks me to give some advice for migrating a large, complex Ant project to Maven. Toward the end of the class, I’ll take questions:
Participant: “Could you give us some guidance for migrating Ant projects to Maven? Is there a process that you recommend to make it easier.”
My (honest) answer: “If it’s a complex project, it won’t be an easy battle. Before you go down this road you need to understand what you are signing up for. It can be very complex, you may end up interrupting an active development cycle, and once you evaluate all of your options you might find it easier to first migrate to a repository manager. Maven’s not the big win, moving a repository is.”
In other words, I often find myself trying to discourage swapping build tools just for the sake of swapping build tools. While I do believe that Maven is preferable to Ant, I think that the build space often suffers from a belief that the grass really is greener on the other side. It might be, but is it greener enough to justify that work stoppage that is involved in taking a big important project and moving it to a different build technology? Often the answer is no. If it isn’t related to making money, switching a build system is often the last thing an enterprise wants to do.
In this post I’m going to talk about the process of migrating build tools starts with a repository manager.
Behold the Crazyant Legacy Build
This system is inspired by a real system I worked on for a brief period of time a few years ago. To protect the innocent I’ll call it the “Crazyant Legacy project”. It was awful, and by awful I mean I wanted to run away as fast as I could only an hour after the client granted me access to the source code.
“Wait? Why do these boxes overlap?”, you ask as I lean back in my chair laughing at your fright. “The code modules, they overlap because they share source trees”, I continue, “there is only but one source directory in this project, yet there are many builds. If you’ve done battle with this project, you’ll understand that this diagram is only an approximation. Any one of the hundreds of developers that use this project could define a new project at any moment. Good luck.”
Ok, it wasn’t that dramatic, but it was close. You had Ant projects that were referencing sourcepaths using relative directories like, “../../../../../shared”, and you had a directory called “dependencies/” which contained, you guessed it, all of the binary dependencies in unmarked jars like “SpringProj22.jar”. You pick up a JAR named “SpringPro22” and there’s no telling what’s in that thing. It could be Spring, maybe? It could be Spring plus someone’s customizations? It could be nothing? You would ask one of the developers, but everyone is too busy because the project can’t stop to migrate to a new build system (also 80% of the developers are proud of this awful build and resent your consultant-tinkering).
A project like this is an unknown minefield, and when someone calls you in to “migrate the build to Maven”, you are being asked to sprint through this minefield as fast as possible. First, you can’t just uproot the whole project and change the structure. Second, you have to take an iterative approach to migration that involves milestones. The alternative, which is never possible in a production environment, is to ask every stakeholder to hold off on requests for two weeks and ask developers to take two weeks of synchronized vacation during that period so you can migrate the build.
That’s never happening, so what’s the first milestone?
Step 1: Download Dependencies from a Repository Manager
Without a doubt, if your goal is to move to a new build tool your first step should have nothing to do with changing the build. Just take a snapshot of the artifacts the build depends on, take a full accounting of the JARs and libraries this behemoth consumes, and adapt the build to download from something like Nexus.
And, you don’t need to be running Maven to do this. You could run Gradle or Ivy or SBT or whatever. The reason you want to move your build to download dependencies from Nexus is that it starts the process of decoupling external dependencies from the contents of your source code repository. It also forces you to make a record of all the builds, how they relate to one another, and what they depend upon. This information will come in useful in a later step.
Step 1 likely took a week or two to complete (longer if your project is huge), but the important thing is that you didn’t have to change toolsets. You didn’t have to ask your developers to stop, drop, and change toolsets, and no one had to tell the boss, “Oh, we’re all just sitting around because the build guy wants to fiddle with the system.” If you carry this process through to the end, you will eventually need to interrupt active development. Minimize this downtime by taking a phased approach.
Step 2a: Publish Artifacts to a Repository
Step 2b: Adapt deployment scripts to consume binaries from a Repository
After the build is consuming artifacts from Nexus, the next step is to start publishing the output of the project to Nexus. While you are making this change you are also concurrently modifying any deployment scripts to grab binaries from Nexus.
Why? Because, quite often, these Crazyant legacy projects have confused development with deployment and there are a series of intricate build scripts that do things like compile the projects and deploy them to production. You’ll want to refactor this out of your project’s source code and make your deployment scripts dependent on binary build output via Nexus. If you do this, it’ll be easier to divide and conquer your builds and get your developers out of the business of production operations.
You will also be defining an interface for your software projects. What is a software project in the context of a repository manager? It is a build that consumes artifacts via Nexus and subsequently publishes artifacts to Nexus. Dependencies in, artifacts out. If you can adapt your existing build to satisfy this contract, the next step will be easier.
Step 3: Carve Out Projects 1 by 1 to a New Build
If you were changing a bunch of awful code what’s the first thing you would do? Would you dive right in and start changing stuff “willy nilly”? No, you’d write a series of unit tests. No one goes near a brittle disaster of technical debt without first making sure that you are not changing the contract.
This is exactly what Steps 1, 2a, and 2b defined for your build. Your build is producing binary output from source code and binary inputs, and your repository manager now stores both the inputs and the outputs. If Crazyant Legacy project had 20 projects and they were all consuming and publishing to Nexus, you have a “testable interface”.
So should you rush everything over to Maven all at once? No. Take your time and carve off projects to a new Maven build one at a time. Greenfield projects can start publishing to Maven by default, and existing projects can move at their own pace.
Decouple with Repositories
If you are moving to a new build system, be it Tesla, Gradle, Maven, or anything else, do yourself a favor and move to Nexus first. Decoupling your projects from direct source dependencies and using tools like Maven or Gradle to consume binary artifacts from a repository will enable you to conduct a lower risk, more piecemeal approach to your build migrations.