How do you run an external process during configuration in a config-cache-safe way using ProviderFactory?
answer
- providers.exec returns ExecOutput
- standardOutput.asText is a Provider
- .result for exit code
- project.exec is NOT config-cache safe
- git SHA capture; capture Provider not Project
basics
~10 sUse 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 sCalling `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 linesval gitSha = providers.exec {
commandLine("git", "rev-parse", "--short", "HEAD")
}.standardOutput.asText.map { it.trim() }
version = "1.0.0+${'$'}{gitSha.get()}"go deeper
Recognize that there is a providers.exec for running commands lazily; details optional.
Know it returns ExecOutput with standardOutput.asText as a Provider and is preferred over project.exec.
Explain config-cache compatibility, .result for exit codes, capturing the Provider not Project, and the ValueSource generalization.
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