In Gradle, dependency resolution is often thought of from the standpoint of a consumer and a producer. The consumer declares dependencies and performs dependency resolution, while producers satisfy those dependencies by exposing variants.

Gradle’s resolution engine follows a dynamic approach to dependency resolution called variant-aware resolution, where the consumer defines requirements using attributes, which are matched with the attributes declared by the producer.

Variant-aware resolution allows Gradle to automatically select the correct variant from a producer without the consumer explicitly specifying which one to use.

For instance, if you’re working with different architectures (like arm64 and i386), Gradle can choose the appropriate version of a library (myLib) for each architecture:

  1. The producer, myLib, exposes variants (arm64Elements, i386Elements) with specific attributes (e.g., ArchType.ARM64, ArchType.I386).

  2. The consumer, myApp, specifies the required attributes (e.g., ArchType.ARM64) in its resolvable configuration (runtimeClasspath).

  3. If the consumer, myApp, requires dependencies for the arm64 architecture, Gradle will automatically pick the arm64Elements variant from the myLib producer and use its corresponding artifact.

A coded example

Consider a Java library where you create a new variant called instrumentedJars and want to ensure it’s selected for testing:

  1. Producer Project: Creates a specialized instrumentedJars variant marked with specific attributes.

  2. Consumer Project: Configured to request the instrumented-jar attribute for testing.

Let’s look at the build files of the producer and consumer.

The producer side

1. Create an instrumented JAR:

Our Java library has a task called instrumentedJar which produces a JAR file. We expect other projects to consume this JAR file.

producer/build.gradle.kts
val instrumentedJar = tasks.register("instrumentedJar", Jar::class) {
    archiveClassifier = "instrumented"
}
producer/build.gradle
def instrumentedJar = tasks.register("instrumentedJar", Jar) {
    archiveClassifier = "instrumented"
}

2. Create a custom outgoing configuration:

We want our instrumented classes to be used when executing tests, so we need to define proper attributes on our variant. We create a new configuration named instrumentedJars. This configuration:

  • Can be consumed by other projects.

  • Cannot be resolved (i.e., it’s meant to be used as an output, not an input).

  • Has specific attributes, including LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE set to "instrumented-jar", which explains what the variant contains.

producer/build.gradle.kts
val instrumentedJars by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
        attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
        attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInt())
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("instrumented-jar"))
    }
}
producer/build.gradle
configurations {
    instrumentedJars {
        canBeConsumed = true
        canBeResolved = false
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
            attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
            attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
            attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInteger())
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
        }
    }
}

3. Attach the Artifact:

The instrumentedJar task’s output is added to the instrumentedJars configuration as an artifact. When this variant is included in a dependency graph, this artifact will be resolved during artifact resolution.

producer/build.gradle.kts
artifacts {
    add("instrumentedJars", instrumentedJar)
}
producer/build.gradle
artifacts {
    instrumentedJars(instrumentedJar)
}

What we have done here is that we have added a new variant, which can be used at runtime, but contains instrumented classes instead of the normal classes. However, it now means that for runtime, the consumer has to choose between two variants:

  1. runtimeElements, the regular variant offered by the java-library plugin

  2. instrumentedJars, the variant we have created

The consumer side

1. Add dependencies:

First, on the consumer side, like any other project, we define the Java library as a dependency:

consumer/build.gradle.kts
dependencies {
    testImplementation("junit:junit:4.13")
    testImplementation(project(":producer"))
}
consumer/build.gradle
dependencies {
    testImplementation 'junit:junit:4.13'
    testImplementation project(':producer')
}

At this point, Gradle will still select the default runtimeElements variant for your dependencies. This is because the testRuntimeClasspath configuration is requesting artifacts with the jar library elements attribute, while the producer defines the instrumentedJars variant with a different attribute.

2. Adjust the requested attributes:

The testRuntimeClasspath configuration is modified to ask for "instrumented-jar" versions of the dependencies. This means that when Gradle resolves dependencies for this configuration, it will prefer JAR files that are marked as "instrumented":

consumer/build.gradle.kts
configurations {
    testRuntimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, "instrumented-jar"))
        }
    }
}
consumer/build.gradle
configurations {
    testRuntimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
        }
    }
}

By following these steps, Gradle will intelligently select the correct variants based on the configuration and attributes, while also handling cases where specialized variants are not available.