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 <stephen.alan.conno...@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>