How to Prevent Accidental or Eager Dependency Upgrades in Gradle
This guide explains how to prevent accidental dependency versions due to Gradle’s eager dependency resolution.
Why Prevent Accidental Dependency Upgrades?
When managing dependencies in Gradle, you may encounter situations where transitive dependencies cause unexpected upgrades to newer versions. This can lead to unintended behavior or compatibility issues. Gradle, by default, performs optimistic upgrades, meaning it resolves to the highest available version when multiple versions of a dependency are found in the dependency graph.
For example, if both 3.1
and 3.2
versions of commons-lang3
are present, Gradle will select 3.2
, even if your build script explicitly declares 3.1
.
If your build uses a feature available in version 3.1
but not 3.2
, or if you haven’t updated your build to be compatible with version 3.2
, you may not want this eager upgrade from Gradle’s dependency resolution process.
dependencies {
implementation("org.apache.commons:commons-lang3:3.2")
constraints {
implementation("org.apache.commons:commons-lang3:3.1") {
because("Version 1.3 introduces breaking changes not yet handled")
}
}
}
dependencies {
implementation("org.apache.commons:commons-lang3:3.2")
constraints {
implementation("org.apache.commons:commons-lang3:3.1") {
because("Version 1.3 introduces breaking changes not yet handled")
}
}
}
Running ./gradlew dependencies --configuration runtimeClasspath
showcases the results:
> Task :dependencies
------------------------------------------------------------
Root project 'how_to_prevent_accidental_upgrades'
------------------------------------------------------------
runtimeClasspath - Compile classpath for source set 'main'.
+--- org.apache.commons:commons-lang3:3.2
\--- org.apache.commons:commons-lang3:3.1 -> 3.2 (c)
Option 1: Enforce Strict Dependency Resolution
Gradle provides an option to fail the build when a version conflict occurs, ensuring that no unintended upgrades happen.
To enable this, configure your build to fail on version conflicts:
configurations.all {
resolutionStrategy.failOnVersionConflict()
}
configurations.all {
resolutionStrategy.failOnVersionConflict()
}
When this setting is enabled, Gradle will stop execution and report an error if multiple versions of a dependency conflict, instead of automatically upgrading to the highest version.
Running ./gradlew dependencies --configuration runtimeClasspath
showcases the failure:
> Task :dependencies FAILED
------------------------------------------------------------
Root project 'how_to_prevent_accidental_upgrades'
------------------------------------------------------------
runtimeClasspath - Runtime classpath of source set 'main'.
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':dependencies'.
> Could not resolve all dependencies for configuration ':runtimeClasspath'.
> Conflict found for the following module:
- org.apache.commons:commons-lang3 between versions 3.2 and 3.1
Option 2: Using Dependency Constraints
If you have multiple dependencies that use a shared library, you can enforce a consistent version across all modules using dependency constraints:
dependencies {
implementation("org.apache.commons:commons-lang3")
constraints {
implementation("org.apache.commons:commons-lang3") {
version {
strictly("3.1")
}
}
}
}
dependencies {
implementation("org.apache.commons:commons-lang3")
constraints {
implementation("org.apache.commons:commons-lang3") {
version {
strictly("3.1")
}
}
}
}
With strictly("3.1")
, Gradle ensures that no other version can override the specified dependency.
Running ./gradlew dependencies --configuration runtimeClasspath
showcases the results:
> Task :dependencies
------------------------------------------------------------
Root project 'how_to_prevent_accidental_upgrades'
------------------------------------------------------------
runtimeClasspath - Runtime classpath of source set 'main'.
+--- org.apache.commons:commons-lang3 -> 3.1
\--- org.apache.commons:commons-lang3:{strictly 3.1} -> 3.1 (c)
Option 3: Locking Dependencies to Specific Versions
For a more robust solution, you can use dependency locking to ensure Gradle resolves the same dependency versions consistently across builds.
To enable dependency locking:
configurations.all {
resolutionStrategy.activateDependencyLocking()
}
configurations.all {
resolutionStrategy.activateDependencyLocking()
}
Then, generate a lock file:
./gradlew dependencies --write-locks
This creates a gradle.lockfile
that stores the exact dependency versions, preventing future upgrades unless explicitly updated.
Summary
Gradle’s optimistic dependency resolution may inadvertently upgrade dependencies, causing compatibility issues.
To prevent unexpected upgrades, you can:
-
Enforce strict resolution (
failOnVersionConflict
) -
Explicitly constrain dependencies using the
strictly
keyword -
Use dependency locking (
activateDependencyLocking
) to maintain consistent versions across builds.