How would you build a custom software component in a plugin to publish a non-standard artifact set, and what are the design trade-offs?
answer
- inject SoftwareComponentFactory
- factory.adhoc(name) + components.add
- configurations.consumable + attributes
- outgoing.artifact
- extend components.java when possible
basics
~10 sInject SoftwareComponentFactory, call factory.adhoc("name"), add it to project.components, create consumable configurations with attributes and artifacts, then addVariantsFromConfiguration to map them. Publish via from(components["name"]).
solid answer
~40 sFor a project that doesn't fit the stock `java` component — say a build producing native zips or a non-JVM artifact set — you assemble an `AdhocComponentWithVariants` yourself. Inject `SoftwareComponentFactory` into your plugin, call `factory.adhoc("myComp")`, and register it in `project.components`. Then define **consumable** configurations (`canBeConsumed=true, canBeResolved=false`) with **attributes** describing each variant (Usage, Category, etc.) and attach artifacts via `outgoing.artifact(taskProvider)`. Map each into the component with `addVariantsFromConfiguration(config) { mapToMavenScope(...) / mapToOptional() / skip() }`. Finally publish with `from(components["myComp"])`. Trade-offs: building it by hand gives full control and lets non-JVM ecosystems use Gradle publishing, but you own attribute correctness — wrong/missing attributes break consumer variant selection. Prefer reusing/extending `components.java` when you're really just adding a variant to a JVM library rather than rebuilding the whole component.
code
kotlin · 14 linesabstract class DistPlugin @Inject constructor(
private val factory: org.gradle.api.component.SoftwareComponentFactory
) : Plugin<Project> {
override fun apply(project: Project): Unit = with(project) {
val comp = factory.adhoc("distribution").also { components.add(it) }
val elements = configurations.consumable("distElements") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.NATIVE_RUNTIME))
}
outgoing.artifact(tasks.named("distZip"))
}
comp.addVariantsFromConfiguration(elements.get()) { mapToMavenScope("runtime") }
}
}go deeper
Awareness that custom components exist for non-standard artifacts.
Outline the adhoc + consumable-configuration + addVariantsFromConfiguration steps.
Implement it with injected factory, attributes, and discuss reuse-vs-rebuild and POM-flattening trade-offs.
Weigh publishing-contract design across ecosystems and standardize component-building patterns in shared convention plugins.
## When you need a custom component The built-in `components.java`/`components.javaPlatform` cover JVM libraries and BOMs. If your project produces a **different artifact shape** — a distribution zip, a native binary set, multiple classifier artifacts with distinct dependency contexts — you build your own component so publishing can render proper metadata. ## Step 1 — get a factory and an adhoc component `SoftwareComponentFactory` is a service you **inject** (it can't be `new`-ed): ```kotlin import org.gradle.api.component.SoftwareComponentFactory import javax.inject.Inject abstract class DistributionPlugin @Inject constructor( private val softwareComponentFactory: SoftwareComponentFactory ) : Plugin<Project> { override fun apply(project: Project): Unit = with(project) { val component = softwareComponentFactory.adhoc("distribution") components.add(component) // ... configure below } } ``` ## Step 2 — declare consumable configurations with attributes Each variant is backed by a **consumable** configuration. Attributes are what let consumers select it: ```kotlin val distElements = configurations.consumable("distributionElements") { attributes { attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.NATIVE_RUNTIME)) attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) } outgoing.artifact(tasks.named("distZip")) } ``` `configurations.consumable(...)` (Gradle 8.x) creates it already locked to `canBeConsumed=true, canBeResolved=false` — the correct role for an outgoing/publishable configuration. ## Step 3 — map configurations into the component ```kotlin component.addVariantsFromConfiguration(distElements.get()) { mapToMavenScope("runtime") } ``` ## Step 4 — publish ```kotlin publishing { publications { create<MavenPublication>("dist") { from(components["distribution"]) } } } ``` ## Design trade-offs - **Control vs. correctness burden.** You decide every variant and attribute — powerful, but you own getting attributes right. Missing/ambiguous attributes cause *variant selection failures* or *ambiguity errors* on the consumer side. - **Reuse vs. rebuild.** If you only need to *add* a variant (docs, sources, a platform feature) to an existing JVM library, **extend `components.java`** (it's already an adhoc component) instead of creating a parallel one — fewer moving parts and consistent with the java ecosystem defaults. - **POM friendliness.** Non-JVM variants flatten poorly into a POM; lean on GMM and document that Maven consumers get a degraded view. - **Configuration roles.** Always use consumable (outgoing) configurations — mapping a resolvable configuration is an error and signals a role mix-up. ## Common failure modes - Forgetting attributes → consumers can't disambiguate variants. - Adding the artifact to the wrong configuration role. - Re-using one configuration for both resolution and consumption (deprecated/forbidden in modern Gradle — use `consumable`/`resolvable`/`dependencyScope` factory methods).
- Why must SoftwareComponentFactory be injected rather than constructed?It's a Gradle-provided service; Gradle supplies it via constructor injection (the @Inject mechanism), so you can't instantiate it yourself.
- When should you NOT build a custom component?When you only need to add a variant to a standard JVM library — extend the existing components.java instead, which is already an AdhocComponentWithVariants.
- What happens if your consumable configuration has no attributes?Consumers may be unable to select it unambiguously, causing variant-selection or ambiguity resolution errors.
saying these in an interview costs you the question
- Constructing SoftwareComponentFactory directly instead of injecting it.
- Mapping a resolvable configuration into the component.
- Omitting attributes on consumable configurations and expecting consumers to resolve correctly.