How do you tell whether a piece of build-script code runs at configuration time or execution time, and why does it matter?
answer
- script body + config block = config phase
- doLast/doFirst/@TaskAction = execution
- .get() forces config-time evaluation
- println test: fires without running task = config
- push work into actions or lazy providers
basics
~20 sCode directly in the script body or in a task's configuration block runs at configuration time, on every build. Code inside doFirst/doLast/@TaskAction runs at execution time, only when the task runs. It matters because configuration-time work slows every command.
solid answer
~50 sThe rule of thumb: if code is in the **script body** or a **task configuration block** (the closure passed to `register`/`named`), it runs during the **configuration phase** — evaluated every build, for every project. If code is inside a task **action** (`doFirst {}`, `doLast {}`, an `@TaskAction` method), it runs during the **execution phase**, only if that task is actually executed. The classic mistake is doing real work (reading files, calling `project.exec`, resolving a configuration, computing values) directly in the script body; that work then runs on *every* invocation, even `gradle help`, multiplied across modules. The fix is to push such work into task actions or wrap it in lazy `Provider`/`Property` so it's computed only when the value is needed. You can confirm phase by adding a `println` and running a task that does *not* execute the task in question — anything that still prints ran at configuration time.
code
kotlin · 5 linestasks.register("stamp") {
val sha = providers.exec { commandLine("git", "rev-parse", "HEAD") }
.standardOutput.asText // lazy provider, not resolved yet
doLast { println(sha.get()) } // resolved only at execution
}go deeper
Match the placement to the phase: body/config block = configuration, doLast/@TaskAction = execution.
Explain the per-build cost of configuration-time work and the .get() laziness pitfall; demonstrate with the println test.
Diagnose with --profile/build scans and refactor eager script-body work into providers/actions across a codebase.
Codify in convention plugins that script bodies stay declarative and lazy; review configuration-time budget as a first-class build-health metric.
## The two phases that matter here - **Configuration phase:** Gradle evaluates each `build.gradle.kts` to build the task model. The *script body executes as code* here. - **Execution phase:** Gradle runs the actions of the tasks it was asked to run. ## The placement rule Where you put code decides its phase: | Placement | Phase | |---|---| | Top-level script body statements | Configuration | | Block passed to `tasks.register("x") { ... }` / `tasks.named("x") { ... }` | Configuration (when realized) | | Setting task properties (`x.outputDir = ...`) | Configuration | | `doFirst { }`, `doLast { }` | Execution | | A custom task's `@TaskAction fun run()` | Execution | ## Demonstration ```kotlin println("1: script body") // configuration tasks.register("build-report") { println("2: configuring report") // configuration (on realization) val out = layout.buildDirectory.file("r.txt") doLast { println("3: writing report") // execution out.get().asFile.writeText("done") } } ``` Run `gradle help`: you see `1` (always) and possibly `2` if the task is realized, but **never** `3` because `build-report` isn't executed. Run `gradle build-report`: you see `3` as well. ## Why it matters Configuration-time code is paid on **every** build invocation and for **every** project (default full configuration). So a single innocent-looking line of real work in a script body becomes a tax on all commands: ```kotlin // ANTI-PATTERN: eager, runs every build for every module val gitSha = providers.exec { commandLine("git", "rev-parse", "HEAD") } .standardOutput.asText.get() // .get() forces it NOW, at config time // BETTER: keep it a lazy Provider; resolve only when a task needs it val gitShaProvider = providers.exec { commandLine("git", "rev-parse", "HEAD") } .standardOutput.asText // no .get() — stays deferred ``` Calling `.get()` on a `Provider` in the script body collapses laziness back to configuration time — a common subtle source of slow builds. ## How to diagnose - Add a `println` and observe whether it fires for a command that doesn't run the task — if so, it's configuration-time. - Use a build scan or `--profile` to see configuration vs execution time breakdown. - Look for `.get()`/`.forEach`/`exec`/file I/O in script bodies — usual culprits. ## Takeaway The phase is determined by *placement*, not intent. Keep script bodies cheap and declarative; do the actual work inside task actions or behind lazy providers so it only runs when truly needed.
- Why is calling `.get()` on a Provider in the script body a problem?It forces the provider to resolve immediately, at configuration time, every build — defeating the laziness. Defer `.get()` into a task action so the value is computed only when the task actually runs.
- How would you prove a given line runs at configuration time?Put a println on it and run a command that does not execute the owning task (e.g. `gradle help`). If it still prints, the line ran during configuration.
saying these in an interview costs you the question
- Thinking placement inside `register { }` makes code run at execution — that block is configuration.
- Believing the phase depends on what the code *does* rather than where it sits.
- Doing file I/O / exec / configuration resolution in the script body and calling it harmless.