How do you substitute a dependency while preserving or targeting a specific variant, classifier, or capability?
answer
- variant(selector){ attributes/capabilities }
- withClassifier / withoutClassifier
- withoutArtifactSelectors
- carry capability on both sides
- fixes test-fixtures / platform failures
basics
~10 sUse the variant-aware substitution DSL: substitute(module(...)).using(project(...)).withClassifier(...) or attach capabilities/attributes via variant { ... }, so the substitute resolves to the correct artifact instead of failing on a missing variant.
solid answer
~40 sPlain `substitute(module("g:n")).using(project(":p"))` works when the target exposes the variant the consumer needs. When it doesn't — say the consumer wanted a `test-fixtures` variant, a classifier, or a specific platform attribute — you must make the substitution variant-aware. Gradle's substitution DSL lets you wrap selectors with `variant(...)` to add attributes/capabilities, and offers `withClassifier(...)` / `withoutClassifier()` / `withoutArtifactSelectors()` to manage classifier-based selection. For example, `substitute(variant(module("g:n")) { attributes { ... }; capabilities { requireCapability("g:n-test-fixtures") } }).using(project(":p"))`. This matters because substitution changes the *node* but the consumer still demands specific variant metadata; if the substitute can't satisfy it, resolution fails with a variant-selection error. Getting this right is the difference between a substitution that silently works and one that breaks on `test-fixtures`, platform, or classifier-specific artifacts.
code
kotlin · 11 linesresolutionStrategy.dependencySubstitution {
substitute(
variant(module("com.acme:lib")) {
capabilities { requireCapability("com.acme:lib-test-fixtures") }
}
).using(
variant(project(":lib")) {
capabilities { requireCapability("com.acme:lib-test-fixtures") }
}
).because("local test-fixtures during dev")
}go deeper
Not expected to know variant-aware substitution beyond awareness it exists.
Recognize that substitution can fail on variant selection and that capabilities/classifiers may need handling.
Use the variant(...) DSL with attributes/capabilities, and withClassifier/withoutArtifactSelectors to make substitutions resolve correctly.
Define org conventions for substituting feature variants/test-fixtures across composite builds and ensure capability metadata is consistent so substitutions don't break.
## Why variant-awareness matters in substitution Modern Gradle resolution is **variant-aware**: a single module publishes multiple variants (api, runtime, test-fixtures, javadoc, sources, platform), each with attributes and possibly capabilities. When you substitute one component for another, the consumer's *demand* — the attributes and capabilities it was matching against — doesn't disappear. The substitute must be able to satisfy that demand, or resolution fails with a "no matching variant" error. ## The variant-aware substitution DSL Gradle exposes builders so the substitution itself can be qualified: ```kotlin configurations.all { resolutionStrategy.dependencySubstitution { // Substitute only the test-fixtures variant of g:n with a local project's fixtures substitute( variant(module("com.acme:lib")) { capabilities { requireCapability("com.acme:lib-test-fixtures") } } ).using( variant(project(":lib")) { capabilities { requireCapability("com.acme:lib-test-fixtures") } } ) } } ``` The key pieces: - **`variant(selector) { ... }`** — wraps a `module(...)` or `project(...)` selector and lets you specify `attributes { ... }` and `capabilities { requireCapability(...) }` so the rule targets (and produces) a specific variant. - **`withClassifier("x")` / `withoutClassifier()`** — for classifier-based artifact selection (older Maven-style classifiers like `linux-x86_64`). - **`withoutArtifactSelectors()`** — strips artifact-level selectors so the substitute resolves via variant metadata rather than a pinned artifact. ## Common failure mode A naive `substitute(module("com.acme:lib")).using(project(":lib"))` breaks when something in the graph requested `lib`'s **test fixtures** capability and `:lib` either doesn't expose it or you didn't carry the capability through. The fix is to make both sides variant-aware so the capability/attribute demand is matched on the substitute. ## Practical guidance - Start simple; add `variant {}` only when a substitution fails on variant selection. - Mirror the capability on both the `substitute(...)` selector and the `using(...)` target when handling test-fixtures or feature-variant capabilities. - Use `because(...)` so the dependency report explains the qualified substitution. - Remember substitution runs before conflict resolution, so the variant the substitute exposes is what downstream selection sees.
- Why might a simple module→project substitution fail at resolution time?Because the consumer demanded a specific variant/capability (e.g. test-fixtures or a platform attribute) that the substitute project doesn't expose, producing a no-matching-variant error.
- What does `withClassifier(...)` do in a substitution?It targets/produces a classifier-qualified artifact (Maven-style classifier) so the substitution selects the right classified artifact instead of the default.
- When would you use `withoutArtifactSelectors()`?To strip pinned artifact selectors from the requested dependency so the substitute resolves via proper variant metadata rather than a hard-coded artifact reference.
saying these in an interview costs you the question
- Assuming a plain substitution always works regardless of variants/capabilities.
- Forgetting to carry the required capability on the using(...) side as well as the substitute(...) side.
- Confusing classifiers (artifact-level) with capabilities (component-level).