Inside a distribution's `contents { }` block, how do `from(...)` and `into(...)` work as a CopySpec to lay out arbitrary files in the archive?
answer
- CopySpec shared by Copy/Zip/Jar/contents
- from = sources accumulate; dir copies contents
- into = dest relative to archive root
- include/exclude/rename/expand/filter
- from(...) { into(...) } nests groups
basics
~10 scontents 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 sA 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 linesdistributions {
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
Knows from picks sources and into sets the destination folder inside the archive.
Can compose nested from { into } groups, apply include/exclude/rename, and explain dir-vs-file copy semantics.
Understands CopySpec is shared across all archive tasks, incrementality, expand/filter pitfalls, and permissions.
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.