skip to content

How do you wire the artifacts of a configuration into a custom task lazily, and why does laziness matter here?

level: seniorimportance: should knowfreq 30%

answer

  1. .files / resolutionResult force eager resolution
  2. artifactView.artifacts.artifactFiles = lazy FileCollection
  3. configurations.named → Provider
  4. @InputFiles ConfigurableFileCollection
  5. config cache serializes graph not results

basics

~10 s

Use configuration.incoming.artifactView{...}.artifacts.artifactFiles (a lazy FileCollection) as the task's @InputFiles. Resolution then happens at execution time, not configuration time, keeping builds fast and configuration-cache compatible.

solid answer

~40 s

Don't resolve a configuration eagerly during configuration. Instead expose its artifacts as a **lazy `FileCollection`** — `configuration.incoming.files`, or better `configuration.incoming.artifactView { }.artifacts.artifactFiles` when you need filtering — and bind that to a `@InputFiles ConfigurableFileCollection` (or pass it to `from(...)`). The `FileCollection` carries the dependency on resolution but defers it until the task actually runs. Laziness matters for three reasons: (1) **performance** — resolving at configuration time happens on *every* invocation, even for unrelated tasks, slowing the whole build; (2) **correctness/up-to-date** — a lazy input lets Gradle track inputs for incremental builds and avoid stale snapshots; (3) **configuration cache** — eagerly resolved results can't be safely serialized across the cache boundary, whereas the lazy FileCollection (and its provider wiring) is cache-friendly. Prefer `Provider`/`Property` wiring and `registerIfAbsent`/`tasks.register` so nothing forces resolution prematurely.

code

kotlin · 11 lines
kotlin
abstract class ScanDeps : DefaultTask() {
    @get:InputFiles abstract val jars: ConfigurableFileCollection
    @TaskAction fun run() = jars.forEach { println(it.name) }
}

tasks.register<ScanDeps>("scanDeps") {
    jars.from(
        configurations.named("runtimeClasspath")
            .map { it.incoming.artifactView { lenient = true }.artifacts.artifactFiles }
    )
}

go deeper

for a junior

Know that you should pass the configuration (or its files) to a task rather than resolving it yourself, and that tasks.register is preferred.

for a middle

Wire artifactView.artifacts.artifactFiles into a @InputFiles ConfigurableFileCollection and explain it resolves at execution time.

for a senior

Explain configurations.named/Provider.map laziness and why eager resolution breaks performance, incrementality, and configuration cache.

for a principal

Set conventions/lint that ban configuration-time resolution across an org's build plugins to keep configuration cache viable at scale.

## The anti-pattern: eager resolution ```kotlin // BAD: resolves at configuration time, every build val files = configurations.getByName("runtimeClasspath").files tasks.register<Copy>("bundle") { from(files) } ``` Calling `.files` (or iterating the configuration, or touching `resolutionResult`) **forces resolution now**, during the configuration phase — for every build invocation, regardless of whether `bundle` runs. This is a classic source of slow configuration and configuration-cache failures. ## The lazy approach ```kotlin // GOOD: lazy FileCollection, resolved only when the task runs val rt = configurations.named("runtimeClasspath") tasks.register<Copy>("bundle") { from(rt.map { it.incoming.artifactView { lenient = true }.artifacts.artifactFiles }) into(layout.buildDirectory.dir("bundle")) } ``` Key pieces: - **`configurations.named(...)`** returns a `Provider<Configuration>` — no eager realization. - **`incoming.files`** / **`artifactView{}.artifacts.artifactFiles`** are **lazy `FileCollection`s**: they encapsulate the dependency on resolution but don't resolve until queried at execution time. - Binding them via `from(...)`, or into a `@InputFiles ConfigurableFileCollection`, registers them as task inputs so Gradle snapshots them for up-to-date checks. ## Custom task wiring ```kotlin abstract class Analyze : DefaultTask() { @get:InputFiles abstract val classpath: ConfigurableFileCollection @TaskAction fun run() { classpath.files.forEach { /* ... */ } } } tasks.register<Analyze>("analyze") { classpath.from(configurations.named("runtimeClasspath") .map { it.incoming.artifactView { }.artifacts.artifactFiles }) } ``` ## Why laziness matters 1. **Build performance** — configuration runs on every invocation. Eager resolution there pays the resolution/download cost even when the dependent task isn't requested. Deferring to execution time means only relevant builds pay it. 2. **Incremental/up-to-date** — a lazily-wired `FileCollection` input is tracked properly; Gradle can skip the task when artifacts are unchanged. 3. **Configuration cache** — the cache serializes the *task graph*, not live resolution results. A `Provider`/lazy `FileCollection` survives serialization; an eagerly captured `Set<File>` or `ResolutionResult` does not, and triggers cache-incompatibility warnings/errors. ## Related lazy primitives - `tasks.register` (vs `create`) — lazy task realization. - `Provider.map`/`flatMap` — transform without resolving. - `registerIfAbsent` for shared build services. All keep the wiring lazy end-to-end so resolution never leaks into the configuration phase.

  • Which call would accidentally force resolution at configuration time?
    Reading configuration.files, iterating the configuration, or touching incoming.resolutionResult outside a task action — any of these resolves immediately during configuration.
  • Why is an eagerly captured Set<File> a problem for the configuration cache?
    The configuration cache serializes the task graph and its lazy providers, not live resolution outputs. A captured Set<File> is a non-lazy snapshot that bypasses provider wiring and can break or invalidate the cache.

saying these in an interview costs you the question

  • Calling .files in the configuration block and claiming it's lazy.
  • Using tasks.create + eager configuration resolution and saying it's config-cache safe.
  • Storing a resolved Set<File> in a task field instead of a ConfigurableFileCollection input.

context