Gradle Plugins

necronomicon but with the gradle logo

About Me

John Burns

Staff Engineer @ GrubHub

CKUG Co-Organizer

twitter logo @wakingrufus

fediverse logo @wakingrufus@mastodon.technology

github logo wakingrufus

GrubHub logo
  • Fully Remote and Hybrid Remote Roles
  • Unlimited PTO
  • 8-16 weeks of parental leave
  • 4.5 day work week

About You

Gradle Users

Plugin Authors

Terrified of Gradle

The Problem

The Goal

  • Abstract away the complexity
  • Simplify standard usage
  • Without preventing customization
  • Testable

Maven vs Gradle

Maven

  • Declarative via XML
  • Extended with Plugins

Gradle

  • DSL
  • Extended with Plugins

DSL


plugins {
  id "java"
  id "jacoco"
}
repositories {
  mavenCentral()
}
dependencies {
  implementations("commons-io:commons-io:2.7")

  testImplementation("org.junit.jupiter:junit-jupiter-api:5.4.+")
  testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.4.+")
  testImplementation("org.junit.jupiter:junit-jupiter-params:5.4.+")
  testImplementation("org.assertj:assertj-core:3.23.1")
}
test {
  useJUnitPlatform()
}

                

DSL


def isNonStable = { String version ->
    def stableKeyword = ['RELEASE', 'FINAL', 'GA']
        .any { it -> version.toUpperCase().contains(it) }
    def regex = /^[0-9,.v-]+(-r)?$/
    return !stableKeyword && !(version ==~ regex)
}
def excludeList = ["guice", "guava"]
tasks.named("dependencyUpdates").configure {
    resolutionStrategy {
        componentSelection {
            all {
                if (isNonStable(it.candidate.version)) {
                    reject('Release candidate')
                } else if (excludeList.contains(it.candidate.module)){
                    reject('dependency excluded from upgrades')
                }
            }
        }
    }
}
                

DSL


                plugins {
                  id "java"
                  id "jacoco"
                  id "com.myorg.gradle.depupdates"
                }
                

Gradle lifecycle

Initialization

  • buildSrc
  • init scripts
  • settings.gradle

Configuration

  • Plugins
  • Build scripts

Execution

  • Tasks

Groovy or Kotlin?

Basic scripts: doesn't matter

Complex scripts: Groovy

Plugins: Kotlin

Groovy DSL

Kotlin DSL

Project Setup


                plugins {
                    kotlin("jvm")
                    `kotlin-dsl`
                    `java-gradle-plugin`
                }
                

Project Setup


                plugins {
                    kotlin("jvm")
                    `kotlin-dsl`
                    `java-gradle-plugin`
                }
                

Project Setup


                plugins {
                    kotlin("jvm")
                    `kotlin-dsl`
                    `java-gradle-plugin`
                }
                

Project Setup


                plugins {
                    kotlin("jvm")
                    `kotlin-dsl`
                    `java-gradle-plugin`
                }
                gradlePlugin {
                    plugins {
                        create("myPlugin") {
                            id = "com.myorg.myplugin"
                            implementationClass = "com.myorg.myplugin.MyPlugin"
                        }
                    }
                }
                

Project Setup


                plugins {
                    kotlin("jvm")
                    `kotlin-dsl`
                    `java-gradle-plugin`
                }
                gradlePlugin {
                    plugins {
                        create("myPlugin") {
                            id = "com.myorg.myplugin"
                            implementationClass = "com.myorg.myplugin.MyPlugin"
                        }
                    }
                }
                

Building Blocks

Tasks

Extensions

Plugins

Tasks

Workhorse of the Execution phase

Tasks

Inputs -> TaskAction -> Outputs

Tasks


                    abstract class MyTask : DefaultTask() {
                        @get:OutputFile
                        abstract val outputFile: Property<File>

                        @get:InputFile
                        abstract val inputFile: Property<File>

                        @TaskAction
                        fun doWork(){
                            outputFile.get().writeText(doThing(inputFile.get()))
                        }
                    }
                

Tasks

Companion Object


                    abstract class MyTask : DefaultTask() {
                        companion object {
                            @JvmStatic
                            fun create(project: Project,
                                       taskName: String = "myTask"): MyTask {
                                return project.tasks.create<MyTask>(taskName).apply {
                                    // TODO
                                }
                            }
                      }
                    }
                

Tasks

Companion Object


                    abstract class MyTask : DefaultTask() {
                        companion object {
                            @JvmStatic
                            fun create(project: Project,
                                       taskName: String = "myTask"): MyTask {
                                return project.tasks.create<MyTask>(taskName).apply {
                                    outputFile.set(input.map {
                                        project.buildDir.resolve("$it.txt")
                                    })
                                    input.convention("name")
                                }
                            }
                        }
                    }
                

Tasks

Companion Object


                    abstract class MyTask : DefaultTask() {
                        companion object {
                            @JvmStatic
                            fun create(project: Project,
                                       taskName: String = "myTask"): MyTask {
                                return project.tasks.create<MyTask>(taskName).apply {
                                    outputs.upToDateWhen { false }
                                }
                            }
                        }
                    }
                

Tasks

