You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by mt...@apache.org on 2021/03/22 14:09:43 UTC

[maven] branch master updated: [MNG-6511] Optional project selection

This is an automated email from the ASF dual-hosted git repository.

mthmulders pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new c3cf294  [MNG-6511] Optional project selection
c3cf294 is described below

commit c3cf29438e3d65d6ee5c5726f8611af99d9a649a
Author: Maarten Mulders <mt...@apache.org>
AuthorDate: Fri Feb 12 15:14:02 2021 +0100

    [MNG-6511] Optional project selection
    
    Closes #446.
---
 .../apache/maven/execution/ActivationSettings.java |  58 ++++++
 .../DefaultBuildResumptionDataRepository.java      |   2 +-
 .../execution/DefaultMavenExecutionRequest.java    |  37 ++--
 .../maven/execution/MavenExecutionRequest.java     |  18 ++
 .../apache/maven/execution/ProfileActivation.java  |  31 ----
 .../apache/maven/execution/ProjectActivation.java  | 202 +++++++++++++++++++++
 .../apache/maven/graph/DefaultGraphBuilder.java    |  99 +++++++---
 .../DefaultBuildResumptionDataRepositoryTest.java  |   6 +-
 .../maven/graph/DefaultGraphBuilderTest.java       | 201 ++++++++++++++++----
 .../main/java/org/apache/maven/cli/MavenCli.java   |  84 +++------
 .../java/org/apache/maven/cli/MavenCliTest.java    |  38 ++--
 11 files changed, 568 insertions(+), 208 deletions(-)

diff --git a/maven-core/src/main/java/org/apache/maven/execution/ActivationSettings.java b/maven-core/src/main/java/org/apache/maven/execution/ActivationSettings.java
new file mode 100644
index 0000000..4e8f8a2
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/execution/ActivationSettings.java
@@ -0,0 +1,58 @@
+package org.apache.maven.execution;
+
+/*
+ * 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.
+ */
+
+/**
+ * Describes whether a target should be activated or not, and if that is required or optional.
+ */
+enum ActivationSettings
+{
+    ACTIVATION_OPTIONAL( true, true ),
+    ACTIVATION_REQUIRED( true, false ),
+    DEACTIVATION_OPTIONAL( false, true ),
+    DEACTIVATION_REQUIRED( false, false );
+
+    /**
+     * Should the target be active?
+     */
+    final boolean active;
+    /**
+     * Should the build continue if the target is not present?
+     */
+    final boolean optional;
+
+    ActivationSettings( final boolean active, final boolean optional )
+    {
+        this.active = active;
+        this.optional = optional;
+    }
+
+    static ActivationSettings of( final boolean active, final boolean optional )
+    {
+        if ( optional )
+        {
+            return active ? ACTIVATION_OPTIONAL : DEACTIVATION_OPTIONAL;
+        }
+        else
+        {
+            return active ? ACTIVATION_REQUIRED : DEACTIVATION_REQUIRED;
+        }
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java
index 02704c5..73a0c1a 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java
@@ -132,7 +132,7 @@ public class DefaultBuildResumptionDataRepository implements BuildResumptionData
             String propertyValue = properties.getProperty( REMAINING_PROJECTS );
             Stream.of( propertyValue.split( PROPERTY_DELIMITER ) )
                     .filter( StringUtils::isNotEmpty )
-                    .forEach( request.getSelectedProjects()::add );
+                    .forEach( request.getProjectActivation()::activateOptionalProject );
             LOGGER.info( "Resuming from {} due to the --resume / -r feature.", propertyValue );
         }
     }
diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java
index 554928b..066d27f 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java
@@ -76,6 +76,7 @@ public class DefaultMavenExecutionRequest
 
     private List<Profile> profiles;
 
+    private final ProjectActivation projectActivation = new ProjectActivation();
     private final ProfileActivation profileActivation = new ProfileActivation();
 
     private List<String> pluginGroups;
@@ -114,10 +115,6 @@ public class DefaultMavenExecutionRequest
 
     private String reactorFailureBehavior = REACTOR_FAIL_FAST;
 
-    private List<String> selectedProjects;
-
-    private List<String> excludedProjects;
-
     private boolean resume = false;
 
     private String resumeFrom;
@@ -281,23 +278,13 @@ public class DefaultMavenExecutionRequest
     @Override
     public List<String> getSelectedProjects()
     {
-        if ( selectedProjects == null )
-        {
-            selectedProjects = new ArrayList<>();
-        }
-
-        return selectedProjects;
+        return this.projectActivation.getSelectedProjects();
     }
 
     @Override
     public List<String> getExcludedProjects()
     {
-        if ( excludedProjects == null )
-        {
-            excludedProjects = new ArrayList<>();
-        }
-
-        return excludedProjects;
+        return this.projectActivation.getExcludedProjects();
     }
 
     @Override
@@ -359,6 +346,12 @@ public class DefaultMavenExecutionRequest
     }
 
     @Override
