skip to content

Why can running external processes break Gradle's configuration cache, and how do ExecOperations and an Exec task differ in this regard?

level: seniorimportance: should knowfreq 22%

answer

  1. config cache serializes configured graph
  2. no live side effects / process exec at config time
  3. no Project reference at execution time
  4. Exec task forks in its action -> safe
  5. @Inject ExecOperations in custom tasks
  6. providers.exec {} for cache-safe config values

basics

~20 s

Running 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 s

The 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 lines
kotlin
abstract 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

for a junior

Know that running commands directly in the build script can cause configuration-cache warnings; prefer an Exec task.

for a middle

Explain that the cache stores the configured graph, so process execution belongs in the execution phase via an Exec task.

for a senior

Distinguish config-time vs execution-time exec, inject ExecOperations in custom tasks, and use providers.exec for cache-safe config values.

for a principal

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.

context