Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,7 @@ dev_maven.install(
],
known_contributing_modules = [
"root_wins_layer",
"root_wins_layer_two",
"rules_jvm_external",
],
lock_file = "//tests/custom_maven_install:root_wins_install.json",
Expand All @@ -979,11 +980,18 @@ dev_maven.amend_artifact(
)

bazel_dep(name = "root_wins_layer", version = "0.0.0", dev_dependency = True)
bazel_dep(name = "root_wins_layer_two", version = "0.0.0", dev_dependency = True)

local_path_override(
module_name = "root_wins_layer",
path = "tests/integration/root_wins_layer",
)

local_path_override(
module_name = "root_wins_layer_two",
path = "tests/integration/root_wins_layer_two",
)

dev_maven.install(
name = "root_module_can_override",
artifacts = ["com.squareup:javapoet:1.11.1"],
Expand Down Expand Up @@ -1115,15 +1123,15 @@ use_repo(

# Final entries
"com_google_http_client_google_http_client",
"legacy_multi_repo_hash",
"multi_repo_hash",
"root_wins",
"testonly_testing",
"unpinned_legacy_multi_repo_hash",
"unpinned_multi_repo_hash",
"unpinned_v1_lock_file_format",
"v1_lock_file_format",
"version_interval_testing",
"multi_repo_hash",
"unpinned_multi_repo_hash",
"legacy_multi_repo_hash",
"unpinned_legacy_multi_repo_hash",
)

http_file(
Expand Down
378 changes: 260 additions & 118 deletions private/extensions/maven.bzl

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ public boolean isForceVersion() {
@Override
public String toString() {
if (exclusions.isEmpty()) {
return "{" + coordinates + "}";
return forceVersion ? "{" + coordinates + ", forceVersion=true}" : "{" + coordinates + "}";
}
return "{" + coordinates + ", exclusions=" + exclusions + "}";
return forceVersion
? "{" + coordinates + ", exclusions=" + exclusions + ", forceVersion=true}"
: "{" + coordinates + ", exclusions=" + exclusions + "}";
}

@Override
Expand All @@ -70,12 +72,13 @@ public boolean equals(Object o) {
return false;
}
Artifact that = (Artifact) o;
return Objects.equals(this.getCoordinates(), that.getCoordinates())
return this.isForceVersion() == that.isForceVersion()
&& Objects.equals(this.getCoordinates(), that.getCoordinates())
&& Objects.equals(this.getExclusions(), that.getExclusions());
}

@Override
public int hashCode() {
return Objects.hash(getCoordinates(), getExclusions());
return Objects.hash(getCoordinates(), getExclusions(), isForceVersion());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,15 @@ public ResolutionRequest addRepository(URI uri) {

public ResolutionRequest addBom(String coordinates, String... exclusions) {
Objects.requireNonNull(coordinates, "BOM coordinates");
return addBom(new Coordinates(coordinates), exclusions);
return addBom(new Coordinates(coordinates), false, exclusions);
}

public ResolutionRequest addBom(Coordinates coordinates, String... exclusions) {
return addBom(coordinates, false, exclusions);
}

public ResolutionRequest addBom(
Coordinates coordinates, boolean forceVersion, String... exclusions) {
Objects.requireNonNull(coordinates, "BOM coordinates");

Coordinates bom =
Expand All @@ -75,7 +80,10 @@ public ResolutionRequest addBom(Coordinates coordinates, String... exclusions) {
"",
coordinates.getVersion());
Artifact artifact =
new Artifact(bom, Stream.of(exclusions).map(Coordinates::new).collect(Collectors.toSet()));
new Artifact(
bom,
Stream.of(exclusions).map(Coordinates::new).collect(Collectors.toSet()),
forceVersion);

return addBom(artifact);
}
Expand Down Expand Up @@ -128,7 +136,7 @@ public ResolutionRequest replaceDependencies(Collection<Artifact> amended) {

getRepositories().forEach(toReturn::addRepository);
amended.forEach(toReturn::addArtifact);
getBoms().stream().map(Objects::toString).forEach(toReturn::addBom);
getBoms().forEach(toReturn::addBom);
getGlobalExclusions().forEach(toReturn::exclude);
toReturn.useUnsafeSharedCache = isUseUnsafeSharedCache();
toReturn.userHome = userHome;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ public ResolverConfig(EventListener listener, String... args) throws IOException
.append(":")
.append(art.getVersion());
request.addBom(
coords.toString(),
new Coordinates(coords.toString()),
art.isForceVersion(),
art.getExclusions().stream()
.map(c -> c.getGroupId() + ":" + c.getArtifactId())
.toArray(String[]::new));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public class GradleBuildScriptGenerator {

private static final Handlebars handlebars = new Handlebars();

private static boolean isForceVersion(GradleDependency dependency) {
String version = dependency.getVersion();
return version != null && version.endsWith("!!");
}

static {
handlebars.registerHelper(
"notEmpty",
Expand Down Expand Up @@ -125,29 +130,22 @@ public static void generateBuildScript(
})
.collect(Collectors.toList()));

// If multiple versions of a BOM exists, we want to enforce the first one like maven
// Unlike maven, gradle seems to choose the highest versions, so this emulates the maven
// behavior by picking the first BOM version in the order we see.
Map<String, String> enforcedBomVersions = new LinkedHashMap<>();
// If multiple versions of a BOM exist, pick one version to keep Gradle from selecting a
// different version during conflict resolution. A forced BOM wins over an unforced BOM, and
// later forced BOMs match the Maven resolver's last-forced-wins behavior.
Map<String, GradleDependency> selectedBoms = new LinkedHashMap<>();
boms.forEach(
bom -> {
String artifactKey = bom.getGroup() + ":" + bom.getArtifact();
if (!enforcedBomVersions.containsKey(artifactKey)) {
enforcedBomVersions.put(artifactKey, bom.getVersion());
GradleDependency selectedBom = selectedBoms.get(artifactKey);
if (selectedBom == null || isForceVersion(bom)) {
selectedBoms.put(artifactKey, bom);
}
});

// Now get the boms filtered to include the "winning" boms if there are multiple versions of a
// given one
List<GradleDependency> actualBoms =
boms.stream()
.filter(
bom -> {
String artifactKey = bom.getGroup() + ":" + bom.getArtifact();
return enforcedBomVersions.containsKey(artifactKey)
&& enforcedBomVersions.get(artifactKey).equals(bom.getVersion());
})
.collect(Collectors.toList());
List<GradleDependency> actualBoms = selectedBoms.values().stream().collect(Collectors.toList());

contextMap.put(
"boms",
Expand All @@ -157,7 +155,16 @@ public static void generateBuildScript(
Map<String, Object> map = new HashMap<>();
map.put("group", dep.getGroup());
map.put("artifact", dep.getArtifact());
map.put("version", dep.getVersion());
String version = dep.getVersion();
boolean isForceVersion = isForceVersion(dep);
if (isForceVersion) {
version = version.substring(0, version.length() - 2);
map.put("forceVersion", true);
map.put("versionOnly", version);
} else {
map.put("forceVersion", false);
}
map.put("version", version);
return map;
})
.collect(Collectors.toList()));
Expand All @@ -172,7 +179,7 @@ public static void generateBuildScript(
map.put("artifact", dep.getArtifact());

String version = dep.getVersion();
boolean isForceVersion = version != null && version.endsWith("!!");
boolean isForceVersion = isForceVersion(dep);
if (isForceVersion) {
// Strip the !! suffix, we'll use version { strictly() } in template
version = version.substring(0, version.length() - 2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ repositories {

dependencies {
{{~#each boms}}
{{~#if forceVersion}}
implementation(platform("{{group}}:{{artifact}}")) {
version { strictly("{{versionOnly}}") }
}
{{~else}}
implementation platform("{{group}}:{{artifact}}:{{version}}")
{{~/if}}
{{~/each}}
{{~#each dependencies}}
implementation("{{group}}:{{artifact}}{{version}}{{~#if classifier}}{{classifier}}{{~/if}}{{~#if extension}}{{extension}}{{~/if}}"){{~#if exclusions}}{{~#if forceVersion}} {
Expand Down Expand Up @@ -53,6 +59,11 @@ configurations.all {
{{~#if forceVersion}}
force("{{group}}:{{artifact}}:{{versionOnly}}")
{{~/if}}
{{~/each}}
{{~#each boms}}
{{~#if forceVersion}}
force("{{group}}:{{artifact}}:{{versionOnly}}")
{{~/if}}
{{~/each}}
eachDependency { details ->
{{~#each dependencies}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -178,7 +179,8 @@ public ResolutionResult resolve(ResolutionRequest request) {
List<Dependency> bomsWithGlobalExclusions = addGlobalExclusions(globalExclusions, boms);
consoleLogListener.setPhase("Resolving " + bomsWithGlobalExclusions.size() + " BOM artifacts");
List<Dependency> bomDependencies =
resolveArtifactsFromBoms(system, session, repositories, bomsWithGlobalExclusions);
resolveArtifactsFromBoms(
system, session, repositories, bomsWithGlobalExclusions, request.getBoms());

List<Dependency> managedDependencies = createManagedDependencies(bomDependencies, dependencies);

Expand Down Expand Up @@ -489,24 +491,34 @@ private List<Dependency> resolveArtifactsFromBoms(
RepositorySystem system,
RepositorySystemSession session,
List<RemoteRepository> repositories,
List<Dependency> boms) {
// Use LinkedHashSet to maintain order of how BOMS were declared
Set<Dependency> managedDependencies = new LinkedHashSet<>();
List<Dependency> boms,
List<com.github.bazelbuild.rules_jvm_external.resolver.Artifact> originalBoms) {
Map<String, Dependency> managedDependencies = new LinkedHashMap<>();

for (Dependency bom : boms) {
for (int i = 0; i < boms.size(); i++) {
Dependency bom = boms.get(i);
boolean forceVersion = originalBoms.get(i).isForceVersion();
ArtifactDescriptorRequest request =
new ArtifactDescriptorRequest(bom.getArtifact(), repositories, JavaScopes.COMPILE);
try {
ArtifactDescriptorResult result = system.readArtifactDescriptor(session, request);
// NOTE: BOM dependencies are added in order so dependencies from eariler BOMs will
// take precedence over dependencies from later BOMs
managedDependencies.addAll(result.getManagedDependencies());
result
.getManagedDependencies()
.forEach(
dep -> {
String artifactKey = getArtifactKey(dep.getArtifact());
if (forceVersion) {
managedDependencies.put(artifactKey, dep);
} else {
managedDependencies.putIfAbsent(artifactKey, dep);
}
});
} catch (ArtifactDescriptorException e) {
throw new RuntimeException(e);
}
}

return ImmutableList.copyOf(managedDependencies);
return ImmutableList.copyOf(managedDependencies.values());
}

private List<Dependency> addGlobalExclusions(
Expand Down
Binary file modified private/tools/prebuilt/lock_file_converter_deploy.jar
Binary file not shown.
15 changes: 15 additions & 0 deletions tests/com/github/bazelbuild/rules_jvm_external/resolver/BUILD
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
load("@rules_java//java:java_library.bzl", "java_library")
load("@rules_java//java:java_test.bzl", "java_test")
load("//:defs.bzl", "artifact")

java_test(
name = "ResolutionRequestTest",
srcs = ["ResolutionRequestTest.java"],
test_class = "com.github.bazelbuild.rules_jvm_external.resolver.ResolutionRequestTest",
deps = [
"//private/tools/java/com/github/bazelbuild/rules_jvm_external",
"//private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver",
artifact(
"junit:junit",
repository_name = "regression_testing_coursier",
),
],
)

java_library(
name = "resolver",
testonly = True,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2026 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.github.bazelbuild.rules_jvm_external.resolver;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

import com.github.bazelbuild.rules_jvm_external.Coordinates;
import java.util.List;
import java.util.Set;
import org.junit.Test;

public class ResolutionRequestTest {

@Test
public void artifactEqualityIncludesForceVersion() {
Coordinates coordinates = new Coordinates("com.example:dependency:1.0");

assertNotEquals(new Artifact(coordinates, Set.of()), new Artifact(coordinates, Set.of(), true));
}

@Test
public void replaceDependenciesPreservesBomMetadata() {
Coordinates bomCoordinates = new Coordinates("com.example", "example-bom", "pom", "", "1.0");
Artifact forcedBom =
new Artifact(bomCoordinates, Set.of(new Coordinates("com.example:excluded")), true);
Artifact replacement = new Artifact(new Coordinates("com.example:replacement:1.0"), Set.of());

ResolutionRequest replaced =
new ResolutionRequest()
.addRepository("https://repo.example.com")
.addBom(forcedBom)
.replaceDependencies(List.of(replacement));

assertEquals(List.of(forcedBom), replaced.getBoms());
assertTrue(replaced.getBoms().get(0).isForceVersion());
assertEquals(
Set.of(new Coordinates("com.example:excluded")), replaced.getBoms().get(0).getExclusions());
assertEquals("pom", replaced.getBoms().get(0).getCoordinates().getExtension());
}
}
Loading