+    public ProjectActivation getProjectActivation()
+    {
+        return this.projectActivation;
+    }
+
+    @Override
     public ProfileActivation getProfileActivation()
     {
         return this.profileActivation;
@@ -569,11 +562,7 @@ public class DefaultMavenExecutionRequest
     {
         if ( selectedProjects != null )
         {
-            this.selectedProjects = new ArrayList<>( selectedProjects );
-        }
-        else
-        {
-            this.selectedProjects = null;
+            this.projectActivation.overwriteActiveProjects( selectedProjects );
         }
 
         return this;
@@ -584,11 +573,7 @@ public class DefaultMavenExecutionRequest
     {
         if ( excludedProjects != null )
         {
-            this.excludedProjects = new ArrayList<>( excludedProjects );
-        }
-        else
-        {
-            this.excludedProjects = null;
+            this.projectActivation.overwriteInactiveProjects( excludedProjects );
         }
 
         return this;
diff --git a/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java b/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java
index d0ac0f1..3989c5f 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java
@@ -154,21 +154,33 @@ public interface MavenExecutionRequest
 
     String getReactorFailureBehavior();
 
+    /**
+     * @deprecated Since Maven 4: use {@link #getProjectActivation()}.
+     */
+    @Deprecated
     MavenExecutionRequest setSelectedProjects( List<String> projects );
 
+    /**
+     * @deprecated Since Maven 4: use {@link #getProjectActivation()}.
+     */
+    @Deprecated
     List<String> getSelectedProjects();
 
     /**
      * @param projects the projects to exclude
      * @return this MavenExecutionRequest
      * @since 3.2
+     * @deprecated Since Maven 4: use {@link #getProjectActivation()}.
      */
+    @Deprecated
     MavenExecutionRequest setExcludedProjects( List<String> projects );
 
     /**
      * @return the excluded projects, never {@code null}
      * @since 3.2
+     * @deprecated Since Maven 4: use {@link #getProjectActivation()}.
      */
+    @Deprecated
     List<String> getExcludedProjects();
 
     /**
@@ -328,6 +340,12 @@ public interface MavenExecutionRequest
     List<String> getInactiveProfiles();
 
     /**
+     * Return the requested activation(s) of project(s) in this execution.
+     * @return requested (de-)activation(s) of project(s) in this execution. Never {@code null}.
+     */
+    ProjectActivation getProjectActivation();
+
+    /**
      * Return the requested activation(s) of profile(s) in this execution.
      * @return requested (de-)activation(s) of profile(s) in this execution. Never {@code null}.
      */
diff --git a/maven-core/src/main/java/org/apache/maven/execution/ProfileActivation.java b/maven-core/src/main/java/org/apache/maven/execution/ProfileActivation.java
index 1837696..52f5e06 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/ProfileActivation.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/ProfileActivation.java
@@ -35,37 +35,6 @@ import static java.util.stream.Collectors.toSet;
  */
 public class ProfileActivation
 {
-    private enum ActivationSettings
-    {
-        ACTIVATION_OPTIONAL( true, true ),
-        ACTIVATION_REQUIRED( true, false ),
-        DEACTIVATION_OPTIONAL( false, true ),
-        DEACTIVATION_REQUIRED( false, false );
-
-        /** Should the profile be active? */
-        final boolean active;
-        /** Should the build continue if the profile is not present? */
-        final boolean optional;
-
-        ActivationSettings( final boolean active, final boolean optional )
-        {
-            this.active = active;
-            this.optional = optional;
-        }
-
-        static ActivationSettings of( final boolean active, final boolean optional )
-        {
-            if ( optional )
-            {
-                return active ? ACTIVATION_OPTIONAL : DEACTIVATION_OPTIONAL;
-            }
-            else
-            {
-                return active ? ACTIVATION_REQUIRED : DEACTIVATION_REQUIRED;
-            }
-        }
-    }
-
     private final Map<String, ActivationSettings> activations = new HashMap<>();
 
     /**
diff --git a/maven-core/src/main/java/org/apache/maven/execution/ProjectActivation.java b/maven-core/src/main/java/org/apache/maven/execution/ProjectActivation.java
new file mode 100644
index 0000000..579ab18
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/execution/ProjectActivation.java
@@ -0,0 +1,202 @@
+package org.apache.maven.execution;
+
+/*
+ * 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.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.stream.Collectors.toSet;
+
+/**
+ * Container for storing the request from the user to activate or deactivate certain projects and optionally fail the
+ * build if those projects do not exist.
+ */
+public class ProjectActivation
+{
+    private static class ProjectActivationSettings
+    {
+        /**
+         * The selector of a project. This can be the project directory, [groupId]:[artifactId] or :[artifactId].
+         */
+        final String selector;
+
+        /**
+         * This describes how/when to active or deactivate the project.
+         */
+        final ActivationSettings activationSettings;
+
+        ProjectActivationSettings( String selector, ActivationSettings activationSettings )
+        {
+            this.selector = selector;
+            this.activationSettings = activationSettings;
+        }
+    }
+
+    /**
+     * List of activated and deactivated projects.
+     */
+    private final List<ProjectActivationSettings> activations = new ArrayList<>();
+
+    /**
+     * Adds a project activation to the request.
+     * @param selector The selector of the project.
+     * @param active Should the project be activated?
+     * @param optional Can the build continue if the project does not exist?
+     */
+    public void addProjectActivation( String selector, boolean active, boolean optional )
+    {
+        final ActivationSettings settings = ActivationSettings.of( active, optional );
+        this.activations.add( new ProjectActivationSettings( selector, settings ) );
+    }
+
+    private Stream<ProjectActivationSettings> getProjects( final Predicate<ActivationSettings> predicate )
+    {
+        return this.activations.stream()
+                .filter( activation -> predicate.test( activation.activationSettings ) );
+    }
+
+    private Set<String> getProjectSelectors( final Predicate<ActivationSettings> predicate )
+    {
+        return getProjects( predicate )
+                .map( activation -> activation.selector )
+                .collect( toSet() );
+    }
+
+    /**
+     * @return Required active project selectors, never {@code null}.
+     */
+    public Set<String> getRequiredActiveProjectSelectors()
+    {
+        return getProjectSelectors( pa -> !pa.optional && pa.active );
+    }
+
+    /**
+     * @return Optional active project selectors, never {@code null}.
+     */
+    public Set<String> getOptionalActiveProjectSelectors()
+    {
+        return getProjectSelectors( pa -> pa.optional && pa.active );
+    }
+
+    /**
+     * @return Required inactive project selectors, never {@code null}.
+     */
+    public Set<String> getRequiredInactiveProjectSelectors()
+    {
+        return getProjectSelectors( pa -> !pa.optional && !pa.active );
+    }
+
+    /**
+     * @return Optional inactive project selectors, never {@code null}.
+     */
+    public Set<String> getOptionalInactiveProjectSelectors()
+    {
+        return getProjectSelectors( pa -> pa.optional && !pa.active );
+    }
+
+    /**
+     * Mimics the pre-Maven 4 "selected projects" list.
+     * @deprecated Use {@link #getRequiredActiveProjectSelectors()} and {@link #getOptionalActiveProjectSelectors()}
+     * instead.
+     */
+    @Deprecated
+    public List<String> getSelectedProjects()
+    {
+        return Collections.unmodifiableList( new ArrayList<>( getProjectSelectors( pa -> pa.active ) ) );
+    }
+
+    /**
+     * Mimics the pre-Maven 4 "excluded projects" list.
+     * @deprecated Use {@link #getRequiredInactiveProjectSelectors()} and {@link #getOptionalInactiveProjectSelectors()}
+     * instead.
+     */
+    @Deprecated
+    public List<String> getExcludedProjects()
+    {
+        return Collections.unmodifiableList( new ArrayList<>( getProjectSelectors( pa -> !pa.active ) ) );
+    }
+
+    /**
+     * Overwrites the active projects based on a pre-Maven 4 "active projects" list.
+     * @param activeProjectSelectors A {@link List} of project selectors that must be activated.
+     * @deprecated Use {@link #activateOptionalProject(String)} or {@link #activateRequiredProject(String)} instead.
+     */
+    @Deprecated
+    public void overwriteActiveProjects( List<String> activeProjectSelectors )
+    {
+        List<ProjectActivationSettings> projects = getProjects( pa -> pa.active ).collect( Collectors.toList() );
+        this.activations.removeAll( projects );
+        activeProjectSelectors.forEach( this::activateOptionalProject );
+    }
+
+    /**
+     * Overwrites the inactive projects based on a pre-Maven 4 "inactive projects" list.
+     * @param inactiveProjectSelectors A {@link List} of project selectors that must be deactivated.
+     * @deprecated Use {@link #deactivateOptionalProject(String)} or {@link #deactivateRequiredProject(String)} instead.
+     */
+    @Deprecated
+    public void overwriteInactiveProjects( List<String> inactiveProjectSelectors )
+    {
+        List<ProjectActivationSettings> projects = getProjects( pa -> !pa.active ).collect( Collectors.toList() );
+        this.activations.removeAll( projects );
+        inactiveProjectSelectors.forEach( this::deactivateOptionalProject );
+    }
+
+    /**
+     * Mark a project as required and activated.
+     * @param selector The selector of the project.
+     */
+    public void activateRequiredProject( String selector )
+    {
+        this.activations.add( new ProjectActivationSettings( selector, ActivationSettings.ACTIVATION_REQUIRED ) );
+    }
+
+    /**
+     * Mark a project as optional and activated.
+     * @param selector The selector of the project.
+     */
+    public void activateOptionalProject( String selector )
+    {
+        this.activations.add( new ProjectActivationSettings( selector, ActivationSettings.ACTIVATION_OPTIONAL ) );
+    }
+
+    /**
+     * Mark a project as required and deactivated.
+     * @param selector The selector of the project.
+     */
+    public void deactivateRequiredProject( String selector )
+    {
+        this.activations.add( new ProjectActivationSettings( selector, ActivationSettings.DEACTIVATION_REQUIRED ) );
+    }
+
+    /**
+     * Mark a project as optional and deactivated.
+     * @param selector The selector of the project.
+     */
+    public void deactivateOptionalProject( String selector )
+    {
+        this.activations.add( new ProjectActivationSettings( selector, ActivationSettings.DEACTIVATION_OPTIONAL ) );
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java b/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java
index 49d52af..e747868 100644
--- a/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java
+++ b/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java
@@ -21,7 +21,6 @@ package org.apache.maven.graph;
 
 import java.io.File;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -29,6 +28,7 @@ import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 import javax.inject.Inject;
@@ -41,6 +41,7 @@ import org.apache.maven.artifact.ArtifactUtils;
 import org.apache.maven.execution.BuildResumptionDataRepository;
 import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.execution.MavenSession;
+import org.apache.maven.execution.ProjectActivation;
 import org.apache.maven.execution.ProjectDependencyGraph;
 import org.apache.maven.model.Plugin;
 import org.apache.maven.model.building.DefaultModelProblem;
@@ -176,38 +177,69 @@ public class DefaultGraphBuilder
     {
         List<MavenProject> result = projects;
 
-        if ( !request.getSelectedProjects().isEmpty() )
+        ProjectActivation projectActivation = request.getProjectActivation();
+        Set<String> requiredSelectors = projectActivation.getRequiredActiveProjectSelectors();
+        Set<String> optionalSelectors = projectActivation.getOptionalActiveProjectSelectors();
+        if ( !requiredSelectors.isEmpty() || !optionalSelectors.isEmpty() )
         {
-            File reactorDirectory = getReactorDirectory( request );
+            Set<MavenProject> selectedProjects = new HashSet<>( requiredSelectors.size() + optionalSelectors.size() );
+            selectedProjects.addAll( getProjectsBySelectors( request, projects, requiredSelectors, true ) );
+            selectedProjects.addAll( getProjectsBySelectors( request, projects, optionalSelectors, false ) );
+
+            // it can be empty when an optional project is missing from the reactor, fallback to returning all projects
+            if ( !selectedProjects.isEmpty() )
+            {
+                result = new ArrayList<>( selectedProjects );
+
+                result = includeAlsoMakeTransitively( result, request, graph );
+
+                // Order the new list in the original order
+                List<MavenProject> sortedProjects = graph.getSortedProjects();
+                result.sort( comparing( sortedProjects::indexOf ) );
+            }
+        }
 
-            Collection<MavenProject> selectedProjects = new LinkedHashSet<>();
+        return result;
+    }
 
-            for ( String selector : request.getSelectedProjects() )
+    private Set<MavenProject> getProjectsBySelectors( MavenExecutionRequest request, List<MavenProject> projects,
+                                                      Set<String> projectSelectors, boolean required )
+            throws MavenExecutionException
+    {
+        Set<MavenProject> selectedProjects = new LinkedHashSet<>();
+        File reactorDirectory = getReactorDirectory( request );
+
+        for ( String selector : projectSelectors )
+        {
+            Optional<MavenProject> optSelectedProject = projects.stream()
+                    .filter( project -> isMatchingProject( project, selector, reactorDirectory ) )
+                    .findFirst();
+            if ( !optSelectedProject.isPresent() )
             {
-                MavenProject selectedProject = projects.stream()
-                        .filter( project -> isMatchingProject( project, selector, reactorDirectory ) )
-                        .findFirst()
-                        .orElseThrow( () -> new MavenExecutionException(
-                                "Could not find the selected project in the reactor: " + selector, request.getPom() ) );
-                selectedProjects.add( selectedProject );
-
-                List<MavenProject> children = selectedProject.getCollectedProjects();
-                if ( children != null )
+                String message = "Could not find the selected project in the reactor: " + selector;
+                if ( required )
                 {
-                    selectedProjects.addAll( children );
+                    throw new MavenExecutionException( message, request.getPom() );
+                }
+                else
+                {
+                    LOGGER.info( message );
+                    break;
                 }
             }
 
-            result = new ArrayList<>( selectedProjects );
+            MavenProject selectedProject = optSelectedProject.get();
 
-            result = includeAlsoMakeTransitively( result, request, graph );
+            selectedProjects.add( selectedProject );
 
-            // Order the new list in the original order
-            List<MavenProject> sortedProjects = graph.getSortedProjects();
-            result.sort( comparing( sortedProjects::indexOf ) );
+            List<MavenProject> children = selectedProject.getCollectedProjects();
+            if ( children != null )
+            {
+                selectedProjects.addAll( children );
+            }
         }
 
-        return result;
+        return selectedProjects;
     }
 
     private List<MavenProject> trimResumedProjects( List<MavenProject> projects, ProjectDependencyGraph graph,
@@ -242,20 +274,19 @@ public class DefaultGraphBuilder
     {
         List<MavenProject> result = projects;
 
-        if ( !request.getExcludedProjects().isEmpty() )
+        ProjectActivation projectActivation = request.getProjectActivation();
+        Set<String> requiredSelectors = projectActivation.getRequiredInactiveProjectSelectors();
+        Set<String> optionalSelectors = projectActivation.getOptionalInactiveProjectSelectors();
+        if ( !requiredSelectors.isEmpty() || !optionalSelectors.isEmpty() )
         {
-            File reactorDirectory = getReactorDirectory( request );
+            Set<MavenProject> excludedProjects = new HashSet<>( requiredSelectors.size() + optionalSelectors.size() );
+            excludedProjects.addAll( getProjectsBySelectors( request, projects, requiredSelectors, true ) );
+            excludedProjects.addAll( getProjectsBySelectors( request, projects, optionalSelectors, false ) );
 
             result = new ArrayList<>( projects );
 
-            for ( String selector : request.getExcludedProjects() )
+            for ( MavenProject excludedProject : excludedProjects )
             {
-                MavenProject excludedProject = projects.stream()
-                        .filter( project -> isMatchingProject( project, selector, reactorDirectory ) )
-                        .findFirst()
-                        .orElseThrow( () -> new MavenExecutionException( "Could not find the selected project in "
-                                + "the reactor: " + selector, request.getPom() ) );
-
                 boolean isExcludedProjectRemoved = result.remove( excludedProject );
 
                 if ( isExcludedProjectRemoved )
@@ -267,6 +298,14 @@ public class DefaultGraphBuilder
                     }
                 }
             }
+
+            if ( result.isEmpty() )
+            {
+                boolean isPlural = excludedProjects.size() > 1;
+                String message = String.format( "The project exclusion%s in --projects/-pl resulted in an "
+                        + "empty reactor, please correct %s.", isPlural ? "s" : "", isPlural ? "them" : "it" );
+                throw new MavenExecutionException( message, request.getPom() );
+            }
         }
 
         return result;
diff --git a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java
index 5667418..7a03f85 100644
--- a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java
+++ b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java
@@ -29,7 +29,7 @@ import java.util.Properties;
 
 import static java.util.Arrays.asList;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.is;
 
@@ -74,7 +74,7 @@ public class DefaultBuildResumptionDataRepositoryTest
 
         repository.applyResumptionProperties( request, properties );
 
-        assertThat( request.getSelectedProjects(), contains( ":module-a", ":module-b", ":module-c" ) );
+        assertThat( request.getSelectedProjects(), containsInAnyOrder( ":module-a", ":module-b", ":module-c" ) );
     }
 
     @Test
@@ -100,6 +100,6 @@ public class DefaultBuildResumptionDataRepositoryTest
 
         repository.applyResumptionData( request,  rootProject );
 
-        assertThat( request.getSelectedProjects(), contains( "example:module-c" ) );
+        assertThat( request.getSelectedProjects(), containsInAnyOrder( "example:module-c" ) );
     }
 }
diff --git a/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java b/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java
index e82c735..1a5565d 100644
--- a/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java
+++ b/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java
@@ -19,9 +19,11 @@ package org.apache.maven.graph;
  * under the License.
  */
 
+import org.apache.maven.MavenExecutionException;
 import org.apache.maven.execution.BuildResumptionDataRepository;
 import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.execution.MavenSession;
+import org.apache.maven.execution.ProjectActivation;
 import org.apache.maven.execution.ProjectDependencyGraph;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.Parent;
@@ -45,6 +47,7 @@ import org.junit.jupiter.params.provider.MethodSource;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -55,9 +58,11 @@ import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
 import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toList;
 import static org.apache.maven.execution.MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM;
 import static org.apache.maven.execution.MavenExecutionRequest.REACTOR_MAKE_UPSTREAM;
 import static org.apache.maven.graph.DefaultGraphBuilderTest.ScenarioBuilder.scenario;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -65,7 +70,7 @@ import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-public class DefaultGraphBuilderTest
+class DefaultGraphBuilderTest
 {
     /*
     The multi-module structure in this project is displayed as follows:
@@ -78,6 +83,7 @@ public class DefaultGraphBuilderTest
          └─── module-c-1
               module-c-2        (depends on module-b)
      */
+    private static final String GROUP_ID = "unittest";
     private static final String PARENT_MODULE = "module-parent";
     private static final String INDEPENDENT_MODULE = "module-independent";
     private static final String MODULE_A = "module-a";
@@ -109,23 +115,59 @@ public class DefaultGraphBuilderTest
                 scenario( "Full reactor in order" )
                         .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE ),
                 scenario( "Selected project" )
-                        .selectedProjects( MODULE_B )
+                        .activeRequiredProjects( MODULE_B )
                         .expectResult( MODULE_B ),
                 scenario( "Selected project (including child modules)" )
-                        .selectedProjects( MODULE_C )
+                        .activeRequiredProjects( MODULE_C )
                         .expectResult( MODULE_C, MODULE_C_1, MODULE_C_2 ),
+                scenario( "Selected optional project" )
+                        .activeOptionalProjects( MODULE_B )
+                        .expectResult( MODULE_B ),
+                scenario( "Selected missing optional project" )
+                        .activeOptionalProjects( "non-existing-module" )
+                        .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE ),
+                scenario( "Selected missing optional and required project" )
+                        .activeOptionalProjects( "non-existing-module" )
+                        .activeRequiredProjects( MODULE_B )
+                        .expectResult( MODULE_B ),
                 scenario( "Excluded project" )
-                        .excludedProjects( MODULE_B )
+                        .inactiveRequiredProjects( MODULE_B )
+                        .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE ),
+                scenario( "Excluded optional project" )
+                        .inactiveOptionalProjects( MODULE_B )
+                        .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE ),
+                scenario( "Excluded missing optional project" )
+                        .inactiveOptionalProjects( "non-existing-module" )
+                        .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE ),
+                scenario( "Excluded missing optional and required project" )
+                        .inactiveOptionalProjects( "non-existing-module" )
+                        .inactiveRequiredProjects( MODULE_B )
                         .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE ),
