$30 off During Our Annual Pro Sale. View Details »

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. How to write Gradle plugins
    in Kotlin
    1 / 42

    View Slide

  2. 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

    View Slide

  3. 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

    View Slide

  4. • Why?
    • The Project: Gloc (Gradle Lines Of Code)
    • How to write a Plugin
    3 / 42

    View Slide

  5. • 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

    View Slide

  6. • 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

    View Slide

  7. • Why?
    • The Project: Gloc (Gradle Lines Of Code)
    • How to write a Plugin
    6 / 42

    View Slide

  8. plugins {
    id("guru.stefma.gloc") version "0.0.1"
    }
    gloc {
    enabled = true
    dirs = arrayOf("src/main", "src/test")
    }
    7 / 42

    View Slide

  9. ./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

    View Slide

  10. 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

    View Slide

  11. 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

    View Slide

  12. 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

    View Slide

  13. cat /Users/stefan/Developer/DroidconItaly/Gloc/gloc/build/gloc/gloc.txt
    Directory 'main':
    'kt' has '145' LOC in sum
    Directory 'test':
    'kt' has '413' LOC in sum
    9 / 42

    View Slide

  14. • Why?
    • The Project: Gloc
    • How to write a Plugin
    10 / 42

    View Slide

  15. 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

    View Slide

  16. 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

    View Slide

  17. plugins {
    kotlin("jvm") version ("1.2.30")
    id("java-gradle-plugin")
    id("maven-publish")
    }
    12 / 42

    View Slide

  18. lugins {
    kotlin("jvm") version ("1.2.30")
    id("java-gradle-plugin")
    id("maven-publish")
    • applies java plugin
    • gradleApi() dependency
    • gradleTestKit() dependency
    13 / 42

    View Slide

  19. plugins {
    kotlin("jvm") version ("1.2.30")
    id("java-gradle-plugin")
    id("maven-publish")
    }
    repositories { 14 / 42

    View Slide

  20. }
    repositories {
    jcenter()
    }
    dependencies {
    implementation(kotlin("stdlib-jdk8"))
    testImplementation("org.assertj:assertj-core:3.9.0")
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.1.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.1.0")
    }
    14 / 42

    View Slide

  21. testImplementation("org.junit.jupiter:junit-jupiter-api:5.1.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.1.0")
    }
    (tasks.findByName("test") as Test).useJUnitPlatform()
    group = "guru.stefma.gloc"
    version = "0.0.1"
    gradlePlugin {
    plugins {
    create("gloc") {
    id = "guru.stefma.gloc"
    implementationClass = "guru.stefma.gloc.GlocPlugin"
    }
    }
    }
    14 / 42

    View Slide

  22. open class GlocPlugin : Plugin {
    override fun apply(project: Project) {
    println("Hello Plugin \\o/")
    }
    }
    15 / 42

    View Slide

  23. val buildResult = GradleRunner.create()
    .withProjectDir(tempDir)
    .withPluginClasspath()
    .withArguments("tasks")
    .build()
    16 / 42

    View Slide

  24. 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

    View Slide

  25. open class GlocExtension {
    var enabled: Boolean = true
    var dirs: Array = emptyArray()
    }
    18 / 42

    View Slide

  26. override fun apply(project: Project) {
    println("Hello Plugin \\o/")
    val extension = project.extensions.run {
    create("gloc", GlocExtension::class.java)
    }
    }
    19 / 42

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide

  30. 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

    View Slide

  31. plugins {
    id("guru.stefma.gloc") version "0.0.1"
    }
    gloc {
    enabled = true
    dirs = arrayOf("src/main", "src/test")
    }
    23 / 42

    View Slide

  32. open class GlocTask : DefaultTask() {
    @TaskAction
    fun action() {
    println("Hello from Task")
    }
    }
    24 / 42

    View Slide

  33. 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

    View Slide

  34. File(tempDir, "build.gradle").run {
    writeText(
    """
    plugins {
    id("guru.stefma.gloc")
    }
    """
    )
    }
    val buildResult = GradleRunner.create()
    .withProjectDir(tempDir)
    .withPluginClasspath()
    .withArguments("gloc")
    .build()
    26 / 42

    View Slide

  35. """
    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

    View Slide

  36. 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

    View Slide

  37. 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

    View Slide

  38. 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

    View Slide

  39. 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

    View Slide

  40. 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

    View Slide

  41. 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

    View Slide

  42. 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

    View Slide

  43. 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

    View Slide

  44. 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

    View Slide

  45. )
    }
    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

    View Slide

  46. @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

    View Slide

  47. @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

    View Slide

  48. @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

    View Slide

  49. 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

    View Slide

  50. 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

    View Slide

  51. 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

    View Slide

  52. 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

    View Slide

  53. @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

    View Slide

  54. @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

    View Slide

  55. open class GlocTask : DefaultTask() {
    @OutputFile
    ...
    @InputFile
    ...
    @TaskAction
    ...
    }
    39 / 42

    View Slide

  56. open class GlocTask : DefaultTask() {
    @OutputFile
    ...
    @InputFile
    ...
    @TaskAction
    ...
    }
    @CacheableTask
    39 / 42

    View Slide

  57. 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

    View Slide

  58. 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

    View Slide

  59. 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

    View Slide

  60. }
    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

    View Slide

  61. .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

    View Slide

  62. 41 / 42

    View Slide

  63. • Created the project
    42 / 42

    View Slide

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

    View Slide

  65. • 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  69. • 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 = emptyArray()
    }
    42 / 42

    View Slide

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

    View Slide

  71. • 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

    View Slide

  72. 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

    View Slide