skip to content

Why does the configuration cache forbid accessing the Project object at execution time, and what error do you see when a task does it?

level: middleimportance: must knowfreq 70%

answer

  1. Project is live/mutable, not serializable
  2. skip configuration phase => capture inputs
  3. 'Task.project at execution time is unsupported'
  4. inject ProjectLayout/ExecOperations/FileSystemOperations
  5. capture at config, read property at execution

basics

~20 s

The configuration cache stores the task graph and reuses it without re-running configuration. Tasks must not touch the live Project at execution time; doing so triggers a configuration-cache problem because Project cannot be serialized into the cached graph.

solid answer

~40 s

Gradle's configuration cache serializes the configured task graph so later builds skip the configuration phase entirely. A `Project` is a live, configuration-phase object tied to the build model; it is mutable, references the whole project graph, and cannot be safely captured and replayed. So any reference to `Project` (including `task.getProject()` or capturing `project` in a `doLast {}` closure) reaching into execution time is a configuration-cache problem. Gradle reports it as `invocation of 'Task.project' at execution time is unsupported`. The fix is to capture the concrete values you need during configuration into task `@Input`/`@Internal` properties (ideally lazy `Provider`/`Property` types), or use a supported provider API like `providers.environmentVariable`, a `ValueSource`, or a `BuildService`. The task body then reads only those captured inputs, never the Project.

code

kotlin · 23 lines
kotlin
// BAD: touches Project at execution time -> config-cache problem
abstract class BadTask : DefaultTask() {
    @TaskAction
    fun run() {
        val out = project.buildDir.resolve("out.txt") // project access at execution!
        out.writeText("hi")
    }
}

// GOOD: capture inputs at configuration time via injected services
abstract class GoodTask : DefaultTask() {
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun run() {
        outputFile.get().asFile.writeText("hi") // no Project reference
    }
}

tasks.register<GoodTask>("good") {
    outputFile.set(layout.buildDirectory.file("out.txt"))
}

go deeper

for a junior

Know that tasks shouldn't use project inside doLast/@TaskAction, and that the cache stores the task graph.

for a middle

Explain why Project isn't serializable, recognize the exact error, and fix by capturing inputs into properties.

for a senior

Reach for injected services (ProjectLayout/ExecOperations/FileSystemOperations) and lazy Provider/Property types; reason about isolation.

for a principal

Set conventions so plugin/task authors are config-cache compatible by default; gate it in CI and steer the org's migration.

## Why Project is off-limits at execution time Gradle has two phases: **configuration** (build scripts run, tasks are registered and wired) and **execution** (task actions run). The **configuration cache** captures the fully-configured task graph at the end of configuration, serializes it to disk, and on the next compatible invocation **skips configuration entirely**, deserializing the graph and running tasks directly. For this to work, everything a task needs at execution time must be **captured as serializable state** inside the task. The `Project` object is the opposite of that: it is the live, mutable entry point to the entire build model (other projects, configurations, the file system API, the task container). It cannot be meaningfully serialized and replayed, and reaching through it would let one task mutate global build state — breaking the isolation the cache depends on. ## What triggers the problem Any path that reads `Project` while a task **action** is running: - `task.getProject()` / `project` inside a `doLast { }` or `doFirst { }` block - Capturing `project` in a closure/lambda that runs at execution time - Calling `project.file(...)`, `project.exec(...)`, `project.buildDir`, `project.property(...)`, etc. from a `@TaskAction` Gradle emits a problem like: ``` 1 problem was found storing the configuration cache. - Task `:myTask` of type `MyTask`: invocation of 'Task.project' at execution time is unsupported. ``` ## How to fix it — capture at configuration time The rule: **read the Project during configuration, store the concrete value in a property, read the property at execution time.** Prefer lazy provider types so the value resolves correctly and participates in up-to-date checks. - File system: inject `ProjectLayout` (`layout.buildDirectory`, `layout.projectDirectory`) and capture `DirectoryProperty`/`RegularFileProperty`. - Running processes: inject `ExecOperations` instead of `project.exec`. - File operations: inject `FileSystemOperations` instead of `project.copy`/`project.delete`. - Environment variables / system properties: use `providers.environmentVariable(...)` / `providers.systemProperty(...)`. - Arbitrary external state: wrap it in a `ValueSource`. - Shared mutable state across tasks: use a `BuildService`. These **injected services** (`@Inject`-ed `ObjectFactory`, `ProjectLayout`, `ExecOperations`, `FileSystemOperations`, `ProviderFactory`) are configuration-cache safe substitutes for the corresponding `Project` methods. ## Mental model Configuration time = "plan the build, gather all inputs." Execution time = "do the work using only what was gathered." The Project belongs to planning; it must never leak into doing.

  • Name two injected services that replace common Project methods, and what they replace.
    `ProjectLayout` replaces `project.buildDir`/`project.file` (gives `layout.buildDirectory`, project/file paths); `ExecOperations` replaces `project.exec`/`project.javaexec`; `FileSystemOperations` replaces `project.copy`/`project.delete`. They are obtained via constructor `@Inject` and are cache-safe.
  • Is reading the Project at configuration time also forbidden?
    No. Configuration-time Project access is fine — that is exactly when you should read it and capture concrete values into task properties. The cache only forbids Project access during task execution (and during the build that is serialized).

Configuration time is packing a suitcase; execution time is the trip. You can't phone home mid-trip for things you forgot — you pack everything you'll need (into properties) before you leave.

saying these in an interview costs you the question

  • Saying Project access is always banned — it's only banned at execution time.
  • Claiming you must disable the configuration cache to use Project — you migrate the API instead.
  • Confusing this with up-to-date checks; the issue is serializability/isolation, not staleness.

context