diff --git a/src/main/java/rife/bld/BaseProject.java b/src/main/java/rife/bld/BaseProject.java
index 9e3330f..58b801b 100644
--- a/src/main/java/rife/bld/BaseProject.java
+++ b/src/main/java/rife/bld/BaseProject.java
@@ -1032,9 +1032,10 @@ public class BaseProject extends BuildExecutor {
/**
* Creates a new bill of materials instance from a string representation.
- * The format is {@code groupId:artifactId:version:classifier}.
- * The {@code version} and {@code classifier} are optional, and an
- * optional {@code @bom} or {@code @pom} type suffix is accepted.
+ * The format is {@code groupId:artifactId:version}.
+ * The {@code version} is optional, and an optional {@code @bom} or
+ * {@code @pom} type suffix is accepted. BOMs can't have classifiers,
+ * strings that contain one are rejected.
*
* If the string can't be successfully parsed, {@code null} will be returned.
*
diff --git a/src/main/java/rife/bld/dependencies/Bom.java b/src/main/java/rife/bld/dependencies/Bom.java
index e83bbe3..02314d7 100644
--- a/src/main/java/rife/bld/dependencies/Bom.java
+++ b/src/main/java/rife/bld/dependencies/Bom.java
@@ -24,24 +24,21 @@ import java.util.regex.Pattern;
*/
public class Bom extends Dependency {
public Bom(String groupId, String artifactId) {
- this(groupId, artifactId, null, null);
+ this(groupId, artifactId, null);
}
public Bom(String groupId, String artifactId, Version version) {
- this(groupId, artifactId, version, null);
+ super(groupId, artifactId, version, null, TYPE_BOM);
}
- public Bom(String groupId, String artifactId, Version version, String classifier) {
- super(groupId, artifactId, version, classifier, TYPE_BOM);
- }
-
- private static final Pattern BOM_PATTERN = Pattern.compile("^(?[^:@]+):(?[^:@]+)(?::(?[^:@]+)(?::(?[^:@]+))?)?(?:@(?:bom|pom))?$");
+ private static final Pattern BOM_PATTERN = Pattern.compile("^(?[^:@]+):(?[^:@]+)(?::(?[^:@]+))?(?:@(?:bom|pom))?$");
/**
* Parses a BOM from a string representation.
- * The format is {@code groupId:artifactId:version:classifier}.
- * The {@code version} and {@code classifier} are optional, and an
- * optional {@code @bom} or {@code @pom} type suffix is accepted.
+ * The format is {@code groupId:artifactId:version}.
+ * The {@code version} is optional, and an optional {@code @bom} or
+ * {@code @pom} type suffix is accepted. BOMs can't have classifiers,
+ * strings that contain one are rejected.
*
* If the string can't be successfully parsed, {@code null} will be returned.
*
@@ -63,8 +60,7 @@ public class Bom extends Dependency {
var groupId = matcher.group("groupId");
var artifactId = matcher.group("artifactId");
var version = Version.parse(matcher.group("version"));
- var classifier = matcher.group("classifier");
- return new Bom(groupId, artifactId, version, classifier);
+ return new Bom(groupId, artifactId, version);
}
}
diff --git a/src/main/java/rife/bld/dependencies/DependencyScopes.java b/src/main/java/rife/bld/dependencies/DependencyScopes.java
index ae8ef8c..1d973dc 100644
--- a/src/main/java/rife/bld/dependencies/DependencyScopes.java
+++ b/src/main/java/rife/bld/dependencies/DependencyScopes.java
@@ -68,28 +68,72 @@ public class DependencyScopes extends LinkedHashMap {
}
/**
- * Returns the version-less dependencies that are not covered by the
- * BOMs that are declared in their scope.
+ * Returns the BOMs that apply to the version resolution of a particular
+ * scope.
*
- * Scopes that don't declare any BOMs are not considered, their
+ * BOMs follow the same scope composition as the classpaths: the
+ * {@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 {@code standalone} scope only uses its own BOMs, and its BOMs
+ * deliberately never apply to any other scope.
+ *
+ * @param scope the scope to retrieve the applicable BOMs for
+ * @return the BOMs that apply to the scope's version resolution
+ * @since 2.4.0
+ */
+ public List effectiveBoms(Scope scope) {
+ var boms = new ArrayList();
+ for (var bom_scope : bomScopes(scope)) {
+ var scoped_dependencies = get(bom_scope);
+ if (scoped_dependencies != null) {
+ boms.addAll(scoped_dependencies.boms());
+ }
+ }
+ return boms;
+ }
+
+ 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};
+ default -> new Scope[]{scope};
+ };
+ }
+
+ /**
+ * Returns the version-less dependencies that are not covered by the
+ * BOMs that apply to their scope.
+ *
+ * Scopes that no BOMs apply to are not considered, their
* version-less dependencies simply resolve to the latest version.
+ * Dependencies whose version is supplied by a {@code bld.override}
+ * property are not reported either.
*
* @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-less dependencies that no BOM in their scope covers
+ * @return the version-less dependencies that no applicable BOM covers
* @since 2.4.0
*/
public List versionlessDependenciesWithoutBom(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories) {
var result = new ArrayList();
- for (var scoped_dependencies : values()) {
- if (scoped_dependencies.boms().isEmpty()) {
+ for (var entry : entrySet()) {
+ var effective_boms = effectiveBoms(entry.getKey());
+ if (effective_boms.isEmpty()) {
continue;
}
- var resolution = new VersionResolution(properties, retriever, repositories, scoped_dependencies.boms());
- for (var dependency : scoped_dependencies) {
+ var resolution = new VersionResolution(properties, retriever, repositories, effective_boms);
+ for (var dependency : entry.getValue()) {
if (dependency.version().equals(VersionNumber.UNKNOWN) &&
- !resolution.bomVersions().containsKey(dependency.toArtifactString()) &&
+ !resolution.coversDependency(dependency) &&
+ !resolution.versionOverrides().containsKey(dependency.toArtifactString()) &&
!result.contains(dependency)) {
result.add(dependency);
}
@@ -109,6 +153,7 @@ public class DependencyScopes extends LinkedHashMap {
*/
public DependencySet resolveCompileDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories) {
return resolveScopedDependencies(properties, retriever, repositories,
+ Scope.compile,
new Scope[]{Scope.compile},
new Scope[]{Scope.compile},
null);
@@ -125,6 +170,7 @@ public class DependencyScopes extends LinkedHashMap {
*/
public DependencySet resolveProvidedDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories) {
return resolveScopedDependencies(properties, retriever, repositories,
+ Scope.provided,
new Scope[]{Scope.provided},
new Scope[]{Scope.compile, Scope.runtime},
null);
@@ -141,6 +187,7 @@ public class DependencyScopes extends LinkedHashMap {
*/
public DependencySet resolveRuntimeDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories) {
return resolveScopedDependencies(properties, retriever, repositories,
+ Scope.runtime,
new Scope[]{Scope.compile, Scope.runtime},
new Scope[]{Scope.compile, Scope.runtime},
resolveCompileDependencies(properties, retriever, repositories));
@@ -157,6 +204,7 @@ public class DependencyScopes extends LinkedHashMap {
*/
public DependencySet resolveStandaloneDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories) {
return resolveScopedDependencies(properties, retriever, repositories,
+ Scope.standalone,
new Scope[]{Scope.standalone},
new Scope[]{Scope.compile, Scope.runtime},
null);
@@ -173,22 +221,21 @@ public class DependencyScopes extends LinkedHashMap {
*/
public DependencySet resolveTestDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories) {
return resolveScopedDependencies(properties, retriever, repositories,
+ Scope.test,
new Scope[]{Scope.test},
new Scope[]{Scope.compile, Scope.runtime},
null);
}
- private DependencySet resolveScopedDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories, Scope[] resolvedScopes, Scope[] transitiveScopes, DependencySet excluded) {
+ private DependencySet resolveScopedDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories, Scope scope, Scope[] resolvedScopes, Scope[] transitiveScopes, DependencySet excluded) {
var roots = new ArrayList();
- var boms = new ArrayList();
- for (var scope : resolvedScopes) {
- var scoped_dependencies = get(scope);
+ for (var resolved_scope : resolvedScopes) {
+ var scoped_dependencies = get(resolved_scope);
if (scoped_dependencies != null) {
roots.addAll(scoped_dependencies);
- boms.addAll(scoped_dependencies.boms());
}
}
- var resolution = new VersionResolution(properties, retriever, repositories, boms);
+ var resolution = new VersionResolution(properties, retriever, repositories, effectiveBoms(scope));
var dependencies = new ParallelDependencyResolver(resolution, retriever, repositories).resolveAllDependencies(roots, transitiveScopes);
if (excluded != null) {
dependencies.removeAll(excluded);
diff --git a/src/main/java/rife/bld/dependencies/VersionResolution.java b/src/main/java/rife/bld/dependencies/VersionResolution.java
index 409009b..f346770 100644
--- a/src/main/java/rife/bld/dependencies/VersionResolution.java
+++ b/src/main/java/rife/bld/dependencies/VersionResolution.java
@@ -148,11 +148,15 @@ public class VersionResolution {
this(base, resolveBomVersions(base, retriever, repositories, boms));
}
+ // returns a resolution that additionally imports the provided BOMs,
+ // versions that are already imported take precedence over the new ones
VersionResolution withBoms(ArtifactRetriever retriever, List repositories, Collection boms) {
if (boms == null || boms.isEmpty()) {
return this;
}
- return new VersionResolution(this, retriever, repositories, boms);
+ var merged = new HashMap<>(resolveBomVersions(this, retriever, repositories, boms));
+ merged.putAll(bomVersions_);
+ return new VersionResolution(this, merged);
}
private static Map resolveBomVersions(VersionResolution resolution, ArtifactRetriever retriever, List repositories, Collection boms) {
@@ -163,7 +167,7 @@ public class VersionResolution {
for (var managed : pom.getManagedDependencies()) {
if (managed.version() != null && !managed.version().isBlank()) {
var dependency = managed.convertToDependency();
- bom_versions.putIfAbsent(dependency.toArtifactString(), dependency.version());
+ bom_versions.putIfAbsent(managedKey(dependency), dependency.version());
}
}
}
@@ -171,6 +175,20 @@ public class VersionResolution {
return bom_versions;
}
+ // 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
+ // that BOMs manage
+ private static String managedKey(Dependency dependency) {
+ var type = dependency.type();
+ if (type == null || type.isBlank() ||
+ Dependency.TYPE_MODULAR_JAR.equals(type) ||
+ Dependency.TYPE_CLASSPATH_JAR.equals(type)) {
+ type = Dependency.TYPE_JAR;
+ }
+ return dependency.toArtifactString() + ":" + type + ":" + dependency.classifier();
+ }
+
private static int parseParallelism(HierarchicalProperties properties, String property, int defaultValue) {
if (properties != null) {
var parallelism = properties.getValueString(property);
@@ -198,7 +216,7 @@ public class VersionResolution {
return overridden;
}
if (VersionNumber.UNKNOWN.equals(original.version())) {
- var bom_version = bomVersions_.get(original.toArtifactString());
+ var bom_version = bomVersions_.get(managedKey(original));
if (bom_version != null) {
return bom_version;
}
@@ -239,7 +257,7 @@ public class VersionResolution {
public Dependency overrideDeclaredDependency(Dependency declared) {
var overridden = versionOverrides_.get(declared.toArtifactString());
if (overridden == null && VersionNumber.UNKNOWN.equals(declared.version())) {
- overridden = bomVersions_.get(declared.toArtifactString());
+ overridden = bomVersions_.get(managedKey(declared));
}
if (overridden == null) {
return declared;
@@ -264,7 +282,7 @@ public class VersionResolution {
public Dependency overrideTransitiveDependency(Dependency transitive) {
var overridden = versionOverrides_.get(transitive.toArtifactString());
if (overridden == null) {
- overridden = bomVersions_.get(transitive.toArtifactString());
+ overridden = bomVersions_.get(managedKey(transitive));
}
if (overridden == null) {
return transitive;
@@ -292,10 +310,27 @@ public class VersionResolution {
return versionOverrides_;
}
+ /**
+ * Indicates whether a bill of materials supplies the version of a
+ * particular dependency.
+ *
+ * The dependency identity takes the group, artifact, type and
+ * classifier into account, mirroring Maven's dependency management.
+ *
+ * @param dependency the dependency to check
+ * @return {@code true} when a BOM manages the dependency's version;
+ * {@code false} otherwise
+ * @since 2.4.0
+ */
+ public boolean coversDependency(Dependency dependency) {
+ return bomVersions_.containsKey(managedKey(dependency));
+ }
+
/**
* Returns the map of versions that were imported from bills of
- * materials, where the key is the name of the dependency and the value
- * is the managed version.
+ * materials, where the key is the dependency identity composed of the
+ * group, artifact, type and classifier, and the value is the managed
+ * version.
*
* @return the map of BOM versions
* @since 2.4.0
diff --git a/src/main/java/rife/bld/operations/DependencyTreeOperation.java b/src/main/java/rife/bld/operations/DependencyTreeOperation.java
index dcd41ca..d7cd4bf 100644
--- a/src/main/java/rife/bld/operations/DependencyTreeOperation.java
+++ b/src/main/java/rife/bld/operations/DependencyTreeOperation.java
@@ -178,7 +178,7 @@ public class DependencyTreeOperation extends AbstractOperation {
}
/**
- * Part of the {@link #execute} operation, resolves the versions of
- * dependencies that were declared without one and that are not covered
- * by a BOM in the same scope, so that the published POM is always
- * complete.
+ * Part of the {@link #execute} operation, freezes the versions of
+ * dependencies that were declared without one, so that the published
+ * POM is always complete.
*
- * BOMs that were declared without a version are resolved to their
- * latest version too.
+ * Dependencies that are covered by a BOM that applies to their scope
+ * stay version-less, the dependency management imports of the POM
+ * supply their versions. They are frozen anyway when the flattened
+ * dependency management of the POM would supply a different version
+ * than the BOMs that apply to their scope, or when a
+ * {@code bld.override} property supplies their version, since the POM
+ * can't reflect either. Uncovered dependencies are frozen to their
+ * latest version.
+ *
+ * BOMs that were declared without a version are frozen too, to their
+ * {@code bld.override} version or to their latest version.
*
* @since 2.4.0
*/
protected void executeResolveVersionlessDependencies() {
+ // freeze the versions of the declared BOMs, a version from a
+ // bld.override property is used by the build itself and takes
+ // precedence, version-less BOMs resolve to their latest version
+ var base_resolution = new VersionResolution(properties());
+ for (var scope : List.of(Scope.compile, Scope.runtime, Scope.provided)) {
+ var scoped_dependencies = dependencies().get(scope);
+ if (scoped_dependencies == null || scoped_dependencies.boms().isEmpty()) {
+ continue;
+ }
+
+ var declared_boms = List.copyOf(scoped_dependencies.boms());
+ scoped_dependencies.boms().clear();
+ for (var bom : declared_boms) {
+ var version = base_resolution.overrideVersion(bom);
+ if (version.equals(VersionNumber.UNKNOWN)) {
+ version = new DependencyResolver(base_resolution, artifactRetriever(), dependencyRepositories(), bom).latestVersion();
+ }
+ if (!version.equals(bom.version())) {
+ bom = new Bom(bom.groupId(), bom.artifactId(), version);
+ }
+ scoped_dependencies.boms().add(bom);
+ }
+ }
+
+ // the published POM flattens all the BOM imports into a single
+ // dependency management list, compute the versions that this list
+ // supplies so that scoped differences can be frozen explicitly
+ var flattened_boms = new LinkedHashMap();
+ for (var scope : List.of(Scope.compile, Scope.runtime, Scope.provided)) {
+ var scoped_dependencies = dependencies().get(scope);
+ if (scoped_dependencies != null) {
+ for (var bom : scoped_dependencies.boms()) {
+ flattened_boms.putIfAbsent(bom.toArtifactString(), bom);
+ }
+ }
+ }
+ var flattened_resolution = new VersionResolution(properties(), artifactRetriever(), dependencyRepositories(), flattened_boms.values());
+
+ // freeze the versions of version-less dependencies that no
+ // applicable BOM covers
for (var scope : List.of(Scope.compile, Scope.runtime, Scope.provided)) {
var scoped_dependencies = dependencies().get(scope);
if (scoped_dependencies == null) {
continue;
}
- // resolve the versions of BOMs that were declared without one
- if (!scoped_dependencies.boms().isEmpty()) {
- var base_resolution = new VersionResolution(properties());
- var declared_boms = List.copyOf(scoped_dependencies.boms());
- scoped_dependencies.boms().clear();
- for (var bom : declared_boms) {
- if (bom.version().equals(VersionNumber.UNKNOWN)) {
- var latest = new DependencyResolver(base_resolution, artifactRetriever(), dependencyRepositories(), bom).latestVersion();
- bom = new Bom(bom.groupId(), bom.artifactId(), latest, bom.classifier());
- }
- scoped_dependencies.boms().add(bom);
- }
- }
-
- var resolution = new VersionResolution(properties(), artifactRetriever(), dependencyRepositories(), scoped_dependencies.boms());
+ var resolution = new VersionResolution(properties(), artifactRetriever(), dependencyRepositories(), dependencies().effectiveBoms(scope));
for (var dependency : List.copyOf(scoped_dependencies)) {
- if (dependency.version().equals(VersionNumber.UNKNOWN) &&
- !resolution.bomVersions().containsKey(dependency.toArtifactString())) {
- var latest = new DependencyResolver(resolution, artifactRetriever(), dependencyRepositories(), dependency).latestVersion();
- scoped_dependencies.add(new Dependency(dependency.groupId(), dependency.artifactId(), latest,
- dependency.classifier(), dependency.type(), dependency.exclusions(), dependency.parent()));
+ if (!dependency.version().equals(VersionNumber.UNKNOWN)) {
+ continue;
}
+ // a version from a bld.override property is used by the
+ // build itself, it's frozen into the POM even when a BOM
+ // covers the dependency since the import can't reflect it
+ var version = resolution.versionOverrides().get(dependency.toArtifactString());
+ if (version == null) {
+ if (resolution.coversDependency(dependency)) {
+ // the dependency can only stay version-less when the
+ // flattened dependency management of the POM supplies
+ // the same version as the BOMs that apply to its scope
+ var scoped_version = resolution.overrideVersion(dependency);
+ if (scoped_version.equals(flattened_resolution.overrideVersion(dependency))) {
+ continue;
+ }
+ version = scoped_version;
+ } else {
+ version = new DependencyResolver(resolution, artifactRetriever(), dependencyRepositories(), dependency).latestVersion();
+ }
+ }
+ scoped_dependencies.add(new Dependency(dependency.groupId(), dependency.artifactId(), version,
+ dependency.classifier(), dependency.type(), dependency.exclusions(), dependency.parent()));
}
}
}
diff --git a/src/main/java/rife/bld/operations/UpdatesOperation.java b/src/main/java/rife/bld/operations/UpdatesOperation.java
index 04c8ff5..aaf8f9a 100644
--- a/src/main/java/rife/bld/operations/UpdatesOperation.java
+++ b/src/main/java/rife/bld/operations/UpdatesOperation.java
@@ -42,16 +42,21 @@ public class UpdatesOperation extends AbstractOperation {
}
// dependencies that are declared without a version and that are
- // covered by a BOM in the same scope have their version governed
- // by that BOM, the BOM update itself carries their update signal
- var scope_resolution = new VersionResolution(properties(), artifactRetriever(), repositories(), scoped_dependencies.boms());
+ // covered by a BOM that applies to their scope have their version
+ // pinned by that BOM, the BOM update itself carries their update
+ // signal, unless a bld.override supplies the version and takes
+ // that control away from the BOM
+ var scope_resolution = new VersionResolution(properties(), artifactRetriever(), repositories(), dependencies_.effectiveBoms(entry.getKey()));
for (var dependency : scoped_dependencies) {
if (VersionNumber.UNKNOWN.equals(dependency.version()) &&
- scope_resolution.bomVersions().containsKey(dependency.toArtifactString())) {
+ scope_resolution.coversDependency(dependency) &&
+ !scope_resolution.versionOverrides().containsKey(dependency.toArtifactString())) {
continue;
}
scopes.add(entry.getKey());
- dependencies.add(dependency);
+ // an overridden version is the one the build uses, it's the
+ // baseline that update checks compare against
+ dependencies.add(scope_resolution.overrideDependency(dependency));
}
}
@@ -63,8 +68,7 @@ public class UpdatesOperation extends AbstractOperation {
var latest = latest_versions.get(i);
if (latest.compareTo(dependency.version()) > 0) {
if (dependency instanceof Bom) {
- result.scope(scopes.get(i)).include(new Bom(dependency.groupId(), dependency.artifactId(), latest,
- dependency.classifier()));
+ result.scope(scopes.get(i)).include(new Bom(dependency.groupId(), dependency.artifactId(), latest));
} else {
result.scope(scopes.get(i)).include(new Dependency(dependency.groupId(), dependency.artifactId(), latest,
dependency.classifier(), dependency.type()));
diff --git a/src/test/java/rife/bld/dependencies/TestBom.java b/src/test/java/rife/bld/dependencies/TestBom.java
index c7942c0..9f77f67 100644
--- a/src/test/java/rife/bld/dependencies/TestBom.java
+++ b/src/test/java/rife/bld/dependencies/TestBom.java
@@ -30,7 +30,8 @@ public class TestBom {
assertEquals(new Bom("com.example", "bom1", new VersionNumber(1, 2, 3)), Bom.parse("com.example:bom1:1.2.3"));
assertEquals(new Bom("com.example", "bom1", new VersionNumber(1, 2, 3)), Bom.parse("com.example:bom1:1.2.3@bom"));
assertEquals(new Bom("com.example", "bom1", new VersionNumber(1, 2, 3)), Bom.parse("com.example:bom1:1.2.3@pom"));
- assertEquals(new Bom("com.example", "bom1", new VersionNumber(1, 2, 3), "classifier1"), Bom.parse("com.example:bom1:1.2.3:classifier1"));
+ // BOMs can't have classifiers, POM imports don't support them
+ assertNull(Bom.parse("com.example:bom1:1.2.3:classifier1"));
assertNull(Bom.parse(null));
assertNull(Bom.parse(""));
assertNull(Bom.parse("not@a@bom"));
@@ -287,14 +288,20 @@ public class TestBom {
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"))
.include(new Dependency("com.example", "f"));
- scopes.scope(Scope.test)
- // no BOMs in this scope : it's not reported on
+ scopes.scope(Scope.standalone)
+ // no BOMs apply to this scope : it's not reported on
.include(new Dependency("com.example", "f"));
// only the dependency that the BOM doesn't cover is reported
assertEquals(List.of(new Dependency("com.example", "f")),
scopes.versionlessDependenciesWithoutBom(new HierarchicalProperties(), retriever, repositories));
+ // a bld.override that supplies the version prevents the report
+ var override_properties = new HierarchicalProperties();
+ override_properties.put(VersionResolution.PROPERTY_OVERRIDE_PREFIX, "com.example:f:2.2.0");
+ assertEquals(List.of(),
+ scopes.versionlessDependenciesWithoutBom(override_properties, retriever, repositories));
+
var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), retriever, repositories);
// covered by the BOM : the BOM version applies
@@ -312,7 +319,7 @@ public class TestBom {
}
@Test
- void testBomScopeIsolation() throws Exception {
+ void testBomScopeInheritance() throws Exception {
var server = createArtifactServer(Map.of(
"bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0")),
"a:1.4.0", pom("a", "1.4.0", ""),
@@ -324,22 +331,168 @@ public class TestBom {
scopes.scope(compile)
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"));
+ scopes.scope(Scope.provided)
+ .include(new Dependency("com.example", "a"));
scopes.scope(Scope.test)
.include(new Dependency("com.example", "a"));
+ scopes.scope(Scope.standalone)
+ .include(new Dependency("com.example", "a"));
- // the compile scope BOM applies to the compile scope resolution
- var compile_resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ var properties = new HierarchicalProperties();
+ var retriever = ArtifactRetriever.cachingInstance();
+ var repositories = serverRepositories(server);
+
+ // the compile scope BOM applies to the compile scope resolution,
+ // and follows the classpath composition into the provided and
+ // test scope resolutions
+ var compile_resolved = scopes.resolveCompileDependencies(properties, retriever, repositories);
assertEquals(Version.parse("1.4.0"), compile_resolved.get(new Dependency("com.example", "a")).version());
+ var provided_resolved = scopes.resolveProvidedDependencies(properties, retriever, repositories);
+ assertEquals(Version.parse("1.4.0"), provided_resolved.get(new Dependency("com.example", "a")).version());
+ var test_resolved = scopes.resolveTestDependencies(properties, retriever, repositories);
+ assertEquals(Version.parse("1.4.0"), test_resolved.get(new Dependency("com.example", "a")).version());
- // and doesn't leak into the test scope resolution, whose version
- // stays unknown and falls back to the latest from the metadata
- var test_resolved = scopes.resolveTestDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
- assertEquals(VersionNumber.UNKNOWN, test_resolved.get(new Dependency("com.example", "a")).version());
+ // the standalone scope is its own world, its version stays
+ // unknown and falls back to the latest from the metadata
+ var standalone_resolved = scopes.resolveStandaloneDependencies(properties, retriever, repositories);
+ assertEquals(VersionNumber.UNKNOWN, standalone_resolved.get(new Dependency("com.example", "a")).version());
} finally {
server.stop(0);
}
}
+ @Test
+ void testBomScopeInheritanceIsDirectional() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0")),
+ "a:1.4.0", pom("a", "1.4.0", ""),
+ "a:2.2.0", pom("a", "2.2.0", "")),
+ Map.of("a", metadata("a", "2.2.0", "1.4.0", "2.2.0")));
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(Scope.test)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+ scopes.scope(compile)
+ .include(new Dependency("com.example", "a"));
+
+ var properties = new HierarchicalProperties();
+ var retriever = ArtifactRetriever.cachingInstance();
+ var repositories = serverRepositories(server);
+
+ // a test scope BOM applies to the test scope resolution
+ var test_resolved = scopes.resolveTestDependencies(properties, retriever, repositories);
+ assertEquals(Version.parse("1.4.0"), test_resolved.get(new Dependency("com.example", "a")).version());
+
+ // but never reaches back into the compile scope resolution
+ var compile_resolved = scopes.resolveCompileDependencies(properties, retriever, repositories);
+ assertEquals(VersionNumber.UNKNOWN, compile_resolved.get(new Dependency("com.example", "a")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testBomPrecedenceAcrossScopes() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0")),
+ "bom2:1.0.0", bomPom("bom2", "1.0.0", managed("a", "2.2.0")),
+ "a:1.4.0", pom("a", "1.4.0", ""),
+ "a:2.2.0", pom("a", "2.2.0", "")),
+ Map.of("a", metadata("a", "2.2.0", "1.4.0", "2.2.0")));
+ 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)))
+ .include(new Dependency("com.example", "a"));
+
+ // when several applicable BOMs manage the same dependency, the
+ // first one in classpath composition order determines the version
+ 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());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testUncoveredReportingFollowsScopeComposition() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.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)
+ // covered through scope composition by the compile scope BOM
+ .include(new Dependency("com.example", "a"))
+ // not covered by any applicable BOM
+ .include(new Dependency("com.example", "g"));
+
+ assertEquals(List.of(new Dependency("com.example", "g")),
+ scopes.versionlessDependenciesWithoutBom(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server)));
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testBomIdentityIncludesTypeAndClassifier() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0",
+ managedTyped("a", "1.5.0", "test-jar", null) +
+ managedTyped("b", "1.6.0", "jar", "linux-x86_64") +
+ managed("b", "1.4.0"))),
+ Map.of());
+ server.start();
+ try {
+ var resolution = new VersionResolution(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server),
+ List.of(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0))));
+
+ // a test-jar entry doesn't cover the regular jar dependency
+ assertFalse(resolution.coversDependency(new Dependency("com.example", "a")));
+ // entries with the same identifiers but different classifiers
+ // are managed independently
+ assertEquals(Version.parse("1.4.0"), resolution.overrideDeclaredDependency(new Dependency("com.example", "b")).version());
+ assertEquals(Version.parse("1.6.0"), resolution.overrideDeclaredDependency(new Dependency("com.example", "b", null, "linux-x86_64")).version());
+ // the modular and forced-classpath JAR types match the plain
+ // jar entries of the BOM
+ assertEquals(Version.parse("1.4.0"), resolution.overrideDeclaredDependency(new Dependency("com.example", "b", null, null, Dependency.TYPE_MODULAR_JAR)).version());
+ assertEquals(Version.parse("1.4.0"), resolution.overrideDeclaredDependency(new Dependency("com.example", "b", null, null, Dependency.TYPE_CLASSPATH_JAR)).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testEffectiveBomsComposition() {
+ var compile_bom = new Bom("com.example", "compile-bom", new VersionNumber(1, 0, 0));
+ var runtime_bom = new Bom("com.example", "runtime-bom", new VersionNumber(2, 0, 0));
+ var provided_bom = new Bom("com.example", "provided-bom", new VersionNumber(3, 0, 0));
+ var test_bom = new Bom("com.example", "test-bom", new VersionNumber(4, 0, 0));
+ var standalone_bom = new Bom("com.example", "standalone-bom", new VersionNumber(5, 0, 0));
+
+ var scopes = new DependencyScopes();
+ scopes.scope(compile).include(compile_bom);
+ scopes.scope(Scope.runtime).include(runtime_bom);
+ scopes.scope(Scope.provided).include(provided_bom);
+ scopes.scope(Scope.test).include(test_bom);
+ scopes.scope(Scope.standalone).include(standalone_bom);
+
+ 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(standalone_bom), scopes.effectiveBoms(Scope.standalone));
+ }
+
@Test
void testMissingBomFailsLoudly() throws Exception {
var server = createArtifactServer(Map.of());
@@ -452,7 +605,7 @@ public class TestBom {
assertNotSame(base, resolution);
assertEquals(base.versionOverrides(), resolution.versionOverrides());
assertEquals(3, resolution.transferParallelism());
- assertEquals(Map.of("com.example:a", Version.parse("1.4.0")), resolution.bomVersions());
+ assertEquals(Map.of("com.example:a:jar:", Version.parse("1.4.0")), resolution.bomVersions());
// a declared dependency without version is filled in from the BOM
assertEquals(Version.parse("1.4.0"), resolution.overrideDeclaredDependency(new Dependency("com.example", "a")).version());
@@ -584,6 +737,42 @@ public class TestBom {
}
}
+ @Test
+ void testProjectUpdatesWithBomAndOverride() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0"))),
+ Map.of("bom1", metadata("bom1", "1.0.0", "1.0.0", "1.0.0"),
+ "a", metadata("a", "9.9.9", "1.4.0", "9.9.9")));
+ server.start();
+ var tmp = Files.createTempDirectory("bomupdatesoverride").toFile();
+ try {
+ var project = new BomProject(tmp, transferRepository(server));
+ project.properties().put(VersionResolution.PROPERTY_OVERRIDE_PREFIX, "com.example:a:1.4.0");
+ project.dependencies().scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+
+ var original_out = System.out;
+ var captured = new java.io.ByteArrayOutputStream();
+ System.setOut(new java.io.PrintStream(captured, true));
+ int status;
+ try {
+ status = project.execute(new String[]{"updates"});
+ } finally {
+ System.setOut(original_out);
+ }
+ var output = captured.toString();
+
+ assertEquals(0, status);
+ // the override takes the version control away from the BOM,
+ // the dependency is checked individually against the override
+ assertTrue(output.contains("com.example:a:9.9.9"), output);
+ } finally {
+ server.stop(0);
+ FileUtils.deleteDirectory(tmp);
+ }
+ }
+
static class BomProject extends rife.bld.Project {
BomProject(File tmp, Repository repository) {
workDirectory = tmp;
@@ -692,6 +881,10 @@ public class TestBom {
project.dependencies().scope(compile)
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"));
+ project.dependencies().scope(Scope.test)
+ // covered through scope composition by the compile scope BOM,
+ // must not be reported individually either
+ .include(new Dependency("com.example", "a"));
var original_out = System.out;
var captured = new java.io.ByteArrayOutputStream();
@@ -729,6 +922,9 @@ public class TestBom {
project.dependencies().scope(compile)
.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
.include(new Dependency("com.example", "a"));
+ project.dependencies().scope(Scope.test)
+ // covered through scope composition by the compile scope BOM
+ .include(new Dependency("com.example", "a"));
var original_out = System.out;
var captured = new java.io.ByteArrayOutputStream();
@@ -742,9 +938,13 @@ public class TestBom {
var output = captured.toString();
assertEquals(0, status);
- // the tree reflects the BOM version, not the latest version
+ // the trees reflect the BOM version, not the latest version,
+ // in the compile scope as well as in the test scope that the
+ // compile scope BOM also applies to
assertTrue(output.contains("com.example:a:1.4.0"), output);
assertFalse(output.contains("com.example:a:2.2.0"), output);
+ var test_tree = output.substring(output.indexOf("test:"));
+ assertTrue(test_tree.contains("com.example:a:1.4.0"), output);
} finally {
server.stop(0);
FileUtils.deleteDirectory(tmp);
@@ -925,6 +1125,13 @@ public class TestBom {
return dependency(artifact, version);
}
+ private static String managedTyped(String artifact, String version, String type, String classifier) {
+ return "com.example" + artifact + "" + version + "" +
+ "" + type + "" +
+ (classifier == null ? "" : "" + classifier + "") +
+ "";
+ }
+
private static String pom(String artifact, String version, String dependencies) {
return """
diff --git a/src/test/java/rife/bld/operations/TestPublishOperation.java b/src/test/java/rife/bld/operations/TestPublishOperation.java
index 5696d25..707a874 100644
--- a/src/test/java/rife/bld/operations/TestPublishOperation.java
+++ b/src/test/java/rife/bld/operations/TestPublishOperation.java
@@ -799,6 +799,182 @@ public class TestPublishOperation {
}
}
+ @Test
+ void testResolveVersionlessDependenciesWithOverrides()
+ throws Exception {
+ var poms = java.util.Map.of(
+ "bom1:1.0.0", """
+
+
+ 4.0.0
+ com.example
+ bom1
+ 1.0.0
+ pom
+
+
+ com.examplea1.4.0
+
+
+ """,
+ "bom2:2.5.0", """
+
+
+ 4.0.0
+ com.example
+ bom2
+ 2.5.0
+ pom
+
+
+ com.exampled4.0.0
+
+
+ """);
+ var metadata = java.util.Map.of(
+ "b", """
+
+
+ com.example
+ b
+
+ 2.2.0
+ 2.2.0
+ 2.2.0
+
+ """);
+ var server = com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress("localhost", 0), 0);
+ server.createContext("/", exchange -> {
+ var segments = exchange.getRequestURI().getPath().split("/");
+ var filename = segments[segments.length - 1];
+ String content = null;
+ if (filename.endsWith(".pom") && segments.length >= 3) {
+ content = poms.get(segments[segments.length - 3] + ":" + segments[segments.length - 2]);
+ } else if (filename.equals("maven-metadata.xml") && segments.length >= 2) {
+ content = metadata.get(segments[segments.length - 2]);
+ }
+ if (content == null) {
+ exchange.sendResponseHeaders(404, -1);
+ } else {
+ var body = content.getBytes();
+ exchange.sendResponseHeaders(200, body.length);
+ exchange.getResponseBody().write(body);
+ }
+ exchange.close();
+ });
+ server.start();
+ try {
+ var properties = new rife.ioc.HierarchicalProperties();
+ properties.put(VersionResolution.PROPERTY_OVERRIDE_PREFIX,
+ "com.example:a:1.9.9,com.example:b:1.1.1,com.example:bom2:2.5.0");
+
+ var scopes = new DependencyScopes();
+ scopes.scope(Scope.compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"))
+ .include(new Dependency("com.example", "b"));
+ scopes.scope(Scope.runtime)
+ .include(new Bom("com.example", "bom2"));
+
+ var operation = new PublishOperation()
+ .properties(properties)
+ .artifactRetriever(ArtifactRetriever.cachingInstance())
+ .dependencyRepositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/")))
+ .dependencies(scopes);
+ operation.executeResolveVersionlessDependencies();
+
+ var compile_scope = operation.dependencies().scope(Scope.compile);
+ // the override is what the build resolves, it's frozen into the
+ // POM even though the BOM covers the dependency
+ assertEquals(new VersionNumber(1, 9, 9), compile_scope.get(new Dependency("com.example", "a")).version());
+ // an uncovered dependency freezes to its override instead of
+ // the latest version
+ assertEquals(new VersionNumber(1, 1, 1), compile_scope.get(new Dependency("com.example", "b")).version());
+ // a version-less BOM freezes to its override
+ var runtime_boms = operation.dependencies().scope(Scope.runtime).boms();
+ assertEquals(new Bom("com.example", "bom2", new VersionNumber(2, 5, 0)), runtime_boms.iterator().next());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testResolveVersionlessDependenciesFreezesFlattenedConflicts()
+ throws Exception {
+ var poms = java.util.Map.of(
+ "bomr:1.0.0", """
+
+
+ 4.0.0
+ com.example
+ bomr
+ 1.0.0
+ pom
+
+
+ com.examplea1.0.0
+
+
+ """,
+ "bomp:1.0.0", """
+
+
+ 4.0.0
+ com.example
+ bomp
+ 1.0.0
+ pom
+
+
+ com.examplea2.0.0
+
+
+ """);
+ var server = com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress("localhost", 0), 0);
+ server.createContext("/", exchange -> {
+ var segments = exchange.getRequestURI().getPath().split("/");
+ var filename = segments[segments.length - 1];
+ String content = null;
+ if (filename.endsWith(".pom") && segments.length >= 3) {
+ content = poms.get(segments[segments.length - 3] + ":" + segments[segments.length - 2]);
+ }
+ if (content == null) {
+ exchange.sendResponseHeaders(404, -1);
+ } else {
+ var body = content.getBytes();
+ exchange.sendResponseHeaders(200, body.length);
+ exchange.getResponseBody().write(body);
+ }
+ exchange.close();
+ });
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(Scope.runtime)
+ .include(new Bom("com.example", "bomr", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+ scopes.scope(Scope.provided)
+ .include(new Bom("com.example", "bomp", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+
+ var operation = new PublishOperation()
+ .artifactRetriever(ArtifactRetriever.cachingInstance())
+ .dependencyRepositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/")))
+ .dependencies(scopes);
+ operation.executeResolveVersionlessDependencies();
+
+ // the flattened dependency management of the POM imports the
+ // runtime BOM first and would supply 1.0.0, the runtime scope
+ // agrees with that so its dependency stays version-less
+ assertEquals(VersionNumber.UNKNOWN, operation.dependencies().scope(Scope.runtime).get(new Dependency("com.example", "a")).version());
+ // the provided scope resolves 2.0.0 through its own BOM, that
+ // conflicts with the flattened list so the version is frozen
+ assertEquals(new VersionNumber(2, 0, 0), operation.dependencies().scope(Scope.provided).get(new Dependency("com.example", "a")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
@Test
void testResolveVersionlessDependenciesUntouchedScopes() {
// no network access is needed when nothing has to be resolved :
@@ -926,6 +1102,9 @@ public class TestPublishOperation {
.include(new Dependency("com.example", "c", new VersionNumber(5, 0, 0)));
scopes.scope(Scope.runtime)
.include(new Bom("com.example", "bom2"));
+ scopes.scope(Scope.provided)
+ // covered by the compile scope BOM through scope inheritance
+ .include(new Dependency("com.example", "a"));
var operation = new PublishOperation()
.artifactRetriever(ArtifactRetriever.cachingInstance())
@@ -944,6 +1123,9 @@ public class TestPublishOperation {
// a version-less BOM is resolved to its latest version
var runtime_boms = operation.dependencies().scope(Scope.runtime).boms();
assertEquals(new Bom("com.example", "bom2", new VersionNumber(3, 0, 0)), runtime_boms.iterator().next());
+ // the compile scope BOM also covers the provided scope,
+ // its version-less dependency stays version-less
+ assertEquals(VersionNumber.UNKNOWN, operation.dependencies().scope(Scope.provided).get(new Dependency("com.example", "a")).version());
} finally {
server.stop(0);
}