From e74bde05c926579b0adc75ef6d9e46bf8b307e42 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 5 Dec 2024 09:02:41 +0100 Subject: [PATCH] [MNG-8403] Maven ITs use maven-executor (#1940) The goal of this PR is manifold, but major one is to be able to use in ITs new options introduced in Maven4. Currently the "embedded" mode supports only Maven3 options, as Maven4 got new CLI entry point (CLIng), while verifier uses old MavenCli entry point, that is also deprecated. Finally, a full cleanup of (black) magic happened as well, keep ITs simple and clean. Changes: * dropped from ITs classpath maven-shared-util * dropped from ITs classpath maven-verifier, copied last master Verifier to maven-it-helper and modified * enhancements to new maven-executor to make it fully replace maven-verifier * ITs are now using new infra and are using new CLIng "entry point" as well (so far ITs used deprecated maven-embedder/MavenCLI class). --- https://issues.apache.org/jira/browse/MNG-8403 --- .github/workflows/maven.yml | 2 +- impl/maven-executor/pom.xml | 7 +- .../org/apache/maven/api/cli/Executor.java | 4 +- .../apache/maven/api/cli/ExecutorRequest.java | 203 +++- .../maven/cling/executor/ExecutorHelper.java | 83 ++ .../maven/cling/executor/ExecutorTool.java | 76 ++ .../embedded/EmbeddedMavenExecutor.java | 322 ++++-- .../executor/forked/ForkedMavenExecutor.java | 97 +- .../cling/executor/internal/HelperImpl.java | 123 +++ .../cling/executor/internal/ToolboxTool.java | 137 +++ .../executor/MavenExecutorTestSupport.java | 41 +- .../cling/executor/impl/HelperImplTest.java | 168 +++ its/core-it-suite/pom.xml | 37 +- .../it/MavenITmng3183LoggingToFileTest.java | 3 + ...Tmng3379ParallelArtifactDownloadsTest.java | 6 +- .../it/MavenITmng3951AbsolutePathsTest.java | 3 +- .../MavenITmng3955EffectiveSettingsTest.java | 2 +- ...ITmng5868NoDuplicateAttachedArtifacts.java | 1 - ...Tmng8106OverlappingDirectoryRolesTest.java | 6 +- .../it/MavenITmng8181CentralRepoTest.java | 1 + ...rsionedAndUnversionedDependenciesTest.java | 3 +- .../MavenITmng8400CanonicalMavenHomeTest.java | 9 +- its/core-it-support/maven-it-helper/pom.xml | 38 +- .../it/AbstractMavenIntegrationTestCase.java | 2 +- .../java/org/apache/maven/it/Verifier.java | 977 +++++++++++++++++- .../verifier/VerificationException.java} | 26 +- .../verifier/util/ResourceExtractor.java | 140 +++ its/pom.xml | 4 +- 28 files changed, 2235 insertions(+), 286 deletions(-) create mode 100644 impl/maven-executor/src/main/java/org/apache/maven/cling/executor/ExecutorHelper.java create mode 100644 impl/maven-executor/src/main/java/org/apache/maven/cling/executor/ExecutorTool.java create mode 100644 impl/maven-executor/src/main/java/org/apache/maven/cling/executor/internal/HelperImpl.java create mode 100644 impl/maven-executor/src/main/java/org/apache/maven/cling/executor/internal/ToolboxTool.java create mode 100644 impl/maven-executor/src/test/java/org/apache/maven/cling/executor/impl/HelperImplTest.java rename its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/{it/AnsiSupport.java => shared/verifier/VerificationException.java} (58%) create mode 100644 its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/shared/verifier/util/ResourceExtractor.java diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 91929cab73..1dfc8e98b3 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -52,7 +52,7 @@ jobs: - name: Set up Maven shell: bash - run: mvn --errors --batch-mode --show-version org.apache.maven.plugins:maven-wrapper-plugin:3.3.2:wrapper "-Dmaven=4.0.0-beta-4" + run: mvn --errors --batch-mode --show-version org.apache.maven.plugins:maven-wrapper-plugin:3.3.2:wrapper "-Dmaven=4.0.0-rc-1" - name: Build Maven distributions shell: bash diff --git a/impl/maven-executor/pom.xml b/impl/maven-executor/pom.xml index f80126b1ff..1047082914 100644 --- a/impl/maven-executor/pom.xml +++ b/impl/maven-executor/pom.xml @@ -35,7 +35,7 @@ under the License. 3.9.9 - 4.0.0-beta-5 + 4.0.0-rc-1 @@ -50,6 +50,11 @@ under the License. junit-jupiter-api test + + org.junit.jupiter + junit-jupiter-params + test + diff --git a/impl/maven-executor/src/main/java/org/apache/maven/api/cli/Executor.java b/impl/maven-executor/src/main/java/org/apache/maven/api/cli/Executor.java index fec88707df..2d2ddf40d1 100644 --- a/impl/maven-executor/src/main/java/org/apache/maven/api/cli/Executor.java +++ b/impl/maven-executor/src/main/java/org/apache/maven/api/cli/Executor.java @@ -34,7 +34,7 @@ public interface Executor extends AutoCloseable { boolean IS_WINDOWS = System.getProperty("os.name", "unknown").startsWith("Windows"); /** - * Maven version string returned when the actual version of Maven cannot be determinet. + * Maven version string returned when the actual version of Maven cannot be determined. */ String UNKNOWN_VERSION = "unknown"; @@ -50,7 +50,7 @@ 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 + * Returns the Maven version that provided {@link ExecutorRequest} point at (would use). Please note, 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()}. * 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 index 1f40efa4b6..d13fa7ee51 100644 --- 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 @@ -19,10 +19,13 @@ package org.apache.maven.api.cli; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import org.apache.maven.api.annotations.Experimental; @@ -35,7 +38,7 @@ 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 + * Maven command with arguments. The arguments are not parsed, they are just passed over * to executed tool. * * @since 4.0.0 @@ -43,6 +46,11 @@ import static java.util.Objects.requireNonNull; @Immutable @Experimental public interface ExecutorRequest { + /** + * The Maven command. + */ + String MVN = "mvn"; + /** * The command to execute, ie "mvn". */ @@ -82,10 +90,27 @@ public interface ExecutorRequest { @Nonnull Path userHomeDirectory(); + /** + * Returns the map of Java System Properties to set before executing process. + * + * @return an Optional containing the map of Java System Properties, or empty if not specified + */ + @Nonnull + Optional> jvmSystemProperties(); + + /** + * Returns the map of environment variables to set before executing process. + * This property is used ONLY by executors that spawn a new JVM. + * + * @return an Optional containing the map of environment variables, or empty if not specified + */ + @Nonnull + Optional> environmentVariables(); + /** * 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. + * This property is used ONLY by executors that spawn a new JVM. * * @return an Optional containing the list of extra JVM arguments, or empty if not specified */ @@ -93,7 +118,25 @@ public interface ExecutorRequest { Optional> jvmArguments(); /** - * Returns {@link Builder} for this instance. + * Optional consumer for STD out of the Maven. If given, this consumer will get all output from the std out of + * Maven. Note: whether consumer gets to consume anything depends on invocation arguments passed in + * {@link #arguments()}, as if log file is set, not much will go to stdout. + * + * @return an Optional containing the stdout consumer, or empty if not specified. + */ + Optional stdoutConsumer(); + + /** + * Optional consumer for STD err of the Maven. If given, this consumer will get all output from the std err of + * Maven. Note: whether consumer gets to consume anything depends on invocation arguments passed in + * {@link #arguments()}, as if log file is set, not much will go to stderr. + * + * @return an Optional containing the stderr consumer, or empty if not specified. + */ + Optional stderrConsumer(); + + /** + * Returns {@link Builder} created from this instance. */ @Nonnull default Builder toBuilder() { @@ -103,28 +146,29 @@ public interface ExecutorRequest { cwd(), installationDirectory(), userHomeDirectory(), - jvmArguments().orElse(null)); + jvmSystemProperties().orElse(null), + environmentVariables().orElse(null), + jvmArguments().orElse(null), + stdoutConsumer().orElse(null), + stderrConsumer().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. + * Returns new builder pre-set to run Maven. The discovery of maven home is attempted, user cwd and home are + * also discovered by standard means. */ @Nonnull static Builder mavenBuilder(@Nullable Path installationDirectory) { return new Builder( - "mvn", + MVN, null, getCanonicalPath(Paths.get(System.getProperty("user.dir"))), installationDirectory != null ? getCanonicalPath(installationDirectory) : discoverMavenHome(), getCanonicalPath(Paths.get(System.getProperty("user.home"))), + null, + null, + null, + null, null); } @@ -134,23 +178,36 @@ public interface ExecutorRequest { private Path cwd; private Path installationDirectory; private Path userHomeDirectory; + private Map jvmSystemProperties; + private Map environmentVariables; private List jvmArguments; + private OutputStream stdoutConsumer; + private OutputStream stderrConsumer; private Builder() {} + @SuppressWarnings("ParameterNumber") private Builder( String command, List arguments, Path cwd, Path installationDirectory, Path userHomeDirectory, - List jvmArguments) { + Map jvmSystemProperties, + Map environmentVariables, + List jvmArguments, + OutputStream stdoutConsumer, + OutputStream stderrConsumer) { this.command = command; this.arguments = arguments; this.cwd = cwd; this.installationDirectory = installationDirectory; this.userHomeDirectory = userHomeDirectory; + this.jvmSystemProperties = jvmSystemProperties; + this.environmentVariables = environmentVariables; this.jvmArguments = jvmArguments; + this.stdoutConsumer = stdoutConsumer; + this.stderrConsumer = stderrConsumer; } @Nonnull @@ -176,19 +233,54 @@ public interface ExecutorRequest { @Nonnull public Builder cwd(Path cwd) { - this.cwd = requireNonNull(cwd, "cwd"); + this.cwd = getCanonicalPath(requireNonNull(cwd, "cwd")); return this; } @Nonnull public Builder installationDirectory(Path installationDirectory) { - this.installationDirectory = requireNonNull(installationDirectory, "installationDirectory"); + this.installationDirectory = + getCanonicalPath(requireNonNull(installationDirectory, "installationDirectory")); return this; } @Nonnull public Builder userHomeDirectory(Path userHomeDirectory) { - this.userHomeDirectory = requireNonNull(userHomeDirectory, "userHomeDirectory"); + this.userHomeDirectory = getCanonicalPath(requireNonNull(userHomeDirectory, "userHomeDirectory")); + return this; + } + + @Nonnull + public Builder jvmSystemProperties(Map jvmSystemProperties) { + this.jvmSystemProperties = jvmSystemProperties; + return this; + } + + @Nonnull + public Builder jvmSystemProperty(String key, String value) { + requireNonNull(key, "env key"); + requireNonNull(value, "env value"); + if (jvmSystemProperties == null) { + this.jvmSystemProperties = new HashMap<>(); + } + this.jvmSystemProperties.put(key, value); + return this; + } + + @Nonnull + public Builder environmentVariables(Map environmentVariables) { + this.environmentVariables = environmentVariables; + return this; + } + + @Nonnull + public Builder environmentVariable(String key, String value) { + requireNonNull(key, "env key"); + requireNonNull(value, "env value"); + if (environmentVariables == null) { + this.environmentVariables = new HashMap<>(); + } + this.environmentVariables.put(key, value); return this; } @@ -207,9 +299,31 @@ public interface ExecutorRequest { return this; } + @Nonnull + public Builder stdoutConsumer(OutputStream stdoutConsumer) { + this.stdoutConsumer = stdoutConsumer; + return this; + } + + @Nonnull + public Builder stderrConsumer(OutputStream stderrConsumer) { + this.stderrConsumer = stderrConsumer; + return this; + } + @Nonnull public ExecutorRequest build() { - return new Impl(command, arguments, cwd, installationDirectory, userHomeDirectory, jvmArguments); + return new Impl( + command, + arguments, + cwd, + installationDirectory, + userHomeDirectory, + jvmSystemProperties, + environmentVariables, + jvmArguments, + stdoutConsumer, + stderrConsumer); } private static class Impl implements ExecutorRequest { @@ -218,21 +332,34 @@ public interface ExecutorRequest { private final Path cwd; private final Path installationDirectory; private final Path userHomeDirectory; + private final Map jvmSystemProperties; + private final Map environmentVariables; private final List jvmArguments; + private final OutputStream stdoutConsumer; + private final OutputStream stderrConsumer; + @SuppressWarnings("ParameterNumber") private Impl( String command, List arguments, Path cwd, Path installationDirectory, Path userHomeDirectory, - List jvmArguments) { + Map jvmSystemProperties, + Map environmentVariables, + List jvmArguments, + OutputStream stdoutConsumer, + OutputStream stderrConsumer) { 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.cwd = getCanonicalPath(requireNonNull(cwd)); + this.installationDirectory = getCanonicalPath(requireNonNull(installationDirectory)); + this.userHomeDirectory = getCanonicalPath(requireNonNull(userHomeDirectory)); + this.jvmSystemProperties = jvmSystemProperties != null ? Map.copyOf(jvmSystemProperties) : null; + this.environmentVariables = environmentVariables != null ? Map.copyOf(environmentVariables) : null; this.jvmArguments = jvmArguments != null ? List.copyOf(jvmArguments) : null; + this.stdoutConsumer = stdoutConsumer; + this.stderrConsumer = stderrConsumer; } @Override @@ -260,20 +387,44 @@ public interface ExecutorRequest { return userHomeDirectory; } + @Override + public Optional> jvmSystemProperties() { + return Optional.ofNullable(jvmSystemProperties); + } + + @Override + public Optional> environmentVariables() { + return Optional.ofNullable(environmentVariables); + } + @Override public Optional> jvmArguments() { return Optional.ofNullable(jvmArguments); } + @Override + public Optional stdoutConsumer() { + return Optional.ofNullable(stdoutConsumer); + } + + @Override + public Optional stderrConsumer() { + return Optional.ofNullable(stderrConsumer); + } + @Override public String toString() { - return "ExecutionRequest{" + "command='" + return "Impl{" + "command='" + command + '\'' + ", arguments=" + arguments + ", cwd=" + cwd + ", installationDirectory=" + installationDirectory + ", userHomeDirectory=" - + userHomeDirectory + ", jvmArguments=" - + jvmArguments + '}'; + + userHomeDirectory + ", jvmSystemProperties=" + + jvmSystemProperties + ", environmentVariables=" + + environmentVariables + ", jvmArguments=" + + jvmArguments + ", stdoutConsumer=" + + stdoutConsumer + ", stderrConsumer=" + + stderrConsumer + '}'; } } } diff --git a/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/ExecutorHelper.java b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/ExecutorHelper.java new file mode 100644 index 0000000000..14a5e2abd2 --- /dev/null +++ b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/ExecutorHelper.java @@ -0,0 +1,83 @@ +/* + * 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 org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.cli.Executor; +import org.apache.maven.api.cli.ExecutorException; +import org.apache.maven.api.cli.ExecutorRequest; + +/** + * Helper class for routing Maven execution based on preferences and/or issued execution requests. + */ +public interface ExecutorHelper extends ExecutorTool { + /** + * The modes of execution. + */ + enum Mode { + /** + * Automatically decide. For example, presence of {@link ExecutorRequest#environmentVariables()} or + * {@link ExecutorRequest#jvmArguments()} will result in choosing {@link #FORKED} executor. Otherwise, + * {@link #EMBEDDED} executor is preferred. + */ + AUTO, + /** + * Forces embedded execution. May fail if {@link ExecutorRequest} contains input unsupported by executor. + */ + EMBEDDED, + /** + * Forces forked execution. Always carried out, most isolated and "most correct", but is slow as it uses child process. + */ + FORKED + } + + /** + * Returns the preferred mode of this helper. + */ + @Nonnull + Mode getDefaultMode(); + + /** + * Creates pre-populated builder for {@link ExecutorRequest}. Users of helper must use this method to create + * properly initialized request builder. + */ + @Nonnull + ExecutorRequest.Builder executorRequest(); + + /** + * Executes the request with preferred mode executor. + */ + default int execute(ExecutorRequest executorRequest) throws ExecutorException { + return execute(getDefaultMode(), executorRequest); + } + + /** + * Executes the request with passed in mode executor. + */ + int execute(Mode mode, ExecutorRequest executorRequest) throws ExecutorException; + + /** + * High level operation, returns the version of the Maven covered by this helper. This method call caches + * underlying operation, and is safe to invoke as many times needed. + * + * @see Executor#mavenVersion(ExecutorRequest) + */ + @Nonnull + String mavenVersion(); +} diff --git a/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/ExecutorTool.java b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/ExecutorTool.java new file mode 100644 index 0000000000..4bfe4be448 --- /dev/null +++ b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/ExecutorTool.java @@ -0,0 +1,76 @@ +/* + * 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.util.Map; + +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.cli.ExecutorException; +import org.apache.maven.api.cli.ExecutorRequest; + +/** + * A tool implementing some common Maven operations. + */ +public interface ExecutorTool { + /** + * Performs a diagnostic dump of the environment. + * + * @param request never {@code null} + */ + Map dump(ExecutorRequest.Builder request) throws ExecutorException; + + /** + * Returns the location of local repository, as detected by Maven. The {@code userSettings} param may contain + * some override (equivalent of {@code -s settings.xml} on CLI). + * + * @param request never {@code null} + */ + String localRepository(ExecutorRequest.Builder request) throws ExecutorException; + + /** + * Returns relative (to {@link #localRepository(ExecutorRequest.Builder)}) path of given artifact in local repository. + * + * @param request never {@code null} + * @param gav the usual resolver artifact GAV string, never {@code null} + * @param repositoryId the remote repository ID in case "remote artifact" is asked for + */ + String artifactPath(ExecutorRequest.Builder request, String gav, @Nullable String repositoryId) + throws ExecutorException; + + /** + * Returns relative (to {@link #localRepository(ExecutorRequest.Builder)}) path of given metadata in local repository. + * The metadata coordinates in form of {@code [G]:[A]:[V]:[type]}. Absence of {@code A} implies absence of {@code V} + * as well (in other words, it can be {@code G}, {@code G:A} or {@code G:A:V}). The absence of {@code type} implies + * it is "maven-metadata.xml". The simplest spec string is {@code :::}. + *

+ * Examples: + *

    + *
  • {@code :::} is root metadata named "maven-metadata.xml"
  • + *
  • {@code :::my-metadata.xml} is root metadata named "my-metadata.xml"
  • + *
  • {@code G:::} equals to {@code G:::maven-metadata.xml}
  • + *
  • {@code G:A::} equals to {@code G:A::maven-metadata.xml}
  • + *
+ * + * @param request never {@code null} + * @param gav the resolver metadata GAV string + * @param repositoryId the remote repository ID in case "remote metadata" is asked for + */ + String metadataPath(ExecutorRequest.Builder request, String gav, @Nullable String repositoryId) + throws ExecutorException; +} diff --git a/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java index 350b0cc648..32b936a448 100644 --- a/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java +++ b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; @@ -31,9 +32,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Properties; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Stream; @@ -45,40 +50,51 @@ import static java.util.Objects.requireNonNull; /** * Embedded executor implementation, that invokes Maven from installation directory within this same JVM but in isolated - * classloader. This class supports Maven 4.x and Maven 3.x as well. - * The class world with Maven is kept in memory as long as instance of this class is not closed. Subsequent execution - * requests over same installation home are cached. + * classloader. This class supports Maven 4.x and Maven 3.x as well. The ClassWorld of Maven is kept in memory as + * long as instance of this class is not closed. Subsequent execution requests over same installation home are cached. */ public class EmbeddedMavenExecutor implements Executor { protected static final class Context { - private final Properties properties; private final URLClassLoader bootClassLoader; private final String version; private final Object classWorld; + private final Set originalClassRealmIds; private final ClassLoader tccl; private final Function exec; public Context( - Properties properties, URLClassLoader bootClassLoader, String version, Object classWorld, + Set originalClassRealmIds, ClassLoader tccl, Function exec) { - this.properties = properties; this.bootClassLoader = bootClassLoader; this.version = version; this.classWorld = classWorld; + this.originalClassRealmIds = originalClassRealmIds; this.tccl = tccl; this.exec = exec; } } - private final Properties originalProperties; - private final ClassLoader originalClassLoader; - private final ConcurrentHashMap contexts; + protected final boolean cacheContexts; + protected final AtomicBoolean closed; + protected final PrintStream originalStdout; + protected final PrintStream originalStderr; + protected final Properties originalProperties; + protected final ClassLoader originalClassLoader; + protected final ConcurrentHashMap contexts; public EmbeddedMavenExecutor() { + this(true); + } + + public EmbeddedMavenExecutor(boolean cacheContexts) { + this.cacheContexts = cacheContexts; + this.closed = new AtomicBoolean(false); + this.originalStdout = System.out; + this.originalStderr = System.err; this.originalClassLoader = Thread.currentThread().getContextClassLoader(); this.contexts = new ConcurrentHashMap<>(); this.originalProperties = System.getProperties(); @@ -87,18 +103,51 @@ public class EmbeddedMavenExecutor implements Executor { @Override public int execute(ExecutorRequest executorRequest) throws ExecutorException { requireNonNull(executorRequest); + if (closed.get()) { + throw new ExecutorException("Executor is closed"); + } validate(executorRequest); Context context = mayCreate(executorRequest); - System.setProperties(context.properties); Thread.currentThread().setContextClassLoader(context.tccl); try { + if (executorRequest.stdoutConsumer().isPresent()) { + System.setOut(new PrintStream(executorRequest.stdoutConsumer().get(), true)); + } + if (executorRequest.stderrConsumer().isPresent()) { + System.setErr(new PrintStream(executorRequest.stderrConsumer().get(), true)); + } return context.exec.apply(executorRequest); } catch (Exception e) { throw new ExecutorException("Failed to execute", e); } finally { - Thread.currentThread().setContextClassLoader(originalClassLoader); - System.setProperties(originalProperties); + try { + disposeRuntimeCreatedRealms(context); + } finally { + System.setOut(originalStdout); + System.setErr(originalStderr); + Thread.currentThread().setContextClassLoader(originalClassLoader); + System.setProperties(originalProperties); + if (!cacheContexts) { + doClose(context); + } + } + } + } + + protected void disposeRuntimeCreatedRealms(Context context) { + try { + Method getRealms = context.classWorld.getClass().getMethod("getRealms"); + Method disposeRealm = context.classWorld.getClass().getMethod("disposeRealm", String.class); + List realms = (List) getRealms.invoke(context.classWorld); + for (Object realm : realms) { + String realmId = (String) realm.getClass().getMethod("getId").invoke(realm); + if (!context.originalClassRealmIds.contains(realmId)) { + disposeRealm.invoke(context.classWorld, realmId); + } + } + } catch (Exception e) { + throw new ExecutorException("Failed to dispose runtime created realms", e); } } @@ -106,117 +155,172 @@ public class EmbeddedMavenExecutor implements Executor { public String mavenVersion(ExecutorRequest executorRequest) throws ExecutorException { requireNonNull(executorRequest); validate(executorRequest); + if (closed.get()) { + throw new ExecutorException("Executor is closed"); + } return mayCreate(executorRequest).version; } protected Context mayCreate(ExecutorRequest executorRequest) { - Path installation = executorRequest.installationDirectory(); - if (!Files.isDirectory(installation)) { - throw new IllegalArgumentException("Installation directory must point to existing directory"); + Path mavenHome = ExecutorRequest.getCanonicalPath(executorRequest.installationDirectory()); + if (cacheContexts) { + return contexts.computeIfAbsent(mavenHome, k -> doCreate(mavenHome, executorRequest)); + } else { + return doCreate(mavenHome, executorRequest); } - return contexts.computeIfAbsent(installation, k -> { - Path mavenHome = installation.toAbsolutePath().normalize(); - Path boot = mavenHome.resolve("boot"); - Path m2conf = mavenHome.resolve("bin/m2.conf"); - if (!Files.isDirectory(boot) || !Files.isRegularFile(m2conf)) { - throw new IllegalArgumentException("Installation directory does not point to Maven installation"); - } - - Properties properties = new Properties(); - properties.putAll(System.getProperties()); - properties.put( - "user.dir", - executorRequest.cwd().toAbsolutePath().normalize().toString()); - properties.put( - "maven.multiModuleProjectDirectory", - executorRequest.cwd().toAbsolutePath().normalize().toString()); - properties.put( - "user.home", - executorRequest - .userHomeDirectory() - .toAbsolutePath() - .normalize() - .toString()); - properties.put("maven.home", mavenHome.toString()); - properties.put("maven.mainClass", "org.apache.maven.cling.MavenCling"); - properties.put( - "library.jline.path", mavenHome.resolve("lib/jline-native").toString()); - - System.setProperties(properties); - URLClassLoader bootClassLoader = createMavenBootClassLoader(boot, Collections.emptyList()); - Thread.currentThread().setContextClassLoader(bootClassLoader); - try { - Class launcherClass = bootClassLoader.loadClass("org.codehaus.plexus.classworlds.launcher.Launcher"); - Object launcher = launcherClass.getDeclaredConstructor().newInstance(); - Method configure = launcherClass.getMethod("configure", InputStream.class); - try (InputStream inputStream = Files.newInputStream(m2conf)) { - configure.invoke(launcher, inputStream); - } - Object classWorld = launcherClass.getMethod("getWorld").invoke(launcher); - Class cliClass = - (Class) launcherClass.getMethod("getMainClass").invoke(launcher); - String version = getMavenVersion(cliClass); - Function exec; - - if (version.startsWith("3.")) { - // 3.x - Constructor newMavenCli = cliClass.getConstructor(classWorld.getClass()); - Object mavenCli = newMavenCli.newInstance(classWorld); - Class[] parameterTypes = {String[].class, String.class, PrintStream.class, PrintStream.class}; - Method doMain = cliClass.getMethod("doMain", parameterTypes); - exec = r -> { - try { - return (int) doMain.invoke(mavenCli, new Object[] { - r.arguments().toArray(new String[0]), r.cwd().toString(), null, null - }); - } catch (Exception e) { - throw new ExecutorException("Failed to execute", e); - } - }; - } else { - // assume 4.x - Method mainMethod = cliClass.getMethod("main", String[].class, classWorld.getClass()); - exec = r -> { - try { - return (int) mainMethod.invoke(null, r.arguments().toArray(new String[0]), classWorld); - } catch (Exception e) { - throw new ExecutorException("Failed to execute", e); - } - }; - } - - return new Context(properties, bootClassLoader, version, classWorld, cliClass.getClassLoader(), exec); - } catch (Exception e) { - throw new ExecutorException("Failed to create executor", e); - } finally { - Thread.currentThread().setContextClassLoader(originalClassLoader); - System.setProperties(originalProperties); - } - }); } - @Override - public void close() throws ExecutorException { + protected Context doCreate(Path mavenHome, ExecutorRequest executorRequest) { + if (!Files.isDirectory(mavenHome)) { + throw new IllegalArgumentException("Installation directory must point to existing directory"); + } + if (!Objects.equals(executorRequest.command(), ExecutorRequest.MVN)) { + throw new IllegalArgumentException( + getClass().getSimpleName() + " does not support command " + executorRequest.command()); + } + if (executorRequest.environmentVariables().isPresent()) { + throw new IllegalArgumentException(getClass().getSimpleName() + " does not support environment variables"); + } + if (executorRequest.jvmArguments().isPresent()) { + throw new IllegalArgumentException(getClass().getSimpleName() + " does not support jvmArguments"); + } + Path boot = mavenHome.resolve("boot"); + Path m2conf = mavenHome.resolve("bin/m2.conf"); + if (!Files.isDirectory(boot) || !Files.isRegularFile(m2conf)) { + throw new IllegalArgumentException("Installation directory does not point to Maven installation"); + } + + Properties properties = prepareProperties(executorRequest); + + System.setProperties(properties); + URLClassLoader bootClassLoader = createMavenBootClassLoader(boot, Collections.emptyList()); + Thread.currentThread().setContextClassLoader(bootClassLoader); try { - ArrayList exceptions = new ArrayList<>(); - for (Context context : contexts.values()) { - try { - doClose(context); - } catch (Exception e) { - exceptions.add(e); - } + Class launcherClass = bootClassLoader.loadClass("org.codehaus.plexus.classworlds.launcher.Launcher"); + Object launcher = launcherClass.getDeclaredConstructor().newInstance(); + Method configure = launcherClass.getMethod("configure", InputStream.class); + try (InputStream inputStream = Files.newInputStream(m2conf)) { + configure.invoke(launcher, inputStream); } - if (!exceptions.isEmpty()) { - ExecutorException e = new ExecutorException("Could not close cleanly"); - exceptions.forEach(e::addSuppressed); - throw e; + Object classWorld = launcherClass.getMethod("getWorld").invoke(launcher); + Set originalClassRealmIds = new HashSet<>(); + + // collect pre-created (in m2.conf) class realms as "original ones"; the rest are created at runtime + Method getRealms = classWorld.getClass().getMethod("getRealms"); + List realms = (List) getRealms.invoke(classWorld); + for (Object realm : realms) { + Method realmGetId = realm.getClass().getMethod("getId"); + originalClassRealmIds.add((String) realmGetId.invoke(realm)); } + + Class cliClass = + (Class) launcherClass.getMethod("getMainClass").invoke(launcher); + String version = getMavenVersion(cliClass); + Function exec; + + if (version.startsWith("3.")) { + // 3.x + Constructor newMavenCli = cliClass.getConstructor(classWorld.getClass()); + Object mavenCli = newMavenCli.newInstance(classWorld); + Class[] parameterTypes = {String[].class, String.class, PrintStream.class, PrintStream.class}; + Method doMain = cliClass.getMethod("doMain", parameterTypes); + exec = r -> { + System.setProperties(null); + System.setProperties(prepareProperties(r)); + try { + return (int) doMain.invoke(mavenCli, new Object[] { + r.arguments().toArray(new String[0]), r.cwd().toString(), null, null + }); + } catch (Exception e) { + throw new ExecutorException("Failed to execute", e); + } + }; + } else { + // assume 4.x + Method mainMethod = cliClass.getMethod("main", String[].class, classWorld.getClass()); + Class ansiConsole = cliClass.getClassLoader().loadClass("org.jline.jansi.AnsiConsole"); + Field ansiConsoleInstalled = ansiConsole.getDeclaredField("installed"); + ansiConsoleInstalled.setAccessible(true); + exec = r -> { + System.setProperties(null); + System.setProperties(prepareProperties(r)); + try { + try { + if (r.stdoutConsumer().isPresent() + || r.stderrConsumer().isPresent()) { + ansiConsoleInstalled.set(null, 1); + } + return (int) mainMethod.invoke(null, r.arguments().toArray(new String[0]), classWorld); + } finally { + if (r.stdoutConsumer().isPresent() + || r.stderrConsumer().isPresent()) { + ansiConsoleInstalled.set(null, 0); + } + } + } catch (Exception e) { + throw new ExecutorException("Failed to execute", e); + } + }; + } + + return new Context( + bootClassLoader, version, classWorld, originalClassRealmIds, cliClass.getClassLoader(), exec); + } catch (Exception e) { + throw new ExecutorException("Failed to create executor", e); } finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); System.setProperties(originalProperties); } } - protected void doClose(Context context) throws Exception { + protected Properties prepareProperties(ExecutorRequest request) { + Properties properties = new Properties(); + properties.putAll(System.getProperties()); + + properties.setProperty("user.dir", request.cwd().toString()); + properties.setProperty("user.home", request.userHomeDirectory().toString()); + + Path mavenHome = request.installationDirectory(); + properties.setProperty("maven.home", mavenHome.toString()); + properties.setProperty( + "maven.multiModuleProjectDirectory", request.cwd().toString()); + properties.setProperty("maven.mainClass", "org.apache.maven.cling.MavenCling"); + properties.setProperty( + "library.jline.path", mavenHome.resolve("lib/jline-native").toString()); + // TODO: is this needed? + properties.setProperty("org.jline.terminal.provider", "dumb"); + + if (request.jvmSystemProperties().isPresent()) { + properties.putAll(request.jvmSystemProperties().get()); + } + + return properties; + } + + @Override + public void close() throws ExecutorException { + if (closed.compareAndExchange(false, true)) { + try { + ArrayList exceptions = new ArrayList<>(); + for (Context context : contexts.values()) { + try { + doClose(context); + } catch (Exception e) { + exceptions.add(e); + } + } + if (!exceptions.isEmpty()) { + ExecutorException e = new ExecutorException("Could not close cleanly"); + exceptions.forEach(e::addSuppressed); + throw e; + } + } finally { + System.setProperties(originalProperties); + } + } + } + + protected void doClose(Context context) throws ExecutorException { Thread.currentThread().setContextClassLoader(context.bootClassLoader); try { try { @@ -224,6 +328,8 @@ public class EmbeddedMavenExecutor implements Executor { } finally { context.bootClassLoader.close(); } + } catch (Exception e) { + throw new ExecutorException("Failed to close cleanly", e); } finally { Thread.currentThread().setContextClassLoader(originalClassLoader); } 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 index 47ae235cac..74e7983c53 100644 --- 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 @@ -18,21 +18,24 @@ */ package org.apache.maven.cling.executor.forked; -import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.function.Consumer; +import org.apache.maven.api.annotations.Nullable; 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; +import static org.apache.maven.api.cli.ExecutorRequest.getCanonicalPath; /** * Forked executor implementation, that spawns a subprocess with Maven from the installation directory. Very costly @@ -44,7 +47,7 @@ public class ForkedMavenExecutor implements Executor { requireNonNull(executorRequest); validate(executorRequest); - return doExecute(executorRequest, null); + return doExecute(executorRequest, wrapStdouterrConsumer(executorRequest)); } @Override @@ -54,27 +57,18 @@ public class ForkedMavenExecutor implements Executor { try { Path cwd = Files.createTempDirectory("forked-executor-maven-version"); try { - ArrayList 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); - } - }); + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + int exitCode = execute(executorRequest.toBuilder() + .cwd(cwd) + .arguments(List.of("--version", "--quiet")) + .stdoutConsumer(stdout) + .build()); if (exitCode == 0) { - for (String line : stdout) { - if (line.startsWith("Apache Maven ")) { - return line.substring(13, line.indexOf("(") - 1); - } + if (stdout.size() > 0) { + return stdout.toString() + .replace("\n", "") + .replace("\r", "") + .trim(); } return UNKNOWN_VERSION; } else { @@ -91,6 +85,29 @@ public class ForkedMavenExecutor implements Executor { protected void validate(ExecutorRequest executorRequest) throws ExecutorException {} + @Nullable + protected Consumer wrapStdouterrConsumer(ExecutorRequest executorRequest) { + if (executorRequest.stdoutConsumer().isEmpty() + && executorRequest.stderrConsumer().isEmpty()) { + return null; + } else { + return p -> { + try { + if (executorRequest.stdoutConsumer().isPresent()) { + p.getInputStream() + .transferTo(executorRequest.stdoutConsumer().get()); + } + if (executorRequest.stderrConsumer().isPresent()) { + p.getErrorStream() + .transferTo(executorRequest.stderrConsumer().get()); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + } + } + protected int doExecute(ExecutorRequest executorRequest, Consumer processConsumer) throws ExecutorException { ArrayList cmdAndArguments = new ArrayList<>(); @@ -102,16 +119,38 @@ public class ForkedMavenExecutor implements Executor { cmdAndArguments.addAll(executorRequest.arguments()); + ArrayList jvmArgs = new ArrayList<>(); + if (!executorRequest.userHomeDirectory().equals(getCanonicalPath(Paths.get(System.getProperty("user.home"))))) { + jvmArgs.add("-Duser.home=" + executorRequest.userHomeDirectory().toString()); + } + if (executorRequest.jvmArguments().isPresent()) { + jvmArgs.addAll(executorRequest.jvmArguments().get()); + } + if (executorRequest.jvmSystemProperties().isPresent()) { + jvmArgs.addAll(executorRequest.jvmSystemProperties().get().entrySet().stream() + .map(e -> "-D" + e.getKey() + "=" + e.getValue()) + .toList()); + } + + HashMap env = new HashMap<>(); + if (executorRequest.environmentVariables().isPresent()) { + env.putAll(executorRequest.environmentVariables().get()); + } + if (!jvmArgs.isEmpty()) { + String mavenOpts = env.getOrDefault("MAVEN_OPTS", ""); + if (!mavenOpts.isEmpty()) { + mavenOpts += " "; + } + mavenOpts += String.join(" ", jvmArgs); + env.put("MAVEN_OPTS", mavenOpts); + } + 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())); + if (!env.isEmpty()) { + pb.environment().putAll(env); } Process process = pb.start(); diff --git a/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/internal/HelperImpl.java b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/internal/HelperImpl.java new file mode 100644 index 0000000000..d78a9161a1 --- /dev/null +++ b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/internal/HelperImpl.java @@ -0,0 +1,123 @@ +/* + * 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.internal; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.maven.api.annotations.Nullable; +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.cling.executor.ExecutorHelper; +import org.apache.maven.cling.executor.ExecutorTool; + +import static java.util.Objects.requireNonNull; + +/** + * Simple router to executors, and delegate to executor tool. + */ +public class HelperImpl implements ExecutorHelper { + private final Mode defaultMode; + private final Path installationDirectory; + private final ExecutorTool executorTool; + private final HashMap executors; + + private final ConcurrentHashMap cache; + + public HelperImpl(Mode defaultMode, @Nullable Path installationDirectory, Executor embedded, Executor forked) { + this.defaultMode = requireNonNull(defaultMode); + this.installationDirectory = installationDirectory != null + ? ExecutorRequest.getCanonicalPath(installationDirectory) + : ExecutorRequest.discoverMavenHome(); + this.executorTool = new ToolboxTool(this); + this.executors = new HashMap<>(); + + this.executors.put(Mode.EMBEDDED, requireNonNull(embedded, "embedded")); + this.executors.put(Mode.FORKED, requireNonNull(forked, "forked")); + this.cache = new ConcurrentHashMap<>(); + } + + @Override + public Mode getDefaultMode() { + return defaultMode; + } + + @Override + public ExecutorRequest.Builder executorRequest() { + return ExecutorRequest.mavenBuilder(installationDirectory); + } + + @Override + public int execute(Mode mode, ExecutorRequest executorRequest) throws ExecutorException { + return getExecutor(mode, executorRequest).execute(executorRequest); + } + + @Override + public String mavenVersion() { + return cache.computeIfAbsent("maven.version", k -> { + ExecutorRequest request = executorRequest().build(); + return getExecutor(Mode.AUTO, request).mavenVersion(request); + }); + } + + @Override + public Map dump(ExecutorRequest.Builder request) throws ExecutorException { + return executorTool.dump(request); + } + + @Override + public String localRepository(ExecutorRequest.Builder request) throws ExecutorException { + return executorTool.localRepository(request); + } + + @Override + public String artifactPath(ExecutorRequest.Builder request, String gav, String repositoryId) + throws ExecutorException { + return executorTool.artifactPath(request, gav, repositoryId); + } + + @Override + public String metadataPath(ExecutorRequest.Builder request, String gav, String repositoryId) + throws ExecutorException { + return executorTool.metadataPath(request, gav, repositoryId); + } + + protected Executor getExecutor(Mode mode, ExecutorRequest request) throws ExecutorException { + return switch (mode) { + case AUTO -> getExecutorByRequest(request); + case EMBEDDED -> executors.get(Mode.EMBEDDED); + case FORKED -> executors.get(Mode.FORKED); + }; + } + + private Executor getExecutorByRequest(ExecutorRequest request) { + if (Objects.equals(request.command(), ExecutorRequest.MVN) + && request.environmentVariables().orElse(Collections.emptyMap()).isEmpty() + && request.jvmArguments().orElse(Collections.emptyList()).isEmpty()) { + return getExecutor(Mode.EMBEDDED, request); + } else { + return getExecutor(Mode.FORKED, request); + } + } +} diff --git a/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/internal/ToolboxTool.java b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/internal/ToolboxTool.java new file mode 100644 index 0000000000..2f4ef540a3 --- /dev/null +++ b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/internal/ToolboxTool.java @@ -0,0 +1,137 @@ +/* + * 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.internal; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; + +import org.apache.maven.api.cli.ExecutorException; +import org.apache.maven.api.cli.ExecutorRequest; +import org.apache.maven.cling.executor.ExecutorHelper; +import org.apache.maven.cling.executor.ExecutorTool; + +import static java.util.Objects.requireNonNull; + +/** + * {@link ExecutorTool} implementation based on Maveniverse Toolbox. It uses Toolbox mojos to implement all the + * required operations. + * + * @see Maveniverse Toolbox + */ +public class ToolboxTool implements ExecutorTool { + private static final String TOOLBOX = "eu.maveniverse.maven.plugins:toolbox:0.5.2:"; + + private final ExecutorHelper helper; + + public ToolboxTool(ExecutorHelper helper) { + this.helper = requireNonNull(helper); + } + + @Override + public Map dump(ExecutorRequest.Builder executorRequest) throws ExecutorException { + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + ExecutorRequest.Builder builder = mojo(executorRequest, "gav-dump") + .argument("-DasProperties") + .stdoutConsumer(stdout) + .stderrConsumer(stderr); + doExecute(builder); + try { + Properties properties = new Properties(); + properties.load(new ByteArrayInputStream(stdout.toByteArray())); + return properties.entrySet().stream() + .collect(Collectors.toMap( + e -> String.valueOf(e.getKey()), + e -> String.valueOf(e.getValue()), + (prev, next) -> next, + HashMap::new)); + } catch (IOException e) { + throw new ExecutorException("Unable to parse properties", e); + } + } + + @Override + public String localRepository(ExecutorRequest.Builder executorRequest) throws ExecutorException { + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + ExecutorRequest.Builder builder = mojo(executorRequest, "gav-local-repository-path") + .stdoutConsumer(stdout) + .stderrConsumer(stderr); + doExecute(builder); + return shaveStdout(stdout); + } + + @Override + public String artifactPath(ExecutorRequest.Builder executorRequest, String gav, String repositoryId) + throws ExecutorException { + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + ExecutorRequest.Builder builder = mojo(executorRequest, "gav-artifact-path") + .argument("-Dgav=" + gav) + .stdoutConsumer(stdout) + .stderrConsumer(stderr); + if (repositoryId != null) { + builder.argument("-Drepository=" + repositoryId + "::unimportant"); + } + doExecute(builder); + return shaveStdout(stdout); + } + + @Override + public String metadataPath(ExecutorRequest.Builder executorRequest, String gav, String repositoryId) + throws ExecutorException { + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + ExecutorRequest.Builder builder = mojo(executorRequest, "gav-metadata-path") + .argument("-Dgav=" + gav) + .stdoutConsumer(stdout) + .stderrConsumer(stderr); + if (repositoryId != null) { + builder.argument("-Drepository=" + repositoryId + "::unimportant"); + } + doExecute(builder); + return shaveStdout(stdout); + } + + private ExecutorRequest.Builder mojo(ExecutorRequest.Builder builder, String mojo) { + if (helper.mavenVersion().startsWith("4.")) { + builder.argument("--raw-streams"); + } + return builder.argument(TOOLBOX + mojo).argument("--quiet").argument("-DforceStdout"); + } + + private void doExecute(ExecutorRequest.Builder builder) { + ExecutorRequest request = builder.build(); + int ec = helper.execute(request); + if (ec != 0) { + throw new ExecutorException("Unexpected exit code=" + ec + "; stdout=" + + request.stdoutConsumer().orElse(null) + "; stderr=" + + request.stderrConsumer().orElse(null)); + } + } + + private String shaveStdout(ByteArrayOutputStream stdout) { + return stdout.toString().replace("\n", "").replace("\r", ""); + } +} 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 index bf1086ae88..0bf1b30f6a 100644 --- 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 @@ -35,6 +35,43 @@ import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; public abstract class MavenExecutorTestSupport { + @Disabled("JUnit on Windows fails to clean up as mvn3 seems does not close log file properly") + @Test + void dump3( + @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path cwd, + @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path userHome) + throws Exception { + String logfile = "m3.log"; + execute( + cwd.resolve(logfile), + List.of(mvn3ExecutorRequestBuilder() + .cwd(cwd) + .userHomeDirectory(userHome) + .argument("eu.maveniverse.maven.plugins:toolbox:0.5.2:gav-dump") + .argument("-l") + .argument(logfile) + .build())); + System.out.println(Files.readString(cwd.resolve(logfile))); + } + + @Test + void dump4( + @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path cwd, + @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path userHome) + throws Exception { + String logfile = "m4.log"; + execute( + cwd.resolve(logfile), + List.of(mvn4ExecutorRequestBuilder() + .cwd(cwd) + .userHomeDirectory(userHome) + .argument("eu.maveniverse.maven.plugins:toolbox:0.5.2:gav-dump") + .argument("-l") + .argument(logfile) + .build())); + System.out.println(Files.readString(cwd.resolve(logfile))); + } + @Test void defaultFs(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws Exception { layDownFiles(tempDir); @@ -141,11 +178,11 @@ public abstract class MavenExecutorTestSupport { } } - protected ExecutorRequest.Builder mvn3ExecutorRequestBuilder() { + public static ExecutorRequest.Builder mvn3ExecutorRequestBuilder() { return ExecutorRequest.mavenBuilder(Paths.get(System.getProperty("maven3home"))); } - protected ExecutorRequest.Builder mvn4ExecutorRequestBuilder() { + public static ExecutorRequest.Builder mvn4ExecutorRequestBuilder() { return ExecutorRequest.mavenBuilder(Paths.get(System.getProperty("maven4home"))); } diff --git a/impl/maven-executor/src/test/java/org/apache/maven/cling/executor/impl/HelperImplTest.java b/impl/maven-executor/src/test/java/org/apache/maven/cling/executor/impl/HelperImplTest.java new file mode 100644 index 0000000000..452ab530f8 --- /dev/null +++ b/impl/maven-executor/src/test/java/org/apache/maven/cling/executor/impl/HelperImplTest.java @@ -0,0 +1,168 @@ +/* + * 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.impl; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +import org.apache.maven.cling.executor.ExecutorHelper; +import org.apache.maven.cling.executor.embedded.EmbeddedMavenExecutor; +import org.apache.maven.cling.executor.forked.ForkedMavenExecutor; +import org.apache.maven.cling.executor.internal.HelperImpl; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.apache.maven.cling.executor.MavenExecutorTestSupport.mvn3ExecutorRequestBuilder; +import static org.apache.maven.cling.executor.MavenExecutorTestSupport.mvn4ExecutorRequestBuilder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HelperImplTest { + private static final EmbeddedMavenExecutor EMBEDDED_MAVEN_EXECUTOR = new EmbeddedMavenExecutor(); + private static final ForkedMavenExecutor FORKED_MAVEN_EXECUTOR = new ForkedMavenExecutor(); + + @ParameterizedTest + @EnumSource(ExecutorHelper.Mode.class) + void dump3(ExecutorHelper.Mode mode) throws Exception { + ExecutorHelper helper = new HelperImpl( + mode, + mvn3ExecutorRequestBuilder().build().installationDirectory(), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + Map dump = helper.dump(helper.executorRequest()); + assertEquals(System.getProperty("maven3version"), dump.get("maven.version")); + } + + @ParameterizedTest + @EnumSource(ExecutorHelper.Mode.class) + void dump4(ExecutorHelper.Mode mode) throws Exception { + ExecutorHelper helper = new HelperImpl( + mode, + mvn4ExecutorRequestBuilder().build().installationDirectory(), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + Map dump = helper.dump(helper.executorRequest()); + assertEquals(System.getProperty("maven4version"), dump.get("maven.version")); + } + + @ParameterizedTest + @EnumSource(ExecutorHelper.Mode.class) + void version3(ExecutorHelper.Mode mode) { + ExecutorHelper helper = new HelperImpl( + mode, + mvn3ExecutorRequestBuilder().build().installationDirectory(), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + assertEquals(System.getProperty("maven3version"), helper.mavenVersion()); + } + + @ParameterizedTest + @EnumSource(ExecutorHelper.Mode.class) + void version4(ExecutorHelper.Mode mode) { + ExecutorHelper helper = new HelperImpl( + mode, + mvn4ExecutorRequestBuilder().build().installationDirectory(), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + assertEquals(System.getProperty("maven4version"), helper.mavenVersion()); + } + + @ParameterizedTest + @EnumSource(ExecutorHelper.Mode.class) + void localRepository3(ExecutorHelper.Mode mode) { + ExecutorHelper helper = new HelperImpl( + mode, + mvn3ExecutorRequestBuilder().build().installationDirectory(), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + String localRepository = helper.localRepository(helper.executorRequest()); + Path local = Paths.get(localRepository); + assertTrue(Files.isDirectory(local)); + } + + @ParameterizedTest + @EnumSource(ExecutorHelper.Mode.class) + void localRepository4(ExecutorHelper.Mode mode) { + ExecutorHelper helper = new HelperImpl( + mode, + mvn4ExecutorRequestBuilder().build().installationDirectory(), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + String localRepository = helper.localRepository(helper.executorRequest()); + Path local = Paths.get(localRepository); + assertTrue(Files.isDirectory(local)); + } + + @ParameterizedTest + @EnumSource(ExecutorHelper.Mode.class) + void artifactPath3(ExecutorHelper.Mode mode) { + ExecutorHelper helper = new HelperImpl( + mode, + mvn3ExecutorRequestBuilder().build().installationDirectory(), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + String path = helper.artifactPath(helper.executorRequest(), "aopalliance:aopalliance:1.0", "central"); + assertEquals( + "aopalliance" + File.separator + "aopalliance" + File.separator + "1.0" + File.separator + + "aopalliance-1.0.jar", + path); + } + + @ParameterizedTest + @EnumSource(ExecutorHelper.Mode.class) + void artifactPath4(ExecutorHelper.Mode mode) { + ExecutorHelper helper = new HelperImpl( + mode, + mvn4ExecutorRequestBuilder().build().installationDirectory(), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + String path = helper.artifactPath(helper.executorRequest(), "aopalliance:aopalliance:1.0", "central"); + assertEquals( + "aopalliance" + File.separator + "aopalliance" + File.separator + "1.0" + File.separator + + "aopalliance-1.0.jar", + path); + } + + @ParameterizedTest + @EnumSource(ExecutorHelper.Mode.class) + void metadataPath3(ExecutorHelper.Mode mode) { + ExecutorHelper helper = new HelperImpl( + mode, + mvn3ExecutorRequestBuilder().build().installationDirectory(), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + String path = helper.metadataPath(helper.executorRequest(), "aopalliance", "someremote"); + assertEquals("aopalliance" + File.separator + "maven-metadata-someremote.xml", path); + } + + @ParameterizedTest + @EnumSource(ExecutorHelper.Mode.class) + void metadataPath4(ExecutorHelper.Mode mode) { + ExecutorHelper helper = new HelperImpl( + mode, + mvn4ExecutorRequestBuilder().build().installationDirectory(), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + String path = helper.metadataPath(helper.executorRequest(), "aopalliance", "someremote"); + assertEquals("aopalliance" + File.separator + "maven-metadata-someremote.xml", path); + } +} diff --git a/its/core-it-suite/pom.xml b/its/core-it-suite/pom.xml index 361a2d299f..8e79059b38 100644 --- a/its/core-it-suite/pom.xml +++ b/its/core-it-suite/pom.xml @@ -98,17 +98,6 @@ under the License. plexus-utils - - org.apache.maven.shared - maven-verifier - - - - org.apache.maven.shared - maven-shared-utils - 3.4.2 - @@ -526,6 +515,7 @@ under the License. 0 true true + false ${maven.version} ${preparedMavenHome} @@ -571,7 +561,7 @@ under the License. org.apache.maven apache-maven - 4.0.0-beta-6-SNAPSHOT + ${maven-version} bin zip @@ -649,28 +639,6 @@ under the License. - - maven-repo-local-layout - - - maven.repo.local.layout - - - - - - maven-surefire-plugin - - - - ${maven.repo.local.layout} - - - - - - jdk-properties @@ -697,7 +665,6 @@ under the License. false auto - false diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3183LoggingToFileTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3183LoggingToFileTest.java index 0f6f9b1655..6253c01040 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3183LoggingToFileTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3183LoggingToFileTest.java @@ -24,6 +24,7 @@ import java.util.Iterator; import java.util.List; import org.apache.maven.shared.verifier.util.ResourceExtractor; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -34,6 +35,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; * * @author Benjamin Bentmann */ +@Disabled( + "This IT is testing -l, while new Verifier uses same switch to make Maven4 log to file; in short, if that is broken, all ITs would be broken as well") public class MavenITmng3183LoggingToFileTest extends AbstractMavenIntegrationTestCase { public MavenITmng3183LoggingToFileTest() { diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3379ParallelArtifactDownloadsTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3379ParallelArtifactDownloadsTest.java index 31f9ae1b6b..8229daef4a 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3379ParallelArtifactDownloadsTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3379ParallelArtifactDownloadsTest.java @@ -116,15 +116,13 @@ public class MavenITmng3379ParallelArtifactDownloadsTest extends AbstractMavenIn } private void assertMetadata(Verifier verifier, String gid, String aid, String ver, String sha1) throws Exception { - String name = "maven-metadata-maven-core-it.xml"; - File file = new File(verifier.getArtifactMetadataPath(gid, aid, ver, name)); + File file = new File(verifier.getArtifactMetadataPath(gid, aid, ver, "maven-metadata.xml", "maven-core-it")); assertTrue(file.isFile(), file.getAbsolutePath()); assertEquals(sha1, ItUtils.calcHash(file, "SHA-1")); } private void assertMetadata(Verifier verifier, String gid, String aid, String sha1) throws Exception { - String name = "maven-metadata-maven-core-it.xml"; - File file = new File(verifier.getArtifactMetadataPath(gid, aid, null, name)); + File file = new File(verifier.getArtifactMetadataPath(gid, aid, null, "maven-metadata.xml", "maven-core-it")); assertTrue(file.isFile(), file.getAbsolutePath()); assertEquals(sha1, ItUtils.calcHash(file, "SHA-1")); } diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3951AbsolutePathsTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3951AbsolutePathsTest.java index 94bb197717..f210c85bfc 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3951AbsolutePathsTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3951AbsolutePathsTest.java @@ -54,8 +54,7 @@ public class MavenITmng3951AbsolutePathsTest extends AbstractMavenIntegrationTes */ String repoDir = new File(verifier.getLocalRepository()).getAbsolutePath(); if (getRoot(new File(repoDir)).equals(getRoot(testDir))) { - // NOTE: We can only test the local repo if it resides on the same drive as the test - verifier.setLocalRepo(repoDir.substring(repoDir.indexOf(File.separator))); + verifier.addCliArgument("-Dmaven.repo.local=" + repoDir.substring(repoDir.indexOf(File.separator))); } verifier.setAutoclean(false); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3955EffectiveSettingsTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3955EffectiveSettingsTest.java index 8b015cb954..080127126a 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3955EffectiveSettingsTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng3955EffectiveSettingsTest.java @@ -62,7 +62,7 @@ public class MavenITmng3955EffectiveSettingsTest extends AbstractMavenIntegratio assertEquals("true", props.getProperty("settings.offline")); assertEquals("false", props.getProperty("settings.interactiveMode")); assertEquals( - new File(verifier.getLocalRepository()).getAbsoluteFile(), + new File(verifier.getLocalRepositoryWithSettings("settings.xml")).getAbsoluteFile(), new File(props.getProperty("settings.localRepository")).getAbsoluteFile()); } } diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5868NoDuplicateAttachedArtifacts.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5868NoDuplicateAttachedArtifacts.java index 24d8d1ab9d..b04ad20b8f 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5868NoDuplicateAttachedArtifacts.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5868NoDuplicateAttachedArtifacts.java @@ -114,7 +114,6 @@ public class MavenITmng5868NoDuplicateAttachedArtifacts extends AbstractMavenInt verifier.deleteArtifacts("org.apache.maven.its.mng5868"); verifier.addCliArgument("-Dartifact.attachedFile=" + tmp.toFile().getCanonicalPath()); verifier.addCliArgument("-DdeploymentPort=" + port); - verifier.displayStreamBuffers(); verifier.addCliArguments("org.apache.maven.its.plugins:maven-it-plugin-artifact:2.1-SNAPSHOT:attach", "deploy"); verifier.execute(); verifier.verifyErrorFreeLog(); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8106OverlappingDirectoryRolesTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8106OverlappingDirectoryRolesTest.java index 2da18f10c3..9b4f595d1d 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8106OverlappingDirectoryRolesTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8106OverlappingDirectoryRolesTest.java @@ -41,22 +41,22 @@ public class MavenITmng8106OverlappingDirectoryRolesTest extends AbstractMavenIn String tailRepo = System.getProperty("user.home") + File.separator + ".m2" + File.separator + "repository"; Verifier verifier = newVerifier(new File(testDir, "plugin").getAbsolutePath()); - verifier.setLocalRepo(repo); verifier.addCliArgument("-X"); + verifier.addCliArgument("-Dmaven.repo.local=" + repo); verifier.addCliArgument("-Dmaven.repo.local.tail=" + tailRepo); verifier.addCliArgument("install"); verifier.execute(); verifier.verifyErrorFreeLog(); verifier = newVerifier(new File(testDir, "jar").getAbsolutePath()); - verifier.setLocalRepo(repo); verifier.addCliArgument("-X"); + verifier.addCliArgument("-Dmaven.repo.local=" + repo); verifier.addCliArgument("-Dmaven.repo.local.tail=" + tailRepo); verifier.addCliArgument("install"); verifier.execute(); verifier.verifyErrorFreeLog(); - File metadataFile = new File(new File(verifier.getLocalRepository()), "mng-8106/it/maven-metadata-local.xml"); + File metadataFile = new File(new File(repo), "mng-8106/it/maven-metadata-local.xml"); Xpp3Dom dom; try (FileReader reader = new FileReader(metadataFile)) { dom = Xpp3DomBuilder.build(reader); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8181CentralRepoTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8181CentralRepoTest.java index 3aea83f526..d2f968d0dc 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8181CentralRepoTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8181CentralRepoTest.java @@ -51,6 +51,7 @@ public class MavenITmng8181CentralRepoTest extends AbstractMavenIntegrationTestC verifier.addCliArgument("-Dmaven.repo.local.tail=target/null"); verifier.addCliArgument("-Dmaven.repo.central=http://repo1.maven.org/"); verifier.addCliArgument("validate"); + verifier.setHandleLocalRepoTail(false); // we want isolation to have Maven fail due non-HTTPS repo assertThrows(VerificationException.class, verifier::execute); verifier.verifyTextInLog("central (http://repo1.maven.org/, default, releases)"); } diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8331VersionedAndUnversionedDependenciesTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8331VersionedAndUnversionedDependenciesTest.java index ca84a69415..9e1df3247d 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8331VersionedAndUnversionedDependenciesTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8331VersionedAndUnversionedDependenciesTest.java @@ -50,7 +50,8 @@ class MavenITmng8331VersionedAndUnversionedDependenciesTest extends AbstractMave Verifier verifier = new Verifier(testDir.getAbsolutePath()); verifier.setLogFileName("allDependenciesArePresentInTheProject.txt"); - verifier.executeGoal("test-compile"); + verifier.addCliArgument("test-compile"); + verifier.execute(); verifier.verifyErrorFreeLog(); } diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8400CanonicalMavenHomeTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8400CanonicalMavenHomeTest.java index e6cdb18c33..539f3c2ea0 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8400CanonicalMavenHomeTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8400CanonicalMavenHomeTest.java @@ -57,16 +57,13 @@ class MavenITmng8400CanonicalMavenHomeTest extends AbstractMavenIntegrationTestC System.setProperty("maven.home", linkedMavenHome.toString()); Verifier verifier = newVerifier(basedir.toString(), null); - verifier.addCliArgument("--raw-streams"); - verifier.addCliArgument("--quiet"); - verifier.addCliArgument("-DforceStdout"); verifier.addCliArgument("-DasProperties"); + verifier.addCliArgument("-DtoFile=dump.properties"); verifier.addCliArgument("eu.maveniverse.maven.plugins:toolbox:0.5.2:gav-dump"); - // TODO: fork until new entry point CLIng is used - verifier.setForkJvm(true); verifier.execute(); + verifier.verifyErrorFreeLog(); - String dump = verifier.loadLogContent(); + String dump = Files.readString(basedir.resolve("dump.properties"), StandardCharsets.UTF_8); Properties props = new Properties(); props.load(new ByteArrayInputStream(dump.getBytes(StandardCharsets.UTF_8))); diff --git a/its/core-it-support/maven-it-helper/pom.xml b/its/core-it-support/maven-it-helper/pom.xml index d3996ccf43..b3a16bd132 100644 --- a/its/core-it-support/maven-it-helper/pom.xml +++ b/its/core-it-support/maven-it-helper/pom.xml @@ -31,30 +31,40 @@ under the License. Maven IT Helper Library + + org.apache.maven + maven-executor + + 4.0.0-rc-2-SNAPSHOT + + org.apache.maven maven-artifact - 3.0 - - - org.codehaus.plexus - plexus-utils - - + 3.6.3 - org.apache.maven.shared - maven-verifier - - - org.apache.maven.shared - maven-shared-utils - 3.4.2 + org.codehaus.plexus + plexus-utils org.junit.jupiter junit-jupiter + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${maven.home} + + + + + diff --git a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/AbstractMavenIntegrationTestCase.java b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/AbstractMavenIntegrationTestCase.java index 4aa94b23c0..d843b7d73b 100644 --- a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/AbstractMavenIntegrationTestCase.java +++ b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/AbstractMavenIntegrationTestCase.java @@ -252,7 +252,7 @@ public abstract class AbstractMavenIntegrationTestCase { } protected Verifier newVerifier(String basedir, String settings, boolean debug) throws VerificationException { - Verifier verifier = new Verifier(basedir, debug); + Verifier verifier = new Verifier(basedir); verifier.setAutoclean(false); diff --git a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java index b7b81d3bb9..b8939d4dcf 100644 --- a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java +++ b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java @@ -18,58 +18,365 @@ */ package org.apache.maven.it; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FilenameFilter; import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.maven.api.cli.ExecutorException; +import org.apache.maven.api.cli.ExecutorRequest; +import org.apache.maven.cling.executor.ExecutorHelper; +import org.apache.maven.cling.executor.embedded.EmbeddedMavenExecutor; +import org.apache.maven.cling.executor.forked.ForkedMavenExecutor; +import org.apache.maven.cling.executor.internal.HelperImpl; import org.apache.maven.shared.verifier.VerificationException; +import org.codehaus.plexus.util.FileUtils; +import org.codehaus.plexus.util.StringUtils; + +import static java.util.Objects.requireNonNull; + +/** + * + */ +public class Verifier { + /** + * Keep executor alive, as long as Verifier is in classloader. Embedded classloader keeps embedded Maven + * ClassWorld alive, instead to re-create it per invocation, making embedded execution fast(er). + */ + private static final EmbeddedMavenExecutor EMBEDDED_MAVEN_EXECUTOR = new EmbeddedMavenExecutor(); + /** + * Keep executor alive, as long as Verifier is in classloader. For forked this means nothing, but is + * at least "handled the same" as embedded counterpart. Later on, we could have some similar solution like + * mvnd has, and keep pool of "hot" processes maybe? + */ + private static final ForkedMavenExecutor FORKED_MAVEN_EXECUTOR = new ForkedMavenExecutor(); + + /** + * The "preferred" fork mode of Verifier, defaults to "auto". In fact, am unsure is any other fork mode usable, + * maybe "forked" that is slow, but offers maximum isolation? + * + * @see ExecutorHelper.Mode + */ + private static final ExecutorHelper.Mode VERIFIER_FORK_MODE = + ExecutorHelper.Mode.valueOf(System.getProperty("verifier.forkMode", ExecutorHelper.Mode.AUTO.toString()) + .toUpperCase(Locale.ROOT)); + + private static final List DEFAULT_CLI_ARGUMENTS = Arrays.asList("--errors", "--batch-mode"); + + private static final String AUTO_CLEAN_ARGUMENT = "org.apache.maven.plugins:maven-clean-plugin:clean"; + + private final ExecutorHelper executorHelper; + + private final Path basedir; // the basedir of IT + + private final Path tempBasedir; // empty basedir for queries + + private final Path userHomeDirectory; + + private final List defaultCliArguments; + + private final Properties systemProperties = new Properties(); + + private final Map environmentVariables = new HashMap<>(); + + private final List cliArguments = new ArrayList<>(); + + private boolean autoClean = true; + + private boolean forkJvm = false; + + private boolean handleLocalRepoTail = true; + + private String logFileName = "log.txt"; + + private Path logFile; -public class Verifier extends org.apache.maven.shared.verifier.Verifier { public Verifier(String basedir) throws VerificationException { - this(basedir, false); + this(basedir, null); } - public Verifier(String basedir, boolean debug) throws VerificationException { - super(basedir, null, debug, defaultCliArguments()); + /** + * Creates verifier instance using passed in basedir as "cwd" and passed in default CLI arguments (if not null). + * The discovery of user home and Maven installation directory is performed as well. + * + * @param basedir The basedir, cannot be {@code null} + * @param defaultCliArguments The defaultCliArguments override, may be {@code null} + * + * @see #DEFAULT_CLI_ARGUMENTS + */ + public Verifier(String basedir, List defaultCliArguments) throws VerificationException { + requireNonNull(basedir); + try { + this.basedir = Paths.get(basedir).toAbsolutePath(); + this.tempBasedir = Files.createTempDirectory("verifier"); + this.userHomeDirectory = Paths.get(System.getProperty("user.home")); + this.executorHelper = new HelperImpl( + VERIFIER_FORK_MODE, + Paths.get(System.getProperty("maven.home")), + EMBEDDED_MAVEN_EXECUTOR, + FORKED_MAVEN_EXECUTOR); + this.defaultCliArguments = + new ArrayList<>(defaultCliArguments != null ? defaultCliArguments : DEFAULT_CLI_ARGUMENTS); + this.logFile = this.basedir.resolve(logFileName); + } catch (IOException e) { + throw new VerificationException("Could not create verifier", e); + } } - static String[] defaultCliArguments() { - return new String[] { - "-e", "--batch-mode", "-Dmaven.repo.local.tail=" + System.getProperty("maven.repo.local.tail") - }; + public String getExecutable() { + return ExecutorRequest.MVN; } - public String loadLogContent() throws IOException { - return Files.readString(Paths.get(getBasedir(), getLogFileName())); + public void execute() throws VerificationException { + List args = new ArrayList<>(defaultCliArguments); + for (String cliArgument : cliArguments) { + args.add(cliArgument.replace("${basedir}", getBasedir())); + } + + if (handleLocalRepoTail) { + // note: all used Strings are non-null/empty if "not present" for simpler handling + // "outer" build pass these in, check are they present or not + String outerTail = System.getProperty("maven.repo.local.tail", "").trim(); + String outerHead = System.getProperty("maven.repo.local", "").trim(); + + // if none of the outer thing is set, we have nothing to do + if (!outerTail.isEmpty() || !outerHead.isEmpty()) { + String itTail = args.stream() + .filter(s -> s.startsWith("-Dmaven.repo.local.tail=")) + .map(s -> s.substring(24).trim()) + .findFirst() + .orElse(""); + if (!itTail.isEmpty()) { + // remove it + args = args.stream() + .filter(s -> !s.startsWith("-Dmaven.repo.local.tail=")) + .collect(Collectors.toList()); + } + String itHead = args.stream() + .filter(s -> s.startsWith("-Dmaven.repo.local=")) + .map(s -> s.substring(19).trim()) + .findFirst() + .orElse(""); + if (!itHead.isEmpty()) { + // remove it + args = args.stream() + .filter(s -> !s.startsWith("-Dmaven.repo.local=")) + .collect(Collectors.toList()); + } + + if (!itHead.isEmpty()) { + // itHead present: itHead left as is, push all to itTail + args.add("-Dmaven.repo.local=" + itHead); + itTail = Stream.of(itTail, outerHead, outerTail) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(",")); + if (!itTail.isEmpty()) { + args.add("-Dmaven.repo.local.tail=" + itTail); + } + } else { + // itHead not present: if outerHead present, make it itHead; join itTail and outerTail as tail + if (!outerHead.isEmpty()) { + args.add("-Dmaven.repo.local=" + outerHead); + } + itTail = Stream.of(itTail, outerTail) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(",")); + if (!itTail.isEmpty()) { + args.add("-Dmaven.repo.local.tail=" + itTail); + } + } + } + } + + // make sure these are first + if (autoClean) { + args.add(0, AUTO_CLEAN_ARGUMENT); + } + if (logFileName != null) { + args.add(0, logFileName); + args.add(0, "-l"); + } + + try { + ExecutorRequest.Builder builder = executorHelper + .executorRequest() + .cwd(basedir) + .userHomeDirectory(userHomeDirectory) + .arguments(args); + if (!systemProperties.isEmpty()) { + builder.jvmSystemProperties(new HashMap(systemProperties)); + } + if (!environmentVariables.isEmpty()) { + builder.environmentVariables(environmentVariables); + } + + ExecutorHelper.Mode mode = executorHelper.getDefaultMode(); + if (forkJvm) { + mode = ExecutorHelper.Mode.FORKED; + } + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + ExecutorRequest request = + builder.stdoutConsumer(stdout).stderrConsumer(stderr).build(); + int ret = executorHelper.execute(mode, request); + if (ret > 0) { + String dump; + try { + dump = executorHelper.dump(request.toBuilder()).toString(); + } catch (Exception e) { + dump = "FAILED: " + e.getMessage(); + } + throw new VerificationException("Exit code was non-zero: " + ret + "; command line and log = \n" + + getExecutable() + " " + + "\nstdout: " + stdout + + "\nstderr: " + stderr + + "\nreq: " + request + + "\ndump: " + dump + + "\n" + getLogContents(logFile)); + } + } catch (ExecutorException e) { + throw new VerificationException("Failed to execute Maven", e); + } } - public List loadLogLines() throws IOException { - return loadLines(getLogFileName()); + public String getMavenVersion() throws VerificationException { + return executorHelper.mavenVersion(); } - public List loadLines(String filename) throws IOException { - return loadLines(filename, null); + /** + * Add a command line argument, each argument must be set separately one by one. + *

+ * ${basedir} in argument will be replaced by value of {@link #getBasedir()} during execution. + * + * @param cliArgument an argument to add + */ + public void addCliArgument(String cliArgument) { + cliArguments.add(cliArgument); } - @Override - public List loadFile(String basedir, String filename, boolean hasCommand) throws VerificationException { - return super.loadFile(basedir, filename, hasCommand); + /** + * Add a command line arguments, each argument must be set separately one by one. + *

+ * ${basedir} in argument will be replaced by value of {@link #getBasedir()} during execution. + * + * @param cliArguments an arguments list to add + */ + public void addCliArguments(String... cliArguments) { + Collections.addAll(this.cliArguments, cliArguments); } - @Override - public List loadFile(File file, boolean hasCommand) throws VerificationException { - return super.loadFile(file, hasCommand); + public Properties getSystemProperties() { + return systemProperties; } - public File filterFile(String srcPath, String dstPath) throws IOException { - return filterFile(srcPath, dstPath, (String) null); + public void setEnvironmentVariable(String key, String value) { + if (value != null) { + environmentVariables.put(key, value); + } else { + environmentVariables.remove(key); + } } - public File filterFile(String srcPath, String dstPath, Map filterMap) throws IOException { - return super.filterFile(srcPath, dstPath, null, filterMap); + public String getBasedir() { + return basedir.toString(); + } + + public void setLogFileName(String logFileName) { + if (logFileName == null || logFileName.isEmpty()) { + throw new IllegalArgumentException("log file name unspecified"); + } + this.logFileName = logFileName; + this.logFile = this.basedir.resolve(this.logFileName); + } + + public void setAutoclean(boolean autoClean) { + this.autoClean = autoClean; + } + + public void setForkJvm(boolean forkJvm) { + this.forkJvm = forkJvm; + } + + public void setHandleLocalRepoTail(boolean handleLocalRepoTail) { + this.handleLocalRepoTail = handleLocalRepoTail; + } + + public String getLocalRepository() { + return getLocalRepositoryWithSettings(null); + } + + public String getLocalRepositoryWithSettings(String settingsXml) { + String outerHead = System.getProperty("maven.repo.local", "").trim(); + if (!outerHead.isEmpty()) { + return outerHead; + } else if (settingsXml != null) { + // when invoked with settings.xml, the file must be resolved from basedir (as Maven does) + // but we should not use basedir, as it may contain extensions.xml or a project, that Maven will eagerly + // load, and may fail, as it would need more (like CI friendly versioning, etc). + // if given, it must exist + Path settingsFile = basedir.resolve(settingsXml).toAbsolutePath().normalize(); + if (!Files.isRegularFile(settingsFile)) { + throw new IllegalArgumentException("settings xml does not exist: " + settingsXml); + } + return executorHelper.localRepository(executorHelper + .executorRequest() + .cwd(tempBasedir) + .userHomeDirectory(userHomeDirectory) + .argument("-s") + .argument(settingsFile.toString())); + } else { + return executorHelper.localRepository( + executorHelper.executorRequest().cwd(tempBasedir).userHomeDirectory(userHomeDirectory)); + } + } + + private String getLogContents(Path logFile) { + try { + return Files.readString(logFile); + } catch (IOException e) { + return "(Error reading log contents: " + e.getMessage() + ")"; + } + } + + public String getLogFileName() { + return logFileName; + } + + public Path getLogFile() { + return logFile; + } + + public void verifyErrorFreeLog() throws VerificationException { + List lines = loadFile(logFile.toFile(), false); + + for (String line : lines) { + // A hack to keep stupid velocity resource loader errors from triggering failure + if (stripAnsi(line).contains("[ERROR]") && !isVelocityError(line)) { + throw new VerificationException("Error in execution: " + line); + } + } } /** @@ -82,10 +389,6 @@ public class Verifier extends org.apache.maven.shared.verifier.Verifier { verifyTextNotInLog(loadLogLines(), text); } - public long textOccurrencesInLog(String text) throws IOException { - return textOccurencesInLog(loadLogLines(), text); - } - public static void verifyTextNotInLog(List lines, String text) throws VerificationException { if (textOccurencesInLog(lines, text) > 0) { throw new VerificationException("Text found in log: " + text); @@ -98,11 +401,625 @@ public class Verifier extends org.apache.maven.shared.verifier.Verifier { } } + public long textOccurrencesInLog(String text) throws IOException { + return textOccurencesInLog(loadLogLines(), text); + } + public static long textOccurencesInLog(List lines, String text) { return lines.stream().filter(line -> stripAnsi(line).contains(text)).count(); } - public void execute() throws VerificationException { - super.execute(); + /** + * Checks whether the specified line is just an error message from Velocity. Especially old versions of Doxia employ + * a very noisy Velocity instance. + * + * @param line The log line to check, must not be null. + * @return true if the line appears to be a Velocity error, false otherwise. + */ + private static boolean isVelocityError(String line) { + return line.contains("VM_global_library.vm") || line.contains("VM #") && line.contains("macro"); + } + + /** + * Throws an exception if the text is not present in the log. + * + * @param text the text to assert present + * @throws VerificationException if text is not found in log + */ + public void verifyTextInLog(String text) throws VerificationException { + List lines = loadFile(logFile.toFile(), false); + + boolean result = false; + for (String line : lines) { + if (stripAnsi(line).contains(text)) { + result = true; + break; + } + } + if (!result) { + throw new VerificationException("Text not found in log: " + text); + } + } + + public static String stripAnsi(String msg) { + return msg.replaceAll("\u001B\\[[;\\d]*[ -/]*[@-~]", ""); + } + + public Properties loadProperties(String filename) throws VerificationException { + Properties properties = new Properties(); + + File propertiesFile = new File(getBasedir(), filename); + try (FileInputStream fis = new FileInputStream(propertiesFile)) { + properties.load(fis); + } catch (IOException e) { + throw new VerificationException("Error reading properties file", e); + } + + return properties; + } + + /** + * Loads the (non-empty) lines of the specified text file. + * + * @param filename The path to the text file to load, relative to the base directory, must not be null. + * @param encoding The character encoding of the file, may be null or empty to use the platform default + * encoding. + * @return The list of (non-empty) lines from the text file, can be empty but never null. + * @throws IOException If the file could not be loaded. + * @since 1.2 + */ + public List loadLines(String filename, String encoding) throws IOException { + List lines = new ArrayList<>(); + try (BufferedReader reader = getReader(filename, encoding)) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.isEmpty()) { + lines.add(line); + } + } + } + return lines; + } + + private BufferedReader getReader(String filename, String encoding) throws IOException { + File file = new File(getBasedir(), filename); + if (encoding != null && !encoding.isEmpty()) { + return Files.newBufferedReader(file.toPath(), Charset.forName(encoding)); + } else { + return Files.newBufferedReader(file.toPath()); + } + } + + public List loadFile(String basedir, String filename, boolean hasCommand) throws VerificationException { + return loadFile(new File(basedir, filename), hasCommand); + } + + public List loadFile(File file, boolean hasCommand) throws VerificationException { + List lines = new ArrayList<>(); + + if (file.exists()) { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line = reader.readLine(); + + while (line != null) { + line = line.trim(); + + if (!line.startsWith("#") && !line.isEmpty()) { + lines.addAll(replaceArtifacts(line, hasCommand)); + } + line = reader.readLine(); + } + } catch (IOException e) { + throw new VerificationException(e); + } + } + + return lines; + } + + public String loadLogContent() throws IOException { + return Files.readString(getLogFile()); + } + + public List loadLogLines() throws IOException { + return loadLines(getLogFileName()); + } + + public List loadLines(String filename) throws IOException { + return loadLines(filename, null); + } + + private static final String MARKER = "${artifact:"; + + private List replaceArtifacts(String line, boolean hasCommand) { + int index = line.indexOf(MARKER); + if (index >= 0) { + String newLine = line.substring(0, index); + index = line.indexOf("}", index); + if (index < 0) { + throw new IllegalArgumentException("line does not contain ending artifact marker: '" + line + "'"); + } + String artifact = line.substring(newLine.length() + MARKER.length(), index); + + newLine += getArtifactPath(artifact); + newLine += line.substring(index + 1); + + List l = new ArrayList<>(); + l.add(newLine); + + int endIndex = newLine.lastIndexOf('/'); + + String command = null; + String filespec; + if (hasCommand) { + int startIndex = newLine.indexOf(' '); + + command = newLine.substring(0, startIndex); + + filespec = newLine.substring(startIndex + 1, endIndex); + } else { + filespec = newLine; + } + + File dir = new File(filespec); + addMetadataToList(dir, hasCommand, l, command); + addMetadataToList(dir.getParentFile(), hasCommand, l, command); + + return l; + } else { + return Collections.singletonList(line); + } + } + + private static void addMetadataToList(File dir, boolean hasCommand, List l, String command) { + if (dir.exists() && dir.isDirectory()) { + String[] files = dir.list(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.startsWith("maven-metadata") && name.endsWith(".xml"); + } + }); + + for (String file : files) { + if (hasCommand) { + l.add(command + " " + new File(dir, file).getPath()); + } else { + l.add(new File(dir, file).getPath()); + } + } + } + } + + private String getArtifactPath(String artifact) { + StringTokenizer tok = new StringTokenizer(artifact, ":"); + if (tok.countTokens() != 4) { + throw new IllegalArgumentException("Artifact must have 4 tokens: '" + artifact + "'"); + } + + String[] a = new String[4]; + for (int i = 0; i < 4; i++) { + a[i] = tok.nextToken(); + } + + String groupId = a[0]; + String artifactId = a[1]; + String version = a[2]; + String ext = a[3]; + return getArtifactPath(groupId, artifactId, version, ext); + } + + public String getArtifactPath(String groupId, String artifactId, String version, String ext) { + return getArtifactPath(groupId, artifactId, version, ext, null); + } + + /** + * Returns the absolute path to the artifact denoted by groupId, artifactId, version, extension and classifier. + * + * @param gid The groupId, must not be null. + * @param aid The artifactId, must not be null. + * @param version The version, must not be null. + * @param ext The extension, must not be null. + * @param classifier The classifier, may be null to be omitted. + * @return the absolute path to the artifact denoted by groupId, artifactId, version, extension and classifier, + * never null. + */ + public String getArtifactPath(String gid, String aid, String version, String ext, String classifier) { + if (classifier != null && classifier.isEmpty()) { + classifier = null; + } + if ("maven-plugin".equals(ext)) { + ext = "jar"; + } else if ("coreit-artifact".equals(ext)) { + ext = "jar"; + classifier = "it"; + } else if ("test-jar".equals(ext)) { + ext = "jar"; + classifier = "tests"; + } + + String gav; + if (classifier != null) { + gav = gid + ":" + aid + ":" + ext + ":" + classifier + ":" + version; + } else { + gav = gid + ":" + aid + ":" + ext + ":" + version; + } + return getLocalRepository() + + File.separator + + executorHelper.artifactPath(executorHelper.executorRequest(), gav, null); + } + + public List getArtifactFileNameList(String org, String name, String version, String ext) { + List files = new ArrayList<>(); + String artifactPath = getArtifactPath(org, name, version, ext); + File dir = new File(artifactPath); + files.add(artifactPath); + addMetadataToList(dir, false, files, null); + addMetadataToList(dir.getParentFile(), false, files, null); + return files; + } + + /** + * Gets the path to the local artifact metadata. Note that the method does not check whether the returned path + * actually points to existing metadata. + * + * @param gid The group id, must not be null. + * @param aid The artifact id, must not be null. + * @param version The artifact version, may be null. + * @return The (absolute) path to the local artifact metadata, never null. + */ + public String getArtifactMetadataPath(String gid, String aid, String version) { + return getArtifactMetadataPath(gid, aid, version, "maven-metadata.xml"); + } + + /** + * Gets the path to a file in the local artifact directory. Note that the method does not check whether the returned + * path actually points to an existing file. + * + * @param gid The group id, must not be null. + * @param aid The artifact id, may be null. + * @param version The artifact version, may be null. + * @param filename The filename to use, must not be null. + * @return The (absolute) path to the local artifact metadata, never null. + */ + public String getArtifactMetadataPath(String gid, String aid, String version, String filename) { + return getArtifactMetadataPath(gid, aid, version, filename, null); + } + + /** + * Gets the path to a file in the local artifact directory. Note that the method does not check whether the returned + * path actually points to an existing file. + * + * @param gid The group id, must not be null. + * @param aid The artifact id, may be null. + * @param version The artifact version, may be null. + * @param filename The filename to use, must not be null. + * @param repoId The remote repository ID from where metadata originate, may be null. + * @return The (absolute) path to the local artifact metadata, never null. + */ + public String getArtifactMetadataPath(String gid, String aid, String version, String filename, String repoId) { + String gav; + if (gid != null) { + gav = gid + ":"; + } else { + gav = ":"; + } + if (aid != null) { + gav += aid + ":"; + } else { + gav += ":"; + } + if (version != null) { + gav += version + ":"; + } else { + gav += ":"; + } + gav += filename; + return getLocalRepository() + + File.separator + + executorHelper.metadataPath(executorHelper.executorRequest(), gav, repoId); + } + + /** + * Gets the path to the local artifact metadata. Note that the method does not check whether the returned path + * actually points to existing metadata. + * + * @param gid The group id, must not be null. + * @param aid The artifact id, must not be null. + * @return The (absolute) path to the local artifact metadata, never null. + */ + public String getArtifactMetadataPath(String gid, String aid) { + return getArtifactMetadataPath(gid, aid, null); + } + + public void deleteArtifact(String org, String name, String version, String ext) throws IOException { + List files = getArtifactFileNameList(org, name, version, ext); + for (String fileName : files) { + FileUtils.forceDelete(new File(fileName)); + } + } + + /** + * Deletes all artifacts in the specified group id from the local repository. + * + * @param gid The group id whose artifacts should be deleted, must not be null. + * @throws IOException If the artifacts could not be deleted. + * @since 1.2 + */ + public void deleteArtifacts(String gid) throws IOException { + String mdPath = executorHelper.metadataPath(executorHelper.executorRequest(), gid, null); + Path dir = Paths.get(getLocalRepository()).resolve(mdPath).getParent(); + FileUtils.deleteDirectory(dir.toFile()); + } + + /** + * Deletes all artifacts in the specified g:a:v from the local repository. + * + * @param gid The group id whose artifacts should be deleted, must not be null. + * @param aid The artifact id whose artifacts should be deleted, must not be null. + * @param version The (base) version whose artifacts should be deleted, must not be null. + * @throws IOException If the artifacts could not be deleted. + * @since 1.3 + */ + public void deleteArtifacts(String gid, String aid, String version) throws IOException { + requireNonNull(gid, "gid is null"); + requireNonNull(aid, "aid is null"); + requireNonNull(version, "version is null"); + + String mdPath = + executorHelper.metadataPath(executorHelper.executorRequest(), gid + ":" + aid + ":" + version, null); + Path dir = Paths.get(getLocalRepository()).resolve(mdPath).getParent(); + FileUtils.deleteDirectory(dir.toFile()); + } + + /** + * Deletes the specified directory. + * + * @param path The path to the directory to delete, relative to the base directory, must not be null. + * @throws IOException If the directory could not be deleted. + * @since 1.2 + */ + public void deleteDirectory(String path) throws IOException { + FileUtils.deleteDirectory(new File(getBasedir(), path)); + } + + public File filterFile(String srcPath, String dstPath) throws IOException { + return filterFile(srcPath, dstPath, (String) null); + } + + public File filterFile(String srcPath, String dstPath, Map filterMap) throws IOException { + return filterFile(srcPath, dstPath, null, filterMap); + } + + /** + * Filters a text file by replacing some user-defined tokens. + * This method is equivalent to: + * + *

+     *     filterFile( srcPath, dstPath, fileEncoding, verifier.newDefaultFilterMap() )
+     * 
+ * + * @param srcPath The path to the input file, relative to the base directory, must not be + * null. + * @param dstPath The path to the output file, relative to the base directory and possibly equal to the + * input file, must not be null. + * @param fileEncoding The file encoding to use, may be null or empty to use the platform's default + * encoding. + * @return The path to the filtered output file, never null. + * @throws IOException If the file could not be filtered. + * @since 2.0 + */ + public File filterFile(String srcPath, String dstPath, String fileEncoding) throws IOException { + return filterFile(srcPath, dstPath, fileEncoding, newDefaultFilterMap()); + } + + /** + * Filters a text file by replacing some user-defined tokens. + * + * @param srcPath The path to the input file, relative to the base directory, must not be + * null. + * @param dstPath The path to the output file, relative to the base directory and possibly equal to the + * input file, must not be null. + * @param fileEncoding The file encoding to use, may be null or empty to use the platform's default + * encoding. + * @param filterMap The mapping from tokens to replacement values, must not be null. + * @return The path to the filtered output file, never null. + * @throws IOException If the file could not be filtered. + * @since 1.2 + */ + public File filterFile(String srcPath, String dstPath, String fileEncoding, Map filterMap) + throws IOException { + Charset charset = fileEncoding != null ? Charset.forName(fileEncoding) : StandardCharsets.UTF_8; + File srcFile = new File(getBasedir(), srcPath); + String data = Files.readString(srcFile.toPath(), charset); + + for (Map.Entry entry : filterMap.entrySet()) { + data = StringUtils.replace(data, entry.getKey(), entry.getValue()); + } + + File dstFile = new File(getBasedir(), dstPath); + //noinspection ResultOfMethodCallIgnored + dstFile.getParentFile().mkdirs(); + Files.writeString(dstFile.toPath(), data, charset); + + return dstFile; + } + + /** + * Gets a new copy of the default filter map. These default filter map, contains the tokens "@basedir@" and + * "@baseurl@" to the test's base directory and its base file: URL, respectively. + * + * @return The (modifiable) map with the default filter map, never null. + * @since 2.0 + */ + public Map newDefaultFilterMap() { + Map filterMap = new HashMap<>(); + + Path basedir = Paths.get(getBasedir()).toAbsolutePath(); + filterMap.put("@basedir@", basedir.toString()); + filterMap.put("@baseurl@", basedir.toUri().toASCIIString()); + + return filterMap; + } + + /** + * Verifies that the given file exists. + * + * @param file the path of the file to check + * @throws VerificationException in case the given file does not exist + */ + public void verifyFilePresent(String file) throws VerificationException { + verifyFilePresence(file, true); + } + + /** + * Verifies that the given file does not exist. + * + * @param file the path of the file to check + * @throws VerificationException if the given file exists + */ + public void verifyFileNotPresent(String file) throws VerificationException { + verifyFilePresence(file, false); + } + + private void verifyArtifactPresence(boolean wanted, String groupId, String artifactId, String version, String ext) + throws VerificationException { + List files = getArtifactFileNameList(groupId, artifactId, version, ext); + for (String fileName : files) { + verifyFilePresence(fileName, wanted); + } + } + + /** + * Verifies that the artifact given through its Maven coordinates exists. + * + * @param groupId the groupId of the artifact (must not be null) + * @param artifactId the artifactId of the artifact (must not be null) + * @param version the version of the artifact (must not be null) + * @param ext the extension of the artifact (must not be null) + * @throws VerificationException if the given artifact does not exist + */ + public void verifyArtifactPresent(String groupId, String artifactId, String version, String ext) + throws VerificationException { + verifyArtifactPresence(true, groupId, artifactId, version, ext); + } + + /** + * Verifies that the artifact given through its Maven coordinates does not exist. + * + * @param groupId the groupId of the artifact (must not be null) + * @param artifactId the artifactId of the artifact (must not be null) + * @param version the version of the artifact (must not be null) + * @param ext the extension of the artifact (must not be null) + * @throws VerificationException if the given artifact exists + */ + public void verifyArtifactNotPresent(String groupId, String artifactId, String version, String ext) + throws VerificationException { + verifyArtifactPresence(false, groupId, artifactId, version, ext); + } + + private void verifyFilePresence(String filePath, boolean wanted) throws VerificationException { + if (filePath.contains("!/")) { + Path basedir = Paths.get(getBasedir()).toAbsolutePath(); + String urlString = "jar:" + basedir.toUri().toASCIIString() + "/" + filePath; + + InputStream is = null; + try { + URL url = new URL(urlString); + + is = url.openStream(); + + if (is == null) { + if (wanted) { + throw new VerificationException("Expected JAR resource was not found: " + filePath); + } + } else { + if (!wanted) { + throw new VerificationException("Unwanted JAR resource was found: " + filePath); + } + } + } catch (MalformedURLException e) { + throw new VerificationException("Error looking for JAR resource", e); + } catch (IOException e) { + if (wanted) { + throw new VerificationException("Error looking for JAR resource: " + filePath); + } + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // ignore + } + } + } + } else { + File expectedFile = new File(filePath); + + // NOTE: On Windows, a path with a leading (back-)slash is relative to the current drive + if (!expectedFile.isAbsolute() && !expectedFile.getPath().startsWith(File.separator)) { + expectedFile = new File(getBasedir(), filePath); + } + + if (filePath.indexOf('*') > -1) { + File parent = expectedFile.getParentFile(); + + if (!parent.exists()) { + if (wanted) { + throw new VerificationException( + "Expected file pattern was not found: " + expectedFile.getPath()); + } + } else { + String shortNamePattern = expectedFile.getName().replaceAll("\\*", ".*"); + + String[] candidates = parent.list(); + + boolean found = false; + + if (candidates != null) { + for (String candidate : candidates) { + if (candidate.matches(shortNamePattern)) { + found = true; + break; + } + } + } + + if (!found && wanted) { + throw new VerificationException( + "Expected file pattern was not found: " + expectedFile.getPath()); + } else if (found && !wanted) { + throw new VerificationException("Unwanted file pattern was found: " + expectedFile.getPath()); + } + } + } else { + if (!expectedFile.exists()) { + if (wanted) { + throw new VerificationException("Expected file was not found: " + expectedFile.getPath()); + } + } else { + if (!wanted) { + throw new VerificationException("Unwanted file was found: " + expectedFile.getPath()); + } + } + } + } + } + + /** + * Verifies that the artifact given by its Maven coordinates exists and contains the given content. + * + * @param groupId the groupId of the artifact (must not be null) + * @param artifactId the artifactId of the artifact (must not be null) + * @param version the version of the artifact (must not be null) + * @param ext the extension of the artifact (must not be null) + * @param content the expected content + * @throws IOException if reading from the artifact fails + * @throws VerificationException if the content of the artifact differs + */ + public void verifyArtifactContent(String groupId, String artifactId, String version, String ext, String content) + throws IOException, VerificationException { + String fileName = getArtifactPath(groupId, artifactId, version, ext); + if (!content.equals(FileUtils.fileRead(fileName))) { + throw new VerificationException("Content of " + fileName + " does not equal " + content); + } } } diff --git a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/AnsiSupport.java b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/shared/verifier/VerificationException.java similarity index 58% rename from its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/AnsiSupport.java rename to its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/shared/verifier/VerificationException.java index 27de46ba99..628981b73f 100644 --- a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/AnsiSupport.java +++ b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/shared/verifier/VerificationException.java @@ -16,29 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.it; +package org.apache.maven.shared.verifier; /** - * Basic Ansi support: can't use Ansi because IT is executed in separate classloader. + * @author Jason van Zyl */ -class AnsiSupport { - private static final String ESC = String.valueOf((char) 27) + '['; - - private static final String NORMAL = ESC + "0;39m"; - - static String success(String msg) { - return ESC + "1;32m" + msg + NORMAL; +public class VerificationException extends Exception { + public VerificationException(String message) { + super(message); } - static String warning(String msg) { - return ESC + "1;33m" + msg + NORMAL; + public VerificationException(Throwable cause) { + super(cause); } - static String error(String msg) { - return ESC + "1;31m" + msg + NORMAL; - } - - static String bold(String msg) { - return ESC + "1m" + msg + NORMAL; + public VerificationException(String message, Throwable cause) { + super(message, cause); } } diff --git a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/shared/verifier/util/ResourceExtractor.java b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/shared/verifier/util/ResourceExtractor.java new file mode 100644 index 0000000000..2172b9439f --- /dev/null +++ b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/shared/verifier/util/ResourceExtractor.java @@ -0,0 +1,140 @@ +/* + * 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.shared.verifier.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.codehaus.plexus.util.FileUtils; + +/** + * TODO this can be replaced with plexus-archiver + */ +public class ResourceExtractor { + + public static File simpleExtractResources(Class cl, String resourcePath) throws IOException { + String tempDirPath = System.getProperty("maven.test.tmpdir", System.getProperty("java.io.tmpdir")); + File tempDir = new File(tempDirPath); + + File testDir = new File(tempDir, resourcePath); + + FileUtils.deleteDirectory(testDir); + + testDir = extractResourcePath(cl, resourcePath, tempDir, false); + return testDir; + } + + public static File extractResourcePath(String resourcePath, File dest) throws IOException { + return extractResourcePath(ResourceExtractor.class, resourcePath, dest); + } + + public static File extractResourcePath(Class cl, String resourcePath, File dest) throws IOException { + return extractResourcePath(cl, resourcePath, dest, false); + } + + public static File extractResourcePath(Class cl, String resourcePath, File tempDir, boolean alwaysExtract) + throws IOException { + File dest = new File(tempDir, resourcePath); + return extractResourceToDestination(cl, resourcePath, dest, alwaysExtract); + } + + public static File extractResourceToDestination( + Class cl, String resourcePath, File destination, boolean alwaysExtract) throws IOException { + URL url = cl.getResource(resourcePath); + if (url == null) { + throw new IllegalArgumentException("Resource not found: " + resourcePath); + } + if ("jar".equalsIgnoreCase(url.getProtocol())) { + File jarFile = getJarFileFromUrl(url); + extractResourcePathFromJar(cl, jarFile, resourcePath, destination); + } else { + try { + File resourceFile = new File(new URI(url.toExternalForm())); + if (!alwaysExtract) { + return resourceFile; + } + if (resourceFile.isDirectory()) { + FileUtils.copyDirectoryStructure(resourceFile, destination); + } else { + FileUtils.copyFile(resourceFile, destination); + } + } catch (URISyntaxException e) { + throw new RuntimeException("Couldn't convert URL to File:" + url, e); + } + } + return destination; + } + + private static void extractResourcePathFromJar(Class cl, File jarFile, String resourcePath, File dest) + throws IOException { + ZipFile z = new ZipFile(jarFile, ZipFile.OPEN_READ); + String zipStyleResourcePath = resourcePath.substring(1) + "/"; + ZipEntry ze = z.getEntry(zipStyleResourcePath); + if (ze != null) { + // DGF If it's a directory, then we need to look at all the entries + for (Enumeration entries = z.entries(); entries.hasMoreElements(); ) { + ze = entries.nextElement(); + if (ze.getName().startsWith(zipStyleResourcePath)) { + String relativePath = ze.getName().substring(zipStyleResourcePath.length()); + File destFile = new File(dest, relativePath); + if (ze.isDirectory()) { + destFile.mkdirs(); + } else { + try (FileOutputStream fos = new FileOutputStream(destFile)) { + z.getInputStream(ze).transferTo(fos); + } finally { + z.close(); + } + } + } + } + } else { + try (FileOutputStream fos = new FileOutputStream(dest)) { + cl.getResourceAsStream(resourcePath).transferTo(fos); + } finally { + z.close(); + } + } + } + + private static File getJarFileFromUrl(URL url) { + if (!"jar".equalsIgnoreCase(url.getProtocol())) { + throw new IllegalArgumentException("This is not a Jar URL:" + url.toString()); + } + String resourceFilePath = url.getFile(); + int index = resourceFilePath.indexOf("!"); + if (index == -1) { + throw new RuntimeException("Bug! " + url.toExternalForm() + " does not have a '!'"); + } + String jarFileURI = resourceFilePath.substring(0, index); + try { + File jarFile = new File(new URI(jarFileURI)); + return jarFile; + } catch (URISyntaxException e) { + throw new RuntimeException("Bug! URI failed to parse: " + jarFileURI, e); + } + } +} diff --git a/its/pom.xml b/its/pom.xml index fb242752f0..ae5cbdd8a4 100644 --- a/its/pom.xml +++ b/its/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.maven maven - 4.0.0-beta-6-SNAPSHOT + 4.0.0-rc-2-SNAPSHOT org.apache.maven.its @@ -73,7 +73,7 @@ under the License. - 4.0.0-beta-6-SNAPSHOT + 4.0.0-rc-2-SNAPSHOT 3.15.1