diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
index bf010a94069d..e051c891f5a1 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
@@ -738,5 +738,40 @@ public final class Constants {
*/
public static final String MAVEN_LOGGER_LOG_PREFIX = MAVEN_LOGGER_PREFIX + "log.";
+ /**
+ * User property for allowing {@code system} scope and {@code systemPath} in dependency
+ * management imported from repository-resolved POMs (BOMs).
+ *
+ * - When set to
false (default), dependency management imported from a
+ * repository-resolved POM may not declare {@code system} scope or a {@code systemPath};
+ * such entries are dropped with a warning.
+ * - When set to
true, such entries are imported as in previous Maven
+ * versions, with a warning.
+ *
+ * Dependency management imported from the local reactor is not affected by this property.
+ *
+ * @since 4.1.0
+ */
+ @Config(type = "java.lang.Boolean", defaultValue = "false")
+ public static final String MAVEN_REPOSITORY_DEPENDENCY_MANAGEMENT_ALLOW_SYSTEM_SCOPE =
+ "maven.repository.dependencyManagement.allowSystemScope";
+
+ /**
+ * User property for opting back into the previous behavior of interpolating
+ * repository-resolved models (dependencies and parents) against the full set of
+ * session properties (system, environment and CLI).
+ *
+ * - When set to
false (default), models resolved from a
+ * repository are interpolated only against their own {@code },
+ * preventing property leaking from the requesting build into transitive POMs.
+ * - When set to
true, full interpolation is applied as in
+ * previous Maven versions.
+ *
+ *
+ * @since 4.1.0
+ */
+ @Config(type = "java.lang.Boolean", defaultValue = "false")
+ public static final String MAVEN_MODEL_DEPENDENCY_INTERPOLATION_FULL = "maven.model.dependencyInterpolation.full";
+
private Constants() {}
}
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java
index 325b1d4419d1..a259ba52cd78 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java
@@ -42,6 +42,22 @@
*/
public interface ModelSource extends Source {
+ /**
+ * Returns the model identifier in the format {@code groupId:artifactId:version}
+ * if this source represents a resolved artifact with known coordinates.
+ *
+ * This method is primarily used by resolved sources to provide the model ID
+ * without requiring the XML to be parsed. For build sources, this typically
+ * returns {@code null} since the coordinates are determined by parsing the POM.
+ *
+ * @return the model identifier, or {@code null} if not available or not applicable
+ * @since 4.0.0
+ */
+ @Nullable
+ default String getModelId() {
+ return null;
+ }
+
/**
* Interface for locating POM files within a project structure.
* Implementations of this interface provide the ability to find POM files
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java
index 4f08f2d62dcc..5fee354703a2 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java
@@ -180,6 +180,12 @@ public Path getPath() {
return null;
}
+ @Override
+ @Nullable
+ public String getModelId() {
+ return location;
+ }
+
@Override
public Source resolve(String relative) {
return null;
diff --git a/compat/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java b/compat/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
index 9a372628aa79..7640c32c91a6 100644
--- a/compat/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
+++ b/compat/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
@@ -290,6 +290,11 @@ protected ModelBuildingResult build(ModelBuildingRequest request, Collection parentIds = new LinkedHashSet<>();
List lineage = new ArrayList<>();
+ // Models built for dependency resolution (a dependency POM, one of its parents, or an
+ // imported BOM) are read at ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL rather than
+ // the default STRICT level used for the project being built.
+ boolean externalModel = isExternalModelBuildingRequest(request);
+
for (ModelData currentData = resultData; currentData != null; ) {
lineage.add(currentData);
@@ -309,8 +314,10 @@ protected ModelBuildingResult build(ModelBuildingRequest request, Collection interpolatedProfiles = getInterpolatedProfiles(rawModel, profileActivationContext, problems);
tmpModel.setProfiles(interpolatedProfiles);
- List activePomProfiles =
- profileSelector.getActiveProfiles(tmpModel.getProfiles(), profileActivationContext, problems);
+ List activePomProfiles = profileSelector.getActiveProfiles(
+ externalModel ? withoutFileAndPropertyActivation(interpolatedProfiles) : tmpModel.getProfiles(),
+ profileActivationContext,
+ problems);
List rawProfiles = new ArrayList<>();
for (Profile activePomProfile : activePomProfiles) {
@@ -320,7 +327,11 @@ protected ModelBuildingResult build(ModelBuildingRequest request, Collection mutator) {
return interpolatedActivations;
}
+ /**
+ * Determines whether the given request builds a model for dependency resolution, i.e. a POM
+ * read from a remote repository (a dependency POM, one of its parents or an imported BOM)
+ * rather than a POM belonging to the project being built. Such requests are issued with
+ * {@link ModelBuildingRequest#VALIDATION_LEVEL_MINIMAL}, see for instance
+ * {@code DefaultArtifactDescriptorReader#loadPom}.
+ */
+ private static boolean isExternalModelBuildingRequest(ModelBuildingRequest request) {
+ return request.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0;
+ }
+
+ /**
+ * Returns the profiles from the given list whose activation does not depend on a file or a
+ * property. Profiles activated by JDK version, operating system, or marked
+ * {@code activeByDefault} are unaffected, since those conditions are a function of the build
+ * platform rather than of the model content.
+ */
+ private static List withoutFileAndPropertyActivation(List profiles) {
+ List eligible = new ArrayList<>(profiles.size());
+ for (Profile profile : profiles) {
+ Activation activation = profile.getActivation();
+ if (activation == null || (activation.getFile() == null && activation.getProperty() == null)) {
+ eligible.add(profile);
+ }
+ }
+ return eligible;
+ }
+
+ /**
+ * Returns a copy of the given profile with its repositories and plugin repositories cleared.
+ */
+ private static Profile withoutRepositories(Profile profile) {
+ Profile stripped = profile.clone();
+ stripped.setRepositories(Collections.emptyList());
+ stripped.setPluginRepositories(Collections.emptyList());
+ return stripped;
+ }
+
@Override
public ModelBuildingResult build(ModelBuildingRequest request, ModelBuildingResult result)
throws ModelBuildingException {
diff --git a/compat/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java b/compat/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java
index 13b81619a531..26f17b757fbf 100644
--- a/compat/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java
+++ b/compat/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java
@@ -50,6 +50,13 @@
*/
@Deprecated(since = "4.0.0")
public abstract class AbstractStringBasedModelInterpolator implements ModelInterpolator {
+
+ /**
+ * Local mirror of {@code org.apache.maven.api.FULL_EXTERNAL_INTERPOLATION_PROPERTY}.
+ * This compat module does not depend on {@code maven-api-core}, so the value is duplicated here.
+ */
+ private static final String FULL_EXTERNAL_INTERPOLATION_PROPERTY = "maven.model.dependencyInterpolation.full";
+
private static final List PROJECT_PREFIXES = Arrays.asList("pom.", "project.");
private static final Collection TRANSLATED_PATH_EXPRESSIONS;
@@ -156,25 +163,70 @@ public Object getValue(String expression) {
valueSources.add(modelValueSource1);
- valueSources.add(new MapBasedValueSource(config.getUserProperties()));
+ // Models built at VALIDATION_LEVEL_MINIMAL are the models Maven builds while resolving
+ // dependency, parent and BOM-import POMs from a repository, not the operator's own
+ // project. Such models interpolate only against their own properties and a small set
+ // of environment-independent expressions; everything else in the user/system property
+ // space stays uninterpolated. Operator project builds use a higher validation level and
+ // keep the full set of value sources, unchanged from previous behavior.
+ boolean restricted = restrictExternalModelInterpolation(config);
+
+ ValueSource userPropertiesValueSource = new MapBasedValueSource(config.getUserProperties());
+ valueSources.add(restricted ? restrictToSafeExpressions(userPropertiesValueSource) : userPropertiesValueSource);
// Overwrite existing values in model properties. Otherwise, it's not possible
// to define them via command line e.g.: mvn -Drevision=6.5.7 ...
versionProcessor.overwriteModelProperties(modelProperties, config);
valueSources.add(new MapBasedValueSource(modelProperties));
- valueSources.add(new MapBasedValueSource(config.getSystemProperties()));
+ ValueSource systemPropertiesValueSource = new MapBasedValueSource(config.getSystemProperties());
+ valueSources.add(
+ restricted ? restrictToSafeExpressions(systemPropertiesValueSource) : systemPropertiesValueSource);
+
+ if (!restricted) {
+ valueSources.add(new AbstractValueSource(false) {
+ @Override
+ public Object getValue(String expression) {
+ return config.getSystemProperties().getProperty("env." + expression);
+ }
+ });
+ }
+
+ valueSources.add(modelValueSource2);
- valueSources.add(new AbstractValueSource(false) {
+ return valueSources;
+ }
+
+ private static boolean restrictExternalModelInterpolation(ModelBuildingRequest config) {
+ return config.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0
+ && !Boolean.parseBoolean(config.getSystemProperties().getProperty(FULL_EXTERNAL_INTERPOLATION_PROPERTY))
+ && !Boolean.parseBoolean(config.getUserProperties().getProperty(FULL_EXTERNAL_INTERPOLATION_PROPERTY));
+ }
+
+ private static ValueSource restrictToSafeExpressions(ValueSource source) {
+ return new AbstractValueSource(false) {
@Override
public Object getValue(String expression) {
- return config.getSystemProperties().getProperty("env." + expression);
+ return isSafeExternalExpression(expression) ? source.getValue(expression) : null;
}
- });
-
- valueSources.add(modelValueSource2);
+ };
+ }
- return valueSources;
+ /**
+ * Expressions that models built at {@link ModelBuildingRequest#VALIDATION_LEVEL_MINIMAL}
+ * may still resolve from the session properties: JVM- and Maven-defined properties, plus
+ * the CI-friendly version properties (MNG-5895). All other expressions are left literal.
+ */
+ private static boolean isSafeExternalExpression(String expression) {
+ return expression.startsWith("java.")
+ || expression.startsWith("os.")
+ || expression.startsWith("maven.")
+ || "file.separator".equals(expression)
+ || "path.separator".equals(expression)
+ || "line.separator".equals(expression)
+ || "revision".equals(expression)
+ || "changelist".equals(expression)
+ || "sha1".equals(expression);
}
protected List extends InterpolationPostProcessor> createPostProcessors(
diff --git a/compat/maven-model-builder/src/test/java/org/apache/maven/model/building/ExternalModelProfileActivationTest.java b/compat/maven-model-builder/src/test/java/org/apache/maven/model/building/ExternalModelProfileActivationTest.java
new file mode 100644
index 000000000000..f6163374a2be
--- /dev/null
+++ b/compat/maven-model-builder/src/test/java/org/apache/maven/model/building/ExternalModelProfileActivationTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.model.building;
+
+import java.util.Properties;
+
+import org.apache.maven.model.Model;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Models built at {@link ModelBuildingRequest#VALIDATION_LEVEL_MINIMAL} come from POMs resolved
+ * from a repository during dependency resolution (a dependency POM, one of its parents, or an
+ * imported BOM), see for instance {@code DefaultArtifactDescriptorReader#loadPom}. Their file and
+ * property activators are not evaluated, and their profiles contribute no repositories. A project
+ * build, at {@link ModelBuildingRequest#VALIDATION_LEVEL_STRICT}, still evaluates every activator.
+ * Platform-derived activation (JDK version, operating system, activeByDefault) is unaffected at
+ * either level.
+ */
+class ExternalModelProfileActivationTest {
+
+ private static final String POM = "\n"
+ + " 4.0.0\n"
+ + " thegroup\n"
+ + " withprofiles\n"
+ + " 1\n"
+ + " pom\n"
+ + " \n"
+ + " \n"
+ + " file-condition\n"
+ + " \n"
+ + " \n"
+ + " ${some.dir}\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " activated\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " property-condition\n"
+ + " \n"
+ + " \n"
+ + " some.gating.property\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " activated\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " jdk-condition\n"
+ + " \n"
+ + " [1,)\n"
+ + " \n"
+ + " \n"
+ + " activated\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " profile-repo\n"
+ + " https://repo.example.test/profile\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + "\n";
+
+ private Model build(int validationLevel) throws Exception {
+ ModelBuilder builder = new DefaultModelBuilderFactory().newInstance();
+
+ Properties systemProperties = new Properties();
+ systemProperties.putAll(System.getProperties());
+ systemProperties.setProperty("some.dir", System.getProperty("java.io.tmpdir"));
+ systemProperties.setProperty("some.gating.property", "true");
+
+ DefaultModelBuildingRequest request = new DefaultModelBuildingRequest();
+ request.setModelSource(new StringModelSource(POM));
+ request.setValidationLevel(validationLevel);
+ request.setSystemProperties(systemProperties);
+
+ return builder.build(request).getEffectiveModel();
+ }
+
+ @Test
+ void testProjectBuildEvaluatesAllActivators() throws Exception {
+ Model model = build(ModelBuildingRequest.VALIDATION_LEVEL_STRICT);
+
+ assertEquals("activated", model.getProperties().get("profile.file"));
+ assertEquals("activated", model.getProperties().get("profile.property"));
+ assertEquals("activated", model.getProperties().get("profile.jdk"));
+ assertTrue(model.getRepositories().stream().anyMatch(r -> "profile-repo".equals(r.getId())));
+ }
+
+ @Test
+ void testDependencyPomActivatesOnlyEnvironmentIndependentProfiles() throws Exception {
+ Model model = build(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
+
+ assertNull(model.getProperties().get("profile.file"));
+ assertNull(model.getProperties().get("profile.property"));
+ assertEquals("activated", model.getProperties().get("profile.jdk"));
+ assertTrue(model.getRepositories().stream().noneMatch(r -> "profile-repo".equals(r.getId())));
+ }
+}
diff --git a/compat/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java b/compat/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java
index 6d0fa698f193..76efc1514319 100644
--- a/compat/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java
+++ b/compat/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java
@@ -319,6 +319,61 @@ public void testEnvars() throws Exception {
assertEquals("/path/to/home", out.getProperties().get("outputDirectory"));
}
+ @Test
+ public void testMinimalValidationInterpolationUsesRestrictedPropertySet() throws Exception {
+ context.put("env.HOME", "/path/to/home");
+ context.put("some.property", "other-value");
+ context.put("java.version", "21");
+
+ Map modelProperties = new HashMap<>();
+ modelProperties.put("envDir", "${env.HOME}");
+ modelProperties.put("propDir", "${some.property}");
+ modelProperties.put("jdk", "${java.version}");
+
+ Model model = new Model(org.apache.maven.api.model.Model.newBuilder()
+ .properties(modelProperties)
+ .build());
+
+ ModelInterpolator interpolator = createInterpolator();
+
+ final SimpleProblemCollector collector = new SimpleProblemCollector();
+ ModelBuildingRequest config = createModelBuildingRequest(context);
+ config.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
+ Model out = interpolator.interpolateModel(model, new File("."), config, collector);
+ assertProblemFree(collector);
+
+ // At minimal validation level (the level used for models built while resolving
+ // dependency, parent and BOM-import POMs) env and arbitrary system/user properties
+ // stay literal...
+ assertEquals("${env.HOME}", out.getProperties().get("envDir"));
+ assertEquals("${some.property}", out.getProperties().get("propDir"));
+ // ...while JVM-defined and other well-known expressions keep resolving.
+ assertEquals("21", out.getProperties().get("jdk"));
+ }
+
+ @Test
+ public void testFullInterpolationOptOutRestoresPreviousBehaviorAtMinimalValidationLevel() throws Exception {
+ context.put("env.HOME", "/path/to/home");
+ context.put("maven.model.dependencyInterpolation.full", "true");
+
+ Map modelProperties = new HashMap<>();
+ modelProperties.put("envDir", "${env.HOME}");
+
+ Model model = new Model(org.apache.maven.api.model.Model.newBuilder()
+ .properties(modelProperties)
+ .build());
+
+ ModelInterpolator interpolator = createInterpolator();
+
+ final SimpleProblemCollector collector = new SimpleProblemCollector();
+ ModelBuildingRequest config = createModelBuildingRequest(context);
+ config.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
+ Model out = interpolator.interpolateModel(model, new File("."), config, collector);
+ assertProblemFree(collector);
+
+ assertEquals("/path/to/home", out.getProperties().get("envDir"));
+ }
+
@Test
public void envarExpressionThatEvaluatesToNullReturnsTheLiteralString() throws Exception {
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java
index d1175b834dc0..88d0c31b08e2 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java
@@ -136,6 +136,7 @@ public class DefaultModelBuilder implements ModelBuilder {
private static final String FILE = "file";
private static final String IMPORT = "import";
private static final String PARENT = "parent";
+ private static final String PARENT_EXTERNAL = "parent-external";
private static final String MODEL = "model";
private final Logger logger = LoggerFactory.getLogger(getClass());
@@ -279,6 +280,14 @@ List getExternalRepositories() {
// Contains both GAV coordinates (groupId:artifactId:version) and file paths
final Set parentChain;
+ // Sticky across derive(): true for a session that is itself resolving a dependency
+ // (ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY), or that was derived, directly
+ // or transitively, from such a session -- for instance a dependency's own parent POM.
+ // Kept separate from request.getRequestType() because a parent lookup always derives a
+ // CONSUMER_PARENT request regardless of what kind of session triggered it, which would
+ // otherwise lose the distinction this flag preserves.
+ final boolean externalOrigin;
+
ModelBuilderSessionState(ModelBuilderRequest request) {
this(
request.getSession(),
@@ -291,7 +300,8 @@ List getExternalRepositories() {
List.of(),
repos(request),
repos(request),
- new LinkedHashSet<>());
+ new LinkedHashSet<>(),
+ isExternalOrigin(request));
}
static List repos(ModelBuilderRequest request) {
@@ -369,7 +379,8 @@ private ModelBuilderSessionState(
List pomRepositories,
List externalRepositories,
List repositories,
- Set parentChain) {
+ Set parentChain,
+ boolean externalOrigin) {
this.session = session;
this.request = request;
this.result = result;
@@ -381,6 +392,7 @@ private ModelBuilderSessionState(
this.externalRepositories = externalRepositories;
this.repositories = repositories;
this.parentChain = parentChain;
+ this.externalOrigin = externalOrigin;
this.result.setSource(this.request.getSource());
}
@@ -434,6 +446,7 @@ private ModelBuilderSessionState derive(
derivedRepos = repositoryFactory.aggregate(session, pomRepositories, derivedExtRepos, false);
}
}
+ boolean derivedExternalOrigin = externalOrigin || isExternalOrigin(request);
return new ModelBuilderSessionState(
session,
request,
@@ -445,7 +458,8 @@ private ModelBuilderSessionState derive(
pomRepositories,
derivedExtRepos,
derivedRepos,
- new LinkedHashSet<>());
+ new LinkedHashSet<>(),
+ derivedExternalOrigin);
}
@Override
@@ -648,7 +662,13 @@ public void mergeRepositories(Model model, boolean replace) {
&& (repo.getId() == null || !repo.getId().contains("${")))
.map(session::createRemoteRepository)
.toList();
- if (replace) {
+ // Repositories contributed by a model resolved from a repository are merged
+ // recessively; repositories supplied by the request or session keep precedence.
+ // Note: the isBuildRequest() guard means any future non-build RequestType will
+ // also use recessive merging (the else branch). This is intentional — only a
+ // build request has a well-defined set of session/request repositories that
+ // should take precedence; dependency and parent resolution do not.
+ if (replace && isBuildRequest()) {
Set ids = repos.stream().map(RemoteRepository::getId).collect(Collectors.toSet());
repositories = repositories.stream()
.filter(r -> !ids.contains(r.getId()))
@@ -1618,12 +1638,37 @@ private void addActivePomProfiles(String modelId, List activePomProfile
private List getActiveProfiles(
Collection interpolatedProfiles, DefaultProfileActivationContext profileActivationContext) {
if (isBuildRequestWithActivation()) {
- return profileSelector.getActiveProfiles(interpolatedProfiles, profileActivationContext, this);
+ Collection eligibleProfiles = interpolatedProfiles;
+ if (externalOrigin) {
+ // A model resolved to satisfy dependency resolution -- a dependency POM
+ // itself, or one of its parents, reached transitively -- evaluates only
+ // platform-derived activation (JDK version, operating system,
+ // activeByDefault); its profiles contribute no repositories.
+ eligibleProfiles = interpolatedProfiles.stream()
+ .filter(profile -> !hasFileOrPropertyOrConditionActivation(profile))
+ .map(profile -> profile.withRepositories(List.of()).withPluginRepositories(List.of()))
+ .toList();
+ }
+ return profileSelector.getActiveProfiles(eligibleProfiles, profileActivationContext, this);
} else {
return List.of();
}
}
+ /**
+ * Determines whether the given profile's activation depends on file existence, a
+ * property, or a condition expression, as opposed to being a function of the build
+ * platform (JDK version, operating system) or {@code activeByDefault}.
+ */
+ private static boolean hasFileOrPropertyOrConditionActivation(Profile profile) {
+ Activation activation = profile.getActivation();
+ return activation != null
+ && (activation.getFile() != null
+ || activation.getProperty() != null
+ || (activation.getCondition() != null
+ && !activation.getCondition().isBlank()));
+ }
+
Model readFileModel() throws ModelBuilderException {
return readFileModel(new HashSet<>());
}
@@ -2024,8 +2069,23 @@ private record ParentModelWithProfiles(Model model, List activatedProfi
*/
Model readAsParentModel(DefaultProfileActivationContext profileActivationContext, Set parentChain)
throws ModelBuilderException {
+ // Partition the cache by externalOrigin so a parent model resolved while building
+ // the operator's own project never shares an entry with the same source resolved
+ // while resolving a dependency: the two contexts activate profiles differently (see
+ // getActiveProfiles below), and the model built for one must not be reused for the
+ // other, even though both are keyed off the same underlying source.
+ //
+ // This partition is a defensive backstop, not the primary guard: cache(source, tag,
+ // supplier) additionally scopes each entry to the top-level request (see
+ // getOuterRequest()), which falls back to the request object's own identity once its
+ // RequestTrace has no further request-typed ancestor. Two independently-built request
+ // objects therefore land in different buckets regardless of this tag, and never reach
+ // this collision in practice; the tag matters only when two reads end up sharing a
+ // request object (as derive() calls from a common ancestor can), which is why it is
+ // kept even though the getActiveProfiles gate above already decides the correct
+ // activation for each read on its own.
Map parentsPerContext =
- cache(request.getSource(), PARENT, ConcurrentHashMap::new);
+ cache(request.getSource(), externalOrigin ? PARENT_EXTERNAL : PARENT, ConcurrentHashMap::new);
for (Map.Entry e :
parentsPerContext.entrySet()) {
@@ -2337,11 +2397,13 @@ private Model doLoadDependencyManagement(
Collection importIds) {
Model importModel;
ModelSource importSource;
+ boolean repositoryResolved = false;
try {
importSource = resolveReactorModel(groupId, artifactId, version);
if (importSource == null) {
importSource = modelResolver.resolveModel(
request.getSession(), repositories, dependency, new AtomicReference<>());
+ repositoryResolved = true;
}
} catch (ModelBuilderException | ModelResolverException e) {
StringBuilder buffer = new StringBuilder(256);
@@ -2392,9 +2454,68 @@ private Model doLoadDependencyManagement(
importModel = importResult.getEffectiveModel();
+ if (repositoryResolved) {
+ importModel = rejectSystemScopeFromRepositoryImport(importModel, dependency);
+ }
+
return importModel;
}
+ /**
+ * Dependency management imported (as a BOM) from a POM resolved from a repository, rather
+ * than from the local reactor, may not declare {@code system} scope or a
+ * {@code systemPath} for a managed dependency: by default, offending entries are dropped
+ * from the imported management (so a cached import cannot re-introduce them) and a
+ * warning is emitted, unless the
+ * {@code maven.repository.dependencyManagement.allowSystemScope} user property is set to
+ * {@code true}, in which case they are imported as before, with a warning. Dependency
+ * management imported from the local reactor is not affected.
+ */
+ private Model rejectSystemScopeFromRepositoryImport(Model importModel, Dependency dependency) {
+ DependencyManagement importMgmt = importModel != null ? importModel.getDependencyManagement() : null;
+ if (importMgmt == null) {
+ return importModel;
+ }
+ String offending = importMgmt.getDependencies().stream()
+ .filter(DefaultModelBuilder::usesSystemScope)
+ .map(Dependency::getManagementKey)
+ .collect(Collectors.joining(", "));
+ if (offending.isEmpty()) {
+ return importModel;
+ }
+ String allow = request.getUserProperties()
+ .getOrDefault(
+ Constants.MAVEN_REPOSITORY_DEPENDENCY_MANAGEMENT_ALLOW_SYSTEM_SCOPE,
+ request.getSystemProperties()
+ .get(Constants.MAVEN_REPOSITORY_DEPENDENCY_MANAGEMENT_ALLOW_SYSTEM_SCOPE));
+ if (Boolean.parseBoolean(allow)) {
+ add(
+ Severity.WARNING,
+ Version.V41,
+ "The import POM " + ModelProblemUtils.toId(importModel)
+ + " declares 'system' scope or 'systemPath' for " + offending
+ + "; importing it because the '"
+ + Constants.MAVEN_REPOSITORY_DEPENDENCY_MANAGEMENT_ALLOW_SYSTEM_SCOPE
+ + "' user property is set to 'true'.",
+ dependency.getLocation(""));
+ return importModel;
+ }
+ add(
+ Severity.WARNING,
+ Version.V41,
+ "The import POM " + ModelProblemUtils.toId(importModel)
+ + " was resolved from a repository and declares 'system' scope or 'systemPath' for "
+ + offending + "; these entries are not imported. Remove the 'system' scope from the"
+ + " imported POM, or set the '"
+ + Constants.MAVEN_REPOSITORY_DEPENDENCY_MANAGEMENT_ALLOW_SYSTEM_SCOPE
+ + "' user property to 'true' to import them as before.",
+ dependency.getLocation(""));
+ List retained = importMgmt.getDependencies().stream()
+ .filter(d -> !usesSystemScope(d))
+ .collect(Collectors.toList());
+ return importModel.withDependencyManagement(importMgmt.withDependencies(retained));
+ }
+
ModelSource resolveReactorModel(String groupId, String artifactId, String version)
throws ModelBuilderException {
Set sources = mappedSources.get(new GAKey(groupId, artifactId));
@@ -2543,6 +2664,24 @@ static String getVersion(Model model) {
return version;
}
+ /**
+ * Whether the model this request builds was resolved from a repository rather than supplied to
+ * Maven. {@link org.apache.maven.api.services.Sources#resolvedSource} carries the resolved
+ * model's coordinates and is the only source kind that does; a POM built from a file the caller
+ * pointed at reports none.
+ */
+ static boolean isExternalOrigin(ModelBuilderRequest request) {
+ return request.getRequestType() == ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY
+ && request.getSource() != null
+ && request.getSource().getModelId() != null;
+ }
+
+ static boolean usesSystemScope(Dependency dependency) {
+ return "system".equals(dependency.getScope())
+ || (dependency.getSystemPath() != null
+ && !dependency.getSystemPath().isEmpty());
+ }
+
private DefaultProfileActivationContext getProfileActivationContext(ModelBuilderRequest request, Model model) {
return new DefaultProfileActivationContext(
pathTranslator,
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelInterpolator.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelInterpolator.java
index e002f30c1dd9..688cd8d63fbb 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelInterpolator.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelInterpolator.java
@@ -31,6 +31,7 @@
import java.util.function.Function;
import java.util.function.UnaryOperator;
+import org.apache.maven.api.Constants;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
@@ -41,6 +42,7 @@
import org.apache.maven.api.services.ModelBuilderRequest;
import org.apache.maven.api.services.ModelProblem;
import org.apache.maven.api.services.ModelProblemCollector;
+import org.apache.maven.api.services.ModelSource;
import org.apache.maven.api.services.model.ModelInterpolator;
import org.apache.maven.api.services.model.PathTranslator;
import org.apache.maven.api.services.model.RootLocator;
@@ -198,8 +200,19 @@ String doCallback(
return new MavenBuildTimestamp(request.getSession().getStartTime(), model.getProperties())
.formattedTimestamp();
}
+ // Models built at ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY are the models
+ // Maven builds while resolving a dependency POM from a repository, not the operator's
+ // own project (see DefaultModelBuilder#resolveAndReadParentExternally and
+ // DefaultArtifactDescriptorReader for the equivalent path used before this builder is
+ // invoked). Such models interpolate their user/system properties only against a small,
+ // environment-independent allowlist; everything else, including environment variables
+ // and arbitrary -D properties, is left as a literal unresolved expression.
+ boolean restricted = restrictExternalModelInterpolation(request);
+
// user properties
- String value = request.getUserProperties().get(expression);
+ String value = (!restricted || isSafeExternalExpression(expression))
+ ? request.getUserProperties().get(expression)
+ : null;
// model properties (check before prefixed model reflection to avoid recursion)
if (value == null) {
value = model.getProperties().get(expression);
@@ -217,11 +230,11 @@ String doCallback(
}
}
// system properties
- if (value == null) {
+ if (value == null && (!restricted || isSafeExternalExpression(expression))) {
value = request.getSystemProperties().get(expression);
}
// environment variables
- if (value == null) {
+ if (value == null && !restricted) {
value = request.getSystemProperties().get("env." + expression);
}
// un-prefixed model reflection
@@ -231,6 +244,46 @@ String doCallback(
return value;
}
+ private static boolean restrictExternalModelInterpolation(ModelBuilderRequest request) {
+ ModelBuilderRequest.RequestType type = request.getRequestType();
+ boolean externalModel = (type == ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY
+ || type == ModelBuilderRequest.RequestType.CONSUMER_PARENT)
+ && isRepositoryResolved(request.getSource());
+ return externalModel
+ && !Boolean.parseBoolean(
+ request.getSystemProperties().get(Constants.MAVEN_MODEL_DEPENDENCY_INTERPOLATION_FULL))
+ && !Boolean.parseBoolean(
+ request.getUserProperties().get(Constants.MAVEN_MODEL_DEPENDENCY_INTERPOLATION_FULL));
+ }
+
+ /**
+ * Whether the given source is one Maven resolved from a repository rather than one supplied to
+ * it. {@link org.apache.maven.api.services.Sources#resolvedSource} carries the resolved model's
+ * coordinates, and it is the only source kind that does; a POM built from a file the caller
+ * pointed at reports none.
+ */
+ private static boolean isRepositoryResolved(ModelSource source) {
+ return source != null && source.getModelId() != null;
+ }
+
+ /**
+ * Expressions that models built at {@link ModelBuilderRequest.RequestType#CONSUMER_DEPENDENCY}
+ * or {@link ModelBuilderRequest.RequestType#CONSUMER_PARENT} may still resolve from the
+ * session properties: JVM- and Maven-defined properties, plus the CI-friendly version
+ * properties (MNG-5895). All other expressions are left literal.
+ */
+ private static boolean isSafeExternalExpression(String expression) {
+ return expression.startsWith("java.")
+ || expression.startsWith("os.")
+ || expression.startsWith("maven.")
+ || "file.separator".equals(expression)
+ || "path.separator".equals(expression)
+ || "line.separator".equals(expression)
+ || "revision".equals(expression)
+ || "changelist".equals(expression)
+ || "sha1".equals(expression);
+ }
+
String projectProperty(Model model, Path projectDir, String subExpr, boolean prefixed) {
if (projectDir != null) {
if (subExpr.equals("basedir")) {
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java
index 8c6dcd6bab4a..bb27c17abf08 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java
@@ -42,10 +42,12 @@
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.services.BuilderProblem;
import org.apache.maven.api.services.ModelBuilder;
import org.apache.maven.api.services.ModelBuilderRequest;
import org.apache.maven.api.services.ModelBuilderResult;
import org.apache.maven.api.services.ModelProblem;
+import org.apache.maven.api.services.ModelSource;
import org.apache.maven.api.services.Sources;
import org.apache.maven.impl.DefaultRemoteRepository;
import org.apache.maven.impl.standalone.ApiRunner;
@@ -120,6 +122,285 @@ public void testPropertiesAndProfiles() {
assertEquals("21", result.getEffectiveModel().getProperties().get("maven.compiler.release"));
}
+ /**
+ * Models built at {@link ModelBuilderRequest.RequestType#CONSUMER_DEPENDENCY} come from a
+ * dependency POM resolved from a repository. Their file, property, and condition activators
+ * are not evaluated, and their profiles contribute no repositories. A project build, at
+ * {@link ModelBuilderRequest.RequestType#BUILD_PROJECT}, still evaluates every activator.
+ * Platform-derived activation (JDK version, operating system, activeByDefault) is unaffected
+ * at either level.
+ */
+ private ModelBuilderRequest.ModelBuilderRequestBuilder resolvedProfilesRequest(
+ ModelBuilderRequest.RequestType requestType) {
+ Map systemProperties = new HashMap<>();
+ for (String name : System.getProperties().stringPropertyNames()) {
+ systemProperties.put(name, System.getProperty(name));
+ }
+ systemProperties.put("some.dir", System.getProperty("java.io.tmpdir"));
+ systemProperties.put("some.gating.property", "true");
+ systemProperties.put("some.condition.property", "true");
+ return ModelBuilderRequest.builder()
+ .session(session)
+ .requestType(requestType)
+ .systemProperties(systemProperties)
+ .source(Sources.buildSource(getPom("resolved-model-with-profiles")));
+ }
+
+ @Test
+ public void testProjectBuildEvaluatesAllActivators() {
+ ModelBuilderRequest request = resolvedProfilesRequest(ModelBuilderRequest.RequestType.BUILD_PROJECT)
+ .build();
+ Model model = builder.newSession().build(request).getEffectiveModel();
+
+ assertEquals("activated", model.getProperties().get("profile.file"));
+ assertEquals("activated", model.getProperties().get("profile.property"));
+ assertEquals("activated", model.getProperties().get("profile.condition"));
+ assertEquals("activated", model.getProperties().get("profile.jdk"));
+ assertTrue(model.getRepositories().stream().anyMatch(r -> "profile-repo".equals(r.getId())));
+ }
+
+ @Test
+ public void testDependencyModelActivatesOnlyEnvironmentIndependentProfiles() {
+ ModelBuilderRequest request = resolvedProfilesRequest(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
+ .source(Sources.resolvedSource(
+ getPom("resolved-model-with-profiles"),
+ "org.apache.maven.test:resolved-model-with-profiles:1.0.0"))
+ .build();
+ Model model = builder.newSession().build(request).getEffectiveModel();
+
+ assertNull(model.getProperties().get("profile.file"));
+ assertNull(model.getProperties().get("profile.property"));
+ assertNull(model.getProperties().get("profile.condition"));
+ assertEquals("activated", model.getProperties().get("profile.jdk"));
+ assertTrue(model.getRepositories().stream().noneMatch(r -> "profile-repo".equals(r.getId())));
+ }
+
+ /**
+ * A model built at {@link ModelBuilderRequest.RequestType#CONSUMER_DEPENDENCY} whose source
+ * is one Maven was merely pointed at -- {@link Sources#buildSource} rather than a source
+ * Maven resolved from a repository -- is not treated as coming from a repository. Every
+ * activator still runs, exactly as for a project build.
+ */
+ @Test
+ public void testCallerSuppliedModelActivatesAllProfiles() {
+ ModelBuilderRequest request = resolvedProfilesRequest(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
+ .build();
+ Model model = builder.newSession().build(request).getEffectiveModel();
+
+ assertEquals("activated", model.getProperties().get("profile.file"));
+ assertEquals("activated", model.getProperties().get("profile.property"));
+ assertEquals("activated", model.getProperties().get("profile.condition"));
+ assertEquals("activated", model.getProperties().get("profile.jdk"));
+ assertTrue(model.getRepositories().stream().anyMatch(r -> "profile-repo".equals(r.getId())));
+ }
+
+ private Map parentActivationSystemProperties() {
+ Map systemProperties = new HashMap<>();
+ for (String name : System.getProperties().stringPropertyNames()) {
+ systemProperties.put(name, System.getProperty(name));
+ }
+ systemProperties.put("some.dir", System.getProperty("java.io.tmpdir"));
+ systemProperties.put("some.gating.property", "true");
+ systemProperties.put("some.condition.property", "true");
+ return systemProperties;
+ }
+
+ private DefaultProfileActivationContext parentActivationContext(Map systemProperties) {
+ org.apache.maven.api.services.Lookup lookup = session.getService(org.apache.maven.api.services.Lookup.class);
+ return new DefaultProfileActivationContext(
+ lookup.lookup(org.apache.maven.api.services.model.PathTranslator.class),
+ lookup.lookup(org.apache.maven.api.services.model.RootLocator.class),
+ lookup.lookup(org.apache.maven.api.services.Interpolator.class),
+ List.of(),
+ List.of(),
+ systemProperties,
+ Map.of(),
+ Model.newInstance());
+ }
+
+ /**
+ * readAsParentModel() caches by (source, tag), where tag depends on externalOrigin, through
+ * a generic per-request cache keyed off the request object itself. To exercise that
+ * partition directly -- independent of how deep a real build's RequestTrace ancestry happens
+ * to be -- both reads below derive from the exact same request instance, differing only in
+ * which parent session state (one already externalOrigin=true, one false) they derive from.
+ * That guarantees both calls address the same underlying per-request cache bucket, so this
+ * proves the tag partition itself separates them, not an accidental difference elsewhere.
+ */
+ @Test
+ public void testResolvedDependencyParentCacheDoesNotShareActivationWithProjectParent() throws Exception {
+ // Trigger mainSession creation with a regular project build.
+ ModelBuilderRequest request = ModelBuilderRequest.builder()
+ .session(session)
+ .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
+ .source(Sources.buildSource(getPom("props-and-profiles")))
+ .build();
+ ModelBuilder.ModelBuilderSession mbs = builder.newSession();
+ mbs.build(request);
+ DefaultModelBuilder.ModelBuilderSessionState mainState =
+ ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession;
+
+ Map systemProperties = parentActivationSystemProperties();
+ ModelSource parentSource = Sources.buildSource(getPom("resolved-model-with-profiles"));
+
+ // A session already carrying externalOrigin=true, as a dependency's own session would
+ // after resolving through at least one CONSUMER_DEPENDENCY hop.
+ DefaultModelBuilder.ModelBuilderSessionState dependencyAncestorState =
+ mainState.derive(ModelBuilderRequest.builder(mainState.request)
+ .requestType(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
+ .source(Sources.resolvedSource(
+ getPom("props-and-profiles"), "org.apache.maven.test:props-and-profiles:1.0.0"))
+ .build());
+ assertTrue(dependencyAncestorState.externalOrigin);
+
+ // The exact same request instance is then used to derive a parent-lookup session from
+ // each ancestor. Sharing one request object guarantees both reads address the same
+ // per-request cache bucket, regardless of RequestTrace ancestry depth.
+ ModelBuilderRequest sharedParentRequest = ModelBuilderRequest.builder(mainState.request)
+ .requestType(ModelBuilderRequest.RequestType.CONSUMER_PARENT)
+ .source(parentSource)
+ .systemProperties(systemProperties)
+ .build();
+
+ DefaultModelBuilder.ModelBuilderSessionState projectState = mainState.derive(sharedParentRequest);
+ assertFalse(projectState.externalOrigin, "derived from a BUILD_PROJECT ancestor, must not be external");
+ Model projectParentModel =
+ projectState.readAsParentModel(parentActivationContext(systemProperties), new HashSet<>());
+
+ assertEquals("activated", projectParentModel.getProperties().get("profile.file"));
+ assertEquals("activated", projectParentModel.getProperties().get("profile.property"));
+ assertEquals("activated", projectParentModel.getProperties().get("profile.condition"));
+ assertEquals("activated", projectParentModel.getProperties().get("profile.jdk"));
+ assertTrue(projectParentModel.getRepositories().stream().anyMatch(r -> "profile-repo".equals(r.getId())));
+
+ DefaultModelBuilder.ModelBuilderSessionState dependencyState =
+ dependencyAncestorState.derive(sharedParentRequest);
+ assertTrue(dependencyState.externalOrigin, "derived from a CONSUMER_DEPENDENCY ancestor, must stay external");
+ Model dependencyParentModel =
+ dependencyState.readAsParentModel(parentActivationContext(systemProperties), new HashSet<>());
+
+ assertNull(dependencyParentModel.getProperties().get("profile.file"));
+ assertNull(dependencyParentModel.getProperties().get("profile.property"));
+ assertNull(dependencyParentModel.getProperties().get("profile.condition"));
+ assertEquals("activated", dependencyParentModel.getProperties().get("profile.jdk"));
+ assertTrue(dependencyParentModel.getRepositories().stream().noneMatch(r -> "profile-repo".equals(r.getId())));
+
+ // The first (fully activated) result must not have been altered by the second read.
+ assertEquals("activated", projectParentModel.getProperties().get("profile.file"));
+ assertTrue(projectParentModel.getRepositories().stream().anyMatch(r -> "profile-repo".equals(r.getId())));
+
+ // Repeating with a second, fresh shared request -- dependency read first this time --
+ // must not bleed the other way either.
+ ModelBuilderRequest sharedParentRequest2 = ModelBuilderRequest.builder(mainState.request)
+ .requestType(ModelBuilderRequest.RequestType.CONSUMER_PARENT)
+ .source(parentSource)
+ .systemProperties(systemProperties)
+ .build();
+
+ DefaultModelBuilder.ModelBuilderSessionState dependencyState2 =
+ dependencyAncestorState.derive(sharedParentRequest2);
+ Model dependencyParentModel2 =
+ dependencyState2.readAsParentModel(parentActivationContext(systemProperties), new HashSet<>());
+ assertNull(dependencyParentModel2.getProperties().get("profile.file"));
+ assertTrue(dependencyParentModel2.getRepositories().stream().noneMatch(r -> "profile-repo".equals(r.getId())));
+
+ DefaultModelBuilder.ModelBuilderSessionState projectState2 = mainState.derive(sharedParentRequest2);
+ Model projectParentModel2 =
+ projectState2.readAsParentModel(parentActivationContext(systemProperties), new HashSet<>());
+ assertEquals("activated", projectParentModel2.getProperties().get("profile.file"));
+ assertTrue(projectParentModel2.getRepositories().stream().anyMatch(r -> "profile-repo".equals(r.getId())));
+ }
+
+ /**
+ * The externalOrigin flag must survive more than one {@code derive()} hop: a dependency's
+ * own parent (itself read as a CONSUMER_PARENT session, not CONSUMER_DEPENDENCY) must still
+ * be treated as external, since request.getRequestType() alone cannot carry that distinction
+ * once a parent lookup has overwritten it.
+ */
+ @Test
+ public void testExternalOriginPropagatesThroughGrandparentHop() throws Exception {
+ ModelBuilderRequest request = ModelBuilderRequest.builder()
+ .session(session)
+ .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
+ .source(Sources.buildSource(getPom("props-and-profiles")))
+ .build();
+ ModelBuilder.ModelBuilderSession mbs = builder.newSession();
+ mbs.build(request);
+ DefaultModelBuilder.ModelBuilderSessionState mainState =
+ ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession;
+
+ Map systemProperties = parentActivationSystemProperties();
+ ModelSource dependencySource =
+ Sources.resolvedSource(getPom("props-and-profiles"), "org.apache.maven.test:props-and-profiles:1.0.0");
+ ModelSource grandparentSource = Sources.buildSource(getPom("resolved-model-with-profiles"));
+
+ // Hop 1: a dependency's own POM, resolved from a repository. externalOrigin becomes
+ // true here.
+ DefaultModelBuilder.ModelBuilderSessionState dependencyState =
+ mainState.derive(ModelBuilderRequest.builder(mainState.request)
+ .requestType(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
+ .source(dependencySource)
+ .systemProperties(systemProperties)
+ .build());
+ assertTrue(dependencyState.externalOrigin);
+
+ // Hop 2: that dependency's own parent. Its own request type is CONSUMER_PARENT, not
+ // CONSUMER_DEPENDENCY -- externalOrigin must still be true, inherited from hop 1.
+ DefaultModelBuilder.ModelBuilderSessionState grandparentHopState =
+ dependencyState.derive(ModelBuilderRequest.builder(dependencyState.request)
+ .requestType(ModelBuilderRequest.RequestType.CONSUMER_PARENT)
+ .source(grandparentSource)
+ .systemProperties(systemProperties)
+ .build());
+ assertTrue(
+ grandparentHopState.externalOrigin,
+ "externalOrigin must survive a second derive() hop, not just the first");
+
+ Model grandparentModel =
+ grandparentHopState.readAsParentModel(parentActivationContext(systemProperties), new HashSet<>());
+
+ assertNull(grandparentModel.getProperties().get("profile.file"));
+ assertNull(grandparentModel.getProperties().get("profile.property"));
+ assertNull(grandparentModel.getProperties().get("profile.condition"));
+ assertEquals("activated", grandparentModel.getProperties().get("profile.jdk"));
+ assertTrue(grandparentModel.getRepositories().stream().noneMatch(r -> "profile-repo".equals(r.getId())));
+ }
+
+ /**
+ * {@code BUILD_CONSUMER} requests already skip all profile activation
+ * ({@code isBuildRequestWithActivation()} returns false for that type) -- unrelated to, and
+ * unaffected by, the externalOrigin distinction. Locking that down explicitly since it is
+ * adjacent code this change reads but does not modify.
+ */
+ @Test
+ public void testBuildConsumerSkipsAllProfileActivation() throws Exception {
+ ModelBuilderRequest request = ModelBuilderRequest.builder()
+ .session(session)
+ .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
+ .source(Sources.buildSource(getPom("props-and-profiles")))
+ .build();
+ ModelBuilder.ModelBuilderSession mbs = builder.newSession();
+ mbs.build(request);
+ DefaultModelBuilder.ModelBuilderSessionState mainState =
+ ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession;
+
+ Map systemProperties = parentActivationSystemProperties();
+ DefaultModelBuilder.ModelBuilderSessionState buildConsumerState =
+ mainState.derive(ModelBuilderRequest.builder(mainState.request)
+ .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER)
+ .source(Sources.buildSource(getPom("resolved-model-with-profiles")))
+ .systemProperties(systemProperties)
+ .build());
+
+ Model model = buildConsumerState.readAsParentModel(parentActivationContext(systemProperties), new HashSet<>());
+
+ assertNull(model.getProperties().get("profile.file"));
+ assertNull(model.getProperties().get("profile.property"));
+ assertNull(model.getProperties().get("profile.condition"));
+ assertNull(model.getProperties().get("profile.jdk"));
+ assertTrue(model.getRepositories().stream().noneMatch(r -> "profile-repo".equals(r.getId())));
+ }
+
@Test
public void testMergeRepositories() throws Exception {
// this is here only to trigger mainSession creation; unrelated
@@ -177,6 +458,67 @@ public void testMergeRepositories() throws Exception {
assertEquals("central", repositories.get(2).getId()); // default
}
+ /**
+ * Repositories contributed by a model resolved from a repository are merged recessively:
+ * the repositories already held by the request or session -- e.g. "central" -- keep their
+ * id and URL. readEffectiveModel() requests replace semantics for every request type, so
+ * this is exercised directly against {@link DefaultModelBuilder.ModelBuilderSessionState}.
+ */
+ @Test
+ public void testResolvedPomRepositoryMergedRecessively() throws Exception {
+ // Trigger mainSession creation with a regular project build.
+ ModelBuilderRequest request = ModelBuilderRequest.builder()
+ .session(session)
+ .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
+ .source(Sources.buildSource(getPom("props-and-profiles")))
+ .build();
+ ModelBuilder.ModelBuilderSession mbs = builder.newSession();
+ mbs.build(request);
+ DefaultModelBuilder.ModelBuilderSessionState mainState =
+ ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession;
+
+ // A model resolved from a repository (e.g. a dependency's POM) derives its own session
+ // state the same way DefaultArtifactDescriptorReader.loadPom does.
+ ModelBuilderRequest dependencyRequest = ModelBuilderRequest.builder()
+ .session(session)
+ .requestType(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
+ .repositoryMerging(ModelBuilderRequest.RepositoryMerging.REQUEST_DOMINANT)
+ .source(Sources.resolvedSource(getPom("props-and-profiles"), "org.example:downloaded:1.0.0"))
+ .build();
+ DefaultModelBuilder.ModelBuilderSessionState state = mainState.derive(dependencyRequest);
+
+ RemoteRepository central = state.getRepositories().stream()
+ .filter(r -> "central".equals(r.getId()))
+ .findFirst()
+ .orElseThrow();
+
+ // The resolved model declares a repository that reuses the "central" id.
+ Model model = Model.newBuilder()
+ .repositories(List.of(Repository.newBuilder()
+ .id("central")
+ .url("https://secondary.example/m2")
+ .build()))
+ .build();
+
+ // readEffectiveModel() requests replace semantics for every request type.
+ state.mergeRepositories(model, true);
+
+ List repositories = state.getRepositories();
+ RemoteRepository mergedCentral = repositories.stream()
+ .filter(r -> "central".equals(r.getId()))
+ .findFirst()
+ .orElseThrow();
+ assertEquals(
+ central.getUrl(),
+ mergedCentral.getUrl(),
+ "repository declared by a resolved model must be merged recessively, keeping the"
+ + " existing repository's URL under a shared id");
+ assertTrue(
+ repositories.stream().noneMatch(r -> r.getUrl().contains("secondary.example")),
+ "repository declared by a resolved model must not enter the resolution repositories"
+ + " under an id it does not own");
+ }
+
/**
* Verifies that when multiple repositories share the same ID (e.g., after mirror injection
* maps both "central" and a profile-defined repo to the same mirror ID), their policies are
@@ -875,6 +1217,126 @@ public void testBomTypeImpliesImportWithoutScope() {
assertEquals("0.1", managed.getVersion());
}
+ /**
+ * Dependency management imported from a repository-resolved POM does not contribute
+ * {@code system} scope or a {@code systemPath}; such entries are dropped with a warning.
+ *
+ * The imported BOM is written into a temporary remote repository at run time, with a
+ * {@code systemPath} that is a real absolute path on whichever OS the test runs on
+ * (a POSIX-only path such as {@code /etc/...} is not absolute on Windows, which would make
+ * model validation reject the entry before the code under test ever ran).
+ */
+ @Test
+ public void testSystemScopeIgnoredOutsideProjectDeclaration(@TempDir Path tempDir) throws Exception {
+ Path basedir = Paths.get(System.getProperty("basedir", ""));
+ Path remoteRepoPath = tempDir.resolve("remote-repo");
+ Path bomDir = remoteRepoPath.resolve("org/apache/maven/its/system-scope-bom/1.0");
+ Files.createDirectories(bomDir);
+
+ Path systemPathFile = tempDir.resolve("provided-tool.jar");
+ Files.createFile(systemPathFile);
+
+ String bomPom = "\n" + "\n"
+ + " 4.0.0\n"
+ + " org.apache.maven.its\n"
+ + " system-scope-bom\n"
+ + " 1.0\n"
+ + " pom\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " org.apache.maven.its\n"
+ + " system-scope-companion\n"
+ + " 1.0\n"
+ + " \n"
+ + " \n"
+ + " org.apache.maven.its\n"
+ + " system-scope-dep\n"
+ + " 1.0\n"
+ + " system\n"
+ + " "
+ + systemPathFile.toAbsolutePath() + "\n" + " \n"
+ + " \n"
+ + " \n"
+ + "\n";
+ Files.writeString(bomDir.resolve("system-scope-bom-1.0.pom"), bomPom);
+
+ // default: the build succeeds, the offending entry is dropped, and a warning is emitted
+ Path localRepoPath = basedir.resolve("target/local-repo-system-scope-bom-reject");
+ Session rejectSession = ApiRunner.createSession(
+ injector -> injector.bindInstance(DefaultModelBuilderTest.class, this), localRepoPath);
+ RemoteRepository remoteRepository = rejectSession.createRemoteRepository(
+ RemoteRepository.CENTRAL_ID, remoteRepoPath.toUri().toString());
+ rejectSession = rejectSession.withRemoteRepositories(List.of(remoteRepository));
+ ModelBuilder rejectBuilder = rejectSession.getService(ModelBuilder.class);
+
+ ModelBuilderRequest request = ModelBuilderRequest.builder()
+ .session(rejectSession)
+ .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
+ .source(Sources.buildSource(getPom("import-system-scope-bom")))
+ .build();
+ ModelBuilderResult rejectResult = rejectBuilder.newSession().build(request);
+ DependencyManagement rejectManagement = rejectResult.getEffectiveModel().getDependencyManagement();
+ assertNotNull(
+ rejectManagement.getDependencies().stream()
+ .filter(d -> "system-scope-companion".equals(d.getArtifactId()))
+ .findFirst()
+ .orElse(null),
+ "The import itself must have happened: the ordinary managed entry from the "
+ + "imported BOM should be present");
+ Dependency rejected = rejectManagement.getDependencies().stream()
+ .filter(d -> "system-scope-dep".equals(d.getArtifactId()))
+ .findFirst()
+ .orElse(null);
+ assertNull(rejected, "By default the 'system' scope managed entry should not be imported");
+ assertTrue(
+ rejectResult
+ .getProblemCollector()
+ .problems()
+ .anyMatch(p -> p.getSeverity() == BuilderProblem.Severity.WARNING
+ && p.getMessage().contains("'system' scope or 'systemPath'")
+ && p.getMessage()
+ .contains(Constants.MAVEN_REPOSITORY_DEPENDENCY_MANAGEMENT_ALLOW_SYSTEM_SCOPE)),
+ "Expected a warning about 'system' scope in the repository-imported BOM");
+
+ // explicit opt-out (fresh session/local repo, so the sanitized import is not served from the cache)
+ Path allowedLocalRepoPath = basedir.resolve("target/local-repo-system-scope-bom-allow");
+ Session allowedSession = ApiRunner.createSession(
+ injector -> injector.bindInstance(DefaultModelBuilderTest.class, this), allowedLocalRepoPath);
+ allowedSession = allowedSession.withRemoteRepositories(List.of(allowedSession.createRemoteRepository(
+ RemoteRepository.CENTRAL_ID, remoteRepoPath.toUri().toString())));
+ ModelBuilder allowedBuilder = allowedSession.getService(ModelBuilder.class);
+
+ ModelBuilderRequest allowed = ModelBuilderRequest.builder()
+ .session(allowedSession)
+ .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
+ .userProperties(Map.of(Constants.MAVEN_REPOSITORY_DEPENDENCY_MANAGEMENT_ALLOW_SYSTEM_SCOPE, "true"))
+ .source(Sources.buildSource(getPom("import-system-scope-bom")))
+ .build();
+ ModelBuilderResult result = allowedBuilder.newSession().build(allowed);
+ DependencyManagement allowedManagement = result.getEffectiveModel().getDependencyManagement();
+ assertNotNull(
+ allowedManagement.getDependencies().stream()
+ .filter(d -> "system-scope-companion".equals(d.getArtifactId()))
+ .findFirst()
+ .orElse(null),
+ "The import itself must have happened: the ordinary managed entry from the "
+ + "imported BOM should be present");
+ Dependency managed = allowedManagement.getDependencies().stream()
+ .filter(d -> "system-scope-dep".equals(d.getArtifactId()))
+ .findFirst()
+ .orElse(null);
+ assertNotNull(managed, "With the opt-out property the managed entry should be imported");
+ assertEquals("system", managed.getScope());
+ assertTrue(
+ result.getProblemCollector()
+ .problems()
+ .anyMatch(p -> p.getSeverity() == BuilderProblem.Severity.WARNING
+ && p.getMessage()
+ .contains(Constants.MAVEN_REPOSITORY_DEPENDENCY_MANAGEMENT_ALLOW_SYSTEM_SCOPE)),
+ "Opting out should still emit a warning about the imported 'system' scope");
+ }
+
@Test
void testBomImportWarningsReportedWhereImportsAreDeclared() {
Path pom = Paths.get("src/test/resources/poms/factory/mng-8450/pom.xml").toAbsolutePath();
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelInterpolatorTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelInterpolatorTest.java
index 065bc79ecd5e..4ee3e14c599d 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelInterpolatorTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelInterpolatorTest.java
@@ -36,6 +36,7 @@
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
+import org.apache.maven.api.Constants;
import org.apache.maven.api.Session;
import org.apache.maven.api.di.Priority;
import org.apache.maven.api.di.Provides;
@@ -48,6 +49,7 @@
import org.apache.maven.api.model.Scm;
import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.services.ModelBuilderRequest;
+import org.apache.maven.api.services.Sources;
import org.apache.maven.api.services.model.ModelInterpolator;
import org.apache.maven.api.services.model.RootLocator;
import org.apache.maven.impl.model.profile.SimpleProblemCollector;
@@ -443,6 +445,101 @@ public void testEnvars() throws Exception {
assertEquals("/path/to/home", out.getProperties().get("outputDirectory"));
}
+ @Test
+ public void testDependencyModelInterpolationUsesRestrictedPropertySet() throws Exception {
+ context.put("env.HOME", "/path/to/home");
+ context.put("some.property", "other-value");
+ context.put("java.version", "21");
+
+ Map modelProperties = new HashMap<>();
+ modelProperties.put("envDir", "${env.HOME}");
+ modelProperties.put("propDir", "${some.property}");
+ modelProperties.put("jdk", "${java.version}");
+
+ Model model = Model.newBuilder().properties(modelProperties).build();
+
+ final SimpleProblemCollector collector = new SimpleProblemCollector();
+ ModelBuilderRequest request = createModelBuildingRequest(context)
+ .requestType(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
+ .source(Sources.resolvedSource(Paths.get("."), "org.apache.maven.test:dependency:1.0.0"))
+ .build();
+ Model out = interpolator.interpolateModel(model, Paths.get("."), request, collector);
+ assertProblemFree(collector);
+
+ // A model built while resolving a dependency POM from a repository does not interpolate
+ // environment variables or arbitrary system/user properties...
+ assertEquals("${env.HOME}", out.getProperties().get("envDir"));
+ assertEquals("${some.property}", out.getProperties().get("propDir"));
+ // ...while JVM-defined and other well-known expressions keep resolving.
+ assertEquals("21", out.getProperties().get("jdk"));
+ }
+
+ @Test
+ public void testParentModelInterpolationUsesRestrictedPropertySet() throws Exception {
+ context.put("env.HOME", "/path/to/home");
+
+ Map modelProperties = new HashMap<>();
+ modelProperties.put("envDir", "${env.HOME}");
+
+ Model model = Model.newBuilder().properties(modelProperties).build();
+
+ final SimpleProblemCollector collector = new SimpleProblemCollector();
+ ModelBuilderRequest request = createModelBuildingRequest(context)
+ .requestType(ModelBuilderRequest.RequestType.CONSUMER_PARENT)
+ .source(Sources.resolvedSource(Paths.get("."), "org.apache.maven.test:parent:1.0.0"))
+ .build();
+ Model out = interpolator.interpolateModel(model, Paths.get("."), request, collector);
+ assertProblemFree(collector);
+
+ assertEquals("${env.HOME}", out.getProperties().get("envDir"));
+ }
+
+ @Test
+ public void testCallerSuppliedModelInterpolationIsNotRestricted() throws Exception {
+ context.put("env.HOME", "/path/to/home");
+ context.put("some.property", "other-value");
+
+ Map modelProperties = new HashMap<>();
+ modelProperties.put("envDir", "${env.HOME}");
+ modelProperties.put("propDir", "${some.property}");
+
+ Model model = Model.newBuilder().properties(modelProperties).build();
+
+ final SimpleProblemCollector collector = new SimpleProblemCollector();
+ // A POM the caller hands to Maven as a file arrives with the same CONSUMER_DEPENDENCY
+ // request type as a dependency POM resolved from a repository, but its source is not
+ // one Maven resolved -- so the restricted property set above does not apply to it.
+ ModelBuilderRequest request = createModelBuildingRequest(context)
+ .requestType(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
+ .source(Sources.buildSource(Paths.get(".")))
+ .build();
+ Model out = interpolator.interpolateModel(model, Paths.get("."), request, collector);
+ assertProblemFree(collector);
+
+ assertEquals("/path/to/home", out.getProperties().get("envDir"));
+ assertEquals("other-value", out.getProperties().get("propDir"));
+ }
+
+ @Test
+ public void testFullInterpolationOptOutRestoresPreviousBehaviorForResolvedDependencyModel() throws Exception {
+ context.put("env.HOME", "/path/to/home");
+ context.put(Constants.MAVEN_MODEL_DEPENDENCY_INTERPOLATION_FULL, "true");
+
+ Map modelProperties = new HashMap<>();
+ modelProperties.put("envDir", "${env.HOME}");
+
+ Model model = Model.newBuilder().properties(modelProperties).build();
+
+ final SimpleProblemCollector collector = new SimpleProblemCollector();
+ ModelBuilderRequest request = createModelBuildingRequest(context)
+ .requestType(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
+ .build();
+ Model out = interpolator.interpolateModel(model, Paths.get("."), request, collector);
+ assertProblemFree(collector);
+
+ assertEquals("/path/to/home", out.getProperties().get("envDir"));
+ }
+
@Test
public void envarExpressionThatEvaluatesToNullReturnsTheLiteralString() throws Exception {
diff --git a/impl/maven-impl/src/test/resources/poms/factory/import-system-scope-bom.xml b/impl/maven-impl/src/test/resources/poms/factory/import-system-scope-bom.xml
new file mode 100644
index 000000000000..616bb010e51f
--- /dev/null
+++ b/impl/maven-impl/src/test/resources/poms/factory/import-system-scope-bom.xml
@@ -0,0 +1,35 @@
+
+
+
+ org.apache.maven.tests
+ import-system-scope-bom
+ 1.0-SNAPSHOT
+ jar
+
+
+
+
+ org.apache.maven.its
+ system-scope-bom
+ 1.0
+ pom
+ import
+
+
+
+
diff --git a/impl/maven-impl/src/test/resources/poms/factory/resolved-model-with-profiles.xml b/impl/maven-impl/src/test/resources/poms/factory/resolved-model-with-profiles.xml
new file mode 100644
index 000000000000..46a923eba6de
--- /dev/null
+++ b/impl/maven-impl/src/test/resources/poms/factory/resolved-model-with-profiles.xml
@@ -0,0 +1,71 @@
+
+
+
+ org.apache.maven.tests
+ resolved-model-with-profiles
+ 1.0-SNAPSHOT
+ pom
+
+
+ file-condition
+
+
+ ${some.dir}
+
+
+
+ activated
+
+
+
+ property-condition
+
+
+ some.gating.property
+
+
+
+ activated
+
+
+
+ condition-condition
+
+ ${some.condition.property} == 'true'
+
+
+ activated
+
+
+
+ jdk-condition
+
+ [1,)
+
+
+ activated
+
+
+
+ profile-repo
+ https://repo.example.test/profile
+
+
+
+
+
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenIT0085TransitiveSystemScopeTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenIT0085TransitiveSystemScopeTest.java
index d0a2df967bc8..d158239b7a80 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenIT0085TransitiveSystemScopeTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenIT0085TransitiveSystemScopeTest.java
@@ -24,6 +24,7 @@
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
public class MavenIT0085TransitiveSystemScopeTest extends AbstractMavenIntegrationTestCase {
public MavenIT0085TransitiveSystemScopeTest() {
@@ -51,10 +52,40 @@ public void testit0085() throws Exception {
verifier.addCliArgument("--settings");
verifier.addCliArgument("settings.xml");
verifier.addCliArgument("validate");
+ verifier.addCliArgument("-Dmaven.model.dependencyInterpolation.full=true");
verifier.execute();
verifier.verifyErrorFreeLog();
Collection lines = verifier.loadLines("target/test.txt");
assertTrue(lines.contains("system.jar"), lines.toString());
}
+
+ /**
+ * Verify that by default the path of a system-scope dependency declared by a POM resolved from a
+ * repository is not interpolated from the session properties, so the expression is left as
+ * written and reported as an invalid path.
+ *
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void testit0085DefaultLeavesPropertyUnresolved() throws Exception {
+ File testDir = extractResources("/it0085");
+
+ Verifier verifier = newVerifier(testDir.getAbsolutePath());
+ verifier.setAutoclean(false);
+ verifier.deleteDirectory("target");
+ verifier.deleteArtifacts("org.apache.maven.its.it0085");
+ verifier.getSystemProperties().setProperty("test.home", testDir.getAbsolutePath());
+ verifier.filterFile("settings-template.xml", "settings.xml");
+ verifier.addCliArgument("--settings");
+ verifier.addCliArgument("settings.xml");
+ verifier.addCliArgument("validate");
+ try {
+ verifier.execute();
+ verifier.verifyErrorFreeLog();
+ fail("Build should not succeed");
+ } catch (VerificationException e) {
+ verifier.verifyTextInLog("must specify an absolute path but is ${test.home}/system.jar");
+ }
+ }
}
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3586SystemScopePluginDependencyTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3586SystemScopePluginDependencyTest.java
index 26e66afb433a..16965d590773 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3586SystemScopePluginDependencyTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3586SystemScopePluginDependencyTest.java
@@ -24,6 +24,7 @@
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
/**
* This is a test set for MNG-3586.
@@ -56,6 +57,7 @@ public void testitFromPlugin() throws Exception {
verifier.addCliArgument("--settings");
verifier.addCliArgument("settings.xml");
verifier.addCliArgument("validate");
+ verifier.addCliArgument("-Dmaven.model.dependencyInterpolation.full=true");
verifier.execute();
verifier.verifyErrorFreeLog();
@@ -84,4 +86,33 @@ public void testitFromProject() throws Exception {
Properties props = verifier.loadProperties("target/pcl.properties");
assertEquals("1", props.getProperty("maven-core-it.properties.count"));
}
+
+ /**
+ * Test that by default a plugin POM resolved from a repository does not interpolate the path of
+ * its system-scope dependency from the session properties, so the expression is left as written
+ * and reported as an invalid path.
+ *
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void testitFromPluginDefaultLeavesPropertyUnresolved() throws Exception {
+ File testDir = extractResources("/mng-3586/test-1");
+
+ Verifier verifier = newVerifier(testDir.getAbsolutePath());
+ verifier.setAutoclean(false);
+ verifier.deleteDirectory("target");
+ verifier.deleteArtifacts("org.apache.maven.its.mng3586");
+ verifier.getSystemProperties().setProperty("test.home", testDir.getAbsolutePath());
+ verifier.filterFile("settings-template.xml", "settings.xml");
+ verifier.addCliArgument("--settings");
+ verifier.addCliArgument("settings.xml");
+ verifier.addCliArgument("validate");
+ try {
+ verifier.execute();
+ verifier.verifyErrorFreeLog();
+ fail("Build should not succeed");
+ } catch (VerificationException e) {
+ verifier.verifyTextInLog("must specify an absolute path but is ${test.home}/tools.jar");
+ }
+ }
}
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4379TransitiveSystemPathInterpolatedWithEnvVarTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4379TransitiveSystemPathInterpolatedWithEnvVarTest.java
index aa25d23f3739..0a9d3ce9c382 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4379TransitiveSystemPathInterpolatedWithEnvVarTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4379TransitiveSystemPathInterpolatedWithEnvVarTest.java
@@ -24,6 +24,7 @@
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
/**
* This is a test set for MNG-4379.
@@ -55,10 +56,40 @@ public void testit() throws Exception {
verifier.addCliArgument("-s");
verifier.addCliArgument("settings.xml");
verifier.addCliArguments("validate");
+ verifier.addCliArgument("-Dmaven.model.dependencyInterpolation.full=true");
verifier.execute();
verifier.verifyErrorFreeLog();
List classpath = verifier.loadLines("target/classpath.txt");
assertTrue(classpath.contains("pom.xml"), classpath.toString());
}
+
+ /**
+ * Test that by default the path of a system-scope dependency declared by a POM resolved from a
+ * repository is not interpolated using environment variables, so the expression is left as
+ * written and reported as an invalid path.
+ *
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void testitDefaultLeavesEnvironmentVariableUnresolved() throws Exception {
+ File testDir = extractResources("/mng-4379");
+
+ Verifier verifier = newVerifier(testDir.getAbsolutePath());
+ verifier.setAutoclean(false);
+ verifier.deleteDirectory("target");
+ verifier.deleteArtifacts("org.apache.maven.its.mng4379");
+ verifier.filterFile("settings-template.xml", "settings.xml");
+ verifier.setEnvironmentVariable("MNG_4379_HOME", testDir.getAbsolutePath());
+ verifier.addCliArgument("-s");
+ verifier.addCliArgument("settings.xml");
+ verifier.addCliArguments("validate");
+ try {
+ verifier.execute();
+ verifier.verifyErrorFreeLog();
+ fail("Build should not succeed");
+ } catch (VerificationException e) {
+ verifier.verifyTextInLog("must specify an absolute path but is ${env.MNG_4379_HOME}/pom.xml");
+ }
+ }
}
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4590ImportedPomUsesSystemAndUserPropertiesTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4590ImportedPomUsesSystemAndUserPropertiesTest.java
index 5a369f906468..f5a66d39f090 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4590ImportedPomUsesSystemAndUserPropertiesTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4590ImportedPomUsesSystemAndUserPropertiesTest.java
@@ -55,6 +55,8 @@ public void testit() throws Exception {
verifier.addCliArgument("--settings");
verifier.addCliArgument("settings.xml");
verifier.addCliArgument("validate");
+ verifier.addCliArgument("-Dmaven.repository.dependencyManagement.allowSystemScope=true");
+ verifier.addCliArgument("-Dmaven.model.dependencyInterpolation.full=true");
verifier.execute();
verifier.verifyErrorFreeLog();
@@ -65,4 +67,31 @@ public void testit() throws Exception {
new File(testDir, "pom.xml").getAbsoluteFile(),
new File(props.getProperty("project.dependencyManagement.dependencies.0.systemPath")));
}
+
+ /**
+ * Verify that by default a POM imported from a repository contributes no managed dependency
+ * that declares {@code system} scope or a {@code systemPath}.
+ *
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void testitDefaultOmitsSystemScopedManagedDependency() throws Exception {
+ File testDir = extractResources("/mng-4590");
+
+ Verifier verifier = newVerifier(testDir.getAbsolutePath());
+ verifier.setAutoclean(false);
+ verifier.deleteDirectory("target");
+ verifier.deleteArtifacts("org.apache.maven.its.mng4590");
+ verifier.filterFile("settings-template.xml", "settings.xml");
+ verifier.setEnvironmentVariable("MAVEN_OPTS", "-Dtest.file=pom.xml");
+ verifier.addCliArgument("-Dtest.dir=" + testDir.getAbsolutePath());
+ verifier.addCliArgument("--settings");
+ verifier.addCliArgument("settings.xml");
+ verifier.addCliArgument("validate");
+ verifier.execute();
+ verifier.verifyErrorFreeLog();
+
+ Properties props = verifier.loadProperties("target/pom.properties");
+ assertEquals("0", props.getProperty("project.dependencyManagement.dependencies"));
+ }
}