From 7384fa48eb3efe8752f9e2edc783070fae5ddc2f Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sat, 11 Apr 2026 13:48:07 +0200 Subject: [PATCH] Promote `project.rootDirectory` for interpolation and profile activation - Added support for `${project.rootDirectory}` to reference the project's root directory (containing the `.mvn` directory). - Enhanced profile activation to support `exists` and `missing` checks based on `${project.rootDirectory}`. fix #11909 --- .../maven/project/DefaultProjectBuilder.java | 13 +++ .../apache/maven/project/MavenProject.java | 28 +++++++ .../DefaultMavenProjectBuilderTest.java | 9 +++ .../maven/project/PomConstructionTest.java | 1 + .../maven/project/ProjectBuilderTest.java | 8 ++ .../repository/TestRepositorySystem.java | 4 +- .../basedir-interpolation/.mvn/.gitkeep | 0 .../pom-with-unusual-name.xml | 1 + .../projects/root-directory/.mvn/.gitkeep | 0 .../resources/projects/root-directory/pom.xml | 6 ++ .../java/org/apache/maven/cli/MavenCli.java | 32 +++----- maven-model-builder/pom.xml | 4 + .../building/DefaultModelBuilderFactory.java | 13 ++- .../AbstractStringBasedModelInterpolator.java | 26 ++++++ ...ProfileActivationFilePathInterpolator.java | 22 +++++ .../activation/FileProfileActivator.java | 2 +- .../maven/model/root/DefaultRootLocator.java | 80 +++++++++++++++++++ .../apache/maven/model/root/RootLocator.java | 49 ++++++++++++ .../validation/DefaultModelValidator.java | 4 +- .../org.apache.maven.model.root.RootLocator | 1 + maven-model-builder/src/site/apt/index.apt | 2 + .../activation/FileProfileActivatorTest.java | 19 ++++- ...tivation-file-with-allowed-expressions.xml | 17 ++++ maven-model/src/main/mdo/maven.mdo | 2 +- pom.xml | 1 + 25 files changed, 313 insertions(+), 31 deletions(-) create mode 100644 maven-core/src/test/resources-project-builder/basedir-interpolation/.mvn/.gitkeep create mode 100644 maven-core/src/test/resources/projects/root-directory/.mvn/.gitkeep create mode 100644 maven-core/src/test/resources/projects/root-directory/pom.xml create mode 100644 maven-model-builder/src/main/java/org/apache/maven/model/root/DefaultRootLocator.java create mode 100644 maven-model-builder/src/main/java/org/apache/maven/model/root/RootLocator.java create mode 100644 maven-model-builder/src/main/resources/META-INF/services/org.apache.maven.model.root.RootLocator diff --git a/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java b/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java index 419c371b9cae..029c906dabb1 100644 --- a/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java +++ b/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java @@ -65,6 +65,7 @@ import org.apache.maven.model.building.ModelSource; import org.apache.maven.model.building.StringModelSource; import org.apache.maven.model.resolution.ModelResolver; +import org.apache.maven.model.root.RootLocator; import org.apache.maven.repository.internal.ArtifactDescriptorUtils; import org.apache.maven.repository.internal.DefaultModelCacheFactory; import org.apache.maven.repository.internal.ModelCacheFactory; @@ -117,6 +118,9 @@ public class DefaultProjectBuilder implements ProjectBuilder { @Inject private ModelCacheFactory modelCacheFactory; + @Inject + private RootLocator rootLocator; + // ---------------------------------------------------------------------- // MavenProjectBuilder Implementation // ---------------------------------------------------------------------- @@ -166,6 +170,11 @@ private ProjectBuildingResult build(File pomFile, ModelSource modelSource, Inter project = new MavenProject(); project.setFile(pomFile); + if (pomFile != null) { + project.setRootDirectory( + rootLocator.findRoot(pomFile.toPath().getParent())); + } + DefaultModelBuildingListener listener = new DefaultModelBuildingListener(project, projectBuildingHelper, projectBuildingRequest); request.setModelBuildingListener(listener); @@ -437,6 +446,10 @@ private boolean build( MavenProject project = new MavenProject(); project.setFile(pomFile); + if (pomFile != null) { + project.setRootDirectory(rootLocator.findRoot(pomFile.toPath().getParent())); + } + request.setPomFile(pomFile); request.setTwoPhaseBuilding(true); request.setLocationTracking(true); diff --git a/maven-core/src/main/java/org/apache/maven/project/MavenProject.java b/maven-core/src/main/java/org/apache/maven/project/MavenProject.java index 72ca2214d232..adb36b42baad 100644 --- a/maven-core/src/main/java/org/apache/maven/project/MavenProject.java +++ b/maven-core/src/main/java/org/apache/maven/project/MavenProject.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.IOException; import java.io.Writer; +import java.nio.file.Path; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; @@ -68,6 +69,7 @@ import org.apache.maven.model.Resource; import org.apache.maven.model.Scm; import org.apache.maven.model.io.xpp3.MavenXpp3Writer; +import org.apache.maven.model.root.RootLocator; import org.apache.maven.project.artifact.InvalidDependencyVersionException; import org.apache.maven.project.artifact.MavenMetadataSource; import org.codehaus.plexus.classworlds.realm.ClassRealm; @@ -182,6 +184,8 @@ public class MavenProject implements Cloneable { private final Set lifecyclePhases = Collections.synchronizedSet(new LinkedHashSet()); + private Path rootDirectory; + public MavenProject() { Model model = new Model(); @@ -1507,6 +1511,30 @@ public void addLifecyclePhase(String lifecyclePhase) { lifecyclePhases.add(lifecyclePhase); } + /** + * Gets the root directory of the project, which is the parent directory + * containing the {@code .mvn} directory. + * + * @return the root directory of the project + * @throws IllegalStateException if the root directory could not be found + * @see org.apache.maven.execution.MavenSession#getRootDirectory() + * + * @since 3.10.0 + */ + public Path getRootDirectory() { + if (rootDirectory == null) { + throw new IllegalStateException(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE); + } + return rootDirectory; + } + + /** + * @since 3.10.0 + */ + public void setRootDirectory(Path rootDirectory) { + this.rootDirectory = rootDirectory; + } + // ---------------------------------------------------------------------------------------------------------------- // // diff --git a/maven-core/src/test/java/org/apache/maven/project/DefaultMavenProjectBuilderTest.java b/maven-core/src/test/java/org/apache/maven/project/DefaultMavenProjectBuilderTest.java index 79f14949c74d..e3a7bd159eb0 100644 --- a/maven-core/src/test/java/org/apache/maven/project/DefaultMavenProjectBuilderTest.java +++ b/maven-core/src/test/java/org/apache/maven/project/DefaultMavenProjectBuilderTest.java @@ -418,4 +418,13 @@ public void testBuildParentVersionRangeExternallyWithChildRevisionExpression() t assertEquals("1.0-SNAPSHOT", mp.getVersion()); } + + @Test + public void rootDirectoryShouldBeDetected() throws Exception { + File f1 = getTestFile("src/test/resources/projects/root-directory/pom.xml"); + + MavenProject mp = getProject(f1); + + assertEquals(f1.toPath().getParent(), mp.getRootDirectory()); + } } diff --git a/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java b/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java index 342cfa4fb92a..5931134837d4 100644 --- a/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java +++ b/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java @@ -912,6 +912,7 @@ public void testInterpolationOfBasedirInPomWithUnusualName() throws Exception { PomTestWrapper pom = buildPom("basedir-interpolation/pom-with-unusual-name.xml"); assertEquals(pom.getBasedir(), new File(pom.getValue("properties/prop0").toString())); assertEquals(pom.getBasedir(), new File(pom.getValue("properties/prop1").toString())); + assertEquals(pom.getBasedir(), new File(pom.getValue("properties/prop2").toString())); } /* MNG-3979 */ diff --git a/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java b/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java index 413c6f590576..128e1665fde6 100644 --- a/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java +++ b/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java @@ -132,6 +132,7 @@ public void run() { assertEquals(project.getArtifacts().size(), artifactsResultInAnotherThread.get()); } + @Test public void testDontResolveDependencies() throws Exception { File pomFile = new File("src/test/resources/projects/basic-resolveDependencies.xml"); MavenSession mavenSession = createMavenSession(null); @@ -150,6 +151,7 @@ public void testDontResolveDependencies() throws Exception { assertEquals(0, mavenProject.getArtifacts().size()); } + @Test public void testReadModifiedPoms() throws Exception { String initialValue = System.setProperty( DefaultProjectBuilder.DISABLE_GLOBAL_MODEL_CACHE_SYSTEM_PROPERTY, Boolean.toString(true)); @@ -185,6 +187,7 @@ public void testReadModifiedPoms() throws Exception { } } + @Test public void testReadErroneousMavenProjectContainsReference() throws Exception { File pomFile = new File("src/test/resources/projects/artifactMissingVersion.xml").getAbsoluteFile(); MavenSession mavenSession = createMavenSession(null); @@ -215,6 +218,7 @@ public void testReadErroneousMavenProjectContainsReference() throws Exception { } } + @Test public void testReadInvalidPom() throws Exception { File pomFile = new File("src/test/resources/projects/badPom.xml").getAbsoluteFile(); MavenSession mavenSession = createMavenSession(null); @@ -240,6 +244,7 @@ public void testReadInvalidPom() throws Exception { } } + @Test public void testReadParentAndChildWithRegularVersionSetParentFile() throws Exception { List toRead = new ArrayList<>(2); File parentPom = getProject("MNG-6723"); @@ -290,6 +295,7 @@ private void assertResultShowNoError(List results) { } } + @Test public void testBuildProperties() throws Exception { File file = new File(getProject("MNG-6716").getParentFile(), "project/pom.xml"); MavenSession mavenSession = createMavenSession(null); @@ -305,6 +311,7 @@ public void testBuildProperties() throws Exception { assertEquals(1, project.getResources().size()); } + @Test public void testPropertyInPluginManagementGroupId() throws Exception { File pom = getProject("MNG-6983"); @@ -316,6 +323,7 @@ public void testPropertyInPluginManagementGroupId() throws Exception { } } + @Test public void testLocationTrackingResolution() throws Exception { File pom = getProject("MNG-7648"); diff --git a/maven-core/src/test/java/org/apache/maven/repository/TestRepositorySystem.java b/maven-core/src/test/java/org/apache/maven/repository/TestRepositorySystem.java index 8f414827f77f..0a221957ec52 100644 --- a/maven-core/src/test/java/org/apache/maven/repository/TestRepositorySystem.java +++ b/maven-core/src/test/java/org/apache/maven/repository/TestRepositorySystem.java @@ -101,14 +101,14 @@ public Artifact createArtifactWithClassifier( public ArtifactRepository createDefaultLocalRepository() throws InvalidRepositoryException { return createLocalRepository( - new File(System.getProperty("basedir", ""), "target/local-repo").getAbsoluteFile()); + new File(System.getProperty("basedir", "."), "target/local-repo").getAbsoluteFile()); } public ArtifactRepository createDefaultRemoteRepository() throws InvalidRepositoryException { return new MavenArtifactRepository( DEFAULT_REMOTE_REPO_ID, "file://" - + new File(System.getProperty("basedir", ""), "src/test/remote-repo") + + new File(System.getProperty("basedir", "."), "src/test/remote-repo") .toURI() .getPath(), new DefaultRepositoryLayout(), diff --git a/maven-core/src/test/resources-project-builder/basedir-interpolation/.mvn/.gitkeep b/maven-core/src/test/resources-project-builder/basedir-interpolation/.mvn/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/maven-core/src/test/resources-project-builder/basedir-interpolation/pom-with-unusual-name.xml b/maven-core/src/test/resources-project-builder/basedir-interpolation/pom-with-unusual-name.xml index 1c037e630622..d6eb8db85d50 100644 --- a/maven-core/src/test/resources-project-builder/basedir-interpolation/pom-with-unusual-name.xml +++ b/maven-core/src/test/resources-project-builder/basedir-interpolation/pom-with-unusual-name.xml @@ -35,5 +35,6 @@ under the License. ${basedir} ${project.basedir} + ${project.rootDirectory} diff --git a/maven-core/src/test/resources/projects/root-directory/.mvn/.gitkeep b/maven-core/src/test/resources/projects/root-directory/.mvn/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/maven-core/src/test/resources/projects/root-directory/pom.xml b/maven-core/src/test/resources/projects/root-directory/pom.xml new file mode 100644 index 000000000000..d3ea9c3d972e --- /dev/null +++ b/maven-core/src/test/resources/projects/root-directory/pom.xml @@ -0,0 +1,6 @@ + + 4.0.0 + test + root-directory + 0.0.1-SNAPSHOT + diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java index 985362b680d5..0859050fe131 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java @@ -39,6 +39,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Properties; +import java.util.ServiceLoader; import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Matcher; @@ -88,6 +89,7 @@ import org.apache.maven.lifecycle.LifecycleExecutionException; import org.apache.maven.message.MessageBuilder; import org.apache.maven.model.building.ModelProcessor; +import org.apache.maven.model.root.RootLocator; import org.apache.maven.project.MavenProject; import org.apache.maven.properties.internal.EnvironmentUtils; import org.apache.maven.properties.internal.SystemProperties; @@ -148,9 +150,6 @@ public class MavenCli { private static final String DOT_MVN = ".mvn"; - private static final String UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE = "Unable to find the root directory. Create a " - + DOT_MVN + " directory in the project root directory to identify it."; - private static final String PROJECT_EXTENSIONS_FILENAME = DOT_MVN + "/extensions.xml"; private static final File USER_EXTENSIONS_FILE = new File(USER_MAVEN_CONFIGURATION_HOME, "extensions.xml"); @@ -356,10 +355,13 @@ void initialize(CliRequest cliRequest) throws ExitException { } topDirectory = getCanonicalPath(topDirectory); cliRequest.request.setTopDirectory(topDirectory); - // We're very early in the process and we don't have the container set up yet, - // so we on searchAcceptableRootDirectory method to find us acceptable directory. - // The method may return null if nothing acceptable found. - cliRequest.request.setRootDirectory(searchAcceptableRootDirectory(topDirectory)); + // We're very early in the process, and we don't have the container set up yet, + // so we rely on the JDK services to eventually look up a custom RootLocator. + // This is used to compute {@code session.rootDirectory} but all {@code project.rootDirectory} + // properties will be computed through the RootLocator found in the container. + RootLocator rootLocator = + ServiceLoader.load(RootLocator.class).iterator().next(); + cliRequest.request.setRootDirectory(rootLocator.findRoot(topDirectory)); // // Make sure the Maven home directory is an absolute path to save us from confusion with say drive-relative @@ -625,7 +627,7 @@ void properties(CliRequest cliRequest) throws ExitException { System.err.println(message); if (cliRequest.request.getRootDirectory() == null) { System.err.println(); - System.err.println(UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE); + System.err.println(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE); } throw new ExitException(1); // user error } @@ -1637,20 +1639,6 @@ static void populateProperties(CliRequest cliRequest, Properties systemPropertie systemProperties.setProperty("maven.build.version", mavenBuildVersion); } - protected boolean isAcceptableRootDirectory(Path path) { - return path != null && Files.isDirectory(path.resolve(DOT_MVN)); - } - - protected Path searchAcceptableRootDirectory(Path path) { - if (path == null) { - return null; - } - if (isAcceptableRootDirectory(path)) { - return path; - } - return searchAcceptableRootDirectory(path.getParent()); - } - protected static StringSearchInterpolator createInterpolator(CliRequest cliRequest, Properties... properties) { StringSearchInterpolator interpolator = new StringSearchInterpolator(); interpolator.addValueSource(new AbstractValueSource(false) { diff --git a/maven-model-builder/pom.xml b/maven-model-builder/pom.xml index bf9600950b71..66fb1aad31e0 100644 --- a/maven-model-builder/pom.xml +++ b/maven-model-builder/pom.xml @@ -64,6 +64,10 @@ under the License. org.ow2.asm asm + + org.slf4j + slf4j-api + org.eclipse.sisu org.eclipse.sisu.plexus diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java index 722007a57ace..5146f402f7ed 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java @@ -62,6 +62,8 @@ import org.apache.maven.model.profile.activation.OperatingSystemProfileActivator; import org.apache.maven.model.profile.activation.ProfileActivator; import org.apache.maven.model.profile.activation.PropertyProfileActivator; +import org.apache.maven.model.root.DefaultRootLocator; +import org.apache.maven.model.root.RootLocator; import org.apache.maven.model.superpom.DefaultSuperPomProvider; import org.apache.maven.model.superpom.SuperPomProvider; import org.apache.maven.model.validation.DefaultModelValidator; @@ -113,7 +115,9 @@ protected ProfileActivator[] newProfileActivators() { } protected ProfileActivationFilePathInterpolator newProfileActivationFilePathInterpolator() { - return new ProfileActivationFilePathInterpolator().setPathTranslator(newPathTranslator()); + return new ProfileActivationFilePathInterpolator() + .setPathTranslator(newPathTranslator()) + .setRootLocator(newRootLocator()); } protected UrlNormalizer newUrlNormalizer() { @@ -124,13 +128,18 @@ protected PathTranslator newPathTranslator() { return new DefaultPathTranslator(); } + private RootLocator newRootLocator() { + return new DefaultRootLocator(); + } + protected ModelInterpolator newModelInterpolator() { UrlNormalizer normalizer = newUrlNormalizer(); PathTranslator pathTranslator = newPathTranslator(); return new StringVisitorModelInterpolator() .setPathTranslator(pathTranslator) .setUrlNormalizer(normalizer) - .setVersionPropertiesProcessor(newModelVersionPropertiesProcessor()); + .setVersionPropertiesProcessor(newModelVersionPropertiesProcessor()) + .setRootLocator(newRootLocator()); } protected ModelVersionProcessor newModelVersionPropertiesProcessor() { diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java index 9b1fae7d007f..8c34d9a92995 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java @@ -33,6 +33,7 @@ import org.apache.maven.model.building.ModelProblemCollector; import org.apache.maven.model.path.PathTranslator; import org.apache.maven.model.path.UrlNormalizer; +import org.apache.maven.model.root.RootLocator; import org.codehaus.plexus.interpolation.AbstractValueSource; import org.codehaus.plexus.interpolation.InterpolationPostProcessor; import org.codehaus.plexus.interpolation.MapBasedValueSource; @@ -81,6 +82,9 @@ public abstract class AbstractStringBasedModelInterpolator implements ModelInter @Inject private ModelVersionProcessor versionProcessor; + @Inject + private RootLocator rootLocator; + public AbstractStringBasedModelInterpolator setPathTranslator(PathTranslator pathTranslator) { this.pathTranslator = pathTranslator; return this; @@ -96,6 +100,11 @@ public AbstractStringBasedModelInterpolator setVersionPropertiesProcessor(ModelV return this; } + public AbstractStringBasedModelInterpolator setRootLocator(RootLocator rootLocator) { + this.rootLocator = rootLocator; + return this; + } + protected List createValueSources( final Model model, final File projectDir, @@ -131,6 +140,23 @@ public Object getValue(String expression) { true); valueSources.add(basedirValueSource); + ValueSource rootDirectoryValueSource = new PrefixedValueSourceWrapper( + new AbstractValueSource(false) { + @Override + public Object getValue(String expression) { + if ("rootDirectory".equals(expression)) { + return rootLocator + .findMandatoryRoot(projectDir.toPath()) + .toAbsolutePath() + .toString(); + } + return null; + } + }, + PROJECT_PREFIXES, + true); + valueSources.add(rootDirectoryValueSource); + ValueSource baseUriValueSource = new PrefixedValueSourceWrapper( new AbstractValueSource(false) { @Override diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/path/ProfileActivationFilePathInterpolator.java b/maven-model-builder/src/main/java/org/apache/maven/model/path/ProfileActivationFilePathInterpolator.java index 6e40aa2243d9..4a4fcb828262 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/path/ProfileActivationFilePathInterpolator.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/path/ProfileActivationFilePathInterpolator.java @@ -23,9 +23,11 @@ import javax.inject.Singleton; import java.io.File; +import java.nio.file.Path; import org.apache.maven.model.ActivationFile; import org.apache.maven.model.profile.ProfileActivationContext; +import org.apache.maven.model.root.RootLocator; import org.codehaus.plexus.interpolation.AbstractValueSource; import org.codehaus.plexus.interpolation.InterpolationException; import org.codehaus.plexus.interpolation.MapBasedValueSource; @@ -43,11 +45,19 @@ public class ProfileActivationFilePathInterpolator { @Inject private PathTranslator pathTranslator; + @Inject + private RootLocator rootLocator; + public ProfileActivationFilePathInterpolator setPathTranslator(PathTranslator pathTranslator) { this.pathTranslator = pathTranslator; return this; } + public ProfileActivationFilePathInterpolator setRootLocator(RootLocator rootLocator) { + this.rootLocator = rootLocator; + return this; + } + /** * Interpolates given {@code path}. * @@ -76,6 +86,18 @@ public Object getValue(String expression) { return null; } + interpolator.addValueSource(new AbstractValueSource(false) { + @Override + public Object getValue(String expression) { + if ("project.rootDirectory".equals(expression)) { + Path base = basedir != null ? basedir.toPath() : null; + Path root = rootLocator.findMandatoryRoot(base); + return root.toFile().getAbsolutePath(); + } + return null; + } + }); + interpolator.addValueSource(new MapBasedValueSource(context.getProjectProperties())); interpolator.addValueSource(new MapBasedValueSource(context.getUserProperties())); diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/profile/activation/FileProfileActivator.java b/maven-model-builder/src/main/java/org/apache/maven/model/profile/activation/FileProfileActivator.java index c11716b1ef50..f8d7d389c80a 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/profile/activation/FileProfileActivator.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/profile/activation/FileProfileActivator.java @@ -38,7 +38,7 @@ /** * Determines profile activation based on the existence/absence of some file. - * File name interpolation support is limited to ${project.basedir} + * File name interpolation support is limited to ${project.basedir}, ${project.rootDirectory} * system properties and user properties. * * @author Benjamin Bentmann diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/root/DefaultRootLocator.java b/maven-model-builder/src/main/java/org/apache/maven/model/root/DefaultRootLocator.java new file mode 100644 index 000000000000..0806d36606bf --- /dev/null +++ b/maven-model-builder/src/main/java/org/apache/maven/model/root/DefaultRootLocator.java @@ -0,0 +1,80 @@ +/* + * 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.root; + +import javax.inject.Named; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Named +public class DefaultRootLocator implements RootLocator { + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Override + public Path findRoot(Path basedir) { + Path rootDirectory = basedir; + while (rootDirectory != null && !isRootDirectory(rootDirectory)) { + rootDirectory = rootDirectory.getParent(); + } + return rootDirectory; + } + + @Override + public Path findMandatoryRoot(Path basedir) { + Path rootDirectory = findRoot(basedir); + Optional rdf = getRootDirectoryFallback(); + if (rootDirectory == null) { + rootDirectory = rdf.orElseThrow(() -> new IllegalStateException(getNoRootMessage())); + } else { + if (rdf.isPresent()) { + try { + if (!Files.isSameFile(rootDirectory, rdf.get())) { + logger.warn("Project root directory and multiModuleProjectDirectory are not aligned"); + } + } catch (IOException e) { + throw new IllegalStateException("findMandatoryRoot failed", e); + } + } + } + return rootDirectory; + } + + protected Optional getRootDirectoryFallback() { + String mmpd = System.getProperty("maven.multiModuleProjectDirectory"); + if (mmpd != null) { + return Optional.of(getCanonicalPath(Paths.get(mmpd))); + } + return Optional.empty(); + } + + protected Path getCanonicalPath(Path path) { + return path.toAbsolutePath().normalize(); + } + + public boolean isRootDirectory(Path dir) { + return Files.isDirectory(dir.resolve(".mvn")); + } +} diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/root/RootLocator.java b/maven-model-builder/src/main/java/org/apache/maven/model/root/RootLocator.java new file mode 100644 index 000000000000..804e73ec8b83 --- /dev/null +++ b/maven-model-builder/src/main/java/org/apache/maven/model/root/RootLocator.java @@ -0,0 +1,49 @@ +/* + * 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.root; + +import java.nio.file.Path; + +/** + * Interface used to locate the root directory for a given project. + * + * The root locator is usually looked up from the plexus container. + * One notable exception is the computation of the early {@code session.rootDirectory} + * property which happens very early. The implementation used in this case + * will be discovered using the JDK service mechanism. + * + * The default implementation will look for a {@code .mvn} child directory. + * + * @see DefaultRootLocator + * + * @since 3.10.0 + */ +public interface RootLocator { + + String UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE = "Unable to find the root directory. Create a .mvn" + + " directory in the project root directory to identify it."; + + Path findRoot(Path basedir); + + Path findMandatoryRoot(Path basedir); + + default String getNoRootMessage() { + return UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE; + } +} diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java b/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java index f4d7369c88d2..b4d00eda3c20 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java @@ -299,7 +299,9 @@ class ActivationFrame { while (matcher.find()) { String propertyName = matcher.group(0); - if (path.startsWith("activation.file.") && "${project.basedir}".equals(propertyName)) { + if (path.startsWith("activation.file.") + && ("${project.basedir}".equals(propertyName) + || "${project.rootDirectory}".equals(propertyName))) { continue; } addViolation( diff --git a/maven-model-builder/src/main/resources/META-INF/services/org.apache.maven.model.root.RootLocator b/maven-model-builder/src/main/resources/META-INF/services/org.apache.maven.model.root.RootLocator new file mode 100644 index 000000000000..4b81cf5c840e --- /dev/null +++ b/maven-model-builder/src/main/resources/META-INF/services/org.apache.maven.model.root.RootLocator @@ -0,0 +1 @@ +org.apache.maven.model.root.DefaultRootLocator diff --git a/maven-model-builder/src/site/apt/index.apt b/maven-model-builder/src/site/apt/index.apt index e072c36edca5..9ddbfd52b870 100644 --- a/maven-model-builder/src/site/apt/index.apt +++ b/maven-model-builder/src/site/apt/index.apt @@ -168,6 +168,8 @@ Maven Model Builder <<>> ()\ <<>> () | the directory containing the <<>> file | <<<$\{project.basedir\}>>> | *----+------+------+ +| <<>> | the project's root directory (containing a <<<.mvn>>> directory) | <<<$\{project.rootDirectory\}>>> | +*----+------+------+ | <<>>\ <<>> () | the directory containing the <<>> file as URI | <<<$\{project.baseUri\}>>> | *----+------+------+ diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/profile/activation/FileProfileActivatorTest.java b/maven-model-builder/src/test/java/org/apache/maven/model/profile/activation/FileProfileActivatorTest.java index 525e9a8edcb6..e0fec0cd9069 100644 --- a/maven-model-builder/src/test/java/org/apache/maven/model/profile/activation/FileProfileActivatorTest.java +++ b/maven-model-builder/src/test/java/org/apache/maven/model/profile/activation/FileProfileActivatorTest.java @@ -28,6 +28,7 @@ import org.apache.maven.model.path.DefaultPathTranslator; import org.apache.maven.model.path.ProfileActivationFilePathInterpolator; import org.apache.maven.model.profile.DefaultProfileActivationContext; +import org.apache.maven.model.root.DefaultRootLocator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -54,8 +55,9 @@ public FileProfileActivatorTest() { public void setUp() throws Exception { super.setUp(); - activator.setProfileActivationFilePathInterpolator( - new ProfileActivationFilePathInterpolator().setPathTranslator(new DefaultPathTranslator())); + activator.setProfileActivationFilePathInterpolator(new ProfileActivationFilePathInterpolator() + .setPathTranslator(new DefaultPathTranslator()) + .setRootLocator(new DefaultRootLocator())); context.setProjectDirectory(new File(tempDir.toString())); @@ -63,6 +65,11 @@ public void setUp() throws Exception { if (!file.createNewFile()) { throw new IOException("Can't create " + file); } + + File dotMvn = new File(tempDir.resolve(".mvn").toString()); + if (!dotMvn.mkdir()) { + throw new IOException("Can't create " + dotMvn); + } } @Test @@ -70,10 +77,14 @@ public void testIsActiveNoFile() { assertActivation(false, newExistsProfile(null), context); assertActivation(false, newExistsProfile("someFile.txt"), context); assertActivation(false, newExistsProfile("${basedir}/someFile.txt"), context); + assertActivation(false, newExistsProfile("${project.basedir}/someFile.txt"), context); + assertActivation(false, newExistsProfile("${project.rootDirectory}/someFile.txt"), context); assertActivation(false, newMissingProfile(null), context); assertActivation(true, newMissingProfile("someFile.txt"), context); assertActivation(true, newMissingProfile("${basedir}/someFile.txt"), context); + assertActivation(true, newMissingProfile("${project.basedir}/someFile.txt"), context); + assertActivation(true, newMissingProfile("${project.rootDirectory}/someFile.txt"), context); } @Test @@ -81,10 +92,14 @@ public void testIsActiveExistsFileExists() { assertActivation(true, newExistsProfile("file.txt"), context); assertActivation(true, newExistsProfile("${basedir}"), context); assertActivation(true, newExistsProfile("${basedir}/" + "file.txt"), context); + assertActivation(true, newExistsProfile("${project.basedir}/" + "file.txt"), context); + assertActivation(true, newExistsProfile("${project.rootDirectory}/" + "file.txt"), context); assertActivation(false, newMissingProfile("file.txt"), context); assertActivation(false, newMissingProfile("${basedir}"), context); assertActivation(false, newMissingProfile("${basedir}/" + "file.txt"), context); + assertActivation(false, newMissingProfile("${project.basedir}/" + "file.txt"), context); + assertActivation(false, newMissingProfile("${project.rootDirectory}/" + "file.txt"), context); } @Test diff --git a/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-file-with-allowed-expressions.xml b/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-file-with-allowed-expressions.xml index 72b6747f1835..85a589d428c2 100644 --- a/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-file-with-allowed-expressions.xml +++ b/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-file-with-allowed-expressions.xml @@ -60,6 +60,23 @@ under the License. + + exists-project-rootDirectory + + + ${project.rootDirectory}/test.txt + + + + + missing-project-rootDirectory + + + ${project.rootDirectory}/test.txt + + + + dynamic-property-available diff --git a/maven-model/src/main/mdo/maven.mdo b/maven-model/src/main/mdo/maven.mdo index 75ea6fbbdd6a..90715f117345 100644 --- a/maven-model/src/main/mdo/maven.mdo +++ b/maven-model/src/main/mdo/maven.mdo @@ -2945,7 +2945,7 @@ activated. On the other hand, exists will test for the existence of the file and if it is there, the profile will be activated.
Variable interpolation for these file specifications is limited to ${project.basedir}, - system properties and user properties.]]> + ${project.rootDirectory}, system properties and user properties.]]> missing diff --git a/pom.xml b/pom.xml index 7fdb3b3fbd8b..8259c2109bed 100644 --- a/pom.xml +++ b/pom.xml @@ -649,6 +649,7 @@ under the License. **/*.odg **/*.svg .asf.yaml + src/main/resources/META-INF/services/**