Gradle Kotlin DSL - Code Reuse

Reuse code between build.gradle.kts files, but how? Back in the Gradle Groovy days, that was so easy to split build.gradle files into multiple. All we had to do was to copy necessary code to a new file and say apply(from: 'path to.build.gradle').

With Gradle Kotlin DSL, it does not work that easily. I’ve been looking for the solution to this problem in Gradle Kotlin DSL scripts for a long time. Now I can share the trick with you.

In short, there are two tricks that make it possible:

  • move some-part.build.gradle.kts under buildSrc sources (to have accessors generated via precompiled plugins)
  • use $id:$id.gradle.plugin to include your plugins as dependencies in buildSrc project

Let me explain these tricks in detail.

Demo Project

As for demo, I use a default generated project from IntelliJ IDEA plugin, which uses org.jetbrains.intellij. In reality, a project should be more complex than our demo project.

Here is the generated script, which we will try to split into several files:

plugins {
  id("java")
  id("org.jetbrains.kotlin.jvm") version "1.6.20"
  id("org.jetbrains.intellij") version "1.5.2"
}

group = "com.example"
version = "1.0-SNAPSHOT"

repositories {
  mavenCentral()
}

intellij {
  version.set("2021.2")
  type.set("IC") // Target IDE Platform
  plugins.set(listOf(/* Plugin Dependencies */))
}

tasks.withType<JavaCompile> {
  sourceCompatibility = "11"
  targetCompatibility = "11"
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
  kotlinOptions.jvmTarget = "11"
}

The demo project sources are on my GitHub, all the steps I show in the blog post are committed to that repo.

We will move the most of the build.gralde.kts file into a plugin.build.gradle.kts and will make it work without major changes to original Gradle scripts.

Moving Code

First of, we move the most (with the plugins { .. } block) of the build.gradle.kts to a dedicated file: plugin-include.build.gradle.kts and add the following to the build.gradle.kts (we may actually remove everything else from the file).

apply(from = "plugin-include.build.gradle.kts")

That trick could have worked in Groovy, but it will not work with Gradle Kotlin DSL. Here is an error you would see:

Unresolved reference: intellij

Why does Gradle knows intellij reference in the build.gradle.kts and it is unable to resolve it in another file? The reason is “Generated Accessors”.

Generated Accessors

Gradle uses a tricky approach to deal with Kotlin DSL. There is a dedicated phase in Gradle which examines the script model and applied plugins to generate a Kotlin code. That generated code is called “Accessors” and it makes Gradle Kotlin scripting more pleasure and short.

For example, it adds tasks.test if you have one of Java plugins enabled, it adds kotlin { .. } block if you have Kotlin plugin enabled. And so on.

We may see (e.g. via IntelliJ) how intellij function is defined in the generated by Gradle code after applying the IntelliJ SDK Gradle plugin:

/// from generated kotlin DSL accessors from under ~/.gradle/caches

/**
 * Configures the [intellij][org.jetbrains.intellij.IntelliJPluginExtension] extension.
 */
