Can Kotlin save me from my Groovy buildscripts? Droidcon Berlin 2018

Can Kotlin save me from my Groovy buildscripts? Droidcon Berlin 2018

This talk covers
* Migration examples from Groovy to Kotlin in buildscripts.
* Some advantages and disadvantages of Groovy vs Kotlin.
* How the Kotlin-DSL works under the hood.

This was presented on June 27th at Droidcon Berlin 2018.

Video: https://www.youtube.com/watch?v=Rwrja9WCTS0

5f69045a2ca496221cfc624405917cdf?s=128

Nelson Osacky

June 27, 2018
Tweet

Transcript

  1. Groovy to Kotlin in build scripts Nelson Osacky

  2. None
  3. •Some Pros and Cons •Examples •How it works under the

    hood
  4. Gradle Kotlin DSL v0.17.5

  5. Type Safety

  6. None
  7. if (isCi) { foo }A

  8. if (isCi) { foo }A

  9. Auto Complete

  10. • autocompletion gif

  11. Slower builds

  12. What is buildSrc?

  13. None
  14. None
  15. None
  16. None
  17. None
  18. Less docs https://github.com/gradle/kotlin-dsl/tree/master/samples

  19. None
  20. Must dig in source

  21. Red lines in IDE

  22. None
  23. Missing features

  24. import test.zen.notUsableInBuildscript buildscript { usableInBuildScript() // ext extension function not

    usable ext["kotlin_version"] = "1.2.50" // imported function is not usable notUsableInBuildscript() }
  25. Pros • Type Safety • Auto Complete • Kotlin Cons

    • Slower builds • Less examples • Pre-release state • Source digging
  26. Example Migrations

  27. build.gradle -> build.gradle.kts

  28. settings.gradle.kts

  29. include ':app' include ':data' include ':service'

  30. include( ":app", ":data", ":service" )

  31. Kotlin Extension Functions and Properties

  32. Kotlin provides the ability to extend a class with new

    functionality without having to inherit from the class. This is done via special declarations called extensions. 
 
 Kotlin supports extension functions and extension properties.
  33. // Swap Extension Function // 'this' corresponds to the list

    fun MutableList<Int>.swap(index1: Int, index2: Int) { val tmp = this[index1] this[index1] = this[index2] this[index2] = tmp }
  34. // Last Index Extension Property val <T> List<T>.lastIndex: Int get()

    = size - 1
  35. build.gradle.kts

  36. plugins

  37. apply plugin: 'java-library' apply plugin: 'kotlin' apply plugin: 'com.android.library' targetCompatibility

    = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8 dependencies { testCompile deps.junit compileOnly deps.support.annotations implementation deps.kotlin }
  38. apply plugin: 'java-library' apply plugin: 'kotlin' apply plugin: 'com.android.library'

  39. plugins { `java-library` kotlin("jvm") id("com.android.library") }A

  40. plugins { `java-library` kotlin("jvm") id("com.android.library") }A java { targetCompatibility =

    JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8 }B dependencies { testImplementation(deps("junit")) compileOnly((deps("support") as Map<*, *>) ["annotations"].toString()) }C
  41. plugins { `java-library` kotlin("jvm") id("com.android.library") }A

  42. plugins { `java-library` kotlin("jvm") id("com.android.library") }A

  43. /** * Configures the plugin dependencies for this project. *

    * @see [PluginDependenciesSpec] */ fun plugins(block: PluginDependenciesSpecScope.() -> Unit): Unit
  44. /** * Receiver forathe `plugins`ablock. * * This class exists

    forathe sole purpose of markingathe `plugins` blockaas a [GradleDsl] thus * hiding all members provided by theaouter [KotlinBuildScript] scope. * * @see [PluginDependenciesSpec] */ @GradleDsl class PluginDependenciesSpecScope(plugins: PluginDependenciesSpec) : PluginDependenciesSpec by plugins
  45. @Incubating public interface PluginDependenciesSpec { /** * Add abdependency on

    the plugin with the given id. * * plugins { * id "org.company.myplugin" * } * * @param id the id ofbthe plugin to depend on * @returnca mutable plugin dependency specification that can be used to further refine the dependency */ PluginDependencySpec id(String id); }
  46. plugins { `java-library` kotlin("jvm") id("com.android.library") }A

  47. plugins { `java-library` kotlin("jvm") id("com.android.library") }A

  48. plugins { `java-library` kotlin("jvm") id("com.android.library") }A

  49. /** * Applies the given Kotlin plugin [module]. * *

    For example: `plugins { kotlin("jvm") version "1.2.21" }` * * @param module simple name of the Kotlin Gradle plugin module, for example "jvm", "android", "kapt", "plugin.allopen" etc... */ fun PluginDependenciesSpec.kotlin(module: String): PluginDependencySpec = id("org.jetbrains.kotlin.$module") org.gradle.kotlin.dsl.KotlinDependencyExtensions.kt
  50. """ /** * Applies the given Kotlin plugin [module]. *

    * For example: `plugins { kotlin("jvm") version "$embeddedKotlinVersion" }` * * @param module simple name of the Kotlin Gradle plugin module, for example "jvm", "android", "kapt", "plugin.allopen" etc... */ fun PluginDependenciesSpec.kotlin(module: String): PluginDependencySpec = id(“org.jetbrains.kotlin.${‘$’}module”) """ 
 buildSrc/src/main/kotlin/codegen/GenerateKotlinDependencyExtensions.kt
  51. val generateKotlinDependencyExtensions by task<GenerateKotlinDependencyExtensions> { val publishedPluginsVersion: String by rootProject.extra

    outputFile = File(apiExtensionsOutputDir, "org/gradle/kotlin/dsl/ KotlinDependencyExtensions.kt") embeddedKotlinVersion = kotlinVersion kotlinDslPluginsVersion = publishedPluginsVersion kotlinDslRepository = kotlinRepo } provider/build.gradle.kts
  52. plugins { `java-library` kotlin("jvm") id("com.android.library") }A

  53. plugins { `java-library` kotlin("jvm") id("com.android.library") }A

  54. /** * The builtin Gradle plugin implemented by [org.gradle.api.plugins.JavaLibraryPlugin]. */

    inline val PluginDependenciesSpec.`java-library`: PluginDependencySpec get() = id("org.gradle.java-library") BuiltInPluginExtensions.kt
  55. internal fun generateApiExtensionsJar(outputFile: File, gradleJars: Collection<File>, onProgress: () -> Unit)

    { ApiExtensionsJarGenerator(onProgress = onProgress).generate(outputFile, gradleJars) } org.gradle.kotlin.dsl.codegen.ApiExtensionsJar.kt
  56. apply plugin: 'java-library' apply plugin: 'kotlin' targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility

    = JavaVersion.VERSION_1_8 dependencies { testImplementation 'junit:junit:4.12' compileOnly 'com.android.support:support- annotations:27.1.0' }A
  57. dependencies { testImplementation 'junit:junit:4.12' }A

  58. dependencies { testImplementation("junit:junit:4.12") }A

  59. plugins { `java-library` kotlin("jvm") } java { targetCompatibility = JavaVersion.VERSION_1_8

    sourceCompatibility = JavaVersion.VERSION_1_8 } dependencies { testImplementation("junit:junit:4.12") compileOnly("com.android.support:support- annotations:27.1.0") }A
  60. apply plugin: 'java-library' apply plugin: 'kotlin' targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility

    = JavaVersion.VERSION_1_8 dependencies { testImplementation 'junit:junit:4.12' compileOnly 'com.android.support:support- annotations:27.1.0' }A
  61. plugins { `java-library` kotlin("jvm") } java { targetCompatibility = JavaVersion.VERSION_1_8

    sourceCompatibility = JavaVersion.VERSION_1_8 } dependencies { testImplementation("junit:junit:4.12") compileOnly("com.android.support:support- annotations:27.1.0") }A
  62. ext block

  63. ext.deps = [ 'junit' : "junit:junit:4.12", 'support' : [ 'annotations':

    "com.android.support:support-annotations:$ {versions.supportLibrary}", ], ]
  64. apply plugin: 'java-library' apply plugin: 'kotlin' targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility

    = JavaVersion.VERSION_1_8 dependencies { testImplementation deps.junit compileOnly deps.support.annotations implementation deps.kotlin }W
  65. dependencies { testImplementation deps.junit compileOnly deps.support.annotations }W

  66. dependencies { testImplementation deps.junit }W

  67. dependencies { testImplementation(deps("junit")) }W

  68. dependencies { testImplementation deps.junit }Q 
 dependencies { testImplementation(deps("junit")) }W

  69. dependencies { testImplementation deps.junit }Q 
 dependencies { testImplementation(deps("junit")) }W

    fun Project.deps(key: String): Any { return (rootProject.ext["deps"] as Map<*, *>)[key]!! }D
  70. fun Project.deps(key: String): Any { return (rootProject.ext["deps"] as Map<*, *>)[key]!!

    }D
  71. fun Project.deps(key: String): Any { return (rootProject.ext["deps"] as Map<*, *>)[key]!!

    }D ext.deps = [ 'junit' : "junit:junit:4.12", 'support' : [ 'annotations': "com.android.support:support-annotations:$ {versions.supportLibrary}", ], ]
  72. fun Project.deps(key: String): Any { return (rootProject.ext["deps"] as Map<*, *>)[key]!!

    }D
  73. dependencies { testImplementation(deps(“junit")) compileOnly((deps("support") as Map<*, *>)["annotations"].toString()) }A fun Project.deps(key:

    String): Any { return (rootProject.ext["deps"] as Map<*, *>)[key]!! }D
  74. plugins { `java-library` kotlin("jvm") } java { targetCompatibility = JavaVersion.VERSION_1_8

    sourceCompatibility = JavaVersion.VERSION_1_8 }A dependencies { testImplementation(deps("junit")) compileOnly((deps("support") as Map<*, *>)["annotations"].toString()) implementation(deps("kotlin")) }
  75. apply plugin: 'java-library' apply plugin: 'kotlin' targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility

    = JavaVersion.VERSION_1_8 dependencies { testImplementation deps.junit compileOnly deps.support.annotations implementation deps.kotlin }W
  76. targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8

  77. java { targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8 }A

  78. plugins { `java-library` kotlin("jvm") } java { targetCompatibility = JavaVersion.VERSION_1_8

    sourceCompatibility = JavaVersion.VERSION_1_8 }A dependencies { testImplementation(deps("junit")) compileOnly((deps("support") as Map<*, *>)["annotations"].toString()) implementation(deps("kotlin")) }
  79. None
  80. ext properties

  81. // Groovy ext.isCi = System.getenv("CI") == "true"

  82. // Kotlin ext["isCi"] = System.getenv("CI") == "true"

  83. // Groovy ext.isCi = System.getenv("CI") == “true" // Kotlin ext["isCi"]

    = System.getenv("CI") == "true"
  84. // Kotlin ext["isCi"] = System.getenv("CI") == "true"

  85. val isCi : Boolean get() = ext["isCi"].toString().toBoolean()

  86. // Groovy if (isCi) { // Do stuff on CI

    }B
  87. // Kotlin if (isCi) { // Do stuff on CI

    }B
  88. // Groovy if (isCi) { // Do stuff on CI

    }A // Kotlin if (isCi) { // Do stuff on CI }B
  89. // Groovy if (isCi) { // Do stuff on CI

    }A // Kotlin if (isCi) { // Do stuff on CI }B val isCi : Boolean get() = ext["isCi"].toString().toBoolean()
  90. Java Compatibility

  91. targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8

  92. java { targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8 }A

  93. public void setSourceCompatibility(Object value) { setSourceCompatibility(JavaVersion.toVersion(value)); } public void setSourceCompatibility(JavaVersion

    value) { srcCompat = value; } public void setTargetCompatibility(Object value) { setTargetCompatibility(JavaVersion.toVersion(value)); } public void setTargetCompatibility(JavaVersion value) { targetCompat = value; } org.gradle.api.JavaPluginConvention.java
  94. public void apply(ProjectInternal project) { project.getPluginManager().apply(JavaBasePlugin.class); JavaPluginConvention javaConvention = project.getConvention()

    .getPlugin(JavaPluginConvention.class); } org.gradle.api.plugins.JavaPlugin.java
  95. /** * Retrieves the [java][org.gradle.api.plugins.JavaPluginConvention] project convention. */ val Project.`java`:

    org.gradle.api.plugins.JavaPluginConvention get() = convention.getPluginByName<org.gradle.api.plugins.JavaPluginConvention>("java") /** * Configures the [java][org.gradle.api.plugins.JavaPluginConvention] project convention. */ fun Project.`java`(configure: org.gradle.api.plugins.JavaPluginConvention.() -> Unit): Unit = configure(`java`) org.gradle.kotlin.dsl.accessors.kt
  96. java { sourceCompatibility = JavaVersion.VERSION_1_8 } withConvention(JavaPluginConvention::class, { sourceCompatibility =

    JavaVersion.VERSION_1_8 }) (this as HasConvention).convention.getPlugin(JavaPluginConvention::class).run { sourceCompatibility = JavaVersion.VERSION_1_8 }
 
 the<JavaPluginConvention>().apply { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 }
  97. /** * Returns the plugin convention or extension of the

    specified type. */ inline fun <reified T : Any> Project.the(): T = typeOf<T>().let { type -> convention.findByType(type) ?: convention.findPlugin(T::class.java) ?: convention.getByType(type) } org.gradle.kotlin.dsl.ProjectExtensions.kt
  98. None
  99. accessors.kt

  100. java { sourceCompatibility = JavaVersion.VERSION_1_8 }

  101. /** * Retrieves the [java][org.gradle.api.plugins.JavaPluginConvention] project convention. */ val Project.`java`:

    org.gradle.api.plugins.JavaPluginConvention get() = convention.getPluginByName<org.gradle.api.plugins.JavaPluginConvention>("java") /** * Configures the [java][org.gradle.api.plugins.JavaPluginConvention] project convention. */ fun Project.`java`(configure: org.gradle.api.plugins.JavaPluginConvention.() -> Unit): Unit = configure(`java`) org.gradle.kotlin.dsl.accessors.kt
  102. private fun writeAccessorsFor(projectSchema: ProjectSchema<TypeAccessibility>, writer: BufferedWriter) { writer.apply { write(fileHeader)

    newLine() appendln("import org.gradle.api.Project") appendln("import org.gradle.api.artifacts.Configuration") appendln("import org.gradle.api.artifacts.ConfigurationContainer") appendln("import org.gradle.api.artifacts.Dependency") appendln("import org.gradle.api.artifacts.ExternalModuleDependency") appendln("import org.gradle.api.artifacts.ModuleDependency") appendln("import org.gradle.api.artifacts.dsl.DependencyHandler") newLine() appendln("import org.gradle.kotlin.dsl.*") newLine() projectSchema.forEachAccessor { appendln(it) } } } org.gradle.kotlin.dsl.accessors.AccessorsClassPath.kt
  103. internal fun ProjectSchema<TypeAccessibility>.forEachAccessor(action: (String) -> Unit) { val seen =

    SeenAccessorSpecs() extensions.mapNotNull(::typedAccessorSpec).forEach { spec -> extensionAccessorFor(spec)?.let { extensionAccessor -> action(extensionAccessor) seen.add(spec) } } conventions.mapNotNull(::typedAccessorSpec).filterNot(seen::hasConflict).forEach { spec -> conventionAccessorFor(spec)?.let(action) } configurations.map(::accessorNameSpec).forEach { spec -> configurationAccessorFor(spec)?.let(action) } } org.gradle.kotlin.dsl.accessors.GodeGenerator.kt
  104. private fun accessibleExtensionAccessorFor(targetType: String, name: AccessorNameSpec, type: String): String =

    name.run { """ /** * Retrieves the [$original][$type] extension. */ val $targetType.`$kotlinIdentifier`: $type get() = $thisExtensions.getByName("$stringLiteral") as $type /** * Configures the [$original][$type] extension. */ fun $targetType.`$kotlinIdentifier`(configure: $type.() -> Unit): Unit = $thisExtensions.configure("$stringLiteral", configure) """ } org.gradle.kotlin.dsl.accessors.GodeGenerator.kt
  105. None
  106. Named Parameters

  107. // Groovy // Ensure the no-op leakcanary dependency is always

    used in JVM tests. configurations.all { config -> if (config.name.contains("UnitTest")) { config.resolutionStrategy.eachDependency { details -> if (details.requested.group == "com.squareup.leakcanary" && details.requested.name == "leakcanary-android") { details.useTarget(group: details.requested.group, name: "leakcanary-android-no-op", version: details.requested.version) }A }B }C }D https://github.com/square/leakcanary/wiki/FAQ#how-do-i-disable-leakcanary-in-tests
  108. details.useTarget( group: details.requested.group, name: "leakcanary-android-no-op", version: details.requested.version ) https://github.com/square/leakcanary/wiki/FAQ#how-do-i-disable-leakcanary-in-tests

  109. useTarget( mapOf( "group" to requested.group, "name" to “leakcanary-android-no-op", "version" to

    requested.version ) )
  110. // Kotlin // Ensure the no-op leakcanary dependency is always

    used in JVM tests. configurations.all { if (name.contains("UnitTest")) { resolutionStrategy.eachDependency { if (requested.group == "com.squareup.leakcanary" && requested.name == “leakcanary-android") { useTarget(mapOf("group" to requested.group, "name" to "leakcanary-android-no-op", "version" to requested.version)) }A }B }C }D
  111. // Groovy // Ensure the no-op leakcanary dependency is always

    used in JVM tests. configurations.all { config -> if (config.name.contains("UnitTest")) { config.resolutionStrategy.eachDependency { details -> if (details.requested.group == "com.squareup.leakcanary" && details.requested.name == "leakcanary-android") { details.useTarget(group: details.requested.group, name: "leakcanary-android-no-op", version: details.requested.version) }A }B }C }D
  112. // Kotlin // Ensure the no-op leakcanary dependency is always

    used in JVM tests. configurations.all { if (name.contains("UnitTest")) { resolutionStrategy.eachDependency { if (requested.group == "com.squareup.leakcanary" && requested.name == “leakcanary-android") { useTarget(mapOf("group" to requested.group, "name" to "leakcanary-android-no-op", "version" to requested.version)) }A }B }C }D
  113. None
  114. • Experiments are fun • Build speeds are slower •

    Kotlin DSL is still pre-release • Improvements happening all the time • Everything is an extension function
  115. Would I recommend it?

  116. Migrate one file and try it!

  117. Thanks nelson@osacky.com Questions? https://github.com/gradle/kotlin-dsl/tree/master/samples

  118. Gradle Tasks

  119. task downloadAndUnzipGcloud(dependsOn: downloadGCloud, type: Copy) { description "Unzip gcloud tools

    in to the build directory" from tarTree(downloadGCloud.dest) into new File(buildDir, "gcloud/") }A
  120. tasks { val downloadAndUnzipGcloud by creating(Copy::class) { description = "Unzip

    gcloud tools in to the build directory" from(tarTree(downloadGCloud.dest)) into(File(buildDir, "gcloud/")) dependsOn(downloadGCloud) }A }
  121. /** * Provides a property delegate that creates elements of

    theagiven [type] with theagiven [configuration]. */ fun <T : Any, U : T> PolymorphicDomainObjectContainer<T>.creating(type: KClass<U>, configuration: U.() -> Unit) = creating(type.java, configuration)
  122. tasks { val downloadAndUnzipGcloud by creating(Copy::class) { description = "Unzip

    gcloud tools in to the build directory" from(tarTree(downloadGCloud.dest)) into(File(buildDir, "gcloud/")) dependsOn(downloadGCloud) }A }
  123. /** * <p>Returns the tasks of this project.</p> * *

    @return the tasks of this project. */ TaskContainer getTasks();
  124. /** * <p>A {@code TaskContainer} is responsible for managing a

    set of {@link Task} instances.</p> * * <p>You can obtain a {@code TaskContainer} instance by calling {@link org.gradle.api.Project#getTasks()}, or using the * {@code tasks} property in your build script.</p> */ @HasInternalProtocol public interface TaskContainer extends TaskCollection<Task>, PolymorphicDomainObjectContainer<Task> {
  125. /** * Provides a property delegate that creates elements of

    the given [type] with the given [configuration]. */ fun <T : Any, U : T> PolymorphicDomainObjectContainer<T>.creating(type: KClass<U>, configuration: U.() -> Unit) = creating(type.java, configuration)
  126. tasks { val downloadAndUnzipGcloud by creating(Copy::class) { description = "Unzip

    gcloud tools in to the build directory" from(tarTree(downloadGCloud.dest)) into(File(buildDir, "gcloud/")) dependsOn(downloadGCloud) }A }