How do you attach an extra file (for example, a fat/uber JAR produced by a custom task) to a Maven publication in Gradle?
answer
- publication.artifact(...)
- pass the task, not a File
- returns ConfigurablePublishArtifact
- set classifier + extension
- task carries builtBy automatically
basics
~10 sInside the publication block call artifact(...), passing the task that produces the file, e.g. artifact(tasks.named("fatJar")). Gradle wires the task as a build dependency and uploads its output as an extra artifact.
solid answer
~40 sIn `publishing { publications { maven(MavenPublication) { ... } } }` you call `artifact(...)` on the publication. The simplest form passes the producing task — `artifact(tasks.named("fatJar"))` — and Gradle infers the file from the task's single archive output, adds the task as a dependency of the publish task, and uploads the file. You can also pass a `File`/`Provider<RegularFile>` directly, but then you must add `builtBy` so Gradle knows what produces it. The call returns a `ConfigurablePublishArtifact`, so you typically configure `classifier` and `extension` in a trailing block to give the file a distinct coordinate (e.g. `classifier = "all"`). Each `artifact(...)` adds one file; coordinates (group/name/version) come from the publication, the classifier/extension distinguish it.
code
kotlin · 17 linesval fatJar by tasks.registering(Jar::class) {
archiveClassifier = "all"
from(sourceSets.main.get().output)
// ... bundle dependencies ...
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
artifact(fatJar) { // returns ConfigurablePublishArtifact
classifier = "all"
extension = "jar"
}
}
}
}go deeper
Know that artifact(...) inside the publication block attaches an extra file and you usually pass it the producing task.
Explain passing a task vs a File, the returned ConfigurablePublishArtifact, and setting classifier/extension; mention automatic build wiring when you pass the task.
Discuss builtBy for bare files, lazy providers, duplicate-coordinate failures, and that raw artifacts show in the POM but not in Gradle Module Metadata as variants.
Frame when raw extra artifacts are appropriate vs feature variants/outgoingVariants for cross-project consumption, and the governance impact of artifacts that lack metadata description.
## What an "artifact" is A Maven publication has coordinates (`group:name:version`) and one or more **files** attached to those coordinates. The main file is usually the project JAR (added automatically when you do `from components.java`). Beyond that you can attach **extra artifacts** — additional files that share the same coordinates but are distinguished by a **classifier** (e.g. `sources`, `javadoc`, `all`) and an **extension** (e.g. `jar`, `zip`, `aar`). ## The `artifact(...)` method Inside a `MavenPublication`, `artifact(Object source)` attaches one extra file. The `source` can be: - A **task** that produces a single archive (`AbstractArchiveTask` like `Jar`/`Zip`). Gradle reads the archive's output file and, crucially, records the task as a dependency of the publish task. This is the cleanest form: `artifact(tasks.named("fatJar"))`. - A **`File`** or **`Provider<RegularFile>`** / file path. Gradle will publish that file but does **not** know how it is built, so you must set `builtBy` (or pass a provider that carries task dependencies) or the publish task may run before the file exists. - An existing `PublishArtifact`. ## Configuring the result — `ConfigurablePublishArtifact` `artifact(...)` returns (and the closure/lambda receives) a `ConfigurablePublishArtifact`, whose mutable properties are `classifier`, `extension`, plus read-only `name`/`type`. You set classifier/extension so the extra file gets its own coordinate slug: ```kotlin artifact(tasks.named("fatJar")) { classifier = "all" extension = "jar" } ``` This publishes `name-version-all.jar` alongside the main `name-version.jar`. ## Why classifier/extension matter If two extra artifacts share the same classifier+extension, you get a duplicate-coordinate error at publish time. The classifier is what lets a consumer ask for that specific file. Note that artifacts attached this raw way appear in the **POM** for Maven consumers but are **not** described as a variant in Gradle Module Metadata — so Gradle-native consumers selecting by attribute won't automatically resolve them (that is the realm of feature variants / `outgoingVariants`). ## Build wiring Because `artifact(tasks.named("fatJar"))` carries the task, `publishMavenPublicationToMavenRepository` automatically depends on `fatJar`. Always prefer passing the task (or a task-output provider) over a bare `File` so the build graph stays correct and lazy.
- What happens if you pass a bare File instead of a task to artifact(...)?Gradle publishes the file but doesn't know what builds it, so the publish task may run before the file exists. You must add `builtBy(theTask)` (or pass a provider carrying the dependency) to wire the build graph.
- Where do the group/name/version of an extra artifact come from?From the publication itself — extra artifacts always share the publication's coordinates. Only the classifier and extension distinguish them; you cannot give a single `artifact(...)` entry different coordinates.
saying these in an interview costs you the question
- Saying you must hardcode the file path string — prefer passing the task or its output provider.
- Claiming `artifact(...)` adds a separate dependency/coordinate — it shares the publication's group:name:version, only classifier/extension differ.
- Forgetting that a bare File needs `builtBy` to wire task ordering.