I think the key is to choose the highest requested version.
The Gradle docs say “Gradle will consider all requested versions, wherever they appear in the dependency graph. Out of these versions, it will select the highest one.”
As you have seen, Gradle supports a concept of rich version declaration, so what is the highest version depends on the way versions were declared:
If no ranges are involved, then the highest version that is not rejected will be selected.
- If a version declared as
strictly
is lower than that version, selection will fail.If ranges are involved:
- If there is a non range version that falls within the specified ranges or is higher than their upper bound, it will be selected.
- If there are only ranges, the highest existing version of the range with the highest upper bound will be selected.
- If a version declared as
strictly
is lower than that version, selection will fail.Note that in the case where ranges come into play, Gradle requires metadata to determine which versions do exist for the considered range. This causes an intermediate lookup for metadata, as described in How Gradle retrieves dependency metadata?.
In the case of ConstraintLayout-Solver, the only requested version was 1.1.3, but EBuild arbitrarily bumped it to 2.0.2 (treating it like *). If it chooses the highest requested version, this would be 1.1.3 and would build successfully. In the case of AppCompat/Annotation, it should also use the highest requested version, which in this case would be 1.1.0.
At that point, if there is a versioning conflict then its up to the developer to decide which versions to use to avoid conflicts. But at least then the developer has control over it. The issue with ConstraintLayout-Solver was that it was a transitive dependency that was being versioned up, so I had no control over the versioning.