Constraining Overlapping, Transitive Version Ranges in Dependency Resolution

Suppose there exist two projects: ‘mainapplication’ and ‘library’, where the former depends on the latter.

Assume the following are all of the available versions of the ‘foo:bar’ artifact in the Maven repository: - 1.1.0 - 1.1.1 - 1.2.0 - 1.2.1

If ‘library’ declares a compile dependency on ‘foo:bar:1.+’, and ‘mainapplication’ declares a stronger (more constrained), overlapping compile dependency on ‘foo:bar:1.1.+’, then what I’m finding in my tests is that by default the weaker of the two specifications, ‘1.+’, appears to be taking precedence over the stronger specification, ‘1.1.+’, since the resolved version reported by the ‘dependencies’ task is ‘1.2.1’. Further, if I configure the ‘mainapplication’ project to ‘failOnVersionConflict()’ then I get a conflict exception – but these ranges are not conflicting.

My question: is there a way to configure the Gradle dependency resolution of the ‘mainapplication’ project to select ‘1.1.1’ as the resolved version because it satisfies both ‘1.+’ and ‘1.1.+’?

I’ve repeated the tests, specifying the ranges with alternative syntax, but still the problem persists: - replace ‘1.+’ with ‘[1.0.0,2[’ - replace ‘1.1.+’ with ‘[1.1.0,1.2[’

I know I can work around the problem by declaring ‘force = true’ for the ‘mainapplication’ dependency, by specifying ‘forcedModules’, or perhaps by excluding the transitive dependency, but none of these seem appropriate if there’s a way to configure dependency resolution to satisfy both specifications automatically.

In a related scenario, suppose the two overlapping dependencies are declared by two separate libraries on which ‘mainapplication’ depends, but not declared at all by the ‘mainapplication’ project itself. When I need to bundle ‘mainapplication’ and all of its dependencies into a deployable distribution, I’d like the JAR for ‘foo:bar:1.1.1’ to be included in the distribution. Again, configuring dependency resolution to satisfy both specifications automatically would be preferred over specifying the dependency again in the ‘mainapplication’ strictly for the purpose of transitive dependency management.

That’s an interesting idea.

Gradle uses a “newest” strategy when dealing with conflicts. http://www.gradle.org/docs/current/userguide/dependency_management.html#sub:version_conflicts

So like you’ve observed, 1.+ resolves to 1.2.1 and 1.1.+ resolves to 1.1.1. 1.2.1 > 1.1.1, so 1.2.1 wins.

If there was a way to select “most constrained” dependencies when there’s a conflict, if you have 1.+ and 1.1.1 (explicit version), the 1.1.1 would always win. Since a lot of published ivy/POMs are going to have explicit versions, this could be surprising.

Some of the work on the resolution strategy and component selection APIs are to make it easier to implement something like you’ve described. I haven’t tried playing around with it to see if there’s a straightforward way to do it with the current APIs. I think we still resolve dependencies on a per-project, per-task sort of way, so I’m not sure we have the insight across all projects.

The Netflix guys have some good dependency related plug-ins too: https://github.com/nebula-plugins/nebula-dependency-recommender and https://github.com/nebula-plugins/gradle-dependency-lock-plugin

Thanks for the suggestions. I’ve used the ‘gradle-dependency-lock’ plugin before, but not the ‘nebula-dependency-recommender’. Unfortunately, neither of these address this particular use case.

I’ll look closer at the available APIs and try to determine the scope of change needed to support this type of resolution strategy.

I just replied to a very similar forum post. For convenience, here’s my answer:

When a dependency is included in the graph twice, the Gradle resolves each independently and then chooses the newer of the 2 versions. This is a behaviour that was inherited from Ivy, and is clearly not idea in the case where 2 version ranges overlap. In this case, we should instead be choosing the newest version that matches both ranges.

This is something we’d like to fix in the future, but it would be considered a breaking change and not something we would change without considering backward compatibility.