Logo

dev-resources.site

for different kinds of informations.

Gradle extensions part 2: Now with shenanigans

Published at
12/11/2024
Categories
gradle
kotlin
java
Author
autonomousapps
Categories
3 categories in total
gradle
open
kotlin
open
java
open
Author
14 person written this
autonomousapps
open
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.

gradle Article's
30 articles in total
Favicon
Understanding (a bit of) the Gradle Kotlin DSL
Favicon
Zero Config Spring Batch: Just Write Business Logic
Favicon
JeKa: The Simplest Way to Create Uber and Shade Jars
Favicon
JeKa: The Simplest Way to Publish on Maven Central
Favicon
Gradle extensions part 2: Now with shenanigans
Favicon
Wednesday Links - Edition 2024-11-27
Favicon
A brand new Java scaffolding has been born today for Make Java Great Again!
Favicon
Wednesday Links - Edition 2024-10-16
Favicon
Gradle 8.11: Faster Configuration Cache and Improved Configuration Time
Favicon
react-native duplicate class problem
Favicon
Breaking the build 😝 : Demystifying Gradle
Favicon
Wednesday Links - Edition 2024-09-11
Favicon
One click dependencies fix
Favicon
ACAB: Fire the (code style) cop in your head
Favicon
Telltale: Automating Experimentation in Gradle Builds
Favicon
Minecraft Modpack Development Update: Beta Test and Musical Additions
Favicon
Gradle upgrade
Favicon
Announcing Dependency Analysis Gradle Plugin 2.0.0!
Favicon
Wednesday Links - Edition 2024-07-24
Favicon
Resource observability case study: jemalloc in Android builds
Favicon
How store signing keystore.
Favicon
Simple way to store secrets in Android Project.
Favicon
Developing a Custom Gradle Plugin for Formatting and Static Analysis
Favicon
Gradle Commands Cheat Sheet
Favicon
Wednesday Links - Edition 2024-04-24
Favicon
Gradle DSL: Configurando JaCoco
Favicon
Unearthing the Quirk: Dealing with File Access Issues that arise from Resource Optimization in Android Applications
Favicon
πŸ’ Cherry-Picked Nx v18.2 Updates
Favicon
Making Your Android Project Modular With Convention Plugins
Favicon
Kradle 9.0: Revolutionizing the JVM Ecosystem with Kotlin at its Core!

Featured ones: