When assembling a complex output directory (e.g. an install image with libs, configs, and scripts in different subfolders), how do you structure Copy/Sync tasks, and what laziness and correctness pitfalls should you watch for?
answer
- one Sync, nested from{ into('sub') }
- configurations.runtimeClasspath lazy input
- DuplicatesStrategy explicit
- copySpec{} + with() reuse
- Sync deletes -> dedicated build dir
- register not create
basics
~20 sUse one Sync/Copy task with nested from(...) { into("subdir") } child CopySpecs to fan files into subfolders. Wire inputs via Providers/configurations so they stay lazy, and target a dedicated build dir so Sync's delete is safe.
solid answer
~50 sModel the whole image as a single `Sync` (so stale files are removed) and compose it from nested `CopySpec`s: each `from(source) { into("subdir") }` routes a source into its own destination subfolder under the single root `into`. Pull dynamic inputs from `Provider`/`Property` and `Configuration`s (e.g. `from(configurations.runtimeClasspath) { into("lib") }`) rather than resolving eagerly at configuration time, so configuration-avoidance holds and the dependency resolution defers to execution. Register with `tasks.register<Sync>` and avoid `project.copy {}` in configuration blocks. Correctness pitfalls: (1) `Sync`'s destructive delete means `into` must be a dedicated build directory; (2) `expand`/`filter` must be scoped to text-only child specs; (3) duplicate file paths from multiple sources trigger a `DuplicatesStrategy` decision — set it explicitly (`EXCLUDE`/`INCLUDE`/`FAIL`); (4) reusing a shared `CopySpec` across tasks via `project.copySpec { }` keeps things DRY. Declare extra inputs/outputs if you add custom logic so incrementality and caching stay correct.
code
kotlin · 8 linestasks.register<Sync>("assembleImage") {
duplicatesStrategy = DuplicatesStrategy.FAIL
into(layout.buildDirectory.dir("image"))
from(configurations.runtimeClasspath) { into("lib") }
from("src/conf") { into("conf"); expand("version" to project.version) }
from("src/bin") { into("bin"); eachFile { permissions { unix("rwxr-xr-x") } } }
preserve { include("data/**") }
}go deeper
Likely beyond junior; at most: use one task with from/into.
Show nested from{ into('sub') } fan-out and the Sync delete-safety rule.
Cover lazy inputs (configurations/Providers), DuplicatesStrategy, copySpec reuse, incrementality of custom actions.
Standardize a shared distribution-assembly convention/plugin; govern DuplicatesStrategy and destructive-target policy org-wide.
## Composing a real install image A distribution directory usually has structure: `lib/` for jars, `conf/` for templated config, `bin/` for scripts. Build it as **one** task so the whole tree is consistent and (with `Sync`) free of stale leftovers. ### Nested CopySpecs for fan-out `CopySpec` is composable. A `from(source) { ... }` block is itself a child spec, and its `into("sub")` is *relative* to the parent's root `into`: ```kotlin tasks.register<Sync>("installDist") { into(layout.buildDirectory.dir("install")) // single root from(configurations.runtimeClasspath) { into("lib") } // -> install/lib from("src/conf") { // -> install/conf into("conf") expand("version" to project.version) // text-only scope } from("src/bin") { // -> install/bin into("bin") eachFile { permissions { unix("rwxr-xr-x") } } } } ``` ### Laziness / configuration avoidance - Use `tasks.register`, not `create`. - Feed inputs as `Provider`s: `layout.buildDirectory.dir(...)`, `configurations.runtimeClasspath` (a lazy `FileCollection`). This avoids resolving dependencies or touching the filesystem at configuration time. - Never call `project.copy {}` inside a configuration block — it executes immediately and isn't tracked. ### `DuplicatesStrategy` When two `from`s contribute the same relative path, Gradle must choose. The default for many copy tasks is `INCLUDE` (last wins) but processResources defaults to `FAIL` in newer versions. Be explicit: ```kotlin duplicatesStrategy = DuplicatesStrategy.EXCLUDE // or FAIL / INCLUDE ``` This prevents non-deterministic outputs. ### Reusable specs Extract shared copy logic with `project.copySpec { ... }` and `with(spec)`: ```kotlin val distConf = copySpec { from("src/conf"); expand("version" to project.version) } tasks.register<Sync>("a") { into("build/a"); with(distConf) } ``` ### Safety with `Sync` Because `Sync` deletes anything in `into` not produced by the sources, the destination MUST be a dedicated build subdirectory. Use `preserve { include(...) }` for any legitimately external content. Never sync into project root or a source dir. ### Incrementality The nested sources are all tracked inputs; the destination tree is the output. As long as you don't do untracked imperative IO in `doLast`, the task is incremental and cacheable. If you add a custom action that reads/writes files, declare them with `@InputFiles`/`@OutputDirectory` (in a custom type) or via `inputs`/`outputs` wiring so up-to-date checks remain valid.
- How does `into` on a nested `from { }` block relate to the task's root `into`?The nested `into` is resolved relative to the parent/root `into`, so `from(x) { into("lib") }` lands in `<root>/lib`. This is how you fan sources into subdirectories from one task.
- Why set `DuplicatesStrategy` explicitly?When multiple sources contribute the same relative path, the default (last-wins/INCLUDE, or FAIL for some tasks) may be wrong or non-deterministic. Setting EXCLUDE/INCLUDE/FAIL makes the output deterministic and intentional.
- How do you keep dependency resolution from happening at configuration time?Pass `configurations.runtimeClasspath` directly as a lazy FileCollection input and use `tasks.register`; resolution then defers to task execution, preserving configuration avoidance.
saying these in an interview costs you the question
- Resolving configurations or doing IO at configuration time (breaks configuration avoidance).
- Ignoring DuplicatesStrategy and getting non-deterministic outputs.
- Using Sync against an unsafe destination and deleting hand-maintained files.