skip to content

What goes wrong when ad-hoc input/output declarations are configured eagerly or with hardcoded paths, and how do lazy Providers help?

level: seniorimportance: should knowfreq 40%

answer

  1. eager File = no dependency inference
  2. config-time capture = stale value
  3. absolute paths hurt relocatability
  4. layout.buildDirectory / Provider / Property
  5. wiring output→input infers dependsOn

basics

~10 s

Eager File paths and values captured at configuration time can be wrong, miss task-dependency inference, or read stale config. Using lazy Providers/Property and layout.buildDirectory defers resolution and links producer→consumer tasks automatically.

solid answer

~40 s

Declaring inputs/outputs with eager `File` literals or hardcoded strings has two problems. First, **timing**: a value captured at configuration time may not reflect later configuration (e.g. a version set in another plugin), so the fingerprint can be wrong. Second, **dependency inference**: passing a raw `File` doesn't tell Gradle which task produces it, so you must add `dependsOn` manually and it's easy to forget, causing ordering bugs or missing inputs. The fix is Gradle's **lazy configuration API**: declare inputs/outputs from `Provider`/`Property`, `layout.buildDirectory.file(...)`, `layout.projectDirectory.dir(...)`, or another task's output property. When you wire one task's output `Provider` into another's input, Gradle infers the task dependency automatically and resolves the path at execution time. This keeps fingerprints accurate, avoids hardcoded absolute paths (which also hurt cache relocatability), and makes declarations robust to configuration order.

code

kotlin · 10 lines
kotlin
val gen = tasks.register("gen") {
    val out = layout.buildDirectory.file("gen/data.txt")
    outputs.file(out)
    doLast { out.get().asFile.writeText("x") }
}
tasks.register("use") {
    inputs.files(gen.map { it.outputs.files })  // infers dependency on gen
    outputs.file(layout.buildDirectory.file("final.txt"))
    doLast { /* ... */ }
}

go deeper

for a junior

Recognize that hardcoded paths and eager values are discouraged; prefer layout.buildDirectory.

for a middle

Explain config-time vs execution-time resolution and that lazy providers defer the read.

for a senior

Articulate dependency inference via output→input wiring and relocatability benefits for caching.

for a principal

Set conventions/lint to forbid eager File/hardcoded paths across a large build for reliability and cache hit-rate.

## The problem with eager declarations Consider declaring inputs/outputs like this: ```kotlin tasks.register("build") { inputs.file(File("/abs/path/version.txt")) // hardcoded absolute path inputs.property("version", project.version) // captured NOW, at config time outputs.file(File("build/out.jar")) } ``` Three latent bugs: 1. **Captured-too-early values.** `project.version` is read when the task is *configured*. If another plugin sets the version later in the configuration phase, the fingerprint uses the stale value. Wrapping in a `Provider` defers the read until it's actually needed. 2. **No dependency inference.** A raw `File` is just a path. If that file is produced by another task, Gradle has no way to know — you must remember `dependsOn(otherTask)`. Forget it and the consuming task may run before the file exists, or treat a stale file as its input. 3. **Absolute / hardcoded paths.** They leak machine-specific strings into reasoning and defeat cross-machine cache relocation; they also break if the build dir is relocated. ## The lazy API Gradle's lazy configuration types fix all three: - `Provider<T>` / `Property<T>` — a value computed on demand, not at declaration time. - `layout.buildDirectory` (a `DirectoryProperty`) and `layout.projectDirectory` — resolve relative to the build, never hardcoded. - A task's output property (e.g. another task's `RegularFileProperty`) — wiring it as an input both supplies the path AND tells Gradle the producing task is a dependency (**implicit task dependency / task-output wiring**). ```kotlin val generate = tasks.register("generate") { val out = layout.buildDirectory.file("gen/data.txt") outputs.file(out) doLast { out.get().asFile.writeText("...") } } tasks.register("consume") { // Wiring generate's output as input infers the dependency automatically inputs.file(generate.map { it.outputs.files.singleFile }) inputs.property("version", providers.provider { project.version.toString() }) outputs.file(layout.buildDirectory.file("final.txt")) doLast { /* ... */ } } ``` ## Why it matters for caching - Lazy providers keep the **input fingerprint accurate** (values read at the right time) — no silent staleness. - Resolving paths through `layout` keeps them **relative**, which combined with path-sensitivity normalization makes the cache **relocatable** across machines. - Automatic dependency inference means the real producer→consumer edge exists, so the consumer's declared input reflects the actually-produced file, not a stale leftover. ## Rule of thumb For any ad-hoc task: declare inputs/outputs from `layout.*`, task output providers, and `Provider`/`Property` values — avoid raw `File`, hardcoded strings, and config-time captures of mutable state.

  • How does wiring a producing task's output Provider as an input avoid a manual dependsOn?
    When an input is backed by another task's output property/Provider, Gradle records an implicit task dependency on the producer. It schedules the producer first automatically, so you never write dependsOn and can't forget it — the data dependency is the task dependency.
  • Why does a hardcoded absolute output path hurt the build cache?
    Absolute paths are machine-specific. They prevent relocation, so a cache entry produced on CI (one checkout path) can't be restored to a developer's different path. Using layout.buildDirectory keeps paths relative and relocatable.

saying these in an interview costs you the question

  • Claiming raw File inputs are fine because the path is correct — they skip dependency inference and lazy resolution.
  • Capturing mutable project state (like project.version) eagerly at configuration time into a property.
  • Hardcoding absolute paths in outputs.file().

context