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 d631e8fd7e2c..6a22927970a7 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 @@ -48,7 +48,9 @@ import org.apache.maven.api.services.ModelBuilderResult; import org.apache.maven.api.services.ModelSource; import org.apache.maven.api.services.model.LifecycleBindingsInjector; +import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.impl.InternalSession; +import org.apache.maven.internal.impl.InternalMavenSession; import org.apache.maven.model.v4.MavenModelVersion; import org.apache.maven.project.MavenProject; import org.apache.maven.project.SourceQueries; @@ -161,14 +163,14 @@ public Model build(RepositorySystemSession session, MavenProject project, ModelS protected Model buildPom(RepositorySystemSession session, MavenProject project, ModelSource src) throws ModelBuilderException { - ModelBuilderResult result = buildModel(session, src); + ModelBuilderResult result = buildModel(session, project, src); Model model = result.getRawModel(); return transformPom(model, project); } protected Model buildBomWithoutFlatten(RepositorySystemSession session, MavenProject project, ModelSource src) throws ModelBuilderException { - ModelBuilderResult result = buildModel(session, src); + ModelBuilderResult result = buildModel(session, project, src); Model model = result.getRawModel(); // For BOMs without flattening, we just need to transform the packaging from "bom" to "pom" // but keep everything else from the raw model (including unresolved versions) @@ -177,20 +179,21 @@ protected Model buildBomWithoutFlatten(RepositorySystemSession session, MavenPro protected Model buildBom(RepositorySystemSession session, MavenProject project, ModelSource src) throws ModelBuilderException { - ModelBuilderResult result = buildModel(session, src); + ModelBuilderResult result = buildModel(session, project, src); Model model = result.getEffectiveModel(); return transformBom(model, project); } protected Model buildNonPom(RepositorySystemSession session, MavenProject project, ModelSource src) throws ModelBuilderException { - Model model = buildEffectiveModel(session, src); + Model model = buildEffectiveModel(session, project, src); return transformNonPom(model, project); } - private Model buildEffectiveModel(RepositorySystemSession session, ModelSource src) throws ModelBuilderException { + private Model buildEffectiveModel(RepositorySystemSession session, MavenProject project, ModelSource src) + throws ModelBuilderException { InternalSession iSession = InternalSession.from(session); - ModelBuilderResult result = buildModel(session, src); + ModelBuilderResult result = buildModel(session, project, src); Model model = result.getEffectiveModel(); if (model.getDependencyManagement() != null @@ -295,7 +298,7 @@ private static String getDependencyKey(Dependency dependency) { + (dependency.getClassifier() != null ? dependency.getClassifier() : ""); } - private ModelBuilderResult buildModel(RepositorySystemSession session, ModelSource src) + private ModelBuilderResult buildModel(RepositorySystemSession session, MavenProject project, ModelSource src) throws ModelBuilderException { InternalSession iSession = InternalSession.from(session); ModelBuilderRequest.ModelBuilderRequestBuilder request = ModelBuilderRequest.builder(); @@ -306,6 +309,38 @@ private ModelBuilderResult buildModel(RepositorySystemSession session, ModelSour request.systemProperties(session.getSystemProperties()); request.userProperties(session.getUserProperties()); request.lifecycleBindingsInjector(lifecycleBindingsInjector::injectLifecycleBindings); + // Pass remote repositories so that the model builder can resolve BOM imports + // from non-central repositories (e.g., repositories defined in settings.xml profiles). + // Prefer project repositories, but fall back to session repositories if the project's + // remote repository list is not populated (e.g., during install/deploy phases). + if (project != null + && project.getRemoteProjectRepositories() != null + && !project.getRemoteProjectRepositories().isEmpty()) { + request.repositories(project.getRemoteProjectRepositories().stream() + .map(iSession::getRemoteRepository) + .toList()); + } else { + request.repositories(iSession.getRemoteRepositories()); + } + // Pass profiles and active/inactive profile IDs from the execution request + // so that settings.xml profiles are applied during consumer POM model building. + if (iSession instanceof InternalMavenSession mavenSession) { + MavenExecutionRequest executionRequest = + mavenSession.getMavenSession().getRequest(); + if (executionRequest.getProfiles() != null) { + request.profiles(executionRequest.getProfiles().stream() + .map(org.apache.maven.model.Profile::getDelegate) + .toList()); + } + request.activeProfileIds(executionRequest.getActiveProfiles()); + request.inactiveProfileIds(executionRequest.getInactiveProfiles()); + } else { + LOGGER.debug( + "Session is not an InternalMavenSession ({}); settings.xml profiles will not be " + + "passed to the consumer POM model builder. BOM imports from repositories " + + "defined only in settings.xml profiles may fail to resolve.", + iSession.getClass().getName()); + } ModelBuilder.ModelBuilderSession mbSession = iSession.getData().get(SessionData.key(ModelBuilder.ModelBuilderSession.class)); return mbSession.build(request.build()); diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java index d9744cad5fcb..495600db4acf 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java @@ -48,7 +48,9 @@ import org.apache.maven.internal.impl.InternalMavenSession; import org.apache.maven.internal.transformation.AbstractRepositoryTestCase; import org.apache.maven.project.MavenProject; +import org.eclipse.aether.repository.RemoteRepository; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -198,4 +200,65 @@ void testScmInheritance() throws Exception { assertNull(transformed.getScm().getChildScmUrlInheritAppendPath()); assertNull(transformed.getScm().getChildScmDeveloperConnectionInheritAppendPath()); } + + /** + * Verifies that the consumer POM builder passes the project's remote repositories + * to the model builder request, so that BOM imports from non-central repositories + * (e.g. repositories defined in settings.xml profiles) can be resolved. + *

+ * Without the fix in {@code DefaultConsumerPomBuilder.buildModel()}, the + * {@code ModelBuilderRequest} is constructed without repositories, profiles, or + * active profile IDs. This causes the model builder to only see Maven Central + * when resolving BOM imports, leading to "Non-resolvable import POM" failures + * for artifacts hosted in private/corporate repositories. + */ + @Test + void testConsumerPomPassesProjectRepositoriesToModelBuilder() throws Exception { + setRootDirectory("trivial"); + Path file = Paths.get("src/test/resources/consumer/trivial/child/pom.xml"); + + MavenProject project = getEffectiveModel(file); + + // Add a custom remote repository to the project, simulating a repository + // injected from settings.xml profile (e.g. a corporate/private repository) + RemoteRepository customRepo = + new RemoteRepository.Builder("custom-repo", "default", "https://repo.example.com/maven2").build(); + project.getRemoteProjectRepositories().add(customRepo); + + // Spy on the ModelBuilderSession to capture the ModelBuilderRequest + ModelBuilder.ModelBuilderSession originalMbs = modelBuilder.newSession(); + ModelBuilder.ModelBuilderSession spyMbs = Mockito.spy(originalMbs); + InternalSession.from(session).getData().set(SessionData.key(ModelBuilder.ModelBuilderSession.class), spyMbs); + + // Build the consumer POM + builder.build(session, project, Sources.buildSource(file)); + + // Capture the ModelBuilderRequest passed to the ModelBuilderSession + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ModelBuilderRequest.class); + Mockito.verify(spyMbs, Mockito.atLeastOnce()).build(requestCaptor.capture()); + + // Find the BUILD_CONSUMER request (there may be multiple calls) + ModelBuilderRequest consumerRequest = requestCaptor.getAllValues().stream() + .filter(r -> r.getRequestType() == ModelBuilderRequest.RequestType.BUILD_CONSUMER) + .findFirst() + .orElse(null); + + assertNotNull(consumerRequest, "Expected a BUILD_CONSUMER request to be made"); + + // Verify that repositories were passed to the request. + // Without the fix, getRepositories() returns null because buildModel() never sets them. + assertNotNull( + consumerRequest.getRepositories(), + "Consumer POM model builder request should include repositories from the project. " + + "Without this, BOM imports from non-central repositories (e.g. settings.xml profiles) " + + "cannot be resolved, causing 'Non-resolvable import POM' errors."); + assertFalse( + consumerRequest.getRepositories().isEmpty(), + "Consumer POM model builder request should have at least one repository"); + + // Verify the custom repository is included + boolean hasCustomRepo = + consumerRequest.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())); + assertTrue(hasCustomRepo, "Consumer POM model builder request should include the project's custom repository"); + } } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java index 60a49fcc2e96..b42eb3cf6b78 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java @@ -277,6 +277,14 @@ protected class ModelBuilderSessionState implements ModelProblemCollector { List externalRepositories; List repositories; + List getRepositories() { + return repositories; + } + + List getExternalRepositories() { + return externalRepositories; + } + // Cycle detection chain shared across all derived sessions // Contains both GAV coordinates (groupId:artifactId:version) and file paths final Set parentChain; @@ -345,6 +353,23 @@ ModelBuilderSessionState derive(ModelBuilderRequest request, DefaultModelBuilder } // Create a new parentChain for each derived session to prevent cycle detection issues // The parentChain now contains both GAV coordinates and file paths + // For BUILD_CONSUMER requests, use the request's explicit repositories so that + // BOM imports can be resolved from non-central repos (e.g., settings.xml profiles). + // This is scoped to BUILD_CONSUMER to avoid unintended side effects on other + // derived sessions (e.g., parent POM resolution during project builds). + List derivedExtRepos = externalRepositories; + List derivedRepos = repositories; + if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_CONSUMER + && request.getRepositories() != null + && !request.getRepositories().isEmpty()) { + derivedExtRepos = List.copyOf(request.getRepositories()); + if (pomRepositories.isEmpty()) { + derivedRepos = derivedExtRepos; + } else { + RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class); + derivedRepos = repositoryFactory.aggregate(session, pomRepositories, derivedExtRepos, false); + } + } return new ModelBuilderSessionState( session, request, @@ -352,8 +377,8 @@ ModelBuilderSessionState derive(ModelBuilderRequest request, DefaultModelBuilder dag, mappedSources, pomRepositories, - externalRepositories, - repositories, + derivedExtRepos, + derivedRepos, new LinkedHashSet<>()); } diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java index 5f6146fc2c26..aea6f370dae6 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java @@ -41,6 +41,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * @@ -254,6 +255,56 @@ public void testMissingDependencyGroupIdInference() throws Exception { } } + /** + * Verifies that when a BUILD_CONSUMER derived session is created with explicit + * repositories, those repositories are propagated to the derived session's + * {@code repositories} and {@code externalRepositories}. + *

+ * This is critical for consumer POM building: the consumer POM builder reuses the + * existing {@code ModelBuilderSession} and calls {@code build()} with a request + * containing the project's repositories (which may include non-central repos from + * settings.xml profiles). Without this, BOM imports from non-central repositories fail. + */ + @Test + public void testBuildConsumerWithExplicitRepositories() { + // First build to create the mainSession (simulates project build phase) + ModelBuilderRequest firstRequest = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("simple-standalone"))) + .build(); + ModelBuilder.ModelBuilderSession mbs = builder.newSession(); + mbs.build(firstRequest); + + // Access the mainSession (package-private) to call derive() and verify state + DefaultModelBuilder.ModelBuilderSessionState mainState = + ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession; + + // Verify the main session only has central + assertEquals(1, mainState.getRepositories().size()); + assertEquals("central", mainState.getRepositories().get(0).getId()); + + // Derive a BUILD_CONSUMER session with explicit repositories + RemoteRepository customRepo = session.createRemoteRepository("custom-repo", "https://repo.example.com/maven2"); + ModelBuilderRequest consumerRequest = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) + .source(Sources.buildSource(getPom("simple-standalone"))) + .repositories(List.of( + customRepo, session.createRemoteRepository("central", "https://repo.maven.apache.org/maven2"))) + .build(); + + DefaultModelBuilder.ModelBuilderSessionState derived = mainState.derive(consumerRequest); + + // Verify the derived session includes the custom repository + assertTrue( + derived.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())), + "Derived session repositories should include the custom repo from the request"); + assertTrue( + derived.getExternalRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())), + "Derived session externalRepositories should include the custom repo from the request"); + } + private Path getPom(String name) { return Paths.get("src/test/resources/poms/factory/" + name + ".xml").toAbsolutePath(); } diff --git a/impl/maven-impl/src/test/resources/poms/factory/simple-standalone.xml b/impl/maven-impl/src/test/resources/poms/factory/simple-standalone.xml new file mode 100644 index 000000000000..1ccfe631c16a --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/simple-standalone.xml @@ -0,0 +1,22 @@ + + + + org.apache.maven.tests + simple-standalone + 1.0 + diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITConsumerPomBomFromSettingsRepoTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITConsumerPomBomFromSettingsRepoTest.java new file mode 100644 index 000000000000..22f913910967 --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITConsumerPomBomFromSettingsRepoTest.java @@ -0,0 +1,115 @@ +/* + * 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.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.maven.api.model.Model; +import org.apache.maven.model.v4.MavenStaxReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that the consumer POM builder can resolve BOM imports from repositories + * defined only in settings.xml profiles (not in the project POM itself). + *

+ * This is a regression test for a bug where {@code DefaultConsumerPomBuilder.buildModel()} + * did not pass repositories, profiles, or active profile IDs to the {@code ModelBuilderRequest}, + * and {@code DefaultModelBuilder.derive()} ignored the request's repositories when creating + * derived sessions. This caused "Non-resolvable import POM" failures during the install phase + * for artifacts hosted in private/corporate repositories configured via settings.xml. + * + * @since 4.0.0 + */ +class MavenITConsumerPomBomFromSettingsRepoTest extends AbstractMavenIntegrationTestCase { + + /** + * Verifies that consumer POM flattening works when the BOM is only available + * from a repository defined in a settings.xml profile. + *

+ * Without the fix, this test fails with: + *

+     * Non-resolvable import POM: Could not find artifact
+     * org.apache.maven.its.cpbom:the-bom:pom:1.0 in central
+     * 
+ */ + @Test + void testConsumerPomWithBomFromSettingsProfileRepo() throws Exception { + Path basedir = extractResources("/gh-11767-consumer-pom-bom-from-settings-repo") + .toPath() + .toAbsolutePath(); + + Verifier verifier = newVerifier(basedir.toString()); + verifier.deleteArtifacts("org.apache.maven.its.cpbom"); + + // Apply settings template with the custom repository URL + verifier.filterFile("settings-template.xml", "settings.xml"); + verifier.addCliArgument("--settings"); + verifier.addCliArgument("settings.xml"); + + // Enable consumer POM flattening to trigger full BOM resolution + // during the install phase consumer POM transformation + verifier.addCliArgument("-Dmaven.consumer.pom.flatten=true"); + verifier.addCliArgument("install"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + // Verify the consumer POM was generated + Path consumerPom = basedir.resolve(Path.of( + "target", + "project-local-repo", + "org.apache.maven.its.cpbom", + "consumer-pom-bom-settings-repo", + "1.0", + "consumer-pom-bom-settings-repo-1.0-consumer.pom")); + assertTrue(Files.exists(consumerPom), "consumer POM not found at " + consumerPom); + + // Read and validate the consumer POM content + Model consumerPomModel; + try (Reader r = Files.newBufferedReader(consumerPom)) { + consumerPomModel = new MavenStaxReader().read(r); + } + + // The consumer POM should have the dependency with the version resolved from the BOM + assertNotNull(consumerPomModel.getDependencies(), "Consumer POM should have dependencies"); + assertFalse(consumerPomModel.getDependencies().isEmpty(), "Consumer POM should have at least one dependency"); + + boolean hasLibA = consumerPomModel.getDependencies().stream() + .anyMatch(d -> "lib-a".equals(d.getArtifactId()) + && "org.apache.maven.its.cpbom".equals(d.getGroupId()) + && "2.0".equals(d.getVersion())); + assertTrue( + hasLibA, + "Consumer POM should contain lib-a with version 2.0 resolved from the BOM. " + + "Actual dependencies: " + consumerPomModel.getDependencies()); + + // The BOM import should NOT appear in the consumer POM (it's been flattened) + if (consumerPomModel.getDependencyManagement() != null) { + boolean hasBomImport = consumerPomModel.getDependencyManagement().getDependencies().stream() + .anyMatch(d -> "the-bom".equals(d.getArtifactId()) && "import".equals(d.getScope())); + assertFalse(hasBomImport, "Consumer POM should not contain the BOM import after flattening"); + } + } +} diff --git a/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/pom.xml b/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/pom.xml new file mode 100644 index 000000000000..4f92898f82c3 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/pom.xml @@ -0,0 +1,62 @@ + + + + 4.0.0 + + org.apache.maven.its.cpbom + consumer-pom-bom-settings-repo + 1.0 + jar + + Maven Integration Test :: Consumer POM BOM from Settings Repo + Verifies that the consumer POM builder can resolve BOM imports from + repositories defined only in settings.xml profiles (not in the project POM). + + + + + org.apache.maven.its.cpbom + the-bom + 1.0 + pom + import + + + + + + + org.apache.maven.its.cpbom + lib-a + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + + + diff --git a/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/repo/org/apache/maven/its/cpbom/lib-a/2.0/lib-a-2.0.jar b/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/repo/org/apache/maven/its/cpbom/lib-a/2.0/lib-a-2.0.jar new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/repo/org/apache/maven/its/cpbom/lib-a/2.0/lib-a-2.0.pom b/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/repo/org/apache/maven/its/cpbom/lib-a/2.0/lib-a-2.0.pom new file mode 100644 index 000000000000..3151f79f1191 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/repo/org/apache/maven/its/cpbom/lib-a/2.0/lib-a-2.0.pom @@ -0,0 +1,9 @@ + + + 4.0.0 + + org.apache.maven.its.cpbom + lib-a + 2.0 + jar + diff --git a/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/repo/org/apache/maven/its/cpbom/the-bom/1.0/the-bom-1.0.pom b/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/repo/org/apache/maven/its/cpbom/the-bom/1.0/the-bom-1.0.pom new file mode 100644 index 000000000000..e1ed38ccc002 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/repo/org/apache/maven/its/cpbom/the-bom/1.0/the-bom-1.0.pom @@ -0,0 +1,19 @@ + + + 4.0.0 + + org.apache.maven.its.cpbom + the-bom + 1.0 + pom + + + + + org.apache.maven.its.cpbom + lib-a + 2.0 + + + + diff --git a/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/settings-template.xml b/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/settings-template.xml new file mode 100644 index 000000000000..e8a6f6d85a74 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/settings-template.xml @@ -0,0 +1,41 @@ + + + + + + custom-repo + + + custom-repo + @baseurl@/repo + + ignore + + + false + + + + + + + custom-repo + +