Chapter 19Lesson 05~240 minutes

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.

CheckpointAttribute matchingIncompatibilityEvidenceRollback

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:

  1. With no property, standard is requested and only standardElements should match all requested attributes.
  2. With -PacademyFlavor=audit, the coordinate/project edge stays identical but the selected artifact changes.
  3. 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?

What proves that audit selection did not change dependency identity?

Why use a second Gradle User Home in a project-dependency lab?

Could a Maven consumer use dev.academy.flavor directly?

If “debug” becomes a real supported product variant, where should the fix go?

What does Chapter 20 add that this checkpoint does not?

Official references and version notes

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.