skip to content

When substituting a published module to a local included build, how do you target a specific variant or capability (e.g. test fixtures or a platform), and what can go wrong?

level: principalimportance: nice to knowfreq 18%

answer

  1. variants + capabilities, not just main jar
  2. substitute(variant(module){requireCapability})
  3. test fixtures = group:name-test-fixtures capability
  4. platform = java-platform variant
  5. no matching variant -> resolution fails

basics

~10 s

Use the variant-aware form: substitute(module(...)) plus a capability/attribute selector, and using(project(...)) with withClassifier or requireCapability/requireFeature. Mismatched variants (e.g. test fixtures, platforms) otherwise resolve incorrectly or fail.

solid answer

~40 s

Plain `substitute(module("g:n")).using(project(":p"))` redirects the **main** variant. Modern dependencies expose extra variants via **capabilities** — test fixtures (`group:name-test-fixtures`), platforms (`java-platform`), or feature variants. To substitute those correctly you use the variant-aware substitution DSL: build the selector with `variant(module("g:n")) { requireCapability("g:n-test-fixtures") }` (or attribute-based selection) and target the local project's matching variant, optionally via `using(project(":p")) { withCapabilities { ... } }`. If you ignore variants and substitute only the main one, a consumer that asked for the test-fixtures or platform variant will either fail to resolve (no matching variant in the local project) or silently pull the wrong artifacts. This is the subtle, principal-level edge of composite substitution.

code

kotlin · 14 lines
kotlin
includeBuild("../widgets-lib") {
    dependencySubstitution {
        // substitute the test-fixtures variant to the local project's fixtures
        substitute(
            variant(module("com.acme:widgets")) {
                requireCapability("com.acme:widgets-test-fixtures")
            }
        ).using(project(":widgets")) {
            requireCapabilities("com.acme:widgets-test-fixtures")
        }
    }
}
// the consumer side, that triggers this path:
// dependencies { testImplementation(testFixtures("com.acme:widgets:1.0")) }

go deeper

for a junior

Not expected; awareness that test fixtures/platforms are 'special' is plenty.

for a middle

Know that test fixtures and platforms are separate variants and plain substitution targets the main one.

for a senior

Use the variant-aware substitution form and verify the selected variant, ensuring the local project publishes it.

for a principal

Reason about capabilities/variant selection across composites, anticipate capability conflicts, and set conventions so internal libraries publish the variants consumers depend on.

## Background: variants and capabilities A single module can publish multiple **variants** (api, runtime, test-fixtures, javadoc, a platform, feature variants). Gradle picks one using **attributes** and **capabilities**. A *capability* is a `group:name:version` triple that says "I provide this thing"; `testFixtures(...)` for example requests the `g:n-test-fixtures` capability. Substitution must respect this — you can't redirect a test-fixtures request to a project's main jar. ## Variant-aware substitution The substitution DSL supports selecting and targeting variants. Conceptually: ```kotlin includeBuild("../widgets-lib") { dependencySubstitution { // redirect the test-fixtures variant specifically substitute(variant(module("com.acme:widgets")) { requireCapability("com.acme:widgets-test-fixtures") }).using(project(":widgets")) { // ask Gradle to satisfy with the project's test-fixtures variant requireCapabilities("com.acme:widgets-test-fixtures") } } } ``` For classifier-style targeting there's `withClassifier`, and for platforms you substitute the `java-platform` variant by selecting on its attribute/category. The exact builder methods (`variant`, `requireCapability`, `withClassifier`, `withCapabilities`) live on the substitution and target objects. ## What goes wrong without it - **No matching variant:** consumer requests `testFixtures("com.acme:widgets")`; you substituted only the main variant; the local project either lacks a published test-fixtures variant or it isn't selected → resolution fails with a "no variant matching" / capability error. - **Wrong artifacts:** a platform (BOM-like `java-platform`) substituted to a regular project pulls a jar instead of dependency constraints, corrupting version alignment. - **Capability conflicts:** substituting in a local project that also provides the same capability as another node causes a capability conflict. ## Practical guidance 1. For ordinary single-jar libraries, plain `substitute(module).using(project)` is enough — don't overcomplicate. 2. Reach for variant-aware substitution only when the consumer uses `testFixtures(...)`, `platform(...)`, or feature variants of the substituted module. 3. Always verify with `dependencyInsight` that the **right variant** of the local project was selected, not just `-> project`. 4. Ensure the local included project actually publishes the variant (e.g. applies `java-test-fixtures`) — substitution can't synthesize a variant that doesn't exist. ## Why this is principal-level It sits at the intersection of composite builds, variant-aware dependency management, and capabilities — diagnosing it requires reading variant selection in resolution reports and understanding the local project's published variants.

  • Why can't a plain substitute(module).using(project) satisfy a testFixtures(...) request?
    testFixtures requests the `group:name-test-fixtures` capability; the plain form targets the main variant, which doesn't carry that capability, so the requested variant isn't matched.
  • What must the local included project do for a test-fixtures substitution to work?
    It must actually publish that variant — e.g. apply the `java-test-fixtures` plugin — because substitution can't fabricate a variant that doesn't exist.
  • How do you confirm the correct variant was chosen, not just the project?
    Inspect `dependencyInsight`/`outgoingVariants` to see which variant/capability of the local project was selected, beyond the `-> project` arrow.

saying these in an interview costs you the question

  • Assuming `-> project` in the report means the right variant was selected.
  • Substituting a platform (java-platform) to a normal project, getting a jar instead of constraints.
  • Expecting substitution to create a test-fixtures variant the local project never published.

context