Give scope-local BOMs precedence and warn about conflicting BOM versions
Some checks are pending
bld-ci / build (./bld, 17, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 21, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 25, macos-latest, false) (push) Waiting to run
bld-ci / build (./bld, 26, macos-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 17, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 21, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 25, windows-latest, false) (push) Waiting to run
bld-ci / build (.\bld.bat, 26, windows-latest, false) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 17, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 21, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 25, ubuntu-latest, true) (push) Waiting to run
bld-ci / build (unittests, password, unittests, mariadb:10.9, mysql:8, gvenzl/oracle-free:latest, gvenzl/oracle-xe:18-slim, postgres:15, ./bld, 26, ubuntu-latest, true) (push) Waiting to run
javadocs-pages / deploy (push) Waiting to run

This commit is contained in:
Geert Bevin 2026-07-16 14:34:27 -04:00
parent c9298d3de6
commit d7d176eef6
5 changed files with 169 additions and 14 deletions

View file

@ -7,6 +7,7 @@ package rife.bld.dependencies;
import rife.ioc.HierarchicalProperties;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@ -75,10 +76,13 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
* {@code compile} scope BOMs also apply to the {@code provided},
* {@code runtime} and {@code test} scopes, and the {@code runtime} and
* {@code provided} scope BOMs also apply to the {@code test} scope.
* The BOMs are returned in classpath order, for the {@code test} scope
* that is {@code compile}, {@code provided}, {@code runtime},
* {@code test}, and this order determines their precedence when
* several manage the same dependency.
* The BOMs are returned with the scope's own BOMs first, followed by
* the inherited ones from the more specific to the more general scope,
* for the {@code test} scope that is {@code test}, {@code runtime},
* {@code provided}, {@code compile}. This order determines their
* precedence when several manage the same dependency: a BOM declared
* in the scope where a dependency is used takes precedence over a BOM
* that is inherited from a broader scope.
* <p>
* The {@code standalone} scope only uses its own BOMs, and its BOMs
* deliberately never apply to any other scope.
@ -100,9 +104,9 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
private static Scope[] bomScopes(Scope scope) {
return switch (scope) {
case provided -> new Scope[]{Scope.compile, Scope.provided};
case runtime -> new Scope[]{Scope.compile, Scope.runtime};
case test -> new Scope[]{Scope.compile, Scope.provided, Scope.runtime, Scope.test};
case provided -> new Scope[]{Scope.provided, Scope.compile};
case runtime -> new Scope[]{Scope.runtime, Scope.compile};
case test -> new Scope[]{Scope.test, Scope.runtime, Scope.provided, Scope.compile};
default -> new Scope[]{scope};
};
}
@ -142,6 +146,37 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
return result;
}
/**
* Returns the version conflicts between the BOMs that apply to the
* scopes, where more than one applicable BOM manages the same
* dependency at a different version.
* <p>
* Each conflict is reported once, the versions are keyed by their BOM
* in precedence order so that the first is the version that is used.
*
* @param properties the properties to use to get artifacts
* @param retriever the retriever to use to get artifacts
* @param repositories the repositories to use for the BOM resolution
* @return the version conflicts between the applicable BOMs
* @since 2.4.0
*/
public List<VersionResolution.BomVersionConflict> bomVersionConflicts(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) {
var result = new ArrayList<VersionResolution.BomVersionConflict>();
var seen = new HashSet<String>();
for (var scope : keySet()) {
var effective_boms = effectiveBoms(scope);
if (effective_boms.size() < 2) {
continue;
}
for (var conflict : VersionResolution.resolveBomVersionConflicts(properties, retriever, repositories, effective_boms)) {
if (seen.add(conflict.dependency() + conflict.bomVersions())) {
result.add(conflict);
}
}
}
return result;
}
/**
* Returns the transitive set of dependencies that would be used for the compile scope in a project.
*

View file

@ -6,8 +6,10 @@ package rife.bld.dependencies;
import rife.ioc.HierarchicalProperties;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
@ -175,6 +177,62 @@ public class VersionResolution {
return bom_versions;
}
/**
* Describes a version conflict between bills of materials, where more
* than one applicable BOM manages the same dependency at a different
* version.
*
* @param dependency the group and artifact identifiers of the
* dependency that is managed at conflicting versions
* @param bomVersions the versions that the BOMs manage the dependency
* at, keyed by the BOM, in precedence order so that
* the first entry is the version that is used
* @since 2.4.0
*/
public record BomVersionConflict(String dependency, Map<String, Version> bomVersions) {
}
/**
* Resolves the version conflicts between the provided bills of
* materials, where more than one of them manages the same dependency
* at a different version.
*
* @param properties the properties to use to get artifacts
* @param retriever the retriever to use to get the BOMs
* @param repositories the repositories to resolve the BOMs in
* @param boms the BOMs to check, in precedence order
* @return the version conflicts between the BOMs
* @since 2.4.0
*/
public static List<BomVersionConflict> resolveBomVersionConflicts(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories, Collection<Bom> boms) {
var base = new VersionResolution(properties);
var versions_by_key = new LinkedHashMap<String, LinkedHashMap<String, Version>>();
var dependency_by_key = new LinkedHashMap<String, String>();
if (boms != null) {
for (var bom : boms) {
var pom = new DependencyResolver(base, retriever, repositories, bom).getMavenPom(bom);
for (var managed : pom.getManagedDependencies()) {
if (managed.version() != null && !managed.version().isBlank()) {
var dependency = managed.convertToDependency();
var key = managedKey(dependency);
versions_by_key.computeIfAbsent(key, k -> new LinkedHashMap<>())
.putIfAbsent(bom.toArtifactString(), dependency.version());
dependency_by_key.putIfAbsent(key, dependency.toArtifactString());
}
}
}
}
var conflicts = new ArrayList<BomVersionConflict>();
for (var entry : versions_by_key.entrySet()) {
var bom_versions = entry.getValue();
if (bom_versions.values().stream().distinct().count() > 1) {
conflicts.add(new BomVersionConflict(dependency_by_key.get(entry.getKey()), new LinkedHashMap<>(bom_versions)));
}
}
return conflicts;
}
// builds the identity that dependency management entries are matched
// on, mirroring Maven this includes the type and the classifier, the
// modular and forced-classpath JAR types match the plain jar entries

