From fc74ad435964203e5c2bf133bbbbb4f6d61bc124 Mon Sep 17 00:00:00 2001 From: David Tavoularis Date: Mon, 9 Mar 2026 08:46:40 +0100 Subject: [PATCH 1/2] [#11767] Fix consumer POM builder failing to resolve BOM imports from settings.xml profile repositories DefaultConsumerPomBuilder.buildModel() now passes repositories, profiles, and active profile IDs to the ModelBuilderRequest so that BOM imports from non-central repositories (e.g. defined in settings.xml profiles) can be resolved during consumer POM transformation. DefaultModelBuilder.derive() now respects the request's repositories instead of always reusing the parent session's, and caches the RepositoryFactory lookup. Includes unit tests for both fixes and an integration test (gh-11767). Co-Authored-By: Claude Opus 4.6 --- .../impl/DefaultConsumerPomBuilder.java | 43 +++++-- .../impl/ConsumerPomBuilderTest.java | 63 ++++++++++ .../maven/impl/model/DefaultModelBuilder.java | 28 ++++- .../impl/model/DefaultModelBuilderTest.java | 68 +++++++++++ ...nITConsumerPomBomFromSettingsRepoTest.java | 115 ++++++++++++++++++ .../pom.xml | 62 ++++++++++ .../maven/its/cpbom/lib-a/2.0/lib-a-2.0.jar | 0 .../maven/its/cpbom/lib-a/2.0/lib-a-2.0.pom | 9 ++ .../its/cpbom/the-bom/1.0/the-bom-1.0.pom | 19 +++ .../settings-template.xml | 41 +++++++ 10 files changed, 438 insertions(+), 10 deletions(-) create mode 100644 its/core-it-suite/src/test/java/org/apache/maven/it/MavenITConsumerPomBomFromSettingsRepoTest.java create mode 100644 its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/pom.xml create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 its/core-it-suite/src/test/resources/gh-11767-consumer-pom-bom-from-settings-repo/settings-template.xml 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..08808fa8a9c6 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,32 @@ 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()); + } 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..9870c2180d18 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,15 @@ protected class ModelBuilderSessionState implements ModelProblemCollector { List externalRepositories; List repositories; + private RepositoryFactory repositoryFactory; + + RepositoryFactory getRepositoryFactory() { + if (repositoryFactory == null) { + repositoryFactory = session.getService(RepositoryFactory.class); + } + return repositoryFactory; + } + // Cycle detection chain shared across all derived sessions // Contains both GAV coordinates (groupId:artifactId:version) and file paths final Set parentChain; @@ -345,6 +354,19 @@ 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 + // If the derived request specifies explicit repositories, use them as external + // repositories so that BOM imports can be resolved from non-central repos + // (e.g., repositories defined in settings.xml profiles). + List derivedExtRepos = externalRepositories; + List derivedRepos = repositories; + if (request.getRepositories() != null && !request.getRepositories().isEmpty()) { + derivedExtRepos = List.copyOf(request.getRepositories()); + if (pomRepositories.isEmpty()) { + derivedRepos = derivedExtRepos; + } else { + derivedRepos = getRepositoryFactory().aggregate(session, pomRepositories, derivedExtRepos, false); + } + } return new ModelBuilderSessionState( session, request, @@ -352,8 +374,8 @@ ModelBuilderSessionState derive(ModelBuilderRequest request, DefaultModelBuilder dag, mappedSources, pomRepositories, - externalRepositories, - repositories, + derivedExtRepos, + derivedRepos, new LinkedHashSet<>()); } @@ -569,7 +591,7 @@ public void mergeRepositories(Model model, boolean replace) { repos = repos.stream().filter(r -> !ids.contains(r.getId())).toList(); } - RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class); + RepositoryFactory repositoryFactory = getRepositoryFactory(); if (request.getRepositoryMerging() == ModelBuilderRequest.RepositoryMerging.REQUEST_DOMINANT) { repositories = repositoryFactory.aggregate(session, repositories, repos, true); pomRepositories = repositories; 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..0a44f0e3aacd 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,73 @@ public void testMissingDependencyGroupIdInference() throws Exception { } } + /** + * Verifies that when a derived session is created via {@code build()} with a request + * that has explicit repositories, those repositories are used in the derived session + * instead of the parent session's repositories. + *

+ * This is critical for consumer POM building: the consumer POM builder reuses the + * existing {@code ModelBuilderSession} (created during the project build phase) and + * calls {@code build()} with a new request containing the project's repositories + * (which may include non-central repos from settings.xml profiles). Without this fix, + * the {@code derive()} method ignores the request's repositories and reuses the parent + * session's, causing BOM imports from non-central repositories to fail. + */ + @Test + public void testDeriveSessionUsesRequestRepositories() throws Exception { + // First build to create the mainSession + ModelBuilderRequest firstRequest = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("props-and-profiles"))) + .build(); + ModelBuilder.ModelBuilderSession mbs = builder.newSession(); + mbs.build(firstRequest); + + // Get the mainSession via reflection + Field mainSessionField = DefaultModelBuilder.ModelBuilderSessionImpl.class.getDeclaredField("mainSession"); + mainSessionField.setAccessible(true); + DefaultModelBuilder.ModelBuilderSessionState mainState = + (DefaultModelBuilder.ModelBuilderSessionState) mainSessionField.get(mbs); + + // Verify the main session only has central + Field repositoriesField = DefaultModelBuilder.ModelBuilderSessionState.class.getDeclaredField("repositories"); + repositoriesField.setAccessible(true); + List mainRepos = (List) repositoriesField.get(mainState); + assertEquals(1, mainRepos.size()); + assertEquals("central", mainRepos.get(0).getId()); + + // Now build again with explicit repositories (simulating consumer POM build) + RemoteRepository customRepo = session.createRemoteRepository("custom-repo", "https://repo.example.com/maven2"); + ModelBuilderRequest secondRequest = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) + .source(Sources.buildSource(getPom("props-and-profiles"))) + .repositories(List.of( + customRepo, session.createRemoteRepository("central", "https://repo.maven.apache.org/maven2"))) + .build(); + + // The derive() method should use the request's repositories + DefaultModelBuilder.ModelBuilderSessionState derived = mainState.derive(secondRequest); + List derivedRepos = (List) repositoriesField.get(derived); + + // Verify the derived session has the custom repository + boolean hasCustomRepo = derivedRepos.stream().anyMatch(r -> "custom-repo".equals(r.getId())); + assertTrue( + hasCustomRepo, + "Derived session should include repositories from the request, not just the parent session's repositories. " + + "Without this, consumer POM building cannot resolve BOM imports from non-central repositories."); + + // Also verify externalRepositories are updated + Field extReposField = + DefaultModelBuilder.ModelBuilderSessionState.class.getDeclaredField("externalRepositories"); + extReposField.setAccessible(true); + List extRepos = (List) extReposField.get(derived); + boolean extHasCustomRepo = extRepos.stream().anyMatch(r -> "custom-repo".equals(r.getId())); + assertTrue( + extHasCustomRepo, "Derived session's externalRepositories should include the request's repositories."); + } + private Path getPom(String name) { return Paths.get("src/test/resources/poms/factory/" + name + ".xml").toAbsolutePath(); } 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 + + From 4d5cd9735bb8e2a90912e417a1ac208961976b96 Mon Sep 17 00:00:00 2001 From: David Tavoularis Date: Mon, 23 Mar 2026 10:55:56 +0100 Subject: [PATCH 2/2] [#11767] Address review feedback: scope derive() to BUILD_CONSUMER, remove reflection - Scope repository override in derive() to BUILD_CONSUMER requests only, preventing unintended side effects on parent POM resolution - Remove repositoryFactory caching field, inline session.getService() calls - Add LOGGER.debug for non-InternalMavenSession to document the limitation - Replace reflection-based test with package-private accessors on ModelBuilderSessionState (getRepositories/getExternalRepositories) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../impl/DefaultConsumerPomBuilder.java | 6 ++ .../maven/impl/model/DefaultModelBuilder.java | 27 ++++---- .../impl/model/DefaultModelBuilderTest.java | 61 +++++++------------ .../poms/factory/simple-standalone.xml | 22 +++++++ 4 files changed, 65 insertions(+), 51 deletions(-) create mode 100644 impl/maven-impl/src/test/resources/poms/factory/simple-standalone.xml 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 08808fa8a9c6..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 @@ -334,6 +334,12 @@ private ModelBuilderResult buildModel(RepositorySystemSession session, MavenProj } 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)); 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 9870c2180d18..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,13 +277,12 @@ protected class ModelBuilderSessionState implements ModelProblemCollector { List externalRepositories; List repositories; - private RepositoryFactory repositoryFactory; + List getRepositories() { + return repositories; + } - RepositoryFactory getRepositoryFactory() { - if (repositoryFactory == null) { - repositoryFactory = session.getService(RepositoryFactory.class); - } - return repositoryFactory; + List getExternalRepositories() { + return externalRepositories; } // Cycle detection chain shared across all derived sessions @@ -354,17 +353,21 @@ 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 - // If the derived request specifies explicit repositories, use them as external - // repositories so that BOM imports can be resolved from non-central repos - // (e.g., repositories defined in settings.xml profiles). + // 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.getRepositories() != null && !request.getRepositories().isEmpty()) { + if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_CONSUMER + && request.getRepositories() != null + && !request.getRepositories().isEmpty()) { derivedExtRepos = List.copyOf(request.getRepositories()); if (pomRepositories.isEmpty()) { derivedRepos = derivedExtRepos; } else { - derivedRepos = getRepositoryFactory().aggregate(session, pomRepositories, derivedExtRepos, false); + RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class); + derivedRepos = repositoryFactory.aggregate(session, pomRepositories, derivedExtRepos, false); } } return new ModelBuilderSessionState( @@ -591,7 +594,7 @@ public void mergeRepositories(Model model, boolean replace) { repos = repos.stream().filter(r -> !ids.contains(r.getId())).toList(); } - RepositoryFactory repositoryFactory = getRepositoryFactory(); + RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class); if (request.getRepositoryMerging() == ModelBuilderRequest.RepositoryMerging.REQUEST_DOMINANT) { repositories = repositoryFactory.aggregate(session, repositories, repos, true); pomRepositories = repositories; 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 0a44f0e3aacd..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 @@ -256,70 +256,53 @@ public void testMissingDependencyGroupIdInference() throws Exception { } /** - * Verifies that when a derived session is created via {@code build()} with a request - * that has explicit repositories, those repositories are used in the derived session - * instead of the parent session's repositories. + * 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} (created during the project build phase) and - * calls {@code build()} with a new request containing the project's repositories - * (which may include non-central repos from settings.xml profiles). Without this fix, - * the {@code derive()} method ignores the request's repositories and reuses the parent - * session's, causing BOM imports from non-central repositories to fail. + * 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 testDeriveSessionUsesRequestRepositories() throws Exception { - // First build to create the mainSession + 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("props-and-profiles"))) + .source(Sources.buildSource(getPom("simple-standalone"))) .build(); ModelBuilder.ModelBuilderSession mbs = builder.newSession(); mbs.build(firstRequest); - // Get the mainSession via reflection - Field mainSessionField = DefaultModelBuilder.ModelBuilderSessionImpl.class.getDeclaredField("mainSession"); - mainSessionField.setAccessible(true); + // Access the mainSession (package-private) to call derive() and verify state DefaultModelBuilder.ModelBuilderSessionState mainState = - (DefaultModelBuilder.ModelBuilderSessionState) mainSessionField.get(mbs); + ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession; // Verify the main session only has central - Field repositoriesField = DefaultModelBuilder.ModelBuilderSessionState.class.getDeclaredField("repositories"); - repositoriesField.setAccessible(true); - List mainRepos = (List) repositoriesField.get(mainState); - assertEquals(1, mainRepos.size()); - assertEquals("central", mainRepos.get(0).getId()); + assertEquals(1, mainState.getRepositories().size()); + assertEquals("central", mainState.getRepositories().get(0).getId()); - // Now build again with explicit repositories (simulating consumer POM build) + // Derive a BUILD_CONSUMER session with explicit repositories RemoteRepository customRepo = session.createRemoteRepository("custom-repo", "https://repo.example.com/maven2"); - ModelBuilderRequest secondRequest = ModelBuilderRequest.builder() + ModelBuilderRequest consumerRequest = ModelBuilderRequest.builder() .session(session) .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) - .source(Sources.buildSource(getPom("props-and-profiles"))) + .source(Sources.buildSource(getPom("simple-standalone"))) .repositories(List.of( customRepo, session.createRemoteRepository("central", "https://repo.maven.apache.org/maven2"))) .build(); - // The derive() method should use the request's repositories - DefaultModelBuilder.ModelBuilderSessionState derived = mainState.derive(secondRequest); - List derivedRepos = (List) repositoriesField.get(derived); + DefaultModelBuilder.ModelBuilderSessionState derived = mainState.derive(consumerRequest); - // Verify the derived session has the custom repository - boolean hasCustomRepo = derivedRepos.stream().anyMatch(r -> "custom-repo".equals(r.getId())); + // Verify the derived session includes the custom repository assertTrue( - hasCustomRepo, - "Derived session should include repositories from the request, not just the parent session's repositories. " - + "Without this, consumer POM building cannot resolve BOM imports from non-central repositories."); - - // Also verify externalRepositories are updated - Field extReposField = - DefaultModelBuilder.ModelBuilderSessionState.class.getDeclaredField("externalRepositories"); - extReposField.setAccessible(true); - List extRepos = (List) extReposField.get(derived); - boolean extHasCustomRepo = extRepos.stream().anyMatch(r -> "custom-repo".equals(r.getId())); + derived.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())), + "Derived session repositories should include the custom repo from the request"); assertTrue( - extHasCustomRepo, "Derived session's externalRepositories should include the request's repositories."); + 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) { 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 +