While migrating the real-life project’s Gradle build from Groovy to Kotlin, I collected some useful recommendations, code snippets, and explanations. Throughout the post series, we will together learn how to convert to Gradle Kotlin DSL faster and easier.
New to Gradle Kotlin DSL? Take a look at the first post for practical recommendations on migrating from Groovy to Kotlin build scripts. In the second post, we cover Kotlin tasks setup on Gradle Kotlin DSL scripts.
In that post, I’m proud to share my findings for the code
reuse in Gradle: extensions, plugins,
and buildSrc
scripts. It will be the next chapter for the
Ad-hoc Plugins with Gradle
post, but we’ll be using Gradle Kotlin DSL.
Project Extensions
The real-life project that I converted to Kotlin DSL contains
several micro-services, each uses the Application
plugin to create an executable,
and jib
plugin is used to generate Docker images.
We reuse the code via a Gradle ad-hoc
plugin to avoid duplicating scripts.
The pattern
helps to reuse the same Gradle (Groovy) code, the usage of which
for every micro-service is like:
'some-service' {
diImplClassName = 'some-class'
}
The ad-hoc plugin is applied via a project.subprojects.forEach{..}
call
in the parent project.
The only line per micro-service is enough to have a command-line application, docker
container, logging configuration, several common dependencies,
test classpath, and tests included for every micro-service project.
The same code does not work in Kotlin DSL. Instead,
in Kotlin DSL one calls a strongly typed version of it via
the configure<T>{..}
block, where T
is the type of
the extension or convention to configure. We need to know the
extension type to work with it, for example:
configure<MicroPluginSetup> {
diImplClassName = "some-class"
}
In general, we may try the following steps to convert an extension or convention setup
into Kotlin. Gradle generates accessors for conventions and extensions for plugins that
are enabled via plugins{..}
block.
If it is not generated (like in my case), we may check the documentation
or source code to see the type name of the extension.
Try a short debugging in Groovy or Kotlin by printing the
project.extensions
map entries with a println()
function
to see the actual project extensions and their types.
There is yet another way to deal with shared code in Gradle. It is called buildSrc
.
I decided to use that approach together with statically typed Kotlin DSL. All declarations
from the buildSrc
path should be visible in every build.gradle.kts
files of my project,
with types information, error highlighting, code navigation, IDE support.
Let’s see how it works
The buildSrc Project
It is a good practice
in Gradle to move utility classes or functions under the buildSrc
project.
Ad Hoc Plugins with Gradle post
describes for more ways of reusing code with Gradle.
By the convention, Gradle checks the buildSrc
folder for build sources project. The runtime classpath
of that project will be included in every sub-projects build.gradle.kts
and build.gradle
build script classpaths,
We will be able to use our code, utilities, and classes directly from other build files of the
root project, both Gradle/Groovy and Gradle/Kotlin.
The following Gradle/Kotlin script for the buildSrc
project is enough to start, it is normally
placed it to the buildSrc/build.gradle.kts
file under the project root directory:
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
mavenCentral()
}
kotlinDslPluginOptions {
experimentalWarning.set(false)
}
The project is ready to go. You may need to click to refresh your Gradle project in IntelliJ IDEA to continue.
Let’s create a helper function as an example. For that, we need to create a buildSrc/src/main/kotlin/file-op.kt
file with the following contents:
operator fun File.div(s: String) = File(this, s)
It is my favorite operator for builds. It defines the /
overloaded operator
for File
and String
types. So that we may use /
to combine paths, e.g., we can write
the following to create new File
object for a child path:
buildScript / "aaa" / "jonnyzzz.txt"
We spoke about the /
operator with some members of the Gradle team
back at KotlinConf 2018. In addition to that, I’ve noticed the
similar operator somewhere in the
Gradle sources
too :)
Now it is the time to convert the Groovy Ad-Hoc plugin into Kotlin DSL under buildSrc
. Let’s rock!
Ad-Hoc Gradle Plugins
My scripts were written in a Groovy as an ad-hoc plugin class in a parent project file.
For more details on that setup, please see the explanation in the
ad hoc Gradle plugins post.
To start with the buildSrc
folder, we move the plugin code into the buildSrc
folder.
Several conversions steps needed to turn Groovy script into Kotlin DSL. You may check out the
first post of the series for
more insights. We’ll have the following Kotlin code for it now:
package theBuildSrcPackage
fun applyMicroPlugin() {
apply<MicroPlugin>()
}
open class MicroPlugin : Plugin<Project> { ... }
open class MicroPluginSetup { ... }
The applyMicroPlugin()
is a nice shortcut to simplify the way we deal with the plugin.
Thanks to the code completion in the build.gradle.kts
files, it is now easier to apply the
plugin via the function call, instead of calling the longer apply<>()
variant.
The usage of the ad-hoc plugin in Kotlin DSL is now look as follows:
import theBuildSrcPackage.*
applyMicroPlugin()
configure<MicroPluginSetup> {
diImplClassName = "some-class"
}
It is similar to what we have before. We apply the plugin first and pass the configuration of it as the second step. It is a good point to realize that we use a too long API to achieve the goal. Let’s try to make the API more expressive and short. Check out my talk on Expressive APIs in Kotlin for more hints. We do not need to repeat the intent to enable a Gradle plugin more than once. It means all other types and configuration parameters should be included implicitly. Let’s add the following helper function for that:
fun applyMicroPlugin(action: MicroPluginSetup.() -> Unit) {
apply<MicroPlugin>()
configure<MicroPluginSetup>(action)
}
The code above hides all implementation details from us, so we may apply and configure the plugin as easy as:
applyMicroPlugin {
diImplClassName = "some-class"
}
Such code us easy to read and understand. It is now clear what we do. It is no longer possible to enable the plugin without passing a configuration to it. Hopefully, it will help others from my team to deal with Gradle scripting faster.
Conclusion
In the post we’ve seen how to convert an ad-hoc plugin to Gradle/Kotlin. It is easier to re-use Gradle code that way.
Kotlin as a statically typed programming language seems to play well with writing Gradle build scripts. Thanks to the static type inference, the Kotlin compiler detects errors earlier and shows helpful compilation error messages and warnings. Both the IDE and the compiler use information about types to infer the available functions and properties in a given scope, even inside a 5th level nested lambda with receivers.
I will cover more aspects in the coming posts, stay tuned! Check out the
- first post - First steps of the migration
- second post - Kotlin tasks in Gradle Kotlin DSL,
- fourth post - Groovy Closure and Kotlin DSL