You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/08/31 01:56:27 UTC
(maven) 01/01: Merge branch 'master' into mng-8287-consumer-pom-dependencies
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch mng-8287-consumer-pom-dependencies
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 5207aba8e7bdeb020b56688b75c814ffdc9e3df4
Merge: 0319d8928a 833a64b91e
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Aug 31 03:56:15 2026 +0200
Merge branch 'master' into mng-8287-consumer-pom-dependencies
Resolve merge conflicts between packaging profile inlining (MNG-8287)
and executable() condition stripping (GH-12570). Both features are
independent and coexist: stripPackagingActivation now delegates to
the shared isActivationEmpty() method introduced by master.
Add missing test resource packaging-bom-profiles/pom.xml for the
testBomPackagingActivatedProfilesArePreserved test.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
.github/dependabot.yml | 4 +
.github/workflows/maven.yml | 8 +-
.github/workflows/stale-branches.yml | 118 +++++
apache-maven/pom.xml | 5 +-
apache-maven/src/assembly/maven/bin/m2.conf | 4 +-
.../src/assembly/maven/conf/maven-user.properties | 19 +
.../org/apache/maven/api/ArtifactCoordinates.java | 3 +-
.../main/java/org/apache/maven/api/Constants.java | 28 ++
.../java/org/apache/maven/api/MonotonicClock.java | 53 +-
.../org/apache/maven/api/feature/Features.java | 7 +
.../org/apache/maven/api/services/Sources.java | 8 +-
.../maven/api/services/xml/XmlReaderRequest.java | 33 +-
.../apache/maven/api/ArtifactCoordinatesTest.java | 88 ++++
.../org/apache/maven/api/MonotonicClockTest.java | 46 +-
.../org/apache/maven/api/services/SourcesTest.java | 20 +
api/maven-api-model/src/main/mdo/maven.mdo | 17 +-
api/maven-api-settings/src/main/mdo/settings.mdo | 4 +
.../repository/legacy/LegacyRepositorySystem.java | 27 +-
.../main/java/org/apache/maven/cli/CLIManager.java | 1 +
.../sisu/plexus/PlexusXmlBeanConverter.java | 6 +-
.../sisu/plexus/PlexusXmlBeanConverterTest.java | 64 +++
.../maven/model/building/FileModelSource.java | 8 +-
.../StringVisitorModelInterpolator.java | 4 +-
.../reflection/ReflectionValueExtractor.java | 4 +-
.../model/validation/DefaultModelValidator.java | 19 +
.../maven/model/building/FileModelSourceTest.java | 16 +
.../AbstractModelInterpolatorTest.java | 38 ++
.../validation/DefaultModelValidatorTest.java | 10 +
.../poms/validation/bad-parent-relativePath.xml | 23 +-
.../plugin/descriptor/MojoDescriptorTest.java | 41 ++
.../java/org/apache/maven/cling/MavenCling.java | 47 ++
.../maven/cling/invoker/CommonsCliOptions.java | 1 +
.../apache/maven/cling/invoker/LookupInvoker.java | 17 +-
.../BuiltinShellCommandRegistryFactory.java | 12 +-
.../invoker/mvnup/goals/PluginUpgradeStrategy.java | 13 +-
.../mvnup/goals/ToolchainPluginStrategy.java | 349 +++++++++++++
.../org/apache/maven/cling/MavenClingTest.java | 117 +++++
.../maven/cling/invoker/CommonsCliOptionsTest.java | 56 +++
.../mvnup/goals/PluginUpgradeQuarkusTest.java | 522 +++++++++++++++++++
.../mvnup/goals/PluginUpgradeStrategyTest.java | 556 ++-------------------
.../mvnup/goals/ToolchainPluginStrategyTest.java | 531 ++++++++++++++++++++
.../main/java/org/apache/maven/DefaultMaven.java | 24 +-
.../main/java/org/apache/maven/ReactorReader.java | 40 +-
.../apache/maven/bridge/MavenRepositorySystem.java | 27 +-
.../apache/maven/graph/DefaultGraphBuilder.java | 21 +-
.../LegacyRepositorySystemSessionExtender.java | 26 +-
.../impl/ConsumerPomArtifactTransformer.java | 25 +-
.../impl/DefaultConsumerPomBuilder.java | 169 ++++++-
.../DefaultLifecycleExecutionPlanCalculator.java | 28 +-
.../DefaultLifecycleTaskSegmentCalculator.java | 8 +-
.../internal/LifecycleExecutionPlanCalculator.java | 3 +-
.../lifecycle/internal/MojoDescriptorCreator.java | 14 +-
.../maven/lifecycle/internal/MojoExecutor.java | 20 +-
.../maven/lifecycle/internal/PhaseComparator.java | 22 +-
.../maven/lifecycle/internal/ProjectIndex.java | 6 +-
.../internal/concurrent/BuildPlanExecutor.java | 4 +-
.../concurrent/ConcurrentLifecycleStarter.java | 6 -
.../org/apache/maven/logging/AsyncDrainWriter.java | 106 ++++
.../maven/plugin/DefaultExtensionRealmCache.java | 8 +-
.../maven/plugin/DefaultPluginArtifactsCache.java | 16 +-
.../plugin/internal/DefaultMavenPluginManager.java | 10 +
.../internal/DefaultPluginPrefixResolver.java | 16 +-
.../internal/DefaultPluginVersionResolver.java | 122 +++--
.../project/DefaultProjectBuildingHelper.java | 16 +-
.../maven/project/DefaultProjectRealmCache.java | 8 +-
.../maven/project/ProjectBuildingException.java | 87 +---
.../apache/maven/DefaultMavenSessionScopeTest.java | 88 ++++
.../exception/DefaultExceptionHandlerTest.java | 86 ++++
.../impl/ConsumerPomArtifactTransformerTest.java | 43 ++
.../impl/ConsumerPomBuilderTest.java | 304 +++++++++++
...efaultLifecycleExecutionPlanCalculatorTest.java | 94 ++++
.../LifecycleExecutionPlanCalculatorTest.java | 5 +-
.../internal/MojoExecutorForkedExecutionTest.java | 144 ++++++
.../stub/LifecycleExecutionPlanCalculatorStub.java | 2 +-
.../stub/LifecycleTaskSegmentCalculatorStub.java | 2 +-
.../internal/DefaultPluginPrefixResolverTest.java | 123 +++++
.../internal/DefaultPluginVersionResolverTest.java | 169 +++++++
.../project/DefaultMavenProjectBuilderTest.java | 35 +-
.../project/DefaultProjectRealmCacheTest.java | 110 ++++
.../project/ProjectBuildingExceptionTest.java | 163 ------
.../consumer/packaging-bom-profiles/pom.xml | 25 +
.../apache/maven/impl/DefaultModelXmlFactory.java | 1 +
.../apache/maven/impl/DefaultToolchainManager.java | 182 ++++++-
.../apache/maven/impl/JdkSourceLevelSupport.java | 162 ++++++
.../model/DefaultDependencyManagementImporter.java | 11 +
.../maven/impl/model/DefaultModelBuilder.java | 143 +++++-
.../maven/impl/model/DefaultModelObjectPool.java | 47 +-
.../maven/impl/model/DefaultModelValidator.java | 19 +
.../model/DefaultProfileActivationContext.java | 3 -
.../impl/model/profile/ConditionFunctions.java | 11 +-
.../maven/impl/model/profile/ConditionParser.java | 20 +-
.../model/profile/PropertyProfileActivator.java | 42 +-
.../model/reflection/ReflectionValueExtractor.java | 4 +-
.../resolver/validator/MavenValidatorFactory.java | 3 +-
.../impl/standalone/RepositorySystemSupplier.java | 7 +-
.../maven/impl/DefaultToolchainManagerTest.java | 291 +++++++++++
.../maven/impl/JdkSourceLevelSupportTest.java | 178 +++++++
.../DefaultDependencyManagementImporterTest.java | 46 ++
.../maven/impl/model/DefaultModelBuilderTest.java | 478 +++++++++++++++++-
.../impl/model/DefaultModelValidatorTest.java | 10 +
.../maven/impl/model/MavenModelMergerTest.java | 33 ++
.../impl/model/profile/ConditionParserTest.java | 109 ++++
.../profile/PropertyProfileActivatorTest.java | 105 +++-
.../resources/poms/factory/import-bom-type.xml | 35 ++
.../poms/factory/maven-version-range-profile.xml | 37 ++
.../poms/validation/bad-parent-relativePath.xml | 23 +-
impl/maven-jline/pom.xml | 5 +
.../java/org/apache/maven/jline/FastTerminal.java | 121 ++++-
.../java/org/apache/maven/jline/MessageUtils.java | 8 +-
.../maven/jline/FastTerminalReentrancyTest.java | 172 +++++++
.../plugin/stubs/RepositorySystemSupplier.java | 11 +-
impl/maven-xml/pom.xml | 3 +-
.../apache/maven/internal/xml/XmlNodeBuilder.java | 301 -----------
.../apache/maven/internal/xml/XmlNodeImplTest.java | 7 +-
...XmlNodeBuilderTest.java => XmlServiceTest.java} | 2 +-
its/core-it-suite/pom.xml | 64 ++-
.../src/test/java/org/apache/maven/it/ItUtils.java | 1 +
...avenITBomConsumerPomPropertyResolutionTest.java | 1 +
.../MavenITConsumerPomBomFromSettingsRepoTest.java | 1 +
.../MavenITMvnupToolchainPluginStrategyTest.java | 94 ++++
.../it/MavenITgh11162ConsumerPomScopesTest.java | 1 +
.../it/MavenITgh11456MixinsConsumerPomTest.java | 2 +
.../it/MavenITgh12640BomInheritedDepMgmtTest.java | 132 +++++
...avenITgh12646ProjectLocalRepoCleanRaceTest.java | 59 +++
...avenITgh12660BomVersionFromImportedBomTest.java | 101 ++++
...gh12729ModulePathPropertyInterpolationTest.java | 65 +++
...enITgh12770UnversionedPluginPrereleaseTest.java | 55 ++
...ep0590ClassifiedPomArtifactFromReactorTest.java | 49 ++
...enITmng3043BestEffortReactorResolutionTest.java | 5 +-
.../it/MavenITmng3955EffectiveSettingsTest.java | 1 +
.../apache/maven/it/MavenITmng5102MixinsTest.java | 15 +-
.../MavenITmng7967ArtifactHandlerLanguageTest.java | 1 +
.../maven/it/MavenITmng8181CentralRepoTest.java | 1 +
...venITmng8414ConsumerPomWithNewFeaturesTest.java | 2 +
...mng8645ConsumerPomDependencyManagementTest.java | 1 +
...MavenITmng8709ProfileDependencyVersionTest.java | 45 ++
.../.mvn/.gitkeep | 0
.../gh-11162-consumer-pom-scopes/.mvn/.gitkeep | 0
.../.mvn/.gitkeep | 0
.../gh-12640-bom-inherited-depmgmt/bom/pom.xml | 53 ++
.../mod-1/pom.xml} | 23 +-
.../mod-2/pom.xml} | 23 +-
.../gh-12640-bom-inherited-depmgmt/pom.xml | 60 +++
.../.mvn/.gitkeep | 0
.../module-a/pom.xml | 29 ++
.../module-a/src/main/java/a/A.java | 7 +
.../module-b/pom.xml | 29 ++
.../module-b/src/main/java/b/B.java | 7 +
.../module-c/pom.xml | 29 ++
.../module-c/src/main/java/c/C.java | 7 +
.../module-d/pom.xml | 29 ++
.../module-d/src/main/java/d/D.java | 7 +
.../gh-12646-project-local-repo-clean-race/pom.xml | 38 ++
.../super-pom/pom.xml | 25 +
.../bom/pom.xml | 66 +++
.../ext-bom/pom.xml | 51 ++
.../mod-1/pom.xml} | 23 +-
.../mod-2/pom.xml} | 23 +-
.../pom.xml} | 26 +-
.../child/pom.xml} | 16 +-
.../pom.xml} | 21 +-
.../gh-12770-unversioned-plugin-prerelease/pom.xml | 56 +++
.../1.0/maven-it-plugin-prerelease-1.0.jar | Bin 0 -> 5251 bytes
.../1.0/maven-it-plugin-prerelease-1.0.pom | 36 ++
.../maven-it-plugin-prerelease-2.0-beta-1.jar | Bin 0 -> 5276 bytes
.../maven-it-plugin-prerelease-2.0-beta-1.pom | 36 ++
.../maven-it-plugin-prerelease/maven-metadata.xml | 13 +
.../apache/maven/its/prerelease/maven-metadata.xml | 10 +
.../settings-template.xml | 63 +++
.../src/test/resources/mdep-590/consumer/pom.xml | 64 +++
.../{mng-3955/settings.xml => mdep-590/pom.xml} | 21 +-
.../settings.xml => mdep-590/producer/custom.pom} | 8 +-
.../src/test/resources/mdep-590/producer/pom.xml | 57 +++
.../src/test/resources/mng-3043/.mvn/.gitkeep | 0
.../src/test/resources/mng-3955/settings.xml | 2 +-
.../classifier/project/.mvn/.gitkeep | 0
.../mng-5102-mixins/gav/project/.mvn/.gitkeep | 0
.../resources/mng-5102-mixins/path/.mvn/.gitkeep | 0
.../maven-hocon-extension/src/mdo/hocon-reader.vm | 1 -
.../.mvn/.gitkeep | 0
.../mng-8645-consumer-pom-dep-mgmt/.mvn/.gitkeep | 0
.../mng-8709-profile-dependency-version/pom.xml | 49 ++
.../mvnup-toolchain-plugin-strategy/pom.xml | 12 +
.../maven-it-plugin-plexus-component-api/pom.xml | 2 +-
its/pom.xml | 6 +-
pom.xml | 30 +-
src/mdo/java/InputLocation.java | 24 +-
src/mdo/reader.vm | 4 +-
188 files changed, 8064 insertions(+), 1520 deletions(-)
diff --cc impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java
index 354c875094,da2997a031..34d33a565f
--- 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
@@@ -25,7 -25,7 +25,8 @@@ import java.util.ArrayList
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+ import java.util.Properties;
import java.util.function.Function;
import java.util.stream.Collectors;
@@@ -459,122 -554,41 +560,157 @@@ class DefaultConsumerPomBuilder impleme
+ "attribute on the <project> element of your POM.");
}
+ /**
+ * Inlines packaging-activated profiles into the model.
+ * <p>
+ * When a profile is activated by packaging and the packaging matches the project's packaging,
+ * the profile's content (dependencies, dependency management, repositories) is merged into
+ * the main model and the profile is removed. This ensures consistent behavior across all
+ * tools consuming the POM, since packaging activation is a 4.1.0+ feature not available
+ * in Maven 3 or other tools like Gradle.
+ * <p>
+ * If the profile has other activation conditions besides packaging, only the packaging
+ * part is stripped from the activation; the profile's content is <b>not</b> inlined to
+ * preserve AND semantics (the content remains gated by the remaining conditions).
+ * <p>
+ * Profiles with a non-matching packaging activation are dropped entirely, since they
+ * can never activate for this artifact's fixed packaging and their presence would block
+ * model version downgrade to 4.0.0.
+ * <p>
+ * Non-transitive scope dependencies (test, provided, system) from inlined profiles are
+ * filtered out to prevent leakage into the consumer POM.
+ *
+ * @param model the model to process
+ * @param packaging the project's packaging type
+ * @return the model with packaging-activated profiles inlined
+ */
+ static Model inlinePackagingActivatedProfiles(Model model, String packaging) {
+ List<Profile> remainingProfiles = new ArrayList<>();
+ List<Dependency> additionalDeps = new ArrayList<>();
+ List<Dependency> additionalManagedDeps = new ArrayList<>();
+ List<Repository> additionalRepos = new ArrayList<>();
+
+ for (Profile profile : model.getProfiles()) {
+ Activation activation = profile.getActivation();
+ if (activation != null && activation.getPackaging() != null) {
+ if (Objects.equals(activation.getPackaging(), packaging)) {
+ Activation strippedActivation = stripPackagingActivation(activation);
+ if (strippedActivation != null) {
+ // Keep the profile but remove the packaging activation part
+ // Do not inline its contents since it has other activation conditions
+ remainingProfiles.add(profile.withActivation(strippedActivation));
+ } else {
+ // Packaging is the ONLY condition.
+ // Inline profile content into the model
+ additionalDeps.addAll(profile.getDependencies());
+ if (profile.getDependencyManagement() != null) {
+ additionalManagedDeps.addAll(
+ profile.getDependencyManagement().getDependencies());
+ }
+ additionalRepos.addAll(profile.getRepositories());
+ }
+ } else {
+ // Packaging does not match — drop the profile entirely
+ }
+ } else {
+ // No packaging activation — keep the profile as-is
+ remainingProfiles.add(profile);
+ }
+ }
+
+ // Merge additional dependencies into the model, deduplicating by key
+ if (!additionalDeps.isEmpty()) {
+ additionalDeps.removeIf(DefaultConsumerPomBuilder::hasDependencyScope);
+ Map<String, Dependency> mergedDeps = new LinkedHashMap<>();
+ for (Dependency dep : model.getDependencies()) {
+ mergedDeps.put(getDependencyKey(dep), dep);
+ }
+ for (Dependency dep : additionalDeps) {
+ mergedDeps.putIfAbsent(getDependencyKey(dep), dep);
+ }
+ model = model.withDependencies(mergedDeps.values());
+ }
+
+ // Merge additional managed dependencies into the model, deduplicating by key
+ if (!additionalManagedDeps.isEmpty()) {
+ DependencyManagement dm = model.getDependencyManagement();
+ Map<String, Dependency> mergedManagedDeps = new LinkedHashMap<>();
+ if (dm != null) {
+ for (Dependency dep : dm.getDependencies()) {
+ mergedManagedDeps.put(getDependencyKey(dep), dep);
+ }
+ }
+ for (Dependency dep : additionalManagedDeps) {
+ mergedManagedDeps.putIfAbsent(getDependencyKey(dep), dep);
+ }
+ model = model.withDependencyManagement((dm != null ? dm : DependencyManagement.newInstance())
+ .withDependencies(mergedManagedDeps.values()));
+ }
+
+ // Merge additional repositories into the model, deduplicating by id
+ if (!additionalRepos.isEmpty()) {
+ Map<String, Repository> mergedRepos = new LinkedHashMap<>();
+ for (Repository repo : model.getRepositories()) {
+ mergedRepos.put(repo.getId(), repo);
+ }
+ for (Repository repo : additionalRepos) {
+ mergedRepos.putIfAbsent(repo.getId(), repo);
+ }
+ model = model.withRepositories(mergedRepos.values());
+ }
+
+ return model.withProfiles(remainingProfiles);
+ }
+
+ /**
+ * Strips the packaging activation from an activation, returning the remaining activation
+ * or {@code null} if packaging was the only activation condition.
+ */
+ private static Activation stripPackagingActivation(Activation activation) {
+ Activation stripped =
+ Activation.newBuilder(activation, true).packaging(null).build();
+ // Check if the remaining activation has any other conditions
- if (!stripped.isActiveByDefault()
- && stripped.getJdk() == null
- && stripped.getOs() == null
- && stripped.getProperty() == null
- && stripped.getFile() == null
- && stripped.getCondition() == null) {
++ if (isActivationEmpty(stripped)) {
++ return null;
++ }
++ return stripped;
++ }
++
+ /**
+ * Strips {@code executable()} conditions from profile activations.
+ * <p>
+ * The {@code executable()} function evaluates against the local system {@code PATH},
+ * making it environment-dependent. When such a condition survives into a published
+ * consumer POM, downstream consumers silently evaluate it against <em>their own</em>
+ * {@code PATH}, producing non-reproducible builds. This method removes the entire
+ * {@code condition} string when it contains an {@code executable()} call, and drops
+ * the activation entirely when no other activation triggers remain.
+ *
+ * @param profiles the list of profiles to process
+ * @return a new list with {@code executable()} conditions stripped
+ */
+ static List<Profile> stripExecutableConditions(List<Profile> profiles) {
+ return profiles.stream()
+ .map(p -> p.withActivation(stripExecutableCondition(p.getActivation())))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Strips an {@code executable()} condition from a single activation.
+ * Returns {@code null} when the activation has no remaining triggers after stripping.
+ */
+ private static Activation stripExecutableCondition(Activation activation) {
+ if (activation == null) {
+ return null;
+ }
+ String condition = activation.getCondition();
+ if (condition == null || !condition.contains("executable(")) {
+ return activation;
+ }
+ // Remove the entire condition — partial expression surgery could change
+ // the boolean semantics in unexpected ways (e.g. AND vs OR combinations).
+ Activation stripped = activation.withCondition(null);
+ if (isActivationEmpty(stripped)) {
return null;
}
return stripped;
diff --cc impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java
index 61795d6c21,7d7da413bc..d50fe81175
--- 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
@@@ -23,8 -23,8 +23,9 @@@ import javax.inject.Inject
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+ import java.util.Map;
import org.apache.maven.api.DependencyCoordinates;
import org.apache.maven.api.Node;
@@@ -32,11 -32,11 +33,13 @@@ import org.apache.maven.api.PathScope
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.model.Activation;
+ import org.apache.maven.api.model.ActivationOS;
+ import org.apache.maven.api.model.ActivationProperty;
import org.apache.maven.api.model.Dependency;
+import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Profile;
+import org.apache.maven.api.model.Repository;
import org.apache.maven.api.model.Scm;
import org.apache.maven.api.services.DependencyResolver;
import org.apache.maven.api.services.DependencyResolverResult;
@@@ -351,153 -284,304 +354,454 @@@ public class ConsumerPomBuilderTest ext
assertTrue(hasCustomRepo, "Consumer POM model builder request should include the project's custom repository");
}
+ @Test
+ void testInlinePackagingActivatedProfiles() {
+ Dependency dep1 = Dependency.newBuilder()
+ .groupId("g")
+ .artifactId("a1")
+ .version("1")
+ .build();
+ Dependency dep2 = Dependency.newBuilder()
+ .groupId("g")
+ .artifactId("a2")
+ .version("2")
+ .build();
+
+ Profile profileMatching = Profile.newBuilder()
+ .id("matching")
+ .activation(Activation.newBuilder().packaging("jar").build())
+ .dependencies(List.of(dep1))
+ .build();
+
+ Profile profileNotMatching = Profile.newBuilder()
+ .id("not-matching")
+ .activation(Activation.newBuilder().packaging("war").build())
+ .dependencies(List.of(dep2))
+ .build();
+
+ Profile profileMixed = Profile.newBuilder()
+ .id("mixed")
+ .activation(Activation.newBuilder().packaging("jar").jdk("11").build())
+ .dependencies(List.of(dep2))
+ .build();
+
+ Model model = Model.newBuilder()
+ .packaging("jar")
+ .profiles(List.of(profileMatching, profileNotMatching, profileMixed))
+ .build();
+
+ Model transformed = DefaultConsumerPomBuilder.inlinePackagingActivatedProfiles(model, "jar");
+
+ // matching profile should be removed and its deps added to model
+ // not-matching profile should be dropped entirely since its packaging condition can never be met
+ // mixed profile should be kept but packaging activation stripped
+ assertEquals(1, transformed.getProfiles().size());
+
+ assertEquals("mixed", transformed.getProfiles().get(0).getId());
+ assertNull(transformed.getProfiles().get(0).getActivation().getPackaging());
+ assertEquals("11", transformed.getProfiles().get(0).getActivation().getJdk());
+
+ assertEquals(1, transformed.getDependencies().size());
+ assertEquals("a1", transformed.getDependencies().get(0).getArtifactId());
+ }
+
+ @Test
+ void testInlinePackagingActivatedProfilesDependencyManagement() {
+ Dependency managedDep = Dependency.newBuilder()
+ .groupId("g")
+ .artifactId("managed1")
+ .version("1.0")
+ .build();
+
+ Profile profileWithDM = Profile.newBuilder()
+ .id("dm-profile")
+ .activation(Activation.newBuilder().packaging("jar").build())
+ .dependencyManagement(DependencyManagement.newBuilder()
+ .dependencies(Collections.singletonList(managedDep))
+ .build())
+ .build();
+
+ Model model = Model.newBuilder()
+ .packaging("jar")
+ .profiles(List.of(profileWithDM))
+ .build();
+
+ Model transformed = DefaultConsumerPomBuilder.inlinePackagingActivatedProfiles(model, "jar");
+
+ // Profile should be removed
+ assertTrue(transformed.getProfiles().isEmpty());
+
+ // Managed dependency should be inlined
+ assertNotNull(transformed.getDependencyManagement());
+ assertEquals(1, transformed.getDependencyManagement().getDependencies().size());
+ assertEquals(
+ "managed1",
+ transformed.getDependencyManagement().getDependencies().get(0).getArtifactId());
+ }
+
+ @Test
+ void testInlinePackagingActivatedProfilesRepositories() {
+ Repository repo = Repository.newBuilder()
+ .id("custom-repo")
+ .url("https://repo.example.com/maven2")
+ .build();
+
+ Profile profileWithRepo = Profile.newBuilder()
+ .id("repo-profile")
+ .activation(Activation.newBuilder().packaging("jar").build())
+ .repositories(Collections.singletonList(repo))
+ .build();
+
+ Model model = Model.newBuilder()
+ .packaging("jar")
+ .profiles(List.of(profileWithRepo))
+ .build();
+
+ Model transformed = DefaultConsumerPomBuilder.inlinePackagingActivatedProfiles(model, "jar");
+
+ // Profile should be removed
+ assertTrue(transformed.getProfiles().isEmpty());
+
+ // Repository should be inlined
+ assertEquals(1, transformed.getRepositories().size());
+ assertEquals("custom-repo", transformed.getRepositories().get(0).getId());
+ }
+
+ @Test
+ void testInlinePackagingActivatedProfilesDeduplication() {
+ Dependency existingDep = Dependency.newBuilder()
+ .groupId("g")
+ .artifactId("a1")
+ .version("1.0")
+ .build();
+
+ // Profile re-declares the same dependency with a different version
+ Dependency duplicateDep = Dependency.newBuilder()
+ .groupId("g")
+ .artifactId("a1")
+ .version("2.0")
+ .build();
+
+ Profile profileWithDuplicate = Profile.newBuilder()
+ .id("dup-profile")
+ .activation(Activation.newBuilder().packaging("jar").build())
+ .dependencies(List.of(duplicateDep))
+ .build();
+
+ Model model = Model.newBuilder()
+ .packaging("jar")
+ .dependencies(List.of(existingDep))
+ .profiles(List.of(profileWithDuplicate))
+ .build();
+
+ Model transformed = DefaultConsumerPomBuilder.inlinePackagingActivatedProfiles(model, "jar");
+
+ // Should NOT have duplicates — existing model dependency takes precedence
+ assertEquals(1, transformed.getDependencies().size());
+ assertEquals(
+ "1.0",
+ transformed.getDependencies().get(0).getVersion(),
+ "Existing model dependency should take precedence over profile duplicate");
+ }
++
+ /**
+ * Verifies that the consumer POM builder injects properties from the project's
+ * active profiles into the BUILD_CONSUMER request's user properties.
+ * <p>
+ * Without the fix in {@code DefaultConsumerPomBuilder.buildModel()}, the
+ * {@code ModelBuilderRequest} only carries session user properties. When a
+ * dependency version is defined via a profile property (e.g.
+ * {@code <version>${my.version}</version>}), model validation would reject the
+ * unresolved expression as an invalid version.
+ * <p>
+ * Session user properties ({@code -D} flags) must still take precedence over
+ * profile-defined properties.
+ */
+ @Test
+ void testConsumerPomIncludesActiveProfileProperties() throws Exception {
+ setRootDirectory("trivial");
+ Path file = Paths.get("src/test/resources/consumer/trivial/child/pom.xml");
+
+ MavenProject project = getEffectiveModel(file);
+
+ // Create an active profile with a property, simulating a profile that defines
+ // a dependency version (the scenario fixed by MNG-8709).
+ org.apache.maven.api.model.Profile apiProfile = org.apache.maven.api.model.Profile.newBuilder()
+ .id("version-profile")
+ .properties(Map.of("dep.version", "1.0.0"))
+ .build();
+ org.apache.maven.model.Profile modelProfile = new org.apache.maven.model.Profile(apiProfile);
+ project.setActiveProfiles(List.of(modelProfile));
+
+ // 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<ModelBuilderRequest> requestCaptor = ArgumentCaptor.forClass(ModelBuilderRequest.class);
+ Mockito.verify(spyMbs, Mockito.atLeastOnce()).build(requestCaptor.capture());
+
+ // Find the BUILD_CONSUMER request
+ 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 user properties contain the profile property
+ Map<String, String> userProps = consumerRequest.getUserProperties();
+ assertNotNull(userProps, "User properties should not be null");
+ assertTrue(
+ userProps.containsKey("dep.version"), "User properties should contain properties from active profiles");
+ assertTrue(
+ "1.0.0".equals(userProps.get("dep.version")),
+ "Profile property 'dep.version' should have value '1.0.0'");
+ }
+
+ /**
+ * Verifies that session user properties ({@code -D} flags) take precedence
+ * over profile-defined properties in the BUILD_CONSUMER request.
+ */
+ @Test
+ void testConsumerPomSessionPropertiesOverrideProfileProperties() throws Exception {
+ MavenExecutionRequest execRequest = setRootDirectory("trivial");
+ Path file = Paths.get("src/test/resources/consumer/trivial/child/pom.xml");
+
+ MavenProject project = getEffectiveModel(file);
+
+ // Create an active profile with a property
+ org.apache.maven.api.model.Profile apiProfile = org.apache.maven.api.model.Profile.newBuilder()
+ .id("version-profile")
+ .properties(Map.of("dep.version", "1.0.0"))
+ .build();
+ org.apache.maven.model.Profile modelProfile = new org.apache.maven.model.Profile(apiProfile);
+ project.setActiveProfiles(List.of(modelProfile));
+
+ // Set the same property as a session user property (simulating -Ddep.version=2.0.0)
+ execRequest.getUserProperties().setProperty("dep.version", "2.0.0");
+
+ // 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 BUILD_CONSUMER request
+ ArgumentCaptor<ModelBuilderRequest> requestCaptor = ArgumentCaptor.forClass(ModelBuilderRequest.class);
+ Mockito.verify(spyMbs, Mockito.atLeastOnce()).build(requestCaptor.capture());
+
+ 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");
+
+ // Session user properties must override profile properties
+ Map<String, String> userProps = consumerRequest.getUserProperties();
+ assertTrue(
+ "2.0.0".equals(userProps.get("dep.version")),
+ "Session user property should override profile property; expected '2.0.0' but got '"
+ + userProps.get("dep.version") + "'");
+ }
+
+ // ── executable() condition stripping (GH-12570) ──────────────────────────
+
+ /**
+ * A profile whose only activation trigger is an {@code executable()} condition
+ * should have its activation removed entirely after stripping.
+ */
+ @Test
+ void testStripExecutableConditionsOnlyExecutableRemovesActivation() {
+ Profile profile = Profile.newBuilder()
+ .id("exec-only")
+ .activation(Activation.newBuilder()
+ .condition("executable('musl-gcc')")
+ .build())
+ .dependencies(List.of(Dependency.newBuilder()
+ .groupId("org.example")
+ .artifactId("some-lib")
+ .version("1.0")
+ .build()))
+ .build();
+
+ List<Profile> result = DefaultConsumerPomBuilder.stripExecutableConditions(List.of(profile));
+
+ assertEquals(1, result.size());
+ assertNull(result.get(0).getActivation(), "Activation should be null when executable() was the only trigger");
+ }
+
+ /**
+ * When a profile has {@code executable()} in its condition but also has another
+ * activation trigger (e.g. OS), the condition should be stripped but the
+ * remaining activation preserved.
+ */
+ @Test
+ void testStripExecutableConditionsMixedActivationPreservesOtherTriggers() {
+ Profile profile = Profile.newBuilder()
+ .id("exec-and-os")
+ .activation(Activation.newBuilder()
+ .condition("executable('gcc') && ${os.name} == 'linux'")
+ .os(ActivationOS.newBuilder().name("linux").build())
+ .build())
+ .build();
+
+ List<Profile> result = DefaultConsumerPomBuilder.stripExecutableConditions(List.of(profile));
+
+ assertEquals(1, result.size());
+ Activation activation = result.get(0).getActivation();
+ assertNotNull(activation, "Activation should be preserved when other triggers exist");
+ assertNull(activation.getCondition(), "Condition should be stripped");
+ assertNotNull(activation.getOs(), "OS trigger should be preserved");
+ }
+
+ /**
+ * Profiles without {@code executable()} in their condition should pass through
+ * unchanged.
+ */
+ @Test
+ void testStripExecutableConditionsNoExecutableUnchanged() {
+ Activation originalActivation =
+ Activation.newBuilder().condition("${os.name} == 'linux'").build();
+ Profile profile = Profile.newBuilder()
+ .id("no-exec")
+ .activation(originalActivation)
+ .build();
+
+ List<Profile> result = DefaultConsumerPomBuilder.stripExecutableConditions(List.of(profile));
+
+ assertEquals(1, result.size());
+ Activation activation = result.get(0).getActivation();
+ assertNotNull(activation);
+ assertEquals("${os.name} == 'linux'", activation.getCondition());
+ }
+
+ /**
+ * Profiles with no activation at all should pass through unchanged.
+ */
+ @Test
+ void testStripExecutableConditionsNullActivationUnchanged() {
+ Profile profile = Profile.newBuilder().id("no-activation").build();
+
+ List<Profile> result = DefaultConsumerPomBuilder.stripExecutableConditions(List.of(profile));
+
+ assertEquals(1, result.size());
+ assertNull(result.get(0).getActivation());
+ }
+
+ /**
+ * A negated {@code executable()} call (e.g. {@code not(executable(...))})
+ * should also be stripped from the condition.
+ */
+ @Test
+ void testStripExecutableConditionsNegatedExecutableStripped() {
+ Profile profile = Profile.newBuilder()
+ .id("negated-exec")
+ .activation(Activation.newBuilder()
+ .condition("not(executable('musl-gcc'))")
+ .build())
+ .build();
+
+ List<Profile> result = DefaultConsumerPomBuilder.stripExecutableConditions(List.of(profile));
+
+ assertEquals(1, result.size());
+ assertNull(result.get(0).getActivation(), "Negated executable() should also be stripped");
+ }
+
+ /**
+ * When {@code executable()} is combined with a property trigger in the
+ * activation, stripping should remove the condition but preserve the
+ * property trigger.
+ */
+ @Test
+ void testStripExecutableConditionsWithPropertyTriggerPreservesProperty() {
+ Profile profile = Profile.newBuilder()
+ .id("exec-and-property")
+ .activation(Activation.newBuilder()
+ .condition("executable('docker')")
+ .property(ActivationProperty.newBuilder()
+ .name("docker.enabled")
+ .value("true")
+ .build())
+ .build())
+ .build();
+
+ List<Profile> result = DefaultConsumerPomBuilder.stripExecutableConditions(List.of(profile));
+
+ assertEquals(1, result.size());
+ Activation activation = result.get(0).getActivation();
+ assertNotNull(activation, "Activation should be preserved when property trigger exists");
+ assertNull(activation.getCondition(), "Condition should be stripped");
+ assertNotNull(activation.getProperty(), "Property trigger should be preserved");
+ assertEquals("docker.enabled", activation.getProperty().getName());
+ }
+
+ /**
+ * Verifies that {@code transformNonPom} strips {@code executable()} conditions
+ * from profiles via the {@code prune()} path.
+ */
+ @Test
+ void testTransformNonPomStripsExecutableCondition() {
+ Model model = Model.newBuilder()
+ .profiles(List.of(Profile.newBuilder()
+ .id("exec-profile")
+ .activation(Activation.newBuilder()
+ .condition("executable('tool')")
+ .build())
+ .dependencies(List.of(Dependency.newBuilder()
+ .groupId("org.example")
+ .artifactId("tool-support")
+ .version("1.0")
+ .build()))
+ .build()))
+ .build();
+
+ Model result = DefaultConsumerPomBuilder.transformNonPom(model, null);
+
+ // The profile had executable() activation and dependencies.
+ // After pruning: activation is stripped (executable-only), build is stripped,
+ // properties stripped, etc. The profile retains dependencies, so it survives
+ // the isEmpty filter, but its activation should be null.
+ if (!result.getProfiles().isEmpty()) {
+ assertNull(
+ result.getProfiles().get(0).getActivation(),
+ "executable() condition should be stripped from transformNonPom profiles");
+ }
+ }
+
+ /**
+ * Verifies that {@code transformPom} strips {@code executable()} conditions
+ * from profiles in parent/POM-packaged projects.
+ */
+ @Test
+ void testTransformPomStripsExecutableCondition() throws Exception {
+ setRootDirectory("trivial");
+ Path file = Paths.get("src/test/resources/consumer/trivial/child/pom.xml");
+ MavenProject project = getEffectiveModel(file);
+
+ Model model = project.getModel().getDelegate();
+ // Add a profile with an executable() condition to the model
+ Profile execProfile = Profile.newBuilder()
+ .id("exec-profile")
+ .activation(
+ Activation.newBuilder().condition("executable('gcc')").build())
+ .properties(Map.of("native.enabled", "true"))
+ .build();
+ model = model.withProfiles(List.of(execProfile));
+
+ Model result = DefaultConsumerPomBuilder.transformPom(model, project);
+
+ // The profile should have its activation stripped
+ assertFalse(result.getProfiles().isEmpty(), "Profile should survive (it has properties)");
+ assertNull(
+ result.getProfiles().get(0).getActivation(),
+ "executable() condition should be stripped from transformPom profiles");
+ }
}
diff --cc impl/maven-core/src/test/resources/consumer/packaging-bom-profiles/pom.xml
index 0000000000,0000000000..176d83dddf
new file mode 100644
--- /dev/null
+++ b/impl/maven-core/src/test/resources/consumer/packaging-bom-profiles/pom.xml
@@@ -1,0 -1,0 +1,25 @@@
++<project xmlns="http://maven.apache.org/POM/4.1.0">
++ <modelVersion>4.1.0</modelVersion>
++ <groupId>org.my.group</groupId>
++ <artifactId>packaging-bom-profiles-test</artifactId>
++ <version>1.0.0-SNAPSHOT</version>
++ <packaging>bom</packaging>
++
++ <profiles>
++ <profile>
++ <id>jar-profile</id>
++ <activation>
++ <packaging>jar</packaging>
++ </activation>
++ <dependencyManagement>
++ <dependencies>
++ <dependency>
++ <groupId>org.slf4j</groupId>
++ <artifactId>slf4j-api</artifactId>
++ <version>2.0.0</version>
++ </dependency>
++ </dependencies>
++ </dependencyManagement>
++ </profile>
++ </profiles>
++</project>