diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/ClientLayout.java b/client/src/main/java/org/jboss/fuse/mvnd/client/ClientLayout.java index 3cbae145..070ad3fc 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/ClientLayout.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/ClientLayout.java @@ -1,10 +1,10 @@ package org.jboss.fuse.mvnd.client; -import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Objects; import java.util.Properties; +import java.util.function.Supplier; /** * Local paths relevant for the {@link DefaultClient}. @@ -19,31 +19,32 @@ public class ClientLayout extends Layout { public static ClientLayout getEnvInstance() { if (ENV_INSTANCE == null) { - final Properties mvndProperties = loadMvndProperties(); + final Path mvndPropertiesPath = Environment.findMvndPropertiesPath(); + final Supplier mvndProperties = lazyMvndProperties(mvndPropertiesPath); final Path pwd = Paths.get(".").toAbsolutePath().normalize(); - - final Path mvndHome = findMavenHome(mvndProperties); + final Path mvndHome = Environment.findMavenHome(mvndProperties, mvndPropertiesPath); ENV_INSTANCE = new ClientLayout( + mvndPropertiesPath, mvndHome, pwd, - findMultiModuleProjectDirectory(pwd), - findJavaHome(mvndProperties), + Environment.findMultiModuleProjectDirectory(pwd), + Environment.findJavaHome(mvndProperties, mvndPropertiesPath), findLocalRepo(), null); } return ENV_INSTANCE; } - public ClientLayout(Path mavenHome, Path userDir, Path multiModuleProjectDirectory, Path javaHome, + public ClientLayout(Path mvndPropertiesPath, Path mavenHome, Path userDir, Path multiModuleProjectDirectory, Path javaHome, Path localMavenRepository, Path settings) { - super(mavenHome, userDir, multiModuleProjectDirectory); + super(mvndPropertiesPath, mavenHome, userDir, multiModuleProjectDirectory); this.localMavenRepository = localMavenRepository; this.settings = settings; this.javaHome = Objects.requireNonNull(javaHome, "javaHome"); } /** - * @return absolute normalized path to local Maven repository or {@code null} + * @return absolute normalized path to local Maven repository or {@code null} if the server is supposed to use the default */ public Path getLocalMavenRepository() { return localMavenRepository; @@ -61,38 +62,14 @@ public class ClientLayout extends Layout { } static Path findLocalRepo() { - final String rawValue = System.getProperty("maven.repo.local"); - return rawValue != null ? Paths.get(rawValue) : null; + return Environment.MAVEN_REPO_LOCAL.systemProperty().asPath(); } - static Path findJavaHome(Properties mvndProperties) { - String rawValue = System.getenv("JAVA_HOME"); - if (rawValue == null) { - rawValue = mvndProperties.getProperty("java.home"); - } - if (rawValue == null) { - rawValue = System.getProperty("java.home"); - } - if (rawValue == null) { - throw new IllegalStateException( - "Either environment variable JAVA_HOME or java.home property in ~/.m2/mvnd.properties or system property java.home must be set"); - } - final Path path = Paths.get(rawValue); - try { - return path.toRealPath(); - } catch (IOException e) { - throw new RuntimeException("Could not get a real path from path " + path); - } - } - - static Path findLogbackConfigurationFile(Properties mvndProperties, Path mvndHome) { - String rawValue = mvndProperties.getProperty("logback.configurationFile"); - if (rawValue == null) { - rawValue = System.getProperty("logback.configurationFile"); - } - if (rawValue == null) { - rawValue = System.getProperty("logback.configurationFile"); - } + static Path findLogbackConfigurationFile(Supplier mvndProperties, Path mvndPropertiesPath, Path mvndHome) { + final String rawValue = Environment.LOGBACK_CONFIGURATION_FILE + .systemProperty() + .orLocalProperty(mvndProperties, mvndPropertiesPath) + .asString(); if (rawValue != null) { return Paths.get(rawValue).toAbsolutePath().normalize(); } diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java b/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java index 7e66198e..e92fd232 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java @@ -46,8 +46,6 @@ import org.slf4j.LoggerFactory; public class DefaultClient implements Client { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultClient.class); - public static final String DAEMON_DEBUG = "daemon.debug"; - public static final String DAEMON_IDLE_TIMEOUT = "daemon.idleTimeout"; public static final int DEFAULT_IDLE_TIMEOUT = (int) TimeUnit.HOURS.toMillis(3); public static final int DEFAULT_PERIODIC_CHECK_INTERVAL_MILLIS = 10 * 1000; public static final int CANCEL_TIMEOUT = 10 * 1000; @@ -67,10 +65,6 @@ public class DefaultClient implements Client { } else { throw new IllegalArgumentException("-l and --log-file need to befollowed by a path"); } - } else if ("--install".equals(arg)) { - install(false); - } else if ("--update".equals(arg)) { - install(true); } else { args.add(arg); } @@ -81,21 +75,35 @@ public class DefaultClient implements Client { } } - private static void install(boolean overwrite) { + private static void install(boolean overwrite, final Properties commandLineProperties) { final Properties buildProps = loadBuildProperties(); final String version = buildProps.getProperty("version"); - final String rawZipUri = System.getProperty("mvnd.zip.uri", "https://github.com/ppalaga/mvnd/releases/download/"+ version +"/mvnd-" + version + ".zip"); + final String rawZipUri = Environment.MVND_DIST_URI + .commandLineProperty(() -> commandLineProperties) + .orEnvironmentVariable() + .orSystemProperty() + .orDefault(() -> "https://github.com/mvndaemon/mvnd/releases/download/" + version + "/mvnd-dist.zip") + .asString(); final URI zipUri = URI.create(rawZipUri); - final Path mvndHome; - final String rawMvndHome = Layout.findEnvMavenHome(); - if (rawMvndHome == null) { - mvndHome = Paths.get(System.getProperty("user.home")).resolve(".m2/mvnd/" + version); - } else { - mvndHome = Paths.get(rawMvndHome); - } - final String rawJavaHome = System.getProperty("java.home"); - final Path javaHome = rawJavaHome != null ? Paths.get(rawJavaHome) : null; - Installer.installServer(zipUri, Layout.MVND_PROPS_PATH, mvndHome, javaHome, overwrite); + final Path mvndHome = Environment.MAVEN_HOME + .commandLineProperty(() -> commandLineProperties) + .orEnvironmentVariable() + .orSystemProperty() + .orDefault(() -> Paths.get(System.getProperty("user.home")).resolve(".m2/mvnd/" + version).toString()) + .asPath() + .toAbsolutePath().normalize(); + final Path javaHome = Environment.JAVA_HOME + .systemProperty() // only write java.home to mvnd.properties if it was explicitly set on command line + // via -Djava.home=... + .asPath(); + final Path mvndPropertiesPath = Environment.MVND_PROPERTIES_PATH + .commandLineProperty(() -> commandLineProperties) + .orEnvironmentVariable() + .orSystemProperty() + .orDefault(() -> Paths.get(System.getProperty("user.home")).resolve(".m2/mvnd.properties").toString()) + .asPath() + .toAbsolutePath().normalize(); + Installer.installServer(zipUri, mvndPropertiesPath, mvndHome, javaHome, overwrite); } public DefaultClient(ClientLayout layout) { @@ -103,7 +111,7 @@ public class DefaultClient implements Client { this.buildProperties = loadBuildProperties(); } - static Properties loadBuildProperties() { + public static Properties loadBuildProperties() { final Properties result = new Properties(); try (InputStream is = DefaultClient.class.getResourceAsStream("build.properties")) { result.load(is); @@ -117,14 +125,56 @@ public class DefaultClient implements Client { public ExecutionResult execute(ClientOutput output, List argv) { LOGGER.debug("Starting client"); - final List args = new ArrayList<>(argv); + final List args = new ArrayList<>(argv.size()); + boolean version = false; + boolean showVersion = false; + boolean debug = false; + boolean install = false; + final Properties commandLineProperties = new Properties(); + for (String arg : argv) { + switch (arg) { + case "-v": + case "-version": + case "--version": + version = true; + args.add(arg); + break; + case "-V": + case "--show-version": + showVersion = true; + args.add(arg); + break; + case "-X": + case "--debug": + debug = true; + args.add(arg); + break; + case "--install": + install = true; + break; + default: + if (arg.startsWith("-D")) { + final int eqPos = arg.indexOf('='); + if (eqPos >= 0) { + commandLineProperties.setProperty(arg.substring(2, eqPos), arg.substring(eqPos+1)); + } else { + commandLineProperties.setProperty(arg.substring(2), null); + } + } + args.add(arg); + break; + } + } + + if (install) { + install(false, commandLineProperties); + return new DefaultResult(argv, null); + } + // Print version if needed - boolean version = args.contains("-v") || args.contains("-version") || args.contains("--version"); - boolean showVersion = args.contains("-V") || args.contains("--show-version"); - boolean debug = args.contains("-X") || args.contains("--debug"); if (version || showVersion || debug) { - final String nativeSuffix = Layout.isNative() ? " (native)" : ""; + final String nativeSuffix = Environment.isNative() ? " (native)" : ""; final String v = Ansi.ansi().bold().a("Maven Daemon " + buildProperties.getProperty("version") + nativeSuffix) .reset().toString(); output.accept(v); @@ -145,7 +195,7 @@ public class DefaultClient implements Client { LocalDateTime.ofInstant( Instant.ofEpochMilli(Math.max(d.getLastIdle(), d.getLastBusy())), ZoneId.systemDefault())))); - return new DefaultResult(argv, true); + return new DefaultResult(argv, null); } boolean stop = args.remove("--stop"); if (stop) { @@ -164,7 +214,7 @@ public class DefaultClient implements Client { } } } - return new DefaultResult(argv, true); + return new DefaultResult(argv, null); } setDefaultArgs(args); @@ -191,15 +241,16 @@ public class DefaultClient implements Client { while (true) { Message m = daemon.receive(); if (m instanceof BuildException) { - output.error((BuildException) m); - return new DefaultResult(argv, false); + final BuildException e = (BuildException) m; + output.error(e); + return new DefaultResult(argv, new Exception(e.getClassName() + ": "+ e.getMessage() + "\n" + e.getStackTrace())); } else if (m instanceof BuildEvent) { BuildEvent be = (BuildEvent) m; switch (be.getType()) { case BuildStarted: break; case BuildStopped: - return new DefaultResult(argv, true); + return new DefaultResult(argv, null); case ProjectStarted: case MojoStarted: case MojoStopped: @@ -248,16 +299,16 @@ public class DefaultClient implements Client { args.add("\"" + layout.javaHome().resolve(java) + "\""); args.add("-classpath"); args.add("\"" + classpath + "\""); - if (Boolean.getBoolean(DAEMON_DEBUG)) { + if (Environment.DAEMON_DEBUG.systemProperty().orDefault(() -> "false").asBoolean()) { args.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000"); } args.add("-Dmaven.home=\"" + mavenHome + "\""); args.add("-Dlogback.configurationFile=logback.xml"); args.add("-Ddaemon.uid=" + uid); args.add("-Xmx4g"); - final String timeout = System.getProperty(DAEMON_IDLE_TIMEOUT); + final String timeout = Environment.DAEMON_IDLE_TIMEOUT.systemProperty().asString(); if (timeout != null) { - args.add("-D" + DAEMON_IDLE_TIMEOUT + "=" + timeout); + args.add(Environment.DAEMON_IDLE_TIMEOUT.asCommandLineProperty(timeout)); } args.add("\"-Dmaven.multiModuleProjectDirectory=" + layout.multiModuleProjectDirectory().toString() + "\""); @@ -291,26 +342,26 @@ public class DefaultClient implements Client { private class DefaultResult implements ExecutionResult { - private final boolean success; + private final Exception exception; private final List args; - private DefaultResult(List args, boolean success) { + private DefaultResult(List args, Exception exception) { super(); this.args = args; - this.success = success; + this.exception = exception; } @Override public ExecutionResult assertSuccess() { - if (!this.success) { - throw new AssertionError(appendCommand(new StringBuilder("Build failed: "))); + if (exception != null) { + throw new AssertionError(appendCommand(new StringBuilder("Build failed: ")).toString(), exception); } return this; } @Override public ExecutionResult assertFailure() { - if (this.success) { + if (exception == null) { throw new AssertionError(appendCommand(new StringBuilder("Build did not fail: "))); } return this; @@ -318,7 +369,7 @@ public class DefaultClient implements Client { @Override public boolean isSuccess() { - return success; + return exception == null; } StringBuilder appendCommand(StringBuilder sb) { diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/Environment.java b/client/src/main/java/org/jboss/fuse/mvnd/client/Environment.java new file mode 100644 index 00000000..2cc807a5 --- /dev/null +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/Environment.java @@ -0,0 +1,256 @@ +package org.jboss.fuse.mvnd.client; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Collects system properties and environment variables used by mvnd client or server. + */ +public enum Environment { + LOGBACK_CONFIGURATION_FILE("logback.configurationFile", null), + JAVA_HOME("java.home", "JAVA_HOME"), + MAVEN_HOME("maven.home", "MAVEN_HOME"), + MAVEN_REPO_LOCAL("maven.repo.local", null), + MAVEN_MULTIMODULE_PROJECT_DIRECTORY("maven.multiModuleProjectDirectory", null), + MVND_PROPERTIES_PATH("mvnd.properties.path", "MVND_PROPERTIES_PATH"), + MVND_DIST_URI("mvnd.dist.uri", "MVND_DIST_URI"), + DAEMON_DEBUG("daemon.debug", null), + DAEMON_IDLE_TIMEOUT("daemon.idleTimeout", null), + DAEMON_UID("daemon.uid", null); + + static Properties properties = System.getProperties(); + static Map env = System.getenv(); + + private final String property; + private final String environmentVariable; + + Environment(String property, String environmentVariable) { + this.property = property; + this.environmentVariable = environmentVariable; + } + + public Environment.EnvValue systemProperty() { + return new EnvValue(this, systemPropertySource()); + } + + public Environment.EnvValue commandLineProperty(Supplier commandLineProperties) { + return new EnvValue(this, new ValueSource( + description -> description.append("command line property ").append(property), + () -> commandLineProperties.get().getProperty(property))); + } + + public Environment.EnvValue environmentVariable() { + return new EnvValue(this, environmentVariableSource()); + } + + public String asCommandLineProperty(String value) { + return "-D" + property + "=" + value; + } + + public boolean hasCommandLineProperty(Collection args) { + final String prefix = "-D" + property + "="; + return args.stream().anyMatch(s -> s.startsWith(prefix)); + } + + public static Path findJavaHome(Supplier mvndProperties, Path mvndPropertiesPath) { + final Path result = JAVA_HOME + .environmentVariable() + .orLocalProperty(mvndProperties, mvndPropertiesPath) + .orSystemProperty() + .orFail() + .asPath(); + try { + return result + .toRealPath(); + } catch (IOException e) { + throw new RuntimeException("Could not get a real path from path " + result); + } + } + + public static Path findMvndPropertiesPath() { + return MVND_PROPERTIES_PATH + .environmentVariable() + .orSystemProperty() + .orDefault(() -> Paths.get(System.getProperty("user.home")).resolve(".m2/mvnd.properties").toString()) + .asPath() + .toAbsolutePath().normalize(); + } + + public static Path findMavenHome(Supplier mvndProperties, Path mvndPropertiesPath) { + return findBasicMavenHome() + .orLocalProperty(mvndProperties, mvndPropertiesPath) + .orFail() + .asPath() + .toAbsolutePath().normalize(); + } + + public static EnvValue findBasicMavenHome() { + return MAVEN_HOME + .environmentVariable() + .orSystemProperty(); + } + + public static Path findMultiModuleProjectDirectory(Path pwd) { + return MAVEN_MULTIMODULE_PROJECT_DIRECTORY + .systemProperty() + .orDefault(() -> { + Path dir = pwd; + do { + if (Files.isDirectory(dir.resolve(".mvn"))) { + return dir.toString(); + } + dir = dir.getParent(); + } while (dir != null); + throw new IllegalStateException("Could not detect maven.multiModuleProjectDirectory by climbing up from [" + + pwd + + "] seeking a .mvn directory. You may want to create a .mvn directory in the root directory of your source tree."); + }) + .asPath() + .toAbsolutePath().normalize(); + } + + public static boolean isNative() { + return "executable".equals(System.getProperty("org.graalvm.nativeimage.kind")); + } + + private Environment.ValueSource systemPropertySource() { + if (property == null) { + throw new IllegalStateException("Cannot use " + Environment.class.getName() + " for getting a system property"); + } + return new ValueSource( + description -> description.append("system property ").append(property), + () -> properties.getProperty(property)); + } + + private Environment.ValueSource environmentVariableSource() { + if (environmentVariable == null) { + throw new IllegalStateException( + "Cannot use " + Environment.class.getName() + "."+ name() +" for getting an environment variable"); + } + return new ValueSource( + description -> description.append("environment variable ").append(environmentVariable), + () -> env.get(environmentVariable)); + } + + /** + * A source of an environment value with a description capability. + */ + public static class ValueSource { + final Function descriptionFunction; + final Supplier valueSupplier; + + public ValueSource(Function descriptionFunction, Supplier valueSupplier) { + this.descriptionFunction = descriptionFunction; + this.valueSupplier = valueSupplier; + } + + /** Mostly for debugging */ + @Override + public String toString() { + return descriptionFunction.apply(new StringBuilder()).toString(); + } + + } + + /** + * A chained lazy environment value. + */ + public static class EnvValue { + private final Environment envKey; + private final Environment.ValueSource valueSource; + protected Environment.EnvValue previous; + + public EnvValue(Environment envKey, Environment.ValueSource valueSource) { + this.previous = null; + this.envKey = envKey; + this.valueSource = valueSource; + } + + public EnvValue(Environment.EnvValue previous, Environment envKey, Environment.ValueSource valueSource) { + this.previous = previous; + this.envKey = envKey; + this.valueSource = valueSource; + } + + public Environment.EnvValue orSystemProperty() { + return new EnvValue(this, envKey, envKey.systemPropertySource()); + } + + public Environment.EnvValue orLocalProperty(Supplier localProperties, Path localPropertiesPath) { + return new EnvValue(this, envKey, new ValueSource( + description -> description.append("property ").append(envKey.property).append(" in ") + .append(localPropertiesPath), + () -> localProperties.get().getProperty(envKey.property))); + } + + public Environment.EnvValue orEnvironmentVariable() { + return new EnvValue(this, envKey, envKey.environmentVariableSource()); + } + + public Environment.EnvValue orDefault(Supplier defaultSupplier) { + return new EnvValue(this, envKey, + new ValueSource(sb -> sb.append("default").append(defaultSupplier.get()), defaultSupplier)); + } + + public Environment.EnvValue orFail() { + return new EnvValue(this, envKey, new ValueSource(sb -> sb, () -> { + final StringBuilder sb = new StringBuilder("Could not get value for ") + .append(Environment.class.getSimpleName()) + .append(".").append(envKey.name()).append(" from any of the following sources: "); + + /* Compose the description functions to invert the order thus getting the resolution order in the message */ + Function description = (s -> s); + EnvValue val = this; + while (val != null) { + description = description.compose(val.valueSource.descriptionFunction); + val = val.previous; + if (val != null) { + description = description.compose(s -> s.append(", ")); + } + } + description.apply(sb); + throw new IllegalStateException(sb.toString()); + })); + } + + String get() { + if (previous != null) { + final String result = previous.get(); + if (result != null) { + return result; + } + } + return valueSource.valueSupplier.get(); + } + + public String asString() { + return get(); + } + + public Optional asOptional() { + return Optional.ofNullable(get()); + } + + public Path asPath() { + final String result = get(); + return result == null ? null : Paths.get(result); + } + + public boolean asBoolean() { + return Boolean.parseBoolean(get()); + } + + public int asInt() { + return Integer.parseInt(get()); + } + + } +} diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/Installer.java b/client/src/main/java/org/jboss/fuse/mvnd/client/Installer.java index 2248f840..74796e2d 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/Installer.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/Installer.java @@ -25,10 +25,14 @@ import java.util.stream.Stream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Installer { + private static final Logger LOG = LoggerFactory.getLogger(Installer.class); private static final int BUFFER_SIZE = 4096; private static final int MAX_PERMISSIONS = 0777; + public static void installServer(URI zipUri, Path mvndPropsPath, Path mvndHome, Path javaHome, boolean overwrite) { final boolean mvndHomeExists = Files.exists(mvndHome); if (!overwrite && mvndHomeExists) { @@ -47,7 +51,7 @@ public class Installer { writeMvndProperties(mvndPropsPath, mvndHome, javaHome); } - private static void deleteIfExists(Path path) { + static void deleteIfExists(Path path) { if (Files.isRegularFile(path)) { try { Files.delete(path); @@ -71,6 +75,7 @@ public class Installer { } static void writeMvndProperties(Path mvndPropsPath, Path mvndHome, Path javaHome) { + LOG.debug("Writing {}", mvndPropsPath); final String template = readTemplate(); final String javaHomeLine = javaHome == null ? "" : "java.home = " + javaHome.toString(); final String content = String.format(template, mvndHome.toString(), javaHomeLine); @@ -91,21 +96,22 @@ public class Installer { } } - static void unzip(Path localZip, Path mvndHome) { + static void unzip(Path localZip, Path destinationDir) { + LOG.debug("Unzipping {} to {}", localZip, destinationDir); try { - Files.createDirectories(mvndHome); + Files.createDirectories(destinationDir); } catch (IOException e) { - throw new RuntimeException("Could not create directories " + mvndHome, e); + throw new RuntimeException("Could not create directories " + destinationDir, e); } try (ZipFile zip = new ZipFile(Files.newByteChannel(localZip))) { final Map> permissionCache = new HashMap<>(); final Enumeration entries = zip.getEntries(); while (entries.hasMoreElements()) { final ZipArchiveEntry entry = entries.nextElement(); - final Path dest = mvndHome.resolve(entry.getName()).normalize(); - if (!dest.startsWith(mvndHome)) { + final Path dest = destinationDir.resolve(entry.getName()).normalize(); + if (!dest.startsWith(destinationDir)) { /* Avoid writing to paths outside of mvndHome */ - throw new IllegalStateException("Tainted ZIP entry name " + entry.getName()); + throw new IllegalStateException("Possibly tainted ZIP entry name " + entry.getName() + " would have to be unpacked outside of " + destinationDir); } if (entry.isDirectory()) { Files.createDirectories(dest); @@ -120,21 +126,24 @@ public class Installer { "Could not unzip entry " + entry.getName() + " from " + localZip + " to " + dest); } } - final PosixFileAttributeView attributes = Files.getFileAttributeView(dest, PosixFileAttributeView.class); - if (attributes != null) { - int mode = (int) (entry.getUnixMode() & MAX_PERMISSIONS); - Files.setPosixFilePermissions(dest, permissionCache.computeIfAbsent(mode, Installer::toPermissionSet)); + final int mode = (int) (entry.getUnixMode() & MAX_PERMISSIONS); + if (mode != 0) { + final PosixFileAttributeView attributes = Files.getFileAttributeView(dest, PosixFileAttributeView.class); + if (attributes != null) { + Files.setPosixFilePermissions(dest, permissionCache.computeIfAbsent(mode, Installer::toPermissionSet)); + } } Files.setLastModifiedTime(dest, FileTime.from(entry.getTime(), TimeUnit.MILLISECONDS)); } } catch (IOException e) { - throw new RuntimeException("Could not unzip " + localZip, e); + throw new RuntimeException("Could not unzip " + localZip + " to " + destinationDir, e); } } static Path download(URI zipUri) { try { final Path localZip = Files.createTempFile("", "-mvnd-dist.zip"); + LOG.debug("Downloading {} to {}", zipUri, localZip); try ( InputStream in = new BufferedInputStream(zipUri.toURL().openStream(), BUFFER_SIZE); OutputStream out = new BufferedOutputStream(Files.newOutputStream(localZip), BUFFER_SIZE)) { diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/Layout.java b/client/src/main/java/org/jboss/fuse/mvnd/client/Layout.java index a883681a..3164977c 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/Layout.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/Layout.java @@ -17,25 +17,24 @@ package org.jboss.fuse.mvnd.client; import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Properties; -import java.util.stream.Stream; +import java.util.function.Supplier; public class Layout { - public static final Path MVND_PROPS_PATH = Paths.get(System.getProperty("user.home")).resolve(".m2/mvnd.properties"); - private static Layout ENV_INSTANCE; private final Path mavenHome; private final Path userDir; private final Path multiModuleProjectDirectory; + private final Path mvndPropertiesPath; - public Layout(Path mavenHome, Path userDir, Path multiModuleProjectDirectory) { + public Layout(Path mvndPropertiesPath, Path mavenHome, Path userDir, Path multiModuleProjectDirectory) { super(); + this.mvndPropertiesPath = mvndPropertiesPath; this.mavenHome = mavenHome; this.userDir = userDir; this.multiModuleProjectDirectory = multiModuleProjectDirectory; @@ -61,90 +60,47 @@ public class Layout { return multiModuleProjectDirectory; } - public static boolean isNative() { - return "executable".equals(System.getProperty("org.graalvm.nativeimage.kind")); + public Path getMvndPropertiesPath() { + return mvndPropertiesPath; } public static Layout getEnvInstance() { if (ENV_INSTANCE == null) { - final Properties mvndProperties = loadMvndProperties(); + final Path mvndPropertiesPath = Environment.findMvndPropertiesPath(); + final Supplier mvndProperties = lazyMvndProperties(mvndPropertiesPath); final Path pwd = Paths.get(".").toAbsolutePath().normalize(); ENV_INSTANCE = new Layout( - findMavenHome(mvndProperties), + mvndPropertiesPath, + Environment.findMavenHome(mvndProperties, mvndPropertiesPath), pwd, - findMultiModuleProjectDirectory(pwd)); + Environment.findMultiModuleProjectDirectory(pwd)); } return ENV_INSTANCE; } + static Supplier lazyMvndProperties(Path mvndPropertiesPath) { + return new Supplier() { - static Properties loadMvndProperties() { - final Properties result = new Properties(); - if (Files.exists(MVND_PROPS_PATH)) { - try (InputStream in = Files.newInputStream(MVND_PROPS_PATH)) { - result.load(in); - } catch (IOException e) { - throw new RuntimeException("Could not read " + MVND_PROPS_PATH); - } - } - return result; - } + private volatile Properties properties; - static Path findMavenHome(Properties mvndProperties) { - String rawValue = findEnvMavenHome(); - if (isNative()) { - try { - final Path nativeExecutablePath = Paths.get(Class.forName("org.graalvm.nativeimage.ProcessProperties").getMethod("getExecutableName").invoke(null).toString()).toAbsolutePath().normalize(); - final Path bin = nativeExecutablePath.getParent(); - if (bin.getFileName().toString().equals("bin")) { - final Path candidateMvnHome = bin.getParent(); - final Path libExt = candidateMvnHome.resolve("lib/ext"); - if (Files.isDirectory(libExt)) { - try (Stream files = Files.list(libExt)) { - if (files.filter(path -> path.getFileName().toString().startsWith("mvnd-")).findFirst().isPresent()) { - rawValue = candidateMvnHome.toString(); - } + @Override + public Properties get() { + Properties result = this.properties; + if (result == null) { + result = new Properties(); + if (Files.exists(mvndPropertiesPath)) { + try (InputStream in = Files.newInputStream(mvndPropertiesPath)) { + result.load(in); } catch (IOException e) { - throw new RuntimeException("Could not list " + libExt); + throw new RuntimeException("Could not read " + mvndPropertiesPath); } } + this.properties = result; } - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException - | SecurityException | ClassNotFoundException e) { - throw new RuntimeException("Could not invoke org.graalvm.nativeimage.ProcessProperties.getExecutableName() via reflection"); + return result; } - } - if (rawValue == null) { - rawValue = mvndProperties.getProperty("maven.home"); - } - if (rawValue == null) { - throw new IllegalStateException("Either environment variable MAVEN_HOME or maven.home property in ~/.m2/mvnd.properties or system property maven.home must be set"); - } - return Paths.get(rawValue).toAbsolutePath().normalize(); - } - - public static String findEnvMavenHome() { - String rawValue = System.getenv("MAVEN_HOME"); - if (rawValue == null) { - rawValue = System.getProperty("maven.home"); - } - return rawValue; - } - - static Path findMultiModuleProjectDirectory(Path pwd) { - final String multiModuleProjectDirectory = System.getProperty("maven.multiModuleProjectDirectory"); - if (multiModuleProjectDirectory != null) { - return Paths.get(multiModuleProjectDirectory).toAbsolutePath().normalize(); - } - Path dir = pwd; - do { - if (Files.isDirectory(dir.resolve(".mvn"))) { - return dir.toAbsolutePath().normalize(); - } - dir = dir.getParent(); - } while (dir != null); - throw new IllegalStateException("Could not detect maven.multiModuleProjectDirectory by climbing up from ["+ pwd +"] seeking a .mvn directory. You may want to create a .mvn directory in the root directory of your source tree."); + }; } @Override diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/ServerMain.java b/client/src/main/java/org/jboss/fuse/mvnd/client/ServerMain.java index 45bd5516..be852134 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/ServerMain.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/ServerMain.java @@ -20,19 +20,13 @@ import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.stream.Stream; public class ServerMain { public static void main(String[] args) throws Exception { - String uidStr = System.getProperty("daemon.uid"); - String mavenHomeStr = System.getProperty("maven.home"); - if (uidStr == null || mavenHomeStr == null) { - throw new IllegalStateException("The system properties 'daemon.uid' and 'maven.home' must be valid"); - } - - Path mavenHome = Paths.get(mavenHomeStr); + final String uidStr = Environment.DAEMON_UID.systemProperty().orFail().asString(); + final Path mavenHome = Environment.MAVEN_HOME.systemProperty().orFail().asPath(); URL[] classpath = Stream.concat( Stream.concat(Files.list(mavenHome.resolve("lib/ext")), diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/svm/ExtraFieldUtilsSubstitution.java b/client/src/main/java/org/jboss/fuse/mvnd/client/svm/ExtraFieldUtilsSubstitution.java new file mode 100644 index 00000000..14d1c4d7 --- /dev/null +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/svm/ExtraFieldUtilsSubstitution.java @@ -0,0 +1,92 @@ +package org.jboss.fuse.mvnd.client.svm; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.zip.ZipException; + +import org.apache.commons.compress.archivers.zip.AsiExtraField; +import org.apache.commons.compress.archivers.zip.ExtraFieldParsingBehavior; +import org.apache.commons.compress.archivers.zip.ExtraFieldUtils; +import org.apache.commons.compress.archivers.zip.ExtraFieldUtils.UnparseableExtraField; +import org.apache.commons.compress.archivers.zip.ZipExtraField; +import org.apache.commons.compress.archivers.zip.ZipShort; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.KeepOriginal; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(ExtraFieldUtils.class) +@Substitute +public final class ExtraFieldUtilsSubstitution { + + @Alias + @RecomputeFieldValue(kind = Kind.None) + private static final Map> implementations; + + static { + implementations = new ConcurrentHashMap<>(); + registerInst(new AsiExtraField()); + } + + + public static void registerInst(ZipExtraField ze) { + implementations.put(ze.getHeaderId(), ze.getClass()); + } + + @KeepOriginal + public static ZipExtraField createExtraField(final ZipShort headerId) + throws InstantiationException, IllegalAccessException { + return null; + } + + @KeepOriginal + public static ZipExtraField createExtraFieldNoDefault(final ZipShort headerId) + throws InstantiationException, IllegalAccessException { + return null; + } + + @KeepOriginal + public static ZipExtraField[] parse(final byte[] data) throws ZipException { + return null; + } + + @KeepOriginal + public static ZipExtraField[] parse(final byte[] data, final boolean local) + throws ZipException { + return null; + } + + @KeepOriginal + public static ZipExtraField[] parse(final byte[] data, final boolean local, + final UnparseableExtraField onUnparseableData) + throws ZipException { + return null; + } + + @KeepOriginal + public static ZipExtraField[] parse(final byte[] data, final boolean local, + final ExtraFieldParsingBehavior parsingBehavior) + throws ZipException { + return null; + } + + @KeepOriginal + public static byte[] mergeLocalFileDataData(final ZipExtraField[] data) { + return null; + } + + @KeepOriginal + public static byte[] mergeCentralDirectoryData(final ZipExtraField[] data) { + return null; + } + + @KeepOriginal + public static ZipExtraField fillExtraField(final ZipExtraField ze, final byte[] data, final int off, + final int len, final boolean local) throws ZipException { + return null; + } + +} diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/svm/ReflectionRegistration.java b/client/src/main/java/org/jboss/fuse/mvnd/client/svm/ReflectionRegistration.java index 5bfd6dee..47037f41 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/svm/ReflectionRegistration.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/svm/ReflectionRegistration.java @@ -8,7 +8,8 @@ import com.oracle.svm.core.annotate.AutomaticFeature; public class ReflectionRegistration implements Feature { public void beforeAnalysis(BeforeAnalysisAccess access) { // try { -// } catch (NoSuchMethodException | SecurityException e) { +// RuntimeReflection.register(AsiExtraField.class.getConstructors()); +// } catch (SecurityException e) { // throw new RuntimeException(e); // } } diff --git a/client/src/test/java/org/jboss/fuse/mvnd/daemon/DaemonRegistryTest.java b/client/src/test/java/org/jboss/fuse/mvnd/client/DaemonRegistryTest.java similarity index 98% rename from client/src/test/java/org/jboss/fuse/mvnd/daemon/DaemonRegistryTest.java rename to client/src/test/java/org/jboss/fuse/mvnd/client/DaemonRegistryTest.java index b22d0cea..8d4045f6 100644 --- a/client/src/test/java/org/jboss/fuse/mvnd/daemon/DaemonRegistryTest.java +++ b/client/src/test/java/org/jboss/fuse/mvnd/client/DaemonRegistryTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jboss.fuse.mvnd.daemon; +package org.jboss.fuse.mvnd.client; import java.io.File; import java.io.IOException; diff --git a/client/src/test/java/org/jboss/fuse/mvnd/client/EnvironmentTest.java b/client/src/test/java/org/jboss/fuse/mvnd/client/EnvironmentTest.java new file mode 100644 index 00000000..fdbff7c2 --- /dev/null +++ b/client/src/test/java/org/jboss/fuse/mvnd/client/EnvironmentTest.java @@ -0,0 +1,108 @@ +package org.jboss.fuse.mvnd.client; + +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class EnvironmentTest { + + @Test + void prop() { + try (EnvironmentResource env = new EnvironmentResource()) { + env.props("maven.home", "/maven/home/prop"); + Assertions.assertEquals("/maven/home/prop", Environment.MAVEN_HOME.systemProperty().asString()); + } + } + + @Test + void env() { + try (EnvironmentResource env = new EnvironmentResource()) { + env.env("MAVEN_HOME", "/maven/home/env"); + Assertions.assertEquals("/maven/home/env", Environment.MAVEN_HOME.environmentVariable().asString()); + } + } + + @Test + void localProps() { + try (EnvironmentResource env = new EnvironmentResource()) { + final Properties localProps = new Properties(); + localProps.put("maven.home", "/maven/home/local"); + Assertions.assertEquals(Paths.get("/maven/home/local"), + Environment.MAVEN_HOME + .environmentVariable() + .orSystemProperty() + .orLocalProperty(() -> localProps, Paths.get("/local/properties")) + .orFail() + .asPath()); + } + } + + @Test + void envBeforeProp() { + try (EnvironmentResource env = new EnvironmentResource()) { + env.props("maven.home", "/maven/home/prop"); + env.env("MAVEN_HOME", "/maven/home/env"); + Assertions.assertEquals("/maven/home/env", + Environment.MAVEN_HOME + .environmentVariable() + .orSystemProperty() + .asString()); + } + } + + @Test + void fail() { + try (EnvironmentResource env = new EnvironmentResource()) { + try { + Assertions.assertEquals("/maven/home/env", + Environment.MAVEN_HOME + .environmentVariable() + .orSystemProperty() + .orFail() + .asString()); + Assertions.fail("IllegalStateException expected"); + } catch (IllegalStateException e) { + Assertions.assertEquals( + "Could not get value for Environment.MAVEN_HOME from any of the following sources: environment variable MAVEN_HOME, system property maven.home", + e.getMessage()); + } + } + } + + static class EnvironmentResource implements AutoCloseable { + + private final Properties props = new Properties(); + private final Map env = new HashMap<>(); + + public EnvironmentResource() { + Environment.env = env; + Environment.properties = props; + } + + public void props(String... props) { + int i = 0; + while (i < props.length) { + this.props.setProperty(props[i++], props[i++]); + } + } + + public void env(String... env) { + int i = 0; + while (i < env.length) { + this.env.put(env[i++], env[i++]); + } + } + + @Override + public void close() { + Environment.env = System.getenv(); + Environment.properties = System.getProperties(); + } + + } + +} diff --git a/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java b/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java index 629e34a5..3466bbd1 100644 --- a/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java +++ b/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java @@ -43,6 +43,7 @@ import org.apache.maven.cli.CliRequest; import org.apache.maven.cli.CliRequestBuilder; import org.apache.maven.cli.DaemonMavenCli; import org.jboss.fuse.mvnd.client.DefaultClient; +import org.jboss.fuse.mvnd.client.Environment; import org.jboss.fuse.mvnd.client.DaemonConnection; import org.jboss.fuse.mvnd.client.DaemonException; import org.jboss.fuse.mvnd.client.DaemonExpirationStatus; @@ -91,12 +92,10 @@ public class Server implements AutoCloseable, Runnable { registry = new DaemonRegistry(layout.registry()); socket = ServerSocketChannel.open().bind(new InetSocketAddress(0)); - int idleTimeout; - if (System.getProperty(DefaultClient.DAEMON_IDLE_TIMEOUT) != null) { - idleTimeout = Integer.parseInt(System.getProperty(DefaultClient.DAEMON_IDLE_TIMEOUT)); - } else { - idleTimeout = DefaultClient.DEFAULT_IDLE_TIMEOUT; - } + final int idleTimeout = Environment.DAEMON_IDLE_TIMEOUT + .systemProperty() + .orDefault(() -> String.valueOf(DefaultClient.DEFAULT_IDLE_TIMEOUT)) + .asInt(); executor = Executors.newScheduledThreadPool(1); strategy = DaemonExpiration.master(); diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 38c39080..65100e51 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -58,6 +58,7 @@ ${project.version} ${project.basedir}/../daemon/target/maven-distro + ${project.basedir}/../daemon/target/mvnd-dist-${project.version}.zip @@ -94,6 +95,7 @@ ${project.version} ${project.basedir}/../daemon/target/maven-distro ${mvnd.native.executable} + ${project.basedir}/../daemon/target/mvnd-dist-${project.version}.zip diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/InstallDaemonNativeIT.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/InstallDaemonNativeIT.java new file mode 100644 index 00000000..ffa3d09b --- /dev/null +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/InstallDaemonNativeIT.java @@ -0,0 +1,76 @@ +package org.jboss.fuse.mvnd.it; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Objects; +import java.util.Set; + +import javax.inject.Inject; + +import org.assertj.core.api.Assertions; +import org.jboss.fuse.mvnd.client.Client; +import org.jboss.fuse.mvnd.client.ClientOutput; +import org.jboss.fuse.mvnd.client.DefaultClient; +import org.jboss.fuse.mvnd.client.Environment; +import org.jboss.fuse.mvnd.junit.MvndNativeTest; +import org.jboss.fuse.mvnd.junit.TestLayout; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +@MvndNativeTest(projectDir = "src/test/projects/single-module") +public class InstallDaemonNativeIT { + + @Inject + Client client; + + @Inject + TestLayout layout; + + @Test + void installDaemon() throws IOException, InterruptedException { + + final Path testDir = layout.getTestDir(); + final Path mvndPropertiesPath = testDir.resolve("installation-mvnd.properties"); + final Path mavenHome = testDir.resolve("installation-maven-home"); + + Assertions.assertThat(mvndPropertiesPath).doesNotExist(); + Assertions.assertThat(mavenHome).doesNotExist(); + + final ClientOutput o = Mockito.mock(ClientOutput.class); + final Path mvndDistPath = Paths.get(Objects.requireNonNull(System.getProperty("mvnd.dist.path"))) + .toAbsolutePath() + .normalize(); + Assertions.assertThat(mvndDistPath).exists(); + + final String mvndDistUri = mvndDistPath.toUri().toString(); + + client.execute(o, + "--install", + Environment.MVND_DIST_URI.asCommandLineProperty(mvndDistUri), + Environment.MVND_PROPERTIES_PATH.asCommandLineProperty(mvndPropertiesPath.toString()), + Environment.JAVA_HOME.asCommandLineProperty(layout.javaHome().toString()), + Environment.MAVEN_HOME.asCommandLineProperty(mavenHome.toString())) + .assertSuccess(); + + Assertions.assertThat(mvndPropertiesPath).exists(); + Assertions.assertThat(mavenHome).exists(); + final Path mvndShPath = mavenHome.resolve("bin/mvnd"); + Assertions.assertThat(mvndShPath).exists(); + if (!System.getProperty("os.name").toLowerCase().contains("windows")) { + final PosixFileAttributeView attributes = Files.getFileAttributeView(mvndShPath, PosixFileAttributeView.class); + Assertions.assertThat(attributes).isNotNull(); + final Set permissions = attributes.readAttributes().permissions(); + Assertions.assertThat(permissions).contains(PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.OTHERS_EXECUTE); + } + + final String version = DefaultClient.loadBuildProperties().getProperty("version"); + Assertions.assertThat(mavenHome.resolve("lib/ext/mvnd-client-" + version + ".jar")).exists(); + Assertions.assertThat(mavenHome.resolve("lib/ext/mvnd-daemon-" + version + ".jar")).exists(); + } + +} diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/InstallDaemonTest.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/InstallDaemonTest.java new file mode 100644 index 00000000..14c1b572 --- /dev/null +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/InstallDaemonTest.java @@ -0,0 +1,8 @@ +package org.jboss.fuse.mvnd.it; + +import org.jboss.fuse.mvnd.junit.MvndTest; + +@MvndTest(projectDir = "src/test/projects/single-module") +public class InstallDaemonTest extends InstallDaemonNativeIT { + +} diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/SingleModuleNativeIT.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/SingleModuleNativeIT.java index 46167685..55442e73 100644 --- a/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/SingleModuleNativeIT.java +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/SingleModuleNativeIT.java @@ -13,7 +13,6 @@ import org.jboss.fuse.mvnd.client.ClientLayout; import org.jboss.fuse.mvnd.client.ClientOutput; import org.jboss.fuse.mvnd.junit.MvndNativeTest; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; import org.mockito.InOrder; import org.mockito.Mockito; @@ -27,7 +26,7 @@ public class SingleModuleNativeIT { ClientLayout layout; @Test - void cleanInstall(TestInfo testInfo) throws IOException, InterruptedException { + void cleanInstall() throws IOException, InterruptedException { final Path helloFilePath = layout.multiModuleProjectDirectory().resolve("target/hello.txt"); if (Files.exists(helloFilePath)) { Files.delete(helloFilePath); diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/MvndTestExtension.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/MvndTestExtension.java index de6be4b1..7f8ac843 100644 --- a/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/MvndTestExtension.java +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/MvndTestExtension.java @@ -68,13 +68,11 @@ public class MvndTestExtension implements BeforeAllCallback, BeforeEachCallback, f.setAccessible(true); if (f.getType() == DaemonRegistry.class) { f.set(testInstance, resource.registry); - } else if (f.getType() == ClientLayout.class) { - f.set(testInstance, resource.layout); - } else if (f.getType() == Layout.class) { + } else if (Layout.class.isAssignableFrom(f.getType())) { f.set(testInstance, resource.layout); } else if (f.getType() == Client.class) { if (resource.isNative) { - final Path mvndNativeExecutablePath = Paths.get(System.getProperty("mvnd.native.executable")); + final Path mvndNativeExecutablePath = Paths.get(System.getProperty("mvnd.native.executable")).toAbsolutePath().normalize(); if (!Files.isRegularFile(mvndNativeExecutablePath)) { throw new IllegalStateException("mvnd executable does not exist: " + mvndNativeExecutablePath); } @@ -101,7 +99,7 @@ public class MvndTestExtension implements BeforeAllCallback, BeforeEachCallback, static class MvndResource implements ExtensionContext.Store.CloseableResource { - private final ClientLayout layout; + private final TestLayout layout; private final DaemonRegistry registry; private final boolean isNative; private final long timeoutMs; @@ -141,9 +139,12 @@ public class MvndTestExtension implements BeforeAllCallback, BeforeEachCallback, throw new IllegalStateException( "The value of mvnd.home system property points at a path that does not exist or is not a directory"); } + final Path mvndPropertiesPath = testDir.resolve("mvnd.properties"); final Path localMavenRepository = deleteDir(testDir.resolve("local-maven-repo")); final Path settingsPath = createSettings(testDir.resolve("settings.xml")); - final ClientLayout layout = new ClientLayout( + final TestLayout layout = new TestLayout( + testDir, + mvndPropertiesPath, mvndHome, testExecutionDir, testExecutionDir, @@ -188,7 +189,7 @@ public class MvndTestExtension implements BeforeAllCallback, BeforeEachCallback, return settingsPath; } - public MvndResource(ClientLayout layout, DaemonRegistry registry, boolean isNative, long timeoutMs) { + public MvndResource(TestLayout layout, DaemonRegistry registry, boolean isNative, long timeoutMs) { super(); this.layout = layout; this.registry = registry; diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/NativeTestClient.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/NativeTestClient.java index e92d53a8..910e5b4d 100644 --- a/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/NativeTestClient.java +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/NativeTestClient.java @@ -16,6 +16,7 @@ import java.util.stream.Collectors; import org.jboss.fuse.mvnd.client.Client; import org.jboss.fuse.mvnd.client.ClientLayout; import org.jboss.fuse.mvnd.client.ClientOutput; +import org.jboss.fuse.mvnd.client.Environment; import org.jboss.fuse.mvnd.client.ExecutionResult; /** @@ -43,14 +44,23 @@ public class NativeTestClient implements Client { final List cmd = new ArrayList(args.size() + 1); cmd.add(mvndNativeExecutablePath.toString()); args.stream().forEach(cmd::add); - cmd.add("-Dmaven.repo.local=" + layout.getLocalMavenRepository().toString()); - ProcessBuilder builder = new ProcessBuilder(cmd.toArray(new String[0])) + if (!Environment.MVND_PROPERTIES_PATH.hasCommandLineProperty(args)) { + cmd.add(Environment.MVND_PROPERTIES_PATH.asCommandLineProperty(layout.getMvndPropertiesPath().toString())); + } + if (!Environment.MAVEN_REPO_LOCAL.hasCommandLineProperty(args)) { + cmd.add(Environment.MAVEN_REPO_LOCAL.asCommandLineProperty(layout.getLocalMavenRepository().toString())); + } + final ProcessBuilder builder = new ProcessBuilder(cmd.toArray(new String[0])) .directory(layout.userDir().toFile()) // - .redirectErrorStream(false); + .redirectErrorStream(true); - Map env = builder.environment(); - env.put("MAVEN_HOME", System.getProperty("mvnd.home")); - env.put("JAVA_HOME", System.getProperty("java.home")); + final Map env = builder.environment(); + if (!Environment.MAVEN_HOME.hasCommandLineProperty(args)) { + env.put("MAVEN_HOME", System.getProperty("mvnd.home")); + } + if (!Environment.JAVA_HOME.hasCommandLineProperty(args)) { + env.put("JAVA_HOME", System.getProperty("java.home")); + } final String cmdString = cmd.stream().collect(Collectors.joining(" ")); output.accept("Executing " + cmdString); try (CommandProcess process = new CommandProcess(builder.start(), cmd, output)) { @@ -64,15 +74,16 @@ public class NativeTestClient implements Client { private final int exitCode; private final List args; + private final List log; - public Result(List args, int exitCode) { + public Result(List args, int exitCode, List log) { super(); this.args = new ArrayList<>(args); this.exitCode = exitCode; + this.log = log; } StringBuilder appendCommand(StringBuilder sb) { - sb.append("mvnd"); for (String arg : args) { sb.append(" \"").append(arg).append('"'); } @@ -90,7 +101,16 @@ public class NativeTestClient implements Client { public Result assertSuccess() { if (exitCode != 0) { - throw new AssertionError(appendCommand(new StringBuilder("mvnd returned ").append(exitCode).append(": "))); + final StringBuilder sb = appendCommand(new StringBuilder("mvnd returned ").append(exitCode)); + if (exitCode == TIMEOUT_EXIT_CODE) { + sb.append(" (timeout)"); + } + sb.append("--- stderr+stdout start ---"); + synchronized (log) { + log.stream().forEach(s -> sb.append('\n').append(s)); + } + sb.append("--- stderr+stdout end ---"); + throw new AssertionError(sb); } return this; } @@ -114,13 +134,19 @@ public class NativeTestClient implements Client { private final Process process; private final Thread shutDownHook; private final StreamGobbler stdOut; - private List args; + private final List args; + private final List log = new ArrayList<>(); public CommandProcess(Process process, List args, Consumer outputConsumer) { super(); this.process = process; this.args = args; - this.stdOut = new StreamGobbler(process.getInputStream(), outputConsumer); + final Consumer loggingConsumer = s -> { + synchronized (log) { + log.add(s); + } + }; + this.stdOut = new StreamGobbler(process.getInputStream(), loggingConsumer.andThen(outputConsumer)); stdOut.start(); this.shutDownHook = new Thread(new Runnable() { @@ -148,7 +174,8 @@ public class NativeTestClient implements Client { Runtime.getRuntime().removeShutdownHook(shutDownHook); } catch (Exception ignored) { } - return new Result(args, timeouted ? TIMEOUT_EXIT_CODE : process.exitValue()); + final int exitCode = timeouted ? TIMEOUT_EXIT_CODE : process.exitValue(); + return new Result(args, exitCode, log); } } diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/TestLayout.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/TestLayout.java new file mode 100644 index 00000000..725270b9 --- /dev/null +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/TestLayout.java @@ -0,0 +1,21 @@ +package org.jboss.fuse.mvnd.junit; + +import java.nio.file.Path; + +import org.jboss.fuse.mvnd.client.ClientLayout; + +public class TestLayout extends ClientLayout { + private final Path testDir; + + public TestLayout(Path testDir, Path mvndPropertiesPath, Path mavenHome, Path userDir, Path multiModuleProjectDirectory, + Path javaHome, + Path localMavenRepository, Path settings) { + super(mvndPropertiesPath, mavenHome, userDir, multiModuleProjectDirectory, javaHome, localMavenRepository, settings); + this.testDir = testDir; + } + + public Path getTestDir() { + return testDir; + } + +}