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:
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.
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.