When you attach a raw File (not a task) as a publication artifact, why might the publish task fail, and how do you fix the build wiring lazily?
answer
- bare File = no task dependency
- publish runs before producer -> fail/stale
- builtBy(task) on the artifact
- or pass task-output Provider (lazy)
- config-cache friendly via providers
basics
~20 sA bare File carries no task dependency, so the publish task can run before the file is generated and fails (missing file). Fix it by adding builtBy(theTask) or by passing a Provider<RegularFile> from the task's output, which carries the dependency.
solid answer
~40 s`artifact(...)` only auto-wires a build dependency when you pass something that **carries** task information — a task, a task-output `Provider<RegularFile>`, or a `PublishArtifact`. If you pass a plain `File` or a string path, Gradle has no idea what produces it, so `publish` may run before the file exists and the upload fails (or stales). Two fixes: (1) call `builtBy(producingTask)` on the `ConfigurablePublishArtifact`, or (2) prefer a lazy provider, e.g. `artifact(myTask.flatMap { it.archiveFile })`, where the provider propagates the dependency and keeps configuration lazy/config-cache friendly. The provider route also defers resolving the actual file path until execution. In practice the cleanest is to attach the task itself; reach for `builtBy` only when you genuinely have just a `File`.
code
kotlin · 19 lines// Preferred: provider carries the dependency, stays lazy
val generateReport by tasks.registering(GenerateReport::class)
publishing {
publications {
named<MavenPublication>("maven") {
artifact(generateReport.flatMap { it.output }) {
classifier = "report"
extension = "json"
}
}
}
}
// Fallback when you only have a File:
artifact(file("build/extra.bin")) {
classifier = "extra"
extension = "bin"
builtBy(tasks.named("makeExtra"))
}go deeper
Just know that passing a plain File can publish before it's built; pass the task instead.
Explain builtBy and that tasks/providers carry the dependency automatically.
Compare builtBy vs lazy provider, the stale/missing-file symptom, and configuration-cache implications of eager Files.
Establish conventions (always wire via providers) so publish graphs stay correct and cacheable across many modules and CI.
## The problem: implicit dependencies Gradle's task graph is built from declared inputs/outputs and explicit dependencies. When you attach an artifact, Gradle needs to know *which task produces the file* so that the publish task (`publish<PubName>PublicationTo<Repo>Repository`) runs **after** it. How that dependency is discovered depends on what you pass to `artifact(...)`: - **A task** (`tasks.named("fatJar")`) — Gradle records `builtBy = thatTask` automatically. Best case. - **A `Provider<RegularFile>` from a task output** (`task.flatMap { it.archiveFile }`) — the provider carries the producing task's dependency information, so wiring is automatic and lazy. - **A bare `File` or path string** — **no** dependency info. Gradle treats it as a static file; if nothing else forces the producing task to run first, `publish` can execute with the file absent → failure, or it publishes a stale file from a previous build. ## Fix 1: builtBy `ConfigurablePublishArtifact` (and the `artifact{}` block) supports `builtBy(...)`: ```kotlin val reportFile = layout.buildDirectory.file("reports/report.json") artifact(reportFile) { classifier = "report" extension = "json" builtBy(tasks.named("generateReport")) } ``` Now `publish` depends on `generateReport`. ## Fix 2: lazy provider (preferred) Passing a provider tied to the task output gives you both the dependency and laziness, and it cooperates with the **configuration cache**: ```kotlin val generateReport by tasks.registering(GenerateReport::class) { output = layout.buildDirectory.file("reports/report.json") } artifact(generateReport.flatMap { it.output }) { classifier = "report" extension = "json" } ``` Because `output` is an `@OutputFile RegularFileProperty`, the provider knows `generateReport` builds it — no manual `builtBy` needed. ## Why laziness matters Resolving `File` eagerly at configuration time can break the configuration cache and forces the path even when publishing won't run. Providers defer this to execution. The general Gradle guidance — wire outputs to inputs via `Provider`/`Property`, avoid eager `.get()` — applies directly here. ## Symptoms in interviews The classic bug report is *"my publish succeeds but uploads an old/missing file"* or *"works after a clean build but not incrementally."* The root cause is almost always a bare-File artifact with no `builtBy`.
- Why is passing a Provider<RegularFile> better than File + builtBy?The provider carries the producing task automatically (no manual builtBy to forget), defers file resolution to execution, and plays well with the configuration cache. File + builtBy is eager and easy to wire incorrectly.
- What's the tell-tale symptom of a missing builtBy on a File artifact?Publishing a stale or missing file: it works after `clean build publish` but uploads the wrong thing on incremental runs, because the producing task isn't ordered before publish.
- Does config-cache change anything here?Yes — eagerly captured Files can break or invalidate the configuration cache, so providers/lazy wiring become not just cleaner but often required.
saying these in an interview costs you the question
- Claiming `artifact(file(...))` automatically depends on whatever task writes that file — it doesn't.
- Recommending `dependsOn` on the publish task as the primary fix instead of builtBy/provider.
- Eagerly resolving the file with `.get()` at configuration time.