Version Catalogs
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Gradle 9.8.1 and was run on JDK 26 on 2026-10-16 (macOS; the daemon ran on JDK 26 and no toolchain was involved). Every example was run through the wrapper in scratch copies of the tidepool project, once with Kotlin DSL build files and once with Groovy DSL build files. The TOML file and its error messages are the same for both DSLs, so the experiments that change only the TOML file (the error gallery, the alias names) were run in the Kotlin DSL copy; everything that involves a build script or a settings script was run in both. The scratch copies pointed their wrapper at a local copy of the same distribution archive; the properties file that a real project commits is
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
The commands use --console=plain, and gradle.properties contains org.gradle.configuration-cache=true, so every example ran with the
configuration cache on and each one says whether it is compatible with it. In the listings the blank lines, the Try: hints of failures,
the [Incubating] Problems report line and the Calculating task graph line of the first run are left out, the order of unrelated tasks
varies from run to run (a line with three dots marks trimmed lines), and the scratch directory is shown as /work/tidepool. Gradle 8.14.x was
not installed here, so statements about it come from the official documentation and the release notes and are labelled "not run here". Publishing
a catalog goes only to a file:// repository below the catalog project’s build directory; publishing to a remote repository is not run here.
A version catalog is a list of dependency coordinates and plugin versions kept in one file, from which Gradle generates type-safe
names for the build scripts. It is the answer to the oldest maintenance problem of a multi-project build: the same version number typed in
a dozen places, and a dozen places to change when a library moves. This page covers the file format, the generated accessors, every way of
declaring, sharing and publishing a catalog, how a catalog interacts with platforms and with buildSrc or build-logic, and the problems
Gradle reports, using the tidepool toolkit of the section (a model library, an engine library that reads a CSV file with Commons CSV, and a
command line application that uses picocli). The dependency vocabulary (configurations, api and implementation) is on
Declaring Dependencies & Configurations,
where the versions end up being chosen is on
Platforms, Constraints & Rich Versions and
Dependency Resolution, Conflicts & Capabilities,
and the cross-tool picture is on
Dependency Management Fundamentals.
Why a catalog
Before catalogs, the usual place for a shared version was an ext property in the root build script (or a gradle.properties entry). That
is the style of the book Gradle in Action (Gradle 1.7), and it still works on Gradle 9.8.1: the same tidepool build with the versions in
ext resolves to exactly the same graph.
group = "io.tidepool"
version = "1.0.0"
val commonsCsvVersion by extra("1.14.1")
val picocliVersion by extra("4.7.7")
plugins {
`java-library`
}
val commonsCsvVersion: String by rootProject.extra
dependencies {
api(project(":tidepool-model"))
implementation("org.apache.commons:commons-csv:$commonsCsvVersion")
}
group = 'io.tidepool'
version = '1.0.0'
ext {
commonsCsvVersion = '1.14.1'
picocliVersion = '4.7.7'
}
plugins {
id 'java-library'
}
dependencies {
api project(':tidepool-model')
implementation "org.apache.commons:commons-csv:${rootProject.ext.commonsCsvVersion}"
}
Both versions printed the same tree for the command line application:
$ ./gradlew :tidepool-cli:dependencies --configuration runtimeClasspath
...
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
\--- info.picocli:picocli:4.7.7
The ext approach has three weaknesses that a catalog removes. A version is a bare String property, so nothing says which coordinates it
belongs to and a typo in the property name is found only when the script runs; there is no name for a library, so the group and artifact
are retyped in every project; and an IDE cannot offer completion or navigation for it. A catalog declares the whole coordinate once, gives it a
name, and generates a typed accessor for the name. The Gradle best practices page for dependencies says it plainly: define versions in a
catalog instead of in project.ext, constants or local variables.
A catalog is a declaration device and nothing else. It does not change how Gradle resolves the graph: a version in the catalog is an ordinary requirement of the project that uses it, exactly as if the string had been typed in the build script, and the resolved version can still be higher (conflict resolution) or different (a platform, a constraint, a rule). Consumers of the project cannot see whether a dependency came from a catalog. The section on platforms below shows this on the tidepool build.
History
| Release | Change |
|---|---|
Gradle 7.0 (2021-04-09) |
Version catalogs appear as an experimental feature, switched on in settings with |
Gradle 7.2 |
Still a feature preview. Aliases can now have sub-aliases ( |
Gradle 7.4 (2022-02-08) |
Version catalogs are stable. |
Gradle 8.1 |
Plugin aliases in the |
Gradle 8.5 |
The |
Gradle 8.7 |
The TOML parser is upgraded and is now compliant with version 1.0.0 of the TOML specification. |
Gradle 8.8 |
A plugin alias without a version is explicitly allowed. |
Gradle 9.0.0 (2025-07-31) |
The deprecated use of libraries and bundles in the |
Source: Gradle 7.0 release notes: Centralized dependency versions, 7.2: Version catalog improvements, 7.4: Version catalogs, 8.5: Build init utilizing version catalogs, 8.5: Version catalog API in precompiled scripts, Upgrading from 8.x: parser upgrade, 8.8: aliases without a version and Upgrading to 9.0: removal in the plugins block
The book Gradle in Action was written for Gradle 1.7 and predates catalogs by eight years: its builds keep versions in ext or in plain
local variables and declare them with the compile and testCompile configurations, which were removed in Gradle 7.0 (see
Versions, Compatibility & Release History).
Translate the idea (one place for versions) into a catalog and the configurations into implementation and testImplementation.
The tidepool catalog
The catalog file lives in the gradle directory of the root build, with exactly the name libs.versions.toml. Gradle imports that file
automatically, under the catalog name libs, so nothing in settings.gradle(.kts) mentions it. The file below is the one used on the
whole page. It is shown as plain text because the syntax highlighter has no TOML grammar:
[versions]
assertj = "3.27.7"
commons-csv = "1.14.1"
junit = "6.1.3"
picocli = "4.7.7"
[libraries]
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
commons-csv = { module = "org.apache.commons:commons-csv", version.ref = "commons-csv" }
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }
junit-launcher = { module = "org.junit.platform:junit-platform-launcher" }
picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
[bundles]
testing = ["junit-jupiter", "assertj-core"]
[plugins]
versions = { id = "io.github.ben-manes.versions", version = "0.66.0" }
The settings file only says where repositories come from. Forbidding repositories in the projects is what makes the catalog and the repositories the two things you have to look at to know where a dependency comes from (see Repositories & Content Filtering):
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
rootProject.name = "tidepool"
include("tidepool-model", "tidepool-engine", "tidepool-cli")
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
rootProject.name = 'tidepool'
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
The root project applies one plugin from the catalog, and the three projects use libraries and the bundle:
plugins {
alias(libs.plugins.versions)
}
group = "io.tidepool"
version = "1.0.0"
plugins {
`java-library`
}
dependencies {
testImplementation(platform(libs.junit.bom))
testImplementation(libs.bundles.testing)
testRuntimeOnly(libs.junit.launcher)
}
tasks.test {
useJUnitPlatform()
}
plugins {
`java-library`
}
dependencies {
api(project(":tidepool-model"))
implementation(libs.commons.csv)
}
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation(libs.picocli)
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
alias(libs.plugins.versions)
}
group = 'io.tidepool'
version = '1.0.0'
plugins {
id 'java-library'
}
dependencies {
testImplementation platform(libs.junit.bom)
testImplementation libs.bundles.testing
testRuntimeOnly libs.junit.launcher
}
tasks.named('test') {
useJUnitPlatform()
}
plugins {
id 'java-library'
}
dependencies {
api project(':tidepool-model')
implementation libs.commons.csv
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation libs.picocli
}
application {
mainClass = 'io.tidepool.cli.Main'
}
No version number appears in any build script. Both copies built and ran the JUnit test of the model project (one test, passed), and the second run was served from the configuration cache:
$ ./gradlew build
> Task :tidepool-model:compileJava
> Task :tidepool-model:jar
> Task :tidepool-engine:compileJava
> Task :tidepool-engine:jar
> Task :tidepool-cli:compileJava
> Task :tidepool-cli:startScripts
> Task :tidepool-cli:distZip
...
> Task :tidepool-model:test
> Task :tidepool-model:check
> Task :tidepool-model:build
BUILD SUCCESSFUL in 1s
11 actionable tasks: 11 executed
Configuration cache entry stored.
$ ./gradlew build
BUILD SUCCESSFUL in 493ms
11 actionable tasks: 11 up-to-date
Configuration cache entry reused.
The resolved graph of the model project’s test classpath shows what the catalog did not decide. junit-jupiter has no version in the
catalog; the version comes from the JUnit BOM that the project imports with platform(libs.junit.bom), and the dependency tree shows it as
junit-jupiter -> 6.1.3:
$ ./gradlew :tidepool-model:dependencies --configuration testRuntimeClasspath
...
testRuntimeClasspath - Runtime classpath of source set 'test'.
+--- org.junit:junit-bom:6.1.3
| +--- org.junit.jupiter:junit-jupiter:6.1.3 (c)
| +--- org.junit.platform:junit-platform-launcher:6.1.3 (c)
| +--- org.junit.jupiter:junit-jupiter-engine:6.1.3 (c)
| +--- org.junit.jupiter:junit-jupiter-api:6.1.3 (c)
| +--- org.junit.jupiter:junit-jupiter-params:6.1.3 (c)
| +--- org.junit.platform:junit-platform-engine:6.1.3 (c)
| \--- org.junit.platform:junit-platform-commons:6.1.3 (c)
+--- org.junit.jupiter:junit-jupiter -> 6.1.3
| +--- org.junit.jupiter:junit-jupiter-engine:6.1.3
...
If the BOM line is removed, the version-less alias has nothing to take a version from, and the failure message says so with a trailing colon and nothing after it (run with the configuration cache off, because with it on the failure is wrapped in a configuration cache message):
$ ./gradlew --no-configuration-cache :tidepool-model:compileTestJava
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':tidepool-model:compileTestJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':tidepool-model:testCompileClasspath'.
> Could not find org.junit.jupiter:junit-jupiter:.
Required by:
project ':tidepool-model'
The same example is configuration cache compatible: the catalog accessors are providers that Gradle serialises like any other dependency
notation, and every run above after the first said Configuration cache entry reused.
The TOML format
The file has four tables. Everything is optional; an alias in one table never conflicts with the same alias in another.
| Table | Purpose |
|---|---|
|
Named versions that libraries and plugins refer to with |
|
Aliases for coordinates: group, name and (optionally) a version. |
|
Aliases for lists of library aliases, so that a group that always travels together is declared with one name. |
|
Aliases for a plugin id and its version. |
Versions
[versions]
other-lib = "5.5.0" # a required version
my-lib = { strictly = "[1.0, 2.0[", prefer = "1.2" } # a rich version
The members of a version table are require, strictly, prefer, reject (a list of rejected versions) and rejectAll (a boolean).
Their meaning is the one of rich versions in a build script, described on
Platforms, Constraints & Rich Versions. The
catalog is only a way to write the same thing down. With the catalog changed to
[versions]
commons-csv = { strictly = "[1.10, 2.0[", prefer = "1.13.0", reject = ["1.12.0"] }
and nothing else touched, the engine project resolves the preferred version, and the tree prints the whole rich version before the arrow (the same in both DSLs):
$ ./gradlew :tidepool-engine:dependencies --configuration runtimeClasspath
...
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-model'
\--- org.apache.commons:commons-csv:{strictly [1.10, 2.0[; prefer 1.13.0; reject 1.12.0} -> 1.13.0
+--- commons-io:commons-io:2.18.0
\--- commons-codec:commons-codec:1.17.2
Source: Gradle User Manual: Versions
The transitive versions changed with the library (commons-io 2.18.0 instead of 2.20.0), which is the reason a catalog entry is a requirement
and not a pin; pinning every transitive version is the job of dependency locking (see
Dependency Locking, Verification & Caching).
Libraries
A library is a string with the coordinates, or a table. All the forms below are valid; the first column is what the manual shows:
| Declaration | Meaning |
|---|---|
|
A string is group, name and version. |
|
A dotted key (a TOML shortcut for a table) with no version; the version comes from somewhere else (a platform). |
|
|
|
Group and name written separately. |
|
The version is itself a table with |
|
|
Source: Gradle User Manual: Libraries
What a library entry cannot say matters as much. Version catalogs capture only the coordinates and a version: classifiers, artifact types, exclusion rules and capability requirements are not supported in the TOML file, and an unknown key is a failure (see the problems below). The workarounds are in the section on classifiers.
Bundles
[libraries]
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
[bundles]
testing = ["junit-jupiter", "assertj-core"]
A bundle is used wherever a dependency is, and adds every member to that one configuration: testImplementation(libs.bundles.testing). A
bundle cannot say that one member is an api dependency and another runtimeOnly; the tidepool catalog therefore keeps
junit-launcher out of the bundle, since it belongs to testRuntimeOnly. A bundle can refer only to aliases of the same catalog; an alias
that does not exist is a failure.
Source: Gradle User Manual: Bundles
Plugins
[plugins]
versions = { id = "io.github.ben-manes.versions", version = "0.66.0" }
A plugin alias is applied with alias(...) in a plugins {} block, with the same limitations as a version in that block (the plugin is
resolved from the plugin repositories before the build script runs). The versions plugin of the example is the Gradle Versions Plugin,
which reports dependency updates and is the plugin that the manual itself uses in its examples. With the catalog in place, dependencyUpdates
lists what has a newer release, for the root project and for every project (excerpt of the Kotlin DSL run; the Groovy DSL run printed the same
report; the report file is written under build):
$ ./gradlew dependencyUpdates
...
> Task :dependencyUpdates
------------------------------------------------------------
: Project Dependency Updates (report to plain text file)
------------------------------------------------------------
The following dependencies are using the latest milestone version:
- info.picocli:picocli:4.7.7
- io.github.ben-manes.versions:io.github.ben-manes.versions.gradle.plugin:0.66.0
- org.apache.commons:commons-csv:1.14.1
- org.junit:junit-bom:6.1.3
- org.junit.jupiter:junit-jupiter:6.1.3
constrained by the platform org.junit:junit-bom
- org.junit.platform:junit-platform-launcher:6.1.3
constrained by the platform org.junit:junit-bom
The following dependencies have later milestone versions:
- org.assertj:assertj-core [3.27.7 -> 4.0.0-M1]
https://assertj.github.io/doc/#assertj-core
Gradle release-candidate updates:
- Gradle: [9.8.1: UP-TO-DATE]
...
BUILD SUCCESSFUL in 2s
5 actionable tasks: 5 executed
Configuration cache entry stored.
The plugin was published under a new id, and the run with the old one (com.github.ben-manes.versions) printed The
com.github.ben-manes.versions plugin id is deprecated; apply io.github.ben-manes.versions instead. A rename like that is a one-line change in the
catalog, not a search through the build scripts. Source of the plugin: Gradle Plugin Portal: io.github.ben-manes.versions.
Two rules about plugin aliases come straight from the manual and from the release notes:
-
A plugin from a catalog can be used in a
plugins {}block of a build script, but not in theplugins {}block of the settings file or a settings plugin. In the Kotlin DSL the attempt fails to compile withUnresolved reference 'libs', because there is no catalog yet while settings are being evaluated. -
Only versions and plugins of a catalog can be used in the
plugins {}block. A library or a bundle there was deprecated in Gradle 8.1 and has been an error since Gradle 9.0.0. Reproduced here on Gradle 9.8.1 withid(libs.picocli.get().module.toString())in the model project:
$ ./gradlew help
FAILURE: Build failed with an exception.
* Where:
Build file '/work/tidepool/tidepool-model/build.gradle.kts' line: 3
* What went wrong:
Accessing libraries or bundles from version catalogs in the plugins block is not allowed. Only use versions or plugins from catalogs in the plugins block.
Aliases and type-safe accessors
When the settings are evaluated, Gradle reads the catalog and generates one class per catalog (org.gradle.accessors.dm.LibrariesForLibs
for libs), registers an instance as the extension libs on every project, and exposes the catalog in four namespaces:
| Accessor | Type of the value | Example from the tidepool catalog |
|---|---|---|
|
A provider of a |
|
|
A provider of the version as a |
|
|
A provider of an |
|
|
A provider of a |
|
Source: Gradle Javadoc: MinimalExternalModuleDependency, Gradle Javadoc: ExternalModuleDependencyBundle and Gradle Javadoc: PluginDependency
Because the accessors are providers, they are lazy and safe for the configuration cache, and they can be passed to any API that takes a
provider of a dependency (dependencies.add(...), constraints, testImplementation(...)) without calling get().
How an alias becomes an accessor
An alias is made of identifiers separated by a dash or an underscore. Gradle normalises the separators to dots and each dot starts a nested group, so that an IDE can offer completion level by level:
| Alias in the file | Accessor | Notes |
|---|---|---|
|
|
Dashes become dots. |
|
|
Underscores do too; Gradle’s own guidance prefers dashes. |
|
|
A single segment stays flat. |
|
|
camelCase avoids the sub-group. |
|
|
camelCase and a dash mix. |
The catalog API normalises the same way. The task below (registered in the root project) lists what the catalog contains through the
VersionCatalogsExtension API, with the aliases printed in their normalised, dotted form:
tasks.register("printCatalog") {
val libs = versionCatalogs.named("libs")
val lines = buildList {
add("versions: " + libs.versionAliases.sorted().joinToString())
libs.libraryAliases.sorted().forEach { add("library " + it + " = " + libs.findLibrary(it).get().get()) }
libs.bundleAliases.sorted().forEach { add("bundle " + it + " = " + libs.findBundle(it).get().get()) }
libs.pluginAliases.sorted().forEach { add("plugin " + it + " = " + libs.findPlugin(it).get().get()) }
}
doLast { lines.forEach { println(it) } }
}
def catalog = extensions.getByType(VersionCatalogsExtension).named('libs')
tasks.register('printCatalog') {
def lines = []
lines << 'versions: ' + catalog.versionAliases.sort().join(', ')
catalog.libraryAliases.sort().each { lines << 'library ' + it + ' = ' + catalog.findLibrary(it).get().get() }
catalog.bundleAliases.sort().each { lines << 'bundle ' + it + ' = ' + catalog.findBundle(it).get().get() }
catalog.pluginAliases.sort().each { lines << 'plugin ' + it + ' = ' + catalog.findPlugin(it).get().get() }
doLast { lines.each { println it } }
}
Both printed the same lines. Note the dotted aliases (commons.csv, even for the version) and the version-less entries:
$ ./gradlew -q printCatalog
versions: assertj, commons.csv, junit, picocli
library assertj.core = org.assertj:assertj-core:3.27.7
library commons.csv = org.apache.commons:commons-csv:1.14.1
library junit.bom = org.junit:junit-bom:6.1.3
library junit.jupiter = org.junit.jupiter:junit-jupiter
library junit.launcher = org.junit.platform:junit-platform-launcher
library picocli = info.picocli:picocli:4.7.7
bundle testing = [org.junit.jupiter:junit-jupiter, org.assertj:assertj-core:3.27.7]
plugin versions = io.github.ben-manes.versions:0.66.0
The task reads the catalog while it is configured and keeps only strings for the execution phase, so it is configuration cache compatible; the
second run said Configuration cache entry reused. A task that kept the extension and called findLibrary in doLast would capture the
project, which the configuration cache does not allow (see
Configuration Cache & Isolated Projects).
The same API is what a convention plugin uses to read the catalog of the project it is applied to, shown on
Convention Plugins & Sharing Build Logic.
Naming rules and the errors for breaking them
An alias must match the regular expression below. Experiments with the tidepool catalog show what Gradle does with names that do not:
[a-z]([a-zA-Z0-9_.\-])+
| Alias added to the catalog | Result on Gradle 9.8.1 |
|---|---|
|
Accepted, becomes |
|
Accepted, becomes |
|
|
|
|
|
|
|
|
|
|
The full text of the reserved alias failure, as printed by the build:
$ ./gradlew help
FAILURE: Build failed with an exception.
* What went wrong:
org.gradle.api.InvalidUserDataException
> Reserved alias name
In version catalog libs, alias 'extensions' is a reserved alias
Alias 'extensions' is a reserved name in Gradle which prevents generation of accessors.
For more information, please refer to https://docs.gradle.org/9.8.1/userguide/version_catalog_problems.html#reserved_alias_name in the Gradle documentation.
Possible solution: Use a different alias which doesn't contain any of 'convention' or 'extensions'.
Two spellings that differ only in separators collide, because they map to the same accessor method. This is a failure of the whole catalog, not a warning:
$ ./gradlew help
FAILURE: Build failed with an exception.
* What went wrong:
org.gradle.api.InvalidUserDataException
> Accessor name clash
In version catalog libs, library aliases some.alias and someAlias are mapped to the same accessor name getSomeAlias()
A name clash was detected
For more information, please refer to https://docs.gradle.org/9.8.1/userguide/version_catalog_problems.html#accessor_name_clash in the Gradle documentation.
Possible solution: Use a different alias for some.alias and someAlias.
The failing catalog had someAlias and some-alias.
Sub-groups: libs.versions.jackson
A sub-group appears whenever one alias is the prefix of another. With these two versions in the catalog
[versions]
jackson = "2.17.1"
jackson-databind = "2.17.2"
libs.versions.jackson is no longer a version but the group that holds databind. Its get() does not exist, and the way back to the
version of the parent alias is asProvider(). Both DSLs gave the same output with asProvider(), and failed without it:
tasks.register("showJackson") {
val parent = libs.versions.jackson.asProvider().get()
val child = libs.versions.jackson.databind.get()
doLast { println("jackson=" + parent + " jackson-databind=" + child) }
}
tasks.register('showJackson') {
def parent = libs.versions.jackson.asProvider().get()
def child = libs.versions.jackson.databind.get()
doLast { println 'jackson=' + parent + ' jackson-databind=' + child }
}
$ ./gradlew -q showJackson
jackson=2.17.1 jackson-databind=2.17.2
Without asProvider() the Kotlin DSL does not compile (Unresolved reference on get), and the Groovy DSL fails with the dynamic version of the
same error:
> Could not create task ':showJackson'.
> No signature of method: org.gradle.accessors.dm.LibrariesForLibs$JacksonVersionAccessors.get() is applicable for argument types: () values: []
The manual’s advice is to avoid sub-group accessors by naming with camelCase when you control the names, and to use asProvider() when you
consume a published catalog whose names you cannot change.
A typo is found when the script is compiled
Misspelling an alias is an error of the build script, not of the catalog. The Kotlin DSL reports it at compile time, with the position, and the Groovy DSL when the script is evaluated:
$ ./gradlew help # Kotlin DSL: implementation(libs.picocl)
> Configure project :tidepool-cli
e: file:///work/tidepool/tidepool-cli/build.gradle.kts:7:25: Unresolved reference 'picocl'.
FAILURE: Build failed with an exception.
* Where:
Build file '/work/tidepool/tidepool-cli/build.gradle.kts' line: 7
* What went wrong:
Script compilation error:
Line 7: implementation(libs.picocl)
^ Unresolved reference 'picocl'.
1 error
$ ./gradlew help # Groovy DSL: implementation libs.picocl
FAILURE: Build failed with an exception.
* Where:
Build file '/work/tidepool/tidepool-cli/build.gradle' line: 7
* What went wrong:
A problem occurred evaluating project ':tidepool-cli'.
> Could not get unknown property 'picocl' for extension 'libs' of type org.gradle.accessors.dm.LibrariesForLibs.
Passing the provider, not its value
libs.picocli is a Provider<MinimalExternalModuleDependency>. In a dependency declaration libs.picocli and libs.picocli.get() are
identical, but in other APIs (substitution rules, constraints, anything that takes a dependency notation) pass the provider: Gradle
converts it as needed, while the unwrapped MinimalExternalModuleDependency is a ModuleVersionSelector, which some resolution APIs do not
accept where they expect a ModuleComponentSelector.
Declaring catalogs in the settings file
The TOML file is the declarative form. The same model is available in settings.gradle(.kts) through the versionCatalogs container of
dependencyResolutionManagement, which is the only way to do four things: use another file name or location, register more than one
catalog, take a catalog from a repository, and override or add entries.
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("gradle/tidepool.versions.toml"))
version("picocli", "4.7.6")
}
create("testLibs") {
from(files("gradle/test.versions.toml"))
}
create("tools") {
version("errorprone", "2.28.0")
library("errorprone-annotations", "com.google.errorprone", "error_prone_annotations").versionRef("errorprone")
library("jspecify", "org.jspecify:jspecify:1.0.0")
}
}
}
rootProject.name = "tidepool"
include("tidepool-model", "tidepool-engine", "tidepool-cli")
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
versionCatalogs {
libs {
from(files('gradle/tidepool.versions.toml'))
version('picocli', '4.7.6')
}
testLibs {
from(files('gradle/test.versions.toml'))
}
tools {
version('errorprone', '2.28.0')
library('errorprone-annotations', 'com.google.errorprone', 'error_prone_annotations').versionRef('errorprone')
library('jspecify', 'org.jspecify:jspecify:1.0.0')
}
}
}
rootProject.name = 'tidepool'
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
The catalog file was renamed from libs.versions.toml to tidepool.versions.toml (see below why), and gradle/test.versions.toml holds the test
libraries:
[versions]
junit = "6.1.3"
[libraries]
bom = { module = "org.junit:junit-bom", version.ref = "junit" }
jupiter = { module = "org.junit.jupiter:junit-jupiter" }
launcher = { module = "org.junit.platform:junit-platform-launcher" }
assertj = "org.assertj:assertj-core:3.27.7"
The model project then uses all three extensions, one per catalog:
plugins {
`java-library`
}
dependencies {
compileOnly(tools.errorprone.annotations)
compileOnly(tools.jspecify)
testImplementation(platform(testLibs.bom))
testImplementation(testLibs.jupiter)
testImplementation(testLibs.assertj)
testRuntimeOnly(testLibs.launcher)
}
tasks.test {
useJUnitPlatform()
}
plugins {
id 'java-library'
}
dependencies {
compileOnly tools.errorprone.annotations
compileOnly tools.jspecify
testImplementation platform(testLibs.bom)
testImplementation testLibs.jupiter
testImplementation testLibs.assertj
testRuntimeOnly testLibs.launcher
}
tasks.named('test') {
useJUnitPlatform()
}
Both DSLs built the three projects, and the command line application resolved the overridden version while the model’s compile classpath got the programmatic entries (configuration cache compatible, entry stored and reused):
$ ./gradlew :tidepool-cli:dependencies --configuration runtimeClasspath
...
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
\--- info.picocli:picocli:4.7.6
$ ./gradlew :tidepool-model:dependencies --configuration compileClasspath
...
compileClasspath - Compile classpath for source set 'main'.
+--- com.google.errorprone:error_prone_annotations:2.28.0
\--- org.jspecify:jspecify:1.0.0
Source: Gradle User Manual: Programming catalogs, Importing a catalog from a file, Importing multiple catalogs and Overwriting catalog versions
The rules this example shows, with the reproduction of each:
-
fromcan be called once per catalog. The default filegradle/libs.versions.tomlis imported automatically, so declaringcreate("libs") { from(files("gradle/libs.versions.toml")) }callsfroma second time and fails in both DSLs. That is the reason the example moved the file to another name. The same applies to a catalog built from two files. The failure:$ ./gradlew help FAILURE: Build failed with an exception. * What went wrong: Multiple 'from' invocations In version catalog libs, you can only call the 'from' method a single time The method was called more than once For more information, please refer to https://docs.gradle.org/9.8.1/userguide/version_catalog_problems.html#too_many_import_invocation in the Gradle documentation. Possible solution: Remove further usages of the method call. -
fromtakes exactly one file. Importing a missing file and importing two files at once each fail with their own message:> Import of external catalog file failed In version catalog tools, import of external catalog file failed File '/work/tidepool/gradle/tools.versions.toml' doesn't exist ... Possible solution: Make sure that the catalog file 'tools.versions.toml' exists before importing it. > Importing multiple files is not supported In version catalog tools, importing multiple files is not supported The import consists of multiple files ... Possible solution: Only import a single file. -
Calls after
fromadd or override entries.version("picocli", "4.7.6")replaced the version that the TOML file declared, and every library that refers to it withversion.reffollowed. An override affects only what is imported and what is written in dependency declarations: the resolved version can still differ because of conflict resolution. -
The programmatic API has the same four verbs as the file:
version,library,bundleandplugin. A library declared without a version in its notation is finished byversionRef,versionorwithoutVersion; one that is not finished is the problemalias_not_finished, an alias that was created but not registered. -
Every catalog is its own extension on all projects. Pick a name that cannot collide with an existing extension (the manual suggests a name that ends with
Libs, astestLibsabove), and do not uselibsfor a programmatic catalog when agradle/libs.versions.tomlexists.
Changing the name of the default catalog
The default catalog is called libs. If that name is taken (for example by an extension of a plugin), rename it for the whole build:
dependencyResolutionManagement {
defaultLibrariesExtensionName = "tidepoolLibs"
}
dependencyResolutionManagement {
defaultLibrariesExtensionName = 'tidepoolLibs'
}
With the setting in place gradle/libs.versions.toml is still imported, but as tidepoolLibs, and the scripts must use the new name
(implementation(tidepoolLibs.picocli)). Scripts that still said libs failed with Unresolved reference 'libs' (Kotlin DSL) and with
a script compilation problem (Groovy DSL); after renaming them, both DSLs built and printed the same graph as before.
More than one catalog, or one
The manual is sceptical about splitting. A single catalog centralises every coordinate, avoids version duplication and gives every project the same
accessors; use alias prefixes to group entries (junit-..., mockito-...) inside it. Split only when you need a boundary the file itself cannot
express:
-
Auditing what ships. When you must distinguish the dependencies of a published distribution from those used only for tests or code generation, separate catalogs make the difference visible where the dependency is declared.
-
Independent build logic. Plugins applied inside
build-logicorbuildSrcmay need their own catalog, resolved before the main build’s catalog exists. -
Organisational sharing. A company-wide catalog is published as an artifact and imported by many repositories, while each repository keeps a local catalog for its own dependencies.
Sharing one version between catalogs
Each TOML file is self-contained. There is no include and no version.ref that reaches into another file, and from is allowed once, so two files
cannot be merged. If a version must be the same in two catalogs, the manual describes three workarounds: declare it in both files and add a check that
they agree; keep the shared versions in a properties file that settings.gradle(.kts) injects into each catalog (what the Gradle project itself does);
or publish a base catalog and layer local overrides on top (the section on publishing below). The second workaround, run on tidepool with
gradle/shared-versions.properties holding commons-csv=1.14.1 and picocli=4.7.7 and those two versions removed from the TOML file:
val shared = java.util.Properties().apply {
file("gradle/shared-versions.properties").inputStream().use { load(it) }
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("gradle/tidepool.versions.toml"))
version("commons-csv", shared.getProperty("commons-csv"))
version("picocli", shared.getProperty("picocli"))
}
}
}
rootProject.name = "tidepool"
include("tidepool-model", "tidepool-engine", "tidepool-cli")
def shared = new Properties()
file('gradle/shared-versions.properties').withInputStream { shared.load(it) }
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
versionCatalogs {
libs {
from(files('gradle/tidepool.versions.toml'))
version('commons-csv', shared.getProperty('commons-csv'))
version('picocli', shared.getProperty('picocli'))
}
}
}
rootProject.name = 'tidepool'
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
The TOML file still refers to commons-csv and picocli with version.ref; the versions now come from the properties file. Both DSLs built the
project and resolved commons-csv:1.14.1 and picocli:4.7.7. The configuration cache tracks the properties file: after changing picocli=4.7.7 to
picocli=4.7.6, the next run printed Calculating task graph as configuration cache cannot be reused because file 'gradle/shared-versions.properties' has changed.
and resolved 4.7.6, and the run after that reused the new entry.
Catalogs in buildSrc and build-logic
The build logic of a build is compiled in a separate build (buildSrc or an included build such as build-logic), and that build has its own settings
and therefore its own catalogs. It does not inherit the catalog of the main build. Using libs in buildSrc/build.gradle(.kts) without doing anything
fails:
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
dependencies {
implementation(libs.commons.csv)
}
plugins {
id 'groovy-gradle-plugin'
}
repositories {
mavenCentral()
}
dependencies {
implementation libs.commons.csv
}
$ ./gradlew help # Kotlin DSL
> Configure project :buildSrc
e: file:///work/tidepool/buildSrc/build.gradle.kts:10:20: Unresolved reference 'libs'.
FAILURE: Build failed with an exception.
* Where:
Build file '/work/tidepool/buildSrc/build.gradle.kts' line: 10
* What went wrong:
Script compilation error:
Line 10: implementation(libs.commons.csv)
^ Unresolved reference 'libs'.
1 error
$ ./gradlew help # Groovy DSL
...
* What went wrong:
A problem occurred evaluating project ':buildSrc'.
> Could not get unknown property 'libs' for object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.
The fix is to import the file of the main build in the settings of the build-logic build, with a path that points up one level:
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
dependencyResolutionManagement {
versionCatalogs {
create('libs') {
from(files('../gradle/libs.versions.toml'))
}
}
}
The libs here is a second libs, built from the same file, and the file buildSrc/gradle/libs.versions.toml does not exist, so the automatic import
does not clash with the explicit one. With the import, both DSLs resolved the library in the buildSrc build:
$ ./gradlew :buildSrc:dependencies --configuration runtimeClasspath
...
Project ':buildSrc'
...
\--- org.apache.commons:commons-csv:1.14.1
+--- commons-io:commons-io:2.20.0
\--- commons-codec:commons-codec:1.19.0
A plugin from the catalog as a build-logic dependency
Convention plugins need the plugins they apply on the classpath of the build-logic project, as ordinary dependencies on the plugin’s marker
artifact (id:id.gradle.plugin:version). The plugins {} block of the build-logic project cannot do that, and a precompiled script plugin
cannot use alias(...) for external plugins. The catalog already holds the id and the version, so a small helper turns a plugin alias into the
dependency notation. The included build below was run in both DSLs and the convention plugin tidepool.updates applies the Versions plugin:
pluginManagement {
includeBuild("build-logic")
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
rootProject.name = "tidepool"
include("tidepool-model", "tidepool-engine", "tidepool-cli")
dependencyResolutionManagement {
repositories {
gradlePluginPortal()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
plugins {
`kotlin-dsl`
}
dependencies {
implementation(plugin(libs.plugins.versions))
}
// turns a plugin alias of the catalog into the dependency notation of the plugin's marker artifact
fun plugin(plugin: Provider<PluginDependency>) =
plugin.map { "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}" }
plugins {
id("io.github.ben-manes.versions")
}
plugins {
id("tidepool.updates")
}
group = "io.tidepool"
version = "1.0.0"
pluginManagement {
includeBuild('build-logic')
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
rootProject.name = 'tidepool'
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
dependencyResolutionManagement {
repositories {
gradlePluginPortal()
}
versionCatalogs {
create('libs') {
from(files('../gradle/libs.versions.toml'))
}
}
}
rootProject.name = 'build-logic'
plugins {
id 'groovy-gradle-plugin'
}
dependencies {
implementation plugin(libs.plugins.versions)
}
// turns a plugin alias of the catalog into the dependency notation of the plugin's marker artifact
def plugin(Provider<PluginDependency> plugin) {
return plugin.map { it.pluginId + ':' + it.pluginId + '.gradle.plugin:' + it.version }
}
plugins {
id 'io.github.ben-manes.versions'
}
plugins {
id 'tidepool.updates'
}
group = 'io.tidepool'
version = '1.0.0'
The precompiled script plugin has no version in its plugins {} block, because the version is the one of the dependency, which in turn is the one in the
catalog; dependencyUpdates is then registered in the root project by the convention plugin:
$ ./gradlew help --task dependencyUpdates
...
> Task :help
Detailed task information for dependencyUpdates
Path
:dependencyUpdates (registered by plugin 'io.github.ben-manes.versions')
Type
DependencyUpdatesTask (com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask)
...
Libraries (not plugins) of the catalog are read inside a precompiled script plugin with the VersionCatalogsExtension API, as in the java-conventions
plugin on Convention Plugins & Sharing Build Logic;
since Gradle 8.5 the Kotlin DSL offers the accessor versionCatalogs in such scripts. The plugins {} block of a precompiled script plugin cannot use
the catalog at all. The page on composite builds explains why an included build shares no catalog with its parent by itself (see
Composite Builds).
Publishing and importing a catalog
There are three ways to share a catalog between builds, in increasing effort: check the TOML file into a shared repository that all builds can see;
write a settings plugin that declares the entries and publish it; or publish the catalog as an artifact with the version-catalog plugin. The last is
the way for an organisation that distributes one catalog to many independent builds, and it is the one run here.
Publishing
Applying version-catalog adds a catalog extension, the task generateCatalogAsToml, and a software component versionCatalog that a
publication attaches. The easiest source is the existing TOML file, which then stays the single source of truth:
plugins {
`version-catalog`
`maven-publish`
}
group = "io.tidepool"
version = "1.0.0"
catalog {
versionCatalog {
from(files("gradle/libs.versions.toml"))
}
}
publishing {
publications {
create<MavenPublication>("catalog") {
from(components["versionCatalog"])
}
}
repositories {
maven {
name = "local"
url = uri(layout.buildDirectory.dir("repo"))
}
}
}
plugins {
id 'version-catalog'
id 'maven-publish'
}
group = 'io.tidepool'
version = '1.0.0'
catalog {
versionCatalog {
from(files('gradle/libs.versions.toml'))
}
}
publishing {
publications {
catalog(MavenPublication) {
from components.versionCatalog
}
}
repositories {
maven {
name = 'local'
url = uri(layout.buildDirectory.dir('repo'))
}
}
}
The settings file of that small build only names it (rootProject.name = "tidepool-catalog"), and gradle.properties turns the configuration cache on.
Publishing worked in both DSLs and is configuration cache compatible (the second run reused the entry):
$ ./gradlew publish
Calculating task graph as no cached configuration is available for tasks: publish
> Task :generateCatalogAsToml
> Task :generatePomFileForCatalogPublication
> Task :generateMetadataFileForCatalogPublication
> Task :publishCatalogPublicationToLocalRepository
> Task :publish
BUILD SUCCESSFUL in 1s
4 actionable tasks: 4 executed
Configuration cache entry stored.
The repository got a POM, a Gradle module metadata file and the catalog itself as an artifact with the extension toml (the checksum files are left out
of the listing):
build/repo/io/tidepool/tidepool-catalog/maven-metadata.xml
build/repo/io/tidepool/tidepool-catalog/1.0.0/tidepool-catalog-1.0.0.module
build/repo/io/tidepool/tidepool-catalog/1.0.0/tidepool-catalog-1.0.0.pom
build/repo/io/tidepool/tidepool-catalog/1.0.0/tidepool-catalog-1.0.0.toml
The POM has the packaging toml and a comment that tells Maven-only consumers that richer metadata exists. The module metadata describes one variant,
versionCatalogElements, with the attributes org.gradle.category=platform and org.gradle.usage=version-catalog (see
Variant-aware Resolution & Module Metadata). The published
TOML is rewritten by Gradle in the expanded format, with a [metadata] header, one-line tables and the group and name split:
#
# This file has been generated by Gradle and is intended to be consumed by Gradle
#
[metadata]
format.version = "1.1"
[versions]
assertj = "3.27.7"
commons-csv = "1.14.1"
junit = "6.1.3"
picocli = "4.7.7"
[libraries]
assertj-core = {group = "org.assertj", name = "assertj-core", version.ref = "assertj" }
commons-csv = {group = "org.apache.commons", name = "commons-csv", version.ref = "commons-csv" }
junit-bom = {group = "org.junit", name = "junit-bom", version.ref = "junit" }
junit-jupiter = {group = "org.junit.jupiter", name = "junit-jupiter", version = "" }
junit-launcher = {group = "org.junit.platform", name = "junit-platform-launcher", version = "" }
picocli = {group = "info.picocli", name = "picocli", version.ref = "picocli" }
[bundles]
testing = ["junit-jupiter", "assertj-core"]
[plugins]
versions = {id = "io.github.ben-manes.versions", version = "0.66.0" }
A catalog can also be declared inside the catalog { versionCatalog { ... } } block with the same version, library, bundle and plugin verbs as in
the settings (for example library("my-lib", "com.mycompany:mylib:1.2")), with no TOML file at all. Publishing to a remote repository only changes the
repositories block (a Maven Central or internal repository with credentials from the environment) and is not run here.
Importing
The consumer imports the catalog in settings with ordinary dependency notation. Gradle downloads the TOML file from the repositories declared in
dependencyResolutionManagement (so the repository that serves the catalog must be listed there), and any entry can be overridden afterwards:
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven {
name = "catalogs"
url = uri("../tidepool-catalog/build/repo")
}
mavenCentral()
}
versionCatalogs {
create("libs") {
from("io.tidepool:tidepool-catalog:1.0.0")
version("picocli", "4.7.6")
}
}
}
rootProject.name = "tidepool"
include("tidepool-model", "tidepool-engine", "tidepool-cli")
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven {
name = 'catalogs'
url = uri('../tidepool-catalog/build/repo')
}
mavenCentral()
}
versionCatalogs {
libs {
from('io.tidepool:tidepool-catalog:1.0.0')
version('picocli', '4.7.6')
}
}
}
rootProject.name = 'tidepool'
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
The consumer has no gradle/libs.versions.toml at all. Both DSLs built it, and the tree shows the published versions plus the local override:
$ ./gradlew build
...
BUILD SUCCESSFUL in 778ms
11 actionable tasks: 11 executed
Configuration cache entry stored.
$ ./gradlew :tidepool-cli:dependencies --configuration runtimeClasspath
...
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
\--- info.picocli:picocli:4.7.6
If the repository that serves the catalog is not declared, the failure is a dependency-resolution failure of a configuration with the name
incomingCatalogForLibs0, not a catalog message:
$ ./gradlew help
FAILURE: Build failed with an exception.
* What went wrong:
Could not resolve all artifacts for configuration 'incomingCatalogForLibs0'.
> Could not find io.tidepool:tidepool-catalog:1.0.0.
Searched in the following locations:
- https://repo.maven.apache.org/maven2/io/tidepool/tidepool-catalog/1.0.0/tidepool-catalog-1.0.0.pom
Required by:
Dependency Resolution Management
> If the artifact you are trying to retrieve can be found in the repository but without metadata in 'Maven POM' format, you need to adjust the 'metadataSources { ... }' of the repository declaration.
An imported catalog is a dependency like any other: it is resolved from the repositories of the settings, so pin it with an exact version (never a
dynamic one), and for builds that must be reproducible see how downloaded artifacts are verified on Supply Chain Security and
Dependency Locking, Verification & Caching.
Overriding a version of an imported catalog (version("picocli", "4.7.6")) changes what the accessors return, not what conflict resolution does with it.
Catalogs and platforms
A catalog and a platform answer different questions. A catalog decides what you write; a platform decides what Gradle selects.
| Version catalog | Platform (java-platform, BOM) |
|
|---|---|---|
Purpose |
Names the coordinates and versions that a build declares, with type-safe accessors. |
A module in the dependency graph that supplies or enforces versions for dependencies, including transitive ones. |
Effect on resolution |
None by itself: a version from the catalog is an ordinary requirement. |
Constraints take part in conflict resolution and are propagated to consumers. |
Visible to consumers of your library |
No: they cannot tell that a dependency came from a catalog. |
Yes: it is part of your published metadata. |
Typical use |
Share coordinates across the projects of one build, or across builds. |
Align versions across modules and enforce them. |
The tidepool build shows how little the catalog decides. A tidepool-platform project (the java-platform plugin) has a constraint on
commons-csv, and the engine imports the platform next to a catalog entry:
plugins {
`java-platform`
}
dependencies {
constraints {
api("org.apache.commons:commons-csv:1.12.0")
}
}
plugins {
`java-library`
}
dependencies {
api(project(":tidepool-model"))
implementation(platform(project(":tidepool-platform")))
implementation(libs.commons.csv)
}
plugins {
id 'java-platform'
}
dependencies {
constraints {
api 'org.apache.commons:commons-csv:1.12.0'
}
}
plugins {
id 'java-library'
}
dependencies {
api project(':tidepool-model')
implementation platform(project(':tidepool-platform'))
implementation libs.commons.csv
}
With the catalog still saying 1.14.1, the constraint (1.12.0) loses, because conflict resolution takes the highest requirement (both DSLs printed the same tree;
(c) marks a constraint):
$ ./gradlew :tidepool-engine:dependencies --configuration runtimeClasspath
...
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-model'
+--- project ':tidepool-platform'
| \--- org.apache.commons:commons-csv:1.12.0 -> 1.14.1 (c)
\--- org.apache.commons:commons-csv:1.14.1
+--- commons-io:commons-io:2.20.0
\--- commons-codec:commons-codec:1.19.0
(c) - A dependency constraint, not a dependency. The dependency affected by the constraint occurs elsewhere in the tree.
If the platform constraint is made strict, the catalog’s 1.14.1 is outside the allowed range, and the result is not a silent win for either side but a resolution failure (both DSLs; excerpt):
dependencies {
constraints {
api("org.apache.commons:commons-csv") {
version {
strictly("1.12.0")
}
}
}
}
dependencies {
constraints {
api('org.apache.commons:commons-csv') {
version {
strictly '1.12.0'
}
}
}
}
$ ./gradlew :tidepool-engine:dependencies --configuration runtimeClasspath
...
+--- project ':tidepool-platform'
| \--- org.apache.commons:commons-csv:{strictly 1.12.0} FAILED
\--- org.apache.commons:commons-csv:1.14.1 FAILED
$ ./gradlew :tidepool-engine:dependencyInsight --configuration runtimeClasspath --dependency commons-csv
org.apache.commons:commons-csv:1.12.0 (by constraint) FAILED
Failures:
- Could not resolve org.apache.commons:commons-csv:{strictly 1.12.0}.
- Cannot find a version of 'org.apache.commons:commons-csv' that satisfies the version constraints:
Dependency path: project ':tidepool-engine' (runtimeClasspath) --> 'org.apache.commons:commons-csv:1.14.1'
Constraint path: project ':tidepool-engine' (runtimeClasspath) --> project ':tidepool-platform' (runtimeElements) --> 'org.apache.commons:commons-csv:{strictly 1.12.0}'
...
The pattern the manual recommends for using both is to give the catalog entry no version and let the platform decide, so that there is one place where
the version is chosen: the catalog names the coordinates and the platform supplies the number. With the line commons-csv = { module = "org.apache.commons:commons-csv" } in the
catalog (and the plain constraint again), the engine gets the platform’s version:
$ ./gradlew :tidepool-engine:dependencies --configuration runtimeClasspath
...
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-model'
+--- project ':tidepool-platform'
| \--- org.apache.commons:commons-csv:1.12.0 (c)
\--- org.apache.commons:commons-csv -> 1.12.0
+--- commons-io:commons-io:2.17.0
\--- commons-codec:commons-codec:1.17.1
That build then fails to compile, which is a good reminder that downgrading is not free: the engine calls CSVFormat.Builder.get(), a method that
Commons CSV 1.12.0 does not have (error: cannot find symbol, symbol method get(), location class Builder). The JUnit BOM in the tidepool catalog is the same idea
in the other direction: junit-jupiter and junit-launcher are version-less aliases, and platform(libs.junit.bom) is the platform that gives them a version. Platforms,
enforcedPlatform and the rich-version vocabulary are covered on
Platforms, Constraints & Rich Versions.
Classifiers, artifact types and the TOML limits
The TOML file cannot express classifiers, artifact types (@aar, @zip), exclusion rules or capability requirements. The workaround is to declare these where
the dependency is declared, using the catalog entry as the base. A classifier is added with variantOf:
plugins {
`java-library`
}
val sources by configurations.creating {
isCanBeConsumed = false
isTransitive = false
}
dependencies {
api(project(":tidepool-model"))
implementation(libs.commons.csv)
sources(variantOf(libs.commons.csv) { classifier("sources") })
}
tasks.register("showSources") {
val jars = sources.incoming.files
doLast { jars.forEach { println(it.name) } }
}
plugins {
id 'java-library'
}
configurations {
sources {
canBeConsumed = false
transitive = false
}
}
dependencies {
api project(':tidepool-model')
implementation libs.commons.csv
sources variantOf(libs.commons.csv) { classifier('sources') }
}
tasks.register('showSources') {
def jars = configurations.sources.incoming.files
doLast { jars.each { println it.name } }
}
$ ./gradlew -q :tidepool-engine:showSources
commons-csv-1.14.1-sources.jar
Both DSLs resolved the sources JAR from the same catalog entry that the production dependency uses, and the task is configuration cache compatible (a second run said
Configuration cache entry reused). The same variantOf is used for platform-specific native artifacts (classifier("linux-x86_64")); for test fixtures the manual prefers
testFixtures(libs.my.lib) over spelling the classifier. An artifact type is set in an artifact {} block of the dependency (artifact { name = "my-lib"; type = "aar" }),
the form that Android builds use; it was not run here because it needs an Android library (see
Project Structure & Gradle for a catalog in an Android project). Exclusions and capabilities are written in the
dependency’s own configuration closure, as on
Dependency Resolution, Conflicts & Capabilities.
Fixing catalog problems
Every problem in a catalog fails the build at settings time, before any project is configured, with a message in the same four-part shape: a title, what is wrong, a link,
and a "Possible solution". The link in the message of Gradle 9.8.1 points to version_catalog_problems.html, a page that exists but shows no content; the explanations are on the
page How to Fix Version Catalog Problems under the same anchor names (accessor_name_clash, too_many_import_invocation, and so on). The table lists all nineteen documented
problems with the cause and the fix; the ones marked "run" were reproduced for this page, and the messages shown on the page above are real output.
| Problem (anchor) | Cause | Fix | Run |
|---|---|---|---|
|
Two aliases normalise to the same accessor ( |
Use distinct aliases. |
run |
|
The catalog exceeds the 32,000 entries that the generated JVM class format allows. |
Split the catalog. |
no |
|
An alias is |
Rename it ( |
run |
|
|
Declare it, or use an existing one. The message lists the existing versions: |
run |
|
A bundle contains a library alias that does not exist. |
Declare the library or remove it from the bundle ( |
run |
|
A programmatic |
Use |
no |
|
The imported file is not TOML (for example |
Use a TOML file, or the Settings API. |
no |
|
|
Create the file or fix the path. |
run |
|
An alias does not match the regular expression above ( |
Rename it. |
run |
|
|
Write |
run |
|
The file is not valid TOML ( |
Fix the syntax; the message has the file and the line. |
run |
|
The catalog file uses a format version that the installed Gradle does not support. |
Upgrade Gradle. |
no |
|
A plugin entry does not separate the id from the version. |
Use the |
no |
|
A programmatic alias was created but not completed. |
End it with |
no |
|
|
One |
run |
|
The import statement references a file that does not exist. |
Make the file available before importing it. |
no |
|
|
Give it exactly one file. |
run |
|
A table has a key that is not allowed there ( |
Use only |
run |
|
A version string cannot be parsed (a strict |
Put a version in front of |
no |
Three of the failures with their exact text, for recognition:
> Undefined version reference
In version catalog libs, version reference 'picocl' doesn't exist
Dependency 'info.picocli:picocli' references version 'picocl' which doesn't exist
For more information, please refer to https://docs.gradle.org/9.8.1/userguide/version_catalog_problems.html#undefined_version_reference in the Gradle documentation.
Possible solutions:
1. Declare 'picocl' in the catalog.
2. Use one of the following existing versions: 'assertj', 'commons.csv', 'junit', or 'picocli'.
> Bundle declares dependency on non-existent alias
In version catalog libs, a bundle with name 'testing' declares a dependency on 'assertj.coree' which doesn't exist
Bundles can only contain references to existing library aliases.
...
Possible solutions:
1. Make sure that the library alias 'assertj.coree' is declared.
2. Remove 'assertj.coree' from bundle 'testing'.
> Invalid TOML definition
On library declaration 'extra' expected to find any of 'group', 'module', 'name', or 'version' but found unexpected key 'scope'
TOML file contains an unexpected key in a known table
...
Possible solution: Remove the unexpected key, or use one of 'group', 'module', 'name', or 'version'.
and the two that depend on TOML syntax:
> TOML syntax error
Unexpected '.', expected a newline or end-of-input
TOML syntax invalid
...
Location: /work/tidepool/gradle/libs.versions.toml:5
Possible solution: Fix the TOML file according to the syntax described at https://toml.io.
> Invalid module notation
In version catalog libs, on alias 'picocli' module 'info.picocli' is not a valid module notation
When using a string to declare library module coordinates, you must use a valid module notation
...
Possible solution: Make sure that the module consist of 2 parts separated by colons, eg: my.group:artifact.
Source: Gradle User Manual: How to Fix Version Catalog Problems
(with the anchors too_many_entries, reserved_alias_name, undefined_version_reference, undefined_alias_reference, invalid_dependency_notation, unsupported_file_format, catalog_file_does_not_exist, invalid_alias_notation, invalid_module_notation, toml_syntax_error, unsupported_format_version, invalid_plugin_notation, alias_not_finished, too_many_import_invocation, no_import_files, too_many_import_files, invalid_toml_definition and invalid_version_notation)
Naming entries
The best practices page asks for consistent, descriptive aliases and gives rules that make the accessors read well. They are guidelines, not checked by Gradle:
-
Separate segments with dashes, not underscores (
log4j-api). -
Derive the first segment from the group, without the top-level domain (
jackson-databind,armeria-grpc), and the second from the artifact. -
Leave out generic terms such as
core,java,gradle,moduleorsdkwhen they stand alone (google-java-format, notgoogle-java). -
Omit a repeated segment when group and artifact agree (
ktor-client-core, notktor-ktor-client-core). -
Turn internal dashes of the artifact into camelCase (
springBootStarterWeb), which also avoids unwanted sub-groups. -
Suffix a plugin that is declared as a library (not in
[plugins]) with-plugin(dependency-check-plugin).
The same page warns against the opposite misuse: a catalog is for coordinates and versions, not for shared strings, constants or build logic. It also says that a catalog only influences declared versions, so combine it with dependency locking and version alignment when the resolved versions must be pinned.
Starting from gradle init
Since Gradle 8.5 the Build Init plugin generates a catalog. Run on 9.8.1 with gradle init --type java-application --dsl kotlin --test-framework junit-jupiter --package demo --project-name demo --java-version 21 --use-defaults,
it wrote this file (with a comment header pointing to the TOML documentation), plus a build script that already uses libs.junit.jupiter and libs.guava:
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/version_catalogs.html#sec::toml-dependencies-format
[versions]
guava = "33.5.0-jre"
junit-jupiter = "6.0.1"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
Treat the generated versions as a starting point to bump (the Versions plugin above is the tool for that). The generated build leaves
testRuntimeOnly("org.junit.platform:junit-platform-launcher") as a string with no version, and it still resolves: running dependencies on the test runtime classpath of the
generated project showed junit-platform-launcher -> 6.0.1, because junit-jupiter brings the JUnit BOM into the graph. See
Installation for gradle init itself.
Gradle 8.14 and Gradle 9.8.1
Gradle 8.14.x was not available here, so this table is from the official documentation and is not run here:
| Topic | Difference between 8.14 and 9.8.1 |
|---|---|
Catalog file, accessors, settings API, |
Version catalogs have been stable since 7.4, and the manual describes the same format and API. The changes in between are the ones in the history table above: |
Libraries and bundles in the |
On 8.14 this is a deprecation warning (deprecated in 8.1); on 9.x it is the build failure shown above. |
JVM to run Gradle |
8.14 runs on JVM 8 and newer; 9.x needs JVM 17 or newer to run the build (see Versions, Compatibility & Release History). |
Configuration cache |
Off by default in both; preferred mode in 9.x. Every example above is compatible with it. |
Equivalents in Maven and Ant with Ivy
| Concern | Gradle | Maven | Ant with Ivy |
|---|---|---|---|
One place for versions |
|
|
A properties file loaded with |
One place for coordinates |
|
|
|
Group that travels together |
|
A POM with |
An Ivy configuration or a dependency on a module that depends on the others |
Share across builds |
|
Publish a parent POM or a BOM and inherit or import it |
Publish the properties file or the settings file, which can be loaded from a URL |
Decides the resolved version? |
No (use a platform) |
Yes: managed versions apply to transitive dependencies too |
Through Ivy conflict managers and overrides |
See Dependency Management & BOMs, POM Fundamentals, Ant Properties, Ivy Settings & Resolvers and the cross-tool view in Comparing Ant, Maven & Gradle.
Classic mistakes
| Mistake | What happens and the fix |
|---|---|
Treating the catalog version as the resolved version |
A platform, a constraint, a higher transitive requirement or a rule changes the selection. Check with |
|
The file is already imported, so |
Using |
|
A library or bundle in the |
Fails on Gradle 9 ( |
A catalog plugin alias in the settings file |
Not supported: the catalog does not exist yet while the settings are evaluated. |
Aliases that differ only in separators or case |
|
|
The sub-group has no |
A version-less alias without a platform |
|
Putting a classifier, an exclusion or a capability in the TOML file |
Not supported ( |
Reading the catalog extension inside |
Captures the project and breaks the configuration cache; read it while configuring and keep only values. |
Editing the published TOML or trusting a floating catalog version |
Pin an exact catalog version and verify it like any other artifact. |
Where to go next
The projects that use a catalog and the shared logic around it are on Multi-Project Builds and Convention Plugins & Sharing Build Logic; the way a version is finally chosen is on Dependency Resolution, Conflicts & Capabilities and Resolution Rules & Substitution; the migration of a Maven build, whose versions go into a catalog, is on Migrating from Maven; and the recommended habits are collected on Best Practices & Troubleshooting.