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 13:26:26 UTC

[maven] branch mng-6511 created (now 99ee0ac)

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

mthmulders pushed a change to branch mng-6511
in repository https://gitbox.apache.org/repos/asf/maven.git.


      at 99ee0ac  [MNG-6511] Optional project selection

This branch includes the following new commits:

     new 99ee0ac  [MNG-6511] Optional project selection

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[maven] 01/01: [MNG-6511] Optional project selection

Posted by mt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

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

    [MNG-6511] Optional project selection
---
 .../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