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

cstamas 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 7ad2578e77 [MNG-8386] Pull out executor (#1932)
7ad2578e77 is described below

commit 7ad2578e77da3639ac8f295aa40377272fa44c4d
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Fri Nov 22 15:09:32 2024 +0100

    [MNG-8386] Pull out executor (#1932)
    
    Yet another CLIng cleanup.
    
    Changes:
    * pull out Executor, it does not belong to CLIng (new maven-executor module 
created with no deps)
    * resident and maven invoker fixes (proper handling of resources now), no 
more SO/OOMs
    * enabled UTs in maven-cli (that revealed the issues)
    * small bug fixes discovered in cli, improved executor to reveal maven 
version
    
    ---
    
    https://issues.apache.org/jira/browse/MNG-8386
---
 .../org/apache/maven/api/cli/ExecutorRequest.java  |  82 ------
 .../org/apache/maven/api/cli/InvokerRequest.java   |  45 +++-
 .../main/java/org/apache/maven/api/cli/Parser.java |  12 -
 impl/maven-cli/pom.xml                             |  19 ++
 .../maven/cling/invoker/BaseExecutorRequest.java   |  77 ------
 .../maven/cling/invoker/BaseInvokerRequest.java    |  39 ++-
 .../org/apache/maven/cling/invoker/BaseParser.java |  20 --
 .../apache/maven/cling/invoker/LookupContext.java  |  13 +-
 .../apache/maven/cling/invoker/LookupInvoker.java  |  50 +---
 .../java/org/apache/maven/cling/invoker/Utils.java |   9 -
 .../maven/cling/invoker/mvn/MavenContext.java      |   8 +-
 .../maven/cling/invoker/mvn/MavenInvoker.java      |  25 +-
 .../invoker/mvn/forked/ForkedMavenExecutor.java    |  73 -----
 .../invoker/mvn/resident/ResidentMavenContext.java |  15 +-
 .../transfer/AbstractMavenTransferListener.java    |   5 -
 .../transfer/ConsoleMavenTransferListener.java     |   6 -
 .../invoker/mvn/MavenExecutorTestSupport.java      |  64 -----
 .../cling/invoker/mvn/MavenInvokerTestSupport.java |  49 +++-
 .../maven/cling/invoker/mvn/MavenTestSupport.java  |  69 -----
 .../mvn/embedded/EmbeddedMavenExecutorTest.java    |  61 -----
 .../invoker/mvn/forked/ForkedMavenInvokerTest.java |   5 +-
 ...InvokerTest.java => LocalMavenInvokerTest.java} |   6 +-
 ...okerTest.java => ResidentMavenInvokerTest.java} |   6 +-
 impl/maven-executor/pom.xml                        | 103 +++++++
 .../java/org/apache/maven/api/cli/Executor.java    |  20 ++
 .../apache/maven/api/cli/ExecutorException.java    |   3 +-
 .../org/apache/maven/api/cli/ExecutorRequest.java  | 299 +++++++++++++++++++++
 .../executor}/embedded/EmbeddedMavenExecutor.java  |  26 +-
 .../cling/executor/forked/ForkedMavenExecutor.java | 128 +++++++++
 .../cling/executor/MavenExecutorTestSupport.java   | 187 +++++++++++++
 .../embedded/EmbeddedMavenExecutorTest.java}       |  26 +-
 .../executor/forked/ForkedMavenExecutorTest.java}  |  26 +-
 impl/pom.xml                                       |   1 +
 pom.xml                                            |   1 +
 src/graph/ReactorGraph.java                        |   2 +-
 35 files changed, 990 insertions(+), 590 deletions(-)

diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorRequest.java 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorRequest.java
deleted file mode 100644
index 811fe87686..0000000000
--- 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorRequest.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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.
- */
-package org.apache.maven.api.cli;
-
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Optional;
-
-import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.annotations.Immutable;
-import org.apache.maven.api.annotations.Nonnull;
-
-/**
- * Represents a request to execute Maven with command-line arguments.
- * This interface encapsulates all the necessary information needed to execute
- * Maven command with arguments. The arguments were not parsed, they are just 
passed over
- * to executed tool.
- *
- * @since 4.0.0
- */
-@Immutable
-@Experimental
-public interface ExecutorRequest {
-    /**
-     * The parser request this instance was created from.
-     */
-    @Nonnull
-    ParserRequest parserRequest();
-
-    /**
-     * Returns the current working directory for the Maven execution.
-     * This is typically the directory from which Maven was invoked.
-     *
-     * @return the current working directory path
-     */
-    @Nonnull
-    Path cwd();
-
-    /**
-     * Returns the Maven installation directory.
-     * This is usually set by the Maven launcher script using the "maven.home" 
system property.
-     *
-     * @return the Maven installation directory path
-     */
-    @Nonnull
-    Path installationDirectory();
-
-    /**
-     * Returns the user's home directory.
-     * This is typically obtained from the "user.home" system property.
-     *
-     * @return the user's home directory path
-     */
-    @Nonnull
-    Path userHomeDirectory();
-
-    /**
-     * Returns the list of extra JVM arguments to be passed to the forked 
process.
-     * These arguments allow for customization of the JVM environment in which 
tool will run.
-     * This property is used ONLY by executors and invokers that spawn a new 
JVM.
-     *
-     * @return an Optional containing the list of extra JVM arguments, or 
empty if not specified
-     */
-    @Nonnull
-    Optional<List<String>> jvmArguments();
-}
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java
index ed019663c6..36ad78d8c6 100644
--- 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java
+++ 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java
@@ -40,7 +40,50 @@ import org.apache.maven.api.services.MessageBuilderFactory;
  */
 @Immutable
 @Experimental
