Checkpoint Lab — Gradle Configurations, Dependency Resolution, Variants, Attributes, Capabilities, and Metadata Rules
Construct a producer with two meaningful runtime variants and a consumer that requests one by attribute, then force an incompatible request, repair it narrowly, and document what would or would not survive publication to Maven-only consumers.
Construct a producer with two meaningful runtime variants and a consumer that requests one by attribute, then force an incompatible request, repair it narrowly, and document what would or would not survive publication to Maven-only consumers.
Learning objectives
- Build a three-step producer/consumer checkpoint with two explicitly attributed producer variants.
- Predict selected artifacts before resolution and verify consumer and producer attributes independently.
- Capture clean, standard, audit, and incompatible-selection evidence without relying on warm user cache state.
- Repair the incompatible request by changing the smallest legitimate selection input.
- Document capability/GMM/POM interoperability assumptions for publishing the same component contract.
- Clean up only disposable checkpoint state and bridge into version-governance controls in Chapter 20.
1. Checkpoint scenario and required prediction
You are building an internal runtime payload that can be consumed in standard or audit form. Both are legitimate artifacts from the same producer component. The consumer must choose one through an attribute, not through hard-coded filenames. You will then request an unsupported third value, interpret the variant incompatibility, repair the request, and document what a Maven-only consumer could understand.
Before running anything, write these predictions:
-
With no property,
standardis requested and onlystandardElementsshould match all requested attributes. -
With
-PacademyFlavor=audit, the coordinate/project edge stays identical but the selected artifact changes. -
With
-PacademyFlavor=debug, no attributed producer variant should match, so resolution must fail rather than silently choose one.
2. Preflight and isolation
Use the verified Gradle 9.7.1 Wrapper. The project does not require network dependencies or plugins.
mkdir gradle-variant-checkpoint
cd gradle-variant-checkpoint
# Copy reviewed Gradle 9.7.1 Wrapper files here first.
export GRADLE_USER_HOME="$PWD/.gradle-user-home"
./gradlew --version
java -version
mkdir -p producer/src/standard producer/src/audit consumer evidence
printf 'standard-policy\n' > producer/src/standard/policy.txt
printf 'audit-policy\n' > producer/src/audit/policy.txt
Record the Wrapper distribution URL/checksum from
gradle/wrapper/gradle-wrapper.properties in
evidence/runtime.txt along with Gradle/JVM identity.
3. Define the two-project build
// settings.gradle.kts
rootProject.name = "variant-checkpoint"
include("producer", "consumer")
4. Producer: two meaningful consumable variants
plugins { base }
group = "dev.academy"
version = "1.0.0"
val flavor = Attribute.of("dev.academy.flavor", String::class.java)
fun Configuration.checkpointVariant(value: String, artifact: TaskProvider<Jar>) {
isCanBeDeclared = false
isCanBeResolved = false
isCanBeConsumed = true
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class.java, Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category::class.java, Category.LIBRARY))
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, LibraryElements.JAR))
attribute(flavor, value)
}
outgoing.artifact(artifact)
}
val standardJar by tasks.registering(Jar::class) {
archiveBaseName.set("policy-bundle")
archiveClassifier.set("standard")
from("src/standard")
}
val auditJar by tasks.registering(Jar::class) {
archiveBaseName.set("policy-bundle")
archiveClassifier.set("audit")
from("src/audit")
}
configurations.create("standardElements") {
checkpointVariant("standard", standardJar)
}
configurations.create("auditElements") {
checkpointVariant("audit", auditJar)
}
Do not add a third catch-all attributed variant. The checkpoint depends on unsupported flavor values failing clearly.
5. Consumer: one resolvable request
plugins { base }
val flavor = Attribute.of("dev.academy.flavor", String::class.java)
val requestedFlavor = providers.gradleProperty("academyFlavor").orElse("standard")
val policyRuntime = configurations.create("policyRuntime") {
isCanBeDeclared = false
isCanBeResolved = true
isCanBeConsumed = false
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class.java, Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category::class.java, Category.LIBRARY))
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, LibraryElements.JAR))
attributeProvider(flavor, requestedFlavor)
}
}
dependencies {
add("policyRuntime", project(":producer"))
}
tasks.register("verifySelectedPolicy") {
inputs.files(policyRuntime)
doLast {
val files = policyRuntime.files.sortedBy { it.name }
check(files.size == 1) { "expected exactly one selected artifact, got $files" }
val selected = files.single()
println("SELECTED=${selected.name}")
println("SHA256=" + java.security.MessageDigest.getInstance("SHA-256")
.digest(selected.readBytes()).joinToString("") { "%02x".format(it) })
}
}
The attribute value remains provider-backed until the attribute container finalizes. The task declares the selected file set as an input and resolves only when the task executes.
6. Capture the protocol before resolution
./gradlew :producer:outgoingVariants > evidence/producer-variants.txt
./gradlew :consumer:resolvableConfigurations --configuration policyRuntime > evidence/consumer-request-standard.txt
Verify independently: producer report contains exactly the supported
standard and audit flavor attributes on
selectable variants; consumer report requests
standard by default.
7. Resolve the standard variant
./gradlew :consumer:verifySelectedPolicy | tee evidence/standard-run.txt
./gradlew :consumer:dependencyInsight --configuration policyRuntime --dependency producer > evidence/standard-insight.txt
Expected artifact name:
policy-bundle-1.0.0-standard.jar. Preserve its SHA-256
as identity evidence.
8. Change only the consumer attribute and resolve audit
./gradlew :consumer:resolvableConfigurations --configuration policyRuntime -PacademyFlavor=audit > evidence/consumer-request-audit.txt
./gradlew :consumer:verifySelectedPolicy -PacademyFlavor=audit | tee evidence/audit-run.txt
./gradlew :consumer:dependencyInsight --configuration policyRuntime --dependency producer -PacademyFlavor=audit > evidence/audit-insight.txt
Expected artifact: policy-bundle-1.0.0-audit.jar.
Coordinates and project dependency remain unchanged; the request
attribute and selected variant/artifact differ.
9. Inject the incompatibility and preserve the failure
set +e
./gradlew :consumer:verifySelectedPolicy -PacademyFlavor=debug > evidence/debug-failure.txt 2>&1
status=$?
set -e
printf 'debug failure status=%s\n' "$status" | tee evidence/debug-status.txt
test "$status" -ne 0
Interpretation: the consumer requested
dev.academy.flavor=debug; the producer exposes only
standard/audit candidates. The correct outcome is failure. Do not
add a compatibility rule that declares arbitrary strings compatible
just to make the build green.
10. Apply the narrowest repair
The requirement says the supported diagnostic artifact is
audit, not a new debug artifact. Repair
the request:
./gradlew :consumer:verifySelectedPolicy -PacademyFlavor=audit | tee evidence/repaired-run.txt
If the product requirement truly introduced a third variant, the
producer would need a reviewed debugElements contract
and artifact. The consumer must not invent producer capabilities by
metadata guesswork.
11. Verify without depending on warm Gradle User Home state
Project dependencies require no remote artifact cache, but a second User Home still proves the model does not rely on hidden user scripts/caches:
export GRADLE_USER_HOME="$PWD/.gradle-user-home-fresh"
./gradlew :consumer:verifySelectedPolicy -PacademyFlavor=standard | tee evidence/fresh-standard.txt
The selected standard artifact and SHA-256 should match the first standard run for unchanged source and toolchain.
12. Publication and Maven interoperability dossier
This checkpoint deliberately tests project-to-project variant selection, where Gradle has the full producer model. Before publishing externally, record these facts:
| Question | Checkpoint answer |
|---|---|
| Can GMM represent the custom flavor attribute and multiple variants? | Yes, if the publication component is explicitly modeled to publish those variants. |
Can a Maven POM represent arbitrary
dev.academy.flavor matching?
|
No. |
| Would Maven automatically choose audit vs standard by this attribute? | No; Maven does not implement Gradle attribute matching. |
| What if Maven consumers are required? | Publish a conventional interoperable main artifact or explicit Maven-visible coordinates/classifiers and test that contract separately. |
| Does creating custom consumable configurations automatically guarantee they are published? | No. Publication/component modeling is a separate concern, covered later in the course. |
This is the verification the prompt requires: you are not claiming Maven interoperability that the current project-only lab has not implemented.
13. Verification checklist
- Gradle 9.7.1 Wrapper and JDK identity recorded.
- All Gradle User Home state is checkpoint-local.
- Producer exposes two attributed selectable variants.
- Consumer's default request selects standard.
- Audit request selects audit without changing coordinates.
- Unsupported debug request fails and original message is preserved.
- Repair changes only the requested supported flavor.
- Fresh User Home repeats the expected standard artifact identity.
- No credentials, production repositories, or shared caches are touched.
- Interoperability notes explicitly distinguish GMM from Maven POM capability.
14. Cleanup and rollback
./gradlew --stop || true
cd ..
rm -rf gradle-variant-checkpoint
Delete only disposable checkpoint state. In a real repository, rollback is source-control restoration of the attribute/variant contract plus preserved CI evidence—not cache deletion.
15. What Chapter 19 adds, and the bridge to Chapter 20
You can now describe dependency resolution as an explicit protocol: declaration buckets feed resolvable requests; producer variants expose attributes/capabilities; metadata transports that model; Gradle matches attributes and detects semantic conflicts; and reports prove the selected path.
Chapter 20 adds version catalogs, platforms, constraints, and dependency locking. Those mechanisms answer “which coordinates/versions are intended and resolved?” while Chapter 19 answers “which variant/capability of the selected component satisfies this consumer?” Keeping those questions separate is the foundation of explainable dependency governance.
Knowledge check
Why does the debug request fail instead of falling back to standard?
Because the consumer explicitly requests an attribute value for which no selectable producer variant is compatible; silent fallback would violate the declared contract.
What proves that audit selection did not change dependency identity?
The project dependency/coordinates are unchanged; only the consumer flavor attribute changes, and dependencyInsight/outgoingVariants show the resulting variant selection.
Why use a second Gradle User Home in a project-dependency lab?
It proves the build is not depending on hidden user init scripts/cache state and preserves the normal user environment.
Could a Maven consumer use
dev.academy.flavor directly?
No. Maven POM has no general Gradle attribute-matching model.
If “debug” becomes a real supported product variant, where should the fix go?
Into the producer's explicit variant/artifact contract, followed by consumer tests—not into a permissive compatibility rule that guesses equivalence.
What does Chapter 20 add that this checkpoint does not?
Centralized dependency aliases/version intent, platforms/constraints, and lock state. It does not replace attribute/capability variant selection.
Official references and version notes
- Gradle 9.7.1 Release Notes — pinned Gradle baseline; released August 19, 2026.
- Creating Dependency Configurations — declarable, resolvable, consumable roles and role flags. The role-specific factory methods are currently incubating.
- Dependency Resolution and Variant Selection and Attribute Matching.
- Variants and Attributes — standard/custom attributes, compatibility and disambiguation.
- Capabilities — conflicting providers and capability resolution.
- Modifying Dependency Metadata — narrowly scoped, cacheable component metadata rules.
- Gradle Module Metadata and Metadata Formats — GMM, Maven POM, Ivy, and interoperability limits.
-
Viewing Dependencies
—
dependenciesanddependencyInsight;--all-variantsis currently incubating. - Compatibility Matrix — Gradle currently requires JVM 17 through 26 to execute.
Version snapshot: Generated August 24, 2026 with Gradle 9.7.1, JDK 21 as the Gradle runtime, Java 17 as the course target, only core Gradle APIs, a verified project Wrapper, and project-local Gradle User Homes. Re-check incubating APIs before standardizing them.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.