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>