/gradle-kotlin-dsl-migration-guide

The missing migration guide to the Gradle Kotlin DSL

Primary LanguageKotlin

The missing migration guide to the Gradle Kotlin DSL

ℹ️

All of this guide, and more, has now been integrated into an official migration guide at gradle. Please refer to the official guide.

In case you didn’t know, Gradle build scripts can be written in Kotlin rather than Groovy. However, as far as I know, the Kotlin DSL has never been properly documented. The closest I found to a documentation is the set of examples in the kotlin-dsl project

This README hopefully constitutes the temporary missing guide to migrate from the Groovy DSL to the Kotlin DSL.

It assumes you already know the Groovy DSL, and that you’re familiar with the Kotlin language syntax.

Disclaimer

I’m by no means an expert of Gradle and even less of its Kotlin DSL. What follows is what I understood and tested. There might be better ways of doing, and this guide is not, at all, exhaustive. So issues and PRs are welcome.

File names

To use the Kotlin DSL, simply name your files build.gradle.kts instead of build.gradle.

The settings.gradle file can also be renamed settings.gradle.kts.

In a multi-project build, you can have some modules using the Groovy DSL (and thus use build.gradle files), and some other modules using the Kotlin DSL (and thus use build.gradle.kts files). So you’re not forced to migrate everything at once.

Applying built-in plugins

Using the plugins block:

Groovy
plugins {
    id 'java'
    id 'jacoco'
}
Kotlin
plugins {
    java
    id("jacoco")
}

As you can see with the jacoco example, the same syntax can be used in Groovy and Kotlin (except for double quotes and parentheses that must be used in Kotlin, of course).

But the Kotlin DSL also defines extension properties for all (AFAIK) built-in plugins, so you can use them, as shown above with the java example.

You can also use the older apply syntax:

Groovy
apply plugin: 'checkstyle'
Kotlin
apply(plugin = "checkstyle")

Applying external plugins

Using the plugins block:

Groovy
plugins {
    id 'org.springframework.boot' version '2.0.1.RELEASE'
}
Kotlin
plugins {
    id("org.springframework.boot") version "2.0.1.RELEASE"
}

You can also use the older apply syntax, but then the plugin must be added to the classpath of the build script:

Groovy
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath("gradle.plugin.com.boxfuse.client:gradle-plugin-publishing:5.0.3")
    }
}

apply plugin: 'org.flywaydb.flyway'
Kotlin
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath("gradle.plugin.com.boxfuse.client:gradle-plugin-publishing:5.0.3")
    }
}

apply(plugin = "org.flywaydb.flyway")

Applying the Kotlin plugin

If you’re using Kotlin to write your build scripts, you probably also use Kotlin in your project. Applying the Kotlin plugin is no different from applying any external other plugin. But the DSL has an extension function to make it shorter:

Groovy
plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.2.41'
}
Kotlin
plugins {
    kotlin("jvm") version "1.2.41"
}

Customizing an existing task

This is where Groovy and Kotlin start to differ. Since Kotlin is a statically typed language, and since you want to benefit from this static typing by discovering available properties and methods using auto-completion, you need to know and provide the type of the task you want to configure.

Here is how you can configure a single property of the existing jar task:

Groovy
jar.archiveName = 'foo.jar'
Kotlin
val jar: Jar by tasks
jar.archiveName = "foo.jar"

Note that specifying the type of the task explicitly is necessary. Otherwise, the script won’t compile because the inferred type of jar will be Task, and the archiveName property is specific to the Jar task.

You can, however, omit the type if you only need to configure properties or call methods declared in Task:

Groovy
test.doLast {
    println("test completed")
}
Kotlin
val test by tasks
test.doLast { println("test completed") }

If you need to configure several properties or call multiple methods on the same task you can group them in a block as follows:

Groovy
jar {
    archiveName = 'foo.jar'
    into('META-INF') {
        from('bar')
    }
}
Kotlin
val jar by tasks.getting(Jar::class) {
    archiveName = "foo.jar"
    into("META-INF") {
        from("bar")
    }
}

If you already have a val for the task you want to configure in scope, the Kotlin apply function is handy:

Kotlin
val jar: Jar by tasks
jar.apply {
    archiveName = "foo.jar"
    into("META-INF") {
        from("bar")
    }
}

But there is another idiomatic way to configure tasks: using a tasks block:

Groovy
jar {
    archiveName = 'foo.jar'
    into('META-INF') {
        from('bar')
    }
}

test.doLast {
    println("test completed")
}
Kotlin
tasks {
    "jar"(Jar::class) {
        archiveName = "foo.jar"
        into("META-INF") {
            from("bar")
        }
    }

    "test" {
        doLast { println("test completed") }
    }
}

Once again, note that if you need to apply task-specific configurations, you need to provide the type of the task (Jar in this example).

This means that you’ll sometimes need to dive in the documentation or source code of custom plugins to discover what the types of its custom tasks are, and to import them, or use their fully qualified name.

Groovy
plugins {
    id('java')
    id 'org.springframework.boot' version '2.0.1.RELEASE'
}

