Mastodon

Gradle Footguns: Don't add potentially-empty providers to collection properties

An elephant watching its step

After pairing with Tony Robalik on a recent weird behavior I was seeing in Gradle, we encountered a deliciously evil bug in Gradle. It's documented a little in Gradle 8.7's release notes (see Better API for updating collection properties), but the basic premise is below.

abstract class MyTask : DefaultTask() {
  @get:Input
  abstract val listProperty: ListProperty<String>
}

listProperty.add("one")
listProperty.add(providers.gradleProperty("hi"))

listProperty will be "empty" if there is no "hi" Gradle property, clearing the previously added elements.Where this gets worse is that Gradle's docs kinda regularly conflate empty and absent when dealing with collections, so my understanding was that it just cleared the elements. Turns out, it actually clears the value entirely.

So - doing this...

listProperty
  .add("one")
  .add(providers.gradleProperty("hi"))
  .get()

... actually throws a MissingValueException!

Even better, it also completely destroys your conventions

listProperty
  .convention(emptyList())
  .add("one")
  .add(providers.gradleProperty("hi"))
  .get()

... also throws an exception!

Finally, if you hide it behind a non-managed-property type like the Kotlin Gradle plugin does in its legacy API, you get something like this later.

kotlinOptions.freeCompilerArgs += listOf("hi")

This quietly becomes an inert action later if the underlying provider was added to with an absent provider, and the property is rendered unusable unless you call set() on it again with a fresh collection.

TL;DR don't add providers to collections without also using orElse()

listProperty
  .convention(emptyList())
  .add("one")
  .add(
    providers.gradleProperty("hi")
      .map { listOf(it) }
      .orElse(emptyList())
  )
  .get()