Maven is complicated but also very stable and powerful. It's no less arcane than webpack and if you're using any decent IDE all the available options will autocomplete. Fat jar deploys were always super simple and having a self-contained executable is even better.
Maven at its core is very simple. The lifecycle is well-documented and so is the dependency resolution.
Many coworkers I've worked with have called it complicated and then I find out they really haven't learned the (very simple) lifecycle and how build behavior gets attached, etc. This is all very simple but it is something to learn.
I don't know why this happens. I think programmers are so used to imperative (ant/Grunt/etc/etc) programming/building that it becomes confusing for them when they encounter a declarative system. And their brain revolts. Because they are like how do I just do a thing (i.e., copy some file somewhere, call a "build" method, etc, etc.)!? Instead of realizing the value of the declarative + idiomatic Tao and learning it...they say no, no gimme gimme let meee do it. Much like my two year old. And then I'm left fighting non-reproducible builds and getting their projects going in my IDE like it's somehow 2005 again. But Maven is great, declarative systems -- especially declarative build systems are the optimal way to do builds (in most cases).
I think it happens because maven has poor documentation, a ton of unexplained "convention over configuration", and very strong poorly explained opinions about how you must structure your project. Basically it's a ton of inflexible magic. Also, the attitude in the maven community that people that want to deviate from the god-given-way are two year old children.
This is basically the exact opposite of my experience with Maven. The documentation feels very clear and good to me and I've always found answers to my questions. I don't recall ever encountering "unexplained inflexible magic".
In around 2008 I co-wrote a set of maven javascript mojos (before NodeJS), incl QUnit test integration et all. I collaborated with another fellow on the other side of the world. Documentation was stellar. IIRC, to extend, even the upgrade from M2 to M3 didn't affect most community mojos.
For example, if you have a multi-pom project with a parent pom (not unusual in a medium-sized codebase, never mind large), and you have unit tests in your child poms, they'll get executed with dependencies resolved locally for that child pom, and not with the global dependency resolution that satisfies all constraints that need satisfying at the parent level.
The upshot is that you easily end up running tests using different versions of dependencies than what you deploy with, misleading you into thinking things are working, and can end up with hilarious NoSuchMethodError at runtime.
The "solution" is to pin your versions using dependencyManagement, but that's no real solution at all, not least because it's neither automatic (though dependency convergance using Maven Enforcer helps, and I've developed some scripts that help further) nor is there an easy route forward when you want to bump versions safely.
> For example, if you have a multi-pom project with a parent pom (not unusual in a medium-sized codebase, never mind large), and you have unit tests in your child poms, they'll get executed with dependencies resolved locally for that child pom, and not with the global dependency resolution that satisfies all constraints that need satisfying at the parent level.
As it should, otherwise you'd get the even more hilarious situation where your tests pass when you run the single-module build but fail when you run the multi-module build.
> The "solution" is to pin your versions using dependencyManagement, but that's no real solution at all, not least because it's neither automatic (though dependency convergance using Maven Enforcer helps, and I've developed some scripts that help further) nor is there an easy route forward when you want to bump versions safely.
This is not a maven issue, it's a combination of Java's flat classpath and the reality of diamond dependencies on versioned libraries in any system. We could allow multiple versions of a transitive dependency on the classpath but then you get Rust's hilarious "expected Foo but was Foo" type errors. Java 9 modularisation should reduce the problem by allowing modules to have explicitly private dependencies, but the problem exists everywhere.
This is not a maven issue, it's a combination of Java's flat classpath and the reality of diamond dependencies on versioned libraries in any system
This isn't true; there are better solutions already existing. E.g. Ruby with the Gemfile.lock approach. I'd like to be able to do a two-step: do a global analysis of dependencies and pin the common versions for diamond situations, but not need to maintain or calculate those pinned versions manually. And I still get to use the pinned versions from the global resolution for executing tests etc. on submodules.
As it is, I've had to write a bunch of tools to rewrite the dependencyManagement as and when required. The main app I work on is linked into the Hadoop ecosystem, and that dependency set ends up being quite large.
Another issue with Maven is rebranding: when the groupId of an artifact changes, but the package and class names don't; see e.g. asm / asm vs org.ow2.asm / asm, or the whole codehaus debacle.
I have spent more than two weeks of my life updating, maintaining, fixing, researching and tooling Maven pom files, and it was almost all around deficiencies in dependency management.
Let's not get started on trying to get builds to work reliably offline, caching maven repositories, etc.
> This isn't true; there are better solutions already existing. E.g. Ruby with the Gemfile.lock approach. I'd like to be able to do a two-step: do a global analysis of dependencies and pin the common versions for diamond situations, but not need to maintain or calculate those pinned versions manually. And I still get to use the pinned versions from the global resolution for executing tests etc. on submodules.
I dislike this for the same reason as before: I find it really valuable that building a single module on its own behaves the exact same way as building that module as part of a multi-module project. If you want to ensure that the tests run against the same version as in the final product, I do that by making eviction an error.
> Another issue with Maven is rebranding: when the groupId of an artifact changes, but the package and class names don't; see e.g. asm / asm vs org.ow2.asm / asm, or the whole codehaus debacle.
Maven has better handling of this than most; it can at least warn/error when this has happened, and if you're using the shade plugin or similar it will warn or error when two jars contain the same classes. Yes it's a problem, but it's much more of a problem in other ecosystems.
> As it is, I've had to write a bunch of tools to rewrite the dependencyManagement as and when required. The main app I work on is linked into the Hadoop ecosystem, and that dependency set ends up being quite large.
There are standardized plugins for the common use cases e.g. "mvn versions:use-latest-releases". Most things that can be automated are. But since most of the library ecosystem predates version ranges and semVer, there simply isn't an automated way to know when using a newer version of a transitive dependency is safe or not.
Having the choice to pin versions separately per module is a desirable feature, especially for multi-module builds with different deploy artifacts. So this is no problem at all; deciding which versions to pin per artifact/module is the programming job — no tool can automagically decide this for you correctly.
I'd prefer the choice to specify versions in submodules, with the versions of dependencies chosen and pinned via the global resolution, producing a fixed set of dependencies in a file I can check in, like Gemfile.lock in the Ruby world.
Could not agree more. Every time I have seen an implementation the imperative type (ant etc) it leads to different ways of doing the same things so over multiple projects things just gets really complicated and difficult to maintain.
>I don't know why this happens. I think programmers are so used to imperative (ant/Grunt/etc/etc) programming/building that it becomes confusing for them when they encounter a declarative system.
Because, after all is said and done, programming is about having your computer "do" something - whether it be to write some bytes to the disk or to send some bytes to the network.
It's just that sometimes there's an advantage to use an abstraction - such as say "SELECT * FROM TABLE WHERE ROW_ID =14" is an abstraction over something like "for (int i =0;i<table.size();i++) if(table.row[i].elem[1] == 14) ... ", but often you don't need such a complicated abstraction, you just "need something done" before (or after) building. Whether it be that you want to download an HTML file to package, or to move the resulting file to a certain directory, or to clean a certain directory, your build language shouldn't be that arcane that you have to create a "build.sh" to get things done.
— “SELECT...” is not an abstraction for “for...”, by the way. Abstractions are not for specific implementations. “SELECT...” is an abstraction for specifying Sets etc.
— Maven is simple, not arcane. I’ve produced countless “custom” builds with wild variations of need and never has Maven let me down. In some sense this is easy to prove (see maven-antrun-plugin); however, Maven’s core plugin/idioms supports 90+% of use cases I’ve seen over many many “weird” build requirements. To your example, “copying files” is fundamental; however you have to learn the idiomatic way to do it if you want to reap the benefits. Otherwise, just call out to maven-antrun- or similar. But then you’re chasing your tail.
> after all is said and done, programming is about having your computer "do" something - whether it be to write some bytes to the disk or to send some bytes to the network.
Not always. Often, particularly in scientific computing, you just want to ... compute ... a result. I mean sure, the computer needs to display the result to you, but the computation isn't about displaying it, it's about computing it. Just like adding up a bunch of numbers on a calculator isn't about getting the calculator to "do" anything.
> often you don't need such a complicated abstraction, you just "need something done" before (or after) building.
Abstractions should be simpler than the thing they abstract over, that's their whole point. Treating my computer's memory as a flat array of numbers is simpler than thinking about how much current is flowing through each RAM wire. "Compile all source files" is simpler than "execute javac Foo.java, then execute javac Bar.java, then..."
> Whether it be that you want to download an HTML file to package, or to move the resulting file to a certain directory, or to clean a certain directory, your build language shouldn't be that arcane that you have to create a "build.sh" to get things done.
You shouldn't need a build.sh, agreed, but nor should you need particular things to happen to particular directories. Maven has great support for things like "package my documentation in the standard way". It's only when people try to fight it about the low-level details that they really shouldn't be caring about that they run into trouble.
> they really haven't learned the (very simple) lifecycle
Building is a simple concept: dependency DAG + inputs -> artifacts. The "->" here can involve arbitrary intermediate artifacts and steps.
Maven ignores this fundamental concept of building. Rather than treat interim dependencies as part of the DAG, there's a hard-coded recipe of lifecycle steps.
This is why I personally cringe at maven - it has nothing to do with the fact that it's declarative (virtually every build system is largely declarative). It has everything to do with hard-coding one particular set of build steps into a supposedly general build tool.
I was under the impression that the build chain in maven is pretty defined and not arbitrary. It performs the same thing over and over again, being highly reliable. It’s pretty extensible since you can tweak your own build steps if you want. And in the general case you don’t need to define a thing?
I used to use Maven but I've moved completely to Gradle.
With Maven, if you follow Maven's way of doing things, everything just works. But, if you need to do something different, or have some rare use case Maven's designers weren't focused on, you start struggling a lot to get Maven to do what you want.
Gradle is a lot more flexible and if you have some uncommon requirement you are far more likely to successfully implement it using Gradle than using Maven.
> But, if you need to do something different, or have some rare use case Maven's designers weren't focused on, you start struggling a lot to get Maven to do what you want.
You'll be fine as long as you realise the Maven-ey way of doing the thing is to write your own plugin.
But most of the time people just want their build to do something different for no real reason, IME.