diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java index 5aba71386686..8a9e57e64602 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java @@ -21,9 +21,11 @@ import javax.inject.Inject; import javax.inject.Named; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.function.Function; import java.util.stream.Collectors; @@ -195,10 +197,98 @@ protected Model buildPom(RepositorySystemSession session, MavenProject project, protected Model buildBom(RepositorySystemSession session, MavenProject project, ModelSource src) throws ModelBuilderException { ModelBuilderResult result = buildModel(session, project, src); - Model model = result.getEffectiveModel(); + Model rawModel = result.getRawModel(); + Model effectiveModel = result.getEffectiveModel(); + // The raw model has no inheritance — only entries declared in this POM. + // The effective model has fully interpolated values but includes inherited entries. + // Filter the effective model's dependency management to only include entries + // explicitly declared in this BOM, using the effective model for resolved values. + Model model = filterToOwnDependencyManagement(rawModel, effectiveModel, project); return transformBom(model, project); } + /** + * Filters the effective model's dependency management to include only entries + * that were explicitly declared in this BOM's raw model, not inherited from the + * parent chain. For non-import entries, the effective model's fully resolved entry + * is used. For import-scoped entries (which are consumed/flattened in the effective + * model), the raw entry is preserved as a BOM reference with its version interpolated + * from the project's properties. + * + * @param rawModel the raw model (no inheritance, no interpolation) + * @param effectiveModel the effective model (inheritance + interpolation) + * @param project the Maven project (provides resolved properties) + * @return the effective model with dependency management filtered to own entries + */ + private Model filterToOwnDependencyManagement(Model rawModel, Model effectiveModel, MavenProject project) { + if (rawModel.getDependencyManagement() == null + || rawModel.getDependencyManagement().getDependencies().isEmpty()) { + // Nothing declared in this BOM — strip all inherited entries + return effectiveModel.withDependencyManagement(null); + } + + List declaredDeps = rawModel.getDependencyManagement().getDependencies(); + + // Build lookup from the effective model's resolved dependency management + Map effectiveLookup = new LinkedHashMap<>(); + if (effectiveModel.getDependencyManagement() != null) { + for (Dependency dep : effectiveModel.getDependencyManagement().getDependencies()) { + effectiveLookup.put(getDependencyKey(dep), dep); + } + } + + // For each declared entry, resolve it against the effective model + List resolvedDeps = new ArrayList<>(); + for (Dependency declared : declaredDeps) { + if ("import".equals(declared.getScope())) { + // BOM import entries are consumed (flattened) in the effective model, + // so they won't be in effectiveLookup. Preserve the import reference + // with its version resolved from project properties. + String resolvedVersion = interpolateVersion(declared.getVersion(), project); + resolvedDeps.add(declared.withVersion(resolvedVersion)); + } else { + // Regular entry: use the effective model's fully resolved entry + String key = getDependencyKey(declared); + Dependency resolved = effectiveLookup.get(key); + resolvedDeps.add(resolved != null ? resolved : declared); + } + } + + return effectiveModel.withDependencyManagement( + effectiveModel.getDependencyManagement() != null + ? effectiveModel.getDependencyManagement().withDependencies(resolvedDeps) + : org.apache.maven.api.model.DependencyManagement.newBuilder() + .dependencies(resolvedDeps) + .build()); + } + + /** + * Resolves property references ({@code ${...}}) in a version string using the + * Maven project's fully-resolved properties. Handles model properties, inherited + * properties, and CI-friendly properties ({@code ${revision}}, etc.). + */ + private static String interpolateVersion(String version, MavenProject project) { + if (version == null || !version.contains("${")) { + return version; + } + String result = version; + Properties props = project.getProperties(); + for (String name : props.stringPropertyNames()) { + String placeholder = "${" + name + "}"; + if (result.contains(placeholder)) { + result = result.replace(placeholder, props.getProperty(name)); + } + } + // Handle built-in project-coordinate properties + if (result.contains("${project.version}") && project.getVersion() != null) { + result = result.replace("${project.version}", project.getVersion()); + } + if (result.contains("${project.groupId}") && project.getGroupId() != null) { + result = result.replace("${project.groupId}", project.getGroupId()); + } + return result; + } + protected Model buildNonPom(RepositorySystemSession session, MavenProject project, ModelSource src) throws ModelBuilderException { Model model = buildEffectiveModel(session, project, src); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh12640BomInheritedDepMgmtTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh12640BomInheritedDepMgmtTest.java new file mode 100644 index 000000000000..18487dc02dad --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh12640BomInheritedDepMgmtTest.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.it; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that the BOM consumer POM contains only the dependency management + * entries explicitly declared in the BOM module, not entries inherited from the + * parent POM. + *

+ * Reproducer for #12640: + * when the parent POM defines dependency management entries (or imports BOMs), + * those entries were leaking into the BOM's consumer POM because + * {@code DefaultConsumerPomBuilder.buildBom()} used the effective model which + * includes inherited dependency management. + * + * @since 4.0.0 + */ +class MavenITgh12640BomInheritedDepMgmtTest extends AbstractMavenIntegrationTestCase { + + /** + * Verify that the BOM consumer POM contains only entries declared in the BOM, + * not entries inherited from the parent's dependency management. + *

+ * The parent defines {@code ext-lib-a} and {@code ext-lib-b} in its + * dependencyManagement. The BOM declares only {@code mod-1} and {@code mod-2}. + * The consumer POM must NOT contain {@code ext-lib-a} or {@code ext-lib-b}. + */ + @Test + void testBomConsumerPomExcludesInheritedDepMgmt() throws Exception { + Path basedir = extractResources("/gh-12640-bom-inherited-depmgmt"); + + Verifier verifier = newVerifier(basedir); + verifier.deleteArtifacts("org.apache.maven.its.gh12640"); + verifier.addCliArguments("install"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + // Read the consumer POM that was installed to the local repo + Path consumerPomPath = + verifier.getArtifactPath("org.apache.maven.its.gh12640", "bom", "1.0.0-SNAPSHOT", "pom"); + + assertTrue(Files.exists(consumerPomPath), "Consumer POM not found at " + consumerPomPath); + + String content = Files.readString(consumerPomPath); + + // 1. Packaging must be "pom" (not "bom") + assertTrue(content.contains("pom"), "Consumer POM packaging should be 'pom'"); + + // 2. Must contain the declared entries: mod-1 and mod-2 + assertTrue( + content.contains("mod-1"), + "Consumer POM must contain mod-1.\nActual:\n" + content); + assertTrue( + content.contains("mod-2"), + "Consumer POM must contain mod-2.\nActual:\n" + content); + + // 3. Must NOT contain inherited entries from the parent's dependency management + assertFalse( + content.contains("ext-lib-a"), + "Consumer POM must NOT contain parent's ext-lib-a.\nActual:\n" + content); + assertFalse( + content.contains("ext-lib-b"), + "Consumer POM must NOT contain parent's ext-lib-b.\nActual:\n" + content); + + // 4. Must have resolved versions (not raw property references) + assertFalse( + content.contains("${"), + "Consumer POM must not contain unresolved property references.\nActual:\n" + content); + + // 5. Versions must be inferred from the reactor (1.0.0-SNAPSHOT) + assertTrue( + content.contains("1.0.0-SNAPSHOT"), + "Consumer POM must contain resolved reactor version.\nActual:\n" + content); + } + + /** + * Same test with flatten enabled — the behaviour should be the same for BOMs. + */ + @Test + void testBomConsumerPomWithFlattenExcludesInheritedDepMgmt() throws Exception { + Path basedir = extractResources("/gh-12640-bom-inherited-depmgmt"); + + Verifier verifier = newVerifier(basedir); + verifier.deleteArtifacts("org.apache.maven.its.gh12640"); + verifier.addCliArguments("install", "-Dmaven.consumer.pom.flatten=true"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + Path consumerPomPath = + verifier.getArtifactPath("org.apache.maven.its.gh12640", "bom", "1.0.0-SNAPSHOT", "pom"); + + assertTrue(Files.exists(consumerPomPath), "Consumer POM not found at " + consumerPomPath); + + String content = Files.readString(consumerPomPath); + + // Must contain declared entries + assertTrue(content.contains("mod-1"), "Must contain mod-1"); + assertTrue(content.contains("mod-2"), "Must contain mod-2"); + + // Must NOT contain inherited entries + assertFalse( + content.contains("ext-lib-a"), + "Consumer POM must NOT contain parent's ext-lib-a.\nActual:\n" + content); + assertFalse( + content.contains("ext-lib-b"), + "Consumer POM must NOT contain parent's ext-lib-b.\nActual:\n" + content); + } +} diff --git a/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/bom/pom.xml b/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/bom/pom.xml new file mode 100644 index 000000000000..d54b882cdacd --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/bom/pom.xml @@ -0,0 +1,53 @@ + + + + 4.0.0 + + + org.apache.maven.its.gh12640 + parent + 1.0.0-SNAPSHOT + + + bom + bom + + GH-12640 :: BOM + + + + + + org.apache.maven.its.gh12640 + mod-1 + + + org.apache.maven.its.gh12640 + mod-2 + + + + diff --git a/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/mod-1/pom.xml b/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/mod-1/pom.xml new file mode 100644 index 000000000000..17ee3a466d1e --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/mod-1/pom.xml @@ -0,0 +1,35 @@ + + + + 4.0.0 + + + org.apache.maven.its.gh12640 + parent + 1.0.0-SNAPSHOT + + + mod-1 + jar + + Module 1 + diff --git a/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/mod-2/pom.xml b/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/mod-2/pom.xml new file mode 100644 index 000000000000..5d747b540ece --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/mod-2/pom.xml @@ -0,0 +1,35 @@ + + + + 4.0.0 + + + org.apache.maven.its.gh12640 + parent + 1.0.0-SNAPSHOT + + + mod-2 + jar + + Module 2 + diff --git a/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/pom.xml b/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/pom.xml new file mode 100644 index 000000000000..e4f9746d0328 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-12640-bom-inherited-depmgmt/pom.xml @@ -0,0 +1,60 @@ + + + + 4.0.0 + + org.apache.maven.its.gh12640 + parent + 1.0.0-SNAPSHOT + pom + + GH-12640: BOM inherited dependency management test + + + 2.0.0 + + + + + + + org.apache.maven.its.gh12640 + ext-lib-a + ${ext.lib.version} + + + org.apache.maven.its.gh12640 + ext-lib-b + ${ext.lib.version} + + + + + + mod-1 + mod-2 + bom + +