Maven4 ante portas! (Part 1)

Tamás Cservenák

December 7, 2024

Maven4 is coming, so it may be time to explain some of the changes it will bring on. I’d like to start from “user facing” end, the mvn command and what has changed (visible and invisible changes).

How it was: Maven3

In short, Maven3 has a well known mvn (and mvn.cmd Windows counterpart) script in place that users invoke. This script observed some of the environment variables (think MAVEN_OPTS and others), observed mavenrc file, tried to find .mvn directory and also load .mvn/jvm.config from it, if existed, and finally, it fired up Java calling ClassWorlds launcher class and setting the launcher configuration to conf/m2.conf file.

From this point on, the ClassWorlds launcher populated the classloader(s) following the instructions in the configuration file, and finally invoked the “enhanced” main method of the configured main class, that was maven-embedder:org.apache.maven.cli.MavenCli. This is how MavenCli received the (preconfigured) ClassWorld instance.

The MavenCli class then parsed arguments, collected Maven system and user properties, built “effective” settings (from various user and installation settings.xml files), created DI container, while loading any possible build extension from .mvn/extensions.xml, if present, then populating MavenExecutionRequest, looked up Maven component, and finally invoked it with the populated request. And this is where “Maven as you know it” started.

The subproject maven-embedder is frozen and deprecated. It still works, but offers “legacy” face (like no new Maven4 options support and alike).

How it is: Maven4

We faced several problems with this setup. For start, there was a lot of scaffolding around to invoke just one executable: mvn. The whole thing was not extensible, it had many things “bolted on” and finally it was not reusable. But let’s start in order.

Maven3 introduced “password encryption” (that it had issues on its own, but that is another story). The CLI options to handle those actions were in fact “bolted on” to MavenCli, that “semi booted” Maven (kinda, but not fully), and all this only to perform those actions without DI or anything helpful. This was akin (and comments in source were suggesting this as well) for a separate tool to handle password encryption. We knew we need more “Maven tools”, as mvn alone is not enough. The mvnenc tool handling password encryption in Maven4 is first of these. One example of what Maven3 is unable to handle: Plexus Sec Dispatcher was always “extensible” with regard of Ciphers. Idea was to add it as extension. But alas, to have that new Cipher picked up, you’d need DI (and load extensions), so without DI all this never worked.

Moreover, the MavenCli was mish-mash of some static methods, making it not extensible nor reusable. This was really evident and most painful bit in new Maven Daemon (and biggest obstacle to make it “keep up” with Maven) The Maven Daemon was forced to copy-pasta it, and then modify this class, to make it work for mvnd use case. And as expected, this lead to ton of burden to maintain and “port” changes from Maven CLI to mvnd CLI. We knew this situation is bad, and we wanted to reuse most if not all of Maven CLI related code in mvnd. Essentially, they both do the “same thing”, just latter “daemonize” Maven.

There were the ITs as well: the Maven ITs were not able to test new Maven4 features (like new CLI options) as ITs historically used Maven Verifier, that either executed Maven in forked process (read: was slow) or used “embedded” mode, that again went directly into MavenCli “tricky method”. It was not justified to fork any IT that wanted to use new Maven 4 CLI options.

Finally, the state of the MavenCli was quite horrible, frankly. Few of us dared to enter that jungle. Was very-very easy to mess up things in there, and moreover, have those issues become known only after release, as Verifier “trick method” did not catch it.

I don’t want to go into details, so on high level these are the changes:

The CLIng module resides in impl/maven-cli and the idea is simple this:

That’s really the high level picture. CLIng itself is “layered” in a sense of different requirements per layer. Various Parsers support various CLI options, so parsers are layered as:

The idea is that all these “specialization” build upon previous layer, in this case BaseParser, that is the common ground: options supported by all Maven CLI tools (not just mvn).

For execution, similar thing happen, it is layered as this:

In short: “lookup” provides the “base ground” (that mvn and other tools like mvnenc) build upon. Again. goal is to have in all tools shared support for “common” features (think -X logging, or -e show stack traces, etc.).

Furthermore, MavenInvoker (an abstract class) has two “specializations” that are non-abstract:

With these things in Maven “proper” we were able to achieve substantial “diet” in Maven Daemon, where CLIng is really “just reused”. From that PR, release of Maven and Maven Daemon can really happen “in sync” as latest rc-1 release proved. Also, we got first tool in Maven Tools suite, the mvnenc.

Finally, for integration tests, we introduced impl/maven-executor module with similar design as CLIng has. It is a dependency-less library that is reusable and is amalgam of good old maven-verifier and maven-invoker but do the job less intrusive and in a future-proof way. Maven Executor has 4 key bits:

The Maven Integration Tests already contained own “extended Verifier” instance that extended the old maven-verifier Verifier class. The redirection happened in maven-it-helper “extended Verifier”, by dropping maven-verifier and maven-shared-utils dependencies, and introducing maven-executor instead. Very few ITs were harmed in this process.