+                scenario( "Selected and excluded same project" )
+                        .activeRequiredProjects( MODULE_A )
+                        .inactiveRequiredProjects( MODULE_A )
+                        .expectResult( MavenExecutionException.class, "empty reactor" ),
+                scenario( "Project selected with different selector resolves to same project" )
+                        .activeRequiredProjects( GROUP_ID + ":" + MODULE_A )
+                        .inactiveRequiredProjects( MODULE_A )
+                        .expectResult( MavenExecutionException.class, "empty reactor" ),
+                scenario( "Selected and excluded same project, but also selected another project" )
+                        .activeRequiredProjects( MODULE_A, MODULE_B )
+                        .inactiveRequiredProjects( MODULE_A )
+                        .expectResult( MODULE_B ),
+                scenario( "Selected missing project as required and as optional" )
+                        .activeRequiredProjects( "non-existing-module" )
+                        .activeOptionalProjects( "non-existing-module" )
+                        .expectResult( MavenExecutionException.class, "not find the selected project" ),
                 scenario( "Resuming from project" )
                         .resumeFrom( MODULE_B )
                         .expectResult( MODULE_B, MODULE_C_2, INDEPENDENT_MODULE ),
                 scenario( "Selected project with also make dependencies" )
-                        .selectedProjects( MODULE_C_2 )
+                        .activeRequiredProjects( MODULE_C_2 )
                         .makeBehavior( REACTOR_MAKE_UPSTREAM )
                         .expectResult( PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2 ),
                 scenario( "Selected project with also make dependents" )
