Photo by Karsten Würth on Unsplash
Welcome to the spiritual successor to Gradle plugins and extensions: A primer for the bemused (one of my most popular posts, such that it competes for space with actual Gradle documentation at the top of a Google search).
As part of my long-running quest to Destroy buildSrc
With Fire, I have recently had occasion to learn how to add extensions to other kinds of types, such as tasks. We have code like this duplicated across many many repos that are under our care:
// buildSrc/src/main/kotlin/magic/Magic.kt
package magic
object Magic {
fun shouldPracticeTheDarkArts(): Boolean {
return System.getenv("DO_ANCIENT_MAGIC")?.toBoolean()
?: System.getenv("DO_SLIGHTLY_MORE_MODERN_MAGIC")?.toBoolean()
?: false
}
}
This code is used in build scripts like this:
// build.gradle.kts
import magic.Magic
tasks.withType<Test>().configureEach {
if (Magic.shouldPracticeTheDarkArts()) {
logger.quiet("👻")
}
}
There are several things about this that I'd like to improve:
- I don't want this code in buildSrc. I want a version of it in our build-logic that is under test and which is shared widely (instead of duplicated in a dozen different repos).
- I don't like the import. It is Unclean. (Build scripts should be simple, declarative, easy for tools to parse.)
- I'm not a big fan of calling
System.getenv()
in a Gradle context. I prefer to use theProviderFactory
.
Extending Test
tasks
Many Gradle types, including all Task
s (and of course the Project
type), are ExtensionAware
. This means they all have an ExtensionContainer
available on which new extensions can be created and added.
package magic
abstract class TestMagicExtension @Inject constructor(
private val providers: ProviderFactory
) {
internal companion object {
const val NAME = "magic"
fun create(
testTask: Test,
providers: ProviderFactory,
) {
testTask.extensions.create(
NAME,
TestMagicExtension::class.java,
providers,
)
}
}
fun shouldPracticeTheDarkArts(): Boolean {
return providers
.environmentVariable("DO_ANCIENT_MAGIC")
.orElse(providers.environmentVariable("DO_SLIGHTLY_MORE_MODERN_MAGIC"))
.map { it.isNotEmpty() }
.getOrElse(false)
}
And in our plugin, we can add this to all our Test
tasks:
project.tasks.withType<Test>().configureEach { t ->
TestMagicExtension.create(t, project.providers)
}
And now we can update our build scripts:
// build.gradle.kts
import magic.TestMagicExtension
tasks.withType<Test>().configureEach {
// the "extensions" call is on the Test instance,
// not the project instance
val magic = extensions.getByType(TestMagicExtension::class.java)
if (magic.shouldPracticeTheDarkArts()) {
logger.quiet("👻")
}
}
...that's not better at all!
Groovy: An interlude
First of all, let's take a step back and remind ourselves that "we love Kotlin, type safety is great, I don't care that performance is worse..." We can say that over and over again a few times while rocking in a fetal position on the floor till we feel better. Now, here's the same build script but in Groovy:
// build.gradle
tasks.withType(Test).configureEach {
if (magic.shouldPracticeTheDarkArts()) {
// I'm being cheeky by also omitting the
// "redundant" parentheses
logger.quiet "👻"
}
}
Groovy isn't supposed to be better! Damnit!
Sprinkle on some shenanigans
How the heck does Gradle Kotlin DSL do it? Why isn't it generating "typesafe accessors" for my test task extension? Well, that second one is a good question and I have no answer. But for the first... let's just "generate" (that is, write) our own typesafe accessors!
We add some code in a new (to us) package:
package org.gradle.kotlin.dsl
import magic.TestMagicExtension
public val Test.magic: TestMagicExtension
get() = extensions.getByType(TestMagicExtension::class.java)
public fun Test.magic(configure: TestMagicExtension.() -> Unit) {
configure(TestMagicExtension.NAME, configure)
}
And now we can update our Kotlin DSL build script:
// build.gradle.kts
tasks.withType<Test>().configureEach {
if (magic.shouldPracticeTheDarkArts()) {
logger.quiet("👻")
}
}
Here we're (ab)using the fact that Gradle automatically imports everything in the org.gradle.kotlin.dsl
package into build scripts, so all those functions are Just There (in a global namespace, so be careful!).
This is a common enough pattern that Gradle itself uses it in its test-retry-gradle-plugin. There's also an open issue (from, er, 2018) on Gradle's issue tracker with a feature request to permit custom plugins to add their own default imports with resorting to using split packages like this.
Now go forth and be merry, for it is that time of year.
Top comments (0)