Search

April 19, 2019

Gradle Goodness: Manage Dependency Versions With Dependency Constraints

From Maven builds we know the dependencyManagement section in our POM file. In the section we can describe dependencies with their version and later in the dependencies section we can refer to the dependency without the version. We can use dependency constraints in Gradle to do the same thing. A dependency constraint can be used to define the version or version range for a dependency defined in our scripts or a transitive dependency. Just like a dependency the dependency constraint is defined for a configuration, so we can fine tune the constraints to the correct configuration.

Using dependency constraints in a multi-project build allows us to define the dependency versions in the root build file and define project dependencies per project without a version. The version will then be used from the dependency constraint we defined in the root build file.

In the following example build script we define two dependency constraints for different configurations:

// File: build.gradle.kts
plugins {
    groovy
}

repositories {
    jcenter()
}

// In a multi-project build, this dependencies
// block with constraints could be in the 
// root build file, so all versions are 
// defined in one place.
dependencies {
    constraints {
        // Define dependency with version to be used.
        // This version is used when we define a dependency
        // for guava without a version.
        implementation("com.google.guava:guava:27.1-jre")

        // Constraints are scoped to configurations,
        // so we can make specific constraints for a configuration.
        // In this case we want the dependency on Spock defined
        // in the testImplementation configuration to be a specific version.
        // Here we use named arguments to define the dependency constraint.
        testImplementation(group = "org.spockframework", 
                           name = "spock-core", 
                           version = "1.3-groovy-2.5")
    }
}

// In a multi-project build this dependencies block
// could be in subprojects, where the dependency
// declarations do not need a version, because the
// versions are defined in the root build file using 
// constraints.
dependencies {
    // Because of the dependency constraint, 
    // we don't have to specify the dependency version here.
    implementation("com.google.guava:guava")

    // Another dependency without version for the 
    // testImplementation configuration.
    testImplementation("org.spockframework:spock-core")
}

Let's run the dependencies task for the configuration compileClasspath to see how the dependencies are resolved:

$ gradle -q dependencies --configuration compileClasspath
------------------------------------------------------------
Root project
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- com.google.guava:guava -> 27.1-jre
|    +--- com.google.guava:failureaccess:1.0.1
|    +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
|    +--- com.google.code.findbugs:jsr305:3.0.2
|    +--- org.checkerframework:checker-qual:2.5.2
|    +--- com.google.errorprone:error_prone_annotations:2.2.0
|    +--- com.google.j2objc:j2objc-annotations:1.1
|    \--- org.codehaus.mojo:animal-sniffer-annotations:1.17
\--- com.google.guava:guava:27.1-jre (c)

(c) - dependency constraint
A web-based, searchable dependency report is available by adding the --scan option.
$

We see that the Guava dependency is resolved for version 27.1-jre. In the output is also shown with (c) that a dependency constraint is used to resolve the dependency.

When we look at the testCompileClasspath configuration we see the following output:

$ gradle -q dependencies --configuration testCompileClasspath
> Task :dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

testCompileClasspath - Compile classpath for source set 'test'.
+--- com.google.guava:guava -> 27.1-jre
|    +--- com.google.guava:failureaccess:1.0.1
|    +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
|    +--- com.google.code.findbugs:jsr305:3.0.2
|    +--- org.checkerframework:checker-qual:2.5.2
|    +--- com.google.errorprone:error_prone_annotations:2.2.0
|    +--- com.google.j2objc:j2objc-annotations:1.1
|    \--- org.codehaus.mojo:animal-sniffer-annotations:1.17
+--- com.google.guava:guava:27.1-jre (c)
+--- org.spockframework:spock-core:1.3-groovy-2.5
|    +--- org.codehaus.groovy:groovy:2.5.4
|    +--- org.codehaus.groovy:groovy-json:2.5.4
|    |    \--- org.codehaus.groovy:groovy:2.5.4
|    +--- org.codehaus.groovy:groovy-nio:2.5.4
|    |    \--- org.codehaus.groovy:groovy:2.5.4
|    +--- org.codehaus.groovy:groovy-macro:2.5.4
|    |    \--- org.codehaus.groovy:groovy:2.5.4
|    +--- org.codehaus.groovy:groovy-templates:2.5.4
|    |    +--- org.codehaus.groovy:groovy:2.5.4
|    |    \--- org.codehaus.groovy:groovy-xml:2.5.4
|    |         \--- org.codehaus.groovy:groovy:2.5.4
|    +--- org.codehaus.groovy:groovy-test:2.5.4
|    |    +--- org.codehaus.groovy:groovy:2.5.4
|    |    \--- junit:junit:4.12
|    |         \--- org.hamcrest:hamcrest-core:1.3
|    +--- org.codehaus.groovy:groovy-sql:2.5.4
|    |    \--- org.codehaus.groovy:groovy:2.5.4
|    +--- org.codehaus.groovy:groovy-xml:2.5.4 (*)
|    \--- junit:junit:4.12 (*)
\--- org.spockframework:spock-core -> 1.3-groovy-2.5 (*)

(c) - dependency constraint
(*) - dependencies omitted (listed previously)

A web-based, searchable dependency report is available by adding the --scan option.
$

Read more about Gradle dependency constraints on the Gradle website.

Written with Gradle 5.4.