From f6aeaa090dfbac6c3d19a9418cb3e2bdfe6905f5 Mon Sep 17 00:00:00 2001 From: Geert Bevin Date: Tue, 14 Jul 2026 21:52:48 -0400 Subject: [PATCH] Emit dependency management in published POMs and freeze uncovered versions --- .../rife/bld/operations/PublishOperation.java | 74 ++++++ .../java/rife/bld/publish/PomBuilder.java | 29 +++ .../resources/templates/bld/pom_blueprint.xml | 18 ++ .../bld/operations/TestPublishOperation.java | 244 ++++++++++++++++++ .../java/rife/bld/publish/TestPomBuilder.java | 89 +++++++ 5 files changed, 454 insertions(+) diff --git a/src/main/java/rife/bld/operations/PublishOperation.java b/src/main/java/rife/bld/operations/PublishOperation.java index 5d19eed..4eb62d6 100644 --- a/src/main/java/rife/bld/operations/PublishOperation.java +++ b/src/main/java/rife/bld/operations/PublishOperation.java @@ -52,6 +52,7 @@ public class PublishOperation extends AbstractOperation { private ZonedDateTime moment_ = null; private final List repositories_ = new ArrayList<>(); + private final List dependencyRepositories_ = new ArrayList<>(); private final DependencyScopes dependencies_ = new DependencyScopes(); private PublishInfo info_ = new PublishInfo(); private PublishProperties publishProperties_ = new PublishProperties(); @@ -78,6 +79,7 @@ public class PublishOperation extends AbstractOperation { } executeValidateArtifacts(); + executeResolveVersionlessDependencies(); var actual_version = info().version(); @@ -103,6 +105,50 @@ public class PublishOperation 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. + *

+ * BOMs that were declared without a version are resolved to their + * latest version too. + * + * @since 2.4.0 + */ + protected void executeResolveVersionlessDependencies() { + 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()); + 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())); + } + } + } + } + /** * Part of the {@link #execute} operation, validates the publishing artifacts. * @@ -625,6 +671,7 @@ public class PublishOperation extends AbstractOperation { offline(project.offline()); properties(project.properties()); artifactRetriever(project.artifactRetriever()); + dependencyRepositories(project.repositories()); dependencies().include(project.dependencies()); artifacts(List.of( new PublishArtifact(new File(project.buildDistDirectory(), project.jarFileName()), "", TYPE_JAR), @@ -733,6 +780,33 @@ public class PublishOperation extends AbstractOperation { return this; } + /** + * Provides a list of repositories to resolve the dependencies against. + *

+ * These are used to resolve the versions of dependencies that were + * declared without one, they are not publication targets. + * + * @param repositories a list of repositories against which dependencies will be resolved + * @return this operation instance + * @since 2.4.0 + */ + public PublishOperation dependencyRepositories(List repositories) { + dependencyRepositories_.addAll(repositories); + return this; + } + + /** + * Retrieves the repositories that dependencies will be resolved against. + *

+ * This is a modifiable list that can be retrieved and changed. + * + * @return the repositories used for dependency resolution + * @since 2.4.0 + */ + public List dependencyRepositories() { + return dependencyRepositories_; + } + /** * Provides scoped dependencies to reference in the publication. * diff --git a/src/main/java/rife/bld/publish/PomBuilder.java b/src/main/java/rife/bld/publish/PomBuilder.java index fa2bceb..bfb0ac6 100644 --- a/src/main/java/rife/bld/publish/PomBuilder.java +++ b/src/main/java/rife/bld/publish/PomBuilder.java @@ -12,6 +12,8 @@ import rife.tools.StringUtils; import rife.tools.exceptions.FileUtilsErrorException; import java.io.File; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Objects; import static rife.bld.dependencies.Dependency.TYPE_JAR; @@ -152,6 +154,7 @@ public class PomBuilder { } if (dependencies() != null && !dependencies().isEmpty()) { + addDependencyManagement(t); addDependencies(t, Scope.compile); addDependencies(t, Scope.runtime); addDependencies(t, Scope.provided); @@ -172,6 +175,32 @@ public class PomBuilder { FileUtils.writeString(pomBuilder.build(), file); } + private void addDependencyManagement(Template t) { + var boms = new LinkedHashMap(); + for (var scope : List.of(Scope.compile, Scope.runtime, Scope.provided)) { + var scoped_dependencies = dependencies().get(scope); + if (scoped_dependencies == null) { + continue; + } + for (var bom : scoped_dependencies.boms()) { + if (!bom.version().equals(VersionNumber.UNKNOWN)) { + boms.putIfAbsent(bom.toArtifactString(), bom); + } + } + } + if (boms.isEmpty()) { + return; + } + + for (var bom : boms.values()) { + t.setValueEncoded("dependency-management-groupId", bom.groupId()); + t.setValueEncoded("dependency-management-artifactId", bom.artifactId()); + t.setValueEncoded("dependency-management-version", bom.version()); + t.appendBlock("dependency-management", "dependency-management-entry"); + } + t.setBlock("dependency-management-tag"); + } + private void addDependencies(Template t, Scope scope) { var scoped_dependencies = dependencies().scope(scope); if (!scoped_dependencies.isEmpty()) { diff --git a/src/main/resources/templates/bld/pom_blueprint.xml b/src/main/resources/templates/bld/pom_blueprint.xml index 154a90c..522c980 100644 --- a/src/main/resources/templates/bld/pom_blueprint.xml +++ b/src/main/resources/templates/bld/pom_blueprint.xml @@ -32,6 +32,24 @@ + + + + + + + + + + + pom + import + + + + + + diff --git a/src/test/java/rife/bld/operations/TestPublishOperation.java b/src/test/java/rife/bld/operations/TestPublishOperation.java index 8e6ec47..5696d25 100644 --- a/src/test/java/rife/bld/operations/TestPublishOperation.java +++ b/src/test/java/rife/bld/operations/TestPublishOperation.java @@ -11,9 +11,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; import rife.bld.Project; +import rife.bld.WebProject; import rife.bld.blueprints.AppProjectBlueprint; import rife.bld.dependencies.*; import rife.bld.publish.PublishArtifact; +import rife.bld.publish.PublishInfo; import rife.tools.FileUtils; import rife.tools.exceptions.FileUtilsErrorException; @@ -704,4 +706,246 @@ public class TestPublishOperation { FileUtils.deleteDirectory(tmp1); } } + + @Test + void testPublishLocalWithBom() + 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 + + + """); + 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(); + + var tmp_local = Files.createTempDirectory("bomlocal").toFile(); + var artifact_file = File.createTempFile("myapp", ".jar"); + try { + var operation = new PublishOperation() + .artifactRetriever(ArtifactRetriever.cachingInstance()) + .repositories(new Repository(tmp_local.getAbsolutePath())) + .dependencyRepositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/"))) + .info(new PublishInfo() + .groupId("test.pkg") + .artifactId("myapp") + .version(new VersionNumber(3, 3, 3))) + .artifacts(List.of(new PublishArtifact(artifact_file, "", "jar"))); + operation.dependencies().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")); + operation.execute(); + + var pom = FileUtils.readString(Path.of(tmp_local.getAbsolutePath(), "test", "pkg", "myapp", "3.3.3", "myapp-3.3.3.pom").toFile()); + + // the BOM is imported through dependency management + assertTrue(pom.contains(""), pom); + assertTrue(pom.contains("bom1"), pom); + assertTrue(pom.contains("1.0.0"), pom); + assertTrue(pom.contains("pom"), pom); + assertTrue(pom.contains("import"), pom); + + // the covered dependency stays version-less, + // the BOM import provides its version downstream + assertTrue(pom.contains("a"), pom); + assertFalse(java.util.regex.Pattern.compile("a\\s*").matcher(pom).find(), pom); + + // the uncovered dependency is frozen to its resolved version + assertTrue(java.util.regex.Pattern.compile("b\\s*2\\.2\\.0").matcher(pom).find(), pom); + } finally { + server.stop(0); + artifact_file.delete(); + FileUtils.deleteDirectory(tmp_local); + } + } + + @Test + void testResolveVersionlessDependenciesUntouchedScopes() { + // no network access is needed when nothing has to be resolved : + // explicitly versioned dependencies are untouched and the test + // scope isn't part of the publication + var scopes = new DependencyScopes(); + scopes.scope(Scope.compile) + .include(new Dependency("com.example", "c", new VersionNumber(5, 0, 0))); + scopes.scope(Scope.test) + .include(new Dependency("com.example", "t")); + + var operation = new PublishOperation() + .dependencies(scopes); + operation.executeResolveVersionlessDependencies(); + + assertEquals(new VersionNumber(5, 0, 0), operation.dependencies().scope(Scope.compile).get(new Dependency("com.example", "c")).version()); + assertEquals(VersionNumber.UNKNOWN, operation.dependencies().scope(Scope.test).get(new Dependency("com.example", "t")).version()); + } + + static class BomPublishProject extends WebProject { + BomPublishProject(File tmp) { + workDirectory = tmp; + pkg = "test.pkg"; + name = "myapp"; + version = new VersionNumber(0, 0, 1); + repositories = List.of(new Repository("https://example.com/resolve")); + } + } + + @Test + void testFromProjectDependencyRepositories() + throws Exception { + var tmp = Files.createTempDirectory("test").toFile(); + try { + var project = new BomPublishProject(tmp); + var operation = new PublishOperation() + .fromProject(project); + assertEquals(project.repositories(), operation.dependencyRepositories()); + } finally { + FileUtils.deleteDirectory(tmp); + } + } + + @Test + void testResolveVersionlessDependencies() + 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:3.0.0", """ + + + 4.0.0 + com.example + bom2 + 3.0.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 + + """, + "bom2", """ + + + com.example + bom2 + + 3.0.0 + 3.0.0 + 3.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]); + } 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 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")) + .include(new Dependency("com.example", "c", new VersionNumber(5, 0, 0))); + scopes.scope(Scope.runtime) + .include(new Bom("com.example", "bom2")); + + var operation = new PublishOperation() + .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); + // covered by the BOM : stays version-less for the POM, + // the dependency management import provides the version + assertEquals(VersionNumber.UNKNOWN, compile_scope.get(new Dependency("com.example", "a")).version()); + // not covered : resolved to its latest version + assertEquals(new VersionNumber(2, 2, 0), compile_scope.get(new Dependency("com.example", "b")).version()); + // explicitly versioned : untouched + assertEquals(new VersionNumber(5, 0, 0), compile_scope.get(new Dependency("com.example", "c")).version()); + // 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()); + } finally { + server.stop(0); + } + } } diff --git a/src/test/java/rife/bld/publish/TestPomBuilder.java b/src/test/java/rife/bld/publish/TestPomBuilder.java index 16a427e..1dd7994 100644 --- a/src/test/java/rife/bld/publish/TestPomBuilder.java +++ b/src/test/java/rife/bld/publish/TestPomBuilder.java @@ -652,4 +652,93 @@ public class TestPomBuilder { """, builder.build()); } + + @Test + void testDependencyManagementScopeBoundaries() { + var builder = new PomBuilder(); + builder.dependencies().scope(Scope.provided) + .include(new Bom("org.eclipse.jetty", "jetty-bom", new VersionNumber(12, 0, 16))); + builder.dependencies().scope(Scope.test) + // the test scope isn't published, its BOMs are not emitted + .include(new Bom("org.junit", "junit-bom", new VersionNumber(6, 1, 1))); + assertEquals(""" + + + 4.0.0 + + + + + + + + + + org.eclipse.jetty + jetty-bom + 12.0.16 + pom + import + + + + + + + """, builder.build()); + } + + @Test + void testDependencyManagement() { + var builder = new PomBuilder(); + builder.dependencies().scope(Scope.compile) + .include(new Bom("io.vertx", "vertx-stack-depchain", new VersionNumber(4, 5, 12))) + .include(new Dependency("io.vertx", "vertx-core")); + builder.dependencies().scope(Scope.runtime) + // duplicate BOMs are only emitted once + .include(new Bom("io.vertx", "vertx-stack-depchain", new VersionNumber(4, 5, 12))) + .include(new Bom("org.junit", "junit-bom", new VersionNumber(6, 1, 1))); + builder.dependencies().scope(Scope.provided) + // BOMs without a version are not emitted + .include(new Bom("com.example", "versionless-bom")); + assertEquals(""" + + + 4.0.0 + + + + + + + + + + io.vertx + vertx-stack-depchain + 4.5.12 + pom + import + + + org.junit + junit-bom + 6.1.1 + pom + import + + + + + + io.vertx + vertx-core + compile + + + + """, builder.build()); + } }