Getting started with the Gradle Kotlin DSL

Paul Merlin (@eskat0s) & Rodrigo B. de Oliveira (@rodrigobamboo) - Gradle Inc.

Who are we?

who s who

Gradle Kotlin DSL Team

Who are you?

  • How are you using Gradle?

  • What is your primary programming language?

What is Gradle?

Gradle’s purpose

Gradle is a build and automation tool.

  • JVM based

  • Implemented in Java

  • 100% Free Open Source - Apache Standard License 2.0

Agnostic Build System

  • Java ecosystem

    • Groovy, Kotlin, Scala, …​

  • Native ecosystem

    • C, C++, Swift, …​

  • Android

  • Misc

    • Python, Go, Asciidoctor, …​

Gradle Enterprise

gradle enterprise

What type of application are you building?

Agenda

  • Creating a Gradle Kotlin DSL build

  • Customizing the build

  • Organizing build logic

  • Authoring plugins and DSLs

Introduction

Gradle, the basics

  • Automate execution of configurable tasks

  • Groovy and Kotlin build scripts

  • A Java API with a Kotlin and Groovy DSL on top

  • Work avoidance, dependency management etc…​

  • Reusable functionality packaged as plugins

gradle task dag

A Java library

plugins {
   `java-library`
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

dependencies {
   api("com.acme:foo:1.0")
   implementation("com.zoo:monkey:1.1")
}

A native app

plugins {
    `cpp-application`
}

application {
    baseName = "my-app"
}

toolChains {
    // ...
}

Creating a Gradle Kotlin DSL build

Creating a Gradle Kotlin DSL build

  • With Gradle locally installed

  • gradle init

Creating a Gradle Kotlin DSL build

Creating a Gradle Kotlin DSL build

One-liner without Gradle installed

➜ mkdir webinar-app \
    && cd webinar-app \
    && curl https://gradle-initializr.cleverapps.io/starter.zip \
    -d type=kotlin-application \
    -d dsl=kotlin \
    -d projectName=webinar-app \
    -d gradleVersion=5.3.1 \
    -o webinar-app.zip \
    && unzip -o webinar-app.zip \
    && rm webinar-app.zip

Anatomy of the build

.
├── settings.gradle.kts
├── build.gradle.kts
├── gradlew
├── gradlew.bat
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
└── src
    ├── main
    │   └── kotlin
    │       └── *
    └── test
        └── kotlin
            └── *

The settings script

.
├── settings.gradle.kts                 ⬅
├── build.gradle.kts
├── gradlew
├── gradlew.bat
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
└── src
    ├── main
    │   └── kotlin
    │       └── *
    └── test
        └── kotlin
            └── *

settings.gradle.kts

rootProject.name = "webinar-app"

The root project build script

.
├── settings.gradle.kts
├── build.gradle.kts                    ⬅
├── gradlew
├── gradlew.bat
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
└── src
    ├── main
    │   └── kotlin
    │       └── *
    └── test
        └── kotlin
            └── *

build.gradle.kts

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.3.21"
    application
}
repositories {
    jcenter()
}
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.jetbrains.kotlin:kotlin-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
application {
    mainClassName = "webinar.app.AppKt"
}

The Gradle wrapper

.
├── settings.gradle.kts
├── build.gradle.kts
├── gradlew                             ⬅
├── gradlew.bat                         ⬅
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar          ⬅
│       └── gradle-wrapper.properties   ⬅
└── src
    ├── main
    │   └── kotlin
    │       └── *
    └── test
        └── kotlin
            └── *

The project sources

.
├── settings.gradle.kts
├── build.gradle.kts
├── gradlew
├── gradlew.bat
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
└── src
    ├── main
    │   └── kotlin
    │       └── *                       ⬅
    └── test
        └── kotlin
            └── *                       ⬅

Running the build

From the command line

DEMO

Working with the build

Using an IDE

DEMO

Customizing the build

build.gradle.kts

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.3.21"
    application
}
repositories {
    jcenter()
}
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.jetbrains.kotlin:kotlin-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
application {
    mainClassName = "webinar.app.AppKt"
}

Configuring the application plugin

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.3.21"
    application
}
repositories {
    jcenter()
}
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.jetbrains.kotlin:kotlin-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
application {
    mainClassName = "webinar.app.AppKt"
    applicationDefaultJvmArgs = listOf("-Xmx4m")
}

Running the app with the new arguments

  • demo with ./gradlew run -i

    • highligh the new -Xmx4m argument

  • demo ./gradlew install

    • open script and highlight new argument

Configuring the Kotlin compiler

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.3.21"
    application
}
repositories {
    jcenter()
}
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.jetbrains.kotlin:kotlin-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
application {
    mainClassName = "webinar.app.AppKt"
    applicationDefaultJvmArgs = listOf("-Xmx4m")
}

Building with the new configuration

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.3.21"
    application
}
repositories {
    jcenter()
}
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.jetbrains.kotlin:kotlin-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
application {
    mainClassName = "webinar.app.AppKt"
    applicationDefaultJvmArgs = listOf("-Xmx4m")
}
tasks {
    compileKotlin {
        kotlinOptions { allWarningsAsErrors = true }
    }
}

What we learned so far

  • we can change the behavior of the build by

    • configuring extensions and tasks

    • both contributed by plugins

    • in other words, plugins extend the Gradle DSL which can then be configured in scripts like the one we just saw

Going further: modularizing

DEMO