-public interface InvokerRequest extends ExecutorRequest {
+public interface InvokerRequest {
+    /**
+     * The parser request this instance was created from.
+     */
+    @Nonnull
+    ParserRequest parserRequest();
+
+    /**
+     * Returns the current working directory for the Maven execution.
+     * This is typically the directory from which Maven was invoked.
+     *
+     * @return the current working directory path
+     */
+    @Nonnull
+    Path cwd();
+
+    /**
+     * Returns the Maven installation directory.
+     * This is usually set by the Maven launcher script using the "maven.home" 
system property.
+     *
+     * @return the Maven installation directory path
+     */
+    @Nonnull
+    Path installationDirectory();
+
+    /**
+     * Returns the user's home directory.
+     * This is typically obtained from the "user.home" system property.
+     *
+     * @return the user's home directory path
+     */
+    @Nonnull
+    Path userHomeDirectory();
+
+    /**
+     * Returns the list of extra JVM arguments to be passed to the forked 
process.
+     * These arguments allow for customization of the JVM environment in which 
tool will run.
+     * This property is used ONLY by executors and invokers that spawn a new 
JVM.
+     *
+     * @return an Optional containing the list of extra JVM arguments, or 
empty if not specified
+     */
+    @Nonnull
+    Optional<List<String>> jvmArguments();
+
     /**
      * Shorthand for {@link Logger} to use.
      */
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Parser.java 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Parser.java
index 6b791cbaae..0db563a2d5 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Parser.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Parser.java
@@ -30,18 +30,6 @@ import org.apache.maven.api.annotations.Nonnull;
  */
 @Experimental
 public interface Parser {
-    /**
-     * Parses the given ParserRequest to create an {@link ExecutorRequest}.
-     * This method does not interpret tool arguments.
-     *
-     * @param parserRequest the request containing all necessary information 
for parsing
-     * @return the parsed executor request
-     * @throws ParserException if there's an error during parsing of the 
request
-     * @throws IOException if there's an I/O error during the parsing process
-     */
-    @Nonnull
-    ExecutorRequest parseExecution(@Nonnull ParserRequest parserRequest) 
throws ParserException, IOException;
-
     /**
      * Parses the given ParserRequest to create an {@link InvokerRequest}.
      * This method does interpret tool arguments.
diff --git a/impl/maven-cli/pom.xml b/impl/maven-cli/pom.xml
index a34a19eb8b..ab55f3da0d 100644
--- a/impl/maven-cli/pom.xml
+++ b/impl/maven-cli/pom.xml
@@ -95,6 +95,21 @@ under the License.
       <version>1.3.0</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-connector-basic</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-transport-file</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-transport-jdk</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -145,6 +160,10 @@ under the License.
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-plugin</artifactId>
         <configuration>
+          <properties>
+            <configurationParameters>junit.jupiter.testclass.order.default = 
org.junit.jupiter.api.ClassOrderer$OrderAnnotation</configurationParameters>
+          </properties>
+          
<promoteUserPropertiesToSystemProperties>false</promoteUserPropertiesToSystemProperties>
           <systemPropertyVariables>
             <maven.home>${maven.home}</maven.home>
           </systemPropertyVariables>
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseExecutorRequest.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseExecutorRequest.java
deleted file mode 100644
index 00dbef9177..0000000000
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseExecutorRequest.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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.
- */
-package org.apache.maven.cling.invoker;
-
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Optional;
-
-import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.annotations.Nullable;
-import org.apache.maven.api.cli.ExecutorRequest;
-import org.apache.maven.api.cli.ParserRequest;
-
-import static java.util.Objects.requireNonNull;
-
-public class BaseExecutorRequest implements ExecutorRequest {
-    private final ParserRequest parserRequest;
-    private final Path cwd;
-    private final Path installationDirectory;
-    private final Path userHomeDirectory;
-    private final List<String> jvmArguments;
-
-    @SuppressWarnings("ParameterNumber")
-    public BaseExecutorRequest(
-            @Nonnull ParserRequest parserRequest,
-            @Nonnull Path cwd,
-            @Nonnull Path installationDirectory,
-            @Nonnull Path userHomeDirectory,
-            @Nullable List<String> jvmArguments) {
-        this.parserRequest = requireNonNull(parserRequest);
-        this.cwd = requireNonNull(cwd);
-        this.installationDirectory = requireNonNull(installationDirectory);
-        this.userHomeDirectory = requireNonNull(userHomeDirectory);
-        this.jvmArguments = jvmArguments;
-    }
-
-    @Override
-    public ParserRequest parserRequest() {
-        return parserRequest;
-    }
-
-    @Override
-    public Path cwd() {
-        return cwd;
-    }
-
-    @Override
-    public Path installationDirectory() {
-        return installationDirectory;
-    }
-
-    @Override
-    public Path userHomeDirectory() {
-        return userHomeDirectory;
-    }
-
-    @Override
-    public Optional<List<String>> jvmArguments() {
-        return Optional.ofNullable(jvmArguments);
-    }
-}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
index 25dcf754a7..e7b099d74a 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
@@ -33,7 +33,12 @@ import org.apache.maven.api.cli.extensions.CoreExtension;
 
 import static java.util.Objects.requireNonNull;
 
-public abstract class BaseInvokerRequest extends BaseExecutorRequest 
implements InvokerRequest {
+public abstract class BaseInvokerRequest implements InvokerRequest {
+    private final ParserRequest parserRequest;
+    private final Path cwd;
+    private final Path installationDirectory;
+    private final Path userHomeDirectory;
+    private final List<String> jvmArguments;
     private final Map<String, String> userProperties;
     private final Map<String, String> systemProperties;
     private final Path topDirectory;
@@ -58,7 +63,12 @@ public abstract class BaseInvokerRequest extends 
BaseExecutorRequest implements
             @Nullable OutputStream err,
             @Nullable List<CoreExtension> coreExtensions,
             @Nullable List<String> jvmArguments) {
-        super(parserRequest, cwd, installationDirectory, userHomeDirectory, 
jvmArguments);
+        this.parserRequest = requireNonNull(parserRequest);
+        this.cwd = requireNonNull(cwd);
+        this.installationDirectory = requireNonNull(installationDirectory);
+        this.userHomeDirectory = requireNonNull(userHomeDirectory);
+        this.jvmArguments = jvmArguments;
+
         this.userProperties = requireNonNull(userProperties);
         this.systemProperties = requireNonNull(systemProperties);
         this.topDirectory = requireNonNull(topDirectory);
@@ -70,6 +80,31 @@ public abstract class BaseInvokerRequest extends 
BaseExecutorRequest implements
         this.err = err;
     }
 
+    @Override
+    public ParserRequest parserRequest() {
+        return parserRequest;
+    }
+
+    @Override
+    public Path cwd() {
+        return cwd;
+    }
+
+    @Override
+    public Path installationDirectory() {
+        return installationDirectory;
+    }
+
+    @Override
+    public Path userHomeDirectory() {
+        return userHomeDirectory;
+    }
+
+    @Override
+    public Optional<List<String>> jvmArguments() {
+        return Optional.ofNullable(jvmArguments);
+    }
+
     @Override
     public Map<String, String> userProperties() {
         return userProperties;
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
index 853fea00d8..1b72a0a6cd 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
@@ -37,7 +37,6 @@ import java.util.stream.Collectors;
 
 import org.apache.maven.api.Constants;
 import org.apache.maven.api.annotations.Nullable;
-import org.apache.maven.api.cli.ExecutorRequest;
 import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.cli.Parser;
@@ -94,25 +93,6 @@ public abstract class BaseParser implements Parser {
         }
     }
 
-    @Override
-    public ExecutorRequest parseExecution(ParserRequest parserRequest) throws 
ParserException, IOException {
-        requireNonNull(parserRequest);
-
-        LocalContext context = new LocalContext(parserRequest);
-
-        // the basics
-        context.cwd = requireNonNull(getCwd(context));
-        context.installationDirectory = 
requireNonNull(getInstallationDirectory(context));
-        context.userHomeDirectory = 
requireNonNull(getUserHomeDirectory(context));
-
-        return getExecutionRequest(context);
-    }
-
-    protected ExecutorRequest getExecutionRequest(LocalContext context) {
-        return new BaseExecutorRequest(
-                context.parserRequest, context.cwd, 
context.installationDirectory, context.userHomeDirectory, null);
-    }
-
     @Override
     public InvokerRequest parseInvocation(ParserRequest parserRequest) throws 
ParserException, IOException {
         requireNonNull(parserRequest);
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java
index 51a2ad96d9..d0cae34c5f 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java
@@ -57,7 +57,7 @@ public class LookupContext implements AutoCloseable {
 
         Map<String, String> user = new 
HashMap<>(invokerRequest.userProperties());
         user.put("session.topDirectory", 
invokerRequest.topDirectory().toString());
-        if (invokerRequest.rootDirectory().isEmpty()) {
+        if (invokerRequest.rootDirectory().isPresent()) {
             user.put(
                     "session.rootDirectory",
                     invokerRequest.rootDirectory().get().toString());
@@ -112,4 +112,15 @@ public class LookupContext implements AutoCloseable {
             throw exception;
         }
     }
+
+    protected void closeContainer() {
+        if (containerCapsule != null) {
+            try {
+                containerCapsule.close();
+            } finally {
+                lookup = null;
+                containerCapsule = null;
+            }
+        }
+    }
 }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
index 766414d165..b4c3b57484 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
@@ -64,10 +64,6 @@ import org.apache.maven.bridge.MavenRepositorySystem;
 import org.apache.maven.cling.invoker.spi.PropertyContributorsHolder;
 import org.apache.maven.cling.logging.Slf4jConfiguration;
 import org.apache.maven.cling.logging.Slf4jConfigurationFactory;
-import org.apache.maven.cling.transfer.ConsoleMavenTransferListener;
-import org.apache.maven.cling.transfer.QuietMavenTransferListener;
-import org.apache.maven.cling.transfer.SimplexTransferListener;
-import org.apache.maven.cling.transfer.Slf4jMavenTransferListener;
 import org.apache.maven.cling.utils.CLIReportingUtils;
 import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.internal.impl.SettingsUtilsV4;
@@ -76,7 +72,6 @@ import org.apache.maven.jline.MessageUtils;
 import org.apache.maven.logging.LoggingOutputStream;
 import org.apache.maven.logging.api.LogLevelRecorder;
 import org.apache.maven.slf4j.MavenSimpleLogger;
-import org.eclipse.aether.transfer.TransferListener;
 import org.jline.terminal.Terminal;
 import org.jline.terminal.TerminalBuilder;
 import org.jline.terminal.impl.AbstractPosixTerminal;
@@ -132,6 +127,7 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
         validate(context);
         prepare(context);
         configureLogging(context);
+        createTerminal(context);
         activateLogging(context);
         helpOrVersionAndMayExit(context);
         preCommands(context);
@@ -216,18 +212,9 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
         context.slf4jConfiguration.setRootLoggerLevel(context.loggerLevel);
         // else fall back to default log level specified in conf
         // see https://issues.apache.org/jira/browse/MNG-2570
-
-        // JLine is quite slow to start due to the native library unpacking 
and loading
-        // so boot it asynchronously
-        context.terminal = createTerminal(context);
-        context.closeables.add(MessageUtils::systemUninstall);
-        MessageUtils.registerShutdownHook(); // safety belt
-        if (context.coloredOutput != null) {
-            MessageUtils.setColorEnabled(context.coloredOutput);
-        }
     }
 
-    protected Terminal createTerminal(C context) {
+    protected void createTerminal(C context) {
         MessageUtils.systemInstall(
                 builder -> {
                     builder.streams(
@@ -243,7 +230,15 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
                     }
                 },
                 terminal -> doConfigureWithTerminal(context, terminal));
-        return MessageUtils.getTerminal();
+
+        context.terminal = MessageUtils.getTerminal();
+        // JLine is quite slow to start due to the native library unpacking 
and loading
+        // so boot it asynchronously
+        context.closeables.add(MessageUtils::systemUninstall);
+        MessageUtils.registerShutdownHook(); // safety belt
+        if (context.coloredOutput != null) {
+            MessageUtils.setColorEnabled(context.coloredOutput);
+        }
     }
 
     protected void doConfigureWithTerminal(C context, Terminal terminal) {
@@ -271,7 +266,7 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
         if (options.logFile().isPresent()) {
             Path logFile = context.cwdResolver.apply(options.logFile().get());
             try {
-                PrintWriter printWriter = new 
PrintWriter(Files.newBufferedWriter(logFile));
+                PrintWriter printWriter = new 
PrintWriter(Files.newBufferedWriter(logFile), true);
                 context.closeables.add(printWriter);
                 return printWriter::println;
             } catch (IOException e) {
@@ -376,7 +371,7 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
 
     protected void container(C context) throws Exception {
         context.containerCapsule = 
createContainerCapsuleFactory().createContainerCapsule(this, context);
-        context.closeables.add(context.containerCapsule);
+        context.closeables.add(context::closeContainer);
         context.lookup = context.containerCapsule.getLookup();
 
         // refresh logger in case container got customized by spy
@@ -743,24 +738,5 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
         return ciEnv != null && !"false".equals(ciEnv);
     }
 
-    protected TransferListener determineTransferListener(C context, boolean 
noTransferProgress) {
-        boolean quiet = context.invokerRequest.options().quiet().orElse(false);
-        boolean logFile = 
context.invokerRequest.options().logFile().isPresent();
-        boolean runningOnCI = isRunningOnCI(context);
-        boolean quietCI = runningOnCI
-                && 
!context.invokerRequest.options().forceInteractive().orElse(false);
-
-        if (quiet || noTransferProgress || quietCI) {
-            return new QuietMavenTransferListener();
-        } else if (context.interactive && !logFile) {
-            return new SimplexTransferListener(new 
ConsoleMavenTransferListener(
-                    context.invokerRequest.messageBuilderFactory(),
-                    context.terminal.writer(),
-                    context.invokerRequest.options().verbose().orElse(false)));
-        } else {
-            return new Slf4jMavenTransferListener();
-        }
-    }
-
     protected abstract int execute(C context) throws Exception;
 }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/Utils.java 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/Utils.java
index b278724a77..853cccc931 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/Utils.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/Utils.java
@@ -18,7 +18,6 @@
  */
 package org.apache.maven.cling.invoker;
 
-import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
@@ -47,14 +46,6 @@ import static java.util.Objects.requireNonNull;
 public final class Utils {
     private Utils() {}
 
-    @Nullable
-    public static File toFile(Path path) {
-        if (path != null) {
-            return path.toFile();
-        }
-        return null;
-    }
-
     @Nonnull
     public static String stripLeadingAndTrailingQuotes(String str) {
         requireNonNull(str, "str");
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
index 7ffbcab9e9..c533b601ac 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
@@ -32,6 +32,12 @@ public class MavenContext extends LookupContext {
 
     public BuildEventListener buildEventListener;
     public EventSpyDispatcher eventSpyDispatcher;
-
     public Maven maven;
+
+    @Override
+    protected void closeContainer() {
+        eventSpyDispatcher = null;
+        maven = null;
+        super.closeContainer();
+    }
 }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
index ffcf67c3c2..579f1e5683 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
@@ -50,6 +50,10 @@ import org.apache.maven.cling.event.ExecutionEventLogger;
 import org.apache.maven.cling.invoker.LookupInvoker;
 import org.apache.maven.cling.invoker.ProtoLookup;
 import org.apache.maven.cling.invoker.Utils;
+import org.apache.maven.cling.transfer.ConsoleMavenTransferListener;
+import org.apache.maven.cling.transfer.QuietMavenTransferListener;
+import org.apache.maven.cling.transfer.SimplexTransferListener;
+import org.apache.maven.cling.transfer.Slf4jMavenTransferListener;
 import org.apache.maven.cling.utils.CLIReportingUtils;
 import org.apache.maven.eventspy.internal.EventSpyDispatcher;
 import org.apache.maven.exception.DefaultExceptionHandler;
@@ -389,12 +393,27 @@ public abstract class MavenInvoker<C extends 
MavenContext> extends LookupInvoker
         if (context.eventSpyDispatcher != null) {
             listener = context.eventSpyDispatcher.chainListener(listener);
         }
-        listener = new LoggingExecutionListener(listener, 
determineBuildEventListener(context));
-        return listener;
+        return new LoggingExecutionListener(listener, 
determineBuildEventListener(context));
     }
 
     protected TransferListener determineTransferListener(C context, boolean 
noTransferProgress) {
-        TransferListener delegate = super.determineTransferListener(context, 
noTransferProgress);
+        boolean quiet = context.invokerRequest.options().quiet().orElse(false);
+        boolean logFile = 
context.invokerRequest.options().logFile().isPresent();
+        boolean runningOnCI = isRunningOnCI(context);
+        boolean quietCI = runningOnCI
+                && 
!context.invokerRequest.options().forceInteractive().orElse(false);
+
+        TransferListener delegate;
+        if (quiet || noTransferProgress || quietCI) {
+            delegate = new QuietMavenTransferListener();
+        } else if (context.interactive && !logFile) {
+            delegate = new SimplexTransferListener(new 
ConsoleMavenTransferListener(
+                    context.invokerRequest.messageBuilderFactory(),
+                    context.terminal.writer(),
+                    context.invokerRequest.options().verbose().orElse(false)));
+        } else {
+            delegate = new Slf4jMavenTransferListener();
+        }
         return new MavenTransferListener(delegate, 
determineBuildEventListener(context));
     }
 
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenExecutor.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenExecutor.java
deleted file mode 100644
index b8e25b84df..0000000000
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenExecutor.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.
- */
-package org.apache.maven.cling.invoker.mvn.forked;
-
-import java.io.IOException;
-import java.util.ArrayList;
-
-import org.apache.maven.api.cli.Executor;
-import org.apache.maven.api.cli.ExecutorException;
-import org.apache.maven.api.cli.ExecutorRequest;
-import org.apache.maven.internal.impl.model.profile.Os;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Forked executor implementation, that spawns a subprocess with Maven from 
the installation directory.
- */
-public class ForkedMavenExecutor implements Executor {
-    @Override
-    public int execute(ExecutorRequest executorRequest) throws 
ExecutorException {
-        requireNonNull(executorRequest);
-        validate(executorRequest);
-
-        ArrayList<String> cmdAndArguments = new ArrayList<>();
-        cmdAndArguments.add(executorRequest
-                .installationDirectory()
-                .resolve("bin")
-                .resolve(
-                        Os.IS_WINDOWS
-                                ? executorRequest.parserRequest().command() + 
".cmd"
-                                : executorRequest.parserRequest().command())
-                .toString());
-
-        cmdAndArguments.addAll(executorRequest.parserRequest().args());
-
-        try {
-            ProcessBuilder pb = new ProcessBuilder()
-                    .directory(executorRequest.cwd().toFile())
-                    .command(cmdAndArguments);
-
-            if (executorRequest.jvmArguments().isPresent()) {
-                pb.environment()
-                        .put(
-                                "MAVEN_OPTS",
-                                String.join(" ", 
executorRequest.jvmArguments().get()));
-            }
-
-            return pb.start().waitFor();
-        } catch (IOException e) {
-            throw new ExecutorException("IO problem while executing command: " 
+ cmdAndArguments, e);
-        } catch (InterruptedException e) {
-            throw new ExecutorException("Interrupted while executing command: 
" + cmdAndArguments, e);
-        }
-    }
-
-    protected void validate(ExecutorRequest executorRequest) throws 
ExecutorException {}
-}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenContext.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenContext.java
index 7d96785d4f..fb1e2ed7ea 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenContext.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenContext.java
@@ -29,12 +29,12 @@ public class ResidentMavenContext extends MavenContext {
     }
 
     @Override
-    public void close() throws InvokerException {
-        // we are resident, we do not shut down here
+    protected void closeContainer() {
+        // we are resident; we do not shut down here
     }
 
     public void shutDown() throws InvokerException {
-        super.close();
+        super.closeContainer();
     }
 
     public ResidentMavenContext copy(InvokerRequest invokerRequest) {
@@ -43,16 +43,9 @@ public class ResidentMavenContext extends MavenContext {
         }
         ResidentMavenContext shadow = new ResidentMavenContext(invokerRequest);
 
-        shadow.logger = logger;
-        shadow.loggerFactory = loggerFactory;
-        shadow.loggerLevel = loggerLevel;
+        // we carry over only "resident" things
         shadow.containerCapsule = containerCapsule;
         shadow.lookup = lookup;
-
-        shadow.interactive = interactive;
-        shadow.localRepositoryPath = localRepositoryPath;
-        shadow.effectiveSettings = effectiveSettings;
-
         shadow.eventSpyDispatcher = eventSpyDispatcher;
         shadow.maven = maven;
 
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/transfer/AbstractMavenTransferListener.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/transfer/AbstractMavenTransferListener.java
index 6511072093..b07abab37e 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/transfer/AbstractMavenTransferListener.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/transfer/AbstractMavenTransferListener.java
@@ -18,7 +18,6 @@
  */
 package org.apache.maven.cling.transfer;
 
-import java.io.PrintStream;
 import java.io.PrintWriter;
 
 import org.apache.maven.api.services.MessageBuilder;
@@ -37,10 +36,6 @@ public abstract class AbstractMavenTransferListener extends 
AbstractTransferList
     protected final MessageBuilderFactory messageBuilderFactory;
     protected final PrintWriter out;
 
-    protected AbstractMavenTransferListener(MessageBuilderFactory 
messageBuilderFactory, PrintStream out) {
-        this(messageBuilderFactory, new PrintWriter(out));
-    }
-
     protected AbstractMavenTransferListener(MessageBuilderFactory 
messageBuilderFactory, PrintWriter out) {
         this.messageBuilderFactory = messageBuilderFactory;
         this.out = out;
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/transfer/ConsoleMavenTransferListener.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/transfer/ConsoleMavenTransferListener.java
index 46a17908ec..3b4078c5da 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/transfer/ConsoleMavenTransferListener.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/transfer/ConsoleMavenTransferListener.java
@@ -18,7 +18,6 @@
  */
 package org.apache.maven.cling.transfer;
 
-import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -43,11 +42,6 @@ public class ConsoleMavenTransferListener extends 
AbstractMavenTransferListener
     private final boolean printResourceNames;
     private int lastLength;
 
-    public ConsoleMavenTransferListener(
-            MessageBuilderFactory messageBuilderFactory, PrintStream out, 
boolean printResourceNames) {
-        this(messageBuilderFactory, new PrintWriter(out), printResourceNames);
-    }
-
     public ConsoleMavenTransferListener(
             MessageBuilderFactory messageBuilderFactory, PrintWriter out, 
boolean printResourceNames) {
         super(messageBuilderFactory, out);
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenExecutorTestSupport.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenExecutorTestSupport.java
deleted file mode 100644
index c3c487dcc1..0000000000
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenExecutorTestSupport.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.
- */
-package org.apache.maven.cling.invoker.mvn;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.List;
-
-import org.apache.maven.api.cli.Executor;
-import org.apache.maven.api.cli.Parser;
-import org.apache.maven.api.cli.ParserRequest;
-import org.apache.maven.cling.invoker.ProtoLogger;
-import org.apache.maven.jline.JLineMessageBuilderFactory;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public abstract class MavenExecutorTestSupport {
-
-    protected void execute(Path cwd, Collection<String> goals) throws 
Exception {
-        Files.createDirectory(cwd.resolve(".mvn"));
-        Path pom = cwd.resolve("pom.xml").toAbsolutePath();
-        Files.writeString(pom, MavenTestSupport.POM_STRING);
-        Path appJava = 
cwd.resolve("src/main/java/org/apache/maven/samples/sample/App.java");
-        Files.createDirectories(appJava.getParent());
-        Files.writeString(appJava, MavenTestSupport.APP_JAVA_STRING);
-
-        Parser parser = createParser();
-        try (Executor invoker = createExecutor()) {
-            for (String goal : goals) {
-                Path logFile = cwd.resolve(goal + 
"-build.log").toAbsolutePath();
-                int exitCode = 
invoker.execute(parser.parseExecution(ParserRequest.mvn(
-                                List.of("-l", logFile.toString(), goal),
-                                new ProtoLogger(),
-                                new JLineMessageBuilderFactory())
-                        .cwd(cwd)
-                        .build()));
-                String log = Files.readString(logFile);
-                System.out.println(log);
-                assertEquals(0, exitCode, log);
-            }
-        }
-    }
-
-    protected abstract Executor createExecutor();
-
-    protected abstract Parser createParser();
-}
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
index d3dcf3fc9a..4f3f46d4a0 100644
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
+++ 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
@@ -34,6 +34,51 @@ import org.junit.jupiter.api.Assumptions;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public abstract class MavenInvokerTestSupport {
+    public static final String POM_STRING =
+            """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+                         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/maven-v4_0_0.xsd";>
+
+                    <modelVersion>4.0.0</modelVersion>
+
+                    <groupId>org.apache.maven.samples</groupId>
+                    <artifactId>sample</artifactId>
+                    <version>1.0.0</version>
+
+                    <dependencyManagement>
+                      <dependencies>
+                        <dependency>
+                          <groupId>org.junit</groupId>
+                          <artifactId>junit-bom</artifactId>
+                          <version>5.11.1</version>
+                          <type>pom</type>
+                          <scope>import</scope>
+                        </dependency>
+                      </dependencies>
+                    </dependencyManagement>
+
+                    <dependencies>
+                      <dependency>
+                        <groupId>org.junit.jupiter</groupId>
+                        <artifactId>junit-jupiter-api</artifactId>
+                        <scope>test</scope>
+                      </dependency>
+                    </dependencies>
+
+                </project>
+                """;
+
+    public static final String APP_JAVA_STRING =
+            """
+            package org.apache.maven.samples.sample;
+
+            public class App {
+                public static void main(String... args) {
+                    System.out.println("Hello World!");
+                }
+            }
+            """;
 
     protected void invoke(Path cwd, Collection<String> goals) throws Exception 
{
         // works only in recent Maven4
@@ -45,10 +90,10 @@ public abstract class MavenInvokerTestSupport {
 
         Files.createDirectory(cwd.resolve(".mvn"));
         Path pom = cwd.resolve("pom.xml").toAbsolutePath();
-        Files.writeString(pom, MavenTestSupport.POM_STRING);
+        Files.writeString(pom, POM_STRING);
         Path appJava = 
cwd.resolve("src/main/java/org/apache/maven/samples/sample/App.java");
         Files.createDirectories(appJava.getParent());
-        Files.writeString(appJava, MavenTestSupport.APP_JAVA_STRING);
+        Files.writeString(appJava, APP_JAVA_STRING);
 
         Parser parser = createParser();
         try (Invoker invoker = createInvoker()) {
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenTestSupport.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenTestSupport.java
deleted file mode 100644
index 9c1ebeff36..0000000000
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenTestSupport.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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.
- */
-package org.apache.maven.cling.invoker.mvn;
-
-public final class MavenTestSupport {
-    private MavenTestSupport() {}
-
-    public static final String POM_STRING =
-            """
-                <?xml version="1.0" encoding="UTF-8"?>
-                <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
-                         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/maven-v4_0_0.xsd";>
-
-                    <modelVersion>4.0.0</modelVersion>
-
-                    <groupId>org.apache.maven.samples</groupId>
-                    <artifactId>sample</artifactId>
-                    <version>1.0.0</version>
-
-                    <dependencyManagement>
-                      <dependencies>
-                        <dependency>
-                          <groupId>org.junit</groupId>
-                          <artifactId>junit-bom</artifactId>
-                          <version>5.11.1</version>
-                          <type>pom</type>
-                          <scope>import</scope>
-                        </dependency>
-                      </dependencies>
-                    </dependencyManagement>
-
-                    <dependencies>
-                      <dependency>
-                        <groupId>org.junit.jupiter</groupId>
-                        <artifactId>junit-jupiter-api</artifactId>
-                        <scope>test</scope>
-                      </dependency>
-                    </dependencies>
-
-                </project>
-                """;
-
-    public static final String APP_JAVA_STRING =
-            """
-            package org.apache.maven.samples.sample;
-
-            public class App {
-                public static void main(String... args) {
-                    System.out.println("Hello World!");
-                }
-            }
-            """;
-}
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutorTest.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutorTest.java
deleted file mode 100644
index 7199d43fdd..0000000000
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutorTest.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.
- */
-package org.apache.maven.cling.invoker.mvn.embedded;
-
-import java.nio.file.Path;
-import java.util.List;
-
-import org.apache.maven.api.cli.Executor;
-import org.apache.maven.api.cli.Parser;
-import org.apache.maven.cling.invoker.mvn.MavenExecutorTestSupport;
-import org.apache.maven.cling.invoker.mvn.MavenParser;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.CleanupMode;
-import org.junit.jupiter.api.io.TempDir;
-
-/**
- * Forked UT: it cannot use jimFS as it runs in child process.
- */
-@Disabled(
-        "The tests reuse properties from the JVM being launched, thus may lead 
to failures depending on which options are used")
-public class EmbeddedMavenExecutorTest extends MavenExecutorTestSupport {
-
-    @Override
-    protected Executor createExecutor() {
-        return new EmbeddedMavenExecutor();
-    }
-
-    @Override
-    protected Parser createParser() {
-        return new MavenParser();
-    }
-
-    @Test
-    void defaultFs(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) 
throws Exception {
-        System.setProperty("maven.home", 
"/home/cstamas/Tools/maven/apache-maven-4.0.0-beta-6-SNAPSHOT");
-        execute(tempDir, List.of("verify"));
-    }
-
-    @Test
-    void defaultFs3x(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) 
throws Exception {
-        System.setProperty("maven.home", 
"/home/cstamas/.sdkman/candidates/maven/3.9.9");
-        execute(tempDir, List.of("verify"));
-    }
-}
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvokerTest.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvokerTest.java
index 01ddfe3267..56c1cd2388 100644
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvokerTest.java
+++ 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvokerTest.java
@@ -25,7 +25,7 @@ import org.apache.maven.api.cli.Invoker;
 import org.apache.maven.api.cli.Parser;
 import org.apache.maven.cling.invoker.mvn.MavenInvokerTestSupport;
 import org.apache.maven.cling.invoker.mvn.MavenParser;
-import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Order;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.CleanupMode;
 import org.junit.jupiter.api.io.TempDir;
@@ -33,8 +33,7 @@ import org.junit.jupiter.api.io.TempDir;
 /**
  * Forked UT: it cannot use jimFS as it runs in child process.
  */
-@Disabled(
-        "The tests reuse properties from the JVM being launched, thus may lead 
to failures depending on which options are used")
+@Order(300)
 public class ForkedMavenInvokerTest extends MavenInvokerTestSupport {
 
     @Override
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/DefaultLocalMavenInvokerTest.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvokerTest.java
similarity index 91%
rename from 
impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/DefaultLocalMavenInvokerTest.java
rename to 
impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvokerTest.java
index d4389788fa..11b8a4d878 100644
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/DefaultLocalMavenInvokerTest.java
+++ 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvokerTest.java
@@ -31,6 +31,7 @@ import 
org.apache.maven.cling.invoker.mvn.MavenInvokerTestSupport;
 import org.apache.maven.cling.invoker.mvn.MavenParser;
 import org.codehaus.plexus.classworlds.ClassWorld;
 import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Order;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.CleanupMode;
 import org.junit.jupiter.api.io.TempDir;
@@ -38,9 +39,8 @@ import org.junit.jupiter.api.io.TempDir;
 /**
  * Local UT.
  */
-@Disabled(
-        "The tests reuse properties from the JVM being launched, thus may lead 
to failures depending on which options are used")
-public class DefaultLocalMavenInvokerTest extends MavenInvokerTestSupport {
+@Order(200)
+public class LocalMavenInvokerTest extends MavenInvokerTestSupport {
     @Override
     protected Invoker createInvoker() {
         return new LocalMavenInvoker(ProtoLookup.builder()
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/DefaultResidentMavenInvokerTest.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvokerTest.java
similarity index 91%
rename from 
impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/DefaultResidentMavenInvokerTest.java
rename to 
impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvokerTest.java
index de65db9503..004b234de6 100644
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/DefaultResidentMavenInvokerTest.java
+++ 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvokerTest.java
@@ -31,6 +31,7 @@ import 
org.apache.maven.cling.invoker.mvn.MavenInvokerTestSupport;
 import org.apache.maven.cling.invoker.mvn.MavenParser;
 import org.codehaus.plexus.classworlds.ClassWorld;
 import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Order;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.CleanupMode;
 import org.junit.jupiter.api.io.TempDir;
@@ -38,9 +39,8 @@ import org.junit.jupiter.api.io.TempDir;
 /**
  * Resident UT.
  */
-@Disabled(
-        "The tests reuse properties from the JVM being launched, thus may lead 
to failures depending on which options are used")
-public class DefaultResidentMavenInvokerTest extends MavenInvokerTestSupport {
+@Order(100)
+public class ResidentMavenInvokerTest extends MavenInvokerTestSupport {
 
     @Override
     protected Invoker createInvoker() {
diff --git a/impl/maven-executor/pom.xml b/impl/maven-executor/pom.xml
new file mode 100644
index 0000000000..da060ae30a
--- /dev/null
+++ b/impl/maven-executor/pom.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven</groupId>
+    <artifactId>maven</artifactId>
+    <version>4.0.0-beta-6-SNAPSHOT</version>
+
+    <relativePath>../../</relativePath>
+  </parent>
+
+  <artifactId>maven-executor</artifactId>
+
+  <name>Maven 4 Executor</name>
+  <description>Maven 4 Executor, for executing Maven 3/4.</description>
+
+  <properties>
+    <maven3version>3.9.9</maven3version>
+    <maven4version>4.0.0-beta-5</maven4version>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-api-meta</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>prepare-maven-distros</id>
+            <goals>
+              <goal>unpack</goal>
+            </goals>
+            <phase>generate-test-resources</phase>
+            <configuration>
+              <artifactItems>
+                <artifactItem>
+                  <groupId>org.apache.maven</groupId>
+                  <artifactId>apache-maven</artifactId>
+                  <version>${maven3version}</version>
+                  <classifier>bin</classifier>
+                  <type>zip</type>
+                </artifactItem>
+                <artifactItem>
+                  <groupId>org.apache.maven</groupId>
+                  <artifactId>apache-maven</artifactId>
+                  <version>${maven4version}</version>
+                  <classifier>bin</classifier>
+                  <type>zip</type>
+                </artifactItem>
+              </artifactItems>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          
<promoteUserPropertiesToSystemProperties>false</promoteUserPropertiesToSystemProperties>
+          <systemPropertyVariables>
+            <maven3version>${maven3version}</maven3version>
+            <maven4version>${maven4version}</maven4version>
+            
<maven3home>${project.build.directory}/dependency/apache-maven-${maven3version}</maven3home>
+            
<maven4home>${project.build.directory}/dependency/apache-maven-${maven4version}</maven4home>
+          </systemPropertyVariables>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Executor.java 
b/impl/maven-executor/src/main/java/org/apache/maven/api/cli/Executor.java
similarity index 70%
rename from 
api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Executor.java
rename to 
impl/maven-executor/src/main/java/org/apache/maven/api/cli/Executor.java
index 60cd38921a..fec88707df 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Executor.java
+++ b/impl/maven-executor/src/main/java/org/apache/maven/api/cli/Executor.java
@@ -30,6 +30,14 @@ import org.apache.maven.api.annotations.Nonnull;
  */
 @Experimental
 public interface Executor extends AutoCloseable {
+    // Logic borrowed from Commons-Lang3
+    boolean IS_WINDOWS = System.getProperty("os.name", 
"unknown").startsWith("Windows");
+
+    /**
+     * Maven version string returned when the actual version of Maven cannot 
be determinet.
+     */
+    String UNKNOWN_VERSION = "unknown";
+
     /**
      * Invokes the tool application using the provided {@link ExecutorRequest}.
      * This method is responsible for executing the command or build
@@ -41,6 +49,18 @@ public interface Executor extends AutoCloseable {
      */
     int execute(@Nonnull ExecutorRequest executorRequest) throws 
ExecutorException;
 
+    /**
+     * Returns the Maven version that provided {@link ExecutorRequest} point 
at (would use). Please not, that this
+     * operation, depending on underlying implementation may be costly. If 
caller use this method often, it is
+     * caller responsibility to properly cache returned values (key can be 
{@link ExecutorRequest#installationDirectory()}.
+     *
+     * @param executorRequest the request containing all necessary information 
for the execution
+     * @return a string representing the Maven version or {@link 
#UNKNOWN_VERSION}
+     * @throws ExecutorException if an error occurs during the execution 
process
+     */
+    @Nonnull
+    String mavenVersion(@Nonnull ExecutorRequest executorRequest) throws 
ExecutorException;
+
     /**
      * Closes and disposes of this {@link Executor} instance, releasing any 
resources it may hold.
      * This method is called automatically when using try-with-resources 
statements.
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorException.java
 
b/impl/maven-executor/src/main/java/org/apache/maven/api/cli/ExecutorException.java
similarity index 94%
rename from 
api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorException.java
rename to 
impl/maven-executor/src/main/java/org/apache/maven/api/cli/ExecutorException.java
index 8ce5bf157d..d1c4a918c1 100644
--- 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorException.java
+++ 
b/impl/maven-executor/src/main/java/org/apache/maven/api/cli/ExecutorException.java
@@ -20,7 +20,6 @@ package org.apache.maven.api.cli;
 
 import org.apache.maven.api.annotations.Experimental;
 import org.apache.maven.api.annotations.Nullable;
-import org.apache.maven.api.services.MavenException;
 
 /**
  * Represents an exception that occurs during the execution of a Maven build 
or command.
@@ -30,7 +29,7 @@ import org.apache.maven.api.services.MavenException;
  * @since 4.0.0
  */
 @Experimental
-public class ExecutorException extends MavenException {
+public class ExecutorException extends RuntimeException {
     /**
      * Constructs a new {@code InvokerException} with the specified detail 
message.
      *
diff --git 
a/impl/maven-executor/src/main/java/org/apache/maven/api/cli/ExecutorRequest.java
 
b/impl/maven-executor/src/main/java/org/apache/maven/api/cli/ExecutorRequest.java
new file mode 100644
index 0000000000..1f40efa4b6
--- /dev/null
+++ 
b/impl/maven-executor/src/main/java/org/apache/maven/api/cli/ExecutorRequest.java
@@ -0,0 +1,299 @@
+/*
+ * 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.
+ */
+package org.apache.maven.api.cli;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.annotations.Immutable;
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Represents a request to execute Maven with command-line arguments.
+ * This interface encapsulates all the necessary information needed to execute
+ * Maven command with arguments. The arguments were not parsed, they are just 
passed over
+ * to executed tool.
+ *
+ * @since 4.0.0
+ */
+@Immutable
+@Experimental
+public interface ExecutorRequest {
+    /**
+     * The command to execute, ie "mvn".
+     */
+    @Nonnull
+    String command();
+
+    /**
+     * The immutable list of arguments to pass to the command.
+     */
+    @Nonnull
+    List<String> arguments();
+
+    /**
+     * Returns the current working directory for the Maven execution.
+     * This is typically the directory from which Maven was invoked.
+     *
+     * @return the current working directory path
+     */
+    @Nonnull
+    Path cwd();
+
+    /**
+     * Returns the Maven installation directory.
+     * This is usually set by the Maven launcher script using the "maven.home" 
system property.
+     *
+     * @return the Maven installation directory path
+     */
+    @Nonnull
+    Path installationDirectory();
+
+    /**
+     * Returns the user's home directory.
+     * This is typically obtained from the "user.home" system property.
+     *
+     * @return the user's home directory path
+     */
+    @Nonnull
+    Path userHomeDirectory();
+
+    /**
+     * Returns the list of extra JVM arguments to be passed to the forked 
process.
+     * These arguments allow for customization of the JVM environment in which 
tool will run.
+     * This property is used ONLY by executors and invokers that spawn a new 
JVM.
+     *
+     * @return an Optional containing the list of extra JVM arguments, or 
empty if not specified
+     */
+    @Nonnull
+    Optional<List<String>> jvmArguments();
+
+    /**
+     * Returns {@link Builder} for this instance.
+     */
+    @Nonnull
+    default Builder toBuilder() {
+        return new Builder(
+                command(),
+                arguments(),
+                cwd(),
+                installationDirectory(),
+                userHomeDirectory(),
+                jvmArguments().orElse(null));
+    }
+
+    /**
+     * Returns new empty builder.
+     */
+    @Nonnull
+    static Builder empyBuilder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns new builder pre-set to run Maven. The discovery of maven home 
is attempted.
+     */
+    @Nonnull
+    static Builder mavenBuilder(@Nullable Path installationDirectory) {
+        return new Builder(
+                "mvn",
+                null,
+                getCanonicalPath(Paths.get(System.getProperty("user.dir"))),
+                installationDirectory != null ? 
getCanonicalPath(installationDirectory) : discoverMavenHome(),
+                getCanonicalPath(Paths.get(System.getProperty("user.home"))),
+                null);
+    }
+
+    class Builder {
+        private String command;
+        private List<String> arguments;
+        private Path cwd;
+        private Path installationDirectory;
+        private Path userHomeDirectory;
+        private List<String> jvmArguments;
+
+        private Builder() {}
+
+        private Builder(
+                String command,
+                List<String> arguments,
+                Path cwd,
+                Path installationDirectory,
+                Path userHomeDirectory,
+                List<String> jvmArguments) {
+            this.command = command;
+            this.arguments = arguments;
+            this.cwd = cwd;
+            this.installationDirectory = installationDirectory;
+            this.userHomeDirectory = userHomeDirectory;
+            this.jvmArguments = jvmArguments;
+        }
+
+        @Nonnull
+        public Builder command(String command) {
+            this.command = requireNonNull(command, "command");
+            return this;
+        }
+
+        @Nonnull
+        public Builder arguments(List<String> arguments) {
+            this.arguments = requireNonNull(arguments, "arguments");
+            return this;
+        }
+
+        @Nonnull
+        public Builder argument(String argument) {
+            if (arguments == null) {
+                arguments = new ArrayList<>();
+            }
+            this.arguments.add(requireNonNull(argument, "argument"));
+            return this;
+        }
+
+        @Nonnull
+        public Builder cwd(Path cwd) {
+            this.cwd = requireNonNull(cwd, "cwd");
+            return this;
+        }
+
+        @Nonnull
+        public Builder installationDirectory(Path installationDirectory) {
+            this.installationDirectory = requireNonNull(installationDirectory, 
"installationDirectory");
+            return this;
+        }
+
+        @Nonnull
+        public Builder userHomeDirectory(Path userHomeDirectory) {
+            this.userHomeDirectory = requireNonNull(userHomeDirectory, 
"userHomeDirectory");
+            return this;
+        }
+
+        @Nonnull
+        public Builder jvmArguments(List<String> jvmArguments) {
+            this.jvmArguments = jvmArguments;
+            return this;
+        }
+
+        @Nonnull
+        public Builder jvmArgument(String jvmArgument) {
+            if (jvmArguments == null) {
+                jvmArguments = new ArrayList<>();
+            }
+            this.jvmArguments.add(requireNonNull(jvmArgument, "jvmArgument"));
+            return this;
+        }
+
+        @Nonnull
+        public ExecutorRequest build() {
+            return new Impl(command, arguments, cwd, installationDirectory, 
userHomeDirectory, jvmArguments);
+        }
+
+        private static class Impl implements ExecutorRequest {
+            private final String command;
+            private final List<String> arguments;
+            private final Path cwd;
+            private final Path installationDirectory;
+            private final Path userHomeDirectory;
+            private final List<String> jvmArguments;
+
+            private Impl(
+                    String command,
+                    List<String> arguments,
+                    Path cwd,
+                    Path installationDirectory,
+                    Path userHomeDirectory,
+                    List<String> jvmArguments) {
+                this.command = requireNonNull(command);
+                this.arguments = arguments == null ? List.of() : 
List.copyOf(arguments);
+                this.cwd = requireNonNull(cwd);
+                this.installationDirectory = 
requireNonNull(installationDirectory);
+                this.userHomeDirectory = requireNonNull(userHomeDirectory);
+                this.jvmArguments = jvmArguments != null ? 
List.copyOf(jvmArguments) : null;
+            }
+
+            @Override
+            public String command() {
+                return command;
+            }
+
+            @Override
+            public List<String> arguments() {
+                return arguments;
+            }
+
+            @Override
+            public Path cwd() {
+                return cwd;
+            }
+
+            @Override
+            public Path installationDirectory() {
+                return installationDirectory;
+            }
+
+            @Override
+            public Path userHomeDirectory() {
+                return userHomeDirectory;
+            }
+
+            @Override
+            public Optional<List<String>> jvmArguments() {
+                return Optional.ofNullable(jvmArguments);
+            }
+
+            @Override
+            public String toString() {
+                return "ExecutionRequest{" + "command='"
+                        + command + '\'' + ", arguments="
+                        + arguments + ", cwd="
+                        + cwd + ", installationDirectory="
+                        + installationDirectory + ", userHomeDirectory="
+                        + userHomeDirectory + ", jvmArguments="
+                        + jvmArguments + '}';
+            }
+        }
+    }
+
+    @Nonnull
+    static Path discoverMavenHome() {
+        String mavenHome = System.getProperty("maven.home");
+        if (mavenHome == null) {
+            throw new ExecutorException("requires maven.home Java System 
Property set");
+        }
+        return getCanonicalPath(Paths.get(mavenHome));
+    }
+
+    @Nonnull
+    static Path getCanonicalPath(Path path) {
+        requireNonNull(path, "path");
+        try {
+            return path.toRealPath();
+        } catch (IOException e) {
+            return 
getCanonicalPath(path.getParent()).resolve(path.getFileName());
+        }
+    }
+}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutor.java
 
b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java
similarity index 93%
rename from 
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutor.java
rename to 
impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java
index ff34b8c489..350b0cc648 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutor.java
+++ 
b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.cling.invoker.mvn.embedded;
+package org.apache.maven.cling.executor.embedded;
 
 import java.io.Closeable;
 import java.io.IOException;
@@ -102,6 +102,13 @@ public class EmbeddedMavenExecutor implements Executor {
         }
     }
 
+    @Override
+    public String mavenVersion(ExecutorRequest executorRequest) throws 
ExecutorException {
+        requireNonNull(executorRequest);
+        validate(executorRequest);
+        return mayCreate(executorRequest).version;
+    }
+
     protected Context mayCreate(ExecutorRequest executorRequest) {
         Path installation = executorRequest.installationDirectory();
         if (!Files.isDirectory(installation)) {
@@ -148,7 +155,7 @@ public class EmbeddedMavenExecutor implements Executor {
                 Object classWorld = 
launcherClass.getMethod("getWorld").invoke(launcher);
                 Class<?> cliClass =
                         (Class<?>) 
launcherClass.getMethod("getMainClass").invoke(launcher);
-                String version = getMavenVersion(cliClass.getClassLoader());
+                String version = getMavenVersion(cliClass);
                 Function<ExecutorRequest, Integer> exec;
 
                 if (version.startsWith("3.")) {
@@ -160,10 +167,7 @@ public class EmbeddedMavenExecutor implements Executor {
                     exec = r -> {
                         try {
                             return (int) doMain.invoke(mavenCli, new Object[] {
-                                r.parserRequest().args().toArray(new 
String[0]),
-                                r.cwd().toString(),
-                                null,
-                                null
+                                r.arguments().toArray(new String[0]), 
r.cwd().toString(), null, null
                             });
                         } catch (Exception e) {
                             throw new ExecutorException("Failed to execute", 
e);
@@ -174,8 +178,7 @@ public class EmbeddedMavenExecutor implements Executor {
                     Method mainMethod = cliClass.getMethod("main", 
String[].class, classWorld.getClass());
                     exec = r -> {
                         try {
-                            return (int) mainMethod.invoke(
-                                    null, r.parserRequest().args().toArray(new 
String[0]), classWorld);
+                            return (int) mainMethod.invoke(null, 
r.arguments().toArray(new String[0]), classWorld);
                         } catch (Exception e) {
                             throw new ExecutorException("Failed to execute", 
e);
                         }
@@ -250,10 +253,9 @@ public class EmbeddedMavenExecutor implements Executor {
                 urls.toArray(new URL[0]), 
ClassLoader.getSystemClassLoader().getParent());
     }
 
-    public String getMavenVersion(ClassLoader classLoader) throws IOException {
+    protected String getMavenVersion(Class<?> clazz) throws IOException {
         Properties props = new Properties();
-        try (InputStream is =
-                
classLoader.getResourceAsStream("/META-INF/maven/org.apache.maven/maven-core/pom.properties"))
 {
+        try (InputStream is = 
clazz.getResourceAsStream("/META-INF/maven/org.apache.maven/maven-core/pom.properties"))
 {
             if (is != null) {
                 props.load(is);
             }
@@ -261,7 +263,7 @@ public class EmbeddedMavenExecutor implements Executor {
             if (version != null) {
                 return version;
             }
-            return "unknown";
+            return UNKNOWN_VERSION;
         }
     }
 }
diff --git 
a/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/forked/ForkedMavenExecutor.java
 
b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/forked/ForkedMavenExecutor.java
new file mode 100644
index 0000000000..47ae235cac
--- /dev/null
+++ 
b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/forked/ForkedMavenExecutor.java
@@ -0,0 +1,128 @@
+/*
+ * 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.
+ */
+package org.apache.maven.cling.executor.forked;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.apache.maven.api.cli.Executor;
+import org.apache.maven.api.cli.ExecutorException;
+import org.apache.maven.api.cli.ExecutorRequest;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Forked executor implementation, that spawns a subprocess with Maven from 
the installation directory. Very costly
+ * but provides the best isolation.
+ */
+public class ForkedMavenExecutor implements Executor {
+    @Override
+    public int execute(ExecutorRequest executorRequest) throws 
ExecutorException {
+        requireNonNull(executorRequest);
+        validate(executorRequest);
+
+        return doExecute(executorRequest, null);
+    }
+
+    @Override
+    public String mavenVersion(ExecutorRequest executorRequest) throws 
ExecutorException {
+        requireNonNull(executorRequest);
+        validate(executorRequest);
+        try {
+            Path cwd = 
Files.createTempDirectory("forked-executor-maven-version");
+            try {
+                ArrayList<String> stdout = new ArrayList<>();
+                int exitCode = doExecute(
+                        executorRequest.toBuilder()
+                                .cwd(cwd)
+                                .arguments(List.of("--version", "--color", 
"never"))
+                                .build(),
+                        p -> {
+                            String line;
+                            try (BufferedReader br = new BufferedReader(new 
InputStreamReader(p.getInputStream()))) {
+                                while ((line = br.readLine()) != null) {
+                                    stdout.add(line);
+                                }
+                            } catch (IOException e) {
+                                throw new UncheckedIOException(e);
+                            }
+                        });
+                if (exitCode == 0) {
+                    for (String line : stdout) {
+                        if (line.startsWith("Apache Maven ")) {
+                            return line.substring(13, line.indexOf("(") - 1);
+                        }
+                    }
+                    return UNKNOWN_VERSION;
+                } else {
+                    throw new ExecutorException(
+                            "Maven version query unexpected exitCode=" + 
exitCode + "\nLog: " + stdout);
+                }
+            } finally {
+                Files.deleteIfExists(cwd);
+            }
+        } catch (IOException e) {
+            throw new ExecutorException("Failed to determine maven version", 
e);
+        }
+    }
+
+    protected void validate(ExecutorRequest executorRequest) throws 
ExecutorException {}
+
+    protected int doExecute(ExecutorRequest executorRequest, Consumer<Process> 
processConsumer)
+            throws ExecutorException {
+        ArrayList<String> cmdAndArguments = new ArrayList<>();
+        cmdAndArguments.add(executorRequest
+                .installationDirectory()
+                .resolve("bin")
+                .resolve(IS_WINDOWS ? executorRequest.command() + ".cmd" : 
executorRequest.command())
+                .toString());
+
+        cmdAndArguments.addAll(executorRequest.arguments());
+
+        try {
+            ProcessBuilder pb = new ProcessBuilder()
+                    .directory(executorRequest.cwd().toFile())
+                    .command(cmdAndArguments);
+
+            if (executorRequest.jvmArguments().isPresent()) {
+                pb.environment()
+                        .put(
+                                "MAVEN_OPTS",
+                                String.join(" ", 
executorRequest.jvmArguments().get()));
+            }
+
+            Process process = pb.start();
+            if (processConsumer != null) {
+                processConsumer.accept(process);
+            }
+            return process.waitFor();
+        } catch (IOException e) {
+            throw new ExecutorException("IO problem while executing command: " 
+ cmdAndArguments, e);
+        } catch (InterruptedException e) {
+            throw new ExecutorException("Interrupted while executing command: 
" + cmdAndArguments, e);
+        }
+    }
+}
diff --git 
a/impl/maven-executor/src/test/java/org/apache/maven/cling/executor/MavenExecutorTestSupport.java
 
b/impl/maven-executor/src/test/java/org/apache/maven/cling/executor/MavenExecutorTestSupport.java
new file mode 100644
index 0000000000..bf1086ae88
--- /dev/null
+++ 
b/impl/maven-executor/src/test/java/org/apache/maven/cling/executor/MavenExecutorTestSupport.java
@@ -0,0 +1,187 @@
+/*
+ * 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.
+ */
+package org.apache.maven.cling.executor;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.maven.api.cli.Executor;
+import org.apache.maven.api.cli.ExecutorRequest;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.CleanupMode;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public abstract class MavenExecutorTestSupport {
+    @Test
+    void defaultFs(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) 
throws Exception {
+        layDownFiles(tempDir);
+        String logfile = "m4.log";
+        execute(
+                tempDir.resolve(logfile),
+                List.of(mvn4ExecutorRequestBuilder()
+                        .cwd(tempDir)
+                        .argument("verify")
+                        .argument("-l")
+                        .argument(logfile)
+                        .build()));
+    }
+
+    @Test
+    void version() throws Exception {
+        assertEquals(
+                System.getProperty("maven4version"),
+                mavenVersion(mvn4ExecutorRequestBuilder().build()));
+    }
+
+    @Disabled("JUnit on Windows fails to clean up as mvn3 seems does not close 
log file properly")
+    @Test
+    void defaultFs3x(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) 
throws Exception {
+        layDownFiles(tempDir);
+        String logfile = "m3.log";
+        execute(
+                tempDir.resolve(logfile),
+                List.of(mvn3ExecutorRequestBuilder()
+                        .cwd(tempDir)
+                        .argument("verify")
+                        .argument("-l")
+                        .argument(logfile)
+                        .build()));
+    }
+
+    @Test
+    void version3x() throws Exception {
+        assertEquals(
+                System.getProperty("maven3version"),
+                mavenVersion(mvn3ExecutorRequestBuilder().build()));
+    }
+
+    public static final String POM_STRING =
+            """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+                         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/maven-v4_0_0.xsd";>
+
+                    <modelVersion>4.0.0</modelVersion>
+
+                    <groupId>org.apache.maven.samples</groupId>
+                    <artifactId>sample</artifactId>
+                    <version>1.0.0</version>
+
+                    <dependencyManagement>
+                      <dependencies>
+                        <dependency>
+                          <groupId>org.junit</groupId>
+                          <artifactId>junit-bom</artifactId>
+                          <version>5.11.1</version>
+                          <type>pom</type>
+                          <scope>import</scope>
+                        </dependency>
+                      </dependencies>
+                    </dependencyManagement>
+
+                    <dependencies>
+                      <dependency>
+                        <groupId>org.junit.jupiter</groupId>
+                        <artifactId>junit-jupiter-api</artifactId>
+                        <scope>test</scope>
+                      </dependency>
+                    </dependencies>
+
+                </project>
+                """;
+
+    public static final String APP_JAVA_STRING =
+            """
+            package org.apache.maven.samples.sample;
+
+            public class App {
+                public static void main(String... args) {
+                    System.out.println("Hello World!");
+                }
+            }
+            """;
+
+    protected void execute(Path logFile, Collection<ExecutorRequest> requests) 
throws Exception {
+        try (Executor invoker = createExecutor()) {
+            for (ExecutorRequest request : requests) {
+                int exitCode = invoker.execute(request);
+                if (exitCode != 0) {
+                    throw new FailedExecution(request, exitCode, 
Files.readString(logFile));
+                }
+            }
+        }
+    }
+
+    protected String mavenVersion(ExecutorRequest request) throws Exception {
+        try (Executor invoker = createExecutor()) {
+            return invoker.mavenVersion(request);
+        }
+    }
+
+    protected ExecutorRequest.Builder mvn3ExecutorRequestBuilder() {
+        return 
ExecutorRequest.mavenBuilder(Paths.get(System.getProperty("maven3home")));
+    }
+
+    protected ExecutorRequest.Builder mvn4ExecutorRequestBuilder() {
+        return 
ExecutorRequest.mavenBuilder(Paths.get(System.getProperty("maven4home")));
+    }
+
+    protected void layDownFiles(Path cwd) throws IOException {
+        Files.createDirectory(cwd.resolve(".mvn"));
+        Path pom = cwd.resolve("pom.xml").toAbsolutePath();
+        Files.writeString(pom, POM_STRING);
+        Path appJava = 
cwd.resolve("src/main/java/org/apache/maven/samples/sample/App.java");
+        Files.createDirectories(appJava.getParent());
+        Files.writeString(appJava, APP_JAVA_STRING);
+    }
+
+    protected static class FailedExecution extends Exception {
+        private final ExecutorRequest request;
+        private final int exitCode;
+        private final String log;
+
+        public FailedExecution(ExecutorRequest request, int exitCode, String 
log) {
+            super(request.toString() + " => " + exitCode + "\n" + log);
+            this.request = request;
+            this.exitCode = exitCode;
+            this.log = log;
+        }
+
+        public ExecutorRequest getRequest() {
+            return request;
+        }
+
+        public int getExitCode() {
+            return exitCode;
+        }
+
+        public String getLog() {
+            return log;
+        }
+    }
+
+    protected abstract Executor createExecutor();
+}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
 
b/impl/maven-executor/src/test/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutorTest.java
similarity index 58%
copy from 
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
copy to 
impl/maven-executor/src/test/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutorTest.java
index 7ffbcab9e9..1fe0caef83 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
+++ 
b/impl/maven-executor/src/test/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutorTest.java
@@ -16,22 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.cling.invoker.mvn;
+package org.apache.maven.cling.executor.embedded;
 
-import org.apache.maven.Maven;
-import org.apache.maven.api.cli.InvokerRequest;
-import org.apache.maven.cling.invoker.LookupContext;
-import org.apache.maven.eventspy.internal.EventSpyDispatcher;
-import org.apache.maven.logging.BuildEventListener;
+import org.apache.maven.api.cli.Executor;
+import org.apache.maven.cling.executor.MavenExecutorTestSupport;
 
-@SuppressWarnings("VisibilityModifier")
-public class MavenContext extends LookupContext {
-    public MavenContext(InvokerRequest invokerRequest) {
-        super(invokerRequest);
-    }
-
-    public BuildEventListener buildEventListener;
-    public EventSpyDispatcher eventSpyDispatcher;
+/**
+ * Embedded executor UT
+ */
+public class EmbeddedMavenExecutorTest extends MavenExecutorTestSupport {
 
-    public Maven maven;
+    @Override
+    protected Executor createExecutor() {
+        return new EmbeddedMavenExecutor();
+    }
 }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
 
b/impl/maven-executor/src/test/java/org/apache/maven/cling/executor/forked/ForkedMavenExecutorTest.java
similarity index 58%
copy from 
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
copy to 
impl/maven-executor/src/test/java/org/apache/maven/cling/executor/forked/ForkedMavenExecutorTest.java
index 7ffbcab9e9..ee1c007fb9 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
+++ 
b/impl/maven-executor/src/test/java/org/apache/maven/cling/executor/forked/ForkedMavenExecutorTest.java
@@ -16,22 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.cling.invoker.mvn;
+package org.apache.maven.cling.executor.forked;
 
-import org.apache.maven.Maven;
-import org.apache.maven.api.cli.InvokerRequest;
-import org.apache.maven.cling.invoker.LookupContext;
-import org.apache.maven.eventspy.internal.EventSpyDispatcher;
-import org.apache.maven.logging.BuildEventListener;
+import org.apache.maven.api.cli.Executor;
+import org.apache.maven.cling.executor.MavenExecutorTestSupport;
 
-@SuppressWarnings("VisibilityModifier")
-public class MavenContext extends LookupContext {
-    public MavenContext(InvokerRequest invokerRequest) {
-        super(invokerRequest);
-    }
-
-    public BuildEventListener buildEventListener;
-    public EventSpyDispatcher eventSpyDispatcher;
+/**
+ * Forked executor UT
+ */
+public class ForkedMavenExecutorTest extends MavenExecutorTestSupport {
 
-    public Maven maven;
+    @Override
+    protected Executor createExecutor() {
+        return new ForkedMavenExecutor();
+    }
 }
diff --git a/impl/pom.xml b/impl/pom.xml
index b2740230de..f7b38fe395 100644
--- a/impl/pom.xml
+++ b/impl/pom.xml
@@ -38,5 +38,6 @@ under the License.
     <module>maven-logging</module>
     <module>maven-core</module>
     <module>maven-cli</module>
+    <module>maven-executor</module>
   </modules>
 </project>
diff --git a/pom.xml b/pom.xml
index f933906b63..7aff69544f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -675,6 +675,7 @@ under the License.
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-surefire-plugin</artifactId>
+          <version>3.5.2</version>
           <configuration>
             <argLine>-Xmx256m</argLine>
           </configuration>
diff --git a/src/graph/ReactorGraph.java b/src/graph/ReactorGraph.java
index c5010b404f..4034dbd1d5 100755
--- a/src/graph/ReactorGraph.java
+++ b/src/graph/ReactorGraph.java
@@ -42,7 +42,7 @@ public class ReactorGraph {
         CLUSTER_PATTERNS.put("JLine", Pattern.compile("^org\\.jline:.*"));
         CLUSTER_PATTERNS.put("Maven API", 
Pattern.compile("^org\\.apache\\.maven:maven-api-(?!impl).*"));
         CLUSTER_PATTERNS.put("Maven Resolver", 
Pattern.compile("^org\\.apache\\.maven\\.resolver:.*"));
-        CLUSTER_PATTERNS.put("Maven Implementation", 
Pattern.compile("^org\\.apache\\.maven:maven-(impl|di|core|cli|xml|jline|logging):.*"));
+        CLUSTER_PATTERNS.put("Maven Implementation", 
Pattern.compile("^org\\.apache\\.maven:maven-(impl|di|core|cli|xml|jline|logging|executor):.*"));
         CLUSTER_PATTERNS.put("Maven Compatibility", 
Pattern.compile("^org\\.apache\\.maven:maven-(artifact|builder-support|compat|embedder|model|model-builder|plugin-api|repository-metadata|resolver-provider|settings|settings-builder|toolchain-builder|toolchain-model):.*"));
         CLUSTER_PATTERNS.put("Sisu", 
Pattern.compile("(^org\\.eclipse\\.sisu:.*)|(.*:guice:.*)|(.*:javax.inject:.*)|(.*:javax.annotation-api:.*)"));
         CLUSTER_PATTERNS.put("Plexus", 
Pattern.compile("^org\\.codehaus\\.plexus:.*"));


Reply via email to