-                        .selectedProjects( MODULE_B )
+                        .activeRequiredProjects( MODULE_B )
                         .makeBehavior( REACTOR_MAKE_DOWNSTREAM )
                         .expectResult( MODULE_B, MODULE_C_2 ),
                 scenario( "Resuming from project with also make dependencies" )
@@ -133,42 +175,42 @@ public class DefaultGraphBuilderTest
                         .resumeFrom( MODULE_C_2 )
                         .expectResult( PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE ),
                 scenario( "Selected project with resume from and also make dependency (MNG-4960 IT#1)" )
-                        .selectedProjects( MODULE_C_2 )
+                        .activeRequiredProjects( MODULE_C_2 )
                         .resumeFrom( MODULE_B )
                         .makeBehavior( REACTOR_MAKE_UPSTREAM )
                         .expectResult( PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2 ),
                 scenario( "Selected project with resume from and also make dependent (MNG-4960 IT#2)" )
-                        .selectedProjects( MODULE_B )
+                        .activeRequiredProjects( MODULE_B )
                         .resumeFrom( MODULE_C_2 )
                         .makeBehavior( REACTOR_MAKE_DOWNSTREAM )
                         .expectResult( MODULE_C_2 ),
                 scenario( "Excluding an also make dependency from selectedProject does take its transitive dependency" )