What we learned - modularizing

  • project evaluation order follows the hierarchy

  • script classpath is inherited

  • cross-configuration requires dynamic references

  • type-safe accessors are not always available

  • see the Kotlin DSL Primer in the User Manual for more information

Going further: interoperability

Using non statically typed APIs and DSLs from a statically typed language

DEMO

What we learned - interoperability

  • you can mix Kotlin and Groovy scripts

  • when types are not available, withGroovyBuilder {} to the rescue

  • see the Kotlin DSL Primer in the User Manual for more information

Organizing build logic

Patterns for organizing build logic

  • cross-configuration

  • buildSrc

  • sharing build logic between builds

Organizing build logic with buildSrc

.
├── buildSrc
│   ├── build.gradle.kts
│   ├── settings.gradle.kts
│   └── src
│       └── *
├── app
│   ├── build.gradle.kts
│   └── src
│       └── *
├── core
│   ├── build.gradle.kts
│   └── src
│       └── *
├── gradle
│   └── *
├── gradlew.bat
├── gradlew
├── build.gradle.kts
└── settings.gradle.kts

What we learned

  • buildSrc is the conventional build for build logic

  • the kotlin-dsl plugin brings the Gradle Kotlin DSL support to Kotlin source sets

  • Gradle Kotlin DSL scripts in Kotlin source sets are exposed as Gradle plugins

  • see the Kotlin DSL Primer in the User Manual for more information

Sharing build logic between builds

.
├── build-logic
│   ├── build.gradle.kts
│   ├── settings.gradle.kts
│   └── src
│       └── *
└── webinar-app
    ├── app
    │   ├── build.gradle.kts
    │   └── src
    │       └── *
    ├── core
    │   ├── build.gradle.kts
    │   └── src
    │       └── *
    ├── gradle
    │   └── *
    ├── gradlew
    ├── gradlew.bat
    ├── build.gradle.kts
    └── settings.gradle.kts

Authoring plugins

Authoring plugins

Sharing plugins with a wider audience

  • Publishing plugins

  • DSLs that play well with both Kotlin, Groovy and other JVM languages

  • Plugins that can be used across Gradle versions

Publishing plugins

  • To the Gradle Plugin Portal or an internal repository

  • Well covered in the Plugin Development guides

  • Doesn’t prevent the included build workflow

  • Which is orthogonal to choosing mono-repo vs. multi-repo

DSLs that play well with JVM languages

  • Diverse build script DSLs languages - Kotlin, Groovy

  • Diverse plugin implementation languages - any JVM language

  • Common denominator is static typing

  • Applicable to every JVM language

  • It’s always easier to use statically typed APIs from dynamically typed languages than the other way around

DSLs that play well with JVM languages

Prefer exposing statically typed APIs over dynamically or unityped APIs

enum class MyOption { NONE, FEW, LOTS, ALL }

// The good 👍
interface MyDslType {

    fun doSomethingOn(option: MyOption)

    // Optionally
    fun doSomethingOn(option: String) =
        doSomethingOn(MyOption.valueOf(option.toUpperCase()))
}

// The bad 👎
interface MyDslType {
    fun doSomethingOn(option: Any)
}

DSLs that play well with JVM languages

Prefer Gradle types over language specific types

interface MyDslType {
    // The good 👍
    void doThis(org.gradle.api.Action<Type> block);
}
interface MyDslType {
    // The bad 👎
    fun doThis(block: Typed.() -> Unit)
}
interface MyDslType {
    // The ugly 👹
    def doThis(groovy.lang.Closure block)
}

Plugins that can be used across Gradle versions

Always start with / aim for the latest Gradle version

  • In order to take advantage of all the goodness

  • Apply best practices (see the Plugin Development guides)

  • Do cross-version testing

  • Can be as simple as JUnit parameterized tests driving TestKit

Plugins that can be used across Gradle versions

Cross-version testing with JUnit

abstract class AbstractPluginTest(val gradleVersion: String) {
    companion object {
        @Parameterized.Parameters(name = "Gradle {0}")
        @JvmStatic
        fun testedGradleVersions() = listOf("5.3.1", ...., "4.7")   ⬅
    }

    fun gradleRunnerFor(vararg arguments: String) =
        GradleRunner.create()
            .withGradleVersion(gradleVersion)                       ⬅
            .withPluginClasspath()
            .withProjectDir(rootDir)
            .withArguments(*arguments)
}

Plugins that can be used across Gradle versions

Incrementally support Gradle versions

  • Add Gradle version to the test matrix

  • Fix as needed (see the Upgrading Gradle section of the User Manual)

  • Repeat

  • Stop when satisfied or overwhelmed

Plugins that can be used across Gradle versions

Supporting multiple Gradle versions

val myTaskConfiguration: MyTask.() -> Unit = {
    // ...
}
if (GradleVersion.current() >= GradleVersion.version("4.9")) {
    // Support lazy task registration
    val myTask = tasks.register("my", MyTask::class.java, myTaskConfiguration)
    // ...
} else {
    // Eager task creation
    val myTask = tasks.create("my", MyTask::class.java, myTaskConfiguration)
    // ...
}

Wrap up

What we learned

  • Creating a Gradle Kotlin DSL build with or without Gradle installed

  • Customizing builds

    • by configuring extensions and tasks contributed by plugins

    • by mixing Kotlin and Groovy build logic

    • by modularizing into projects

  • Organizing build logic using cross-configuration, buildSrc and shared plugins

  • Authoring plugins and DSLs

    • that play well with different DSLs and JVM languages

    • that support multiple Gradle versions

Questions?

Thank you!