skip to content

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?

level: seniorimportance: should knowfreq 33%

answer

  1. bare File = no task dependency
  2. publish runs before producer -> fail/stale
  3. builtBy(task) on the artifact
  4. or pass task-output Provider (lazy)
  5. config-cache friendly via providers

basics

~20 s

A 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
kotlin
// 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

for a junior

Just know that passing a plain File can publish before it's built; pass the task instead.

for a middle

Explain builtBy and that tasks/providers carry the dependency automatically.

for a senior

Compare builtBy vs lazy provider, the stale/missing-file symptom, and configuration-cache implications of eager Files.

for a principal

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.

context