skip to content

Why should a CommandLineArgumentProvider read its values through Provider/Property and resolve them inside asArguments() rather than capturing plain values at configuration time?

level: middleimportance: should knowfreq 25%

answer

  1. configuration vs execution phase
  2. Property/Provider resolved on read
  3. eager capture freezes stale/default value
  4. .get()/.getOrElse() inside asArguments() only
  5. convention() defaults; @Optional + isPresent for optional args
  6. configuration-cache friendly

basics

~10 s

Using Property/Provider keeps the values lazy: they're resolved when asArguments() runs at execution time, so later configuration changes and convention/defaults are picked up. Capturing plain values early freezes stale or unconfigured data.

solid answer

~40 s

A provider's `asArguments()` is called at execution time, but the *fields* it reads should be lazy too. Modeling them as `Property<T>` / `RegularFileProperty` / `ConfigurableFileCollection` means the actual value is resolved only when `asArguments()` (or input fingerprinting) demands it. This matters because Gradle's configuration is order-independent and incremental: a value may be set or overridden after the provider is created (by a convention, another plugin, a `withType` block, or the user's build script). If you capture a plain `String`/`File` in the constructor, you freeze whatever was there at that instant — often a default or empty value — and miss later changes. Lazy properties also integrate cleanly with `@Input`/`@InputFile` annotations on the getters, so the same object serves both fingerprinting and argument computation. Resolve with `.get()`/`.getOrElse(...)` *inside* `asArguments()`, never in the constructor.

code

kotlin · 15 lines
kotlin
abstract class VersionArgs : CommandLineArgumentProvider {
    @get:Input abstract val appVersion: Property<String>
    @get:Optional @get:Input abstract val debug: Property<Boolean>

    override fun asArguments(): Iterable<String> = buildList {
        add("-Dapp.version=${appVersion.get()}")
        if (debug.getOrElse(false)) add("-Ddebug=true")
    }
}

tasks.named<JavaExec>("run") {
    val v = objects.newInstance(VersionArgs::class.java)
    v.appVersion.set(provider { project.version.toString() }) // lazy
    jvmArgumentProviders.add(v)
}

go deeper

for a junior

Know values should be read lazily and resolved when asArguments() runs, not captured early.

for a middle

Explain the config-vs-execution phases, convention vs set, and @Optional/isPresent for optional args.

for a senior

Tie laziness to order-independent configuration and configuration-cache compatibility; avoid capturing Project.

for a principal

Mandate lazy, Project-free providers as a portability/cc standard, since eager capture is a recurring source of stale-value and cc-incompatibility incidents.

## The two-phase model Gradle has a **configuration phase** (build the task graph, wire up values) and an **execution phase** (run tasks). Configuration is order-insensitive: a property can be assigned, then re-assigned by another plugin, then read. The lazy `Provider`/`Property` API exists precisely so a value is *computed when read*, not when wired. ## Why providers need laziness too A `CommandLineArgumentProvider` is registered during configuration but invoked during execution. If you write: ```kotlin class BadArgs(version: String) : CommandLineArgumentProvider { private val v = version // captured eagerly! override fun asArguments() = listOf("-Dapp.version=$v") } ``` you freeze `version` at construction time. If `project.version` is set later (very common), you ship the stale value. The lazy form: ```kotlin abstract class GoodArgs : CommandLineArgumentProvider { @get:Input abstract val appVersion: Property<String> override fun asArguments() = listOf("-Dapp.version=${appVersion.get()}") } ``` resolves `appVersion` only when `asArguments()` runs, so it reflects the final configured state. Wire it lazily too: `args.appVersion.set(provider { project.version.toString() })` or `args.appVersion.convention("0.0.0")`. ## Defaults via convention `Property.convention(...)` supplies a default that a later explicit `.set(...)` overrides — ideal for plugin authors. Inside `asArguments()` use `.get()` (fails fast if unset and no convention) or `.getOrElse(fallback)` for optional args. ## Conditional / optional arguments Because computation is deferred, you can branch at execution time: ```kotlin override fun asArguments(): Iterable<String> = buildList { if (debug.get()) add("-Ddebug=true") if (configFile.isPresent) add("-Dconfig=${configFile.get().asFile.absolutePath}") } ``` `isPresent` lets you omit an argument entirely when an optional input wasn't provided (pair with `@Optional` on the getter). ## Configuration cache Lazy `Property`-backed providers also serialize cleanly for the **configuration cache**, which snapshots the task graph. Capturing live `Project` references or mutable plain state can make a provider non-serializable or capture forbidden objects; reading through `Provider`/`Property` keeps the provider self-contained and cc-compatible. ## Rule of thumb Constructor: assign nothing eager. Getters: lazy, annotated. `asArguments()`: the *only* place you call `.get()`.

  • What's the difference between Property.set(...) and Property.convention(...)?
    `convention(...)` provides a default used only if nobody calls `set(...)`. `set(...)` is an explicit value that always wins and overrides any convention. Plugin authors set conventions; users set values.
  • How do you make an argument optional so it's omitted when unset?
    Annotate the getter `@Optional` and check `property.isPresent` inside `asArguments()`, adding the argument only when present (or use `getOrElse`). That way an unset optional input doesn't blow up or emit a bogus arg.
  • How does lazy modeling help the configuration cache?
    Property-backed providers serialize their declared inputs without holding live Project/Task references, so the snapshotted task graph reloads cleanly. Capturing plain values or Project at construction can capture forbidden state and break cc.

saying these in an interview costs you the question

  • Calling .get() in the constructor or during configuration.
  • Capturing project.version (or any mutable value) eagerly as a plain String.
  • Holding a Project reference inside the provider (breaks configuration cache).

context