Show how to write a conditional `outputs.upToDateWhen` predicate (not just `false`), and explain when the predicate is evaluated.
answer
- Spec<Task> returns boolean
- evaluated at execution, before actions
- runs every build — keep cheap
- ANDed with fingerprints
- read live state inside closure
basics
~10 soutputs.upToDateWhen { task -> someCondition } returns a boolean from a Spec<Task>. It's evaluated at execution time, during the up-to-date check, before the task's actions run.
solid answer
~50 s`outputs.upToDateWhen` takes a `Spec<? super Task>` — a closure returning a boolean. Returning a *condition* (not a constant) lets the task be up-to-date only in some situations: ```kotlin tasks.named("generate") { outputs.upToDateWhen { t -> val marker = t.project.layout.buildDirectory.file("marker.txt").get().asFile marker.exists() && marker.readText() == expectedVersion } } ``` Gradle evaluates the predicate **during task graph execution, at the up-to-date check for that task**, after inputs/outputs are snapshotted but before the task actions. The predicate runs on every build that reaches the task. Because it executes per build, keep it cheap and side-effect-free — don't do heavy I/O or mutate state there. It composes with input/output fingerprinting: the task is up-to-date only if the fingerprints match AND every predicate returns true. A common mistake is reading configuration-time values that capture stale state; prefer reading live state inside the closure so the decision reflects the current build.
code
kotlin · 9 linestasks.register("generate") {
val marker = layout.buildDirectory.file("marker.txt")
outputs.upToDateWhen {
val f = marker.get().asFile
// up-to-date only while marker matches expected version
f.exists() && f.readText() == "v2"
}
doLast { /* regenerate, then write marker */ }
}go deeper
Know it takes a closure returning a boolean and that false forces reruns.
Explain it's a Spec<Task> evaluated at execution before actions, runs every build, and is AND-ed with fingerprints.
Discuss configuration-time capture pitfalls, cost of the predicate, and configuration-cache serialization constraints.
Set guidance: predicates must be cheap, pure, and configuration-cache-safe; prefer modeled inputs over bespoke freshness logic.
## The Spec signature `outputs.upToDateWhen(Spec<? super Task>)` accepts a predicate. In Kotlin/Groovy DSL it's a lambda receiving the task and returning `Boolean`: ```kotlin outputs.upToDateWhen { task -> /* boolean */ } ``` ## When does it run? Gradle has distinct phases: **configuration** (build scripts/`register`/`configure` blocks run) and **execution** (tasks in the graph run). The `upToDateWhen` closure is registered during configuration but **invoked during execution**, specifically at the moment Gradle decides whether to skip the task — after it snapshots declared inputs and outputs, before it runs the task's `doFirst/doLast` actions. It is called every time the task is part of the executed graph. ## Constant vs. conditional - `{ false }` ⇒ never up-to-date (always run) — the side-effect escape hatch. - `{ true }` ⇒ defer entirely to input/output fingerprinting (this is the default behavior anyway). - `{ condition }` ⇒ additional, custom freshness logic *on top of* fingerprinting. The AND means a conditional predicate can only make a task run *more* often, never skip when fingerprints differ. ## Pitfalls 1. **Configuration-time capture**: `val now = System.currentTimeMillis()` captured outside the closure freezes at configuration time. Read live state *inside* the lambda. 2. **Heavy work**: the predicate runs every build; expensive network/disk checks slow every invocation. 3. **Side effects**: don't mutate files or state in the predicate — it's a decision function, and may run under configuration cache constraints. 4. **Configuration cache**: the closure must capture only serializable/permitted state; reference `task`/providers rather than the `project` where possible. ## Example: rerun only when an external marker is stale ```kotlin tasks.register("syncSchema") { val markerProvider = layout.buildDirectory.file("schema.hash") outputs.upToDateWhen { val f = markerProvider.get().asFile f.exists() && f.readText() == currentRemoteHash() } doLast { /* fetch + write marker */ } } ``` Here the task skips only while the recorded hash matches the remote; otherwise it reruns.
- Can an `upToDateWhen` predicate that returns true force a task to be skipped even if its inputs changed?No. The predicate is AND-ed with input/output fingerprinting. If fingerprints differ, the task runs regardless of the predicate. The predicate can only add reasons to rerun, not override changed inputs.
- Why is capturing a value at configuration time a bug here?Configuration runs once (and may be skipped by the configuration cache). A value captured there is frozen and won't reflect the live state at execution, so the up-to-date decision becomes stale or wrong. Read state inside the closure instead.
saying these in an interview costs you the question
- Doing heavy I/O or network calls inside the predicate — it runs on every build.
- Claiming a `true` predicate can skip a task whose inputs changed — fingerprinting still forces the rerun.