I found a new workaround for GitHub issue #15383

I might have found another great workaround to a big long-lasting Gradle issue gradle/gradle#15383 with no hacks. As many of you have known, this issue is about accessing version catalog from precompiled script plugins.

The idea is to use the gradle-buildconfig-plugin or the BuildKonfig plugin to buildSrc/build.gradle.kts. I made a quick PoC in my own Gradle template. It actually works!

Here is the code:

// gradle/libs.versions.toml

java = "21"
slf4j = "2.0.13"

slf4j-api = {module = "org.slf4j:slf4j-api", version.ref = "slf4j"}
// build-logic/conventions/build.gradle.kts

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())

The above will generate the my.util.VersionCatalog file which I can use in the precompiled script plugin.


import my.util.VersionCatalog

// other configs

java {
  toolchain {

dependencies {

// other configs

I also wrote a comprehensive blog post to describe my finding in details.

While this of course works, be aware that you loose one of the big advantages of version catalogs that way.

If you just change one version now the plugin classpath changes as the generated class changes and so all tasks are out of date. The same is not true when actually using a version catalog.

1 Like

Interesting point of view, I definitely didn’t think of this disadvantage. But actually, that got me into more questions about how Gradle works. Would you mind answering?

In my understanding, the version catalog is also doing code generation (by generating the libs variable to be used in build.gradle.kts). So my first question is, does Gradle internally trade the code generated by version catalog differently so that the generated class changed by the version catalog doesn’t trigger the invalidation to tasks?

Here come the second question that really confuses me. If the first question is true, does it still hold for this workaround that uses the<LibrariesForLibs>() and implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))? Because based on my understanding, if I also just change one thing in the version catalog, the generated class of the variable from the<LibrariesForLibs>() will also change, which should cause the precompiled script plugin to be recompiled.

Gradle indeed does generate the LibrariesForLibs accessors.
But this generated code does not directly contain the values from the TOML file.
The TOML file is read at execution time.
If you add or remove entries from the TOML, the generated code will change.
But if you only change a version or coordinate in the TOML, then the generated code does not change and so the classpath does not change and only those tasks that get the value from the TOML are out-of-date as their given values change.
But all other tasks stay up-to-date as the classpath did not change.
That was one of the design goals of the TOML-based version catalogs.

1 Like

That’s also the reason why I’d always prefer the TOML over the settings script DSL to define the version catalog if possible.
I’d only use the settings script DSL if you need to calculate something dynamically like taking the information from some other source where it already is present.
But for the regular simple case, I’d always prefer using the TOML.

1 Like