How does Gradle track the inputs of a CommandLineArgumentProvider, and what role does @Nested play?
answer
- @Nested = recurse into the object
- collection getter is @Nested on the task
- nested @Input/@InputFile become task inputs
- un-annotated → silently untracked
- asArguments() return value is NOT fingerprinted
basics
~10 sThe task's provider collection is annotated @Nested, so Gradle recursively scans each provider's own @Input/@InputFile properties and folds them into the task's input fingerprint. That's how dynamic args participate in up-to-date checks.
solid answer
~40 sGradle's process tasks declare their provider collections with `@Nested` (e.g. `Test.getJvmArgumentProviders()` is `@Nested`). `@Nested` tells Gradle: don't treat this object as a single opaque value — instead **recurse into it** and treat its annotated properties as if they were inputs of the owning task. So when you write a `CommandLineArgumentProvider` with `@get:InputFile`, `@get:Input`, `@get:Classpath`, etc., those nested properties become part of the **task's** input fingerprint. A change to the referenced file content (or input value) invalidates the task and busts the cache key, exactly as it should. Properties left un-annotated are **not tracked** — which is a common source of subtle cache bugs. The strings returned by `asArguments()` are *not themselves* fingerprinted as inputs; only the annotated properties are.
code
kotlin · 17 linesabstract class AgentArg : CommandLineArgumentProvider {
@get:Classpath
abstract val agentJar: ConfigurableFileCollection
@get:Input
abstract val agentOptions: Property<String>
override fun asArguments(): Iterable<String> =
listOf("-javaagent:${agentJar.singleFile}=${agentOptions.get()}")
}
tasks.named<Test>("test") {
val agent = objects.newInstance(AgentArg::class.java)
agent.agentJar.from(configurations.named("agent"))
agent.agentOptions.set("includes=com.acme.*")
jvmArgumentProviders.add(agent)
}go deeper
Know that the provider's annotated properties become the task's inputs and that @Nested is involved.
Explain @Nested recursion, the right annotation per kind of input, and the un-annotated-property trap.
Discuss fingerprint composition, why asArguments() output is excluded, and how this preserves relocatable cache entries.
Set a convention requiring every dynamic-arg provider to carry correct input annotations and a review checklist, since silent under-tracking is a fleet-wide correctness risk.
## The mechanism Gradle computes a task's *input fingerprint* by reflecting over the task's properties and reading their annotations (`@Input`, `@InputFile`, `@InputFiles`, `@InputDirectory`, `@Classpath`, `@CompileClasspath`, `@Nested`). The fingerprint is what feeds up-to-date checks and the build-cache key. `@Nested` is the recursion operator. When a property is annotated `@Nested`, Gradle doesn't hash the object directly — it **descends into the object** and reads *its* annotated getters, attaching them to the task's fingerprint under a nested namespace. It works on single objects and on iterables of objects. ## Why the provider collection is @Nested The process tasks declare, in their own source, something equivalent to: ```java @Nested public List<CommandLineArgumentProvider> getJvmArgumentProviders() { ... } ``` Because the collection is `@Nested`, every provider you add is scanned. So a provider like: ```kotlin abstract class AgentArg : CommandLineArgumentProvider { @get:Classpath abstract val agentJar: ConfigurableFileCollection @get:Input abstract val agentOptions: Property<String> override fun asArguments() = listOf("-javaagent:${agentJar.singleFile}=${agentOptions.get()}") } ``` contributes `agentJar` (as a classpath input) and `agentOptions` (as a value input) to the **Test task's** fingerprint. Swap the agent jar or change the options and the task reruns / misses the cache, correctly. ## The trap: un-annotated properties If you forget the annotation: ```kotlin abstract val configFile: RegularFileProperty // <-- no @InputFile! ``` Gradle ignores it. The argument still appears on the command line (because `asArguments()` returns it), but the file's content is **not** an input. You get false up-to-date results and stale cache hits. Build scans / `--info` won't obviously flag it; you discover it when a change "doesn't take." ## What is NOT an input The *return value* of `asArguments()` is not hashed as a task input. That's deliberate: it often contains absolute paths that would break cache relocatability. Track the *meaning* (the file content via `@InputFile`, the value via `@Input`) and let the string stay an execution-time detail. ## Validation Gradle's `validatePlugins` / runtime validation will warn about *some* missing annotations on task properties, but nested provider properties that are simply un-annotated are often silently ignored rather than flagged — so review them by hand.
- If you annotate a property @Input but it actually points to a file the process reads, what's wrong?`@Input` hashes the value (here the path string), not the file content. Changing the file's contents without changing the path won't invalidate the task. Use `@InputFile`/`@InputFiles`/`@Classpath` so the content is tracked.
- Does @Nested work on a List of providers or only a single provider?Both. Gradle treats a `@Nested` iterable by indexing into it and scanning each element's annotated properties, which is exactly why the providers *collection* on the task is annotated `@Nested`.
- Is the literal string returned by asArguments() part of the cache key?No. Only the annotated nested properties are. This is intentional so absolute paths in the arguments don't defeat a relocatable cache.
saying these in an interview costs you the question
- Claiming the returned argument strings are hashed as task inputs.
- Using @Input for something that should be @InputFile/@Classpath.
- Believing Gradle will always warn you about a missing annotation on a nested provider property.