fun org.gradle.api.Project.`intellij`(configure: Action<org.jetbrains.intellij.IntelliJPluginExtension>): Unit =
    (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("intellij", configure)

How does that help to fix our plugin-include.build.gradle.kts script? Accessors are not included there. However, the obvious workaround is to copy (or inline) the generated code to our plugin-include.build.gradle.kts script. DO NOT DO THAT.

After years of using Gradle Kotlin DSL, I found a better solution. Gradle includes accessors for .gradle.kts files which are under the buildSrc directory. See precompiled plugins for more information. It also turns such files into Gradle plugins, so we should use either plugins { name } block or apply(plugin = "name") syntax to enable them.

Now, let’s create a buildSrc project.

buildSrc Project

The buildSrc project is a standard way to re-use build login in Gradle. You may keep common code, tasks, plugins or everything else to re-use with all your build.gradle.kts files. The output of the buildSrc project is included in all other projects classpath. For more details, check out the organizing gradle projects section from Gradle official documentation.

Let’s apply the trick and move our plugin-include.build.gradle.kts script to the buildSrc/src/main/kotlin/ folder. In addition to that, we have to follow the rituals and need to create a buildSrc/build.gradle.kts with the following contents:

plugins {
  `kotlin-dsl`
}

repositories {
  mavenCentral()
}

This is a default buildSrc project that uses the kotlin-dsl plugin, which configures Kotlin the compatible way to be used in buildSrc projects and for usages from other .gradle.kts files. This plugin is bundled into Gradle.

Let’s check if it works now? Now Gradle will complain on the following:

Invalid plugin request [id: ‘org.jetbrains.kotlin.jvm’, version: ‘1.6.20’]. Plugin requests from precompiled scripts must not include a version number. Please remove the version from the offending request and make sure the module containing the requested plugin ‘org.jetbrains.kotlin.jvm’ is an implementation dependency of project ‘:buildSrc’.

Ok, now we need a way to include a plugin as implementation. How would we?

buildSrc Plugin Dependency

How would we create the dependencies block? We need to know Maven coordinates for our plugins. Such coordinates are implementation details of plugins and a subject to change in the future. Where should we find these coordinates? It is yet another tricky question one has to figure out.

Of course, it’s possible to resolve and hack that. Every Gradle plugin has some libraries behind the scenes. DO NOT DO THAT.

Back from the old days, I remember that Gradle plugins are nothing more, but special case maven libraries, which are in Gradle Plugin Portal maven repository.

For example, my old java9c plugin has files under the following maven path: https://plugins.gradle.org/m2/org/jonnyzzz/java9c/org.jonnyzzz.java9c.gradle.plugin/

The trick is as follows: $id:$id.gradle.plugin:$version. You may create that Maven package manually if you would like to create a Gradle plugin manually, without the provided tooling.

Let’s use the trick to include our plugins to the buildSrc project dependencies.


repositories {
  mavenCentral()
  gradlePluginPortal()
}

dependencies {
  fun pluginDependency(id: String, version: String) {
    implementation("$id:$id.gradle.plugin:$version")
  }

  pluginDependency("org.jetbrains.kotlin.jvm", "1.6.20")
  pluginDependency("org.jetbrains.intellij", "1.5.2")
}

I’ve added the gradlePluginPortal() repository to the buildSrc project in order to let it resolve a dependency too.

It is up to you to create a fancy Kotlin DSL to make it look better. I would be happy to learn about your DSL, please let me know via @jonnyzzz.

Plugin Versions

Adding mentioned plugins to the buildSrc dependencies is not enough to make Gradle work on our scripts.

We need to remove plugin versions from all other .gradle.kts files in our project. As long as plugins are included into buildSrc classpath, they are available to every project without a version. Gradle does not allow mixing several versions of the same plugin anyway.

Fix apply Command

The only last move: update the apply in the main build.gradle.kts:

apply(plugin = "plugin-include.build")

Alternatively, and better, if you only need to include the script to the project, you may just use the plugins { ... } block instead of the apply function call:

plugins {
  id("plugin-include.build")
}

The .gradle.kts files from buildSrc are turned into Gradle plugins, that name of the plugin is generated from the original file name by removing .gradle.kts suffix.

Conclusion

I was looking for that for quite a long time. And finally, I’m thrilled to share my findings with you. Hope I’ll solve your pain in Gradle scripting too. In fact, the trick if quite complex, I’m looking forward to a shorter solution, please let me know of any.

I have covered many mode aspects of Gradle Kotlin DSL in the older posts, check out:

  • first post — First steps of the migration
  • second post — Kotlin tasks in Gradle Kotlin DSL,
  • third post — a buildSrc project with Kotlin, ad-hoc plugins and extensions
  • fourth post — Groovy Closure and Kotlin DSL

I’d like to thank Vladimir Sitnikov for corrections and suggestions to that port.

comments powered by Disqus