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?
answer
- variants + capabilities, not just main jar
- substitute(variant(module){requireCapability})
- test fixtures = group:name-test-fixtures capability
- platform = java-platform variant
- no matching variant -> resolution fails
basics
~10 sUse 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 sPlain `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 linesincludeBuild("../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
Not expected; awareness that test fixtures/platforms are 'special' is plenty.
Know that test fixtures and platforms are separate variants and plain substitution targets the main one.
Use the variant-aware substitution form and verify the selected variant, ensuring the local project publishes it.
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.