-                        .selectedProjects( MODULE_C_2 )
-                        .excludedProjects( MODULE_B )
+                        .activeRequiredProjects( MODULE_C_2 )
+                        .inactiveRequiredProjects( MODULE_B )
                         .makeBehavior( REACTOR_MAKE_UPSTREAM )
                         .expectResult( PARENT_MODULE, MODULE_C, MODULE_A, MODULE_C_2 ),
                 scenario( "Excluding a project also excludes its children" )
-                        .excludedProjects( MODULE_C )
+                        .inactiveRequiredProjects( MODULE_C )
                         .expectResult( PARENT_MODULE, MODULE_A, MODULE_B, INDEPENDENT_MODULE ),
                 scenario( "Excluding an also make dependency from resumeFrom does take its transitive dependency" )
                         .resumeFrom( MODULE_C_2 )
-                        .excludedProjects( MODULE_B )
+                        .inactiveRequiredProjects( MODULE_B )
                         .makeBehavior( REACTOR_MAKE_UPSTREAM )
                         .expectResult( PARENT_MODULE, MODULE_C, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE ),
                 scenario( "Resume from exclude project downstream" )
                         .resumeFrom( MODULE_A )
-                        .excludedProjects( MODULE_B )
+                        .inactiveRequiredProjects( MODULE_B )
                         .expectResult( MODULE_A, MODULE_C_2, INDEPENDENT_MODULE ),
                 scenario( "Exclude the project we are resuming from (as proposed in MNG-6676)" )
                         .resumeFrom( MODULE_B )
