Codify your configs with Kotlin DSL and Gradle 5

RIAG Digital
8 min readOct 4, 2019

by Jozef Kruszynski

In this post, I want to talk about my experience migrating my team’s Gradle build scripts from Groovy to Kotlin DSL. This will not be a tutorial, but merely some observations and a bit of advice that may hopefully prove useful to somebody. With that out of the way, let’s get on with it.

With the release of Gradle 5.0 we also saw Kotlin DSL hit version 1.0. Armed with these tools and a little grit and determination, we can now be rewarded with the same smart features we have come to expect from our IDE within our build and execution scripts. If you happen to be writing Android apps and using Kotlin for this, as our mobile team is, then the benefits of having both your application source code and your build configuration code in one single language should be abundantly clear.

Our team uses Java as our weapon of choice, although we consider Kotlin from time to time, and therefore the leap to Kotlin DSL for our Gradle build scripts is easily justifiable to us considering that it’s core GPL also runs on the JVM.

What is Kotlin DSL

Firstly we have to understand what exactly a DSL is. DSL is an acronym for Domain Specific Language which in layman’s terms means that the language is intended for use in a single, specialized application domain, unlike a GPL or General Purpose Language such as the core Kotlin language or Java language, to name but two, which are meant for use across many application domains. If you have ever written a webpage in HTML or a query in SQL then you have already been using a DSL.

A DSL is generally designed to capture the semantics of the application domain that it is written for and cannot, by itself, be used to write an entire application. The concepts behind the definition of such a language can be rather complex and will not be covered further in this post.

If you are interested in the topic, then I suggest reading Martin Fowler’s book — Domain Specific Languages, which covers the subject in great detail.

Kotlin DSL is, therefore, a domain specific language that provides the means by which you describe your build and test executions configurations in a way that is semantically correct, and understandable. It also inherits all of the main features of the core Kotlin GPL as described in this blog post by my colleague Lukas Lechner, and includes all of the benefits of a full-scale GPL within an IDE, such as proper refactoring and code-completion support.

Should you migrate from Groovy to Kotlin

The process of migration can be somewhat troublesome depending on many factors, not least of which is the complexity of your scripts. This is but one factor to consider before undertaking this process, and of course, complexity can be measured by various means.

You may have specific custom tasks defined, or you may have many build stages that have dependencies on one another, either of which can make migration tedious and frustrating.

I will attempt to outline some of the benefits and also some of the possible pitfalls that you may encounter during the process.

Benefits

The main benefits that Kotlin DSL brings are the IDE support for code-completion and refactoring that we have come to expect in our day to day lives, along with type-safe model accessors that come from pre-compiled plugins, although please note that these accessors are not yet available everywhere as described here.

Another obvious benefit, as I pointed out earlier in this article, is the fact that you can enjoy the familiarity and features of the core Kotlin GPL while defining your build and test execution configuration.

These were the main driving factors for our team to look into migrating our scripts from Groovy to Kotlin DSL.

Possible Pitfalls

Your build time and how this affects your work-flow is a very important factor to consider. If your build times are already long, then they will only get longer with Kotlin DSL, at least for the first run and if you are not employing any kind of caching of your build artifacts for one reason or another. Unfortunately, this is a side effect of one of the features that also makes Kotlin DSL so useful, and that is the type-safe model accessors.

When actually writing your scripts, this feature is wonderful, however, during build execution, this same feature means that all of these statically typed accessors must also be compiled before they can be utilized. It’s a kind of beautiful “catch 22” situation that will make you both jump for joy and jump up and down in fits of rage, possibly in equal measure.

Why is my IDE so slow?

Somewhat inexplicably, when making changes to a Kotlin DSL script, it seems to take an inordinate amount of time for the IDE, in my case IntelliJ, to react to these changes. I’m certain that this will improve over time, but it is something to be aware of currently.

Where to begin?

Perhaps I have convinced you to also go through this migration process, or perhaps you just want to see some examples. Either way, it’s time to look at some code, but before we do that it is advisable to update your environment to the latest version of everything. The Gradle-wrapper, your plugins, and your IDE should all be upgraded to their latest versions where possible.

By no means will this section be an extensive tutorial, I simply want to show some of the changes that you will definitely come across.

Plugins

We will start by looking at the differences between our plugins blocks in Groovy and Kotlin DSL:

plugins {
id 'java-library'
id 'org.sonarqube' version '2.6.1'
id 'org.springframework.boot' version '2.1.1.RELEASE'
}
apply plugin: 'maven'plugins {
maven
`java-library`
id("org.sonarqube") version "2.6.1"
id("org.springframework.boot") version "2.1.1.RELEASE"
}

The first thing that you will likely notice is that Groovy doesn’t care about strings too much, we can declare them by wrapping in either single or double quotes, this is not the case with Kotlin DSL as a “String” must be wrapped with double quotes, single quotes, similarly to Java, denote that we are declaring a “Character”.

This brings me to the first piece of advice that you will also find across every other blog post regarding this migration process and that is to refactor all of your single quoted strings into double-quoted strings. This will be the easiest part of the process.

The second thing that you will notice is that we are no longer using “apply plugin: ‘maven’” but are instead making use of the plugins block and applying the maven plugin by using the short name. Note that the java-library plugin must be wrapped with back-ticks as its short name is hyphenated.

To apply plugins that are not a part of the Gradle core plugins, we have to use the fully qualified plugin id, just as we had to before, however, you will see that the id method must now include brackets enclosing the plugin id. The proceeding version method can be written either as you see it here or similarly to the id method, using brackets to enclose the version string like this: id("org.sonarqube").version("2.6.1")

