Why can running external processes break Gradle's configuration cache, and how do ExecOperations and an Exec task differ in this regard?
answer
- config cache serializes configured graph
- no live side effects / process exec at config time
- no Project reference at execution time
- Exec task forks in its action -> safe
- @Inject ExecOperations in custom tasks
- providers.exec {} for cache-safe config values
basics
~20 sRunning a process at configuration time isn't cacheable, so the configuration cache rejects it. Do process work at execution time — inside an Exec task's action, or via the injected ExecOperations service in a custom task — not in the configuration phase.
solid answer
~40 sThe configuration cache stores the *result of the configuration phase* and replays it, so anything that reads live external state during configuration (a `project.exec {}` call, `"cmd".execute()`, file or env reads at config time) invalidates the model and is flagged as a problem. The fix is to defer process execution to the **execution phase**. A built-in `Exec` task already does this — its `commandLine`/`workingDir` are just lazily-evaluated configuration, and the fork happens in the task action. For custom tasks you must not reference `project` at execution time (also a cache violation); instead inject `ExecOperations` via `@Inject` in the task's constructor and call `execOperations.exec { commandLine(...) }` inside the `@TaskAction`. Same for `ProviderFactory.exec`/`environmentVariable` when you legitimately need a value at configuration time in a cache-safe way.
code
kotlin · 16 linesabstract class GenerateTask : DefaultTask() {
@get:Inject
abstract val execOps: ExecOperations
@TaskAction
fun run() {
execOps.exec {
commandLine("protoc", "--version")
}
}
}
// Cache-safe command output as a configuration value:
val gitSha = providers.exec {
commandLine("git", "rev-parse", "--short", "HEAD")
}.standardOutput.asText.map { it.trim() }go deeper
Know that running commands directly in the build script can cause configuration-cache warnings; prefer an Exec task.
Explain that the cache stores the configured graph, so process execution belongs in the execution phase via an Exec task.
Distinguish config-time vs execution-time exec, inject ExecOperations in custom tasks, and use providers.exec for cache-safe config values.
Set org-wide conventions for cache-compatible builds: ban config-time exec/project references, standardize ExecOperations/providers.exec usage, and gate the configuration cache in CI.
## What the configuration cache is Gradle's **configuration cache** serializes the fully-configured task graph after the configuration phase and reuses it on later builds, skipping configuration entirely. For this to be sound, configuration must be **deterministic and free of live side effects** — it can't run processes, read mutable environment, or capture `Project` objects, because those can't be faithfully cached and replayed. ## Why exec breaks it Two distinct mistakes: 1. **Executing a process at configuration time.** Calling `project.exec { ... }` or `"git rev-parse HEAD".execute()` directly in a build script body (not inside a task action) runs during configuration. The configuration cache reports this as a problem because the result isn't a cacheable value — it's a live side effect. 2. **Holding a reference to `Project` (or `Task`, `Gradle`, etc.) at execution time.** Even custom tasks that try to run a process via `project.exec` from inside `@TaskAction` are invalid, because tasks must not reference `project` when they execute — `project` isn't serializable into the cache. ## The Exec task is fine A built-in `Exec` task is configuration-cache compatible because: - Its properties (`commandLine`, `args`, `workingDir`, `environment`) are configuration, captured as serializable values. - The actual process fork happens in the task's **action**, during the execution phase, not during configuration. So `tasks.register<Exec>(...) { commandLine(...) }` is the cache-safe way to shell out as a graph node. ## ExecOperations for custom tasks When you write your own task type and need to run a process in its action, inject the **`ExecOperations`** service: ```kotlin abstract class GenerateTask : DefaultTask() { @get:Inject abstract val execOps: ExecOperations @TaskAction fun run() { execOps.exec { commandLine("protoc", "--version") } } } ``` `ExecOperations` is a managed service Gradle can supply without a `Project` reference, so it's safe under the configuration cache. There's an analogous `FileSystemOperations` for copy/delete in custom tasks. ## Cache-safe values at configuration time If you genuinely need the *output* of a command as a configuration value (e.g. the git SHA to stamp into a manifest), use `ProviderFactory`: ```kotlin val gitSha = providers.exec { commandLine("git", "rev-parse", "--short", "HEAD") }.standardOutput.asText.map { it.trim() } ``` This returns a `Provider` whose value Gradle resolves in a cache-aware way (and re-checks for invalidation), rather than running the process eagerly during configuration. ## Takeaways - Built-in `Exec` task: safe — process runs at execution time. - Custom task running a process: inject `ExecOperations`, never touch `project` at execution time. - Need a command's output as config: use `providers.exec { ... }`, not a raw config-time exec.
- Why is a built-in Exec task configuration-cache compatible but a top-level project.exec {} in the script body is not?The Exec task only stores configuration (commandLine, workingDir) and forks the process in its execution-phase action. A top-level project.exec {} runs during configuration, performing a live side effect that the cache can't capture or replay.
- How do you run a process from inside a custom task action without violating the cache?Inject the ExecOperations service via @Inject in the task constructor/abstract property and call execOps.exec { ... } in the @TaskAction — never reference project at execution time.
- You need the git SHA as a build input. What's the cache-safe way to get it?Use providers.exec { commandLine("git", "rev-parse", "HEAD") }.standardOutput.asText, mapping to a Provider, instead of running the command eagerly at configuration time.
saying these in an interview costs you the question
- Calling project.exec {} or "cmd".execute() in the script body / at configuration time.
- Referencing project from inside a @TaskAction to run a process.
- Claiming the configuration cache forbids external processes entirely — it forbids them at config time, not in task actions.