skip to content

How do you run an external process during configuration in a config-cache-safe way using ProviderFactory?

level: seniorimportance: should knowfreq 45%

answer

  1. providers.exec returns ExecOutput
  2. standardOutput.asText is a Provider
  3. .result for exit code
  4. project.exec is NOT config-cache safe
  5. git SHA capture; capture Provider not Project

basics

~10 s

Use providers.exec { commandLine(...) } which returns an ExecOutput whose standardOutput is a Provider. It is lazy and tracked as a configuration-cache input, unlike project.exec.

solid answer

~40 s

Calling `project.exec { ... }` at configuration time runs eagerly and is **not** config-cache compatible. Instead use `providers.exec { spec -> spec.commandLine("git", "rev-parse", "HEAD") }`, which returns an `ExecOutput`. From it you get lazy providers: `.standardOutput.asText` (`Provider<String>`), `.standardError.asText`, and `.result` (the exit code). The process is only run when one of those providers is queried, and the invocation + output are recorded as configuration-cache inputs, so the cache stays correct. You typically `.map { it.trim() }` the output. Example: deriving a build's Git commit. For arbitrary non-process external reads (REST call, reading a file with custom logic), the more general tool is `providers.of(MyValueSource::class) { ... }`, which gives the same config-cache-safe, lazy semantics.

code

kotlin · 5 lines
kotlin
val gitSha = providers.exec {
    commandLine("git", "rev-parse", "--short", "HEAD")
}.standardOutput.asText.map { it.trim() }

version = "1.0.0+${'$'}{gitSha.get()}"

go deeper

for a junior

Recognize that there is a providers.exec for running commands lazily; details optional.

for a middle

Know it returns ExecOutput with standardOutput.asText as a Provider and is preferred over project.exec.

for a senior

Explain config-cache compatibility, .result for exit codes, capturing the Provider not Project, and the ValueSource generalization.

for a principal

Set guidance to ban project.exec in build logic and standardize ValueSource for external reads, weighing per-build cost.

## Why project.exec is a problem The classic way to capture, say, a Git SHA was: ```groovy def sha = 'git rev-parse HEAD'.execute().text.trim() ``` or `project.exec { ... }`. Both run **immediately at configuration time** and are invisible to Gradle. Under the configuration cache that means the SHA gets frozen into the cached configuration and never updates — and Gradle warns/fails because `project.exec` isn't config-cache compatible. ## providers.exec — the lazy, tracked replacement `ProviderFactory.exec { ExecSpec }` returns an `ExecOutput`. It does **not** run the process when you call it; it runs when you query one of its providers: - `execOutput.standardOutput.asText` → `Provider<String>` - `execOutput.standardError.asText` → `Provider<String>` - `execOutput.result` → `Provider<ExecResult>` (exit code, and lets you assert success) Because the result is a Provider, the read is deferred and tracked as a configuration-cache input. The command and its output become part of what the cache checks. ```kotlin val gitSha: Provider<String> = providers.exec { commandLine("git", "rev-parse", "--short", "HEAD") }.standardOutput.asText.map { it.trim() } tasks.register("printSha") { val sha = gitSha // capture the Provider, not project doLast { println(sha.get()) } } ``` Note we capture the `Provider`, not the `Project`, so the task stays config-cache safe. ## When you need more than a process: ValueSource `providers.exec` is specialized for processes. For *any* external read you want to be lazy + tracked (HTTP call, parsing a file with custom logic, querying the OS), implement a `ValueSource<T, P>` and obtain it with `providers.of(MySource::class) { parameters.* = ... }`. Same guarantees: lazy, isolated, config-cache-friendly. ## Key cautions - The process can still run *every build* if you query it each time; if it is expensive, consider caching the value or gating it. - `result.assertNormalExitValue()` (via `.result`) is how you surface non-zero exits, since failures are otherwise deferred. - Don't mix in `project.exec` — that reintroduces the incompatibility.

  • How do you detect a non-zero exit code from providers.exec?
    Query .result (Provider<ExecResult>) and call assertNormalExitValue(), or inspect getExitValue(); the standardOutput provider alone won't surface failures.
  • What if you need something more general than running a process?
    Implement a ValueSource<T,P> and call providers.of(...). It gives the same lazy, config-cache-tracked behavior for arbitrary external reads.
  • Does providers.exec run the command once and cache it across builds?
    No — it runs whenever the provider is queried in a build (subject to config-cache reuse). It is not a persistent result cache; cache it yourself if expensive.

saying these in an interview costs you the question

  • Recommending project.exec or 'cmd'.execute() at configuration time
  • Assuming providers.exec memoizes the result across builds
  • Forgetting that errors surface only via .result, not standardOutput

context