Why is `new File(project.buildDir, "out.txt")` (or project.buildDir) discouraged, and what breaks with it under lazy configuration and the configuration cache?
answer
- buildDir deprecated
- eager File resolution
- no Provider tracking
- config cache forbids Project at exec
- inject ProjectLayout, read provider.get()
basics
~10 sproject.buildDir resolves the path eagerly at configuration time, isn't a Provider, and references the Project at execution. That breaks build-dir relocation, up-to-date tracking, and the configuration cache. Use layout.buildDirectory.file()/.dir() instead.
solid answer
~40 s`project.buildDir` (deprecated) and `new File(buildDir, ...)` resolve to an absolute `java.io.File` during *configuration*. Three problems follow. (1) **Eager resolution:** if the build directory is relocated after that line runs, your path is stale — lazy `layout.buildDirectory.file(...)` always resolves against the current base. (2) **No Provider semantics:** a raw `File` isn't tracked; Gradle can't fold it into input/output snapshots, so up-to-date checks and the build cache may misbehave unless you separately wire it. (3) **Configuration cache violations:** capturing `project` (or `buildDir`, which goes through `project`) inside a task action references the `Project` object at execution time, which the configuration cache forbids — the build fails with a 'invocation of Project at execution time' error. The fix is to inject/derive a `RegularFileProperty`/`DirectoryProperty` from `layout` at configuration time and read only `provider.get().asFile` inside the action.
code
kotlin · 11 lines// BAD — eager + touches project in the action
tasks.register("bad") {
doLast { File(project.buildDir, "r.txt").writeText("x") } // CC violation
}
// GOOD — lazy provider captured at config time
tasks.register("good") {
val out = layout.buildDirectory.file("r.txt")
outputs.file(out)
doLast { out.get().asFile.writeText("x") } // no project at exec
}go deeper
Know to use layout.buildDirectory instead of project.buildDir for output paths.
Explain eager vs lazy resolution and that buildDir is deprecated.
Articulate the configuration-cache rule (no Project at execution) and the capture-provider-at-config-time fix; inject ProjectLayout when needed.
Drive a migration off buildDir/project access across plugins to make the whole build configuration-cache compatible.
## The deprecated trio - `project.buildDir` — a mutable `File`, deprecated in favor of `layout.buildDirectory` (a `DirectoryProperty`). - `project.file(...)` / `new File(buildDir, ...)` — eager `File` resolution. - Reading any of these *inside a `@TaskAction`/`doLast`* — touches `project` at execution time. ## Problem 1 — eager resolution loses relocation ```kotlin // BAD: resolved now val f = File(project.buildDir, "reports/out.txt") // If someone later does layout.buildDirectory.set(...) this f is stale. ``` `layout.buildDirectory.file("reports/out.txt")` returns a `Provider<RegularFile>` that resolves *when queried*, so it always tracks the current build dir. ## Problem 2 — no input/output tracking A bare `File` isn't a Provider. When you assign it to `outputs.file(f)` it works, but you lose the laziness that lets convention defaults and overrides flow. Properly typed `RegularFileProperty` outputs integrate with up-to-date checks, `@CacheableTask`, stale-output cleanup, and parent-dir creation automatically. ## Problem 3 — the configuration cache The **configuration cache** serializes the task graph so configuration can be skipped on later runs. A hard rule: **tasks must not reference the `Project` object at execution time.** `project.buildDir`, `project.file(...)`, `project.layout` *accessed inside the action* all reach `project` and trigger: ``` Invocation of 'Task.project' at execution time is unsupported. ``` ### The fix pattern Capture providers at configuration time; read plain values at execution time: ```kotlin abstract class Report : DefaultTask() { @get:OutputFile abstract val out: RegularFileProperty // captured at config time @TaskAction fun run() { // no project access here — just the resolved file out.get().asFile.writeText("ok") } } tasks.register<Report>("report") { out.set(layout.buildDirectory.file("reports/out.txt")) } ``` If you need an arbitrary file path inside an action, inject `ProjectLayout` or `ObjectFactory` into the task (constructor `@Inject`) rather than calling `project` — injected services are configuration-cache-safe. ## Summary table | Eager (`buildDir`/`new File`) | Lazy (`layout.buildDirectory`) | |---|---| | Resolves at config time | Resolves when queried | | Stale on relocation | Follows relocation | | Not a Provider | Provider/Property, trackable | | Reading in action hits `project` | Resolved value, CC-safe |
- What error tells you a task touched Project at execution time under the configuration cache?"Invocation of 'Task.project' at execution time is unsupported." It appears when buildDir/project.file/project.layout are read inside an action.
- If you genuinely need ProjectLayout inside a task, how do you get it config-cache-safely?Inject it via the constructor: @Inject constructor(... layout: ProjectLayout) — or capture the specific Provider at configuration time and read it in the action.
- Does layout.buildDirectory replace the ability to change the build dir?Yes — it's a DirectoryProperty, so layout.buildDirectory.set(...) relocates it, and all derived providers follow.
saying these in an interview costs you the question
- Claiming buildDir is fine as long as you read it at configuration time only (it's still deprecated and not a Provider).
- Suggesting to access project.layout inside doLast/@TaskAction to 'stay lazy' — that still violates the configuration cache.
- Saying the configuration cache only cares about inputs, not Project references.