[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
This commit is contained in:
Tamas Cservenak 2024-11-22 15:09:32 +01:00 committed by GitHub
parent 8a88a40c8d
commit 7ad2578e77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1034 additions and 560 deletions

View File

@ -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();
}

View File

@ -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.
*/

View File

@ -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.

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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;
}
}
}
}

View File

@ -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;
}

View File

@ -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");

View File

@ -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();
}
}

View File

@ -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));
}

View File

@ -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 {}
}

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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();
}

View File

@ -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()) {

View File

@ -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!");
}
}
""";
}

View File

@ -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"));
}
}

View File

@ -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

View File

@ -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()

View File

@ -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() {

103
impl/maven-executor/pom.xml Normal file
View File

@ -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>

View File

@ -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.

View File

@ -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.
*

View File

@ -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());
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -0,0 +1,33 @@
/*
* 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.embedded;
import org.apache.maven.api.cli.Executor;
import org.apache.maven.cling.executor.MavenExecutorTestSupport;
/**
* Embedded executor UT
*/
public class EmbeddedMavenExecutorTest extends MavenExecutorTestSupport {
@Override
protected Executor createExecutor() {
return new EmbeddedMavenExecutor();
}
}

View File

@ -0,0 +1,33 @@
/*
* 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 org.apache.maven.api.cli.Executor;
import org.apache.maven.cling.executor.MavenExecutorTestSupport;
/**
* Forked executor UT
*/
public class ForkedMavenExecutorTest extends MavenExecutorTestSupport {
@Override
protected Executor createExecutor() {
return new ForkedMavenExecutor();
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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:.*"));