1. Overview

Groovy is the default language for writing Gradle scripts. However, since Gradle version 5.0, we can write these scripts in Kotlin, too.

In this tutorial, we’ll look at how we can write Gradle scripts in Kotlin. We will also look at some advantages and disadvantages of using Kotlin DSL scripts.

2. How to Create a Kotlin DSL Script

To write Kotlin DSL scripts, we need to use Gradle version 5.0 or later. To activate the Kotlin DSL and get the best IDE support, we need to use the following extensions:

  • .gradle.kts instead of .gradle for our build scripts
  • .settings.gradle.kts instead of .settings.gradle for all settings scripts
  • .init.gradle.kts instead of .init.gradle for initialization scripts

3. How to Write a Basic Gradle Script for a Java Library

In this section, we’ll go through the different building blocks of a Gradle script written in Kotlin DSL script. We will also look at the differences compared to when writing the same script in Groovy DSL.

3.1. Applying Plugins

We can apply the java-library plugin, which is a core plugin:

plugins {
    `java-library`
}

Please note that we don’t need to specify the id function – unlike in Groovy DSL – for core plugins.

We can apply a community plugin by specifying the fully qualified plugin id and version:

plugins {
    id("org.flywaydb.flyway") version "8.0.2"
}

3.2. Declaring Dependencies

We can declare different types of dependencies using type-safe accessors:

