DEV Community

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

Posted on

3

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.

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay