Gradle Platforms, Constraints & Rich Versions
|
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 (the daemon ran on JDK 26; nothing is compiled, so no toolchain was involved). Every example was run through the wrapper in scratch copies of a small project, once with Kotlin DSL build files and once with Groovy DSL build files, and the output is the same in both except where the text says otherwise. The scratch copies pointed their wrapper at a local copy of the 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 -q. The legend lines that Gradle prints under a dependency report ((c) and (*)) and
the Try: hints at the end of a failure are left out, and so is the table of variant attributes in a dependencyInsight
report, which is shown as .... The reports (dependencies, dependencyInsight), the probeVersions helper and publishing were checked with
--configuration-cache (stored, then reused) and are compatible with it; the examples that fail on purpose were run
without it. Gradle 8.14.x was not
installed here, so statements about it come from the official documentation and are labelled "not run here".
A version in a dependency declaration is a small program: it says which versions are acceptable, which one is preferred, and how strongly the declaring build cares. Gradle collects those statements from the whole dependency graph and picks one version per module. This page is about the vocabulary for those statements and the objects that carry them:
-
rich versions (
require,strictly,prefer,reject) say how much a single declaration cares; -
dependency constraints say something about a module without adding it to the graph;
-
platforms, either imported from a Maven BOM or built in Gradle, are sets of constraints that travel as one dependency;
-
alignment makes modules that belong together use one version.
How the chosen version is then computed (graph resolution, capability conflicts, the "highest wins" rule itself) is on Dependency Resolution, Conflicts & Capabilities; declaring dependencies and the configurations they live in on Declaring Dependencies & Configurations; catalogs that centralise the coordinates on Version Catalogs; forcing and substituting on Resolution Rules & Substitution; and pinning the result on Dependency Locking, Verification & Caching. The tool-neutral comparison of coordinates, scopes, conflicts and BOMs is Dependency Management Fundamentals.
The scenario used on this page
To make every dependency graph exact, the examples resolve against two kinds of modules. The first is a small local
file repository of made-up modules in the group io.tidepool.demo (a POM and an empty JAR for each version, generated by
a script), so that the result does not change when someone publishes a new release to Maven Central. The second is real
software from Maven Central where the real thing teaches more: the JUnit BOM 6.1.3 and Apache Commons CSV 1.14.1. Two
projects named after the tidepool modules of the other pages (tidepool-model and tidepool-engine, here without any
source code) are published to a local repository for the platform and alignment examples.
| Module | Versions | What it depends on |
|---|---|---|
|
|
nothing |
|
|
|
|
|
|
|
|
|
|
|
a Maven BOM that manages |
|
|
a Maven BOM that manages |
The settings file of the scratch build declares the repositories once (see
Repositories & Content Filtering); the
file repository is restricted by a content filter so that Gradle never asks Maven Central about the made-up group. Every
example is a subproject that applies java-library, and the listings below show only its dependencies (and anything else
that is not that plugin line).
rootProject.name = "pcr"
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven {
url = uri(settingsDir.resolve("../repo"))
content { includeGroup("io.tidepool.demo") }
}
maven {
url = uri(settingsDir.resolve("../publisher/build/repo"))
content { includeGroup("io.tidepool") }
}
mavenCentral()
}
}
// one subproject per example, named as in the listings below
// included only on request: its configuration fails on purpose
if (providers.gradleProperty("badPlatform").isPresent) {
include("platform-bad")
}
rootProject.name = 'pcr'
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven {
url = new File(settingsDir, '../repo')
content { includeGroup 'io.tidepool.demo' }
}
maven {
url = new File(settingsDir, '../publisher/build/repo')
content { includeGroup 'io.tidepool' }
}
mavenCentral()
}
}
// one subproject per example, named as in the listings below
// included only on request: its configuration fails on purpose
if (providers.gradleProperty('badPlatform').present) {
include 'platform-bad'
}
A report is produced like this, for a project named rich-default, whose graph is the simplest one: harmonics
declared at 1.0.0, and tides-api:1.1.0 which needs harmonics:1.1.0.
dependencies {
implementation("io.tidepool.demo:harmonics:1.0.0")
implementation("io.tidepool.demo:tides-api:1.1.0")
}
dependencies {
implementation 'io.tidepool.demo:harmonics:1.0.0'
implementation 'io.tidepool.demo:tides-api:1.1.0'
}
./gradlew :rich-default:dependencies --configuration runtimeClasspath
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:harmonics:1.0.0 -> 1.1.0
\--- io.tidepool.demo:tides-api:1.1.0
\--- io.tidepool.demo:harmonics:1.1.0
A plain version is a require: "at least this one". The conflict was settled by taking the higher of the two requirements,
and -> 1.1.0 is how the report writes "this was asked for, that was selected".
Version declarations and ordering
A version string is one of five shapes. The shapes decide which versions are candidates; the rest of this page decides which candidate wins.
| Shape | Examples | Meaning |
|---|---|---|
Exact version |
|
One version. As a plain declaration it is a |
Maven-style range |
|
|
Prefix range |
|
Versions that start with the part before the |
Latest status |
|
The highest version with that status in the repository metadata. |
Maven SNAPSHOT |
|
A changing version: the same coordinates may point to different content over time. |
Whether a version lies in a range, and which of two versions is newer in a conflict, is decided by the version ordering. It is not semantic versioning, and it is not Maven’s comparator either (see Versions, SNAPSHOTs & Ranges):
-
A version is cut into parts at
.,-,_and+, and at every change between digits and letters, so1a1is1.a.1. Only the parts are compared, never the separators:1.a.1,1-a+1,1.a-1and1a1are equal. -
A number is lower than a bigger number, and any number is higher than a non-number:
1.ais older than1.1. -
Non-numbers compare alphabetically and case-sensitively:
1.A < 1.B < 1.a < 1.b. -
A version with an extra numeric part is higher, even when the part is zero, so
1.1is older than1.1.0. A version with an extra non-numeric part is lower:1.1.ais older than1.1. -
devis lower than any other word, so1.0-dev < 1.0-ALPHA < 1.0-alpha < 1.0-rc. Andrc,snapshot,final,ga,releaseandspare higher than any other word, in that order, regardless of case.
The "extra numeric part" rule bites in ranges: 1.3 is not the same version as 1.3.0, and 1.3.0 is higher. A range whose
upper bound is written 1.3 therefore does not include 1.3.0, as the dynamic-version run in
Dynamic versions and changing modules shows.
Rich versions
A dependency declaration that only carries a version string is a shorthand. The full form is a version constraint with
up to four parts, reached through the version { } block of a dependency or a constraint. From the weakest to the strongest:
| Term | What it says | Dynamic versions allowed |
|---|---|---|
|
The softest statement: "use this if nobody has a stronger opinion". It applies only when no stronger, non-dynamic version
is specified, and it complements |
No |
|
The selected version can not be lower than this, but conflict resolution may select a higher one, even above an exclusive
upper bound. It is what a plain |
Yes |
|
Only versions inside this range are acceptable, nothing outside it can be selected, and on a direct dependency it may
downgrade. For a transitive dependency with no acceptable version, resolution fails. It overrides a |
Yes |
|
Outside the hierarchy: versions that must not be selected. If one would be selected, resolution fails or moves on. It does not constrain the rest of the range. |
Yes |
The manual’s guidance on which one to use is worth keeping next to this page:
| Goal | Declaration | Why |
|---|---|---|
Prevent a transitive upgrade past a known-good version |
|
A strict range can not be overridden by a deeper participant in the graph. |
Exclude one known-broken version |
|
|
Bound the versions for the consumers of a library, but stay flexible |
|
The strict range caps consumers; |
Enforce a minimum for a security fix |
|
A constraint only applies when the module is in the graph. |
Make a constraint really cap a version |
|
A bare version in |
strictly: a range that can downgrade
rich-strictly-range declares harmonics with strictly("[1.0, 1.2[") and adds tides-api:1.1.0 (which wants
harmonics:1.1.0) and tides-chart:1.0.0 (which wants harmonics:1.2.0, outside the range). Without the strict range the
highest requirement, 1.2.0, would win. With it, 1.2.0 can not be selected, so tides-chart's requirement is overridden and
the highest version inside the range, 1.1.0, is chosen:
dependencies {
implementation("io.tidepool.demo:harmonics") {
version { strictly("[1.0, 1.2[") }
}
implementation("io.tidepool.demo:tides-api:1.1.0")
implementation("io.tidepool.demo:tides-chart:1.0.0")
}
dependencies {
implementation('io.tidepool.demo:harmonics') {
version { strictly '[1.0, 1.2[' }
}
implementation 'io.tidepool.demo:tides-api:1.1.0'
implementation 'io.tidepool.demo:tides-chart:1.0.0'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:harmonics:{strictly [1.0, 1.2[} -> 1.1.0
+--- io.tidepool.demo:tides-api:1.1.0
| \--- io.tidepool.demo:harmonics:1.1.0
\--- io.tidepool.demo:tides-chart:1.0.0
\--- io.tidepool.demo:harmonics:1.2.0 -> 1.1.0
The Maven way to downgrade a transitive version is a dependencyManagement entry (see
Dependency Management & BOMs); Gradle
makes the statement on the dependency itself, and the report shows it as {strictly [1.0, 1.2[} followed by the selected
version.
strictly with prefer: the preferred version wins inside the range
Adding prefer("1.0.0") to the same range changes the result. Nobody requires 1.0.0, and tides-api requires 1.1.0,
which is inside the range, so one could expect 1.1.0. The result is 1.0.0:
dependencies {
implementation("io.tidepool.demo:harmonics") {
version {
strictly("[1.0, 1.2[")
prefer("1.0.0")
}
}
implementation("io.tidepool.demo:tides-api:1.1.0")
implementation("io.tidepool.demo:tides-chart:1.0.0")
}
dependencies {
implementation('io.tidepool.demo:harmonics') {
version {
strictly '[1.0, 1.2['
prefer '1.0.0'
}
}
implementation 'io.tidepool.demo:tides-api:1.1.0'
implementation 'io.tidepool.demo:tides-chart:1.0.0'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:harmonics:{strictly [1.0, 1.2[; prefer 1.0.0} -> 1.0.0
+--- io.tidepool.demo:tides-api:1.1.0
| \--- io.tidepool.demo:harmonics:1.1.0 -> 1.0.0
\--- io.tidepool.demo:tides-chart:1.0.0
\--- io.tidepool.demo:harmonics:1.2.0 -> 1.0.0
Requirements of transitive dependencies are overridden by a direct strictly, including the ones that are inside its range.
Once they are out of the way, nothing but the prefer is left to choose between 1.0.0 and 1.1.0, and prefer picks 1.0.0.
Pairing strictly with prefer therefore says "inside this range, use exactly this unless a direct requirement or a
constraint asks for something else". It is the right shape for a library that wants to give its consumers room to move
(strictly("[1.0, 2.0[") and prefer("1.1.0")), as the publishing example below shows; for an application that only wants a
ceiling, a strictly range without prefer, as in the previous example, selects the highest version in the range.
The !! shorthand
Both strictly and prefer can be written in the string notation: 1.7.15!! is strictly("1.7.15"), and
[1.7, 1.8[!!1.7.25 is strictly("[1.7, 1.8[") with prefer("1.7.25"). A project with the shorthand for the range above
resolves exactly as the one with the version { } block:
dependencies {
implementation("io.tidepool.demo:harmonics:[1.0, 1.2[!!1.0.0")
implementation("io.tidepool.demo:tides-api:1.1.0")
}
dependencies {
implementation 'io.tidepool.demo:harmonics:[1.0, 1.2[!!1.0.0'
implementation 'io.tidepool.demo:tides-api:1.1.0'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:harmonics:{strictly [1.0, 1.2[; prefer 1.0.0} -> 1.0.0
\--- io.tidepool.demo:tides-api:1.1.0
\--- io.tidepool.demo:harmonics:1.1.0 -> 1.0.0
reject: remove a known-broken version
reject takes versions out of the candidate set without capping the range. Here 1.2.0 is declared bad. The range
[1.0, 1.3[ would select 1.2.0 (the upper bound is exclusive and acts as a prefix exclude, so 1.3.0 is out); with the
rejection it falls back to 1.1.0:
dependencies {
implementation("io.tidepool.demo:harmonics") {
version {
require("[1.0, 1.3[")
reject("1.2.0")
}
}
}
dependencies {
implementation('io.tidepool.demo:harmonics') {
version {
require '[1.0, 1.3['
reject '1.2.0'
}
}
}
runtimeClasspath - Runtime classpath of source set 'main'.
\--- io.tidepool.demo:harmonics:{require [1.0, 1.3[; reject 1.2.0} -> 1.1.0
dependencyInsight records why. The reason is rejected version 1.2.0:
./gradlew :rich-reject:dependencyInsight --dependency harmonics --configuration runtimeClasspath
io.tidepool.demo:harmonics:1.1.0
Variant runtime:
...
Selection reasons:
- Was requested: rejected version 1.2.0
io.tidepool.demo:harmonics:{require [1.0, 1.3[; reject 1.2.0} -> 1.1.0
\--- runtimeClasspath
reject accepts several versions, and if nothing is left the build fails instead of silently choosing something. With
require("[1.0, 1.3[") and all of 1.0.0, 1.1.0 and 1.2.0 rejected, the failure names the declaration and the versions
it ruled out (the listing is from a small helper task, resolve, that only resolves the runtime classpath; the Groovy DSL
output is identical except that the first line says build.gradle instead of build.gradle.kts):
dependencies {
implementation("io.tidepool.demo:harmonics") {
version {
require("[1.0, 1.3[")
reject("1.0.0", "1.1.0", "1.2.0")
}
}
}
tasks.register("resolve") {
inputs.files(configurations.runtimeClasspath)
doLast { println("resolved") }
}
dependencies {
implementation('io.tidepool.demo:harmonics') {
version {
require '[1.0, 1.3['
reject '1.0.0', '1.1.0', '1.2.0'
}
}
}
tasks.register('resolve') {
inputs.files(configurations.runtimeClasspath)
doLast { println 'resolved' }
}
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':reject-all:resolve' (registered in build file 'reject-all/build.gradle.kts').
> Could not resolve all files for configuration ':reject-all:runtimeClasspath'.
> Could not resolve io.tidepool.demo:harmonics:{require [1.0, 1.3[; reject 1.0.0 & 1.1.0 & 1.2.0}.
Required by:
project ':reject-all'
> Cannot find a version of 'io.tidepool.demo:harmonics' that satisfies the version constraints:
Dependency path: project ':reject-all' (runtimeClasspath) --> 'io.tidepool.demo:harmonics:{require [1.0, 1.3[; reject 1.0.0 & 1.1.0 & 1.2.0}' because of the following reason: rejected versions 1.2.0, 1.1.0, 1.0.0
This one was run without the configuration cache, because with it the failure shows up when Gradle writes the cache entry.
Competing strictly declarations
Two strict statements about the same module are not combined arbitrarily. The manual distinguishes two cases, and both were run.
At the same level they intersect. A direct dependency and a constraint of the same project count as the same level. If the two ranges overlap, the selection is the highest version inside the overlap; if they do not, the build fails and names both:
dependencies {
implementation("io.tidepool.demo:harmonics") {
version { strictly("[1.0, 1.2[") }
}
constraints {
implementation("io.tidepool.demo:harmonics") {
version { strictly("[1.2, 2.0[") }
}
}
}
tasks.register("resolve") {
inputs.files(configurations.runtimeClasspath)
doLast { println("resolved") }
}
dependencies {
implementation('io.tidepool.demo:harmonics') {
version { strictly '[1.0, 1.2[' }
}
constraints {
implementation('io.tidepool.demo:harmonics') {
version { strictly '[1.2, 2.0[' }
}
}
}
tasks.register('resolve') {
inputs.files(configurations.runtimeClasspath)
doLast { println 'resolved' }
}
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':strict-conflict:resolve' (registered in build file 'strict-conflict/build.gradle.kts').
> Could not resolve all files for configuration ':strict-conflict:runtimeClasspath'.
> Could not resolve io.tidepool.demo:harmonics:{strictly [1.0, 1.2[}.
Required by:
project ':strict-conflict'
> Cannot find a version of 'io.tidepool.demo:harmonics' that satisfies the version constraints:
Dependency path: project ':strict-conflict' (runtimeClasspath) --> 'io.tidepool.demo:harmonics:{strictly [1.0, 1.2[}' because of the following reason: rejected versions 1.1.0, 1.0.0
Constraint path: project ':strict-conflict' (runtimeClasspath) --> 'io.tidepool.demo:harmonics:{strictly [1.2, 2.0[}' because of the following reason: rejected versions 1.3.0, 1.2.0
> There is 1 more failure with an identical cause.
At different depths the root-most one wins outright. A strict statement from a dependency’s own constraints is dropped,
not intersected, when the root project says something strict about the same module. Here the root asks for
[1.0, 2.0[ and the library project lvl-a constrains the module to [1.1, 1.3[. If the two were intersected the answer
would be inside the narrower range, 1.2.0; the root’s range alone is used, and the highest version in it is 1.3.0:
dependencies {
constraints {
api("io.tidepool.demo:harmonics") {
version { strictly("[1.1, 1.3[") }
}
}
}
dependencies {
constraints {
api('io.tidepool.demo:harmonics') {
version { strictly '[1.1, 1.3[' }
}
}
}
dependencies {
implementation(project(":lvl-a"))
implementation("io.tidepool.demo:harmonics") {
version { strictly("[1.0, 2.0[") }
}
}
dependencies {
implementation project(':lvl-a')
implementation('io.tidepool.demo:harmonics') {
version { strictly '[1.0, 2.0[' }
}
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':lvl-a'
| \--- io.tidepool.demo:harmonics:{strictly [1.1, 1.3[} -> 1.3.0 (c)
\--- io.tidepool.demo:harmonics:{strictly [1.0, 2.0[} -> 1.3.0
This protects the right of a build to decide about its direct dependencies without negotiating with whatever it depends on.
The one thing above it is the build’s own resolutionStrategy: force and eachDependency { useVersion(...) } replace the
result even when it contradicts a strictly (see
Resolution Rules & Substitution).
Dynamic versions and changing modules
A dynamic version is a range or a latest-status placeholder where an exact version would be: 1.+, [1.1,),
latest.release. Gradle asks the repository for the list of versions and picks one at resolution time, so the same build file
can resolve differently tomorrow. The helper below creates one resolvable configuration per notation and prints what each one
selected, against the five versions of harmonics (1.0.0, 1.1.0, 1.2.0, 1.3.0, 2.0.0). It is compatible with the
configuration cache (the second run reused the entry) because it only captures providers:
plugins { `java-library` }
val notations = listOf("1.+", "1.1.+", "[1.0, 1.3[", "(1.0, 1.3]", "(1.0.0, 1.3.0]", "[1.1,)", "latest.release", "latest.integration", "+")
val probes = notations.mapIndexed { i, notation ->
val bucket = configurations.dependencyScope("probe$i")
dependencies.add(bucket.name, "io.tidepool.demo:harmonics:$notation")
val classpath = configurations.resolvable("probe${i}Classpath") { extendsFrom(bucket.get()) }
notation to classpath.flatMap { it.incoming.resolutionResult.rootComponent }.map { root ->
root.dependencies.filterIsInstance<ResolvedDependencyResult>()
.joinToString { it.selected.moduleVersion!!.version }
}
}
tasks.register("probeVersions") {
val rows = probes
doLast {
rows.forEach { (notation, version) -> println("%-20s -> %s".format(notation, version.get())) }
}
}
plugins { id 'java-library' }
def notations = ['1.+', '1.1.+', '[1.0, 1.3[', '(1.0, 1.3]', '(1.0.0, 1.3.0]', '[1.1,)', 'latest.release', 'latest.integration', '+']
def probes = notations.indexed().collect { i, notation ->
def bucket = configurations.dependencyScope("probe$i")
dependencies.add(bucket.name, "io.tidepool.demo:harmonics:$notation")
def classpath = configurations.resolvable("probe${i}Classpath") { extendsFrom(bucket.get()) }
[notation, classpath.flatMap { it.incoming.resolutionResult.rootComponent }.map { root ->
root.dependencies.findAll { it instanceof ResolvedDependencyResult }
.collect { it.selected.moduleVersion.version }.join(', ')
}]
}
tasks.register('probeVersions') {
def rows = probes
doLast {
rows.each { notation, version -> println String.format('%-20s -> %s', notation, version.get()) }
}
}
./gradlew :dynamic:probeVersions --configuration-cache
1.+ -> 1.3.0
1.1.+ -> 1.1.0
[1.0, 1.3[ -> 1.2.0
(1.0, 1.3] -> 1.2.0
(1.0.0, 1.3.0] -> 1.3.0
[1.1,) -> 2.0.0
latest.release -> 2.0.0
latest.integration -> 2.0.0
+ -> 2.0.0
Two rows deserve a second look. (1.0, 1.3] includes its upper bound, but the bound is the version 1.3, which is older
than 1.3.0; the same range written (1.0.0, 1.3.0] selects 1.3.0. And latest.release and latest.integration select
2.0.0 here because the repository metadata gives every version the status release; latest.integration would also select
snapshots, if there were any. The Groovy DSL run printed the same table.
Dynamic versions are cached. A resolved version list is kept in the dependency cache for 24 hours by default, so a new
release is not seen at once. For a file repository Gradle does not cache anything, so the demonstration needs a remote
one: a throwaway HTTP server on localhost (Python’s http.server, started for the run and stopped after it) serving a
module tides-live with versions 1.0.0 and 1.1.0, resolved with 1.+. After the first run a
version 1.2.0 was added to the server. The second run still resolved 1.1.0, from the cached version list. The third run used
cacheDynamicVersionsFor(0, "seconds") (switched on with -PnoCache), so Gradle asked again and found 1.2.0. The three runs shared one fresh Gradle user home, so the cache started empty:
dependencies {
implementation("io.tidepool.live:tides-live:1.+")
}
if (providers.gradleProperty("noCache").isPresent) {
configurations.configureEach {
resolutionStrategy.cacheDynamicVersionsFor(0, "seconds")
}
}
dependencies {
implementation 'io.tidepool.live:tides-live:1.+'
}
if (providers.gradleProperty('noCache').present) {
configurations.configureEach {
resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
}
}
$ ./gradlew dependencies --configuration runtimeClasspath
\--- io.tidepool.live:tides-live:1.+ -> 1.1.0
$ (1.2.0 is published) ./gradlew dependencies --configuration runtimeClasspath
\--- io.tidepool.live:tides-live:1.+ -> 1.1.0
$ ./gradlew dependencies --configuration runtimeClasspath -PnoCache
\--- io.tidepool.live:tides-live:1.+ -> 1.2.0
Both DSLs gave the same three results. The repository was declared with isAllowInsecureProtocol = true (allowInsecureProtocol
in the Groovy DSL), which is acceptable for localhost and for nothing else. A changing module is the other half:
a version that keeps its coordinates while its content changes, such as a Maven SNAPSHOT or any dependency declared with
isChanging = true; its cache is controlled by cacheChangingModulesFor. isChanging and cacheChangingModulesFor were not run
here. The
way to get reproducible builds from dynamic versions is dependency locking, on
Dependency Locking, Verification & Caching.
Source: Gradle User Manual: Declaring changing versions and Gradle User Manual: Controlling dynamic version caching
The same idea in the other tools: Maven’s ranges and LATEST/RELEASE (and the SNAPSHOT update policy) are on
Versions, SNAPSHOTs & Ranges, and Ivy’s dynamic revisions
(latest.integration, 1.+, [1.0,2.0]) on
Ivy: Conflicts & Versions.
Dependency constraints
A dependency constraint sets a version requirement for a module without adding the module to the graph. If something else
brings the module in, the constraint takes part in conflict resolution like a declared version; if nothing does, it is
ignored. That is what makes it the right tool for transitive dependencies: a security floor, a "tested with" version, a way to
keep the version declaration out of the places that use the module. Constraints are declared in a constraints { } block
inside dependencies { }, are scoped to a configuration like any dependency, and can carry a because message.
Raising a transitive version
tides-api:1.0.0 pulls in harmonics:1.0.0. The project has no dependency on harmonics of its own and wants at least
1.2.0 whenever it appears. A second constraint is on a module that nothing in the graph uses:
dependencies {
implementation("io.tidepool.demo:tides-api:1.0.0")
constraints {
implementation("io.tidepool.demo:harmonics:1.2.0") {
because("1.2.0 fixes the tidal-range rounding bug")
}
implementation("io.tidepool.demo:tides-chart:1.0.0")
}
}
dependencies {
implementation 'io.tidepool.demo:tides-api:1.0.0'
constraints {
implementation('io.tidepool.demo:harmonics:1.2.0') {
because '1.2.0 fixes the tidal-range rounding bug'
}
implementation 'io.tidepool.demo:tides-chart:1.0.0'
}
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:tides-api:1.0.0
| \--- io.tidepool.demo:harmonics:1.0.0 -> 1.2.0
\--- io.tidepool.demo:harmonics:1.2.0 (c)
harmonics is in the graph because tides-api needs it, and the constraint raised it to 1.2.0 (the (c) marks the
constraint). tides-chart does not appear at all: the constraint on it is a no-op because nothing depends on it. The reason
Gradle selected 1.2.0 is recorded, with the text of because:
./gradlew :constraints:dependencyInsight --dependency harmonics --configuration runtimeClasspath
io.tidepool.demo:harmonics:1.2.0
Variant runtime:
...
Selection reasons:
- By constraint: 1.2.0 fixes the tidal-range rounding bug
- By conflict resolution: between versions 1.2.0 and 1.0.0
io.tidepool.demo:harmonics:1.2.0
\--- runtimeClasspath
io.tidepool.demo:harmonics:1.0.0 -> 1.2.0
\--- io.tidepool.demo:tides-api:1.0.0
\--- runtimeClasspath
A bare version in a constraint does not cap anything
A constraint with a plain version is a require. It can raise a version, never lower one, and it does not stop a dependency
from asking for more. tides-chart needs 1.2.0; the constraint says 1.1.0; the result is 1.2.0:
dependencies {
implementation("io.tidepool.demo:tides-chart:1.0.0")
constraints {
implementation("io.tidepool.demo:harmonics:1.1.0")
}
}
dependencies {
implementation 'io.tidepool.demo:tides-chart:1.0.0'
constraints {
implementation 'io.tidepool.demo:harmonics:1.1.0'
}
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:tides-chart:1.0.0
| \--- io.tidepool.demo:harmonics:1.2.0
\--- io.tidepool.demo:harmonics:1.1.0 -> 1.2.0 (c)
To cap a version, make the constraint strict. The range [1.0, 1.2[ with a preferred 1.1.0 cuts 1.2.0 out of the graph, and
tides-chart is held back to 1.1.0 together with the declared reason:
dependencies {
implementation("io.tidepool.demo:tides-chart:1.0.0")
constraints {
implementation("io.tidepool.demo:harmonics") {
version {
strictly("[1.0, 1.2[")
prefer("1.1.0")
}
because("1.2 changed the Constituent API")
}
}
}
dependencies {
implementation 'io.tidepool.demo:tides-chart:1.0.0'
constraints {
implementation('io.tidepool.demo:harmonics') {
version {
strictly '[1.0, 1.2['
prefer '1.1.0'
}
because '1.2 changed the Constituent API'
}
}
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:tides-chart:1.0.0
| \--- io.tidepool.demo:harmonics:1.2.0 -> 1.1.0
\--- io.tidepool.demo:harmonics:{strictly [1.0, 1.2[; prefer 1.1.0} -> 1.1.0 (c)
./gradlew :strict-capped:dependencyInsight --dependency harmonics --configuration runtimeClasspath
io.tidepool.demo:harmonics:1.1.0
Variant runtime:
...
Selection reasons:
- By ancestor
- By constraint: 1.2 changed the Constituent API
io.tidepool.demo:harmonics:1.2.0 -> 1.1.0
\--- io.tidepool.demo:tides-chart:1.0.0
\--- runtimeClasspath
io.tidepool.demo:harmonics:{strictly [1.0, 1.2[; prefer 1.1.0} -> 1.1.0
\--- runtimeClasspath
The Selection reasons show two things: the constraint with its because text, and By ancestor, which Gradle uses when a
strict version declared elsewhere in the graph decided the selection. This is the pattern for pinning a transitive dependency
without adding it as a direct one (a security fix, for instance): strictly("[2.17.1, 2.18[") inside constraints { }.
Constraints are transitive, and they are published
A constraint declared by a library is part of what it publishes, and it applies to everyone who uses the library. The
tides-kit library built in Rich versions and constraints in published metadata declares a constraint tides-chart:1.0.0. A consumer that depends on
tides-kit and on tides-chart without a version gets 1.0.0, from the library’s constraint:
dependencies {
implementation("io.tidepool:tides-kit:1.0.0")
implementation("io.tidepool.demo:harmonics:1.3.0")
implementation("io.tidepool.demo:tides-chart")
}
dependencies {
implementation 'io.tidepool:tides-kit:1.0.0'
implementation 'io.tidepool.demo:harmonics:1.3.0'
implementation 'io.tidepool.demo:tides-chart'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool:tides-kit:1.0.0
| +--- io.tidepool.demo:tides-api:{require 1.0.0; reject 1.0.1} -> 1.0.0
| | \--- io.tidepool.demo:harmonics:1.0.0 -> 1.3.0
| +--- io.tidepool.demo:harmonics:{strictly [1.0, 2.0[; prefer 1.1.0} -> 1.3.0
| \--- io.tidepool.demo:tides-chart:1.0.0 (c)
+--- io.tidepool.demo:harmonics:1.3.0
\--- io.tidepool.demo:tides-chart -> 1.0.0
\--- io.tidepool.demo:harmonics:1.2.0 -> 1.3.0
The library’s constraint is the tides-chart:1.0.0 (c) line under tides-kit. The manual says constraints are only
guaranteed with Gradle Module Metadata and that Maven or Ivy consumers may not see them; in this run the constraint also
survived into the POM, as a dependencyManagement entry (see the listings in Rich versions and constraints in published metadata), while the rich versions did
not.
Importing a BOM with platform()
Libraries that ship as a family publish a bill of materials: a POM whose dependencyManagement lists the versions of all
members (the tool-neutral picture is in
Dependency Management Fundamentals, the
Maven side in Dependency Management & BOMs).
Gradle imports one with the platform(...) keyword. Gradle derives a platform variant from every Maven POM it
consumes, so any POM with a dependencyManagement section can be imported this way, and every managed entry becomes a
constraint, in exactly the sense of the previous section.
A real BOM: JUnit
The JUnit team publishes org.junit:junit-bom. With it, the test dependencies need no versions:
dependencies {
testImplementation(platform("org.junit:junit-bom:6.1.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
dependencies {
testImplementation platform('org.junit:junit-bom:6.1.3')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
./gradlew :pub-junit: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
| | +--- org.junit:junit-bom:6.1.3 (*)
(The listing is cut after its first lines; the rest of the tree continues in the same way.) The seven BOM entries are the
constraints, marked (c); the two declared dependencies got their versions from them; and every JUnit module in the tree
also points back at the BOM, because the JUnit modules themselves declare a dependency on it.
The platform keyword also sets the org.gradle.category attribute to platform, so that Gradle selects the platform
variant of the BOM and not a library, and it switches on endorseStrictVersions (see Strict versions in a platform are endorsed).
platform: a recommendation that can be outvoted
A platform only recommends. bom-platform imports tidepool-bom (which manages harmonics:1.2.0 and tides-api:1.1.0) and
also declares harmonics:1.3.0 itself. tides-api is declared without a version and takes the BOM’s:
dependencies {
implementation(platform("io.tidepool.demo:tidepool-bom:1.0.0"))
implementation("io.tidepool.demo:harmonics:1.3.0")
implementation("io.tidepool.demo:tides-api")
}
dependencies {
implementation platform('io.tidepool.demo:tidepool-bom:1.0.0')
implementation 'io.tidepool.demo:harmonics:1.3.0'
implementation 'io.tidepool.demo:tides-api'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:tidepool-bom:1.0.0
| +--- io.tidepool.demo:harmonics:1.2.0 -> 1.3.0 (c)
| \--- io.tidepool.demo:tides-api:1.1.0 (c)
+--- io.tidepool.demo:harmonics:1.3.0
\--- io.tidepool.demo:tides-api -> 1.1.0
\--- io.tidepool.demo:harmonics:1.1.0 -> 1.3.0
The direct 1.3.0 is higher than the BOM’s 1.2.0, so it wins: the report shows the BOM’s own entry as 1.2.0 -> 1.3.0, and the
harmonics:1.1.0 that tides-api needs is raised to 1.3.0 as well.
enforcedPlatform: the BOM wins, even by downgrading
enforcedPlatform converts every BOM entry into a strictly. Nobody can raise a version above the BOM’s, and a direct
declaration is overridden. The same project with enforcedPlatform:
dependencies {
implementation(enforcedPlatform("io.tidepool.demo:tidepool-bom:1.0.0"))
implementation("io.tidepool.demo:harmonics:1.3.0")
implementation("io.tidepool.demo:tides-api")
}
dependencies {
implementation enforcedPlatform('io.tidepool.demo:tidepool-bom:1.0.0')
implementation 'io.tidepool.demo:harmonics:1.3.0'
implementation 'io.tidepool.demo:tides-api'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:tidepool-bom:{strictly 1.0.0} -> 1.0.0
| +--- io.tidepool.demo:harmonics:1.2.0 (c)
| \--- io.tidepool.demo:tides-api:1.1.0 (c)
+--- io.tidepool.demo:harmonics:1.3.0 -> 1.2.0
\--- io.tidepool.demo:tides-api -> 1.1.0
\--- io.tidepool.demo:harmonics:1.1.0 -> 1.2.0
The direct 1.3.0 is gone, 1.2.0 is what the BOM says, and the BOM itself is pinned: it appears as
tidepool-bom:{strictly 1.0.0}. In a build’s own dependencies report, that annotation is how to recognise an enforced
platform.
Why enforcedPlatform is discouraged for libraries. The strictness is part of what the build declares, and a library
publishes it. A consumer of such a library can not simply ask for a different version of one of the pinned modules: it has to
beat the strict statement from higher in the graph with its own strictly, or add a component metadata rule that calls
doNotEndorseStrictVersions() on the transitive platform. Use enforcedPlatform in applications that own their whole graph;
in a library, use a plain platform, or put strictly ranges (with a prefer) on the few modules that need one. The manual
adds a rule of thumb: enforcedPlatform to pin all the versions of a BOM as a group, strictly on individual dependencies when
one or two modules need control, and it warns that mixing a platform with a strictly on one of its modules lets the
strictly drive the result for that module and its own transitive dependencies, leaving the BOM’s other members where the BOM
put them.
A BOM and a version catalog together
A version catalog names the coordinates; a platform decides the versions of what they refer to. Both are done in one place
by declaring the BOM in libs.versions.toml and wrapping its accessor in platform(...); a catalog entry can also carry a
rich version. The catalog of the scratch build (shown as text, because the code highlighter has no TOML grammar):
[versions]
tidepool-bom = "1.0.0"
[libraries]
tidepool-bom = { module = "io.tidepool.demo:tidepool-bom", version.ref = "tidepool-bom" }
harmonics = { module = "io.tidepool.demo:harmonics", version = { strictly = "[1.0, 1.2[", prefer = "1.1.0" } }
tides-api = { module = "io.tidepool.demo:tides-api" }
dependencies {
implementation(platform(libs.tidepool.bom))
implementation(libs.harmonics)
implementation(libs.tides.api)
}
dependencies {
implementation platform(libs.tidepool.bom)
implementation libs.harmonics
implementation libs.tides.api
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:tidepool-bom:1.0.0
| +--- io.tidepool.demo:harmonics:1.2.0 -> 1.1.0 (c)
| \--- io.tidepool.demo:tides-api:1.1.0 (c)
+--- io.tidepool.demo:harmonics:{strictly [1.0, 1.2[; prefer 1.1.0} -> 1.1.0
\--- io.tidepool.demo:tides-api -> 1.1.0
\--- io.tidepool.demo:harmonics:1.1.0
The strict range on harmonics beat the BOM’s 1.2.0 (the BOM’s entry shows as -> 1.1.0), while tides-api got the BOM’s
1.1.0. See Version Catalogs for the catalog syntax, and the
manual’s "Using Catalogs with Platforms" for when to prefer each.
Building a platform with the java-platform plugin
A Gradle platform is a component with no sources, made only of constraints (and, if you ask for them, dependencies), that
other builds consume with platform(...). The java-platform plugin builds one. It describes modules published together
(the same version), a set of recommended versions for heterogeneous libraries (the Spring Boot BOM is the usual example) or
the versions shared by the subprojects of one build. The plugin can not be combined with java or java-library in the
same project: a project is a platform, with no binaries, or it produces binaries.
The configurations it adds follow the shape of the Java ones.
| Configuration | Role | Purpose |
|---|---|---|
|
Declarable |
Constraints (and dependencies) that apply when compiling against the platform. They are exported to consumers. |
|
Declarable |
Constraints that are visible only at runtime. |
|
Consumable |
What a consumer gets with |
|
Consumable |
The same constraints exposed as forced, for consumers that use |
A platform holds constraints, not dependencies, on purpose: a constraint only applies when something else brings the module
in, so the platform never adds a library to a build that does not use it. To stop the common mistake of writing a
dependency where a constraint was meant, the plugin refuses dependencies unless the build opts in. This project declares one
(api("...") instead of constraints { api("...") }) and was included in the run only on request, because the failure
happens while it is configured:
dependencies {
api("io.tidepool.demo:harmonics:1.0.0")
}
dependencies {
api 'io.tidepool.demo:harmonics:1.0.0'
}
./gradlew -PbadPlatform :platform-bad:dependencies
FAILURE: Build failed with an exception.
* What went wrong:
A problem occurred configuring project ':platform-bad'.
> Adding dependencies to platforms is not allowed by default.
Most likely you want to add constraints instead.
If you did this intentionally, you need to configure the platform extension to allow dependencies:
javaPlatform.allowDependencies()
Found dependencies in the 'api' configuration.
The opt-in is javaPlatform { allowDependencies() }. It is also what makes it possible to build a platform from another
one, by importing it with api(platform("g:a:v")), which is how a company BOM extends a third-party one. This variant was
not run here.
The tidepool platform
The scenario of the other pages already has two libraries, tidepool-model and tidepool-engine. A platform project
tidepool-platform lists the versions of both and of commons-csv, which the engine uses to read the stations file. The
libraries depend on the platform, so that every published library points at the platform of its own version. The root build
sets the group and the version (from -PpubVersion) and gives every subproject the same file repository, declared once; the
libraries and the platform differ only in the plugin and the component published.
allprojects {
group = "io.tidepool"
version = providers.gradleProperty("pubVersion").getOrElse("1.0.0")
}
subprojects {
apply(plugin = "maven-publish")
configure<PublishingExtension> {
repositories {
maven {
name = "local"
url = uri(rootProject.layout.buildDirectory.dir("repo"))
}
}
}
}
allprojects {
group = 'io.tidepool'
version = providers.gradleProperty('pubVersion').getOrElse('1.0.0')
}
subprojects {
apply plugin: 'maven-publish'
publishing {
repositories {
maven {
name = 'local'
url = rootProject.layout.buildDirectory.dir('repo')
}
}
}
}
plugins { `java-platform` }
dependencies {
constraints {
api(project(":tidepool-model"))
api(project(":tidepool-engine"))
api("org.apache.commons:commons-csv:1.14.1")
}
}
publishing {
publications {
create<MavenPublication>("platform") {
from(components["javaPlatform"])
}
}
}
plugins { id 'java-platform' }
dependencies {
constraints {
api project(':tidepool-model')
api project(':tidepool-engine')
api 'org.apache.commons:commons-csv:1.14.1'
}
}
publishing {
publications {
platform(MavenPublication) {
from components.javaPlatform
}
}
}
plugins { `java-library` }
dependencies {
api(platform(project(":tidepool-platform")))
}
publishing {
publications {
create<MavenPublication>("lib") {
from(components["java"])
}
}
}
plugins { id 'java-library' }
dependencies {
api platform(project(':tidepool-platform'))
}
publishing {
publications {
lib(MavenPublication) {
from components.java
}
}
}
plugins { `java-library` }
dependencies {
api(platform(project(":tidepool-platform")))
api(project(":tidepool-model"))
implementation("org.apache.commons:commons-csv")
}
publishing {
publications {
create<MavenPublication>("lib") {
from(components["java"])
}
}
}
plugins { id 'java-library' }
dependencies {
api platform(project(':tidepool-platform'))
api project(':tidepool-model')
implementation 'org.apache.commons:commons-csv'
}
publishing {
publications {
lib(MavenPublication) {
from components.java
}
}
}
The constraints name the two projects, and Gradle turns the project notation into plain group:name:version coordinates in
the published metadata. The platform is published like any component, from the component named javaPlatform instead of
java. Publishing needs only maven-publish; the repository here is a directory under the build, as everywhere in this
section (publishing to a real repository is on
Publishing and is not run here). The commands, run once for
each version:
./gradlew publishAllPublicationsToLocalRepository --configuration-cache
./gradlew publishAllPublicationsToLocalRepository -PpubVersion=1.1.0
The first run stored a configuration cache entry and a second identical run reused it, so the build is compatible.
The platform’s POM is a BOM with a dependencyManagement block whose entries are the constraints:
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-platform</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Its .module file (not shown) has an apiElements and a runtimeElements variant with the attribute
org.gradle.category: platform and the same three constraints in dependencyConstraints. The engine’s POM shows the other
end of the arrangement: the platform that the engine declared with api(platform(...)) is imported as a BOM, and
commons-csv has no version because the platform supplies it:
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
<version>1.0.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-platform</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
A consumer imports the published platform and declares the two libraries and commons-csv without versions:
dependencies {
implementation(platform("io.tidepool:tidepool-platform:1.1.0"))
implementation("io.tidepool:tidepool-engine")
implementation("org.apache.commons:commons-csv")
}
dependencies {
implementation platform('io.tidepool:tidepool-platform:1.1.0')
implementation 'io.tidepool:tidepool-engine'
implementation 'org.apache.commons:commons-csv'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool:tidepool-platform:1.1.0
| +--- io.tidepool:tidepool-engine:1.1.0 (c)
| +--- org.apache.commons:commons-csv:1.14.1 (c)
| \--- io.tidepool:tidepool-model:1.1.0 (c)
+--- io.tidepool:tidepool-engine -> 1.1.0
| +--- org.apache.commons:commons-csv -> 1.14.1
| | +--- commons-io:commons-io:2.20.0
| | \--- commons-codec:commons-codec:1.19.0
| +--- io.tidepool:tidepool-platform:1.1.0 (*)
| \--- io.tidepool:tidepool-model:1.1.0
| \--- io.tidepool:tidepool-platform:1.1.0 (*)
\--- org.apache.commons:commons-csv -> 1.14.1 (*)
The platform is the first line, with its three constraints ((c)); the engine and commons-csv took their versions from
it; and tidepool-model, which the engine needs, is at the platform’s version too. The Groovy DSL output of this run, and of
the other reports on this page, was identical.
The same structure inside one build, with the platform as a subproject and no publishing, is how a multi-project build shares
its dependency versions without a version catalog or without allprojects configuration (see
Multi Project Builds). The manual’s guidance
on choosing between them is in "Using Catalogs with Platforms": a catalog gives type-safe accessors and consistent
coordinates but does not affect resolution; a platform does.
Strict versions in a platform are endorsed
A platform can contain strictly constraints. When a consumer imports it with platform(...), those strict versions are
treated as if the consumer had declared them: platform() switches on endorseStrictVersions by default. strict-platform pins
harmonics to [1.0, 1.2[ with a preferred 1.1.0, and the consumer also depends on tides-chart (which wants 1.2.0):
plugins { `java-platform` }
dependencies {
constraints {
api("io.tidepool.demo:harmonics") {
version {
strictly("[1.0, 1.2[")
prefer("1.1.0")
}
because("the platform pins the tested harmonics line")
}
}
}
plugins { id 'java-platform' }
dependencies {
constraints {
api('io.tidepool.demo:harmonics') {
version {
strictly '[1.0, 1.2['
prefer '1.1.0'
}
because 'the platform pins the tested harmonics line'
}
}
}
dependencies {
implementation(platform(project(":strict-platform")))
implementation("io.tidepool.demo:tides-chart:1.0.0")
}
dependencies {
implementation platform(project(':strict-platform'))
implementation 'io.tidepool.demo:tides-chart:1.0.0'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':strict-platform'
| \--- io.tidepool.demo:harmonics:{strictly [1.0, 1.2[; prefer 1.1.0} -> 1.1.0 (c)
\--- io.tidepool.demo:tides-chart:1.0.0
\--- io.tidepool.demo:harmonics:1.2.0 -> 1.1.0
The strict range held tides-chart back to 1.1.0, as it would have if the consumer had written the range itself. A consumer
that does not want the platform’s strict statements applied to the rest of its graph calls doNotEndorseStrictVersions() on
the platform dependency. In the Kotlin DSL the function is not available on the lambda’s receiver of platform(...), which is a plain
Dependency: the manual’s snippet fails with Unresolved reference 'doNotEndorseStrictVersions', and a cast to
ModuleDependency fixes it. The Groovy DSL needs none:
dependencies {
implementation(platform(project(":strict-platform"))) {
(this as ModuleDependency).doNotEndorseStrictVersions()
}
implementation("io.tidepool.demo:tides-chart:1.0.0")
}
tasks.register("resolve") {
inputs.files(configurations.runtimeClasspath)
doLast { println("resolved") }
}
dependencies {
implementation(platform(project(':strict-platform'))) {
doNotEndorseStrictVersions()
}
implementation 'io.tidepool.demo:tides-chart:1.0.0'
}
tasks.register('resolve') {
inputs.files(configurations.runtimeClasspath)
doLast { println 'resolved' }
}
The result is not the relaxed graph one might expect. The platform’s strict constraint is still there, but it is no longer
promoted to the whole graph, and tides-chart's 1.2.0 is outside it, so resolution fails (the output below is from the
Kotlin DSL run; the Groovy one differs only in the build file name on the first line):
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':no-endorse:resolve' (registered in build file 'no-endorse/build.gradle.kts').
> Could not resolve all files for configuration ':no-endorse:runtimeClasspath'.
> Could not resolve io.tidepool.demo:harmonics:{strictly [1.0, 1.2[; prefer 1.1.0}.
Required by:
project ':no-endorse' > project ':strict-platform'
> Cannot find a version of 'io.tidepool.demo:harmonics' that satisfies the version constraints:
Dependency path: project ':no-endorse' (runtimeClasspath) --> io.tidepool.demo:tides-chart:1.0.0 (runtime) --> 'io.tidepool.demo:harmonics:1.2.0'
Constraint path: project ':no-endorse' (runtimeClasspath) --> project ':strict-platform' (runtimeElements) --> 'io.tidepool.demo:harmonics:{strictly [1.0, 1.2[; prefer 1.1.0}' because of the following reason: the platform pins the tested harmonics line
> There is 1 more failure with an identical cause.
Endorsement is therefore a property of the consumer’s relation to the platform, and disabling it turns a downgrade into a failure unless the dependency that asks for the out-of-range version is also changed.
Rich versions and constraints in published metadata
Whatever is written in a version { } block is published, in the format that can carry it. Gradle Module Metadata (the
.module file, published next to the POM since Gradle 6.0) keeps all four terms; the POM keeps the highest of strictly
and require (and a range as a Maven range) and drops prefer and reject. tides-kit is a library that gives its
consumers room (a strict range with a preferred version), rejects one known-bad version of an internal dependency and
constrains a third module:
plugins { `java-library` }
dependencies {
api("io.tidepool.demo:harmonics") {
version {
strictly("[1.0, 2.0[")
prefer("1.1.0")
}
because("1.x keeps the Constituent API")
}
implementation("io.tidepool.demo:tides-api") {
version {
require("1.0.0")
reject("1.0.1")
}
}
constraints {
implementation("io.tidepool.demo:tides-chart:1.0.0") {
because("1.0.0 is the first release that renders tide tables")
}
}
}
publishing {
publications {
create<MavenPublication>("lib") {
from(components["java"])
}
}
}
if (providers.gradleProperty("noGmm").isPresent) {
tasks.withType<GenerateModuleMetadata>().configureEach { enabled = false }
}
plugins { id 'java-library' }
dependencies {
api('io.tidepool.demo:harmonics') {
version {
strictly '[1.0, 2.0['
prefer '1.1.0'
}
because '1.x keeps the Constituent API'
}
implementation('io.tidepool.demo:tides-api') {
version {
require '1.0.0'
reject '1.0.1'
}
}
constraints {
implementation('io.tidepool.demo:tides-chart:1.0.0') {
because '1.0.0 is the first release that renders tide tables'
}
}
}
publishing {
publications {
lib(MavenPublication) {
from components.java
}
}
}
if (providers.gradleProperty('noGmm').present) {
tasks.withType(GenerateModuleMetadata).configureEach { enabled = false }
}
The .module file keeps every statement (the files array, with the JAR’s name, size and checksums, is shown as ...):
{
"formatVersion": "1.1",
"component": {
"group": "io.tidepool",
"module": "tides-kit",
"version": "1.0.0",
"attributes": {
"org.gradle.status": "release"
}
},
"createdBy": {
"gradle": {
"version": "9.8.1"
}
},
"variants": [
{
"name": "apiElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.jvm.version": 26,
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-api"
},
"dependencies": [
{
"group": "io.tidepool.demo",
"module": "harmonics",
"version": {
"strictly": "[1.0, 2.0[",
"requires": "[1.0, 2.0[",
"prefers": "1.1.0"
},
"reason": "1.x keeps the Constituent API"
}
],
"files": [ ... ]
},
{
"name": "runtimeElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.jvm.version": 26,
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-runtime"
},
"dependencies": [
{
"group": "io.tidepool.demo",
"module": "tides-api",
"version": {
"requires": "1.0.0",
"rejects": [
"1.0.1"
]
}
},
{
"group": "io.tidepool.demo",
"module": "harmonics",
"version": {
"strictly": "[1.0, 2.0[",
"requires": "[1.0, 2.0[",
"prefers": "1.1.0"
},
"reason": "1.x keeps the Constituent API"
}
],
"dependencyConstraints": [
{
"group": "io.tidepool.demo",
"module": "tides-chart",
"version": {
"requires": "1.0.0"
},
"reason": "1.0.0 is the first release that renders tide tables"
}
],
"files": [ ... ]
}
]
}
strictly, requires and prefers are all there for harmonics, rejects for tides-api, the reasons are carried over as
reason, and the constraint is a dependencyConstraints entry of the runtime variant. The POM, generated at the same time,
keeps less:
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tides-kit</artifactId>
<version>1.0.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool.demo</groupId>
<artifactId>tides-chart</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool.demo</groupId>
<artifactId>harmonics</artifactId>
<version>[1.0, 2.0)</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.tidepool.demo</groupId>
<artifactId>tides-api</artifactId>
<version>1.0.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
The range became [1.0, 2.0), the preferred version, the rejection and the reasons are gone, and the constraint became a
dependencyManagement entry. To see what a consumer gets without the .module file, the same library was published as
1.0.0-pomonly with tasks.withType<GenerateModuleMetadata> { enabled = false }. Two consumers depend on a 2.0.0 of
harmonics that the library does not support. With the Gradle metadata the build fails, and the message names the
library’s statement and its reason:
dependencies {
implementation("io.tidepool:tides-kit:1.0.0")
implementation("io.tidepool.demo:harmonics:2.0.0")
}
tasks.register("resolve") {
inputs.files(configurations.runtimeClasspath)
doLast { println("resolved") }
}
dependencies {
implementation 'io.tidepool:tides-kit:1.0.0'
implementation 'io.tidepool.demo:harmonics:2.0.0'
}
tasks.register('resolve') {
inputs.files(configurations.runtimeClasspath)
doLast { println 'resolved' }
}
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':pub-kit-fail:resolve' (registered in build file 'pub-kit-fail/build.gradle.kts').
> Could not resolve all files for configuration ':pub-kit-fail:runtimeClasspath'.
> Could not resolve io.tidepool.demo:harmonics:2.0.0.
Required by:
project ':pub-kit-fail'
> Cannot find a version of 'io.tidepool.demo:harmonics' that satisfies the version constraints:
Dependency path: project ':pub-kit-fail' (runtimeClasspath) --> 'io.tidepool.demo:harmonics:2.0.0'
Dependency path: project ':pub-kit-fail' (runtimeClasspath) --> io.tidepool:tides-kit:1.0.0 (runtimeElements) --> 'io.tidepool.demo:harmonics:{strictly [1.0, 2.0[; prefer 1.1.0}' because of the following reason: 1.x keeps the Constituent API
Dependency path: project ':pub-kit-fail' (runtimeClasspath) --> io.tidepool:tides-kit:1.0.0 (runtimeElements) --> io.tidepool.demo:tides-api:1.0.0 (runtime) --> 'io.tidepool.demo:harmonics:1.0.0'
> There are 2 more failures with identical causes.
With the POM only, the strict range is not there and the consumer silently gets 2.0.0:
dependencies {
implementation("io.tidepool:tides-kit:1.0.0-pomonly")
implementation("io.tidepool.demo:harmonics:2.0.0")
}
dependencies {
implementation 'io.tidepool:tides-kit:1.0.0-pomonly'
implementation 'io.tidepool.demo:harmonics:2.0.0'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool:tides-kit:1.0.0-pomonly
| +--- io.tidepool.demo:harmonics:[1.0, 2.0) -> 2.0.0
| \--- io.tidepool.demo:tides-api:1.0.0
| \--- io.tidepool.demo:harmonics:1.0.0 -> 2.0.0
\--- io.tidepool.demo:harmonics:2.0.0
That is the practical meaning of "converting this information to Maven or Ivy metadata is lossy": a Gradle consumer sees the
library’s whole intent, a Maven consumer sees a range. A consumer inside the range resolves normally: in the listing of the tides-kit consumer in "Constraints are transitive, and they are published" above, harmonics is 1.3.0. A library that needs
both audiences keeps strictly ranges wide and always pairs them with a prefer.
Aligning versions
Modules that are released together must be used together. Gradle resolves each module on its own, so a graph in which two
modules of one family are required at different versions can end up mixing them. In the scenario, suite-ui:1.1.0 needs
suite-core:1.1.0 and suite-api:1.0.0 needs suite-core:1.0.0. Core is raised to 1.1.0 (the highest wins) but suite-api
stays at 1.0.0, which was built against the older core:
dependencies {
implementation("io.tidepool.demo:suite-ui:1.1.0")
implementation("io.tidepool.demo:suite-api:1.0.0")
}
dependencies {
implementation 'io.tidepool.demo:suite-ui:1.1.0'
implementation 'io.tidepool.demo:suite-api:1.0.0'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:suite-ui:1.1.0
| \--- io.tidepool.demo:suite-core:1.1.0
\--- io.tidepool.demo:suite-api:1.0.0
\--- io.tidepool.demo:suite-core:1.0.0 -> 1.1.0
The manual offers three ways to make them agree. All three were run.
A published platform or BOM. If the family publishes one, import it. suite-bom:1.1.0 manages all three modules at 1.1.0,
and suite-api:1.0.0 is raised to match:
dependencies {
implementation(platform("io.tidepool.demo:suite-bom:1.1.0"))
implementation("io.tidepool.demo:suite-ui:1.1.0")
implementation("io.tidepool.demo:suite-api:1.0.0")
}
dependencies {
implementation platform('io.tidepool.demo:suite-bom:1.1.0')
implementation 'io.tidepool.demo:suite-ui:1.1.0'
implementation 'io.tidepool.demo:suite-api:1.0.0'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:suite-bom:1.1.0
| +--- io.tidepool.demo:suite-api:1.1.0 (c)
| +--- io.tidepool.demo:suite-ui:1.1.0 (c)
| \--- io.tidepool.demo:suite-core:1.1.0 (c)
+--- io.tidepool.demo:suite-ui:1.1.0
| \--- io.tidepool.demo:suite-core:1.1.0
\--- io.tidepool.demo:suite-api:1.0.0 -> 1.1.0
\--- io.tidepool.demo:suite-core:1.1.0
A virtual platform, when nobody published a BOM. A component metadata rule tells Gradle that the modules belong to the same
virtual platform, belongsTo(...), and Gradle aligns every module of that platform to one version, the highest. The rule is a
class with execute(ComponentMetadataContext), registered with components.all:
abstract class SuiteAlignmentRule : ComponentMetadataRule {
override fun execute(ctx: ComponentMetadataContext) {
ctx.details.run {
if (id.group == "io.tidepool.demo" && id.name.startsWith("suite-")) {
belongsTo("io.tidepool.demo:suite-virtual-platform:${id.version}")
}
}
}
}
dependencies {
implementation("io.tidepool.demo:suite-ui:1.1.0")
implementation("io.tidepool.demo:suite-api:1.0.0")
components.all<SuiteAlignmentRule>()
}
abstract class SuiteAlignmentRule implements ComponentMetadataRule {
void execute(ComponentMetadataContext ctx) {
def id = ctx.details.id
if (id.group == 'io.tidepool.demo' && id.name.startsWith('suite-')) {
ctx.details.belongsTo("io.tidepool.demo:suite-virtual-platform:${id.version}")
}
}
}
dependencies {
implementation 'io.tidepool.demo:suite-ui:1.1.0'
implementation 'io.tidepool.demo:suite-api:1.0.0'
components.all(SuiteAlignmentRule)
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool.demo:suite-ui:1.1.0
| \--- io.tidepool.demo:suite-core:1.1.0
\--- io.tidepool.demo:suite-api:1.0.0 -> 1.1.0
\--- io.tidepool.demo:suite-core:1.1.0
./gradlew :align-rule:dependencyInsight --dependency suite-api --configuration runtimeClasspath
io.tidepool.demo:suite-api:1.1.0
Variant runtime:
...
Selection reasons:
- By constraint: belongs to platform io.tidepool.demo:suite-virtual-platform:1.1.0
- By conflict resolution: between versions 1.1.0 and 1.0.0
io.tidepool.demo:suite-api:1.0.0 -> 1.1.0
\--- runtimeClasspath
The selection reason is the platform: By constraint: belongs to platform io.tidepool.demo:suite-virtual-platform:1.1.0. The
rule was run in both DSLs, with a difference worth knowing. The manual’s Groovy form wraps the body in
ctx.details.run { ... }, which is Kotlin’s standard-library run; the details object has no such method in Groovy. In the
Groovy DSL of Gradle 9.8.1 it fails when the rule is evaluated, so the Groovy listing above reads ctx.details.id and calls belongsTo on
ctx.details instead. This is what the manual’s form produced (Kotlin DSL does not hit it):
io.tidepool.demo:suite-api:1.0.0 FAILED
Failures:
- Could not resolve io.tidepool.demo:suite-api:1.0.0.
- Could not resolve io.tidepool.demo:suite-api:1.0.0.
- There was an error while evaluating a component metadata rule for io.tidepool.demo:suite-api:1.0.0.
- Could not find method run() for arguments [SuiteAlignmentRule$_execute_closure1@395db8b6] on io.tidepool.demo:suite-api:1.0.0 of type org.gradle.api.internal.artifacts.repositories.resolver.ComponentMetadataDetailsAdapter_Decorated.
io.tidepool.demo:suite-api:1.0.0 FAILED
\--- runtimeClasspath
The java-platform plugin, for modules you publish. Section "Building a platform" above did this already: tidepool-model
and tidepool-engine each declare api(platform(project(":tidepool-platform"))), and the platform constrains both. The
consumer asks for tidepool-model:1.1.0 and tidepool-engine:1.0.0; the model’s platform (1.1.0) constrains the engine to
1.1.0, and the engine that was asked for at 1.0.0 is raised without the consumer saying anything:
dependencies {
implementation("io.tidepool:tidepool-model:1.1.0")
implementation("io.tidepool:tidepool-engine:1.0.0")
}
dependencies {
implementation 'io.tidepool:tidepool-model:1.1.0'
implementation 'io.tidepool:tidepool-engine:1.0.0'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- io.tidepool:tidepool-model:1.1.0
| \--- io.tidepool:tidepool-platform:1.1.0
| +--- io.tidepool:tidepool-model:1.1.0 (c)
| +--- io.tidepool:tidepool-engine:1.1.0 (c)
| \--- org.apache.commons:commons-csv:1.14.1 (c)
\--- io.tidepool:tidepool-engine:1.0.0 -> 1.1.0
+--- org.apache.commons:commons-csv -> 1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
+--- io.tidepool:tidepool-platform:1.1.0 (*)
\--- io.tidepool:tidepool-model:1.1.0 (*)
The three solutions differ in who has to act. A BOM needs the consumer to import it; a virtual platform needs the consumer
to register a rule; the java-platform platform needs the publisher to do the work once, and consumers get alignment for
free. Maven has no alignment concept, only the Enforcer rules dependencyConvergence and requireUpperBoundDeps that fail
a build whose graph is not aligned (see
Dependency Analysis & Convergence).
Preventing accidental upgrades
Gradle upgrades optimistically: if two parts of the graph want different versions of a module, the highest wins, even above what your own build declared. That is the right default for a library ecosystem and the wrong one when you depend on an API that a newer version removed. The manual names three ways to say "not this time", and they differ in how loud they are.
| Approach | What happens when a higher version is requested | Run on this page |
|---|---|---|
|
The build fails and names the module and the two versions. |
Yes, below. |
A |
The higher requirement is overridden and the strict version is used; the build keeps working. |
Yes, "A bare version in a constraint does not cap anything" above. |
Dependency locking ( |
The versions resolved at lock time are reused until someone updates the lock file. |
No, see the locking page. |
The loud one: failOnVersionConflict turns every conflict into an error, so that no version is upgraded without a person
looking at it. The project asks for harmonics:1.0.0 and also uses tides-api:1.1.0, which needs 1.1.0 (the helper task is
the same resolve task as before, and the run is without the configuration cache because it fails):
dependencies {
implementation("io.tidepool.demo:harmonics:1.0.0")
implementation("io.tidepool.demo:tides-api:1.1.0")
}
configurations.configureEach {
resolutionStrategy.failOnVersionConflict()
}
tasks.register("resolve") {
inputs.files(configurations.runtimeClasspath)
doLast { println("resolved") }
}
dependencies {
implementation 'io.tidepool.demo:harmonics:1.0.0'
implementation 'io.tidepool.demo:tides-api:1.1.0'
}
configurations.configureEach {
resolutionStrategy.failOnVersionConflict()
}
tasks.register('resolve') {
inputs.files(configurations.runtimeClasspath)
doLast { println 'resolved' }
}
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':fail-on-conflict:resolve' (registered in build file 'fail-on-conflict/build.gradle.kts').
> Could not resolve all files for configuration ':fail-on-conflict:runtimeClasspath'.
> Could not resolve io.tidepool.demo:harmonics:1.0.0.
Required by:
project ':fail-on-conflict'
> Conflict found for module 'io.tidepool.demo:harmonics': between versions 1.1.0 and 1.0.0
> There is 1 more failure with an identical cause.
Everything that is not a conflict still resolves; every conflict must be settled by a person, with a strictly, a
constraint or a newer declaration. That is a good setting for a build that is under dependency review and a heavy one for a
large graph, where most conflicts are harmless; locking is the quiet alternative, and it is the one that makes dynamic
versions reproducible, on
Dependency Locking, Verification & Caching.
The blunt tools, force and eachDependency, are on
Resolution Rules & Substitution.
The same statements in Maven and Ivy
| Intent | Maven | Ivy | Gradle |
|---|---|---|---|
"At least this version" |
A plain |
A plain |
|
"Exactly this version, or fail" |
A hard requirement |
|
|
Raise or pin a transitive dependency |
|
|
A constraint, or |
Never use this version |
A range with a hole, |
An |
|
Import a family of versions |
A BOM with |
The POM parser expands it to |
|
Make a family agree |
A BOM; Enforcer |
No equivalent |
Alignment through platforms and |
The Maven side is on Dependency Management & BOMs and Versions, SNAPSHOTs & Ranges; the Ivy side on Ivy: Conflicts & Versions; and the comparison on Comparing Ant, Maven & Gradle.
Source: Maven: Bill of Materials (BOM) POMs and Ivy: override
Releases, Gradle 8.14 and where the books are outdated
The mechanisms on this page arrived over several releases, each from its release notes.
| Release | What changed |
|---|---|
4.6 |
Dependency constraints for transitive dependencies; importing BOMs and compile/runtime separation when consuming POMs, as a feature preview that had to be enabled in the settings file. |
5.0 |
Dependency version alignment, usable with Maven BOMs; the release notes show |
5.3 |
Fixes a bug of 5.0 to 5.2.1 in which an |
6.0 |
Gradle Module Metadata is published by default; rich versions ( |
7.0 |
The |
9.0 |
|
9.8 |
Reuse of Maven mirror settings ( |
Source: Gradle 4.6 release notes, Gradle 5.0 release notes, Upgrading from Gradle 5.x to 6.0 and Gradle 6.0 release notes
Gradle 8.14. Gradle 8.14 was not available here, so everything on this page was run on 9.8.1 only. The features
(version { }, constraints { }, platform(), enforcedPlatform(), java-platform, belongsTo,
doNotEndorseStrictVersions()) date from 6.0 or earlier according to the release notes above and are expected to work the
same way in 8.14, but that, the wording of the version-ordering and competing-strictly rules in the 8.14 manual, and the
dependencyScope/resolvable factories used by the probeVersions helper were not checked (not run here). The 9.0 changes that
touch dependency declarations are removals (see
Versions, Compatibility & Release History).
Gradle in Action (Gradle 1.7). The book predates platforms, constraints, rich versions and alignment. It keeps versions in
ext properties, where the current tool is a version catalog (stable in 7.4), and it declares dependencies with the
compile and testCompile configurations, removed in Gradle 7.0 in favour of implementation, api and
testImplementation. Publisher page for the book: Gradle in Action, Manning.
Pitfalls
| Pitfall | What to do |
|---|---|
A bare version in |
It is a |
|
Intentional: a direct |
|
|
A |
Root-most wins. Put the statement in the root project, or accept that the dependency’s is ignored. |
|
Its strictness is published and spreads to consumers. Use it only in applications. |
|
The platform’s strict constraint is no longer applied to other dependencies' requests, but it still stands. Change the dependency that asks for the out-of-range version, or the platform. |
The manual’s Kotlin snippet for |
The lambda of |
The manual’s Groovy |
Read |
Dynamic versions are cached for 24 hours |
|
A POM-only consumer loses |
Publish Gradle Module Metadata (the default) and keep the strict ranges wide. |