Companion Object


                    abstract class MyTask : DefaultTask() {
                        companion object {
                            @JvmStatic
                            fun create(project: Project,
                                       taskName: String = "myTask"): MyTask {
                                val ext = project.extensions.findByType<MyExtension>()
                                return project.tasks.create<MyTask>(taskName).apply {
                                    input.set(ext.setting)
                                }
                            }
                        }
                    }
                

Extensions

Exposing configuration to buildscripts

Extensions


                    open class MyExtension(objects: ObjectFactory) {
                        companion object {
                            @JvmStatic
                            fun create(project: Project): MyExtension {
                                return project.extensions.create<MyExtension>("my")
                                    .apply { name.convention(project.rootProject.name) }
                            }
                        }

                        val name: Property<String> = objects.property(String::class.java)

                        fun name(newName: String) {
                            name.set(newName)
                        }
                    }
                

Extensions

Usage


                    my {
                        name("customName")
                    }
                

                    extensions.findByType<MyExtension>() {
                        name("customName")
                    }
                

Plugins

Workhorse of the Configuration Phase

Plugins


class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
    }
}
                

Plugins

Apply Tasks & Extensions


class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val ext = MyExtension.create(project)
        val task = MyTask.create(project)
    }
}
                

Plugins

Apply Other Plugins


class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.pluginManager.apply(JavaPlugin::class.java)
    }
}
                

Plugins

React to Other Plugins


class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.pluginManager.withType(JavaPlugin::class) {
            project.repositories {
                // configure custom repo
            }
            project.withConvention(JavaPluginConvention::class) {
                sourceSets.create("customSourceSet"){
                    // configure custom source set
                }
            }
        }
    }
}
                

Best Practices

Avoid afterEvaluate

Avoid afterEvaluate

Don't


class MyPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    val ext = MyExtension.create(project)
    val task = MyTask.create(project)
    project.afterEvaluate {
      task.inputFile = ext.inputFile
    }
  }
}
                

Avoid afterEvaluate

Instead Do


class MyPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    val ext = MyExtension.create(project)
    val task = MyTask.create(project)
    task.inputFile.set(ext.inputFile) // bind properties
  }
}
                

Project Properties

  • Use extension properties when used in task logic
  • Do NOT read extension properties in plugin code
  • Use project properties when used in plugin logic

Project Properties

gradle.properties


                    my.enabled=false
                

Command Line


                    ./gradlew -Pmy.enabled=false
                

Implementation


class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        if(!project.hasProperty("my.enabled")
            || project.property("my.enabled")){
                    // plugin code
        }
    }
}
                

More Best Practices

https://github.com/liutikas/gradle-best-practices

Testing

Testing

Unit Tests

Testing

Component Tests

  • Programmatic access to Project model
  • Good for checking configuration
  • If you run tasks, invoke them directly
  • Avoid causing dependencies to resolve

Testing

Component Tests


        val rootProject = ProjectBuilder.builder().build() as ProjectInternal
        val subProject = ProjectBuilder.builder()
                    .withParent(rootProject)
                    .build() as ProjectInternal

        subProject.plugins.apply(JavaPlugin::class.java)
        subProject.plugins.apply(MyPlugin::class.java)

        subProject.evaluate()
        rootProject.evaluate()

        assertThat(subProject.tasks.findByName("myTask")).isNotNull()
        subProject.tasks.findByName("myTask").doWork()
                

Testing

Integration Tests

  • Use Gradle TestKit
  • Run a whole gradle build
  • works on gradle scripts you write to a temp dir
  • Pass in gradle version to use
  • parameterize to test multiple gradle verisons

Testing

Integration Tests


                rootProjectDir.resolve("build.gradle").apply {
                    createNewFile()
                    writeText("""plugins {
                            id("java")
                            id("com.myorg.myplugin")
                        }""".trimMargin()
                    )
                }
                val buildResult = GradleRunner.create()
                    .withProjectDir(rootProjectDir)
                    .withPluginClasspath()
                    .withArguments("build", "--stacktrace")
                    .withGradleVersion("7.5")
                    .forwardOutput().build()

                assertThat(buildResult.task(":myTask")?.outcome).isEqualTo(SUCCESS)
                

Initialization Phase

  • init scripts
  • Settings Plugin
  • Custom Wrapper Distribution

Initialization Phase

Gradle Wrapper


                wrapper {
                    gradleVersion = "7.5.1"
                }
                     
gradle wrapper

Initialization Phase

Gradle Wrapper


                wrapper {
                    gradleVersion = "7.5.1"
                }
                     
./gradlew build

Initialization Phase

Custom Gradle Wrapper

Base Gradle Wrapper + init scripts

                    wrapper {
                      distributionUrl(
                        "https://myorg.com/wrapper/gradle-7.5.1-mywrapper-2.1.0.zip"
                      )
                    }
                     

Initialization Phase

Custom Gradle Wrapper

tech.harmonysoft.oss.custom-gradle-dist-plugin

Initialization Phase

  • pluginManagement (repo/plugins)
  • Gradle Remote Build Cache
Red Swingline stapler
necronomicon but with the gradle logo
wakingrufus.github.io/developing-gradle-plugins/

twitter logo @wakingrufus

fediverse logo @wakingrufus@mastodon.technology