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
underbuildSrc
sources (to have accessors generated via precompiled plugins) - use
$id:$id.gradle.plugin
to include your plugins as dependencies inbuildSrc
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