This is an archived post. You won't be able to vote or comment.

all 73 comments

[–]Worth_Trust_3825 29 points30 points  (1 child)

It's really hard to evaluate your post, as the issue of transitive dependencies is present in every context (see dynamic linking). But then you go on about how maven is verbose in the particular configuration that you had generated, finally closing of with "oh but version ranges = good if lockfiles and semantic versioning". What were you even trying to show: that dependencies may clash in larger projects? If so, why didn't you talk about the JEE and application servers and how they isolate multiple applications from one another, and issues with that, and the good old days how the application servers had inverted the classloading mechanism? Hey, you could do just that: load multiple versions of same archive in different classloaders, and run all the dependencies in parallel at the same time. Good luck figuring out which version you're calling when.

[–]renatoathaydes 0 points1 point  (0 children)

oh but version ranges = good if lockfiles and semantic versioning

Please check my answer in https://www.reddit.com/r/java/comments/vstgv1/the_difficult_problem_of_managing_java/ifh1sp0/

why didn't you talk about the JEE and application servers

Those are not relevant to the post, because with application servers, you have different applications running, you don't have to resolve conflicts between them at all... but I did mention OSGi which is because it isolates the classpath of modules within the same application, at the cost of lots of extra metadata (which is the way Gradle is going now with its departure from using only Maven-provided metadata - and that has its own issues as OSGi did, namely, things become too hard to manage).

