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); }