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