You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by st...@apache.org on 2020/03/18 13:10:05 UTC
[maven] 05/06: [MNG-5668] Add a feature experiments framework to
allow for opt-in
This is an automated email from the ASF dual-hosted git repository.
stephenc pushed a commit to branch mng-5668-poc
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 71178c5d0307601665e9d4e3662101890266f6a5
Author: Stephen Connolly <st...@gmail.com>
AuthorDate: Fri Nov 22 15:46:35 2019 +0000
[MNG-5668] Add a feature experiments framework to allow for opt-in
Users get to turn on all experiments (no partial activation) by adding the experiments extension to `.mvn/extensions.xml`, e.g.
<?xml version="1.0" encoding="UTF-8"?>
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
<extension>
<groupId>org.apache.maven</groupId>
<artifactId>maven-experiments</artifactId>
<version>3.7.0-SNAPSHOT</version>
</extension>
</extensions>
Without the extension, the dynamic phases feature is disabled
With the extension the feature is enabled, e.g.
[INFO] Enabling experimental features of Maven 3.7.0-SNAPSHOT
[INFO] Experimental features enabled:
[INFO] * dynamic-phases
[INFO] Scanning for projects...
Attempts to build the project with a different (newer) version of Maven will
fail, e.g.
[ERROR] The project uses experimental features that require exactly Maven 3.7.0-SNAPSHOT -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MavenExecutionException
Attempts to build the project with a different (older) version of Maven
will blow up due to class not found (I'd like to determine how to lazy
resolve from Plexus/Sisu to avoid that)
---
maven-core/pom.xml | 4 +
.../org/apache/maven/execution/MavenSession.java | 3 +-
.../maven/feature/spi/DefaultMavenFeatures.java | 130 ++++++++++++++++
.../internal/DefaultLifecycleMappingDelegate.java | 31 ++--
.../DefaultLifecycleTaskSegmentCalculator.java | 12 +-
.../maven/lifecycle/internal/MojoExecutor.java | 12 +-
.../maven/lifecycle/internal/PhaseRecorder.java | 32 +++-
.../main/resources/META-INF/plexus/components.xml | 11 ++
.../lifecycle/internal/PhaseRecorderTest.java | 2 +-
maven-experiments/pom.xml | 59 ++++++++
.../feature/check/MavenExperimentEnabler.java | 164 +++++++++++++++++++++
maven-feature/pom.xml | 44 ++++++
.../maven/feature/api/MavenFeatureContext.java | 30 ++++
.../apache/maven/feature/api/MavenFeatures.java | 39 +++++
pom.xml | 12 ++
15 files changed, 563 insertions(+), 22 deletions(-)
diff --git a/maven-core/pom.xml b/maven-core/pom.xml
index 7a723a2..76bbd0d 100644
--- a/maven-core/pom.xml
+++ b/maven-core/pom.xml
@@ -50,6 +50,10 @@ under the License.
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
+ <artifactId>maven-feature</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
<artifactId>maven-builder-support</artifactId>
</dependency>
<dependency>
diff --git a/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java b/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java
index 5b56df3..0671326 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java
@@ -29,6 +29,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.repository.RepositoryCache;
+import org.apache.maven.feature.api.MavenFeatureContext;
import org.apache.maven.monitor.event.EventDispatcher;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.project.MavenProject;
@@ -44,7 +45,7 @@ import org.eclipse.aether.RepositorySystemSession;
* @author Jason van Zyl
*/
public class MavenSession
- implements Cloneable
+ implements Cloneable, MavenFeatureContext
{
private MavenExecutionRequest request;
diff --git a/maven-core/src/main/java/org/apache/maven/feature/spi/DefaultMavenFeatures.java b/maven-core/src/main/java/org/apache/maven/feature/spi/DefaultMavenFeatures.java
new file mode 100644
index 0000000..93070cc
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/feature/spi/DefaultMavenFeatures.java
@@ -0,0 +1,130 @@
+package org.apache.maven.feature.spi;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.WeakHashMap;
+
+import org.apache.maven.MavenExecutionException;
+import org.apache.maven.feature.api.MavenFeatureContext;
+import org.apache.maven.feature.api.MavenFeatures;
+import org.codehaus.plexus.component.annotations.Component;
+import org.codehaus.plexus.component.annotations.Requirement;
+import org.codehaus.plexus.logging.Logger;
+
+/**
+ * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
+ * <p>
+ * Feature flags for experiments. There is no partial opt-in, you get all the feature that are in turned on in the
+ * current version of Maven if you activate the experiments extension and none of the features if you don't.
+ *
+ * <h2>How we use this</h2>
+ * <ul>
+ * <li>When starting work on a feature, add a constant string to this class to hold the feature name.</li>
+ * <li>When ready to expose the experiment, add the feature name to {@code META-INF/plexus/components.xml}.</li>
+ * <li>When the experiment has been concluded, remove the feature name and collapse whatever branch logic was based
+ * * on the feature flag.</li>
+ * </ul>
+ */
+@Component( role = MavenFeatures.class, hint = "default" )
+public class DefaultMavenFeatures
+ implements MavenFeatures
+{
+ /**
+ * The feature name of dynamic phases.
+ */
+ public static final String DYNAMIC_PHASES = "dynamic-phases";
+
+ @Requirement
+ private Logger log;
+
+ /**
+ * The contexts that are enabled.
+ */
+ private final Map<MavenFeatureContext, Boolean> enabled = new WeakHashMap<>();
+
+ /**
+ * The current experimental features being exposed to opt-in builds.
+ */
+ private Set<String> features;
+
+ public DefaultMavenFeatures()
+ {
+ this.features = Collections.<String>emptySet();
+ }
+
+ public List<String> getFeatures()
+ {
+ return features == null ? Collections.<String>emptyList() : new ArrayList<String>( features );
+ }
+
+ public void setFeatures( List<String> features )
+ {
+ this.features = features == null ? Collections.<String>emptySet() : new HashSet<String>( features );
+ }
+
+ /**
+ * Enabled the feature context. This method is only to be invoked by {@code MavenExperimentEnabler}.
+ *
+ * @param context the context to enable.
+ * @throws MavenExecutionException if we detect illegal usage.
+ * {@code MavenExperimentEnabler}.
+ */
+ public void enable( MavenFeatureContext context )
+ throws MavenExecutionException
+ {
+ StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+ for ( StackTraceElement element : Thread.currentThread().getStackTrace() )
+ {
+ if ( "org.apache.maven.feature.check.MavenExperimentEnabler".equals( element.getClassName() ) )
+ {
+ enabled.put( context, Boolean.TRUE );
+ log.info( "Experimental features enabled:" );
+ for ( String feature: new TreeSet<>( features ) )
+ {
+ log.info( " * " + feature );
+ }
+ return;
+ }
+ }
+ throw new MavenExecutionException( "Detected illegal attempt to bypass experimental feature activation",
+ (File) null );
+ }
+
+ @Override
+ public boolean enabled( MavenFeatureContext context, String featureName )
+ {
+ if ( Boolean.TRUE.equals( enabled.get( context ) ) )
+ {
+ return features != null && features.contains( featureName );
+ }
+ else
+ {
+ return false;
+ }
+ }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java
index a8c6c4b..6a5050b 100644
--- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java
@@ -19,7 +19,15 @@ package org.apache.maven.lifecycle.internal;
* under the License.
*/
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
import org.apache.maven.execution.MavenSession;
+import org.apache.maven.feature.api.MavenFeatures;
+import org.apache.maven.feature.spi.DefaultMavenFeatures;
import org.apache.maven.lifecycle.Lifecycle;
import org.apache.maven.lifecycle.LifecycleMappingDelegate;
import org.apache.maven.model.Plugin;
@@ -36,12 +44,6 @@ import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
/**
* Lifecycle mapping delegate component interface. Calculates project build execution plan given {@link Lifecycle} and
* lifecycle phase. Standard lifecycles use plugin execution {@code <phase>} or mojo default lifecycle phase to
@@ -56,6 +58,9 @@ public class DefaultLifecycleMappingDelegate
@Requirement
private BuildPluginManager pluginManager;
+ @Requirement
+ private MavenFeatures features;
+
public Map<String, List<MojoExecution>> calculateLifecycleMappings( MavenSession session, MavenProject project,
Lifecycle lifecycle, String lifecyclePhase )
throws PluginNotFoundException, PluginResolutionException, PluginDescriptorParsingException,
@@ -66,8 +71,12 @@ public class DefaultLifecycleMappingDelegate
* is interested in, i.e. all phases up to and including the specified phase.
*/
+ boolean dynamicPhasesEnabled = features.enabled( session, DefaultMavenFeatures.DYNAMIC_PHASES );
+
Map<String, Map<Integer, List<MojoExecution>>> mappings =
- new TreeMap<>( new PhaseComparator( lifecycle.getPhases() ) );
+ dynamicPhasesEnabled
+ ? new TreeMap<String, Map<Integer, List<MojoExecution>>>( new PhaseComparator( lifecycle.getPhases() ) )
+ : new LinkedHashMap<String, Map<Integer, List<MojoExecution>>>();
for ( String phase : lifecycle.getPhases() )
{
@@ -97,7 +106,9 @@ public class DefaultLifecycleMappingDelegate
if ( execution.getPhase() != null )
{
Map<Integer, List<MojoExecution>> phaseBindings =
- getPhaseBindings( mappings, execution.getPhase() );
+ dynamicPhasesEnabled
+ ? getPhaseBindings( mappings, execution.getPhase() )
+ : mappings.get( execution.getPhase() );
if ( phaseBindings != null )
{
for ( String goal : execution.getGoals() )
@@ -118,7 +129,9 @@ public class DefaultLifecycleMappingDelegate
session.getRepositorySession() );
Map<Integer, List<MojoExecution>> phaseBindings =
- getPhaseBindings( mappings, mojoDescriptor.getPhase() );
+ dynamicPhasesEnabled
+ ? getPhaseBindings( mappings, mojoDescriptor.getPhase() )
+ : mappings.get( mojoDescriptor.getPhase() );
if ( phaseBindings != null )
{
MojoExecution mojoExecution = new MojoExecution( mojoDescriptor, execution.getId() );
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleTaskSegmentCalculator.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleTaskSegmentCalculator.java
index c10cbf0..6f950c3 100644
--- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleTaskSegmentCalculator.java
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleTaskSegmentCalculator.java
@@ -20,6 +20,8 @@ package org.apache.maven.lifecycle.internal;
*/
import org.apache.maven.execution.MavenSession;
+import org.apache.maven.feature.api.MavenFeatures;
+import org.apache.maven.feature.spi.DefaultMavenFeatures;
import org.apache.maven.lifecycle.LifecycleNotFoundException;
import org.apache.maven.lifecycle.LifecyclePhaseNotFoundException;
import org.apache.maven.plugin.InvalidPluginDescriptorException;
@@ -45,11 +47,11 @@ import java.util.List;
* </p>
* <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
*
- * @since 3.0
* @author Benjamin Bentmann
* @author Jason van Zyl
* @author jdcasey
* @author Kristian Rosenvold (extracted class)
+ * @since 3.0
*/
@Component( role = LifecycleTaskSegmentCalculator.class )
public class DefaultLifecycleTaskSegmentCalculator
@@ -61,6 +63,9 @@ public class DefaultLifecycleTaskSegmentCalculator
@Requirement
private LifecyclePluginResolver lifecyclePluginResolver;
+ @Requirement
+ private MavenFeatures features;
+
public DefaultLifecycleTaskSegmentCalculator()
{
}
@@ -96,7 +101,7 @@ public class DefaultLifecycleTaskSegmentCalculator
{
PhaseId phaseId = PhaseId.of( task );
// if the priority is non-zero then you specified the priority on the CLI
- if ( phaseId.priority() != 0 )
+ if ( phaseId.priority() != 0 && features.enabled( session, DefaultMavenFeatures.DYNAMIC_PHASES ) )
{
throw new LifecyclePhaseNotFoundException(
"Dynamic phases such as \"" + task + "\" are only permitted as execution targets specified "
@@ -125,7 +130,8 @@ public class DefaultLifecycleTaskSegmentCalculator
}
catch ( NoPluginFoundForPrefixException e )
{
- if ( phaseId.executionPoint() != PhaseExecutionPoint.AS && phaseId.phase().indexOf( ':' ) == -1 )
+ if ( phaseId.executionPoint() != PhaseExecutionPoint.AS && phaseId.phase().indexOf( ':' ) == -1
+ && features.enabled( session, DefaultMavenFeatures.DYNAMIC_PHASES ) )
{
LifecyclePhaseNotFoundException lpnfe = new LifecyclePhaseNotFoundException(
"Dynamic phases such as \"" + task + "\" are only permitted as execution targets specified "
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
index ae5b03f..f1ed605 100644
--- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
@@ -24,6 +24,8 @@ import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.artifact.resolver.filter.CumulativeScopeArtifactFilter;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.execution.MavenSession;
+import org.apache.maven.feature.api.MavenFeatures;
+import org.apache.maven.feature.spi.DefaultMavenFeatures;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.lifecycle.MissingProjectException;
import org.apache.maven.plugin.BuildPluginManager;
@@ -77,6 +79,9 @@ public class MojoExecutor
@Requirement
private ExecutionEventCatapult eventCatapult;
+ @Requirement
+ private MavenFeatures features;
+
public MojoExecutor()
{
}
@@ -142,7 +147,8 @@ public class MojoExecutor
{
DependencyContext dependencyContext = newDependencyContext( session, mojoExecutions );
- PhaseRecorder phaseRecorder = new PhaseRecorder( session.getCurrentProject() );
+ boolean dynamicPhasesEnabled = features.enabled( session, DefaultMavenFeatures.DYNAMIC_PHASES );
+ PhaseRecorder phaseRecorder = new PhaseRecorder( session.getCurrentProject(), dynamicPhasesEnabled );
Iterator<MojoExecution> iterator = mojoExecutions.iterator();
try
@@ -155,6 +161,10 @@ public class MojoExecutor
}
catch ( LifecycleExecutionException failure )
{
+ if ( !dynamicPhasesEnabled )
+ {
+ throw failure;
+ }
// run any post: executions for the current phase
while ( iterator.hasNext() )
{
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java
index c76f22f..ae5b63e 100644
--- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java
@@ -35,9 +35,12 @@ public class PhaseRecorder
private final MavenProject project;
- public PhaseRecorder( MavenProject project )
+ private final boolean dynamicPhasesEnabled;
+
+ public PhaseRecorder( MavenProject project, boolean dynamicPhasesEnabled )
{
this.project = project;
+ this.dynamicPhasesEnabled = dynamicPhasesEnabled;
}
public void observeExecution( MojoExecution mojoExecution )
@@ -46,15 +49,30 @@ public class PhaseRecorder
if ( lifecyclePhase != null )
{
- PhaseId phaseId = PhaseId.of( lifecyclePhase );
- if ( lastLifecyclePhase == null )
+ if ( dynamicPhasesEnabled )
{
- lastLifecyclePhase = phaseId.phase();
+ PhaseId phaseId = PhaseId.of( lifecyclePhase );
+ if ( lastLifecyclePhase == null )
+ {
+ lastLifecyclePhase = phaseId.phase();
+ }
+ else if ( !phaseId.phase().equals( lastLifecyclePhase ) )
+ {
+ project.addLifecyclePhase( lastLifecyclePhase );
+ lastLifecyclePhase = phaseId.phase();
+ }
}
- else if ( !phaseId.phase().equals( lastLifecyclePhase ) )
+ else
{
- project.addLifecyclePhase( lastLifecyclePhase );
- lastLifecyclePhase = phaseId.phase();
+ if ( lastLifecyclePhase == null )
+ {
+ lastLifecyclePhase = lifecyclePhase;
+ }
+ else if ( !lifecyclePhase.equals( lastLifecyclePhase ) )
+ {
+ project.addLifecyclePhase( lastLifecyclePhase );
+ lastLifecyclePhase = lifecyclePhase;
+ }
}
}
diff --git a/maven-core/src/main/resources/META-INF/plexus/components.xml b/maven-core/src/main/resources/META-INF/plexus/components.xml
index 3f099cb..c8ff8d5 100644
--- a/maven-core/src/main/resources/META-INF/plexus/components.xml
+++ b/maven-core/src/main/resources/META-INF/plexus/components.xml
@@ -130,5 +130,16 @@ under the License.
<_configuration-file>~/.m2/settings-security.xml</_configuration-file>
</configuration>
</component>
+
+ <component>
+ <role>org.apache.maven.feature.api.MavenFeatures</role>
+ <role-hint>default</role-hint>
+ <implementation>org.apache.maven.feature.spi.DefaultMavenFeatures</implementation>
+ <configuration>
+ <features>
+ <feature>dynamic-phases</feature>
+ </features>
+ </configuration>
+ </component>
</components>
</component-set>
diff --git a/maven-core/src/test/java/org/apache/maven/lifecycle/internal/PhaseRecorderTest.java b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/PhaseRecorderTest.java
index f3d6422..6603bac 100644
--- a/maven-core/src/test/java/org/apache/maven/lifecycle/internal/PhaseRecorderTest.java
+++ b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/PhaseRecorderTest.java
@@ -30,7 +30,7 @@ import java.util.List;
public class PhaseRecorderTest extends TestCase
{
public void testObserveExecution() throws Exception {
- PhaseRecorder phaseRecorder = new PhaseRecorder( ProjectDependencyGraphStub.A);
+ PhaseRecorder phaseRecorder = new PhaseRecorder( ProjectDependencyGraphStub.A, false );
MavenExecutionPlan plan = LifecycleExecutionPlanCalculatorStub.getProjectAExceutionPlan();
final List<MojoExecution> executions = plan.getMojoExecutions();
diff --git a/maven-experiments/pom.xml b/maven-experiments/pom.xml
new file mode 100644
index 0000000..228f10b
--- /dev/null
+++ b/maven-experiments/pom.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven</artifactId>
+ <version>3.7.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>maven-experiments</artifactId>
+
+ <name>Maven Experiments</name>
+ <description>A build extension that enabled the experimental features in the current version of Maven</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.plexus</groupId>
+ <artifactId>plexus-component-annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.plexus</groupId>
+ <artifactId>plexus-component-metadata</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/maven-experiments/src/main/java/org/apache/maven/feature/check/MavenExperimentEnabler.java b/maven-experiments/src/main/java/org/apache/maven/feature/check/MavenExperimentEnabler.java
new file mode 100644
index 0000000..26e2ee4
--- /dev/null
+++ b/maven-experiments/src/main/java/org/apache/maven/feature/check/MavenExperimentEnabler.java
@@ -0,0 +1,164 @@
+package org.apache.maven.feature.check;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.WeakHashMap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.maven.AbstractMavenLifecycleParticipant;
+import org.apache.maven.Maven;
+import org.apache.maven.MavenExecutionException;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.feature.api.MavenFeatures;
+import org.apache.maven.feature.spi.DefaultMavenFeatures;
+import org.codehaus.plexus.component.annotations.Component;
+import org.codehaus.plexus.component.annotations.Requirement;
+import org.codehaus.plexus.logging.Logger;
+
+/**
+ * Enforces that the required version of Maven is running and enables the experimental features of that version.
+ */
+@Component( role = AbstractMavenLifecycleParticipant.class, hint = "experimental" )
+public class MavenExperimentEnabler
+ extends AbstractMavenLifecycleParticipant
+{
+
+ @Requirement
+ private Logger log;
+
+ @Requirement( role = MavenFeatures.class, hint = "default", optional = true )
+ private MavenFeatures features;
+
+ private final Map<MavenSession, Void> startedSessions = new WeakHashMap<>();
+
+ @Override
+ public void afterProjectsRead( MavenSession session )
+ throws MavenExecutionException
+ {
+ if ( !startedSessions.containsKey( session ) )
+ {
+ log.error( "Experimental features cannot be enabled using project/build/extensions" );
+ throw new MavenExecutionException( "Experimental features cannot be enabled using project/build/extensions",
+ topLevelProjectFile( session ) );
+ }
+ }
+
+ @Override
+ public void afterSessionStart( MavenSession session )
+ throws MavenExecutionException
+ {
+ startedSessions.put( session, null );
+ log.debug( "Determining experimental feature version requirements" );
+ String targetVersion;
+ try
+ {
+ targetVersion = parse( getClass(), "/META-INF/maven/org.apache.maven/maven-experiments/pom.properties" );
+ }
+ catch ( IOException e )
+ {
+ throw new MavenExecutionException(
+ "Cannot determine required version of Maven to enable experimental features",
+ topLevelProjectFile( session ) );
+ }
+ if ( StringUtils.isBlank( targetVersion ) )
+ {
+ throw new MavenExecutionException(
+ "Cannot determine required version of Maven to enable experimental features",
+ topLevelProjectFile( session ) );
+ }
+ String activeVersion;
+ try
+ {
+ activeVersion = parse( Maven.class, "/META-INF/maven/org.apache.maven/maven-core/pom.properties" );
+ }
+ catch ( IOException e )
+ {
+ throw new MavenExecutionException( "Cannot confirm executing version of Maven as " + targetVersion
+ + " which is required to enable the experimental features used"
+ + " by this project", topLevelProjectFile( session ) );
+ }
+ if ( Objects.equals( activeVersion, targetVersion ) )
+ {
+ log.info( "Enabling experimental features of Maven " + targetVersion );
+ }
+ else
+ {
+ throw new MavenExecutionException(
+ "The project uses experimental features that require exactly Maven " + targetVersion,
+ topLevelProjectFile( session ) );
+ }
+ try
+ {
+ enableFeatures( session, targetVersion );
+ }
+ catch ( LinkageError e )
+ {
+ throw new MavenExecutionException(
+ "The project uses experimental features that require exactly Maven " + targetVersion,
+ topLevelProjectFile( session ) );
+ }
+ }
+
+ private void enableFeatures( MavenSession session, String targetVersion )
+ throws MavenExecutionException
+ {
+ if ( !( features instanceof DefaultMavenFeatures ) )
+ {
+ throw new MavenExecutionException(
+ "This project uses experimental features that require exactly Maven " + targetVersion
+ + ", cannot enable experimental features because feature flag component is not as expected (was: "
+ + features + ")", topLevelProjectFile( session ) );
+ }
+ ( (DefaultMavenFeatures) features ).enable( session );
+ }
+
+ private File topLevelProjectFile( MavenSession session )
+ {
+ return session.getTopLevelProject() != null ? session.getTopLevelProject().getFile() : null;
+ }
+
+ private static String parse( Class<?> clazz, String resource )
+ throws IOException
+ {
+ Properties targetProperties = new Properties();
+ try ( InputStream is = clazz.getResourceAsStream( resource ) )
+ {
+ if ( is != null )
+ {
+ targetProperties.load( is );
+ }
+ }
+ return targetProperties.getProperty( "version" );
+ }
+
+ @Override
+ public void afterSessionEnd( MavenSession session )
+ throws MavenExecutionException
+ {
+ startedSessions.remove( session );
+ }
+
+}
diff --git a/maven-feature/pom.xml b/maven-feature/pom.xml
new file mode 100644
index 0000000..6e540f0
--- /dev/null
+++ b/maven-feature/pom.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven</artifactId>
+ <version>3.7.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>maven-feature</artifactId>
+
+ <name>Maven Feature</name>
+ <description>Feature Flags for Maven Core</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.plexus</groupId>
+ <artifactId>plexus-utils</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/maven-feature/src/main/java/org/apache/maven/feature/api/MavenFeatureContext.java b/maven-feature/src/main/java/org/apache/maven/feature/api/MavenFeatureContext.java
new file mode 100644
index 0000000..e1b0fdb
--- /dev/null
+++ b/maven-feature/src/main/java/org/apache/maven/feature/api/MavenFeatureContext.java
@@ -0,0 +1,30 @@
+package org.apache.maven.feature.api;
+
+/*
+ * 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.
+ */
+
+
+/**
+ * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
+ *
+ * Defines the context within which a features are evaluated.
+ */
+public interface MavenFeatureContext
+{
+}
diff --git a/maven-feature/src/main/java/org/apache/maven/feature/api/MavenFeatures.java b/maven-feature/src/main/java/org/apache/maven/feature/api/MavenFeatures.java
new file mode 100644
index 0000000..2e9222f
--- /dev/null
+++ b/maven-feature/src/main/java/org/apache/maven/feature/api/MavenFeatures.java
@@ -0,0 +1,39 @@
+package org.apache.maven.feature.api;
+
+/*
+ * 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.
+ */
+
+/**
+ * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
+ *
+ * An API to allow making assertions about the presence of specific features in Maven core.
+ * Features are identified using string constants and are only intended for use in opt-in experiments.
+ * Unknown features will always be reported as disabled.
+ */
+public interface MavenFeatures
+{
+ /**
+ * Returns {@code true} if and only if the specified feature is enabled.
+ *
+ * @param context the context within which to check the feature.
+ * @param featureName the name of the feature.
+ * @return {@code true} if and only if the specified feature is enabled.
+ */
+ boolean enabled( MavenFeatureContext context, String featureName );
+}
diff --git a/pom.xml b/pom.xml
index ef2764d..a5bb34d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -84,6 +84,7 @@ under the License.
<module>maven-plugin-api</module>
<module>maven-builder-support</module>
<module>maven-model</module>
+ <module>maven-feature</module>
<module>maven-model-builder</module>
<module>maven-core</module>
<module>maven-settings</module>
@@ -95,6 +96,7 @@ under the License.
<module>maven-slf4j-wrapper</module>
<module>maven-embedder</module>
<module>maven-compat</module>
+ <module>maven-experiments</module>
<module>apache-maven</module>
</modules>
@@ -181,6 +183,16 @@ under the License.
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
+ <artifactId>maven-experiments</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-feature</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
<artifactId>maven-settings</artifactId>
<version>${project.version}</version>
</dependency>