Using Resolution Rules
Gradle provides several mechanisms to directly influence the behavior of the dependency resolution engine.
Unlike dependency constraints or component metadata rules, which serve as inputs to the resolution process, these mechanisms allow you to inject rules directly into the resolution engine. Because of their direct impact, they can be considered brute-force solutions that may mask underlying issues, such as the introduction of new dependencies.
It’s generally advisable to resort to resolution rules only when other approaches are insufficient. |
If you’re developing a library, it’s best to use dependency constraints, as they are shared with your consumers.
Here are the key resolution strategies in Gradle:
# | Strategy | Info |
---|---|---|
Forcing Dependency Versions |
Force a specific version of a dependency. |
|
Module Replacement |
Substitute one module for another with an explanation. |
|
Dependency Substitution |
Substitute dependencies dynamically. |
|
Component Selection Rules |
Control which versions of a module are allowed. Reject specific versions that are known to be broken or undesirable. |
|
Default Dependencies |
Automatically add dependencies to a configuration when no dependencies are explicitly declared. |
|
Excluding Transitive Dependencies |
Exclude transitive dependencies that you don’t want to be included in the dependency graph. |
|
Force Failed Resolution Strategies |
Force builds to fail when certain conditions occur during resolution. |
|
Disabling Transitive Dependencies |
Dependencies are transitive by default, but you can disable this behavior for individual dependencies. |
|
Dependency Resolve Rules and Other Conditionals |
Transform or filter dependencies directly as they are resolved and other corner case scenarios. |
1. Forcing Dependency Versions
You can enforce a specific version of a dependency, regardless of what versions might be requested or resolved by other parts of the build script.
This is useful for ensuring consistency and avoiding conflicts due to different versions of the same dependency being used.
configurations {
"compileClasspath" {
resolutionStrategy.force("commons-codec:commons-codec:1.9")
}
}
dependencies {
implementation("org.apache.httpcomponents:httpclient:4.5.4")
}
configurations {
compileClasspath {
resolutionStrategy.force 'commons-codec:commons-codec:1.9'
}
}
dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.4'
}
2. Module Replacement
While it’s generally better to manage module conflicts using capabilities, there are scenarios—especially when working with older versions of Gradle-that require a different approach. In these cases, module replacement rules offer a solution by allowing you to specify that a legacy library has been replaced by a newer one.
Module replacement rules allow you to declare that a legacy library has been replaced by a newer one.
For instance, the migration from google-collections
to guava
involved renaming the module from com.google.collections:google-collections
to com.google.guava:guava
.
Such changes impact conflict resolution because Gradle doesn’t treat them as version conflicts due to different module coordinates.
Consider a scenario where both libraries appear in the dependency graph.
Your project depends on guava
, but a transitive dependency pulls in google-collections
.
This can cause runtime errors since Gradle won’t automatically resolve this as a conflict.
Common solutions include:
-
Declaring an exclusion rule to avoid
google-collections
. -
Avoiding dependencies that pull in legacy libraries.
-
Upgrading dependencies that no longer use
google-collections
. -
Downgrading to
google-collections
(not recommended). -
Assigning capabilities so
google-collections
andguava
are mutually exclusive.
These methods can be insufficient for large-scale projects. By declaring module replacements, you can address this issue globally across projects, allowing organizations to handle such conflicts holistically.
dependencies {
modules {
module("com.google.collections:google-collections") {
replacedBy("com.google.guava:guava", "google-collections is now part of Guava")
}
}
}
dependencies {
modules {
module("com.google.collections:google-collections") {
replacedBy("com.google.guava:guava", "google-collections is now part of Guava")
}
}
}
Once declared, Gradle treats any version of guava
as superior to google-collections
during conflict resolution, ensuring only guava
appears in the classpath.
However, if google-collections
is the only module present, it won’t be automatically replaced unless there’s a conflict.
For more examples, refer to the DSL reference for ComponentMetadataHandler.
Gradle does not currently support replacing a module with multiple modules, but multiple modules can be replaced by a single module. |
3. Dependency Substitution
Dependency substitution rules allow for replacing project and module dependencies with specified alternatives, making them interchangeable. While similar to dependency resolve rules, they offer more flexibility by enabling substitution between project and module dependencies.
However, adding a dependency substitution rule affects the timing of configuration resolution. Instead of resolving on first use, the configuration is resolved during task graph construction, which can cause issues if the configuration is modified later or depends on modules published during task execution.
Explanation:
-
A configuration can serve as input to a task and include project dependencies when resolved.
-
If a project dependency is an input to a task (via a configuration), then tasks to build those artifacts are added as dependencies.
-
To determine project dependencies that are inputs to a task, Gradle must resolve the configuration inputs.
-
Because the Gradle task graph is fixed once task execution has commenced, Gradle needs to perform this resolution prior to executing any tasks.
Without substitution rules, Gradle assumes that external module dependencies don’t reference project dependencies, simplifying dependency traversal. With substitution rules, this assumption no longer holds, so Gradle must fully resolve the configuration to determine project dependencies.
Substituting an external module dependency with a project dependency
Dependency substitution can be used to replace an external module with a locally developed project, which is helpful when testing a patched or unreleased version of a module.
The external module can be replaced whether a version is specified:
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(module("org.utils:api"))
.using(project(":api")).because("we work with the unreleased development version")
substitute(module("org.utils:util:2.5")).using(project(":util"))
}
}
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute module("org.utils:api") using project(":api") because "we work with the unreleased development version"
substitute module("org.utils:util:2.5") using project(":util")
}
}
-
Substituted projects must be part of the multi-project build (included via
settings.gradle
). -
The substitution replaces the module dependency with the project dependency and sets up task dependencies, but doesn’t automatically include the project in the build.
Substituting a project dependency with a module replacement
You can also use substitution rules to replace a project dependency with an external module in a multi-project build.
This technique can accelerate development by allowing certain dependencies to be downloaded from a repository instead of being built locally:
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(project(":api"))
.using(module("org.utils:api:1.3")).because("we use a stable version of org.utils:api")
}
}
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute project(":api") using module("org.utils:api:1.3") because "we use a stable version of org.utils:api"
}
}
-
The substituted module must include a version.
-
Even after substitution, the project remains part of the multi-project build, but tasks to build it won’t be executed when resolving the configuration.
Conditionally substituting a dependency
You can conditionally substitute a module dependency with a local project in a multi-project build using dependency substitution rules.
This is particularly useful when you want to use a locally developed version of a dependency if it exists, otherwise fall back to the external module:
configurations.all {
resolutionStrategy.dependencySubstitution.all {
requested.let {
if (it is ModuleComponentSelector && it.group == "org.example") {
val targetProject = findProject(":${it.module}")
if (targetProject != null) {
useTarget(targetProject)
}
}
}
}
}
configurations.all {
resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
if (dependency.requested instanceof ModuleComponentSelector && dependency.requested.group == "org.example") {
def targetProject = findProject(":${dependency.requested.module}")
if (targetProject != null) {
dependency.useTarget targetProject
}
}
}
}
-
The substitution only occurs if a local project matching the dependency name is found.
-
The local project must already be included in the multi-project build (via
settings.gradle
).
Substituting a dependency with another variant
You can substitute a dependency with another variant, such as switching between a platform dependency and a regular library dependency.
This is useful when your build process needs to change the type of dependency based on specific conditions:
configurations.all {
resolutionStrategy.dependencySubstitution {
all {
if (requested is ModuleComponentSelector && requested.group == "org.example" && requested.version == "1.0") {
useTarget(module("org.example:library:1.0")).because("Switching from platform to library variant")
}
}
}
}
-
The substitution is based on the requested dependency’s attributes (like group and version).
-
This approach allows you to switch from a platform component to a library or vice versa.
-
It uses Gradle’s variant-aware engine to ensure the correct variant is selected based on the configuration and consumer attributes.
This flexibility is often required when working with complex dependency graphs where different component types (platforms, libraries) need to be swapped dynamically.
Substituting a dependency with attributes
Substituting a dependency based on attributes allows you to override the default selection of a component by targeting specific attributes (like platform vs. regular library).
This technique is useful for managing platform and library dependencies in complex builds, particularly when you want to consume a regular library but the platform dependency was incorrectly declared:
dependencies {
// This is a platform dependency but you want the library
implementation(platform("com.google.guava:guava:28.2-jre"))
}
dependencies {
// This is a platform dependency but you want the library
implementation platform('com.google.guava:guava:28.2-jre')
}
In this example, the substitution rule targets the platform version of com.google.guava:guava
and replaces it with the regular library version:
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(platform(module("com.google.guava:guava:28.2-jre")))
.using(module("com.google.guava:guava:28.2-jre"))
}
}
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(platform(module('com.google.guava:guava:28.2-jre'))).
using module('com.google.guava:guava:28.2-jre')
}
}
Without the platform
keyword, the substitution would not specifically target the platform dependency.
The following rule performs the same substitution but uses the more granular variant notation, allowing for customization of the dependency’s attributes:
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(variant(module("com.google.guava:guava:28.2-jre")) {
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.REGULAR_PLATFORM))
}
}).using(module("com.google.guava:guava:28.2-jre"))
}
}
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute variant(module('com.google.guava:guava:28.2-jre')) {
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.REGULAR_PLATFORM))
}
} using module('com.google.guava:guava:28.2-jre')
}
}
By using attribute-based substitution, you can precisely control which dependencies are replaced, ensuring Gradle resolves the correct versions and variants in your build.
Refer to the DependencySubstitutions API for a complete reference.
In composite builds, the rule that you have to match the exact requested dependency attributes is not applied. When using composites, Gradle will automatically match the requested attributes. In other words, it is implicit that if you include another build, you are substituting all variants of the substituted module with an equivalent variant in the included build. |
Substituting a dependency with a dependency with capabilities
You can substitute a dependency with a different variant that includes specific capabilities. Capabilities allow you to specify that a particular variant of a dependency offers a set of related features or functionality, such as test fixtures.
This example substitutes a regular dependency with its test fixtures using a capability:
configurations.testCompileClasspath {
resolutionStrategy.dependencySubstitution {
substitute(module("com.acme:lib:1.0")).using(variant(module("com.acme:lib:1.0")) {
capabilities {
requireCapability("com.acme:lib-test-fixtures")
}
})
}
}
configurations.testCompileClasspath {
resolutionStrategy.dependencySubstitution {
substitute(module('com.acme:lib:1.0'))
.using variant(module('com.acme:lib:1.0')) {
capabilities {
requireCapability('com.acme:lib-test-fixtures')
}
}
}
}
Here, we substitute the regular com.acme:lib:1.0
dependency with its lib-test-fixtures
variant.
The requireCapability
function specifies that the new variant must have the com.acme:lib-test-fixtures
capability, ensuring the right version of the dependency is selected for testing purposes.
Capabilities within the substitution rule are used to precisely match dependencies, and Gradle only substitutes dependencies that match the required capabilities.
Refer to the DependencySubstitutions API for a complete reference of the variant substitution API.
Substituting a dependency with a classifier or artifact
You can substitute dependencies that have a classifier with ones that don’t or vice versa. Classifiers are often used to represent different versions of the same artifact, such as platform-specific builds or dependencies with different APIs. Although Gradle discourages the use of classifiers, it provides a way to handle substitutions for cases where classifiers are still in use.
Consider the following setup:
dependencies {
implementation("com.google.guava:guava:28.2-jre")
implementation("co.paralleluniverse:quasar-core:0.8.0")
implementation(project(":lib"))
}
dependencies {
implementation 'com.google.guava:guava:28.2-jre'
implementation 'co.paralleluniverse:quasar-core:0.8.0'
implementation project(':lib')
}
In the example above, the first level dependency on quasar
makes us think that Gradle would resolve quasar-core-0.8.0.jar
but it’s not the case.
The build fails with this message:
Execution failed for task ':consumer:resolve'.
> Could not resolve all files for configuration ':consumer:runtimeClasspath'.
> Could not find quasar-core-0.8.0-jdk8.jar (co.paralleluniverse:quasar-core:0.8.0).
Searched in the following locations:
https://repo.maven.apache.org/maven2/co/paralleluniverse/quasar-core/0.8.0/quasar-core-0.8.0-jdk8.jar
That’s because there’s a dependency on another project, lib
, which itself depends on a different version of quasar-core
:
dependencies {
implementation("co.paralleluniverse:quasar-core:0.7.10:jdk8")
}
dependencies {
implementation "co.paralleluniverse:quasar-core:0.7.10:jdk8"
}
-
The consumer depends on
quasar-core:0.8.0
without a classifier. -
The lib project depends on
quasar-core:0.7.10
with thejdk8
classifier. -
Gradle’s conflict resolution selects the higher version (
0.8.0
), butquasar-core:0.8.0
doesn’t have thejdk8
classifier, leading to a resolution error.
To resolve this conflict, you can instruct Gradle to ignore classifiers when resolving quasar-core
dependencies:
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(module("co.paralleluniverse:quasar-core"))
.using(module("co.paralleluniverse:quasar-core:0.8.0"))
.withoutClassifier()
}
}
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute module('co.paralleluniverse:quasar-core') using module('co.paralleluniverse:quasar-core:0.8.0') withoutClassifier()
}
}
This rule effectively replaces any dependency on quasar-core
found in the graph with a dependency without classifier.
If you need to substitute with a specific classifier or artifact, you can specify the classifier or artifact details in the substitution rule.
For more detailed information, refer to:
-
Artifact selection via the Substitution DSL
-
Artifact selection via the DependencySubstitution API
-
Artifact selection via the ResolutionStrategy API
4. Component Selection Rules
Component selection rules may influence which component instance should be selected when multiple versions are available that match a version selector. Rules are applied against every available version and allow the version to be explicitly rejected.
This allows Gradle to ignore any component instance that does not satisfy conditions set by the rule. Examples include:
-
For a dynamic version like
1.+
certain versions may be explicitly rejected from selection. -
For a static version like
1.4
an instance may be rejected based on extra component metadata such as the Ivy branch attribute, allowing an instance from a subsequent repository to be used.
Rules are configured via the ComponentSelectionRules object. Each rule configured will be called with a ComponentSelection object as an argument that contains information about the candidate version being considered. Calling ComponentSelection.reject(java.lang.String) causes the given candidate version to be explicitly rejected, in which case the candidate will not be considered for the selector.
The following example shows a rule that disallows a particular version of a module but allows the dynamic version to choose the next best candidate:
configurations {
implementation {
resolutionStrategy {
componentSelection {
// Accept the highest version matching the requested version that isn't '1.5'
all {
if (candidate.group == "org.sample" && candidate.module == "api" && candidate.version == "1.5") {
reject("version 1.5 is broken for 'org.sample:api'")
}
}
}
}
}
}
dependencies {
implementation("org.sample:api:1.+")
}
configurations {
implementation {
resolutionStrategy {
componentSelection {
// Accept the highest version matching the requested version that isn't '1.5'
all { ComponentSelection selection ->
if (selection.candidate.group == 'org.sample' && selection.candidate.module == 'api' && selection.candidate.version == '1.5') {
selection.reject("version 1.5 is broken for 'org.sample:api'")
}
}
}
}
}
}
dependencies {
implementation 'org.sample:api:1.+'
}
Note that version selection is applied starting with the highest version first. The version selected will be the first version found that all component selection rules accept.
A version is considered accepted if no rule explicitly rejects it. |
Similarly, rules can be targeted at specific modules.
Modules must be specified in the form of group:module
:
configurations {
create("targetConfig") {
resolutionStrategy {
componentSelection {
withModule("org.sample:api") {
if (candidate.version == "1.5") {
reject("version 1.5 is broken for 'org.sample:api'")
}
}
}
}
}
}
configurations {
targetConfig {
resolutionStrategy {
componentSelection {
withModule("org.sample:api") { ComponentSelection selection ->
if (selection.candidate.version == "1.5") {
selection.reject("version 1.5 is broken for 'org.sample:api'")
}
}
}
}
}
}
Component selection rules can also consider component metadata when selecting a version. Possible additional metadata that can be considered are ComponentMetadata and IvyModuleDescriptor.
Note that this extra information may not always be available and thus should be checked for null
values:
configurations {
create("metadataRulesConfig") {
resolutionStrategy {
componentSelection {
// Reject any versions with a status of 'experimental'
all {
if (candidate.group == "org.sample" && metadata?.status == "experimental") {
reject("don't use experimental candidates from 'org.sample'")
}
}
// Accept the highest version with either a "release" branch or a status of 'milestone'
withModule("org.sample:api") {
if (getDescriptor(IvyModuleDescriptor::class)?.branch != "release" && metadata?.status != "milestone") {
reject("'org.sample:api' must have testing branch or milestone status")
}
}
}
}
}
}
configurations {
metadataRulesConfig {
resolutionStrategy {
componentSelection {
// Reject any versions with a status of 'experimental'
all { ComponentSelection selection ->
if (selection.candidate.group == 'org.sample' && selection.metadata?.status == 'experimental') {
selection.reject("don't use experimental candidates from 'org.sample'")
}
}
// Accept the highest version with either a "release" branch or a status of 'milestone'
withModule('org.sample:api') { ComponentSelection selection ->
if (selection.getDescriptor(IvyModuleDescriptor)?.branch != "release" && selection.metadata?.status != 'milestone') {
selection.reject("'org.sample:api' must be a release branch or have milestone status")
}
}
}
}
}
}
A ComponentSelection argument is always required as a parameter when declaring a component selection rule.
5. Default Dependencies
You can set default dependencies for a configuration to ensure that a default version is used when no explicit dependencies are specified.
This is useful for plugins that rely on versioned tools and want to provide a default if the user doesn’t specify a version:
configurations {
create("pluginTool") {
defaultDependencies {
add(project.dependencies.create("org.gradle:my-util:1.0"))
}
}
}
configurations {
pluginTool {
defaultDependencies { dependencies ->
dependencies.add(project.dependencies.create("org.gradle:my-util:1.0"))
}
}
}
In this example, the pluginTool
configuration will use org.gradle:my-util:1.0
as a default dependency unless another version is specified.
6. Excluding Transitive Dependencies
To completely exclude a transitive dependency for a particular configuration, use the Configuration.exclude(Map)
method.
This approach will automatically exclude the specified transitive dependency from all dependencies declared within the configuration:
configurations {
"implementation" {
exclude(group = "commons-collections", module = "commons-collections")
}
}
dependencies {
implementation("commons-beanutils:commons-beanutils:1.9.4")
implementation("com.opencsv:opencsv:4.6")
}
configurations {
implementation {
exclude group: 'commons-collections', module: 'commons-collections'
}
}
dependencies {
implementation 'commons-beanutils:commons-beanutils:1.9.4'
implementation 'com.opencsv:opencsv:4.6'
}
In this example, the commons-collections
dependency will be excluded from the implementation
configuration, regardless of whether it is a direct or transitive dependency.
7. Force Failed Resolution Strategies
Version conflicts can be forced to fail using:
-
failOnNonReproducibleResolution()
-
failOnDynamicVersions()
-
failOnChangingVersions()
-
failOnVersionConflict()
This will fail the build when conflicting versions of the same dependency are found:
configurations.all {
resolutionStrategy {
failOnVersionConflict()
}
}
configurations.all {
resolutionStrategy {
failOnVersionConflict()
}
}
8. Disabling Transitive Dependencies
By default, Gradle resolves all transitive dependencies for a given module.
However, there are situations where you may want to disable this behavior, such as when you need more control over dependencies or when the dependency metadata is incorrect.
You can tell Gradle to disable transitive dependency management for a dependency by setting ModuleDependency.setTransitive(boolean) to false
.
In the following example, transitive dependency resolution is disabled for the guava
dependency:
dependencies {
implementation("com.google.guava:guava:23.0") {
isTransitive = false
}
}
dependencies {
implementation('com.google.guava:guava:23.0') {
transitive = false
}
}
This ensures only the main artifact for guava
is resolved, and none of its transitive dependencies will be included.
Disabling transitive dependency resolution will likely require you to declare the necessary runtime dependencies in your build script which otherwise would have been resolved automatically. Not doing so might lead to runtime classpath issues. |
If you want to disable transitive resolution globally across all dependencies, you can set this behavior at the configuration level:
configurations.all {
isTransitive = false
}
dependencies {
implementation("com.google.guava:guava:23.0")
}
configurations.all {
transitive = false
}
dependencies {
implementation 'com.google.guava:guava:23.0'
}
This disables transitive resolution for all dependencies in the project. Be aware that this may require you to manually declare any transitive dependencies that are required at runtime.
For more information, see Configuration.setTransitive(boolean).
9. Dependency Resolve Rules and Other Conditionals
Dependency resolve rules are executed for each dependency as it’s being resolved, providing a powerful API to modify a dependency’s attributes—such as group, name, or version—before the resolution is finalized.
This allows for advanced control over dependency resolution, enabling you to substitute one module for another during the resolution process.
This feature is particularly useful for implementing advanced dependency management patterns. With dependency resolve rules, you can redirect dependencies to specific versions or even different modules entirely, allowing you to enforce consistent versions across a project or override problematic dependencies:
configurations.all {
resolutionStrategy {
eachDependency {
if (requested.group == "com.example" && requested.name == "old-library") {
useTarget("com.example:new-library:1.0.0")
because("Our license only allows use of version 1")
}
}
}
}
configurations.all {
resolutionStrategy {
eachDependency {
if (requested.group == "com.example" && requested.name == "old-library") {
useTarget("com.example:new-library:1.0.0")
because("Our license only allows use of version 1")
}
}
}
}
In this example, if a dependency on com.example:old-library
is requested, it will be substituted with com.example:new-library:1.0.0
during resolution.
For more advanced usage and additional examples, refer to the ResolutionStrategy class in the API documentation.
Implementing a custom versioning scheme
In some corporate environments, module versions in Gradle builds are maintained and audited externally. Dependency resolve rules offer an effective way to implement this:
-
Developers declare dependencies in the build script using the module’s group and name, but specify a placeholder version like
default
. -
A dependency resolve rule then resolves the
default
version to an approved version, which is retrieved from a corporate catalog of sanctioned modules.
This approach ensures that only approved versions are used, while allowing developers to work with a simplified and consistent versioning scheme.
The rule implementation can be encapsulated in a corporate plugin, making it easy to apply across all projects within the organization:
configurations.all {
resolutionStrategy.eachDependency {
if (requested.version == "default") {
val version = findDefaultVersionInCatalog(requested.group, requested.name)
useVersion(version.version)
because(version.because)
}
}
}
data class DefaultVersion(val version: String, val because: String)
fun findDefaultVersionInCatalog(group: String, name: String): DefaultVersion {
//some custom logic that resolves the default version into a specific version
return DefaultVersion(version = "1.0", because = "tested by QA")
}
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (details.requested.version == 'default') {
def version = findDefaultVersionInCatalog(details.requested.group, details.requested.name)
details.useVersion version.version
details.because version.because
}
}
}
def findDefaultVersionInCatalog(String group, String name) {
//some custom logic that resolves the default version into a specific version
[version: "1.0", because: 'tested by QA']
}
In this setup, whenever a developer specifies default
as the version, the resolve rule replaces it with the approved version from the corporate catalog.
This strategy ensures compliance with corporate policies while providing flexibility and ease of use for developers. Encapsulating this logic in a plugin also ensures consistency across multiple projects.
Replacing unwanted dependency versions
Dependency resolve rules offer a powerful mechanism for blocking specific versions of a dependency and substituting them with an alternative.
This is particularly useful when a specific version is known to be problematic—such as a version that introduces bugs or relies on a library that isn’t available in public repositories. By defining a resolve rule, you can automatically replace a problematic version with a stable one.
Consider a scenario where version 1.2
of a library is broken, but version 1.2.1
contains important fixes and should always be used instead.
With a resolve rule, you can enforce this substitution: "anytime version 1.2
is requested, it will be replaced with 1.2.1
.
Unlike forcing a version, this rule only affects the specific version 1.2
, leaving other versions unaffected:
configurations.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.software" && requested.name == "some-library" && requested.version == "1.2") {
useVersion("1.2.1")
because("fixes critical bug in 1.2")
}
}
}
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (details.requested.group == 'org.software' && details.requested.name == 'some-library' && details.requested.version == '1.2') {
details.useVersion '1.2.1'
details.because 'fixes critical bug in 1.2'
}
}
}
If version 1.3
is also present in the dependency graph, then even with this rule, Gradle’s default conflict resolution strategy would select 1.3
as the latest version.
Difference from Rich Version Constraints: Using rich version constraints, you can reject certain versions outright, causing the build to fail or select a non-rejected version if a dynamic dependency is used. In contrast, a dependency resolve rule like the one shown here manipulates the version being requested, replacing it with a known good version when a rejected one is found. This approach is a solution for handling rejected versions, while rich version constraints are about expressing the intent to avoid certain versions.
Lazily influencing resolved dependencies
Plugins can lazily influence dependencies by adding them conditionally or setting preferred versions when no version is specified by the user.
Below are two examples illustrating these use cases.
This example demonstrates how to add a dependency to a configuration based on some condition, evaluated lazily:
configurations {
implementation {
dependencies.addLater(project.provider {
val dependencyNotation = conditionalLogic()
if (dependencyNotation != null) {
project.dependencies.create(dependencyNotation)
} else {
null
}
})
}
}
configurations {
implementation {
dependencies.addLater(project.provider {
def dependencyNotation = conditionalLogic()
if (dependencyNotation != null) {
return project.dependencies.create(dependencyNotation)
} else {
return null
}
})
}
}
In this case, addLater
is used to defer the evaluation of the dependency, allowing it to be added only when certain conditions are met.
In this example, the build script sets a preferred version of a dependency, which will be used if no version is explicitly specified:
Example 2: Preferring a Default Version of a Dependency
dependencies {
implementation("org:foo")
// Can indiscriminately be added by build logic
constraints {
implementation("org:foo:1.0") {
version {
// Applied to org:foo if no other version is specified
prefer("1.0")
}
}
}
}
dependencies {
implementation("org:foo")
// Can indiscriminately be added by build logic
constraints {
implementation("org:foo:1.0") {
version {
// Applied to org:foo if no other version is specified
prefer("1.0")
}
}
}
}
This ensures that org:foo
uses version 1.0
unless the user specifies another version.