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

How to write Gradle Plugins in Kotlin

Stefan M.
April 20, 2018

How to write Gradle Plugins in Kotlin

Given at Droidcon Italy 2018

Every Android Developer is familiar with Gradle, right?
We know how to apply a plugin, how to setup the Android extension and how to declare our dependencies.

But from where does the Android extension come from?
Which settings are possible here?
How can a task use these information to run a specific action?

I'll explain all the Gradle magic by showing how to write a Gradle plugin.
The plugin will be pretty simple and straightforward but shows you what Gradle does behind the scenes.

Naturally we will write the plugin in Kotlin.
We will also cover a little bit about Gradle's Kotlin DSL.

Basically you will leave my talk knowing the basics of the Gradle plugin development:
* How can I declare my own task?
* How can I declare my own extension?
* How can I test my plugin?

Stefan M.

April 20, 2018
Tweet

More Decks by Stefan M.

Other Decks in Programming

Transcript

  1. plugins { id("person-creater") version "26" } person { create("stefan") {

    name = "Stefan" lastName = "May" country = "Germany" avatar = "https://goo.gl/BV2a67" jobTitle = "Android Developer" workingSince = "07/2014" company { name = "grandcentrix" location = "Cologne" 2 / 42
  2. lastName = "May" country = "Germany" avatar = "https://goo.gl/BV2a67" jobTitle

    = "Android Developer" workingSince = "07/2014" company { name = "grandcentrix" location = "Cologne" remoteWorker = true } social { twitter = "@StefMa91" github = "StefMa" medium = "@StefMa" } } } 2 / 42
  3. • Android Developer since July 2014 • Android Studio stable

    December 2014 together with the Gradle Plugin Gradle… what is Gradle? • Copy & Paste from stackoverflow • The raise of build variants & product flavors • Raise of plugins… 5 / 42
  4. • Android Developer since July 2014 • Android Studio stable

    December 2014 together with the Gradle Plugin Gradle… what is Gradle? • Copy & Paste from stackoverflow • The raise of build variants & product flavors • Raise of plugins… WHAT IS GRADLE!!!111!1oneeleven 5 / 42
  5. plugins { id("guru.stefma.gloc") version "0.0.1" } gloc { enabled =

    true dirs = arrayOf("src/main", "src/test") } 7 / 42
  6. ./gradlew gloc --console=plain Hello Plugin \o/ :glocInput :gloc Output can

    be found at /Users/stefan/Developer/DroidconItaly/Gloc/gloc/build/gloc/gloc.txt BUILD SUCCESSFUL in 0s 2 actionable tasks: 2 executed 8 / 42
  7. BUILD SUCCESSFUL in 0s 2 actionable tasks: 2 executed ./gradlew

    gloc --console=plain Hello Plugin \o/ :glocInput :gloc UP-TO-DATE BUILD SUCCESSFUL in 0s 2 actionable tasks: 1 executed, 1 up-to-date 8 / 42
  8. 2 actionable tasks: 1 executed, 1 up-to-date ./gradlew clean gloc

    --build-cache --console=plain Hello Plugin \o/ :clean :glocInput :gloc Output can be found at /Users/stefan/Developer/DroidconItaly/Gloc/gloc/build/gloc/gloc.txt BUILD SUCCESSFUL in 0s 3 actionable tasks: 3 executed 8 / 42
  9. BUILD SUCCESSFUL in 0s 3 actionable tasks: 3 executed ./gradlew

    clean gloc --build-cache --console=plain Hello Plugin \o/ :clean :glocInput :gloc FROM-CACHE BUILD SUCCESSFUL in 0s 3 actionable tasks: 2 executed, 1 from cache 8 / 42
  10.  gradle   wrapper   gradle-wrapper.jar  

    gradle-wrapper.properties  gradlew  gradlew.bat   build.gradle.kts  settings.gradle.kts   src  main   kotlin   guru.stefma.gloc  test 11 / 42
  11.  gradle   wrapper   gradle-wrapper.jar  

    gradle-wrapper.properties  gradlew  gradlew.bat   build.gradle.kts  settings.gradle.kts   src  main   kotlin   guru.stefma.gloc  test  kotlin  guru.stefma.gloc 11 / 42
  12. lugins { kotlin("jvm") version ("1.2.30") id("java-gradle-plugin") id("maven-publish") • applies java

    plugin • gradleApi() dependency • gradleTestKit() dependency 13 / 42
  13. File(tempDir, "build.gradle").run { writeText(""" plugins { id "guru.stefma.gloc" } """)

    } val buildResult = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .build() assertThat(buildResult.output).contains("Hello Plugin \\o/") 17 / 42
  14. open class GlocExtension { var enabled: Boolean = true var

    dirs: Array<String> = emptyArray() } 18 / 42
  15. override fun apply(project: Project) { println("Hello Plugin \\o/") val extension

    = project.extensions.run { create("gloc", GlocExtension::class.java) } } 19 / 42
  16. override fun apply(project: Project) { val extension = project.extensions.run {

    create("gloc", GlocExtension::class.java) } project.afterEvaluate { if (!extension.enabled) return@afterEvaluate println("Hello Plugin \\o/") } } 20 / 42
  17. fun `apply and enabled false should not print hello plugin`(tempDir:

    File) { File(tempDir, "build.gradle").run { writeText(""" plugins { id "guru.stefma.gloc" } gloc { enabled = false } """) } val buildResult = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() 21 / 42
  18. File(tempDir, "build.gradle").run { writeText(""" plugins { id "guru.stefma.gloc" } gloc

    { enabled = false } """) } val buildResult = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .build() assertThat(buildResult.output).doesNotContain("Hello Plugin \\o/") } 21 / 42
  19. plugins { id "guru.stefma.gloc" } gloc { enabled = false

    enabled = true } """) } val buildResult = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .build() assertThat(buildResult.output).doesNotContain("Hello Plugin \\o/") assertThat(buildResult.output).contain(„Hello Plugin \\o/") } 22 / 42
  20. plugins { id("guru.stefma.gloc") version "0.0.1" } gloc { enabled =

    true dirs = arrayOf("src/main", "src/test") } 23 / 42
  21. override fun apply(project: Project) { ... val glocTask = with(project.tasks)

    { create("gloc", GlocTask::class.java) { it.group = "Development" it.description = "Get the lines of code for files" } } } 25 / 42
  22. File(tempDir, "build.gradle").run { writeText( """ plugins { id("guru.stefma.gloc") } """

    ) } val buildResult = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() 26 / 42
  23. """ plugins { id("guru.stefma.gloc") } """ ) } val buildResult

    = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() assertThat(buildResult.output).contains("Hello from Task") assertThat(buildResult.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) 26 / 42
  24. val buildResult = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() val buildResult2

    = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() assertThat(buildResult.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) assertThat(buildResult2.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.UP_TO_DATE) 27 / 42
  25. val buildResult = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() val buildResult2

    = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() assertThat(buildResult.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) assertThat(buildResult2.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.UP_TO_DATE) ❌ 27 / 42
  26. open class GlocTask : DefaultTask() { @OutputFile var output =

    project.file("${project.buildDir}/gloc/gloc.txt") @TaskAction fun action() { output.createNewFile() output.writeText("Hello World") } } 28 / 42
  27. val buildResult = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() val buildResult2

    = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() assertThat(buildResult.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) assertThat(buildResult2.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.UP_TO_DATE) 29 / 42
  28. val buildResult = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() val buildResult2

    = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() assertThat(buildResult.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) assertThat(buildResult2.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.UP_TO_DATE) ✅ 29 / 42
  29. open class GlocTask : DefaultTask() { @OutputFile var output =

    project.file("${project.buildDir}/gloc/gloc.txt") @TaskAction fun action() { val extension = project.extensions.run { findByName("gloc") as GlocExtension } if (extension.enabled) { createPrettyOutputFile(extensions.dirs) println("Output can be found at ${output.absolutePath}") } 30 / 42
  30. open class GlocTask : DefaultTask() { @OutputFile var output =

    project.file("${project.buildDir}/gloc/gloc.txt") @TaskAction fun action() { val extension = project.extensions.run { findByName("gloc") as GlocExtension } if (extension.enabled) { createPrettyOutputFile(extensions.dirs) println("Output can be found at ${output.absolutePath}") } } } 30 / 42
  31. fun `task should read test xml file and should write

    loc in it`(tempDir: File) { File(tempDir, "build.gradle").run { writeText( """ plugins { id "guru.stefma.gloc" } gloc { enabled = true dirs = [projectDir.path + "/source"] } """ ) } 31 / 42
  32. enabled = true dirs = [projectDir.path + "/source"] } """

    ) } File(tempDir, "source/test.xml").apply { parentFile.mkdirs() createNewFile() writeText("This\nis\ndroidcon\nitaly\nturin") } GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() 31 / 42
  33. ) } File(tempDir, "source/test.xml").apply { parentFile.mkdirs() createNewFile() writeText("This\nis\ndroidcon\nitaly\nturin") } GradleRunner.create()

    .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc") .build() val glocFileText = File(tempDir, "build/gloc/gloc.txt") assertThat(glocFileText.readText()).contains("5") } 31 / 42
  34. @Test fun `task run twice with different dirs should be

    run twice`(tempDir: File) { runTest(tempDir, "source") { assertThat(it.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) } runTest(tempDir, "anotherSource") { assertThat(it.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) } } 32 / 42
  35. @Test fun `task run twice with different dirs should be

    run twice`(tempDir: File) { runTest(tempDir, "source") { assertThat(it.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) } runTest(tempDir, "anotherSource") { assertThat(it.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) } } ❌ 32 / 42
  36. @Test fun `task run twice with different dirs should be

    run twice`(tempDir: File) { runTest(tempDir, "source") { assertThat(it.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) } runTest(tempDir, "anotherSource") { assertThat(it.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) TaskOutcome.UP_TO_DATE } } # 33 / 42
  37. open class GlocTask : DefaultTask() { @OutputFile var output =

    project.file("${project.buildDir}/gloc/gloc.txt") @Input var inputDirs = (project.extensions.findByName("gloc") as GlocExtension) .dirs.run { map { it }.toString() } ... } 34 / 42
  38. open class GlocTask : DefaultTask() { ... @Input var inputDirs

    = (project.extensions.findByName("gloc") as GlocExtension) .dirs.run { map { it }.toString() } @InputFile var inputDirs = project.file("${project.buildDir}/gloc/inputdirs.txt") ... } 35 / 42
  39. open class GlocInputTask : DefaultTask() { @TaskAction fun writeInput() {

    val filePath = "{project.buildDir}/gloc/inputdirs.txt" val inputFile = project.file("$filePath").apply { parentFile.mkdirs() createNewFile() writeText("") } (project.extensions.findByName("gloc") as GlocExtension) .dirs.forEach { inputFile.appendText(it) } } } 36 / 42
  40. override fun apply(project: Project) { val inputTask = project.tasks.run {

    create("glocInput", GlocInputTask::class.java) } with(project.tasks) { create("gloc", GlocTask::class.java) { it.group = "Development" it.description = "Get the lines of code for files" it.dependsOn(inputTask) } } } 37 / 42
  41. @Test fun `task run twice with different dirs should be

    run twice`(tempDir: File) { runTest(tempDir, "source") { assertThat(it.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) } runTest(tempDir, "anotherSource") { assertThat(it.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) } } 38 / 42
  42. @Test fun `task run twice with different dirs should be

    run twice`(tempDir: File) { runTest(tempDir, "source") { assertThat(it.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) } runTest(tempDir, "anotherSource") { assertThat(it.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) } } ✅ 38 / 42
  43. fun `task should read from build cache`(tempDir: File) { File(tempDir,

    "build.gradle").run { writeText( """ plugins { id "guru.stefma.gloc" } gloc { enabled = true dirs = [projectDir.path + "/source"] } """ ) } 40 / 42
  44. plugins { id "guru.stefma.gloc" } gloc { enabled = true

    dirs = [projectDir.path + "/source"] } """ ) } File(tempDir, "source/test.xml").apply { parentFile.mkdirs() createNewFile() writeText("This\nis\droidcon\nitaly\nturin") } 40 / 42
  45. createNewFile() writeText("This\nis\droidcon\nitaly\nturin") } File(tempDir, "settings.gradle").run { writeText( """ buildCache {

    local { directory = "$tempDir/cache" } } """ ) } val build = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc", "--build-cache") .build() 40 / 42
  46. } val build = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc", "--build-cache") .build()

    assertThat(build.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) // Clean build dir and run again - should be read from build cache File(tempDir, "build/").deleteRecursively() val build2 = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc", "--build-cache") .build() 40 / 42
  47. .withPluginClasspath() .withArguments("gloc", "--build-cache") .build() assertThat(build.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.SUCCESS) // Clean build dir

    and run again - should be read from build cache File(tempDir, "build/").deleteRecursively() val build2 = GradleRunner.create() .withProjectDir(tempDir) .withPluginClasspath() .withArguments("gloc", "--build-cache") .build() assertThat(build2.task(":gloc")!!.outcome) .isEqualTo(TaskOutcome.FROM_CACHE) } 40 / 42
  48. • Created the project • Created the gradle files and

    registered the plugin gradlePlugin { plugins { create("gloc") { id = "guru.stefma.gloc" implementationClass = "guru.stefma.gloc.GlocPlugin" } } } 42 / 42
  49. • Created the project • Created the plugin • Created

    the gradle files and registered the plugin 42 / 42
  50. • Created the project • Created the plugin • Created

    the gradle files and registered the plugin open class GlocPlugin : Plugin<Project> { override fun apply(project: Project) { project.extensions.create(...) project.tasks.create(...) } } 42 / 42
  51. • Created the project • Created the plugin • Created

    the extension • Created the gradle files and registered the plugin 42 / 42
  52. • Created the project • Created the plugin • Created

    the extension • Created the gradle files and registered the plugin open class GlocExtension { var enabled: Boolean = true var dirs: Array<String> = emptyArray() } 42 / 42
  53. • Created the project • Created the plugin • Created

    the extension • Created the task • Created the gradle files and registered the plugin 42 / 42
  54. • Created the project • Created the plugin • Created

    the extension • Created the task • Created the gradle files and registered the plugin @CacheableTask open class GlocTask : DefaultTask() { @OutputFile ... @InputFile ... @Action ... } 42 / 42
  55. plugins { id("gradle-plugin-how-to") version "0.1-ALPHA" } about { name =

    "Stefan May“ twitter = "https://twitter.com/@StefMa91" medium = "https://medium.com/@StefMa" } code { availableAt = "https://github.com/StefMa/Gloc" } 43