Gradle Variant-Aware Resolution & Module Metadata
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Gradle 9.8.1 and was run on JDK 26 on 2026-10-16 (the daemon and the compiler both ran on JDK 26; no toolchain was involved). Every example was run through the wrapper in scratch copies of a small tidepool build, once with Kotlin DSL build files and once with Groovy DSL build files, and the output is the same in both apart from the order of unrelated tasks, which Gradle does not fix. The scratch copies pointed their wrapper at a local copy of the same distribution archive; the properties file that a real project commits is
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
The commands use --console=plain and -q unless a listing shows the task lines. In the listings the "Consider enabling
configuration cache" hint, the link to the problems report, the Try: hints of failures, the lines of the buildSrc
build and the daemon start-up line are left out, and the scratch directory is shown as /work/tidepool. Where a listing
is shortened the shortening is marked on its own line with an ellipsis. The report tasks and the data tasks below were run with --configuration-cache and are compatible
with it; the failing resolutions are shown without it, because with it a failed resolution inside a task is wrapped in a
serialization error that hides the real message. Gradle 8.14.x was not installed here, so statements about it come from
the official documentation and are labelled "not run here".
The Java plugins, dependency declaration and publishing are covered elsewhere and are only used here as a means to show variants: Declaring Dependencies & Configurations covers the configuration roles, Dependency Resolution, Conflicts & Capabilities covers conflict resolution and capability conflicts, and Publishing covers publications and repositories.
From scopes to variants
Maven describes a component with one POM: a list of dependencies, each in a scope, plus optional classifiers on the artifacts. Ivy describes it with configurations. Neither can say "this component has a variant for compiling, another for running, a third that ships sample data, and a fourth for Java 8". Gradle can, because it models a published or project component as a set of variants:
-
a component is a module at a version (
io.tidepool:tidepool-model:1.0); -
a variant is one way of consuming it: its own attributes (key-value pairs), its own dependencies, its own artifacts and its own capabilities;
-
an attribute is a typed key such as
org.gradle.usagewith a value such asjava-api; -
a capability is a
group:name:versionthat the variant provides; by default it is the component itself.
A consumer does not ask for a configuration by name. It asks for attributes ("a library, for use at compile time, for Java 26") and Gradle selects the variant of each dependency that matches. This is variant-aware dependency resolution, the engine behind the Java plugins, Android, Kotlin Multiplatform and every other ecosystem plugin. The selection happens twice: once for the dependency graph (which variant of each component) and once for the artifacts (which files of that variant, possibly transformed).
The attributes that the JVM plugins set are the vocabulary the rest of this page uses.
| Attribute | Typical values | Meaning |
|---|---|---|
|
|
Compile-time API or runtime use. Second in the precedence order, after the category. |
|
|
What kind of thing the variant is. A BOM is a |
|
|
The shape of the artifacts: a jar or a directory of classes. |
|
|
Whether the dependencies are bundled in (a shadow jar) or external. |
|
|
The Java version the binaries target. A consumer on Java 26 accepts a variant built for Java 26 or lower. |
|
|
The kind of JVM. |
|
|
Which kind of documentation, for variants of category |
|
|
The component-level status, set on published metadata. |
Variants arrived in stages: Gradle Module Metadata 1.0 and feature variants in Gradle 5.3, test fixtures in 5.6, and
Gradle Module Metadata published by default in 6.0. The explicit configuration roles used below (consumable,
resolvable, dependencyScope) are the Gradle 8.4 API. Everything on this page behaves the same on Gradle 8.14 apart
from deprecations that are named where they matter (not run here).
The example build
All the examples use one small three-project build with a producer, tidepool-model, and two consumers, tidepool-engine
and tidepool-cli. tidepool-model is a java-library that
-
registers an optional feature,
csv, in its own source set, with its own dependency on Apache Commons CSV; -
applies
java-test-fixtures; -
declares two custom outgoing variants,
tideDataandharmonicsData, that carry sample data files and are told apart by a custom attribute,io.tidepool.flavor; -
publishes all of it to a
file://repository under its own build directory withmaven-publishandivy-publish.
The sample data is data/stations.csv (the three stations of the tidepool example) and a second file,
data/harmonics.csv, with three rows of constituent speeds.
The settings file declares the repository once and fails the build when a project declares its own:
rootProject.name = "tidepool"
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
include("tidepool-model", "tidepool-engine", "tidepool-cli")
rootProject.name = 'tidepool'
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
The producer, with the feature, the test fixtures, the two custom variants and the publications:
plugins {
`java-library`
`java-test-fixtures`
`maven-publish`
`ivy-publish`
}
group = "io.tidepool"
version = "1.0"
val flavor = Attribute.of("io.tidepool.flavor", String::class.java)
java {
registerFeature("csv") {
usingSourceSet(sourceSets.create("csv"))
withSourcesJar()
}
}
dependencies {
"csvApi"("org.apache.commons:commons-csv:1.14.1")
}
// two custom outgoing variants, one per flavor of sample data
fun dataVariant(name: String, flavorName: String, file: String) =
configurations.consumable(name) {
description = "Sample data, flavor $flavorName"
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>("tidepool-sample"))
attribute(Usage.USAGE_ATTRIBUTE, objects.named<Usage>("tidepool-data"))
attribute(flavor, flavorName)
}
outgoing.artifact(rootProject.file(file)) {
classifier = flavorName
}
}
val tideData = dataVariant("tideData", "tides", "data/stations.csv")
val harmonicsData = dataVariant("harmonicsData", "harmonics", "data/harmonics.csv")
// publish the two data variants together with the library variants
val javaComponent = components["java"] as AdhocComponentWithVariants
javaComponent.addVariantsFromConfiguration(tideData.get()) { }
javaComponent.addVariantsFromConfiguration(harmonicsData.get()) { }
publishing {
publications {
create<IvyPublication>("ivy") {
from(javaComponent)
}
create<MavenPublication>("maven") {
from(javaComponent)
suppressPomMetadataWarningsFor("csvApiElements")
suppressPomMetadataWarningsFor("csvRuntimeElements")
suppressPomMetadataWarningsFor("csvSourcesElements")
suppressPomMetadataWarningsFor("testFixturesApiElements")
suppressPomMetadataWarningsFor("testFixturesRuntimeElements")
}
}
repositories {
ivy {
name = "localIvy"
url = uri(rootProject.layout.buildDirectory.dir("ivy-repo"))
}
maven {
name = "local"
url = uri(rootProject.layout.buildDirectory.dir("repo"))
}
}
}
// -PnoGmm: stop publishing the .module file
if (providers.gradleProperty("noGmm").isPresent) {
tasks.withType<GenerateModuleMetadata>().configureEach {
enabled = false
}
}
plugins {
id 'java-library'
id 'java-test-fixtures'
id 'maven-publish'
id 'ivy-publish'
}
group = 'io.tidepool'
version = '1.0'
def flavor = Attribute.of('io.tidepool.flavor', String)
java {
registerFeature('csv') {
usingSourceSet(sourceSets.create('csv'))
withSourcesJar()
}
}
dependencies {
csvApi 'org.apache.commons:commons-csv:1.14.1'
}
// two custom outgoing variants, one per flavor of sample data
def dataVariant = { String variantName, String flavorName, String path ->
configurations.consumable(variantName) {
description = "Sample data, flavor $flavorName"
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, 'tidepool-sample'))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, 'tidepool-data'))
attribute(flavor, flavorName)
}
outgoing.artifact(rootProject.file(path)) {
classifier = flavorName
}
}
}
def tideData = dataVariant.call('tideData', 'tides', 'data/stations.csv')
def harmonicsData = dataVariant.call('harmonicsData', 'harmonics', 'data/harmonics.csv')
// publish the two data variants together with the library variants
def javaComponent = components.java
javaComponent.addVariantsFromConfiguration(tideData.get()) { }
javaComponent.addVariantsFromConfiguration(harmonicsData.get()) { }
publishing {
publications {
ivy(IvyPublication) {
from javaComponent
}
maven(MavenPublication) {
from javaComponent
suppressPomMetadataWarningsFor 'csvApiElements'
suppressPomMetadataWarningsFor 'csvRuntimeElements'
suppressPomMetadataWarningsFor 'csvSourcesElements'
suppressPomMetadataWarningsFor 'testFixturesApiElements'
suppressPomMetadataWarningsFor 'testFixturesRuntimeElements'
}
}
repositories {
ivy {
name = 'localIvy'
url = rootProject.layout.buildDirectory.dir('ivy-repo')
}
maven {
name = 'local'
url = rootProject.layout.buildDirectory.dir('repo')
}
}
}
// -PnoGmm: stop publishing the .module file
if (providers.gradleProperty('noGmm').present) {
tasks.withType(GenerateModuleMetadata).configureEach {
enabled = false
}
}
configurations.consumable(name) { ... } creates a configuration that can only be consumed (it declares outgoing
variants and cannot hold dependencies). It is the Gradle 8.4 replacement for the old
configurations.create(name) { isCanBeResolved = false } idiom. The Groovy version defines the helper as a closure
rather than as a method: a def variable of a Groovy script is not visible inside a script method, and the failure is
confusing, because flavor then silently resolves to the -Pflavor project property of the later examples and
attribute(flavor, flavorName) is called with two strings (No signature of method: ...attribute() is applicable for
argument types: (String, String)).
tidepool-engine asks for the csv feature of the model and depends on its test fixtures:
plugins {
`java-library`
}
dependencies {
api(project(":tidepool-model"))
// ask for the optional "csv" feature of tidepool-model
implementation(project(":tidepool-model")) {
capabilities {
requireFeature("csv")
}
}
testImplementation(testFixtures(project(":tidepool-model")))
}
plugins {
id 'java-library'
}
dependencies {
api project(':tidepool-model')
// ask for the optional "csv" feature of tidepool-model
implementation(project(':tidepool-model')) {
capabilities {
requireFeature('csv')
}
}
testImplementation testFixtures(project(':tidepool-model'))
}
tidepool-cli is the application, and the consumer of the sample data and of the artifact transform; its build file is
shown in the section on attribute matching and explained there and in the section on artifact transforms. The two java files of
the csv feature and of the fixtures are one-class stubs and not shown. The whole build, including the part the later
sections add, compiles and passes ./gradlew build with --configuration-cache on Gradle 9.8.1.
Seeing the variants
The outgoing variants report
outgoingVariants lists what a project offers to consumers. Run in the producer it prints, for the Java library plugin
alone, apiElements and runtimeElements; with the feature, the fixtures and the custom variants it prints these:
$ ./gradlew -q :tidepool-model:outgoingVariants | grep '^Variant'
Variant apiElements
Variant csvApiElements
Variant csvRuntimeElements
Variant csvSourcesElements
Variant harmonicsData
Variant mainSourceElements (i)
Variant runtimeElements
Variant testFixturesApiElements
Variant testFixturesRuntimeElements
Variant testResultsElementsForTest (i)
Variant tideData
Three of them in full. The csv feature’s runtime variant has its own capability, which is what lets a consumer ask for
it:
--------------------------------------------------
Variant csvRuntimeElements
--------------------------------------------------
Runtime elements for the 'csv' feature.
Capabilities
- io.tidepool:tidepool-model-csv:1.0
Attributes
- org.gradle.category = library
- org.gradle.dependency.bundling = external
- org.gradle.jvm.version = 26
- org.gradle.libraryelements = jar
- org.gradle.usage = java-runtime
Artifacts
- build/libs/tidepool-model-1.0-csv.jar (artifactType = jar, classifier = csv)
The custom data variants are variants like any other, with their own attributes and artifacts. The artifact type
(csv) comes from the file extension and the classifier from the build file:
--------------------------------------------------
Variant tideData
--------------------------------------------------
Sample data, flavor tides
Capabilities
- io.tidepool:tidepool-model:1.0 (default capability)
Attributes
- io.tidepool.flavor = tides
- org.gradle.category = tidepool-sample
- org.gradle.usage = tidepool-data
Artifacts
- ../data/stations.csv (artifactType = csv, classifier = tides)
--------------------------------------------------
Variant harmonicsData
--------------------------------------------------
Sample data, flavor harmonics
Capabilities
- io.tidepool:tidepool-model:1.0 (default capability)
Attributes
- io.tidepool.flavor = harmonics
- org.gradle.category = tidepool-sample
- org.gradle.usage = tidepool-data
Artifacts
- ../data/harmonics.csv (artifactType = csv, classifier = harmonics)
Each variant of the Java plugins also lists secondary variants, extra sets of artifacts that take part in artifact
selection but not in graph resolution. apiElements has classes; runtimeElements has classes and resources.
They are what lets a project on the same build consume the build/classes/java/main directory of its neighbour instead of
waiting for its jar:
--------------------------------------------------
Variant apiElements
--------------------------------------------------
API elements for the 'main' feature.
Capabilities
- io.tidepool:tidepool-model:1.0 (default capability)
Attributes
- org.gradle.category = library
- org.gradle.dependency.bundling = external
- org.gradle.jvm.version = 26
- org.gradle.libraryelements = jar
- org.gradle.usage = java-api
Artifacts
- build/libs/tidepool-model-1.0.jar (artifactType = jar)
Secondary Variants (*)
--------------------------------------------------
Secondary Variant classes
--------------------------------------------------
Directories containing compiled class files for main.
Attributes
- org.gradle.category = library
- org.gradle.dependency.bundling = external
- org.gradle.jvm.version = 26
- org.gradle.libraryelements = classes
- org.gradle.usage = java-api
Artifacts
- build/classes/java/main (artifactType = java-classes-directory)
Variants whose attributes carry a status marked incubating (the (i) in the names above, here
mainSourceElements and testResultsElementsForTest, category verification) are used by tools such as test-report
aggregation and are not relevant to compiling or running.
The artifactType shown on each artifact line is the attribute that artifact views and transforms work on, see Artifact transforms.
The resolvable configurations report
The consumer side is resolvableConfigurations. It prints a configuration’s requested attributes and the attributes
that have compatibility and disambiguation rules (io.tidepool.flavor is in both lists because of the rules added to the
consumer below). The disambiguation list shows the precedence (the number in
brackets) that decides which attribute is considered first:
$ ./gradlew -q :tidepool-cli:resolvableConfigurations --configuration dataPath
--------------------------------------------------
Configuration dataPath
--------------------------------------------------
Attributes
- io.tidepool.flavor = tides
- org.gradle.category = tidepool-sample
- org.gradle.usage = tidepool-data
Extended Configurations
- dataDeps
--------------------------------------------------
Compatibility Rules
--------------------------------------------------
The following Attributes have compatibility rules defined.
- io.tidepool.flavor
- org.gradle.dependency.bundling
- org.gradle.jvm.environment
- org.gradle.jvm.version
- org.gradle.libraryelements
- org.gradle.plugin.api-version
- org.gradle.usage
--------------------------------------------------
Disambiguation Rules
--------------------------------------------------
The following Attributes have disambiguation rules defined.
- io.tidepool.flavor
- org.gradle.category (1)
- org.gradle.dependency.bundling (5)
- org.gradle.jvm.environment (6)
- org.gradle.jvm.version (3)
- org.gradle.libraryelements (4)
- org.gradle.plugin.api-version
- org.gradle.usage (2)
(#): Attribute disambiguation precedence
Which variant did a dependency get
The report that answers "why did I get this file" is dependencyInsight, which prints the variant chosen and the
provided and requested attributes side by side. For tidepool-engine, which asked for the csv feature, two variants of
the same project component are selected, the main runtime variant and the feature’s:
$ ./gradlew -q :tidepool-engine:dependencyInsight --configuration runtimeClasspath --dependency tidepool-model
project ':tidepool-model'
Variant runtimeElements:
| Attribute Name | Provided | Requested |
|--------------------------------|--------------|--------------|
| org.gradle.category | library | library |
| org.gradle.dependency.bundling | external | external |
| org.gradle.jvm.version | 26 | 26 |
| org.gradle.libraryelements | jar | jar |
| org.gradle.usage | java-runtime | java-runtime |
| org.gradle.jvm.environment | | standard-jvm |
Variant csvRuntimeElements:
| Attribute Name | Provided | Requested |
|--------------------------------|--------------|--------------|
| org.gradle.category | library | library |
| org.gradle.dependency.bundling | external | external |
| org.gradle.jvm.version | 26 | 26 |
| org.gradle.libraryelements | jar | jar |
| org.gradle.usage | java-runtime | java-runtime |
| org.gradle.jvm.environment | | standard-jvm |
project ':tidepool-model'
\--- runtimeClasspath
project ':tidepool-model'
\--- runtimeClasspath
A web-based, searchable dependency report is available by adding the --scan option.
and the dependency of the feature, Commons CSV, reaches the engine through it, not through a declaration of its own:
$ ./gradlew -q :tidepool-engine:dependencyInsight --configuration runtimeClasspath --dependency commons-csv
org.apache.commons:commons-csv:1.14.1
Variant runtime:
| Attribute Name | Provided | Requested |
|--------------------------------|--------------|--------------|
| org.gradle.status | release | |
| org.gradle.category | library | library |
| org.gradle.libraryelements | jar | jar |
| org.gradle.usage | java-runtime | java-runtime |
| org.gradle.dependency.bundling | | external |
| org.gradle.jvm.environment | | standard-jvm |
| org.gradle.jvm.version | | 26 |
org.apache.commons:commons-csv:1.14.1
\--- project ':tidepool-model'
\--- runtimeClasspath
A web-based, searchable dependency report is available by adding the --scan option.
The attribute matching algorithm
When a consumer’s resolvable configuration meets a component, Gradle runs one algorithm over the candidate variants. In the order of the manual:
| Step | What happens |
|---|---|
1 |
Compatible candidates. Each candidate attribute must be missing, equal to the requested value, or accepted by the attribute’s compatibility rule. A candidate with an incompatible value is eliminated. A candidate that lacks an attribute the consumer asked for is not eliminated; it is marked as missing it. |
2 |
Most matching attributes. If one candidate is a strict superset of the others (it matches more attributes, or has no missing attributes compared to another), it wins. |
3 |
Disambiguation by requested attributes. The requested attributes are visited in precedence order (category, usage,
|
4 |
Extra attributes. Attributes that only some candidates have (and the consumer did not ask for) are used to prefer the candidates that lack them. |
5 |
Extra attribute presence. As a last resort, candidates that carry an extra attribute are removed. |
6 |
Failure. If no candidate remains, resolution fails with a no matching variant error that lists all candidates; if several remain, with an ambiguity error. |
Two cases skip the algorithm: a producer with no variants (a plain artifact) and a consumer that selects a configuration
by name (dependencies { implementation(project(":a", "someConfiguration")) }).
Asking for sample data
tidepool-cli consumes the sample data of the model. The dependency is declared on a dependency scope configuration,
which cannot be resolved, and the resolvable configuration extends it and carries the requested attributes. The flavor is
taken from the -Pflavor property so that every case below can be run; any means "do not ask for a flavor at all". The
file is shown in full: the attribute rules in the middle are explained in the next sections, and so are the transform and
the showRows task at the end.
import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
}
application {
mainClass = "io.tidepool.cli.Main"
}
val flavor = Attribute.of("io.tidepool.flavor", String::class.java)
// "tides-v2" may also be served by a "tides" variant
abstract class TidesFallback : AttributeCompatibilityRule<String> {
override fun execute(details: CompatibilityCheckDetails<String>) {
if (details.consumerValue == "tides-v2" && details.producerValue == "tides") {
details.compatible()
}
}
}
// when no flavor is requested at all, prefer "tides"
abstract class PreferTides : AttributeDisambiguationRule<String> {
override fun execute(details: MultipleCandidatesDetails<String>) {
if (details.consumerValue == null && "tides" in details.candidateValues) {
details.closestMatch("tides")
}
}
}
dependencies.attributesSchema {
attribute(flavor) {
compatibilityRules.add(TidesFallback::class)
disambiguationRules.add(PreferTides::class)
}
}
// where the dependency is declared ...
val dataDeps = configurations.dependencyScope("dataDeps")
// ... and a resolvable configuration that asks for sample data instead of a library
val dataPath = configurations.resolvable("dataPath") {
extendsFrom(dataDeps.get())
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>("tidepool-sample"))
attribute(Usage.USAGE_ATTRIBUTE, objects.named<Usage>("tidepool-data"))
val wanted = providers.gradleProperty("flavor").getOrElse("tides")
if (wanted != "any") {
attribute(flavor, wanted)
}
}
}
dependencies {
add("dataDeps", project(":tidepool-model"))
}
tasks.register("showData") {
val files = dataPath.map { it.incoming.files }
val root = rootProject.layout.projectDirectory.asFile
doLast {
files.get().forEach { println(it.relativeTo(root)) }
}
}
// CountRows (csv file -> number of data rows) lives in buildSrc: a transform must be a static class
dependencies {
registerTransform(CountRows::class) {
from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "csv")
to.attribute(ARTIFACT_TYPE_ATTRIBUTE, "rows")
}
}
tasks.register("showRows") {
val rows = dataPath.get().incoming.artifactView {
attributes { attribute(ARTIFACT_TYPE_ATTRIBUTE, "rows") }
}.files
inputs.files(rows)
doLast {
rows.forEach { println("${it.name}: ${it.readText().trim()}") }
}
}
import org.gradle.api.artifacts.transform.TransformParameters
import static org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
def flavor = Attribute.of('io.tidepool.flavor', String)
// "tides-v2" may also be served by a "tides" variant
abstract class TidesFallback implements AttributeCompatibilityRule<String> {
void execute(CompatibilityCheckDetails<String> details) {
if (details.consumerValue == 'tides-v2' && details.producerValue == 'tides') {
details.compatible()
}
}
}
// when no flavor is requested at all, prefer "tides"
abstract class PreferTides implements AttributeDisambiguationRule<String> {
void execute(MultipleCandidatesDetails<String> details) {
if (details.consumerValue == null && 'tides' in details.candidateValues) {
details.closestMatch('tides')
}
}
}
dependencies.attributesSchema {
attribute(flavor) {
compatibilityRules.add(TidesFallback)
disambiguationRules.add(PreferTides)
}
}
// where the dependency is declared ...
def dataDeps = configurations.dependencyScope('dataDeps')
// ... and a resolvable configuration that asks for sample data instead of a library
def dataPath = configurations.resolvable('dataPath') {
extendsFrom(dataDeps.get())
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, 'tidepool-sample'))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, 'tidepool-data'))
def wanted = providers.gradleProperty('flavor').getOrElse('tides')
if (wanted != 'any') {
attribute(flavor, wanted)
}
}
}
dependencies {
add('dataDeps', project(':tidepool-model'))
}
tasks.register('showData') {
def files = dataPath.map { it.incoming.files }
def root = rootProject.layout.projectDirectory.asFile
doLast {
files.get().each { println root.toPath().relativize(it.toPath()) }
}
}
// CountRows (csv file -> number of data rows) is defined in the script this time
abstract class CountRows implements TransformAction<TransformParameters.None> {
@PathSensitive(PathSensitivity.NAME_ONLY)
@InputArtifact
abstract Provider<FileSystemLocation> getInput()
void transform(TransformOutputs outputs) {
def csv = input.get().asFile
def rows = csv.readLines().size() - 1
outputs.file(csv.name.replace('.csv', '.rows')).text = "$rows\n"
println "transforming ${csv.name}"
}
}
dependencies {
registerTransform(CountRows) {
from.attribute(ARTIFACT_TYPE_ATTRIBUTE, 'csv')
to.attribute(ARTIFACT_TYPE_ATTRIBUTE, 'rows')
}
}
tasks.register('showRows') {
def rows = dataPath.get().incoming.artifactView {
attributes { attribute(ARTIFACT_TYPE_ATTRIBUTE, 'rows') }
}.files
inputs.files(rows)
doLast {
rows.each { println "${it.name}: ${it.text.trim()}" }
}
}
The first error most people hit is declaring the dependency on the resolvable configuration:
* Where:
Build file '/work/tidepool/tidepool-cli/build.gradle.kts' line: 27
* What went wrong:
Dependencies can not be declared against the `dataPath` configuration.
Gradle 8.4 split the roles for that reason: a dependencyScope configuration collects declarations, a resolvable one is
the root of a graph, a consumable one models outgoing variants. A configuration created with the plain create can be
all three, which is how the old build files worked and why a mistake there was not caught.
Now the four requests, with the data task that prints the files Gradle selected (the first run in each listing prints only the paths):
$ ./gradlew -q --configuration-cache :tidepool-cli:showData -Pflavor=tides
data/stations.csv
$ ./gradlew -q --configuration-cache :tidepool-cli:showData -Pflavor=harmonics
data/harmonics.csv
$ ./gradlew -q --configuration-cache :tidepool-cli:showData -Pflavor=tides-v2
data/stations.csv
$ ./gradlew -q --configuration-cache :tidepool-cli:showData -Pflavor=any
data/stations.csv
tides-v2 was not published by anybody; it is served by the tides variant because of the compatibility rule shown
below. any asks for no flavor at all and still gets the tides file because of the disambiguation rule.
A variant that does not say anything is compatible
This was run before the build files above had the org.gradle.category attribute (the producer and consumer then
requested only org.gradle.usage and io.tidepool.flavor). Asking for a flavor nobody has should fail:
$ ./gradlew -q --configuration-cache :tidepool-cli:showData -Pflavor=ephemeris
tidepool-model/src/main/resources
tidepool-model/src/main/java
It did not fail. The only variants that declare a different flavor were eliminated in step 1, but variants that declare no
flavor and no usage at all, such as the verification variants with the src/main directories, are not incompatible,
only "missing" the attributes, so one of them was selected and the task printed source directories. This is the most
common surprise with custom attributes: a variant that omits an attribute accepts any value of it. The cure is to make
the request specific enough that the unrelated variants are incompatible on some other attribute. A dedicated category,
here tidepool-sample, is the usual choice, and it is what the build files above do. With it the same request fails as
it should:
$ ./gradlew -q :tidepool-cli:showData -Pflavor=ephemeris
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':tidepool-cli:showData' (registered in build file 'tidepool-cli/build.gradle.kts').
> Could not resolve all files for configuration ':tidepool-cli:dataPath'.
> Could not resolve project ':tidepool-model'.
Required by:
project ':tidepool-cli'
> No matching variant of project ':tidepool-model' was found. The consumer was configured to find a component of category 'tidepool-sample' for use during 'tidepool-data', as well as attribute 'io.tidepool.flavor' with value 'ephemeris' but:
- Variant 'apiElements':
- Incompatible because this component declares a library for use during compile-time and the consumer needed a component of category 'tidepool-sample' for use during 'tidepool-data'
- Other compatible attribute:
- Doesn't say anything about io.tidepool.flavor (required 'ephemeris')
...
- Variant 'harmonicsData' declares a component of category 'tidepool-sample' for use during 'tidepool-data':
- Incompatible because this component declares a component, as well as attribute 'io.tidepool.flavor' with value 'harmonics' and the consumer needed a component, as well as attribute 'io.tidepool.flavor' with value 'ephemeris'
- Variant 'mainSourceElements':
- Incompatible because this component declares a component of category 'verification' and the consumer needed a component of category 'tidepool-sample'
- Other compatible attributes:
- Doesn't say anything about io.tidepool.flavor (required 'ephemeris')
...
- Variant 'tideData' declares a component of category 'tidepool-sample' for use during 'tidepool-data':
- Incompatible because this component declares a component, as well as attribute 'io.tidepool.flavor' with value 'tides' and the consumer needed a component, as well as attribute 'io.tidepool.flavor' with value 'ephemeris'
Each candidate is listed with the reason it was rejected: the library variants because their category is library, the
two data variants because their flavor differs, the verification variants because their category is verification.
When a no-match error is not obvious, read the candidate that is closest to what you wanted, which is the data variant.
The ambiguity error is the opposite: nothing was rejected, so Gradle cannot choose. It was produced before the disambiguation rule below was added to the build (the consumer did not ask for a flavor):
$ ./gradlew -q :tidepool-cli:showData -Pflavor=any
* What went wrong:
Execution failed for task ':tidepool-cli:showData' (registered in build file 'tidepool-cli/build.gradle.kts').
> Could not resolve all files for configuration ':tidepool-cli:dataPath'.
> Could not resolve project ':tidepool-model'.
Required by:
project ':tidepool-cli'
> The consumer was configured to find a component of category 'tidepool-sample' for use during 'tidepool-data'. There are several available matching variants of project ':tidepool-model'
The only attribute distinguishing these variants is 'io.tidepool.flavor'. Add this attribute to the consumer's configuration to resolve the ambiguity:
- Value: 'harmonics' selects variant: 'harmonicsData'
- Value: 'tides' selects variant: 'tideData'
Its advice is exact: add the attribute to the consumer. When the consumer cannot know, a disambiguation rule picks a default.
Note that the help links at the end of both messages (variant_model.html#sub:variant-no-match,
variant_model.html#sub:variant-ambiguity and variant_attributes.html#sec:abm_algorithm) point to anchors that do not
exist in the 9.8.1 manual; the algorithm is documented at variant_aware_resolution.html#sec:abm-algorithm.
Compatibility and disambiguation rules
An attribute can carry two kinds of rule, registered in the attributes schema of the consumer build (or of the whole build, in the settings file):
-
a compatibility rule says that a producer value is acceptable for a different consumer value (
tidesfor a request oftides-v2). It is called in step 1 and may callcompatible()or leave the pair incompatible. -
a disambiguation rule chooses among several remaining candidates (step 3). It may call
closestMatch(value).
Both are abstract classes that Gradle instantiates itself. Classes declared in a Kotlin build script are accepted for
attribute rules, as the TidesFallback and PreferTides classes and the attributesSchema block of the tidepool-cli
build file above show; artifact transforms are stricter, see Artifact transforms.
With the rules, -Pflavor=tides-v2 and -Pflavor=any print data/stations.csv, as in the listing above, and the rules
show up in the resolvable configurations report (io.tidepool.flavor is listed under both). The ecosystem plugins ship the
same kind of rules: it is why a consumer on Java 26 accepts a library built for Java 17 (jvm.version) and why a request
for libraryelements = classes accepts a jar variant, as the next output shows for the test fixtures.
Artifact transforms
Sometimes the consumer wants the same variant in a different shape: the classes directory instead of a jar, the
instrumented jar, a checksum, a count. The producer cannot publish every shape, and the consumer should not have to run
a task for each. An artifact transform converts an artifact of one artifactType (or other attribute) to another
during resolution, and Gradle chains transforms when no single one matches. A transform is like a task: it has inputs
and outputs, it is cached and up to date like one, and it runs lazily, only for the artifacts that the consumer’s view
requests.
The example turns the csv artifact of a data variant into a one-line file with its number of data rows. A transform is
an abstract class implementing TransformAction, with its input injected as a Provider:
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
import org.gradle.api.artifacts.transform.InputArtifact
import org.gradle.api.artifacts.transform.TransformAction
import org.gradle.api.artifacts.transform.TransformOutputs
import org.gradle.api.artifacts.transform.TransformParameters
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
/** Artifact transform: a csv file becomes a file holding its number of data rows. */
abstract class CountRows : TransformAction<TransformParameters.None> {
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:InputArtifact
abstract val input: Provider<FileSystemLocation>
override fun transform(outputs: TransformOutputs) {
val csv = input.get().asFile
val rows = csv.readLines().size - 1
outputs.file(csv.nameWithoutExtension + ".rows").writeText("$rows\n")
println("transforming ${csv.name}")
}
}
It is in buildSrc (or any included build) because of the first classic error. Written in the build script, the Kotlin
class is an inner class of the generated script class and Gradle refuses to register it:
$ ./gradlew -q :tidepool-cli:showRows
* What went wrong:
Could not register artifact transform Build_gradle.CountRows (from {artifactType=csv} to {artifactType=rows}).
> Class Build_gradle.CountRows is a non-static inner class.
In the Groovy DSL the same class worked when written in the build script; the only thing Groovy needed was the import
of TransformParameters, because the nested None is not in the default imports (unable to resolve class
TransformParameters.None otherwise). The best-practice location for shared build logic is an
included build-logic build instead of buildSrc, see
Convention Plugins & Sharing Build Logic.
The end of the tidepool-cli build file above registers the transform (registerTransform, from csv to rows on the
artifactType attribute) and reads the transformed files through an artifact view in the showRows task. In the
Groovy DSL the transform class is in the same file.
The task declares the view’s files as its inputs. That is the second classic error: if the files are only read in the
action (dataPath.map { ... }.get() inside doLast), nothing tells Gradle that the producer project has to be built,
and the transform of a project artifact fails (Could not access project ':tidepool-model'. No task declared this project
as part of an input, so it was not scheduled). Declaring inputs.files(rows) fixed it, and the example is compatible with the configuration cache.
$ ./gradlew -q --configuration-cache :tidepool-cli:showRows -Pflavor=tides
transforming stations.csv
stations.rows: 3
$ ./gradlew -q --configuration-cache :tidepool-cli:showRows -Pflavor=harmonics
transforming harmonics.csv
harmonics.rows: 3
On the first run in a fresh project the transform logs transforming stations.csv and then
transforming harmonics.csv for the second flavor; both lines are printed by the transform itself, and a rerun of the same
request reuses the cached result and prints only the line of the task. The -Pflavor selected the variant first, then the
transform converted that variant’s single file; the rows view never saw the other flavor.
A transform with parameters, an incremental transform and transform chains are described in the manual. Secondary
variants such as classes (see above) are the cheaper alternative when the producer can offer the other shape itself.
Feature variants
A feature variant is an optional part of a library with its own source set, its own dependencies and its own capability.
It is Gradle’s answer to Maven’s <optional>true</optional>, but typed: instead of a dependency that may or may not be
there, the feature has a name and a consumer asks for it. registerFeature("csv") in the model build above created:
-
the source set
csv(src/csv/java), which the main source set does not see; -
the configurations
csvApiandcsvImplementationfor the feature’s dependencies; -
the variants
csvApiElements,csvRuntimeElementsand, withwithSourcesJar(),csvSourcesElements, all with the capabilityio.tidepool:tidepool-model-csv:1.0, which is the component’sgroup:namewith-csvappended. The capability is how a consumer addresses the feature.
The consumer in tidepool-engine wrote
implementation(project(":tidepool-model")) {
capabilities {
requireFeature("csv")
}
}
and Gradle added the csv variant to the same component, with the transitive dependency on Commons CSV, as the
dependencyInsight output above shows. A consumer that does not ask never sees Commons CSV: in the published-module
consumer below, -Pcsv=false resolves tidepool-model and nothing else. A feature can also be registered on the
main source set (usingSourceSet(sourceSets.main)), in which case it has no classes of its own and only contributes
dependencies.
In a publication a feature variant maps to the formats as follows (the table is from the manual and the Maven and Ivy cells are confirmed by the published files shown later):
| Format | Variant artifacts | Feature dependencies | Capability |
|---|---|---|---|
Gradle Module Metadata |
uploaded |
in the feature’s variants, required by the consumer that asks for the feature |
published |
POM |
uploaded, with a distinct classifier |
|
not published (warning at publication) |
|
uploaded, with a distinct classifier |
extra configurations ( |
not published (warning at publication) |
Test fixtures
java-test-fixtures (Gradle 5.6) is feature variants specialised for sharing test helpers. It creates a testFixtures
source set (src/testFixtures/java) with testFixturesApi and testFixturesImplementation, makes the fixtures see the
main classes (the plugin adds that dependency itself; adding testFixturesApi(project) by hand is redundant and, in 9.8.1,
hits the deprecation of passing a Project object as a dependency notation: Using a Project object as a dependency
notation has been deprecated), makes the tests see the fixtures, and exposes two variants,
testFixturesApiElements and testFixturesRuntimeElements, with the capability
io.tidepool:tidepool-model-test-fixtures:1.0 (see the outgoing variants above). Another project consumes them with
testImplementation(testFixtures(project(":tidepool-model"))), as tidepool-engine does, and an external build with
testFixtures("io.tidepool:tidepool-model:1.0").
The consumer of the published module below declares that, and dependencyInsight shows the two variants of the one
component on the test compile classpath, and the libraryelements compatibility rule at work (the consumer requested
classes, the variants provide jar):
$ ./gradlew -q dependencyInsight --configuration testCompileClasspath --dependency tidepool-model -Pcsv=false
io.tidepool:tidepool-model:1.0
Variant apiElements:
| Attribute Name | Provided | Requested |
|--------------------------------|----------|--------------|
| org.gradle.status | release | |
| org.gradle.category | library | library |
| org.gradle.dependency.bundling | external | external |
| org.gradle.jvm.version | 26 | 26 |
| org.gradle.libraryelements | jar | classes |
| org.gradle.usage | java-api | java-api |
| org.gradle.jvm.environment | | standard-jvm |
Variant testFixturesApiElements:
| Attribute Name | Provided | Requested |
|--------------------------------|----------|--------------|
| org.gradle.status | release | |
| org.gradle.category | library | library |
| org.gradle.dependency.bundling | external | external |
| org.gradle.jvm.version | 26 | 26 |
| org.gradle.libraryelements | jar | classes |
| org.gradle.usage | java-api | java-api |
| org.gradle.jvm.environment | | standard-jvm |
io.tidepool:tidepool-model:1.0
\--- testCompileClasspath
io.tidepool:tidepool-model:1.0
\--- testCompileClasspath
io.tidepool:tidepool-model:1.0
\--- io.tidepool:tidepool-model:1.0 (*)
(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation.
A web-based, searchable dependency report is available by adding the --scan option.
The fixtures are published as an additional jar with the test-fixtures classifier (visible in the repository listing
below).
Gradle Module Metadata
Variants exist at the project level. For a published component they must be written down somewhere, and neither
pom.xml nor ivy.xml has room for attributes, capabilities or variant-specific dependencies. Gradle Module Metadata
(GMM) is that format: a JSON file named <artifactId>-<version>.module, published next to the POM or Ivy descriptor,
that lists every variant with its attributes, dependencies, constraints, files (with size and checksums) and
capabilities. Gradle 5.3 published and consumed version 1.0; since Gradle 6.0 maven-publish and ivy-publish generate
it by default.
Publishing the model to a repository under the build directory:
$ ./gradlew -q :tidepool-model:publishMavenPublicationToLocalRepository
$ find build/repo -type f ! -name "*.md5" ! -name "*.sha*" | sort
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0-csv-sources.jar
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0-csv.jar
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0-harmonics.csv
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0-test-fixtures.jar
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0-tides.csv
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0.jar
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0.module
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0.pom
build/repo/io/tidepool/tidepool-model/maven-metadata.xml
The publication was created with from(components["java"]); the custom variants joined it because the build added them to
the component with addVariantsFromConfiguration. Without that, a custom consumable configuration is not part of the
publication. The two data files had to get distinct classifiers (tides, harmonics) for
a second reason, which is a real error:
$ ./gradlew -q :tidepool-model:publishMavenPublicationToLocalRepository
* What went wrong:
Execution failed for task ':tidepool-model:publishMavenPublicationToLocalRepository' (registered by plugin 'org.gradle.maven-publish').
> Failed to publish publication 'maven' to repository 'local'
> Invalid publication 'maven': multiple artifacts with the identical extension and classifier ('csv', 'null').
Two artifacts of one publication with the same extension and classifier would be the same file in a Maven layout.
The module file is long (273 lines for this component); these are the header, the feature variant that carries the
dependency and the capability, and one of the data variants. Size and checksum lines are replaced by ...:
{
"formatVersion": "1.1",
"component": {
"group": "io.tidepool",
"module": "tidepool-model",
"version": "1.0",
"attributes": {
"org.gradle.status": "release"
}
},
"createdBy": {
"gradle": {
"version": "9.8.1"
}
},
"variants": [
...
{
"name": "csvRuntimeElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.jvm.version": 26,
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-runtime"
},
"dependencies": [
{
"group": "org.apache.commons",
"module": "commons-csv",
"version": {
"requires": "1.14.1"
}
}
],
"files": [
{
"name": "tidepool-model-1.0-csv.jar",
"url": "tidepool-model-1.0-csv.jar",
"size": 1312,
...
}
],
"capabilities": [
{
"group": "io.tidepool",
"name": "tidepool-model-csv",
"version": "1.0"
}
]
},
...
{
"name": "tideData",
"attributes": {
"io.tidepool.flavor": "tides",
"org.gradle.category": "tidepool-sample",
"org.gradle.usage": "tidepool-data"
},
"files": [
{
"name": "tidepool-model-1.0-tides.csv",
"url": "tidepool-model-1.0-tides.csv",
"size": 239,
...
}
]
},
...
]
}
Its variant names are apiElements, runtimeElements, testFixturesApiElements, testFixturesRuntimeElements, csvSourcesElements, csvApiElements, csvRuntimeElements, tideData, harmonicsData.
Source: Gradle User Manual: Understanding Gradle Module Metadata, Gradle Module Metadata specification
Mapping to the POM and to ivy.xml
The POM is written for Maven and for old Gradle; it carries what Maven can express, and a marker comment:
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
</dependencies>
</project>
The commons-csv dependency appears with <optional>true</optional>, the feature’s mapping. Everything else (variants,
the csv capability, the test fixtures, the data variants) is only in the .module file. The marker is the line
do_not_remove: published-with-gradle-metadata; Gradle reads the POM first, sees it, and only then fetches the module
file. The manual describes this as a saving of one request in the common case where there is no module file.
The same component published with ivy-publish maps variants to Ivy configurations (the sequence is the stock
publishIvyPublicationToLocalIvyRepository; the publication timestamp is the time of the run):
<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0" xmlns:m="http://ant.apache.org/ivy/maven">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<info organisation="io.tidepool" module="tidepool-model" revision="1.0" status="integration" publication="20261016205549"/>
<configurations>
<conf name="compile" visibility="public"/>
<conf name="csvApiElements" visibility="public"/>
<conf name="csvRuntimeElements" visibility="public"/>
<conf name="csvSourcesElements" visibility="public"/>
<conf name="default" visibility="public" extends="runtime,tideData,harmonicsData"/>
<conf name="harmonicsData" visibility="public"/>
<conf name="runtime" visibility="public"/>
<conf name="testFixturesApiElements" visibility="public"/>
<conf name="testFixturesRuntimeElements" visibility="public"/>
<conf name="tideData" visibility="public"/>
</configurations>
<publications>
<artifact name="tidepool-model" type="jar" ext="jar" conf="compile,runtime"/>
<artifact name="tidepool-model" type="jar" ext="jar" conf="testFixturesApiElements,testFixturesRuntimeElements" m:classifier="test-fixtures"/>
<artifact name="tidepool-model" type="jar" ext="jar" conf="csvSourcesElements" m:classifier="csv-sources"/>
<artifact name="tidepool-model" type="jar" ext="jar" conf="csvApiElements,csvRuntimeElements" m:classifier="csv"/>
<artifact name="tidepool-model" type="csv" ext="csv" conf="tideData" m:classifier="tides"/>
<artifact name="tidepool-model" type="csv" ext="csv" conf="harmonicsData" m:classifier="harmonics"/>
</publications>
<dependencies>
<dependency org="io.tidepool" name="tidepool-model" rev="1.0" conf="testFixturesApiElements->default"/>
<dependency org="io.tidepool" name="tidepool-model" rev="1.0" conf="testFixturesRuntimeElements->default"/>
<dependency org="org.apache.commons" name="commons-csv" rev="1.14.1" conf="csvApiElements->default"/>
<dependency org="org.apache.commons" name="commons-csv" rev="1.14.1" conf="csvRuntimeElements->default"/>
</dependencies>
</ivy-module>
When a build uses something that only GMM can represent, Gradle warns at publication time rather than silently dropping it. For the model’s first publication, before the warnings were silenced in the build file above, the output began
Maven publication 'maven' pom metadata warnings (silence with 'suppressPomMetadataWarningsFor(variant)'):
- Variant csvApiElements:
- Declares capability io.tidepool:tidepool-model-csv:1.0 which cannot be mapped to Maven
...
- Variant testFixturesRuntimeElements:
- Declares capability io.tidepool:tidepool-model-test-fixtures:1.0 which cannot be mapped to Maven
These issues indicate information that is lost in the published 'pom' metadata file, which may be an issue if the published library is consumed by an old Gradle version or Apache Maven.
The 'module' metadata file, which is used by Gradle 6+ is not affected.
and said the same for the Ivy publication. suppressPomMetadataWarningsFor("csvApiElements") (and the suppress... methods of IvyPublication
for Ivy) silences one variant; the build file above silences all five because the
information loss is intended.
The manual’s mapping table summarizes what is lost in each format. The constraint and custom-component rows were not run here; the feature row was, with the Ivy difference noted above:
| Gradle concept | Maven POM | ivy.xml |
|---|---|---|
dependency constraints |
|
not published |
rich version constraints |
the |
the |
component capabilities |
not published |
not published |
feature variants |
artifacts uploaded; dependencies published as optional |
artifacts uploaded; the manual says the dependencies are not published, but in this run they were, in the feature’s own configurations (see the descriptor below) |
custom component types and variants |
artifacts uploaded; dependencies as described by the mapping |
artifacts uploaded; dependencies ignored |
Consuming the published module
A separate consumer build reads the repository. Its settings file adds the repository and, optionally, makes it behave like Maven; its build file asks for the feature and for the test fixtures:
rootProject.name = "consumer"
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven {
name = "tidepool"
url = uri("../kts/build/repo")
// -PpomOnly: behave like Maven, ignore the .module file
if (providers.gradleProperty("pomOnly").isPresent) {
metadataSources {
mavenPom()
ignoreGradleMetadataRedirection()
}
}
}
mavenCentral()
}
}
rootProject.name = 'consumer'
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven {
name = 'tidepool'
url = '../groovy/build/repo'
// -PpomOnly: behave like Maven, ignore the .module file
if (providers.gradleProperty('pomOnly').present) {
metadataSources {
mavenPom()
ignoreGradleMetadataRedirection()
}
}
}
mavenCentral()
}
}
plugins {
java
}
val withCsv = providers.gradleProperty("csv").map { it.toBoolean() }.getOrElse(true)
dependencies {
implementation("io.tidepool:tidepool-model:1.0") {
if (withCsv) {
capabilities {
requireFeature("csv")
}
}
}
testImplementation(testFixtures("io.tidepool:tidepool-model:1.0"))
}
plugins {
id 'java'
}
def withCsv = providers.gradleProperty('csv').map { it.toBoolean() }.getOrElse(true)
dependencies {
implementation('io.tidepool:tidepool-model:1.0') {
if (withCsv) {
capabilities {
requireFeature('csv')
}
}
}
testImplementation testFixtures('io.tidepool:tidepool-model:1.0')
}
Resolved from the module file (the default), with and without the feature:
$ ./gradlew -q dependencies --configuration runtimeClasspath -Pcsv=true
runtimeClasspath - Runtime classpath of source set 'main'.
\--- io.tidepool:tidepool-model:1.0
\--- org.apache.commons:commons-csv:1.14.1
+--- commons-io:commons-io:2.20.0
\--- commons-codec:commons-codec:1.19.0
$ ./gradlew -q dependencies --configuration runtimeClasspath -Pcsv=false
runtimeClasspath - Runtime classpath of source set 'main'.
\--- io.tidepool:tidepool-model:1.0
Resolved from the POM only (-PpomOnly, which sets ignoreGradleMetadataRedirection()), the consumer sees what a Maven
consumer sees: the optional dependency is not followed, and the feature cannot be requested at all, because the POM has
no capabilities. The variants in the message are the ones Gradle derives from a POM (compile and runtime, javadoc and
sources, and four platform variants):
$ ./gradlew -q dependencies --configuration runtimeClasspath -PpomOnly -Pcsv=false
runtimeClasspath - Runtime classpath of source set 'main'.
\--- io.tidepool:tidepool-model:1.0
$ ./gradlew -q dependencyInsight --configuration runtimeClasspath --dependency tidepool-model -PpomOnly
io.tidepool:tidepool-model:1.0 FAILED
Failures:
- Could not resolve io.tidepool:tidepool-model:1.0.
- Unable to find a variant of 'io.tidepool:tidepool-model:1.0' with the requested capability: feature 'csv':
- Variant 'compile' provides 'io.tidepool:tidepool-model:1.0'
- Variant 'enforced-platform-compile' provides 'io.tidepool:tidepool-model-derived-enforced-platform:1.0'
- Variant 'enforced-platform-runtime' provides 'io.tidepool:tidepool-model-derived-enforced-platform:1.0'
- Variant 'javadoc' provides 'io.tidepool:tidepool-model:1.0'
- Variant 'platform-compile' provides 'io.tidepool:tidepool-model-derived-platform:1.0'
- Variant 'platform-runtime' provides 'io.tidepool:tidepool-model-derived-platform:1.0'
- Variant 'runtime' provides 'io.tidepool:tidepool-model:1.0'
- Variant 'sources' provides 'io.tidepool:tidepool-model:1.0'
io.tidepool:tidepool-model:1.0 FAILED
\--- runtimeClasspath
metadataSources { gradleMetadata() } on a repository goes the other way and tells Gradle to look only for the module
file, which saves a request per module for repositories known to be published by Gradle. mavenPom(), ivyDescriptor() and
artifact() select the other sources.
Disabling GMM publication
Disabling is a build-file decision, made by disabling the task that generates the file. It is for repositories that reject the unknown file, or for builds that use Maven- or Ivy-specific features GMM cannot represent:
if (providers.gradleProperty("noGmm").isPresent) {
tasks.withType<GenerateModuleMetadata>().configureEach {
enabled = false
}
}
if (providers.gradleProperty('noGmm').present) {
tasks.withType(GenerateModuleMetadata).configureEach {
enabled = false
}
}
$ ./gradlew --console=plain --configuration-cache :tidepool-model:publishMavenPublicationToLocalRepository -PnoGmm
> Task :tidepool-model:generateMetadataFileForMavenPublication SKIPPED
> Task :tidepool-model:publishMavenPublicationToLocalRepository
BUILD SUCCESSFUL in 558ms
$ find build/repo -type f ! -name "*.md5" ! -name "*.sha*" | sort
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0-csv-sources.jar
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0-csv.jar
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0-harmonics.csv
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0-test-fixtures.jar
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0-tides.csv
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0.jar
build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0.pom
build/repo/io/tidepool/tidepool-model/maven-metadata.xml
$ grep -c do_not_remove build/repo/io/tidepool/tidepool-model/1.0/tidepool-model-1.0.pom
0
No .module file, and the POM lost its marker comment (grep -c do_not_remove prints 0), so Gradle consumers see
a plain Maven module again: no feature variants, no test-fixtures variants (their jars are there, as classified
artifacts), no data variants. The downstream cost is real; disable GMM only when you must.
What changed from Gradle 8.14 and from the Gradle 1.x book
-
Gradle 8.14: variant-aware resolution, GMM publication by default, feature variants and test fixtures are the same (not run here). The explicit configuration roles API (
consumable,resolvable,dependencyScope) exists since 8.4. Gradle 9 deprecates passing aProjectas a dependency notation (9.6 in this run) and prints the deprecation with a stack trace location under--warning-mode all. -
Gradle in Action covers Gradle 1.7. There a configuration was the only unit a dependency had:
compile,runtime,testCompileand custom ones, selected by name (project(path: ':a', configuration: 'x')). Since Gradle 7.0 thecompile,runtimeandtestCompileconfigurations are gone;implementation,api,compileOnlyandruntimeOnlyare declaration buckets and the consumer selects an outgoing variant by attributes. The book also predates Gradle Module Metadata (5.3), feature variants, test fixtures (5.6) and artifact transforms; what Maven calls an optional dependency is a feature variant now, and the test fixtures plugin is the supported way to share test helpers. -
Gradle 10 is announced but not released; nothing on this page depends on it.
The same idea in the other two tools
| Concept | Gradle | Maven | Ant with Ivy |
|---|---|---|---|
Component metadata |
|
|
|
Compile versus runtime |
|
dependency |
configurations such as |
Optional parts |
feature variants with a capability |
|
an extra configuration |
Same artifact, different shape |
artifact transforms |
classifier or type on the dependency ( |
artifact |
Test helpers shared across projects |
test fixtures plugin |
|
a configuration for the test jar |
See Maven Dependencies, Scopes & Mediation, Maven Dependency Management & BOMs, Ivy Module Descriptors & Configurations, and, for the whole-tool comparison, Comparing Ant, Maven & Gradle. Fundamentals of dependency management are in Dependency Management Fundamentals.
Classic mistakes
-
Declaring dependencies on a
resolvableorconsumableconfiguration (Asking for sample data): use adependencyScopebucket andextendsFrom. -
Expecting a custom attribute to exclude variants that do not set it: it does not (A variant that does not say anything is compatible); add a distinguishing category or usage.
-
Registering an artifact transform from a Kotlin build script (a non-static inner class), or reading the transformed files in
doLastwithout declaring them as task inputs (Artifact transforms). -
Creating a custom consumable configuration and expecting it in the publication: add it to the component with
addVariantsFromConfiguration, and give its artifacts distinct classifiers. -
Using
defhelpers in a Groovy build script that read script-level variables: use closures. -
Publishing a library to a repository that rejects
.modulefiles without disabling GMM, or disabling GMM and then wondering why the feature variants are gone. -
Reading a POM-only view of a Gradle-published module in another tool and concluding that the library "has no optional features": the information is in the module file.