dependencies {
    api("com.google.inject:guice:5.0.1")
    implementation("com.google.guava:guava:31.0-jre")
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

The important thing to note here is that the Kotlin DSL makes all four accessors available in the body of the Gradle script immediately after the plugins {} block. This means that when composing this script, we will have type-safe auto-completion in supported IDEs. This results in a fast and superior scripting experience.

3.3. Declaring Repositories

We can declare both built-in and custom repositories using type-safe accessors:

repositories {
    mavenCentral()
    maven {
        url = uri("https://maven.springframework.org/release")
    }
}

While resolving a dependency, Gradle will first check the maven-central repository followed by the springframework repository.

3.4. Configuring Source Sets

Let’s say we want to define a separate source set for our integration tests. The project layout is:

gradle-kotlin-dsl 
  ├── src 
  │    └── main 
  │    |    ├── java 
  │    |    │    ├── DefaultSorter.java
  │    |    │    ├── InMemoryRepository.java
  │    |    │    ├── Reporter.java
  │    |    │    ├── Repository.java
  │    |    │    ├── Sorter.java
  │    ├── test 
  │    |    ├── java 
  │    |    │    └── DefaultSorterTest.java
  │    |    │    └── InMemoryRepositoryTest.java
  │    |    │    └── ReporterTest.java
  │    └── integrationTest 
  │         └── java 
  │              └── ReporterIntegrationTest.java
  └── build.gradle

We can define the integrationTest source set as:

sourceSets {
    create("integrationTest") {
        compileClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.main.get().output
    }
}

With this setup, we create a Gradle task called compileIntegrationTestJava. Using this task, we can compile the source files inside the src/integrationTest/java directory.

3.5. Defining a Custom Gradle Task

We need a task to run the integration tests. We can create it using Kotlin DSL:

val integrationTest = task<Test>("integrationTest") {
    description = "Task to run integration tests"
    group = "verification"

    testClassesDirs = sourceSets["integrationTest"].output.classesDirs
    classpath = sourceSets["integrationTest"].runtimeClasspath
    shouldRunAfter("test")
}

Here, we created a custom task using the Kotlin extension function task that is attached to the Project object. This is different from the Groovy syntax because there we would use the TaskContainer object to create a custom task. This is possible in Kotlin DSL too, but it’s more verbose.

4. Running Command-Line Command With Kotlin DSL

We can use Kotlin DSL to run command-line commands, enhancing our build workflow. In this section, we’ll explore a few scenarios to learn this in detail.

4.1. Creating Command-Line Task

First, let’s create the helloUserCmd task in the build.gradle.kts build script, and look at it in its entirety:

tasks.register("helloUserCmd") {
    val user: String? = System.getenv("USER")
    project.exec {
        commandLine("echo", "Hello,", "$user!")
    }
}

Let’s break this down to understand the building blocks of the helloUserCmd task. Firstly, we used the tasks.register() function to define the task. Further, we used the project.exec function to configure the execution of an external command with the help of the commandLine() method. Lastly, it’s important to note that we used the System.getenv() method to retrieve the username from the environment.

Next, let’s run the helloUserCmd task and see it in action:

$ ./gradlew helloUserCmd
7:23:17 AM: Executing 'helloUserCmd'...
> Task :helloUserCmd
Hello, tavasthi!
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
7:23:19 AM: Execution finished 'helloUserCmd'.

Great! It worked as expected.

4.2. Capturing Command Output in a Variable

We might sometimes want to capture the command output in a variable for later reuse. Let’s learn how to solve this use case.

Let’s begin by using the logic from the helloUserCmd task to define the helloUserInVarCmd task:

tasks.register("helloUserInVarCmd") {
    val user: String? = System.getenv("USER")
    val outputStream = ByteArrayOutputStream()
    project.exec {
        standardOutput = outputStream
        commandLine("echo", "Hello,", "$user!")
    }
    val output = outputStream.toString().trim()
    println("Command output: $output")
}

We can notice that the only additional logic in this task is the redirection of standard output to an instance of ByteArrayOutputStream class. Furthermore, we captured the value of outputStream in the output variable to use later in a println() statement.

Now, let’s run the helloUserInVarCmd task and verify its behavior:

$ ./gradlew helloUserInVarCmd
7:25:15 AM: Executing 'helloUserInVarCmd'...
> Task :helloUserInVarCmd
Command output: Hello, tavasthi!
BUILD SUCCESSFUL in 137ms
1 actionable task: 1 executed
7:25:16 AM: Execution finished 'helloUserInVarCmd'.

Perfect! We can see that the println() statement containing the command output worked as expected.

4.3. Capturing Command Output in a File

Let’s solve a similar use case where we want to capture the output in a file. For this purpose, let’s imagine that we want to capture the filenames from the /tmp directory in the output.txt file. So, let’s go ahead and write the tmpFilesCmd task in the build.gradle.kts build script:

tasks.register("tmpFilesCmd") {
    val outputFile = File("/tmp/output.txt")
    val outputStream: OutputStream = FileOutputStream(outputFile)
    project.exec {
        standardOutput = outputStream
        workingDir = project.file("/tmp")
        commandLine("ls", workingDir)
    }
}

It’s interesting to note that we’ve defined an instance of the FileOutputStream to redirect the standard output to the underlying file. Additionally, we used the project.file() method to use /tmp as an argument for the ls command.

Now, let’s run the tmpFileCmd task and validate the contents of the output.txt file:

$ ./gradlew tmpFilesCmd
7:26:10 AM: Executing 'tmpFilesCmd'...
> Task :tmpFilesCmd
BUILD SUCCESSFUL in 214ms
1 actionable task: 1 executed
7:26:11 AM: Execution finished 'tmpFilesCmd'.

$ head -2 /tmp/output.txt 
114A32AE-CD3B-45DB-98FB-B0E47CB393DF
1A377CC5-E52F-49B6-BDC6-2AD4A8E60FA5

Fantastic! With this, we’ve got a good understanding of capturing command output.

4.4. Handling Failure

When we execute an external command using the project.exec function, Gradle fails execution of the task if the command returns with a non-zero exit status. Although it might be the desired behavior in some use cases, we might want to handle failures in some cases.

To understand this, let’s write the alwaysFailCmd task that executes the ls command for a non-existent path:

tasks.register("alwaysFailCmd") {
    val result = project.exec {
        commandLine("ls", "invalid_path")
        isIgnoreExitValue = true
    }
    if (result.exitValue == 0) {
        println("Command executed successfully.")
    } else {
        println("Command execution failed.")
    }
    println("Command status: $result")
}

Now, let’s understand the nitty gritty of the logic. The foremost thing to note is that we’ve set the isIgnoreExitValue to true so that Gradle doesn’t consider task execution a failure when the ls command fails. Further, we’ve captured the execution result of the project.exec() function in the result variable to get the exit status of the command. Lastly, we check the exitValue attribute of the result variable to add the failure handler logic, which in this case, is a simple println() statement.

Now, let’s execute the alwaysFailCmd task:

$ ./gradlew alwaysFailCmd
7:27:08 AM: Executing 'alwaysFailCmd'...
> Task :alwaysFailCmd
Command execution failed.
Command status: {exitValue=1, failure=null}
BUILD SUCCESSFUL in 239ms
1 actionable task: 1 executed
ls: invalid_path: No such file or directory
7:27:09 AM: Execution finished 'alwaysFailCmd'.

As expected, even though the ls command failed with an exitValue=1, Gradle executed the task successfully.

5. IDE Support

Since Kotlin is a statically typed language, unlike Groovy, IDEs can provide several useful features like auto-completion, source code navigation, refactoring support, and error highlighting for Gradle scripts. This means we can enjoy a similar coding experience as we normally do when writing application code in Kotlin or Java. Currently, IntelliJ IDEA and Android Studio provide the best support for Kotlin DSLs.

6. Limitations

The Kotlin DSL has some limitations compared to Groovy especially when comparing the speed of script compilation. There are some other uncommon limitations mentioned on the Gradle official website.

7. Conclusion

In this article, we learned how to compose Gradle scripts using the Kotlin DSL. We also looked at some differences between Gradle scripts written in Groovy and those written in Kotlin DSL. Furthermore, we learned how to use Kotlin DSL for running command-line commands.

As usual, all code examples are available over on GitHub.