-                        .excludedProjects( MODULE_B )
+                        .inactiveRequiredProjects( MODULE_B )
                         .expectResult( MODULE_C_2, INDEPENDENT_MODULE ),
                 scenario( "Selected projects in wrong order are resumed correctly in order" )
-                        .selectedProjects( MODULE_C_2, MODULE_B, MODULE_A )
+                        .activeRequiredProjects( MODULE_C_2, MODULE_B, MODULE_A )
                         .resumeFrom( MODULE_B )
                         .expectResult( MODULE_B, MODULE_C_2 ),
                 scenario( "Duplicate projects are filtered out" )
-                        .selectedProjects( MODULE_A, MODULE_A )
+                        .activeRequiredProjects( MODULE_A, MODULE_A )
                         .expectResult( MODULE_A ),
                 scenario( "Select reactor by specific pom" )
                         .requestedPom( MODULE_C )
@@ -184,23 +226,49 @@ public class DefaultGraphBuilderTest
         );
     }
 
+    interface ExpectedResult {
+
+    }
+    static class SelectedProjectsResult implements ExpectedResult {
+        final List<String> projectNames;
+
+        public SelectedProjectsResult( List<String> projectSelectors )
+        {
+            this.projectNames = projectSelectors;
+        }
+    }
+    static class ExceptionThrown implements ExpectedResult {
+        final Class<? extends Throwable> expected;
+        final String partOfMessage;
+
+        public ExceptionThrown( final Class<? extends Throwable> expected, final String partOfMessage )
+        {
+            this.expected = expected;
+            this.partOfMessage = partOfMessage;
+        }
+    }
+
     @ParameterizedTest
     @MethodSource("parameters")
-    public void testGetReactorProjects(
+    void testGetReactorProjects(
             String parameterDescription,
-            List<String> parameterSelectedProjects,
-            List<String> parameterExcludedProjects,
+            List<String> parameterActiveRequiredProjects,
+            List<String> parameterActiveOptionalProjects,
+            List<String> parameterInactiveRequiredProjects,
+            List<String> parameterInactiveOptionalProjects,
             String parameterResumeFrom,
             String parameterMakeBehavior,
-            List<String> parameterExpectedResult,
+            ExpectedResult parameterExpectedResult,
             File parameterRequestedPom)
     {
         // Given
-        List<String> selectedProjects = parameterSelectedProjects.stream().map( p -> ":" + p ).collect( Collectors.toList() );
-        List<String> excludedProjects = parameterExcludedProjects.stream().map( p -> ":" + p ).collect( Collectors.toList() );
+        ProjectActivation projectActivation = new ProjectActivation();
+        parameterActiveRequiredProjects.forEach( projectActivation::activateRequiredProject );
+        parameterActiveOptionalProjects.forEach( projectActivation::activateOptionalProject );
+        parameterInactiveRequiredProjects.forEach( projectActivation::deactivateRequiredProject );
+        parameterInactiveOptionalProjects.forEach( projectActivation::deactivateOptionalProject );
 
-        when( mavenExecutionRequest.getSelectedProjects() ).thenReturn( selectedProjects );
-        when( mavenExecutionRequest.getExcludedProjects() ).thenReturn( excludedProjects );
+        when( mavenExecutionRequest.getProjectActivation() ).thenReturn( projectActivation );
         when( mavenExecutionRequest.getMakeBehavior() ).thenReturn( parameterMakeBehavior );
         when( mavenExecutionRequest.getPom() ).thenReturn( parameterRequestedPom );
         if ( StringUtils.isNotEmpty( parameterResumeFrom ) )
@@ -212,11 +280,27 @@ public class DefaultGraphBuilderTest
         Result<ProjectDependencyGraph> result = graphBuilder.build( session );
 
         // Then
-        List<MavenProject> actualReactorProjects = result.get().getSortedProjects();
-        List<MavenProject> expectedReactorProjects = parameterExpectedResult.stream()
-                .map( artifactIdProjectMap::get )
-                .collect( Collectors.toList());
-        assertEquals( expectedReactorProjects, actualReactorProjects, parameterDescription );
+        if ( parameterExpectedResult instanceof SelectedProjectsResult )
+        {
+            assertThat( result.hasErrors() ).isFalse();
+            List<String> expectedProjectNames = ((SelectedProjectsResult) parameterExpectedResult).projectNames;
+            List<MavenProject> actualReactorProjects = result.get().getSortedProjects();
+            List<MavenProject> expectedReactorProjects = expectedProjectNames.stream()
+                    .map( artifactIdProjectMap::get )
+                    .collect( toList() );
+            assertEquals( expectedReactorProjects, actualReactorProjects, parameterDescription );
+        }
+        else
+        {
+            assertThat( result.hasErrors() ).isTrue();
+            Class<? extends Throwable> expectedException = ((ExceptionThrown) parameterExpectedResult).expected;
+            String partOfMessage = ((ExceptionThrown) parameterExpectedResult).partOfMessage;
+
+            assertThat( result.getProblems() ).hasSize( 1 );
+            result.getProblems().forEach( p ->
+                assertThat( p.getException() ).isInstanceOf( expectedException ).hasMessageContaining( partOfMessage )
+            );
+        }
     }
 
     @BeforeEach
@@ -268,7 +352,7 @@ public class DefaultGraphBuilderTest
     private MavenProject getMavenProject( String artifactId )
     {
         MavenProject mavenProject = new MavenProject();
-        mavenProject.setGroupId( "unittest" );
+        mavenProject.setGroupId( GROUP_ID );
         mavenProject.setArtifactId( artifactId );
         mavenProject.setVersion( "1.0" );
         mavenProject.setPomFile( new File ( artifactId, "pom.xml" ) );
@@ -293,14 +377,16 @@ public class DefaultGraphBuilderTest
                     when( result.getProject() ).thenReturn( project );
                     return result;
                 } )
