How to inherit dependencies scope from different Gradle sub-projects?

72 views Asked by At

Consider the following simple build (settings.gradle.kts):

dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
}

include("main-module")
include("specific-test-module")

The main module is as simple as:

plugins {
    id("java")
}

dependencies {
    implementation("org.slf4j:slf4j-api:2.0.9")
}

My goal is to inherit implementation dependencies from main-module in testImplementation scope in specific-tests-module. The test module is so specific, that I have created a separate Gradle sub-project for it. Now I am trying to achieve my goal with extendsFrom on configuration, but the problem is that extendsFrom stops working as soon as I try to use it outside of a single Gradle sub-project.

While adding a new scope in main-module/build.gradle.kts works as expected:

configurations.create("someNewScope") {
    extendsFrom(configurations.implementation.get()) // <--- 'someNewScope' will inherit 'slf4j-api'
}

an attempt to inherit scope in another sub-project does not make any effect:

configurations.create("someScope") {
    extendsFrom(project(":main-module").configurations.implementation.get())
}

Seems like an attempt to read configuration of another sub-project happens too early, when dependencies of another sub-project are not yet configured. How can I make scopes inheritance to work between different Gradle's sub-projects?

1

There are 1 answers

0
Kirill On BEST ANSWER

The idiomatic and the only recommended approach to share something between Gradle projects is to use "Variants". Here is the corresponding documentation section: Producing and Consuming Variants of Libraries > Sharing outputs between projects.

It explicitly says:

Don’t reference other project tasks directly

and provides the following anti-pattern example:

dependencies {
   // this is unsafe!
   implementation project(":other").tasks.someOtherJar
}

Very similar to what I've tried to do initially and it was wrong.

In my case it was enough to follow the Simple sharing of artifacts between projects section of the documentation and expose dependencies through "consumable" configurations like this:

val exportedCompileOnly by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
    extendsFrom(configurations.compileOnly.get())
}

val exportedTestImplementation by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
    extendsFrom(configurations.implementation.get())
}

val exportedTestRuntimeOnly by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
    extendsFrom(configurations.runtimeOnly.get())
}

After that I was able to add dependency on a project and inherit its exposed configurations as follows:

dependencies {
    testImplementation(project(":main-module"))
    testImplementation(project(mapOf("path" to ":main-module", "configuration" to "exportedTestImplementation")))
    testCompileOnly(project(mapOf("path" to ":main-module", "configuration" to "exportedCompileOnly")))
    testRuntimeOnly(project(mapOf("path" to ":main-module", "configuration" to "exportedTestRuntimeOnly")))
}

The reason why it's necessary to create additional configuration which from the first sight just extendsFrom already existing configuration is because you need "consumable" configuration to make it visible to other projects. Here "consumable" configuration is a variant of how other Gradle projects can consume the project when specifying it in dependency {} block.

In my solution I:

  • implicitly used a default variant by declaring dependency as testImplementation(project(":main-module")) (to add dependency on src/main of main-module)
  • and also added dependencies on different variants of main-module by explicitly specifying variant name, for example testImplementation(project(mapOf("path" to ":main-module", "configuration" to "exportedTestImplementation"))) (to inherit dependencies of :main-module through "consumable" configuration)

Additional references: