skip to content

What is AdhocComponentWithVariants and why would you use it when publishing?

level: seniorimportance: must knowfreq 45%

answer

  1. addVariantsFromConfiguration
  2. mapToMavenScope / mapToOptional / skip
  3. components.java IS adhoc
  4. SoftwareComponentFactory.adhoc
  5. maps consumable configs → published variants

basics

~10 s

AdhocComponentWithVariants is a software component you can assemble yourself by mapping outgoing (consumable) configurations to published variants with addVariantsFromConfiguration, controlling exactly which configurations end up in the publication.

solid answer

~40 s

`AdhocComponentWithVariants` is the API behind Gradle's built-in components and the tool you use to publish a **custom set of variants**. You obtain one via `softwareComponentFactory.adhoc("name")` (or, more commonly, reach the existing `components.java` which *is* an adhoc component) and then call `addVariantsFromConfiguration(consumableConfig) { ... }` to map a consumable configuration into a published variant. Inside the mapping action you can `mapToOptional()` (publish but don't make it a required dependency context), `mapToMavenScope("compile"/"runtime")`, or `skip()` to exclude a configuration. This is how you add things like a documentation or test-fixtures variant to the published module, or how you exclude an outgoing configuration you don't want leaking. The key value: precise, declarative control over the variant graph that ends up in GMM and the corresponding POM scopes.

code

kotlin · 11 lines
kotlin
val java = components["java"] as org.gradle.api.component.AdhocComponentWithVariants

// add a docs variant; optional in POM, full variant in GMM
java.addVariantsFromConfiguration(configurations["docsElements"]) {
  mapToOptional()
}

// exclude an internal outgoing config from the published POM
java.addVariantsFromConfiguration(configurations["internalElements"]) {
  skip()
}

go deeper

for a junior

Awareness only: it's how custom variants get into a publication.

for a middle

Explain addVariantsFromConfiguration and the scope-mapping options at a high level.

for a senior

Demonstrate mapping consumable configs, mapToMavenScope/optional/skip, and why attributes are required.

for a principal

Discuss governing the published variant surface — preventing metadata leakage and standardizing variant policy across modules.

## The problem it solves A project can have **many outgoing (consumable) configurations** — `apiElements`, `runtimeElements`, `javadocElements`, `sourcesElements`, custom ones. Publishing must decide *which* of these become variants in the published module and *how their dependencies map to POM scopes*. `AdhocComponentWithVariants` is the configurable component that makes those decisions explicit. ## Getting one Two paths: 1. **Reuse the existing component** — `components.java` is itself an `AdhocComponentWithVariants`, so you can extend it: ```kotlin val javaComponent = components["java"] as AdhocComponentWithVariants ``` 2. **Create a fresh one** by injecting `SoftwareComponentFactory`: ```kotlin import org.gradle.api.component.SoftwareComponentFactory import javax.inject.Inject abstract class MyPlugin @Inject constructor( private val factory: SoftwareComponentFactory ) : Plugin<Project> { override fun apply(project: Project) { val component = factory.adhoc("myComponent") project.components.add(component) } } ``` ## Mapping configurations to variants The core method is `addVariantsFromConfiguration(consumableConfiguration) { variantDetails -> ... }`. The action receives a `ConfigurationVariantDetails` with three controls: - **`mapToMavenScope("compile")`** / `"runtime"` — how this variant's dependencies appear in the generated POM (POM only has compile/runtime/etc.). - **`mapToOptional()`** — the variant is published but its dependencies are marked optional in the POM. - **`skip()`** — exclude this configuration's artifacts/dependencies from the POM entirely (it still appears in GMM as a variant unless you don't add it at all). ```kotlin val docsElements = configurations.consumable("docsElements") { attributes { attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION)) } outgoing.artifact(tasks.named("docsZip")) } (components["java"] as AdhocComponentWithVariants) .addVariantsFromConfiguration(docsElements.get()) { mapToOptional() // present in GMM, optional in POM } ``` ## Why `skip()` matters Some outgoing configurations are internal plumbing you don't want consumers resolving from the published module. Calling `skip()` keeps the POM clean and avoids exposing unintended dependencies — important for not leaking implementation detail into the published metadata. ## Relationship to the rest of the model The configuration you map must be **consumable** (`canBeConsumed = true`, `canBeResolved = false`) and carry **attributes** so consumers can select it. The adhoc component simply records which consumable configurations are publishable and their POM-scope mapping; GMM then captures the full attribute set, while the POM gets the flattened compile/runtime view.

  • What must be true of a configuration before you can add it as a published variant?
    It must be consumable (canBeConsumed=true, canBeResolved=false) and ideally carry attributes so consumers can select it.
  • What is the difference between skip() and mapToOptional()?
    skip() removes the configuration's contribution from the generated POM entirely; mapToOptional() still publishes it but marks its dependencies optional in the POM.
  • Is components.java itself an AdhocComponentWithVariants?
    Yes — that's why you can cast it and call addVariantsFromConfiguration to extend the java component with extra variants.

saying these in an interview costs you the question

  • Trying to map a resolvable configuration — only consumable ones are publishable.
  • Believing the POM can express multiple variants — it flattens; GMM is what preserves the variant graph.
  • Forgetting that skip() vs not-adding behave differently for GMM presence.

context