repositories {
    mavenCentral()
}

apply plugin: 'io.spring.dependency-management'

bootJar {
    archiveName = 'app.jar'
    mainClassName = 'com.ninja_squad.demo.Demo'
}

bootRun {
    main = 'com.ninja_squad.demo.Demo'
    args '--spring.profiles.active=demo'
}
Kotlin
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

plugins {
    java
    id("org.springframework.boot") version "2.0.1.RELEASE"
}

repositories {
    mavenCentral()
}

apply(plugin = "io.spring.dependency-management")

tasks {
    "bootJar"(BootJar::class) {
        archiveName = "app.jar"
        mainClassName = "com.ninja_squad.demo.Demo"
    }

    "bootRun"(BootRun::class) {
        main = "com.ninja_squad.demo.Demo"
        args("--spring.profiles.active=demo")
    }
}

Creating a task

Creating a task can be done by declaring delegated property, delegating to tasks.creating:

Groovy
task greeting {
    println('always printed: configuration phase')
    doLast {
        println('only printed if executed: execution phase')
    }
}
Kotlin
val greeting by tasks.creating {
    println("always printed: configuration phase")
    doLast {
        println("only printed if executed: execution phase")
    }
}

Sometimes you want to create a task of a given type (Zip in this example):

Groovy
task docZip(type: Zip) {
    archiveName = 'doc.zip'
    from 'doc'
}
Kotlin
val docZip by tasks.creating(Zip::class) {
    archiveName = "doc.zip"
    from("doc")
}

The same things can also be done using the tasks block:

Groovy
task greeting2 {
    println('always printed: configuration phase')
    doLast {
        println('only printed if executed: execution phase')
    }
}

task docZip2(type: Zip) {
    archiveName = 'doc.zip'
    from 'doc'
}
Kotlin
tasks {
    "greeting2" {
        println("always printed: configuration phase")
        doLast {
            println("only printed if executed: execution phase")
        }
    }

    "docZip2"(Zip::class) {
        archiveName = "doc2.zip"
        from("doc")
    }
}

Notice that creating a task uses the exact same syntax as customizing an existing task. This can be confusing, and even lead to bugs: your intention might be to customize an existing task, but if you use the wrong task name, you will end up creating a new task rather than customizing the existing task. The reader might also not know if your intention is to customize an existing task, or to create a new one. For these two reasons, you might prefer using these slightly more verbose variants, which clearly show your intent and avoid the previously described bug:

Kotlin
tasks {
    // get and customize the existing task named test. Fails if there is no test task.
    val test by getting {
        doLast { println("test completed") }
    }

    // create a new docZip3 task. Fails if a task docZip3 already exists.
    val docZip3 by creating(Zip::class) {
        archiveName = "doc3.zip"
        from("doc")
    }
}

Dependencies

Declaring dependencies in the existing Java configurations is not much different from doing it in Groovy:

Groovy
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'io.jsonwebtoken:jjwt:0.9.0'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude(module: 'junit')
    }
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
Kotlin
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("io.jsonwebtoken:jjwt:0.9.0")
    runtimeOnly("org.postgresql:postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(module = "junit")
    }
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

Custom configurations

Sometimes you need to add your own configuration, and add dependencies to that configuration:

Groovy
configurations {
    db
    integTestImplementation {
        extendsFrom testImplementation
    }
}

dependencies {
    db 'org.postgresql:postgresql'
    integTestImplementation 'com.ninja-squad:DbSetup:2.1.0'
}
Kotlin
val db by configurations.creating
val integTestImplementation by configurations.creating {
    extendsFrom(configurations["testImplementation"])
}

dependencies {
    db("org.postgresql:postgresql")
    integTestImplementation("com.ninja-squad:DbSetup:2.1.0")
}

Note that, in the above example, you can only use db(…​) and integTestImplementation(…​) because they’re both declared as properties before. If they were defined elsewhere, you could get them by delegating to configurations, or you could use a string to add a dependency to the configuration:

Kotlin
// get the existing testRuntimeOnly configuration
val testRuntimeOnly by configurations

dependencies {
    testRuntimeOnly("org.postgresql:postgresql")
    "db"("org.postgresql:postgresql")
    "integTestImplementation"("com.ninja-squad:DbSetup:2.1.0")
}

Extensions

Many plugins come with extensions to configure them. If those plugins are applied using the plugins block (which is true for the jacoco and the Spring Boot plugins in the following example), then Kotlin extension functions are made available to configure their extension, the same way as in Groovy.

On the other hand, if you use the older apply function to apply a plugin (which is true for the checkstyle plugin in the following example), you’ll have to use the configure<T> {} function to configure them:

Groovy
jacoco {
    toolVersion = "0.8.1"
}

springBoot {
    buildInfo {
        properties {
            time = null
        }
    }
}

checkstyle {
    maxErrors = 10
}
Kotlin
jacoco {
    toolVersion = "0.8.1"
}

springBoot {
    buildInfo {
        properties {
            time = null
        }
    }
}

configure<CheckstyleExtension> {
    maxErrors = 10
}