Either way is acceptable, and we are as yet undecided upon which variant we will stick with.

Plugin Configuration

Depending on the plugin you are configuring and if it has been updated to fully support Kotlin DSL, many have not yet, this can be as simple as enclosing some strings in brackets, or there may be some significant refactoring to be done. Either way, the best possible advice that I can give to you is that you should not simply refactor your build.gradle or settings.gradle to append the .kts file extension, as many posts that I have read, suggest. You should instead create a new build.gradle.kts script and slowly add each config block from your current script fixing any errors as you go. This way you will not be overwhelmed by all of the errors that you will inevitably encounter.

Let’s take a look at the sonarqube configuration as an example of how easy it can be.

sonarqube {
properties {
property "sonar.host.url", "http://some.url"
property "sonar.projectName", "A Sample Project"
property "sonar.projectKey", "sample-project"
property "sonar.projectVersion", "version"
property "sonar.junit.reportPaths", "sample-project/build/test-results/test"
property "sonar.jacoco.reportPaths", "build/jacoco/test.exec"
property "sonar.coverage.exclusions", "src/main/java/at/sample/config/**"
}
}
sonarqube {
properties {
property("sonar.host.url", "http://some.url")
property("sonar.projectName", "A Sample Project")
property("sonar.projectKey", "sample-project")
property("sonar.projectVersion", "version")
property("sonar.junit.reportPaths", "sample-project/build/test-results/test")
property("sonar.jacoco.reportPaths", "build/jacoco/test.exec")
property("sonar.coverage.exclusions", "src/main/java/at/sample/config/**")
}
}

As you can see all we have to do in this case is enclose our key/value string pairs in brackets. All simple enough, but let’s take a look at the maven uploadArchives plugin/task configuration. Although the configuration is straight-forward enough, I wanted to show an example of a legacy plugin and the means by which we can still use this plugin, should we need to.

uploadArchives {
repositories {
mavenDeployer {
repository(url: "http://nexus.foo.com/repo/releases/") {
authentication(userName: user, password: password)
}
snapshotRepository(url: "http://nexus.foo.com/repo/snapshots/") {
authentication(userName: user, password: password)
}
pom.artifactId = artifactIdPromo
}
}
}
getByName<Upload>("uploadArchives") {
repositories.withGroovyBuilder {
"mavenDeployer" {
"repository"("url" to "http://nexus.foo.com/repo/releases/") {
"authentication"("user" to user, "password" to password)
}
"snapshotRepository"("url" to "http://nexus.foo.com/repo/snapshots/") {
"authentication"("userName" to user, "password" to password)
}
"pom" {
setProperty("artifactId", artifactIdPromo)
}
}
}
}

Although what we have in the two snippets here is essentially the same code, there are some subtle differences. The main feature that I want to show here though is the “.withGroovyBuilder {}” extension function that is part of the GroovyBuilderScope included within the Kotlin DSL API.

There is no point in me explaining this function, I will let you read the documentation from the link back there instead.

The point that I am trying to make here is that some plugins have already been updated to fully support Kotlin DSL, some have not yet! and still, others have not, and never will be. This extension function should prove useful if you are reliant on a plugin that falls into one of the latter two categories.

Dependencies

Lastly, in this section, I wanted to show a subtle difference in how we can declare lists/arrays of dependencies.

dependencies {
runtime("io.micrometer:micrometer-registry-prometheus")
implementation("net.logstash.logback:logstash-logback-encoder:5.2")
testImplementation(["org.junit.jupiter:junit-jupiter-api:5.4.0-RC2",
"org.junit.platform:junit-platform-launcher:1.4.0-RC2",
"org.junit.jupiter:junit-jupiter-params:5.4.0-RC2",
"org.springframework.boot:spring-boot-starter-test"])
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.4.0-RC2")
}
dependencies {
runtime("io.micrometer:micrometer-registry-prometheus")
implementation("net.logstash.logback:logstash-logback-encoder:5.2")
listOf("org.junit.jupiter:junit-jupiter-api:5.4.0-RC2",
"org.junit.platform:junit-platform-launcher:1.4.0-RC2",
"org.junit.jupiter:junit-jupiter-params:5.4.0-RC2",
"org.springframework.boot:spring-boot-starter-test")
.forEach {
testImplementation(it)
}
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.4.0-RC2")
}

As you can see, we now have to explicitly declare our dependencies as a list and then iterate with forEach, applying in this case, the “testImplementation” configuration to each element in the list iterator. It’s a very minor change, but a reasonably significant one that had me scratching my head for a little while, so I thought that the inclusion of this would likely help alleviate somebody else’s head-scratching.

So…What have we learnt?

I hope that this has at least served as a taste of what to expect should you also decide to migrate your scripts from Groovy to Kotlin DSL. Overall I feel that the benefits brought by Kotlin DSL far outweigh the drawbacks, such as slower build execution and IDE change pickup.

In the team’s collective opinion, our scripts are now easier to read and maintain, and we believe that for any new team members the project/task dependencies within these scripts will be immediately more obvious.

We have extracted certain build logic and dependency management into common scripts that we apply to relevant projects when we need to, and we have plans to further extract this in an effort to avoid as much code duplication as possible. Yes, this was also possible previously with Groovy, but if nothing else, this migration made us re-evaluate the structure and separation of our scripts.

If all this post achieves is to make you think about how your team’s scripts can be better organized and maintained, and you end up doing a bit of housekeeping, then writing it was worthwhile, as all of us benefit from clean and well thought out code.

This article was first published on the riag.digital official blog.

--

--

RIAG Digital

We create the best everyday shopping experience for our customers