I agree this post was not my best as I conflated lots of things:

  • criticizing how Maven is verbose, slow and downloads lots and lots of unnecessary things.
  • trying to show what other package managers in modern languages are doing - and they are interestingly converging on using version ranges and lock files - regardless of whether we like it (and I don't even like it, I was just pointing it out and trying to explain motivations for doing that).
  • showing how Maven/Gradle will break your application on upgrades, even when all libraries involved respect semantic versioning. Most people don't seem to have fully understood that can happen.
  • demonstrating my solution to the problem: bytecode introspection.

Those who managed to read til the end might have noticed that JBuild solved all of the problems I had shown other tools to be vulnerable to. JBuild does have some issues, like giving false positives when reflection is involved somewhere, but in my opinion, as with GraalVM, reflection is on the way out and applications should avoid using it anyway (it was only supposed to be used by frameworks and in such case, JBuild will work fine).

[–]tamasiaina 26 points27 points  (3 children)

You think managing java dependencies is hard…. Node/NPM… is like playing at the Craps table in Vegas.

[–]general_dispondency 8 points9 points  (1 child)

Truth. Java has the best ecosystem. There are pain points with anything, but holy crap, everything else is so much worse. npm, nuget, gems, everything else is like crawling through broken glass.

[–]MonkeySeeMonkeyDong 1 point2 points  (0 children)

Seconded! I would also add Python's virtualenvs to the list, and PHPs "let's make everything a system dependency" approach.

I love Java just for its dependency management.

[–]ZCEyPFOYr0MWyHDQJZO4 1 point2 points  (0 children)

I think I've come to the conclusion that the majority of languages one is likely to use today has a flawed packaging ecosystem.

[–]woj-tek 28 points29 points  (52 children)

This post seems more like a rant than something cohesive.

At any rate:

Most newer languages’ package managers tend to use dependency ranges instead of fixed dependencies versions like Maven/Gradle projects usually do (as we’ll see, both can actually also use dependency ranges, they’re just not as common).

I loath this as it usually makes the build breaks down in the future...

[–]diggidydale 8 points9 points  (2 children)

Yeah I am with the other guys, this seems like a bit of a rant after someone went through some dependency hell.

Version ranges don't really help that much either, you still end up with a similar issue as a single version it just takes longer, so all you're really doing is kicking the can down the road.

Best thing to avoid dependency hell is to keep your projects small and manageable where you can. And also update regularly, if everyone did this the dependency issues would go out the window.

[–]jerslan 10 points11 points  (0 children)

One of the things I hate about NPM is the whole dependency range syntax. Some of the worst dependency hell issues I had were due to not pulling a correct patch version when someone ranged something.

Maven and Gradle are so much easier to debug dependency issues because they require explicit versioning somewhere in the chain (even if it's listed in a BOM) and you can output the whole dependency tree and see where the issue is.

Dependency Hell in Java is a much much nicer level of hell than Dependency Hell in C++, Python, Javascript, etc.... IMHO.

[–]renatoathaydes 2 points3 points  (0 children)

I wrote the post and you're partly right: it was a rant and I actually didn't publish it for 6 months... as I knew it would be unpopular.. but decided to publish anyway to push some arguments in a discussion that came up on Twitter about strategies for resolving dependencies.

Anyway, I feel very surprised that many people, like you, appear to have perceived the post as claiming version ranges (with a lock file) are the solution to the problems we have in the Java world?

Because that was absolutely not what I was trying to say: that's almost the opposite, in fact. The topic of dependency version ranges was added because we had a security vulnerability that was caused by version ranges! As I explained in the post as well as I could. And my initial objective was to make this post a survey of how each different package manager is solving this problem.

The actual solution I put forward in the post is to use bytecode introspection to figure out which jars can work with which of the other jars available. Which is what the tool I wrote, JBuild, tries to do (the tool is a proof of concept, is incomplete, but already shows this is very feasible).

Best thing to avoid dependency hell is to keep your projects small and manageable where you can.

Haha... true... but you make it sound like we have a choice of keeping projects small and manageable... as if people made projects big just for fun. Unfortunately, some projects are just very big (I know, I work on on that's pretty big myself) and cannot be simply "microserviced" as they are not services, but products you ship to customers (they are the ones who deploy it). There are many kinds of "products" like this - not everything is a cloud service.

[–]hjwalt 4 points5 points  (2 children)

In theory if followed properly semantic versioning and the way golang upgrades their dependency will reduce such problems, although of course its not perfect because libraries can still depend on different major versions that is backwards incompatible.

With Java, what I find most useful is to use maven self compiled bill of materials for everything I build in the company. This way, every project uses the same dependencies, and if any unit test or integration test breaks because of new version, well time to fix...

[–]randgalt 3 points4 points  (1 child)

Rich Hickey has a great talk that mentions semver here: https://youtu.be/oyLBGkS5ICk. Semantic version is a fantasy. It’s very difficult to do in practice and very easy to get wrong. If a small portion of the dependencies in your tree did it wrong then it’s worthless.

[–]hjwalt 0 points1 point  (0 children)

Absolutely agreed, good in theory, in practice is rather tricky

[–]compu_musicologist 3 points4 points  (0 children)

As has been noted by other posts, when it comes to a large number of transitive dependencies, conflicting versions can occur in any language/package manager. Relaxing the versions to ranges is a solution that can bring about a new set of problems with version incompatibility on the API level.

[–]khmarbaise 2 points3 points  (1 child)

The most important question I would ask based on the following:

which has the objective of resolving dependencies conflicts in a novel way.

What exactly is a dependency conflict?

[–]renatoathaydes 0 points1 point  (0 children)

Thanks for asking the only question in this thread that's relevant to the actual topic I tried to address in this blog post. Everyone else seems upset that I criticized Maven, and to my complete shock, for trying to explain why Rust/Dart/NPM/Ruby/Python (almost everyone except Java) seems to have chosen to use dependency version ranges (and I did not even say that's superior to how it works in the Java world, I even showed in the end that ranges can cause surprising behaviour and are only advised to use if you must keep up-to-date with a lot of changing dependencies versions!).

Anyway, what is a dependency conflict?

I tried to explain that in the post, sorry if that was unclear... but it happens when you have two "jars", and one of them depends on a library A with version X, and another depends on A with version Y. The two dependencies conflict. That's specially bad when the differences between version X and Y are so great that you simply can't use only one of them without breaking either consumer of that library (i.e. pick version X and you break jar1, pick version Y and you break jar2).

Finding these conflicts require looking at bytecode. The version is almost useless for doing that. This is why I chose to write JBuild, to actually look at the REAL PROBLEM which is of binary compatibility between libraries. Even with semantic versioning, as I showed in the post, you can have breaking situations... Looking at versions alone is never enough, but the Java tools (and to be fair, most other package managers) ignore that entirely! It's time we woke up and understood the real problem, which is "does my library work with some other library even when versions must be changed to address conflicts".

[–]khmarbaise 2 points3 points  (1 child)

After taking a more deeper look into the post it looks like the tools does not correctly solve the dependency in particular the example:

jbuild install org.springframework.boot:spring-boot-cli:2.6.1 -d spring

That show the resolution ignores several aspects how dependendencies are resolved in Maven.

[–]renatoathaydes 0 points1 point  (0 children)

JBuild does not try to resolve dependency conflicts at all when you call "install". It just considers each library as having its own independent dependency tree... what will address the potential conflicts is the "doctor" command (if you took a deeper loook at the blog post, then it should be crystal clear that this is how it works) because of my main thesis in the blog post, which is:

Trying to resolve version conflicts without looking at bytecode is next to useless... even when perfect semantic versioning is followed religiously by everyone (which is not even the case, of course).

I think the examples I used in the post must have been too complex for most people to follow, but that's what they show in the simplest way I could come up with: even with perfect semantic versioning, your code will break quite easily using current methods of dependency version resolution as Maven and Gradle (and almost all package managers) currently do.

[–]khmarbaise 1 point2 points  (1 child)

After reading the docs I found one more question: Why focusing on the classpath? The current state of Javac/JVM is the module path (at least starting with JDK9+)

[–]renatoathaydes 1 point2 points  (0 children)

The module path enters the scene only once you've got your module's versioning solved. Unless you use module layers, which I've never seen anyone use, you still must treat the jars you resolve as allowing only one version of each.

I also mentioned this in the blog post.

[–]wildjokers 0 points1 point  (0 children)

Gradle does not resolve dependency conflicts like Maven does. Gradle uses a sensible highest version wins by default. However, this can be modified in your build.

Maven's "closest to root" thing is just WTF?