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/30 20:34:03 UTC
(maven) branch backport/12570-strip-executable-4.0.x created (now 9bb819535e)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a change to branch backport/12570-strip-executable-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
at 9bb819535e Strip executable() conditions from consumer POMs (#12874)
This branch includes the following new commits:
new 9bb819535e Strip executable() conditions from consumer POMs (#12874)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(maven) 01/01: Strip executable() conditions from consumer POMs (#12874)
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch backport/12570-strip-executable-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 9bb819535e29695ff6d0b67d9c65251267f4c382
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Sun Aug 30 22:33:29 2026 +0200
Strip executable() conditions from consumer POMs (#12874)
The executable() function evaluates against the local system PATH,
making profile activation environment-dependent. When such conditions
survive into published consumer POMs, downstream consumers silently
evaluate them against their own PATH, producing non-reproducible builds.
Strip the entire condition string when it contains an executable() call
during DefaultConsumerPomBuilder processing. This applies to all three
transform paths: transformNonPom and transformBom (via prune()), and
transformPom (directly). When no other activation triggers remain after
stripping, the activation is removed entirely.
Closes #12570
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
.../impl/DefaultConsumerPomBuilder.java | 61 ++++++-
.../impl/ConsumerPomBuilderTest.java | 199 +++++++++++++++++++++
2 files changed, 259 insertions(+), 1 deletion(-)
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 4f4062c2de..4a9bf7a173 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
@@ -35,6 +35,7 @@
import org.apache.maven.api.PathScope;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.feature.Features;
+import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DistributionManagement;
import org.apache.maven.api.model.Model;
@@ -461,7 +462,10 @@ static Model transformPom(Model model, MavenProject project) {
boolean preserveModelVersion = model.isPreserveModelVersion();
// raw to consumer transform
- model = model.withRoot(false).withModules(null).withSubprojects(null);
+ model = model.withRoot(false)
+ .withModules(null)
+ .withSubprojects(null)
+ .withProfiles(stripExecutableConditions(model.getProfiles()));
Parent parent = model.getParent();
if (parent != null) {
model = model.withParent(parent.withRelativePath(null));
@@ -496,11 +500,66 @@ private static void warnNotDowngraded(MavenProject project) {
+ "attribute on the <project> element of your POM.");
}
+ /**
+ * 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;
+ }
+
+ /**
+ * Returns {@code true} when the activation carries no triggers at all
+ * (default {@code activeByDefault} is {@code false}).
+ */
+ private static boolean isActivationEmpty(Activation activation) {
+ return !activation.isActiveByDefault()
+ && activation.getJdk() == null
+ && activation.getOs() == null
+ && activation.getProperty() == null
+ && activation.getFile() == null
+ && activation.getPackaging() == null
+ && activation.getCondition() == null;
+ }
+
private static List<Profile> prune(List<Profile> profiles) {
return profiles.stream()
.map(p -> {
Profile.Builder builder = Profile.newBuilder(p, true);
prune((ModelBase.Builder) builder, p);
+ builder.activation(stripExecutableCondition(p.getActivation()));
return builder.build(null).build();
})
.filter(p -> !isEmpty(p))
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 a601a09947..7d7da413bc 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
@@ -31,7 +31,12 @@
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.Model;
+import org.apache.maven.api.model.Profile;
import org.apache.maven.api.model.Scm;
import org.apache.maven.api.services.DependencyResolver;
import org.apache.maven.api.services.DependencyResolverResult;
@@ -55,6 +60,7 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
+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.assertNull;
@@ -385,4 +391,197 @@ void testConsumerPomSessionPropertiesOverrideProfileProperties() throws Exceptio
"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");
+ }
}