View file

@ -86,6 +86,28 @@ public class DownloadOperation extends AbstractOperation<DownloadOperation> {
for (var dependency : dependencies().versionlessDependenciesWithoutBom(properties(), artifactRetriever(), repositories())) {
System.out.println("Warning: '" + dependency.toArtifactString() + "' isn't covered by a BOM, its latest version will be used");
}
for (var conflict : dependencies().bomVersionConflicts(properties(), artifactRetriever(), repositories())) {
System.out.println(formatBomVersionConflict(conflict));
}
}
/**
* Formats a warning message for a BOM version conflict.
*
* @param conflict the BOM version conflict to format
* @return the formatted warning message
* @since 2.4.0
*/
protected static String formatBomVersionConflict(rife.bld.dependencies.VersionResolution.BomVersionConflict conflict) {
var entries = conflict.bomVersions().entrySet().iterator();
var used = entries.next();
var message = new StringBuilder("Warning: '" + conflict.dependency() + "' is managed by multiple BOMs, using " +
used.getValue() + " from '" + used.getKey() + "'");
while (entries.hasNext()) {
var other = entries.next();
message.append(", not ").append(other.getValue()).append(" from '").append(other.getKey()).append('\'');
}
return message.toString();
}
/**

View file

@ -78,6 +78,9 @@ public class PurgeOperation extends AbstractOperation<PurgeOperation> {
for (var dependency : dependencies().versionlessDependenciesWithoutBom(properties(), artifactRetriever(), repositories())) {
System.out.println("Warning: '" + dependency.toArtifactString() + "' isn't covered by a BOM, its latest version will be used");
}
for (var conflict : dependencies().bomVersionConflicts(properties(), artifactRetriever(), repositories())) {
System.out.println(DownloadOperation.formatBomVersionConflict(conflict));
}
}
/**

View file

@ -410,10 +410,11 @@ public class TestBom {
.include(new Bom("com.example", "bom2", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"));
// when several applicable BOMs manage the same dependency, the
// first one in classpath composition order determines the version
// a BOM declared in the dependency's own scope takes precedence
// over a BOM inherited from a broader scope, so the test scope
// bom2 wins over the compile scope bom1
var test_resolved = scopes.resolveTestDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
assertEquals(Version.parse("1.4.0"), test_resolved.get(new Dependency("com.example", "a")).version());
assertEquals(Version.parse("2.2.0"), test_resolved.get(new Dependency("com.example", "a")).version());
} finally {
server.stop(0);
}
@ -470,6 +471,41 @@ public class TestBom {
}
}
@Test
void testBomVersionConflictsAreDetected() throws Exception {
var server = createArtifactServer(Map.of(
"bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0") + managed("b", "3.0.0")),
"bom2:1.0.0", bomPom("bom2", "1.0.0", managed("a", "2.2.0"))),
Map.of());
server.start();
try {
var scopes = new DependencyScopes();
scopes.scope(compile)
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)));
scopes.scope(Scope.test)
.include(new Bom("com.example", "bom2", new VersionNumber(1, 0, 0)));
var conflicts = scopes.bomVersionConflicts(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
// only 'a' is managed by both BOMs at different versions, 'b'
// is managed by a single BOM so it isn't a conflict
assertEquals(1, conflicts.size());
var conflict = conflicts.get(0);
assertEquals("com.example:a", conflict.dependency());
var versions = conflict.bomVersions().entrySet().iterator();
// the test scope bom2 wins and is reported first, the compile
// scope bom1 is the alternative
var used = versions.next();
assertEquals("com.example:bom2", used.getKey());
assertEquals(Version.parse("2.2.0"), used.getValue());
var other = versions.next();
assertEquals("com.example:bom1", other.getKey());
assertEquals(Version.parse("1.4.0"), other.getValue());
} finally {
server.stop(0);
}
}
@Test
void testEffectiveBomsComposition() {
var compile_bom = new Bom("com.example", "compile-bom", new VersionNumber(1, 0, 0));
@ -485,11 +521,12 @@ public class TestBom {
scopes.scope(Scope.test).include(test_bom);
scopes.scope(Scope.standalone).include(standalone_bom);
// the scope's own BOMs come first, then the inherited ones from
// the more specific to the more general scope
assertEquals(List.of(compile_bom), scopes.effectiveBoms(compile));
assertEquals(List.of(compile_bom, provided_bom), scopes.effectiveBoms(Scope.provided));
assertEquals(List.of(compile_bom, runtime_bom), scopes.effectiveBoms(Scope.runtime));
// the test scope order matches the test classpath order
assertEquals(List.of(compile_bom, provided_bom, runtime_bom, test_bom), scopes.effectiveBoms(Scope.test));
assertEquals(List.of(provided_bom, compile_bom), scopes.effectiveBoms(Scope.provided));
assertEquals(List.of(runtime_bom, compile_bom), scopes.effectiveBoms(Scope.runtime));
assertEquals(List.of(test_bom, runtime_bom, provided_bom, compile_bom), scopes.effectiveBoms(Scope.test));
assertEquals(List.of(standalone_bom), scopes.effectiveBoms(Scope.standalone));
}