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

Can Kotlin save me from my Groovy buildscripts?...

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

Nelson Osacky

June 27, 2018
Tweet

More Decks by Nelson Osacky

Other Decks in Technology

Transcript

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

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

    • Slower builds • Less examples • Pre-release state • Source digging
  3. 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.
  4. // 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 }
  5. 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 }
  6. 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
  7. /** * Configures the plugin dependencies for this project. *

    * @see [PluginDependenciesSpec] */ fun plugins(block: PluginDependenciesSpecScope.() -> Unit): Unit
  8. /** * 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
  9. @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); }
  10. /** * 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
  11. """ /** * 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
  12. 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
  13. /** * 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
  14. internal fun generateApiExtensionsJar(outputFile: File, gradleJars: Collection<File>, onProgress: () -> Unit)

    { ApiExtensionsJarGenerator(onProgress = onProgress).generate(outputFile, gradleJars) } org.gradle.kotlin.dsl.codegen.ApiExtensionsJar.kt
  15. 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
  16. 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
  17. 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
  18. 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
  19. ext.deps = [ 'junit' : "junit:junit:4.12", 'support' : [ 'annotations':

    "com.android.support:support-annotations:$ {versions.supportLibrary}", ], ]
  20. 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
  21. dependencies { testImplementation deps.junit }Q 
 dependencies { testImplementation(deps("junit")) }W

    fun Project.deps(key: String): Any { return (rootProject.ext["deps"] as Map<*, *>)[key]!! }D
  22. 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}", ], ]
  23. 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")) }
  24. 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
  25. 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")) }
  26. // Groovy if (isCi) { // Do stuff on CI

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

    }A // Kotlin if (isCi) { // Do stuff on CI }B val isCi : Boolean get() = ext["isCi"].toString().toBoolean()
  28. 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
  29. /** * 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
  30. 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 }
  31. /** * 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
  32. /** * 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
  33. 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
  34. 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
  35. 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
  36. // 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
  37. // 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
  38. // 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
  39. // 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
  40. • Experiments are fun • Build speeds are slower •

    Kotlin DSL is still pre-release • Improvements happening all the time • Everything is an extension function
  41. 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
  42. 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 }
  43. /** * 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)
  44. 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 }
  45. /** * <p>Returns the tasks of this project.</p> * *

    @return the tasks of this project. */ TaskContainer getTasks();
  46. /** * <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> {
  47. /** * 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)
  48. 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 }