Slide 1

Slide 1 text

How to write Gradle plugins in Kotlin 1 / 42

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

• 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

Slide 6

Slide 6 text

• 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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

 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

Slide 16

Slide 16 text

 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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

} 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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

@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

Slide 47

Slide 47 text

@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

Slide 48

Slide 48 text

@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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

@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

Slide 54

Slide 54 text

@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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

} 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

Slide 61

Slide 61 text

.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

Slide 62

Slide 62 text

41 / 42

Slide 63

Slide 63 text

• Created the project 42 / 42

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

• 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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

• 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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

• 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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

• 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

Slide 72

Slide 72 text

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