skip to content

A custom task captures `project` in a `doLast { }` closure. Walk through diagnosing and fixing this for the configuration cache.

level: middleimportance: should knowfreq 50%

answer

  1. doLast closure captures live Project
  2. run --configuration-cache, read HTML report
  3. capture into locals before doLast
  4. layout.buildDirectory replaces project.buildDir
  5. promote to typed task with @Input/@Output

basics

~20 s

The closure runs at execution time but holds a reference to the non-serializable Project, so the cache reports a problem. Capture the value you need into a local val (or task property) during configuration, then reference that local inside doLast.

solid answer

~40 s

When you write `doLast { project.something }`, the closure is a task action that runs at **execution time**, but it captures the enclosing `project` reference, which the configuration cache cannot serialize. Diagnosis: run with `--configuration-cache` and read the problems report — it names the task and says invocation of `Task.project` (or capturing `Project`) at execution time is unsupported. Fix: pull every Project-derived value out of the closure during configuration. Replace `project.buildDir`, `project.version`, `project.exec`, etc. with locals captured before `doLast`, or better, with lazy provider/property types and injected services (`ProjectLayout`, `ExecOperations`). The closure should then close over plain serializable values/providers only. For a properly modeled task, prefer promoting those into `@Input`/`@OutputFile` properties on a typed task class rather than ad-hoc `doLast` blocks, which also gives you up-to-date checking for free.

code

kotlin · 8 lines
kotlin
// Diagnose then fix: capture before doLast
tasks.register("dump") {
    val out = layout.buildDirectory.file("out.txt") // captured Provider
    val ver = project.version.toString()            // read at config time
    doLast {
        out.get().asFile.writeText(ver)             // closes over locals only
    }
}

go deeper

for a junior

Recognize that project shouldn't appear inside doLast and that you read values earlier instead.

for a middle

Diagnose via the report, capture values/providers into locals, know layout.buildDirectory etc.

for a senior

Promote ad-hoc blocks to typed tasks with declared inputs/outputs; reason about serialization of captured state.

for a principal

Discourage ad-hoc doLast glue in shared plugins; standardize typed, cache-compatible tasks across the build.

## The trap: closures capture `project` In an ad-hoc task, `doLast { ... }` registers an **execution-time** action. Kotlin/Groovy closures capture their lexical scope, so referencing `project`, `version`, `buildDir`, or calling `file(...)`/`exec(...)` inside the block captures the **live Project**. The configuration cache serializes the task graph including its actions; a captured Project can't be serialized, so you get a problem. ## Step 1 — reproduce and read the report ```bash ./gradlew myTask --configuration-cache ``` Gradle prints something like: ``` 1 problem was found storing the configuration cache. - Task `:myTask`: invocation of 'Task.project' at execution time is unsupported. See the complete report at build/reports/configuration-cache/.../configuration-cache-report.html ``` The HTML report links each problem to the offending line. ## Step 2 — capture at configuration time The minimal fix: read what you need **before** `doLast`, into locals, then close over the locals. ```kotlin // BEFORE — captures Project tasks.register("dump") { doLast { file("${project.buildDir}/out.txt").writeText(project.version.toString()) } } // AFTER — closes over captured providers/values only tasks.register("dump") { val outFile = layout.buildDirectory.file("out.txt") // Provider<RegularFile> val ver = project.version.toString() // plain String captured now doLast { outFile.get().asFile.writeText(ver) // no Project reference } } ``` Note `layout.buildDirectory` (from the injected `ProjectLayout`) replaces `project.buildDir`, and `project.version` is read **at configuration time** into a plain `String`. ## Step 3 — for non-trivial tasks, model a typed task Ad-hoc `doLast` blocks are fine for tiny glue, but a real task should be a class with declared inputs/outputs: ```kotlin abstract class DumpTask : DefaultTask() { @get:Input abstract val version: Property<String> @get:OutputFile abstract val outputFile: RegularFileProperty @TaskAction fun run() = outputFile.get().asFile.writeText(version.get()) } tasks.register<DumpTask>("dump") { version.set(provider { project.version.toString() }) outputFile.set(layout.buildDirectory.file("out.txt")) } ``` This is config-cache compatible **and** gives correct up-to-date checks and input fingerprinting. ## Checklist for closures - No `project`, `this.project`, or implicit Project methods (`file`, `exec`, `copy`, `version`, `buildDir`) inside `doLast`/`doFirst`. - Capture concrete values or providers into locals during configuration. - Replace Project services with injected equivalents. - When it grows beyond trivial, promote to a typed task with `@Input`/`@Output` properties.

  • Why does capturing `project.version` into a local String during configuration fix the problem, but referencing `project.version` inside doLast doesn't?
    Reading `project.version` at configuration time produces a plain serializable String captured into the action's state. Referencing it inside doLast captures the Project itself and reads at execution time, which the cache can't serialize.
  • Where does Gradle report which line caused the problem?
    In the configuration-cache HTML report under build/reports/configuration-cache/, linked from the console summary; it lists each problem with the task and a stack/location pointing to the offending access.

saying these in an interview costs you the question

  • Capturing the whole `project` into a local val — that still serializes the Project and fails.
  • Assuming `doFirst` is safe while `doLast` isn't — both run at execution time.
  • Disabling the configuration cache for that project instead of fixing the closure.

context