-                .collect( Collectors.toList() );
+                .collect( toList() );
     }
 
     static class ScenarioBuilder
     {
         private String description;
-        private List<String> selectedProjects = emptyList();
-        private List<String> excludedProjects = emptyList();
+        private List<String> activeRequiredProjects = emptyList();
+        private List<String> activeOptionalProjects = emptyList();
+        private List<String> inactiveRequiredProjects = emptyList();
+        private List<String> inactiveOptionalProjects = emptyList();
         private String resumeFrom = "";
         private String makeBehavior = "";
         private File requestedPom = new File( PARENT_MODULE, "pom.xml" );
@@ -314,15 +400,27 @@ public class DefaultGraphBuilderTest
             return scenarioBuilder;
         }
 
-        public ScenarioBuilder selectedProjects( String... selectedProjects )
+        public ScenarioBuilder activeRequiredProjects( String... activeRequiredProjects )
+        {
+            this.activeRequiredProjects = prependWithColonIfNeeded( activeRequiredProjects );
+            return this;
+        }
+
+        public ScenarioBuilder activeOptionalProjects( String... activeOptionalProjects )
         {
-            this.selectedProjects = asList( selectedProjects );
+            this.activeOptionalProjects = prependWithColonIfNeeded( activeOptionalProjects );
             return this;
         }
 
-        public ScenarioBuilder excludedProjects( String... excludedProjects )
+        public ScenarioBuilder inactiveRequiredProjects( String... inactiveRequiredProjects )
         {
-            this.excludedProjects = asList( excludedProjects );
+            this.inactiveRequiredProjects = prependWithColonIfNeeded( inactiveRequiredProjects );
+            return this;
+        }
+
+        public ScenarioBuilder inactiveOptionalProjects( String... inactiveOptionalProjects )
+        {
+            this.inactiveOptionalProjects = prependWithColonIfNeeded( inactiveOptionalProjects );
             return this;
         }
 
@@ -346,9 +444,32 @@ public class DefaultGraphBuilderTest
 
         public Arguments expectResult( String... expectedReactorProjects )
         {
-            return Arguments.arguments(
-                    description, selectedProjects, excludedProjects, resumeFrom, makeBehavior, asList( expectedReactorProjects ), requestedPom
-            );
+            ExpectedResult expectedResult = new SelectedProjectsResult( asList( expectedReactorProjects ) );
+            return createTestArguments( expectedResult );
+        }
+
+        public Arguments expectResult( Class<? extends Exception> expected, final String partOfMessage )
+        {
+            ExpectedResult expectedResult = new ExceptionThrown( expected, partOfMessage );
+            return createTestArguments( expectedResult );
+        }
+
+        private Arguments createTestArguments( ExpectedResult expectedResult )
+        {
+            return Arguments.arguments( description, activeRequiredProjects, activeOptionalProjects,
+                    inactiveRequiredProjects, inactiveOptionalProjects, resumeFrom, makeBehavior, expectedResult,
+                    requestedPom );
+        }
+
+        private List<String> prependWithColonIfNeeded( String[] selectors )
+        {
+            return Arrays.stream( selectors )
+                    .map( this::prependWithColonIfNeeded )
+                    .collect( toList() );
+        }
+
+        private String prependWithColonIfNeeded( String selector ) {
+            return selector.indexOf( ':' ) == -1 ? ":" + selector : selector;
         }
     }
 }
\ No newline at end of file
diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
index c217c9e..c3dc3b7 100644
--- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
+++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
@@ -55,6 +55,7 @@ import org.apache.maven.execution.MavenExecutionRequestPopulationException;
 import org.apache.maven.execution.MavenExecutionRequestPopulator;
 import org.apache.maven.execution.MavenExecutionResult;
 import org.apache.maven.execution.ProfileActivation;
+import org.apache.maven.execution.ProjectActivation;
 import org.apache.maven.execution.scope.internal.MojoExecutionScopeModule;
 import org.apache.maven.extension.internal.CoreExports;
 import org.apache.maven.extension.internal.CoreExtensionEntry;
@@ -113,7 +114,6 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
 import java.util.Set;
-import java.util.StringTokenizer;
 import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -1382,10 +1382,7 @@ public class MavenCli
         request.setCacheNotFound( true );
         request.setCacheTransferError( false );
 
-        final ProjectActivation projectActivation = determineProjectActivation( commandLine );
-        request.setSelectedProjects( projectActivation.activeProjects );
-        request.setExcludedProjects( projectActivation.inactiveProjects );
-
+        performProjectActivation( commandLine, request.getProjectActivation() );
         performProfileActivation( commandLine, request.getProfileActivation() );
 
         final String localRepositoryPath = determineLocalRepositoryPath( request );
@@ -1472,48 +1469,44 @@ public class MavenCli
     }
 
     // Visible for testing
