Article 5: The Adventure of the Shared libs.versions.toml
Published on 27 September 2025
Introduction
In our quest to create the plugin`site-baker`, we have sought to maintain a clean and centralized codebase. One of the best practices in the modern Gradle ecosystem is the use ofcatalog of versions(libs.versions.toml) to manage dependencies. But what happens when your project becomes more complex, like abuild compositewhere an independent build (our plugin) must be included in another?
This is where our adventure took an unexpected turn. We wanted our plugin`site-baker`, which is itself a Gradle project, uses the same version catalog as our main project. This article, the fifth in our series, tells how we solved this challenge.
1. The Versions Catalog (libs.versions.toml) : A Reminder
The file`gradle/libs.versions.toml`is a Gradle feature that allows centralizing the versions and coordinates of your project’s dependencies. It is generally structured in four sections:
-
[versions]: Defines aliases for version numbers (e.g:`kotlin = "1.9.20"`). -
[libraries]: Sets aliases for the full dependencies, using the versions defined above (e.g:`kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }`). -
[bundles]: Groups several libraries under a single alias (e.g.jackson = ["jackson-core", "jackson-databind"]). -
[plugins]: Defines aliases for Gradle plugins.
Once defined, Gradle automatically generates typed accessors, which makes your`build.gradle.kts`much more readable:
dependencies {
// Au lieu de : implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.20")
implementation(libs.kotlin.stdlib)
}
@startuml !theme plain file "gradle/libs.versions.toml" as Toml node "Gradle" as G file "build.gradle.kts" as BuildScript Toml --> G : "is read by" G --> BuildScript : "generates the accessor `libs" BuildScript -> G : "use `libs` to declare the dependencies" @enduml
2. The Problem: A Composite Build and Two Separate Worlds
Our project structure is abuild composite. The main project (thymeleaf.cheroliv.com) includes the plugin project (site-baker) via the command`includeBuild("site-baker")in the file`settings.gradle.kts.
@startuml
!theme plain
package "Main Project" {
file "settings.gradle.kts" as MainSettings
folder "gradle" {
file "libs.versions.toml" as MainToml
}
}
package "Plugin `site-baker` (Build included)" {
file "settings.gradle.kts" as PluginSettings
file "build.gradle.kts" as PluginBuild
}
MainSettings -> PluginSettings : `includeBuild("site-baker")`
PluginBuild ..> MainToml : **Voulait accéder à...**
note right on link: ...mais ne pouvait pas !
@enduml
The problem is that, by default, an included build is aseparate world. It has its own configuration, its own life cycle, and it does not see the file`libs.versions.toml`of the main build. Our`plugin/build.gradle.kts`was trying to use`libs.plugins.kotlin.jvm`, but the object`libs`It was not generated from the correct TOML file, causing build errors.
3. The Solution: Sharing Dependency Resolution
After several searches, the solution turned out to be a Gradle feature designed precisely for this case:`dependencyResolutionManagement`.
In the file`settings.gradle.kts` du build included(site-baker/settings.gradle.kts), we can tell Gradle where to find the version catalog to use.
Here is the configuration that we added to`site-baker/settings.gradle.kts`:
// site-baker/settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
gradlePluginPortal()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "site-baker"
include("plugin")
Let’s analyze the crucial part:
versionCatalogs {
create("libs") { // Crée un catalogue nommé 'libs'
from(files("../gradle/libs.versions.toml")) // En utilisant ce fichier
}
}
-
create("libs")`We declare a version catalog that will be accessible via the alias`libs. -
from(files("../gradle/libs.versions.toml")): This is the magic. We tell Gradle that the source of this catalog is the TOML file located in the directory`gradle` du parent project(../).
With this configuration, the build`site-baker`He now knows that he must use the main project’s version catalog. The object`libs`is generated correctly, and the dependencies are resolved as expected.
4. Conclusion: The Power of Well-Managed Composite Builds
This experience was rich in lessons. The version catalog is a fantastic tool for maintainability, but its behavior in composite build scenarios is not always intuitive.
The key is to remember that an included build remains an independent build. To share configurations like the version catalog, you need to use the explicit mechanisms provided by Gradle, such as the block`dependencyResolutionManagement`in`settings.gradle.kts`.
By solving this problem, we not only cleaned up our build, but we also strengthened the consistency of our project by ensuring that the plugin and the main project share a single source of truth for their dependencies.
In the next article, we will continue to enrich our plugin by adding more complex tasks, now that our dependency management is solid and centralized.