Give the version of each dependency as a single value such as 2.3
.
Do not use a Maven range such as [2.3,2.9]
, [2.3,)
, or even [2.3]
.
Do not use a Gradle dynamic version such as
com.google.api-client:google-api-client:+
.
When a build system specifies a range instead of a single version for a dependency, builds at different points in time can see different versions of that dependency. This causes builds to be non-reproducible and can break your product unexpectedly. Builds of the same code with the same compiler at different times can produce different artifacts with different behavior.
Version ranges can pull in incomplete releases that break the build. For example, a version range in appengine-gcs-client’s dependency on google-http-java-client broke multiple customers’ builds when only half the artifacts in the project were pushed one afternoon.
Version ranges are a security hole. A project that uses a range to select the version of a dependency picks up new versions of that dependency when they’re pushed to Maven Central. Someone with release privileges can take advantage of this to slip malicious code into projects without proper review. A variant of this attack in the node.js ecosystem was used to steal Bitcoins.
Version ranges have another, less obvious problem. A normal soft version
requirement such as <version>2.3</version>
says that Maven prefers
version 2.3 but will accept version 2.0 or 3.0 if other dependencies
pick that first or require it. This flexibility is important
for large projects with many dependencies, since it’s very rare for
all artifacts that depend on a particular library to agree on which version they
need.
A version range does not have this flexibility. When Maven sees
<version>[2.3,2.9]</version>
, it accepts any version between 2.3 and 2.9 inclusive. It does not accept 2.0 or 3.1.5.
A single version range such as <version>[2.3]</version>
is even stricter,
requiring version 2.3 and no other. If two dependency
elements in the
graph have non-overlapping version ranges, the build fails.
Some multimodule projects such as gRPC specify single element version ranges
to cause the build to fail early when multiple versions of intraproject
dependencies are mixed. For example,
io.grpc:grpc-alts:1.25.0
depends on exactly io.grpc:grpc-core:1.25.0
:
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-core</artifactId>
<version>[1.25.0]</version>
</dependency>
io.grpc:grpc-alts:1.25.0
and io.grpc:grpc-core:1.23.0
cannot appear in
the classpath at the same time. If dependency mediation selects both
io.grpc:grpc-alts:1.25.0
and io.grpc:grpc-core:1.23.0
, the build fails, and the developer must fix the conflict before they can proceed. This still
adds friction for developers who depend on multiple versions of
different modules, especially if they only depend on the different
versions through several layers of transitive dependencies. It can be
necessary when different modules are tightly coupled to each other,
but it is not ideal.
Rapidly changing, pre-1.0 projects with unstable APIs have a greater
need for hard requirements such as <version>[0.24.0]</version>
to ensure their modules all work together.
OpenCensus did not do this, and as a result there have been several problems
when different versions of different OpenCensus artifacts that
did not work with each other appeared in other projects’
classpath at the same time. In the best case, hard requirements are
a stopgap measure that can be discarded once the API stabilizes.
Even if all hard requirements can be satisfied, version ranges override the normal Maven algorithm for selecting among different versions of the same artifact. They pin the dependency tree to that version or versions, even when other artifacts in the classpath specify newer compatible versions that include important bug and security fixes.
See Dependency Version Requirement Specification for more information.