-    static ProjectActivation determineProjectActivation ( final CommandLine commandLine )
+    static void performProjectActivation( final CommandLine commandLine, final ProjectActivation projectActivation )
     {
-        final ProjectActivation projectActivation = new ProjectActivation();
-
         if ( commandLine.hasOption( CLIManager.PROJECT_LIST ) )
         {
-            String[] projectOptionValues = commandLine.getOptionValues( CLIManager.PROJECT_LIST );
+            final String[] optionValues = commandLine.getOptionValues( CLIManager.PROJECT_LIST );
 
-            if ( projectOptionValues != null )
+            if ( optionValues == null || optionValues.length == 0 )
             {
-                for ( String projectOptionValue : projectOptionValues )
-                {
-                    StringTokenizer projectTokens = new StringTokenizer( projectOptionValue, "," );
+                return;
+            }
 
-                    while ( projectTokens.hasMoreTokens() )
+            for ( final String optionValue : optionValues )
+            {
+                for ( String token : optionValue.split( "," ) )
+                {
+                    String selector = token.trim();
+                    boolean active = true;
+                    if ( selector.charAt( 0 ) == '-' || selector.charAt( 0 ) == '!' )
                     {
-                        String projectAction = projectTokens.nextToken().trim();
-
-                        if ( projectAction.startsWith( "-" ) || projectAction.startsWith( "!" ) )
-                        {
-                            projectActivation.deactivate( projectAction.substring( 1 ) );
-                        }
-                        else if ( projectAction.startsWith( "+" ) )
-                        {
-                            projectActivation.activate( projectAction.substring( 1 ) );
-                        }
-                        else
-                        {
-                            projectActivation.activate( projectAction );
-                        }
+                        active = false;
+                        selector = selector.substring( 1 );
+                    }
+                    else if ( token.charAt( 0 ) == '+' )
+                    {
+                        selector = selector.substring( 1 );
                     }
+
+                    boolean optional = selector.charAt( 0 ) == '?';
+                    selector = selector.substring( optional ? 1 : 0 );
+
+                    projectActivation.addProjectActivation( selector, active, optional );
                 }
             }
-
         }
-
-        return projectActivation;
     }
 
     // Visible for testing
-    static void performProfileActivation( final CommandLine commandLine,
-                                          final ProfileActivation profileActivation )
+    static void performProfileActivation( final CommandLine commandLine, final ProfileActivation profileActivation )
     {
         if ( commandLine.hasOption( CLIManager.ACTIVATE_PROFILES ) )
         {
@@ -1800,29 +1793,4 @@ public class MavenCli
     {
         return container.lookup( ModelProcessor.class );
     }
-
-    // Visible for testing
-    static class ProjectActivation
-    {
-        List<String> activeProjects;
-        List<String> inactiveProjects;
-
-        public void deactivate( final String project )
-        {
-            if ( inactiveProjects == null )
-            {
-                inactiveProjects = new ArrayList<>();
-            }
-            inactiveProjects.add( project );
-        }
-
-        public void activate( final String project )
-        {
-            if ( activeProjects == null )
-            {
-                activeProjects = new ArrayList<>();
-            }
-            activeProjects.add( project );
-        }
-    }
 }
diff --git a/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java b/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java
index de7485c..f9dbf08 100644
--- a/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java
+++ b/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java
@@ -21,19 +21,18 @@ package org.apache.maven.cli;
 
 import static java.util.Arrays.asList;
 import static org.apache.maven.cli.MavenCli.performProfileActivation;
-import static org.apache.maven.cli.MavenCli.determineProjectActivation;
+import static org.apache.maven.cli.MavenCli.performProjectActivation;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
-import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
@@ -52,6 +51,7 @@ import org.apache.maven.Maven;
 import org.apache.maven.eventspy.internal.EventSpyDispatcher;
 import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.execution.ProfileActivation;
+import org.apache.maven.execution.ProjectActivation;
 import org.apache.maven.project.MavenProject;
 import org.apache.maven.shared.utils.logging.MessageUtils;
 import org.apache.maven.toolchain.building.ToolchainsBuildingRequest;
@@ -119,27 +119,27 @@ public class MavenCliTest
     @Test
     public void testDetermineProjectActivation() throws ParseException
     {
-        MavenCli.ProjectActivation result;
-        Options options = new Options();
+        final Parser parser = new GnuParser();
+
+        final Options options = new Options();
         options.addOption( Option.builder( CLIManager.PROJECT_LIST ).hasArg().build() );
 
-        result = determineProjectActivation( new GnuParser().parse( options, new String[0] ) );
-        assertThat( result.activeProjects, is( nullValue() ) );
-        assertThat( result.inactiveProjects, is( nullValue() ) );
+        ProjectActivation activation;
 
-        result = determineProjectActivation( new GnuParser().parse( options, new String[]{ "-pl", "test1,+test2" } ) );
-        assertThat( result.activeProjects.size(), is( 2 ) );
-        assertThat( result.activeProjects, contains( "test1", "test2" ) );
+        activation = new ProjectActivation();
+        performProjectActivation( parser.parse( options, new String[]{ "-pl", "test1,+test2,?test3,+?test4" } ), activation );
+        assertThat( activation.getRequiredActiveProjectSelectors(), containsInAnyOrder( "test1", "test2" ) );
+        assertThat( activation.getOptionalActiveProjectSelectors(), containsInAnyOrder( "test3", "test4" ) );
 
-        result = determineProjectActivation( new GnuParser().parse( options, new String[]{ "-pl", "!test1,-test2" } ) );
-        assertThat( result.inactiveProjects.size(), is( 2 ) );
-        assertThat( result.inactiveProjects, contains( "test1", "test2" ) );
+        activation = new ProjectActivation();
+        performProjectActivation( parser.parse( options, new String[]{ "-pl", "!test1,-test2,-?test3,!?test4" } ), activation );
+        assertThat( activation.getRequiredInactiveProjectSelectors(), containsInAnyOrder( "test1", "test2" ) );
+        assertThat( activation.getOptionalInactiveProjectSelectors(), containsInAnyOrder( "test3", "test4" ) );
 
-        result = determineProjectActivation( new GnuParser().parse( options, new String[]{ "-pl" ,"-test1,+test2" } ) );
-        assertThat( result.activeProjects.size(), is( 1 ) );
-        assertThat( result.activeProjects, contains( "test2" ) );
-        assertThat( result.inactiveProjects.size(), is( 1 ) );
-        assertThat( result.inactiveProjects, contains( "test1" ) );
+        activation = new ProjectActivation();
+        performProjectActivation( parser.parse( options, new String[]{ "-pl", "-test1,+test2" } ), activation );
+        assertThat( activation.getRequiredActiveProjectSelectors(), containsInAnyOrder( "test2" ) );
+        assertThat( activation.getRequiredInactiveProjectSelectors(), containsInAnyOrder( "test1" ) );
     }
 
     @Test