skip to content

Custom Distributions and Arbitrary Content

Defining extra named distributions and packaging arbitrary content with a CopySpec, for docs, configs, or other non-code files. Asked because not every deliverable is a jar.

on this pageshow

questions

5

Inside a distribution's `contents { }` block, how do `from(...)` and `into(...)` work as a CopySpec to lay out arbitrary files in the archive?

level: middleimportance: must knowfreq 40%

answer

  1. CopySpec shared by Copy/Zip/Jar/contents
  2. from = sources accumulate; dir copies contents
  3. into = dest relative to archive root
  4. include/exclude/rename/expand/filter
  5. from(...) { into(...) } nests groups

basics

~10 s

contents is a CopySpec. from(...) declares source files/dirs; into(...) sets the destination path inside the archive. You nest them to build the archive's folder layout.

solid answer

~40 s

A distribution's `contents { }` is the same `CopySpec` abstraction used by `Copy`, `Zip`, and `Jar` tasks. `from(source)` adds source roots — directories, files, or other CopySpecs. `into(path)` sets the destination directory **inside** the archive (relative to its root). The two compose: a top-level `into("app")` rebases everything, and nested `from(...) { into("sub") }` blocks place groups into subfolders. You can layer `include`/`exclude` filters, `rename { }`, `filter { }` for token substitution, `expand(mapOf(...))` for Groovy-template expansion, and `fileMode`/`dirPermissions` for permissions. Because it's lazy and incremental, only changed inputs trigger re-packaging. This is the heart of shipping docs, configs, or non-code assets: you describe the desired tree declaratively, and `*DistZip`/`*DistTar` materialize it.

code

kotlin · 14 lines
kotlin
distributions {
    create("bundle") {
        contents {
            from("LICENSE")                       // -> archive root
            from("src/docs") { into("docs") }      // -> docs/
            from("config") {
                into("etc")
                rename("(.+)\\.template", "$1")
                expand("version" to project.version)
            }
            exclude("**/*.tmp")
        }
    }
}

go deeper

for a junior

Knows from picks sources and into sets the destination folder inside the archive.

for a middle

Can compose nested from { into } groups, apply include/exclude/rename, and explain dir-vs-file copy semantics.

for a senior

Understands CopySpec is shared across all archive tasks, incrementality, expand/filter pitfalls, and permissions.

for a principal

Can factor a reusable CopySpec (via copySpec { }) shared across multiple distributions and projects, with a consistent layout contract.

## CopySpec: the universal file-layout DSL Gradle unifies all file packaging behind `CopySpec`. The `Copy` task, the `Zip`/`Tar` archive tasks, the `Jar`/`War` tasks, and a distribution's `contents` block are all `CopySpec`s. Learn it once, reuse everywhere. ### `from` — declare sources `from(...)` takes anything resolvable to files: a `String`/`File` path, a `FileCollection`, a `Provider`, a configuration, the output of another task, or a nested closure/`Action`. Multiple `from` calls accumulate. ```kotlin contents { from("src/docs") // a directory from(layout.buildDirectory.file("generated/manual.pdf")) from(tasks.named("renderDocs")) // wires task output + dependency } ``` When `from` points at a **directory**, its *contents* (not the directory itself) are copied, preserving the relative tree beneath it. When it points at a **file**, just that file is copied. ### `into` — declare destination `into(path)` is relative to the archive root. A top-level `into` rebases the whole spec; a `from(...) { into("x") }` nested form rebases only that group. ```kotlin contents { into("") { // archive root from("LICENSE") } from("src/docs") { into("docs") } from("config") { into("etc") } } ``` ### Filtering and transforming - `include("**/*.md")` / `exclude("**/*.tmp")` — Ant-style globs. - `rename("(.+).template", "$1")` — regex rename. - `expand("version" to project.version)` — SimpleTemplateEngine `${}` substitution. - `filter { line -> ... }` — line-by-line transform. - `filePermissions { unix("0755") }` (8.3+, replacing the deprecated `fileMode`). - `duplicatesStrategy = DuplicatesStrategy.EXCLUDE` — resolve collisions. ### Why it matters for distributions The distribution archive's internal tree is **exactly** what the `contents` CopySpec describes. There is no implicit content. So `from(...) { into(...) }` is how you ship a `docs/`, `config/`, `bin/` layout of arbitrary, non-code files as a reproducible zip/tar. ### Incrementality CopySpec inputs are tracked, so `*DistZip` is `UP-TO-DATE` when nothing changed, and only re-runs when a source file, filter, or rename changes.

  • When `from` points at a directory versus a single file, what gets copied?
    A directory copies its *contents* (the tree beneath it), so the directory name itself is not included unless you re-add it via `into`. A file copies just that one file. To preserve the directory name, wrap with `into("dirName")` or use `from("parent")` so the directory appears as a child.
  • How do you do token substitution like `${version}` inside packaged config files?
    Use `expand(mapOf("version" to project.version))`, which runs Groovy's SimpleTemplateEngine over each file, replacing `${version}`. For simpler line edits use `filter { }`. Be careful: `expand` treats `$` and `\` specially, so escape literal dollar signs in shell scripts.

Think of from as 'pick up these piles of files' and into as 'put this pile in that drawer of the cabinet'. The archive is the cabinet; nested into() calls are the labeled drawers.

saying these in an interview costs you the question

  • Believing `from(dir)` includes the directory name itself (it copies the contents).
  • Thinking `into("x")` is an absolute filesystem path — it is relative to the archive root.
  • Forgetting that `expand` will break `$VAR` shell syntax unless escaped.

context

open as a page

How do you define an additional named distribution (beyond 'main') with the Distribution plugin, and how does the name affect the generated tasks and archive files?

level: middleimportance: must knowfreq 45%

basics

~10 s

Apply the distribution plugin and add an entry to the distributions { } container, e.g. create("custom"). Gradle generates customDistZip/customDistTar tasks and archives named <project>-custom.zip.

open as a page

When would you reach for the `distribution` plugin to package arbitrary content instead of the `application` plugin, and how do they relate?

level: middleimportance: should knowfreq 35%

basics

~10 s

Use distribution when you just need to ship arbitrary files (docs, configs) as an archive with no runtime/launcher. The application plugin is for runnable JVM apps and is built on top of distribution.

open as a page

A distribution archive must be reproducible bit-for-bit and preserve correct file permissions (e.g. executable scripts) across builds. How do you configure that on the distribution/archive tasks?

level: seniorimportance: should knowfreq 22%

basics

~10 s

Set isReproducibleFileOrder = true and isPreserveFileTimestamps = false on the *DistZip/*DistTar tasks, and declare permissions with filePermissions { unix("0755") } in the CopySpec for executables.

open as a page

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%

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.

open as a page