skip to content

How would you build a distribution's contents lazily from task outputs and reuse a shared layout across several distributions, using Providers and reusable CopySpecs?

level: seniorimportance: should knowfreq 25%

answer

  1. from(tasks.named(...)) => implicit dependsOn + lazy
  2. layout.buildDirectory not $buildDir
  3. copySpec { } + with(spec) to reuse
  4. DuplicatesStrategy for overlaps
  5. config-cache friendly: no eager project reads

basics

~10 s

Feed from(...) with task Providers (e.g. tasks.named("genDocs")) so packaging is lazy and dependency-wired. Factor common layout into a copySpec { } and reuse it via with(spec) across multiple distributions.

solid answer

~40 s

To stay lazy and avoid eager configuration, declare distribution contents from `Provider`-based sources: `from(tasks.named("renderDocs"))` automatically adds the producing task as a dependency and reads its output at execution time. Use `layout.buildDirectory.file(...)`/`.dir(...)` instead of hardcoded `build/` paths so the build directory is configurable. For reuse, create a standalone CopySpec with `project.copySpec { from(...); into(...) }` and apply it in multiple distributions via `contents { with(commonSpec) }` — `with()` merges one CopySpec into another. This avoids duplicating the layout. Combine with `DuplicatesStrategy` when merged specs overlap. Because all of this is lazy, the `*DistZip` task only realizes inputs when it actually runs, and incremental/up-to-date checks work correctly. The result is a DRY, configuration-cache-friendly packaging setup.

code

kotlin · 17 lines
kotlin
val renderDocs by tasks.registering(Copy::class) {
    from("src/docs"); into(layout.buildDirectory.dir("rendered"))
}

val commonAssets = copySpec {
    from("LICENSE", "NOTICE"); into("legal")
}

distributions {
    create("docs") {
        contents {
            with(commonAssets)
            from(renderDocs) { into("docs") }   // lazy + auto-wired
            duplicatesStrategy = DuplicatesStrategy.EXCLUDE
        }
    }
}

go deeper

for a junior

May only know from/into with literal paths; lazy/reuse concepts are beyond expected recall.

for a middle

Can wire a single task output with from(tasks.named(...)) and knows copySpec exists.

for a senior

Fluently composes lazy providers, reusable CopySpecs with with, duplicatesStrategy, and config-cache-safe layout.

for a principal

Codifies a reusable packaging convention plugin: shared CopySpecs, lazy wiring, and reproducible archives standardized across the org.

## Lazy, wired, and reusable packaging ### 1. Source from task outputs via Providers Never copy from a hardcoded `build/...` path you computed eagerly — wire to the producing task so Gradle infers the dependency and reads outputs lazily: ```kotlin val renderDocs by tasks.registering(Copy::class) { from("src/docs") into(layout.buildDirectory.dir("rendered")) } distributions { create("docs") { contents { from(renderDocs) // implicit dependsOn + lazy output read } } } ``` `from(taskProvider)` resolves to the task's output files and adds the task as an input dependency — no manual `dependsOn`. Use `layout.buildDirectory` (a `DirectoryProperty`) instead of `"$buildDir/..."` so the path is a `Provider` and the build dir stays relocatable and configuration-cache-safe. ### 2. Reuse a layout with `copySpec` + `with` A `CopySpec` is a first-class, composable object. Build a shared one and merge it: ```kotlin val commonAssets = copySpec { from("LICENSE") from("NOTICE") into("legal") } distributions { create("docs") { contents { with(commonAssets); from("src/docs") { into("docs") } } } create("samples") { contents { with(commonAssets); from("samples") { into("examples") } } } } ``` `with(spec)` copies the rules of `spec` into the target CopySpec. Both distributions now share the legal assets without duplication. ### 3. Handle overlaps Merging or multiple `from`s can collide on a path. Set `duplicatesStrategy = DuplicatesStrategy.EXCLUDE` (keep first), `.INCLUDE`, `.FAIL`, or `.WARN` to make collisions deterministic. ### 4. Configuration-cache friendliness Using `Provider`/`layout.buildDirectory` and task-output `from` keeps everything lazy, so the configuration cache can serialize the task graph and the `*DistZip` stays up-to-date when inputs are unchanged. Avoid reading `project` at execution time inside the spec. ### Why it matters For real shippable bundles you typically package *generated* content (rendered docs, generated configs, downloaded assets) plus shared boilerplate. Laziness gives correct dependency ordering and incrementality; reusable CopySpecs keep many distributions consistent.

  • Why prefer `from(tasks.named("x"))` over `from("build/x-output")` with an explicit `dependsOn`?
    Passing the task provider makes `from` resolve the task's declared outputs and automatically registers the task as an input dependency, so ordering and up-to-date checks are correct. A hardcoded path with manual `dependsOn` is fragile: it duplicates the path, can drift from the real output location, and breaks the build-dir relocatability and configuration cache.
  • What does `with(otherSpec)` do, and when is it preferable to copy-pasting `from`/`into` lines?
    `with` merges the rules of another `CopySpec` into the current one, so a shared layout defined once via `copySpec { }` can be applied to many archives. It's preferable whenever multiple distributions or archive tasks need the same files/layout — it keeps them consistent and DRY.
  • How do you make collisions between merged specs deterministic?
    Set `duplicatesStrategy` on the CopySpec to one of EXCLUDE (keep first), INCLUDE, FAIL, or WARN. EXCLUDE is common when a shared spec and a per-distribution spec both supply the same file.

saying these in an interview costs you the question

  • Hardcoding `$buildDir` paths and manual `dependsOn` instead of wiring task providers.
  • Reading `project`/mutable state at execution time, breaking the configuration cache.
  • Duplicating the same `from`/`into` layout across distributions instead of a shared `copySpec`.

context