A beautiful workaround for accessing Gradle Version Catalogs from Precompiled Script Plugins
TL;DR: Apply the gradle-buildconfig-plugin or the BuildKonfig plugin to buildSrc/build.gradle.kts
.
1. Introduction
There is a long-lasted Gradle issue gradle/gradle#15383 that has almost been impossible to resolve, and it has been very frustrating for the Gradle community. It is about accessing the version catalogs from precompiled script plugins. While many workarounds exist, today I want to show you the workaround I found recently.
Let’s begin.
2. Understanding the issue
The issue gradle/gradle#15383, created by Cรฉdric Champeau (melix), states:
In a similar way to gradle/gradle#15382, we want to make the version catalogs accessible to precompiled script plugins. Naively, one might think that it’s easier to do because precompiled script plugins are applied to the “main” build scripts, but this isn’t necessarily the case:
- First, precompiled script plugins can be published too, meaning that they are no different from regular plugins in practice
- Second, precompiled script plugins can be declared in included builds, not just buildSrc
Therefore, the “version catalog” that a precompiled script plugin should see cannot be the catalog declared in the “main build”. It cannot be, either, the catalog declared in the project which itself declares the precompiled script plugin (typically the settings file of the buildSrc project): in particular, the catalogs declared in buildSrc/settings.gradle are for the build logic of buildSrc itself, not for the “main” build.
In short, precompiled script plugins are not able to access version catalogs, and it is quite hard to fix this issue. But why? To better understand this issue, let’s review some concepts in Gradle.
2.1. What is the composite build?
Gradle official documentation says:
A composite build is a build that includes other builds.
Basically, this is the includeBuild("relative/path/to/another/gradle/project")
statement in your settings.gradle.kts
file. The included build itself is a valid, isolated, and self-contained Gradle project that has its own settings.gradle.kts
and/or build.gradle.kts
file.
You can try adding the Gradle wrapper to relative/path/to/another/gradle/project
and opening the folder in your IDEA. Watch your IDEA treat it as a normal Gradle project, just like your typical Java Gradle projects ๐.
2.2. The precompiled script plugin as a composite build
Gradle official documentation recommends two ways to structure your multi-module project. Either use buildSrc
or a composite build to extract common build logic into a build project, used by subprojects in your multi-module project.
Here is the folder structure:
.
โโโ gradle/
โโโ gradlew
โโโ settings.gradle.kts
โโโ build-logic/ or buildSrc/
โ โโโ settings.gradle.kts
โ โโโ conventions
โ โโโ build.gradle.kts
โ โโโ src/main/kotlin/shared-build-conventions.gradle.kts
โโโ sub-project1/
โ โโโ build.gradle.kts
โโโ sub-project2/
โ โโโ build.gradle.kts
โโโ lib/
โโโ build.gradle.kts
Since Gradle 8.0, buildSrc
has started to behave more like a composite build. Both ways are now more or less the same.
Cool! ๐ฒ So now your common build logic is a self-contained Gradle project by itself.
In fact, if you look closely at the folder structure of the build project.
.
โโโ settings.gradle.kts
โโโ conventions
โโโ build.gradle.kts
โโโ src/main/kotlin/shared-build-conventions.gradle.kts
It looks like a normal Gradle project, right?
So, here comes the truth about the precompiled script plugin.
2.3. The truth of the precompiled script plugin
Have you ever wondered why the precompiled script plugin is placed in the src/main/kotlin
folder? ๐ค Like the shared-build-conventions.gradle.kts
example from above. Can it just stay in the first level in the build project like build-logic/shared-build-conventions.gradle.kts
?
Well, here I would like to invite you to watch a video from Jendrik Johannes (jjohannes) titled Understanding Gradle #25 โ Using Java to configure builds. Basically, each Gradle script is just an implementation of the Plugin
interface. The build script build.gradle.kts
corresponds to Plugin<Project>
, and the settings script settings.gradle.kts
corresponds to Plugin<Settings>
.
Therefore, the precompiled script plugin, placed inside the src/main/<jvm language>
folder, is also business logic code, but the business logic here is the build logic of your main project. Such code typically starts from the apply(Project project)
method in your class MyPlugin implements Plugin<Project>
implementation.
2.4. The version catalog, using it from src/main/kotlin
? ๐ค
Now, let’s look back to the statement from the issue gradle/gradle#15383:
We want to make the version catalogs accessible to precompiled script plugins
Let’s translate this using the knowledge we reviewed above; it becomes:
“A file called libs.versions.toml
is meant to be used in the build.gradle.kts
file. Now we want to use it in the business code in the src/main/kotlin
folder.”
Now it sounds weird, right? ๐ค
That’s why the issue is rarely possible to be solved, because it is a design issue that breaks the separation of concerns principle.
3. Existing workaround
Several intelligent people have proposed great workarounds to this issue. The most famous one is from Bjรถrn Kautler (Vampire)’s comment, where you add a sneaky Gradle internal file to the dependencies {}
block. It works, but it is very hacky, not guaranteed to work in any Gradle project (at least it doesn’t work for me ๐), not working in the plugins {}
block, and the workaround relies on a Gradle internal API that is subject to change anytime in the future.
Are there other workarounds, preferably without hacks?
Fortunately, there is one for the plugins {}
block mentioned by this comment. Since applying external plugins to the precompiled script plugin requires adding the corresponding dependency of the external plugin to the build.gradle.kts
file, you can add that dependency to the version catalog. This method assumes that the settings.gradle.kts
file in the build project imports the same version catalog used in the main project. I also discovered that the method works for setting plugins, as I described in this forum post.
I also found a hack-free workaround for the dependencies {}
block and I described it in this comment. This method plays around the Gradle platform and build phases of Gradle in order to do the trick. However, that method is very annoying because adding/removing a dependency requires 3 modifications around your project.
Together, non-hacky workarounds can cover the plugins {}
block and the dependencies {}
block. In most cases, this is enough. But what about extensions? For example, if a precompiled script plugin applies the Micronaut Gradle Plugin, how to set the version of the Micronaut framework in the micronaut {}
extension using the version catalog? ๐คทโโ๏ธ
One day, I was investigating the Micronaut framework, and I realized that I couldn’t apply any centralized version management solution mentioned in Understanding Gradle #09 โ Centralizing Dependency Versions from โโJendrik Johannes (jjohannes).
So I created a feature request to the Micronaut team. I even investigated into the source code of the plugin and pointed out the codes that prevent the use of the version catalog or the Gradle platform.
The same person who created the issue gradle/gradle#15383, โโCรฉdric Champeau (melix), responded. ๐ฒ
He quickly came up with a PR, which adds a new option importMicronautPlatform
to the Micronaut Gradle Plugin that can be set to false
to allow you to achieve centralized version management with Gradle platform. Once I am able to use Gradle platform, I will be able to use the version catalog using the workaround I mentioned above.
Today, you can see that the option is mentioned in the Micronaut Gradle Plugin document.
4. Introducing the new beautiful workaround ๐คฉ
All workarounds mentioned above are more or less doing one thing: “sending” the variables that are meant to be used in Gradle build scripts into the business code in the src/main/kotlin
folder.
One day, I unintentionally found two Gradle plugins: the gradle-buildconfig-plugin and the BuildKonfig plugin.
Both plugins are typically used in Kotlin Multiplatform projects (typically Compose Multiplatform apps) to generate a Kotlin file that contains configuration variables you defined inside the build.gradle.kts
script.
For example, if you have:
plugins {
// ...
id("com.github.gmazzo.buildconfig") version <current version>
}
buildConfig {
className("MyConfig") // forces the class name. Defaults to 'BuildConfig'
packageName("com.foo") // forces the package. Defaults to '${project.group}'
buildConfigField(String::class.java, 'APP_NAME', "my-project")
}
//...
You will get:
package com.foo
object MyConfig {
const val APP_NAME: String = "my-project"
}
Suddenly, I realized that what if I apply this plugin to the build.gradle.kts
inside the build project? ๐ฒ
[versions]
java = "21"
slf4j = "2.0.13"
[libraries]
slf4j-api = {module = "org.slf4j:slf4j-api", version.ref = "slf4j"}
plugins {
// ...
id("com.github.gmazzo.buildconfig") version <current version>
}
buildConfig {
className("VersionCatalog") // forces the class name. Defaults to 'BuildConfig'
packageName("my.util") // forces the package. Defaults to '${project.group}'
buildConfigField(Int::class.java, "JAVA_VERSION", libs.versions.java.get().toInt())
buildConfigField(String::class.java, "SLF4J_API", libs.dep.slf4j.get().toString())
}
Can my precompiled script plugin access JAVA_VERSION
and SLF4J_API
variables?
Guess what? It actually works! ๐ฑ
import my.util.VersionCatalog
// other configs
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(VersionCatalog.JAVA_VERSION))
}
}
dependencies {
implementation(VersionCatalog.SLF4J_API)
}
// other configs
5. Conclusion
The method, that utilizes the gradle-buildconfig-plugin or the BuildKonfig plugin for accessing Gradle Version Catalogs from precompiled script plugins, is a beautiful workaround. It is not too hacky, guaranteed to work in any Gradle project, and doesn’t have the limitation where it is only accessible from the plugins {}
block or the dependencies {}
block. It is a great solution for centralizing version management in your Gradle project.
I hope this workaround can help you in your Gradle project. If you have any questions or suggestions, feel free to leave a comment below. ๐
Ahh, I feel so tired ๐ซ after writing this post. I need a cup of coffee โ to refresh myself. If you like my Gradle ๐ solution, would you mind to buy me a coffee? Thank you for your support! ๐ค