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>