[gradle] Plumb through the force_version attribute (#1515)
Adds support for the force_version attribute in the Gradle resolver,
allowing users to force specific dependency versions.
Changes:
- Add forceVersion field to Artifact class
- Plumb through ConfigArtifact and ResolverConfig
- Implement in both GradleResolver and MavenResolver
- Add test for force version behavior
diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/Artifact.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/Artifact.java
index ecd0a94..92babce 100644
--- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/Artifact.java
+++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/Artifact.java
@@ -24,14 +24,21 @@
private final Coordinates coordinates;
private final Set<Coordinates> exclusions;
+ private final boolean forceVersion;
public Artifact(Coordinates coordinates, Coordinates... exclusions) {
this(coordinates, ImmutableSet.copyOf(exclusions));
}
public Artifact(Coordinates coordinates, Collection<Coordinates> exclusions) {
+ this(coordinates, exclusions, false);
+ }
+
+ public Artifact(
+ Coordinates coordinates, Collection<Coordinates> exclusions, boolean forceVersion) {
this.coordinates = Objects.requireNonNull(coordinates);
this.exclusions = ImmutableSet.copyOf(exclusions);
+ this.forceVersion = forceVersion;
}
public Coordinates getCoordinates() {
@@ -42,6 +49,10 @@
return exclusions;
}
+ public boolean isForceVersion() {
+ return forceVersion;
+ }
+
@Override
public String toString() {
if (exclusions.isEmpty()) {
diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/ConfigArtifact.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/ConfigArtifact.java
index 053a5ad..18e8175 100644
--- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/ConfigArtifact.java
+++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/ConfigArtifact.java
@@ -28,6 +28,7 @@
private String packaging;
private String version;
private Set<String> exclusions;
+ private boolean force_version;
public String getGroupId() {
return group;
@@ -56,4 +57,8 @@
return exclusions.stream().map(Coordinates::new).collect(ImmutableSet.toImmutableSet());
}
+
+ public boolean isForceVersion() {
+ return force_version;
+ }
}
diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/ResolverConfig.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/ResolverConfig.java
index a296702..c0cd745 100644
--- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/ResolverConfig.java
+++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/ResolverConfig.java
@@ -177,11 +177,10 @@
art.getExtension(),
art.getClassifier(),
art.getVersion());
- request.addArtifact(
- coords.toString(),
- art.getExclusions().stream()
- .map(c -> c.getGroupId() + ":" + c.getArtifactId())
- .toArray(String[]::new));
+ com.github.bazelbuild.rules_jvm_external.resolver.Artifact artifact =
+ new com.github.bazelbuild.rules_jvm_external.resolver.Artifact(
+ coords, art.getExclusions(), art.isForceVersion());
+ request.addArtifact(artifact);
});
}
diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolver.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolver.java
index b6eea23..0034d44 100644
--- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolver.java
+++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolver.java
@@ -607,10 +607,18 @@
exclusion -> {
exclusions.add(new ExclusionImpl(exclusion.getGroupId(), exclusion.getArtifactId()));
});
+
+ // When force_version is true, use Gradle's !! shorthand which is equivalent to strictly()
+ // This forces the exact version and prevents transitive dependencies from overriding it
+ String version = coordinates.getVersion();
+ if (artifact.isForceVersion() && version != null && !version.isEmpty()) {
+ version = version + "!!";
+ }
+
return new GradleDependencyImpl(
coordinates.getGroupId(),
coordinates.getArtifactId(),
- coordinates.getVersion(),
+ version,
exclusions,
coordinates.getClassifier(),
coordinates.getExtension());
diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/maven/MavenResolver.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/maven/MavenResolver.java
index 24ed229..82bfe74 100644
--- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/maven/MavenResolver.java
+++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/maven/MavenResolver.java
@@ -118,13 +118,20 @@
private Dependency createDependency(
com.github.bazelbuild.rules_jvm_external.resolver.Artifact source) {
Coordinates coords = source.getCoordinates();
+
+ // When force_version is true, use Maven version range syntax [version] to force exact version
+ String version = coords.getVersion();
+ if (source.isForceVersion() && version != null && !version.isEmpty()) {
+ version = "[" + version + "]";
+ }
+
Artifact artifact =
new DefaultArtifact(
coords.getGroupId(),
coords.getArtifactId(),
coords.getClassifier(),
coords.getExtension(),
- coords.getVersion());
+ version);
Set<Exclusion> excluded =
source.getExclusions().stream().map(this::createExclusion).collect(Collectors.toSet());
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/ResolverTestBase.java b/tests/com/github/bazelbuild/rules_jvm_external/resolver/ResolverTestBase.java
index 3755835..14d97a5 100644
--- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/ResolverTestBase.java
+++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/ResolverTestBase.java
@@ -830,6 +830,47 @@
assertEquals(coords, resolved.nodes().iterator().next());
}
+ @Test
+ public void shouldRespectForceVersionWhenResolvingConflicts() {
+ // When force_version is set on a lower version, it should win over the higher version
+ // that would normally be selected during version conflict resolution.
+ Coordinates lowerVersion = new Coordinates("com.example:forced:1.0");
+ Coordinates higherVersion = new Coordinates("com.example:forced:2.0");
+ Coordinates dependsOnLower = new Coordinates("com.example:uses-lower:1.0");
+ Coordinates dependsOnHigher = new Coordinates("com.example:uses-higher:1.0");
+
+ Path repo =
+ MavenRepo.create()
+ .add(lowerVersion)
+ .add(higherVersion)
+ .add(dependsOnLower, lowerVersion)
+ .add(dependsOnHigher, higherVersion)
+ .getPath();
+
+ // Create a request that forces the lower version using the new forceVersion capability
+ ResolutionRequest request = new ResolutionRequest().addRepository(repo.toUri());
+ request.addArtifact(dependsOnLower.toString());
+ // Force the lower version by creating an Artifact with forceVersion=true
+ Artifact forcedArtifact = new Artifact(lowerVersion, Set.of(), true);
+ request.addArtifact(forcedArtifact);
+ request.addArtifact(dependsOnHigher.toString());
+
+ // Without force_version implementation, this test will fail for Gradle resolver
+ // because it will resolve to the higher version (2.0) instead of the forced lower version (1.0)
+ Graph<Coordinates> graph = resolver.resolve(request).getResolution();
+
+ Set<Coordinates> forcedNodes =
+ graph.nodes().stream()
+ .filter(c -> "forced".equals(c.getArtifactId()))
+ .collect(Collectors.toSet());
+
+ assertEquals("Should resolve to exactly one version", 1, forcedNodes.size());
+
+ assertTrue(
+ "Should resolve to forced lower version when force_version is set",
+ forcedNodes.contains(lowerVersion));
+ }
+
protected ResolutionRequest prepareRequestFor(URI repo, Coordinates... coordinates) {
ResolutionRequest request = new ResolutionRequest().addRepository(repo);
for (Coordinates coordinate : coordinates) {
diff --git a/tests/custom_maven_install/regression_testing_gradle_install.json b/tests/custom_maven_install/regression_testing_gradle_install.json
index 16a8c9a..deced35 100644
--- a/tests/custom_maven_install/regression_testing_gradle_install.json
+++ b/tests/custom_maven_install/regression_testing_gradle_install.json
@@ -526,7 +526,7 @@
"org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21": "org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21",
"org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21": "org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10",
"org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21": "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10",
- "org.jetbrains.kotlin:kotlin-stdlib:1.8.0": "org.jetbrains.kotlin:kotlin-stdlib:1.8.21",
+ "org.jetbrains.kotlin:kotlin-stdlib:1.8.0": "org.jetbrains.kotlin:kotlin-stdlib:1.9.10",
"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1": "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4",
"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1": "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
},