Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Crash Course in Building Gradle Plugins

Crash Course in Building Gradle Plugins

A Gradle plugin is something that we use every day, but have you ever considered how they're created? What's behind the magic of the Kotlin DSLs provided by the plugins we use daily?

In this talk, we'll try to uncover the magic behind the Gradle plugin APIs and how to use them to build your own plugin. We'll explore the process of developing, debugging, testing, and finally publishing your grade plugin just like any other piece of software.

By the end of this, you'll learn how you can turn that custom Gradle task you've been copying and pasting across projects into a fully-fledged Gradle plugin!

Iury Souza

June 08, 2023
Tweet

Other Decks in Programming

Transcript

  1. • Senior Engineer @ Klarna • Currently building a browser

    • Working with android since 2016 @iurysza iurysouza.dev About me
  2. 〉Section Title What is this about? A quick starter guide

    on how to approach building gradle plugins.
  3. *record scratch* *freeze frame* "Yup, that's me. You're probably wondering

    how I ended up in this situation…" Background Story
  4. What It’s an add-on for your build, which you can

    apply using the Gradle plugin DSL
  5. How

  6. Build tasks ----------- assemble - Assemble main outputs for all

    the variants. build - Assembles and tests this project. clean - Deletes the build directory. Exposing tasks that allow you to run some specific action How
  7. $ tree ├── build.gradle.kts ├── gradle │ └── wrapper │

    ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── myplugin │ ├── build.gradle.kts │ └── src │ ├── main │ │ └── kotlin │ │ └── dev │ │ └── iurysouza │ │ └── myplugin │ └── test │ └── kotlin │ └── dev │ └── iurysouza │ └── myplugin └── settings.gradle.kts The project's folder structure Setup
  8. $ tree ├── build.gradle.kts ├── gradle │ └── wrapper │

    ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── myplugin │ ├── build.gradle.kts │ └── src │ ├── main │ │ └── kotlin │ │ └── dev │ │ └── iurysouza │ │ └── myplugin │ └── test │ └── kotlin │ └── dev │ └── iurysouza │ └── myplugin └── settings.gradle.kts Pretty standard Setup
  9. $ tree ├── build.gradle.kts ├── gradle │ └── wrapper │

    ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── myplugin │ ├── build.gradle.kts │ └── src │ ├── main │ │ └── kotlin │ │ └── dev │ │ └── iurysouza │ │ └── myplugin │ └── test │ └── kotlin │ └── dev │ └── iurysouza │ └── myplugin └── settings.gradle.kts Pretty standard Setup
  10. $ tree ├── build.gradle.kts ├── gradle │ └── wrapper │

    ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── app │ ├── build.gradle.kts │ └── src │ ├── main │ │ └── kotlin │ │ └── dev │ │ └── iurysouza │ │ └── app │ └── test │ └── kotlin │ └── dev │ └── iurysouza │ └── app └── settings.gradle.kts Pretty standard Setup
  11. gradlePlugin { plugins { create("myplugin") { id = "dev.iurysouza.myplugin" implementationClass

    = "dev.iurysouza.myplugin.MyPlugin" version = "0.0.1" description = "My plugin description" displayName = "My plugin name" } } } Defining the plugin Setup
  12. gradlePlugin { plugins { create("myplugin") { id = "dev.iurysouza.myplugin" implementationClass

    = "dev.iurysouza.myplugin.MyPlugin" version = "0.0.1" description = "My plugin description" displayName = "My plugin name" } } } Defining the plugin Setup
  13. The Extension Class is a glorified data object. It helps

    us pass in configuration to the plugin. Implementation
  14. import javax.inject.Inject import org.gradle.api.Project import org.gradle.api.provider.Property abstract class MyPluginExtension @Inject

    constructor( project: Project, ) { val shouldDoSomething: Property<Boolean> = project.objects.property(Boolean-:class.java) } Implementing an Extension Class Implementation
  15. import javax.inject.Inject import org.gradle.api.Project import org.gradle.api.provider.Property abstract class MyPluginExtension @Inject

    constructor( project: Project, ) { val shouldDoSomething: Property<Boolean> = project.objects.property(Boolean-:class.java) } Implementing an Extension Class Implementation
  16. import javax.inject.Inject import org.gradle.api.Project import org.gradle.api.provider.Property abstract class MyPluginExtension @Inject

    constructor( project: Project, ) { val shouldDoSomething: Property<Boolean> = project.objects.property(Boolean-:class.java) } Implementing an Extension Class Implementation
  17. import javax.inject.Inject import org.gradle.api.Project import org.gradle.api.provider.Property abstract class MyPluginExtension @Inject

    constructor( project: Project, ) { val shouldDoSomething: Property<Boolean> = project.objects.property(Boolean-:class.java) } Implementing an Extension Class Implementation
  18. import javax.inject.Inject import org.gradle.api.Project import org.gradle.api.provider.Property abstract class MyPluginExtension @Inject

    constructor( project: Project, ) { val shouldDoSomething: Property<Boolean> = project.objects.property(Boolean-:class.java) } Implementing an Extension Class Implementation
  19. import org.gradle.api.DefaultTask import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction abstract class

    MyPluginTask : DefaultTask() { @get:Input abstract val shouldDoSomething: Property<Boolean> @TaskAction fun execute() { if (shouldDoSomething.get()) { println("Did something") } else { println("Did something else") } } } Defining our plugin's Task Class Implementation
  20. import org.gradle.api.DefaultTask import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction abstract class

    MyPluginTask : DefaultTask() { @get:Input abstract val shouldDoSomething: Property<Boolean> @TaskAction fun execute() { if (shouldDoSomething.get()) { println("Did something") } else { println("Did something else") } } } Defining our plugin's Task Class Implementation
  21. import org.gradle.api.DefaultTask import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction abstract class

    MyPluginTask : DefaultTask() { @get:Input abstract val shouldDoSomething: Property<Boolean> @TaskAction fun execute() { if (shouldDoSomething.get()) { println("Did something") } else { println("Did something else") } } } Defining our plugin's Task Class Implementation
  22. import org.gradle.api.DefaultTask import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction abstract class

    MyPluginTask : DefaultTask() { @get:Input abstract val shouldDoSomething: Property<Boolean> @TaskAction fun execute() { if (shouldDoSomething.get()) { println("Did something") } else { println("Did something else") } } } Defining our plugin's Task Class Implementation
  23. import org.gradle.api.DefaultTask import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction abstract class

    MyPluginTask : DefaultTask() { @get:Input abstract val shouldDoSomething: Property<Boolean> @TaskAction fun execute() { if (shouldDoSomething.get()) { println("Did something") } else { println("Did something else") } } } Defining our plugin's Task Class Implementation
  24. gradlePlugin { plugins { create("myplugin") { id = "dev.iurysouza.myplugin" implementationClass

    = "dev.iurysouza.myplugin.MyPlugin" version = "0.0.1" description = "My plugin description" displayName = "My plugin name" } } } Remember this? Implementation
  25. It's our entry point. It defines what happens when we

    the plugin is applied. Implementation
  26. import org.gradle.api.Plugin import org.gradle.api.Project class MyPlugin : Plugin<Project> { override

    fun apply(project: Project) { println("MyPlugin was applied!") } } Implementation Defining our Plugin Class
  27. import org.gradle.api.Plugin import org.gradle.api.Project class MyPlugin : Plugin<Project> { override

    fun apply(project: Project) { println("MyPlugin was applied!") } } Implementation Defining our Plugin Class
  28. import org.gradle.api.Plugin import org.gradle.api.Project class MyPlugin : Plugin<Project> { override

    fun apply(project: Project) { println("MyPlugin was applied!") } } Implementation Defining our Plugin Class
  29. import org.gradle.api.Plugin import org.gradle.api.Project class MyPlugin : Plugin<Project> { override

    fun apply(project: Project) { println("MyPlugin was applied!") } } Implementation Defining our Plugin Class
  30. import org.gradle.api.Plugin import org.gradle.api.Project class MyPlugin : Plugin<Project> { override

    fun apply(project: Project) { println("MyPlugin was applied!") } } Implementation Defining our Plugin Class
  31. import org.gradle.api.Plugin import org.gradle.api.Project class MyPlugin : Plugin<Project> { override

    fun apply(project: Project) { val extension = project.extensions.create( "myPluginConfig", MyPluginExtension-:class.java, project ) project.tasks.register( "myPluginTask", MyPluginTask-:class.java ) { task -> task.shouldDoSomething.set( extension.shouldDoSomething ) } } } Implementation Putting things together
  32. import org.gradle.api.Plugin import org.gradle.api.Project class MyPlugin : Plugin<Project> { override

    fun apply(project: Project) { val extension = project.extensions.create( "myPluginConfig", MyPluginExtension-:class.java, project ) project.tasks.register( “myPluginTask”, MyPluginTask-:class.java ) { task -> task.shouldDoSomething.set( extension.shouldDoSomething ) } } } Implementation Putting things together
  33. import org.gradle.api.Plugin import org.gradle.api.Project class MyPlugin : Plugin<Project> { override

    fun apply(project: Project) { val extension = project.extensions.create( "myPluginConfig", MyPluginExtension-:class.java, project ) project.tasks.register( "myPluginTask", MyPluginTask-:class.java ) { task -> task.shouldDoSomething.set( extension.shouldDoSomething ) } } } Implementation Putting things together
  34. Testing A bit of setup first plugins { kotlin("jvm") `java-gradle-plugin`

    } dependencies { implementation(kotlin("stdlib")) implementation(gradleApi()) }
  35. Testing A bit of setup first plugins { kotlin("jvm") `java-gradle-plugin`

    } dependencies { implementation(kotlin("stdlib")) implementation(gradleApi()) testRuntimeOnly(libs.junit5Engine) testImplementation(libs.junit5Api) } tasks.test { useJUnitPlatform() }
  36. Testing Just add your tests here $ tree ├── build.gradle.kts

    ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── myplugin │ ├── build.gradle.kts │ └── src │ │── main │ │ └── kotlin │ │ └── dev │ │ └── iurysouza │ │ └── myplugin │ └── test │ └── kotlin │ └── dev │ └── iurysouza │ └── myplugin └── settings.gradle.kts
  37. class MyPluginTest { @Test fun `When plugin is applied, the

    task is added to the build`() { val project = ProjectBuilder.builder().build() project.pluginManager.apply("myPlugin") assert(project.tasks.getByName("myPluginTask") is MyPluginTask) } } Testing Unit tests with ProjectBuilder
  38. plugins { kotlin("jvm") `java-gradle-plugin` } dependencies { implementation(kotlin("stdlib")) implementation(gradleApi()) testImplementation(libs.junit5Api)

    testRuntimeOnly(libs.junit5Engine) testImplementation(gradleTestKit()) } tasks.test { useJUnitPlatform() } Testing Setting up
  39. class MyPluginFunctionalTest { @TempDir lateinit var projDir: File private lateinit

    var settingsFile: File private lateinit var buildFile: File @BeforeEach fun setup() { settingsFile = File(projDir, "settings.gradle.kts") buildFile = File(projDir, "build.gradle.kts") } } Testing Functional Tests with testKit
  40. class MyPluginFunctionalTest { @TempDir lateinit var projDir: File private lateinit

    var settingsFile: File private lateinit var buildFile: File @BeforeEach fun setup() { settingsFile = File(projDir, "settings.gradle.kts") buildFile = File(projDir, "build.gradle.kts") } } Testing Functional Tests with testKit
  41. class MyPluginFunctionalTest { @TempDir lateinit var projDir: File private lateinit

    var settingsFile: File private lateinit var buildFile: File @BeforeEach fun setup() { settingsFile = File(projDir, "settings.gradle.kts") buildFile = File(projDir, "build.gradle.kts") } } Testing Functional Tests with testKit
  42. class MyPluginFunctionalTest { @BeforeEach fun setup() {--.} @Test fun `when

    plugin's task is ran it produces the expected output`() { settingsFile.writeText( """ rootProject.name = "example" """.trimIndent() ) buildFile.writeText( """ plugins { java id("dev.iurysouza.myplugin") } myPluginConfig { shouldDoSomething.set(true) } """.trimIndent() ) -/ Run the plugin task val buildResult: BuildResult = GradleRunner.create() .withProjectDir(testProjectDir) .withArguments("myPluginTask") .build() -/ Check if the output matches the expected result val expectedOutput = "didSomething" assertEquals(expectedOutput, buildResult.output) } } Testing Functional Tests with testKit
  43. class MyPluginFunctionalTest { @BeforeEach fun setup() {--.} @Test fun `when

    plugin's task is ran it produces the expected output`() { settingsFile.writeText( """ rootProject.name = "example" """.trimIndent() ) buildFile.writeText( """ plugins { java id("dev.iurysouza.myplugin") } myPluginConfig { shouldDoSomething.set(true) } """.trimIndent() ) -/ Run the plugin task val buildResult: BuildResult = GradleRunner.create() .withProjectDir(testProjectDir) .withArguments("myPluginTask") .build() -/ Check if the output matches the expected result val expectedOutput = "didSomething" assertEquals(expectedOutput, buildResult.output) } } Testing Functional Tests with testKit
  44. class MyPluginFunctionalTest { @BeforeEach fun setup() {--.} @Test fun `when

    plugin's task is ran it produces the expected output`() { settingsFile.writeText( """ rootProject.name = "example" """.trimIndent() ) buildFile.writeText( """ plugins { java id("dev.iurysouza.myplugin") } myPluginConfig { shouldDoSomething.set(true) } """.trimIndent() ) -/ Run the plugin task val buildResult: BuildResult = GradleRunner.create() .withProjectDir(testProjectDir) .withArguments("myPluginTask") .build() -/ Check if the output matches the expected result val expectedOutput = "Did something" assertEquals(expectedOutput, buildResult.output) } } Testing Functional Tests with testKit
  45. class MyPluginFunctionalTest { @BeforeEach fun setup() {--.} @Test fun `when

    plugin is ran it produces the expected output`() { settingsFile.writeText( """ rootProject.name = "example" """.trimIndent() ) buildFile.writeText( """ plugins { java id("dev.iurysouza.myplugin") } myPluginConfig { shouldDoSomething.set(true) } """.trimIndent() ) -/ Run the plugin task val buildResult: BuildResult = GradleRunner.create() .withProjectDir(testProjectDir) .withArguments("myPluginTask") .build() -/ Check if the output matches the expected result val expectedOutput = "Did something" assertEquals(expectedOutput, buildResult.output) } } Testing Functional Tests with testKit
  46. class MyPluginFunctionalTest { @BeforeEach fun setup() {--.} @Test fun `when

    plugin is ran it produces the expected output`() { settingsFile.writeText( """ rootProject.name = "example" """.trimIndent() ) buildFile.writeText( """ plugins { java id("dev.iurysouza.myplugin") } myPluginConfig { shouldDoSomething.set(true) } """.trimIndent() ) -/ Run the plugin task val buildResult: BuildResult = GradleRunner.create() .withProjectDir(testProjectDir) .withArguments("myPluginTask") .build() -/ Check if the output matches the expected result val expectedOutput = "Did Something" assertEquals(expectedOutput, buildResult.output) } } Testing Functional Tests with testKit
  47. plugins { kotlin("jvm") `java-gradle-plugin` } dependencies { implementation(kotlin("stdlib")) implementation(gradleApi()) testRuntimeOnly(libs.junit5Engine)

    testImplementation(libs.junit5Api) testImplementation(gradleTestKit()) } tasks.test { useJUnitPlatform() } Publishing Setup plugin-publish
  48. plugins { kotlin("jvm") `java-gradle-plugin` id("com.gradle.plugin-publish") version "1.1.0" } dependencies {

    implementation(kotlin("stdlib")) implementation(gradleApi()) testRuntimeOnly(libs.junit5Engine) testImplementation(libs.junit5Api) testImplementation(gradleTestKit()) } tasks.test { useJUnitPlatform() } Publishing Setup plugin-publish
  49. Publishing Setup plugin-publish $ ./gradlew :myplugin:tasks Plugin Portal tasks -------------------

    publishPlugins - Publishes this plugin to the Gradle Plugin portal. Publishing tasks ---------------- publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.
  50. Publishing Setup plugin-publish $ ./gradlew :myplugin:tasks Plugin Portal tasks -------------------

    publishPlugins - Publishes this plugin to the Gradle Plugin portal. Publishing tasks ---------------- publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.
  51. Publishing Setup plugin-publish $ ./gradlew :myplugin:tasks Plugin Portal tasks -------------------

    publishPlugins - Publishes this plugin to the Gradle Plugin portal. Publishing tasks ---------------- publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.
  52. Publishing Publishing locally $ cd ~/.m2/repository/dev/iurysouza/myplugin/0.0.1 -& ls -la .rw-r--r--

    261 myplugin-0.0.1-javadoc.jar .rw-r--r-- 4.9k myplugin-0.0.1-sources.jar .rw-r--r-- 28k myplugin-0.0.1.jar .rw-r--r-- 3.9k myplugin-0.0.1.module .rw-r--r-- 973 myplugin-0.0.1.pom
  53. import org.gradle.api.Plugin import org.gradle.api.Project class MyPlugin : Plugin<Project> { override

    fun apply(project: Project) { println("MyPlugin was applied!") } } Running Remember this?
  54. import org.gradle.api.DefaultTask import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction abstract class

    MyPluginTask : DefaultTask() { @get:Input abstract val shouldDoSomething: Property<Boolean> @TaskAction fun execute() { if (shouldDoSomething.get()) { println("Did something") } else { println("Did something else") } } } Running Remember this?
  55. Where to go from Here? - Rules for Gradle plugin

    authors - Defensive development Gradle plugin development for busy engineers - Module Graph plugin