Teabyte

A personal blog by Alexander WeiƟ

Write a custom Gradle Kotlin DSL task

2019-06-20

I recently graduated and finished writing my master thesis. Some parts of it involved writing a small Kotlin library, working with Google's FlatBuffers library.
Flatbuffers is a library used for efficient cross platform serialization. It uses a custom interface definition language (IDL) to define the data structures you may want to serialize in your application. With the help of a special compiler files written in this language are compiled to generate source code for a lot of languages, e.g. Java (a lot more possible). You can take those generated files and integrate them into your application. After that you are ready to go. In fact a very simple process. The compiler itself is a small executable binary. It takes several arguments, for example what files you wish to compile, the target programming language and the target output directory.

Since I am working inside a Kotlin project, using Gradle as my build tool, I thought it would be nice to integrate the step of compiling the schema files from Gradle. In order to do so I would need to invoke the FlatBuffers compiler from a Gradle task. This project was also the first for me to try out the new Kotlin DSL for Gradle. It turned out to be easier than expected to create new custom tasks in the Kotlin DSL. The idea is to invoke the compiler by running the the executable in a new process started by Gradle. With this small post I want to share how I created a new Kotlin DSL task spawning a separate process.

Extending DefaultTask

The first thing to start with, is to declare a new class which extends DefaultTask.

open class SystemProcess: DefaultTask() {
}

Now we have to think about what arguments our task should take. I aimed the task to be as general as possible, to even work with other executables, so I declared three arguments:

  • the command to be executed
  • the arguments of the command
  • the working directory of the command

Without @Optional annotation all arguments are mandatory and need to be present when defining the task.

open class SystemProcess: DefaultTask() {
    lateinit var command: String
    lateinit var workingDir: String
    lateinit var arguments: List<String>
}

Next up we need to tell Gradle what to execute when it runs our task. This is achieved with the @TaskAction annotation.

open class SystemProcess: DefaultTask() {
    // snip
     @TaskAction
     fun runCommand() {
        val res = (command + " " + arguments.joinToString(" ")).runCommand(File(workingDir))
    }
    // snip
}

To make full use of Kotlin we define an extension method on the String type named runCommand().

private fun String.runCommand(workingDir: File): String? {
    return try {
        val parts = this.split("\\s".toRegex())
        val proc = ProcessBuilder(*parts.toTypedArray())
                .directory(workingDir)
                .redirectOutput(ProcessBuilder.Redirect.PIPE)
                .redirectError(ProcessBuilder.Redirect.PIPE)
                .start()
 
        proc.waitFor(60, TimeUnit.MINUTES)
        proc.inputStream.bufferedReader().readText()
    } catch(e: Exception) {
        e.printStackTrace()
        null
    }
}

This will use the ProcessBuilder class and create a new process out of the "command" string we created and starts it. It will also return all of the eventually generated outputs of the process.

Execute the task

The full class looks like this:

SystemProcess.kt
open class SystemProcess: DefaultTask() {
    lateinit var command: String
    lateinit var workingDir: String
    lateinit var arguments: List<String>
 
    @TaskAction
    fun runCommand() {
        val res = (command + " " + arguments.joinToString(" ")).runCommand(File(workingDir))
    }
 
    private fun String.runCommand(workingDir: File): String? {
        return try {
            val parts = this.split("\\s".toRegex())
            val proc = ProcessBuilder(*parts.toTypedArray())
                    .directory(workingDir)
                    .redirectOutput(ProcessBuilder.Redirect.PIPE)
                    .redirectError(ProcessBuilder.Redirect.PIPE)
                    .start()
 
            proc.waitFor(60, TimeUnit.MINUTES)
            proc.inputStream.bufferedReader().readText()
        } catch(e: Exception) {
            e.printStackTrace()
            null
        }
    }
}

For now, that's it. We can now use this class to create our custom task to invoke any executable we like, for example the Flatbuffers compiler. Create a new task with the CmdLine class and name it accordingly.

gradle.build.kts
tasks.create<SystemProcess>("generateProtocolFiles") {
    group = "build"
    description = "Produces the compiled adaptive authentication source files"
    command = "../../flatc"
    workingDir =  "./"
    arguments = listOf("--java", "-o", "/output/directory", "../../schema_file.fbs")
    dependsOn("deleteGeneratedFiles")
}

Now this task can be run by calling gradle run generateProtocolFiles. Furthermore you can integrate it in your normal gradle build task. The arguments group, description and the dependsOn method are inherited from DefaultTask.

In the end it was a very pleasant experience to create a custom task within the Kotlin DSL for Gradle. We can just use default Kotlin code, wrap it in a DefaultTask and then call it from Gradle.