Open steve-todorov opened 1 month ago
I don't think this information is provided in the gradle apis. If I understand correctly, it is internally modeled as a strictly dependency constraint. We'd have to see if this dependency was also listed in the configuration's [getAllDependencyConstraints](https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/Configuration.html#getAllDependencyConstraints()) to guess that it was a BOM that might reported on differently. However, since we wouldn't know that, it could just as easily be your own constraints to avoid a version with a CVE, etc.
Since builds vary quite a bit and not all of the information is available in the APIs, I find it slightly better to workaround in the build instead of trying to bake assumptions into this plugin. Then you have a few options for how you might coerce this plugin to report on the updates.
One approach is to simply apply a resolution strategy to force the version. This is the easiest even though it is a little surprising when someone forgets about this configuration. Lets say you use version catalogs with an enforcedPlatforms
bundle. The report will now show those libraries in the up-to-date section at its declared version.
tasks.named<DependencyUpdatesTask>("dependencyUpdates").configure {
resolutionStrategy {
libs.bundles.enforcedPlatforms.get().forEach { library ->
force(library)
}
}
}
The other approach is to rewrite the report before it is printed.
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
import com.github.benmanes.gradle.versions.reporter.PlainTextReporter
tasks.withType<DependencyUpdatesTask> {
outputFormatter {
val reporter = PlainTextReporter(project, revision, gradleReleaseChannel)
libs.bundles.enforcedPlatforms.get().forEach { library ->
exceeded.dependencies.removeAll { dependency ->
"${dependency.group}:${dependency.name}" == "${library.module}"
}
}
reporter.write(System.out, this)
}
}
In both cases you would want to centralize your dependency definitions so that you can work with them programmatically. This tends to be a nice build cleanup in general and now idiomatic with version catalogs. You'll still need to opt-in the dependencies to ignore, but it will be very clear the next time you revisit the plugin's configuration what your customizations were and can easily add or remove as you see fit.
@ben-manes Thank you very much for your fast reply!
You understood me correctly. The idea is to prevent dependencies defined in a BOM
from being suggested in the reports, because they are enforced.
Your suggestion sounds like a possible solution, but from what I understand I should be defining the BOM
in a enforcedPlatforms
bundle, right? Or do you mean to define each dependency from the BOM
into the bundle catalogue and then use those with the force()
(which is what your code suggests)
I'm not really sure the difference between your question options. I can show you what I did in Caffeine to apply constraints to ensure transitives with CVEs were upgraded for security analyzers.
[bundles]
constraints = ["bcel", "bouncycastle-jdk18on", "commons-compress", "commons-text", "h2",
"httpclient", "guava", "jcommander", "jgit", "jsoup", "protobuf", "snakeyaml" ]
dependencies {
libs.bundles.constraints.get().forEach { library ->
constraints.add("implementation", library.module.toString())
.version { require(library.version!!) }
}
}
My examples adapted this to your situation. I defined the bundle so that I could group the dependencies and programmatically apply them. I could do it one-by-one, as I did for my report's individual forces, but with only a few (due to jdk compatibility reasons) I didn't generalize it there.
Does those details help?
re-reading and I see what you mean. You would want force the platform BOM itself instead of rewriting the report where you would need to know the individual dependencies and that would certainly be painful. You would need to somehow do this within the resolution strategy to apply the strictly
constraint on the BOM there and force the version. I'm not sure if that can be defined there. maybe something like
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (details.requested.group == 'com.google.cloud' && details.requested.name == 'libraries-bom') {
details.useVersion {
strictly '26.40.0'
}
}
}
Yeah, the idea is to avoid reports that suggest updating transitive
dependencies which were included from a bom
. Having to list them one-by-one in a catalogue
would be tedious.
I am not sure your suggestion makes sense to me. The implementation(enforcedPlatform("com.google.cloud:libraries-bom:26.40.0"))
line already enforces all transitive dependencies pulled in via the bom
. I did try adding the resolutionStrategy
you mentioned to the configurations
, but that did not really do anything. The report still suggests the newer gson version:
The following dependencies have later release versions:
- com.google.code.gson:gson [2.10.1 -> 2.11.0]
https://github.com/google/gson
I was thinking perhaps I could get the pom.xml
of the bom
and attempt to use your force
suggestion but looping through the dependencies from the pom.xml
instead of a catalog. Unless you know of a better way to get those?
Ahh.. I just realized the pom.xml
reading idea won't work as well, because some BOM
pom.xml files might import others:
<dependencyManagement>
<dependencies>
<!-- first-party-dependencies is part of java-shared-dependencies
BOM in https://github.com/googleapis/sdk-platform-java/blob/main/java-shared-dependencies/first-party-dependencies/pom.xml.
This includes Guava, Protobuf, gRPC, Google Auth Libraries, etc. -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>first-party-dependencies</artifactId>
<version>3.30.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- google-cloud-java from https://github.com/googleapis/java-cloud-bom -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-bom</artifactId>
<version>0.221.0</version><!-- {x-version-update:google-cloud-bom:current} -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
hmm, yeah this one is tricky. I don't know how using Gradle's apis one can do all of this.
You can get the pom from an ArtifactResolutionQuery (example), but that full evaluation is a lot of messy code. I think that might be the only way though...
When we copy a configuration we clear the dependencies and rewrite them with a dynamic version, +
. That loses the enforcedPlatform, which seems to work by adding a dependency attribute. We do copy attributes those as onto our dynamic query, so I am unsure why it wouldn't be forced that way already. There's not any knobs in the resolution strategy or other APIs that I see for us to play with to get your desired result.
Here is a proof-of-concept that works for you to extend further. It resolves all of the BOM dependencies and rejects any later version found. It is certainly a bit hacky but should be enough for you to play with until you are happy about the code quality.
plugins {
id "com.github.ben-manes.versions" version "0.51.0"
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.code.gson:gson:2.10.1")
}
tasks.named("dependencyUpdates").configure {
def resolved = []
doFirst {
resolveBom("com.google.cloud", "libraries-bom", "26.40.0", resolved)
}
resolutionStrategy {
componentSelection {
all {
if (resolved.contains("${candidate.group}:${candidate.module}")
&& (candidate.version != currentVersion)) {
reject('Bom dependency')
}
}
}
}
}
def resolveBom(def group, def name, def version, def accumulator) {
def resolutionResult =
project.dependencies
.createArtifactResolutionQuery()
.forModule(group, name, version)
.withArtifacts(MavenModule, MavenPomArtifact)
.execute()
for (def result : resolutionResult.resolvedComponents) {
for (def artifact : result.getArtifacts(MavenPomArtifact)) {
if (artifact instanceof ResolvedArtifactResult) {
def file = artifact.file
def pom = new XmlSlurper(/* validating */ false, /* namespaceAware */ false).parse(file)
def dependencies = pom.dependencyManagement.dependencies.dependency
dependencies.each {
def coordinate = "${it.groupId.text()}:${it.artifactId.text()}"
if (accumulator.add(coordinate)) {
if ((it.scope.text() == 'import') || (it.type.text() == 'pom')) {
resolveBom(it.groupId.text(), it.artifactId.text(), it.version.text(), accumulator)
}
}
}
}
}
}
}
Feature request
We'd like to suggest adding a flag that would stop showing dependencies in reports for dependencies that are pulled in via a
BOM
. The flag would probably need to have three modes:default
- print the report as-is.silent
- don't show anydependency
update reports that have been pulled in from a BOM.warn
- print a one-liner saying `heyThe different modes would make the reports act slightly differently:
default
mode the end report would be as it is now:silent
mode would remove the dependency from the report.warn
could output something like this:A bit more context
We have multiple projects that enforce
bom
dependencies. Here's an example:When we run
./gradlew dependencyUpdates
the report will say there's a newercom.google.code.gson:gson
version:On its own the report is absolutely correct, but kind of lacks some flexibility and additional context. Someone could easily be confused and decide to define
implementation("com.google.code.gson:gson:2.11.0")
which won't work, because the version2.10.1
is enforced via theenforcedPlatform
.Our case would fall under the
silent
mode if there were a flag.