DEV Community

Cover image for Gradle extensions part 2: Now with shenanigans
Tony Robalik
Tony Robalik

Posted on

Gradle extensions part 2: Now with shenanigans

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).

Google search results for

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
  }
}
Enter fullscreen mode Exit fullscreen mode

This code is used in build scripts like this:

// build.gradle.kts
import magic.Magic

tasks.withType<Test>().configureEach {
  if (Magic.shouldPracticeTheDarkArts()) {
    logger.quiet("👻")
  }
}
Enter fullscreen mode Exit fullscreen mode

There are several things about this that I'd like to improve:

  1. 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).
  2. I don't like the import. It is Unclean. (Build scripts should be simple, declarative, easy for tools to parse.)
  3. I'm not a big fan of calling System.getenv() in a Gradle context. I prefer to use the ProviderFactory.

Extending Test tasks

Many Gradle types, including all Tasks (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)
  }
Enter fullscreen mode Exit fullscreen mode

And in our plugin, we can add this to all our Test tasks:

project.tasks.withType<Test>().configureEach { t ->
  TestMagicExtension.create(t, project.providers)
}
Enter fullscreen mode Exit fullscreen mode

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("👻")
  }
}
Enter fullscreen mode Exit fullscreen mode

...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 "👻"
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

And now we can update our Kotlin DSL build script:

// build.gradle.kts
tasks.withType<Test>().configureEach {
  if (magic.shouldPracticeTheDarkArts()) {
    logger.quiet("👻")
  }
}
Enter fullscreen mode Exit fullscreen mode

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)