skip to content

Why are archive task naming properties modeled as lazy Property/Provider types, and what practical bug does this avoid when project.version is set by a plugin or later in the build?

level: seniorimportance: should knowfreq 30%

answer

  1. Property<String> resolved at execution
  2. config vs execution phase
  3. eager capture freezes 'unspecified'
  4. archiveVersion defaults to provider of project.version
  5. map() instead of early .get()

basics

~20 s

archiveBaseName/Version/Classifier are lazy Property<String> objects resolved at execution time. So if project.version is set later (e.g. by a versioning plugin), the jar still picks up the correct value instead of a stale one captured early.

solid answer

~50 s

Gradle's archive naming properties (`archiveBaseName`, `archiveVersion`, `archiveClassifier`, `archiveExtension`) are lazy `Property<String>` values, and `archiveFileName`/`archiveFile` are derived `Provider` values. Lazy means the value isn't computed when you write the build script but when the task actually executes (or when the provider is queried). This avoids a classic eager-evaluation bug: if you eagerly read `project.version` during configuration but a versioning plugin (or a later script line) sets the real version afterwards, an eager capture would freeze the placeholder/`unspecified` value into the file name. Because `archiveVersion` defaults to a **provider** of `project.version`, it reflects whatever the version is at execution time. The same laziness underpins configuration-cache compatibility and avoids unnecessary task realization when combined with `tasks.named(...)`/`configureEach`. When you derive a name yourself, prefer mapping providers (`project.version.map { ... }`) over reading `.get()` early, so you preserve the lazy chain.

code

kotlin · 8 lines
kotlin
// Keep it lazy: don't capture project.version eagerly.
tasks.named<Jar>("jar") {
    // GOOD: derive via a provider so a later version assignment is honored
    archiveVersion.set(providers.provider { project.version.toString() })
}

// Elsewhere a plugin may set the version after the jar block runs:
version = "1.4.2"   // jar still becomes app-1.4.2.jar

go deeper

for a junior

Know that the properties are lazy and resolved when the task runs, not when the script is read.

for a middle

Explain configuration vs execution phases and that archiveVersion defaults to a provider of project.version, avoiding stale captures.

for a senior

Show the eager-capture bug with a versioning plugin and how to keep the chain lazy with map()/provider(); connect to configuration cache.

for a principal

Establish lazy-API conventions across the org build so plugin ordering never leaks 'unspecified' versions into published artifact names.

## Eager vs lazy configuration Gradle builds have two phases that matter here: **configuration** (the build scripts run, tasks are wired) and **execution** (tasks actually do work). A value read **eagerly** is fixed during configuration; a **lazy** value is a `Provider` that's only resolved when queried, typically at execution. ## How archive naming uses laziness The naming inputs are `Property<String>` (a writable `Provider`): - `archiveBaseName`, `archiveAppendix`, `archiveVersion`, `archiveClassifier`, `archiveExtension`. And the outputs are read-only `Provider`s: - `archiveFileName: Provider<String>`, `archiveFile: Provider<RegularFile>`. `archiveVersion`'s default is effectively a provider of `project.version`. Nothing is concatenated until the provider is queried. ## The bug laziness avoids Consider a versioning plugin that computes the version from Git tags and assigns it during configuration, possibly **after** your `jar` block runs: ```kotlin // configuration order can be subtle tasks.named<Jar>("jar") { // If you did this eagerly, you'd freeze the CURRENT version: // val v = project.version.toString() // might be 'unspecified' here! // archiveVersion.set(v) } // later, a plugin sets: version = gitVersion() // e.g. '1.4.2' ``` If you eagerly captured `project.version` before the plugin set it, you'd publish `app-unspecified.jar`. Because the property is lazy and defaults to a provider of `project.version`, leaving it alone gives the correct `app-1.4.2.jar`. ## Preserving the lazy chain when customizing When you must derive a name, map the provider instead of calling `.get()` early: ```kotlin tasks.named<Jar>("jar") { archiveVersion.set(providers.provider { project.version.toString() }) // or build from another provider: archiveClassifier.set(buildTypeProvider.map { it.lowercase() }) } ``` ## Broader payoffs - **Configuration cache**: lazy providers are how Gradle serializes task inputs without re-running configuration; eager reads can break cacheability. - **Task-avoidance API**: `tasks.named(...)` + `configureEach` keep tasks unrealized; lazy values complement that by not forcing evaluation. ## Takeaway for publishing Leaning on the lazy defaults (especially `archiveVersion`) means your published artifact names always match the final resolved version, even when that version is computed by a plugin — a subtle but important correctness property for repeatable releases.

  • What file name would you get if you eagerly captured project.version before a versioning plugin set it?
    The placeholder value at that moment — often 'unspecified' — e.g. app-unspecified.jar, because the eager read froze the early value.
  • How does lazy evaluation help the configuration cache?
    Providers let Gradle serialize task inputs without re-running configuration; eager reads of mutable state can make a task non-cacheable or capture stale state.

saying these in an interview costs you the question

  • Calling project.version.toString() eagerly inside the jar block and assigning it, defeating laziness.
  • Claiming naming properties are plain Strings computed at script-evaluation time.
  • Saying the order of plugin application can never affect the version baked into the jar name.

context