What is a ValueSource and when would you use one to make a build configuration-cache compatible?
answer
- obtain() reads external state, result tracked
- providers.of(Source::class) { parameters {} }
- inject ExecOperations, never Project, inside obtain
- Gradle may re-run obtain() to check changes => keep cheap
- first choice for git/CLI output stamps
basics
~20 sA ValueSource wraps a read of external state (like a git command's output) so its result is tracked as a build input. You use it when no built-in provider (environmentVariable, systemProperty, fileContents) covers the source.
solid answer
~50 sA `ValueSource<T, P : ValueSourceParameters>` is Gradle's general mechanism for obtaining an external value in a configuration-cache-compatible way. You implement `obtain(): T`, declaring parameters via the parameter type, and register it with `providers.of(MySource::class) { parameters { ... } }`, which returns a `Provider<T>`. Its result is treated as a tracked input, so the cache stays correct instead of baking in an untracked value. The classic use is shelling out — e.g., running `git rev-parse HEAD` for a build stamp — where `System.getenv`/`fileContents` don't apply. Two subtleties: `obtain()` runs isolated from the Project (use injected services like `ExecOperations` inside it, never `Project`), and Gradle may **re-execute** the ValueSource to check whether its value changed, so it should be cheap or model its real inputs as parameters. ValueSources also let a value be read without forcing the configuration cache to invalidate just because the source object was touched.
code
kotlin · 16 linesabstract class CmdSource : ValueSource<String, CmdSource.Params> {
interface Params : ValueSourceParameters {
val args: ListProperty<String>
}
@get:Inject abstract val exec: ExecOperations
override fun obtain(): String {
val o = java.io.ByteArrayOutputStream()
exec.exec { commandLine(parameters.args.get()); standardOutput = o }
return o.toString().trim()
}
}
val sha = providers.of(CmdSource::class) {
parameters { args.set(listOf("git", "rev-parse", "--short", "HEAD")) }
}
tasks.register("stamp") { val s = sha; doLast { println(s.get()) } }go deeper
Awareness that a ValueSource exists for reading external values in a cache-safe way; details optional.
Recognize when built-in providers don't fit and that a ValueSource is the next step; read a basic example.
Implement one with parameters and injected ExecOperations; reason about re-evaluation cost and isolation.
Decide ValueSource vs BuildService at design level; provide shared, reusable sources (e.g., git stamp) across the build.
## The gap ValueSource fills The provider API gives you tracked readers for the common cases — env vars, system properties, Gradle properties, file contents. But builds often need values from sources Gradle has no built-in provider for: the current git commit, the output of a CLI tool, a value computed from several files. Reading those directly (`"git rev-parse HEAD".execute()`, `new File(...).text`) is **untracked** and breaks the configuration cache. A **`ValueSource`** is the extensible escape hatch. You implement the interface, put your external read in `obtain()`, and Gradle wraps it as a tracked `Provider`. ## Anatomy ```kotlin abstract class GitHeadValueSource : ValueSource<String, GitHeadValueSource.Params> { interface Params : ValueSourceParameters { val projectDir: DirectoryProperty } @get:Inject abstract val execOps: ExecOperations // injected, cache-safe — NOT Project override fun obtain(): String { val out = java.io.ByteArrayOutputStream() execOps.exec { workingDir = parameters.projectDir.get().asFile commandLine("git", "rev-parse", "--short", "HEAD") standardOutput = out } return out.toString().trim() } } val gitHead: Provider<String> = providers.of(GitHeadValueSource::class) { parameters { projectDir.set(layout.projectDirectory) } } ``` - **Parameters** are declared via a `ValueSourceParameters` interface using managed property types (`Property`, `DirectoryProperty`, …). They become part of the source's identity/inputs. - **Injected services** (`ExecOperations`, `ProviderFactory`, etc.) are obtained with `@Inject` — you must not reach for the Project inside `obtain()`. - The returned `Provider<String>` is wired into task `@Input` properties like any other provider. ## How tracking works The ValueSource's obtained value is recorded as a configuration input. Importantly, Gradle may **re-run `obtain()`** on subsequent builds to decide if the value (and therefore the cache entry) is still valid. This has two consequences: 1. **Keep `obtain()` cheap / deterministic-ish.** A heavy computation re-run on every build hurts performance. Model genuine inputs as parameters so Gradle can reason about them. 2. **Don't rely on side effects.** `obtain()` is for *reading* a value, not for doing build work. ## ValueSource vs. the alternatives - **`providers.environmentVariable` / `systemProperty` / `fileContents`** — use these first; they're cheaper and purpose-built. - **`ValueSource`** — when the source is custom (CLI output, derived value). Tracked, isolated, parameterizable. - **`BuildService`** — when you need *shared, possibly mutable* state or a resource (a connection pool, a counter) across tasks, not just a one-shot read. ## Bottom line Reach for a ValueSource when you must read external state the built-in providers don't cover, you want that read tracked by the configuration cache, and you can express it as a pure-ish `obtain()` using injected services rather than the Project.
- Why must you not access the Project inside a ValueSource's obtain()?obtain() runs isolated so the value can be safely tracked and re-evaluated; the Project is live, non-serializable build state. You inject the services you need (ExecOperations, ProviderFactory) via @Inject instead.
- Gradle re-runs obtain() on some builds — what's the performance implication?Re-running is how Gradle checks whether the external value changed to validate the cache entry. A costly obtain() therefore runs on builds that hit the cache. Keep it cheap and push real inputs into parameters so changes are detectable without heavy work.
- When would a BuildService be the better choice over a ValueSource?When you need shared, possibly mutable state or a managed resource across multiple tasks (e.g., a counter, a connection, a server), with optional lifecycle and concurrency limits — not a one-shot tracked read of an external value.
saying these in an interview costs you the question
- Using `"...".execute()` / Project.exec inside obtain() — defeats isolation.
- Putting expensive work in obtain() and ignoring that Gradle may re-run it.
- Treating ValueSource as a place to perform build actions/side effects rather than read a value.