--- /dev/null 2019-12-03 13:55:25.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java 2019-12-03 13:55:22.524325400 -0500 @@ -0,0 +1,732 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.incubator.jpackage.internal.ApplicationLayout; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.jpackage.test.Functional.ThrowingFunction; + +/** + * jpackage command line with prerequisite actions. Prerequisite actions can be + * anything. The simplest is to compile test application and pack in a jar for + * use on jpackage command line. + */ +public final class JPackageCommand extends CommandArguments { + + public JPackageCommand() { + actions = new ArrayList<>(); + } + + public JPackageCommand(JPackageCommand cmd) { + this(); + args.addAll(cmd.args); + withToolProvider = cmd.withToolProvider; + saveConsoleOutput = cmd.saveConsoleOutput; + suppressOutput = cmd.suppressOutput; + ignoreDefaultRuntime = cmd.ignoreDefaultRuntime; + immutable = cmd.immutable; + actionsExecuted = cmd.actionsExecuted; + } + + JPackageCommand createImmutableCopy() { + JPackageCommand reply = new JPackageCommand(this); + reply.immutable = true; + return reply; + } + + public JPackageCommand setArgumentValue(String argName, String newValue) { + verifyMutable(); + + String prevArg = null; + ListIterator it = args.listIterator(); + while (it.hasNext()) { + String value = it.next(); + if (prevArg != null && prevArg.equals(argName)) { + if (newValue != null) { + it.set(newValue); + } else { + it.remove(); + it.previous(); + it.remove(); + } + return this; + } + prevArg = value; + } + + if (newValue != null) { + addArguments(argName, newValue); + } + + return this; + } + + public JPackageCommand setArgumentValue(String argName, Path newValue) { + return setArgumentValue(argName, newValue.toString()); + } + + public JPackageCommand removeArgumentWithValue(String argName) { + return setArgumentValue(argName, (String)null); + } + + public JPackageCommand removeArgument(String argName) { + args = args.stream().filter(arg -> !arg.equals(argName)).collect( + Collectors.toList()); + return this; + } + + public boolean hasArgument(String argName) { + return args.contains(argName); + } + + public T getArgumentValue(String argName, + Function defaultValueSupplier, + Function stringConverter) { + String prevArg = null; + for (String arg : args) { + if (prevArg != null && prevArg.equals(argName)) { + return stringConverter.apply(arg); + } + prevArg = arg; + } + if (defaultValueSupplier != null) { + return defaultValueSupplier.apply(this); + } + return null; + } + + public String getArgumentValue(String argName, + Function defaultValueSupplier) { + return getArgumentValue(argName, defaultValueSupplier, v -> v); + } + + public T getArgumentValue(String argName, + Supplier defaultValueSupplier, + Function stringConverter) { + return getArgumentValue(argName, (unused) -> defaultValueSupplier.get(), + stringConverter); + } + + public String getArgumentValue(String argName, + Supplier defaultValueSupplier) { + return getArgumentValue(argName, defaultValueSupplier, v -> v); + } + + public String getArgumentValue(String argName) { + return getArgumentValue(argName, (Supplier)null); + } + + public String[] getAllArgumentValues(String argName) { + List values = new ArrayList<>(); + String prevArg = null; + for (String arg : args) { + if (prevArg != null && prevArg.equals(argName)) { + values.add(arg); + } + prevArg = arg; + } + return values.toArray(String[]::new); + } + + public JPackageCommand addArguments(String name, Path value) { + return addArguments(name, value.toString()); + } + + public boolean isImagePackageType() { + return PackageType.IMAGE == getArgumentValue("--type", + () -> null, PACKAGE_TYPES::get); + } + + public PackageType packageType() { + // Don't try to be in sync with jpackage defaults. Keep it simple: + // if no `--type` explicitely set on the command line, consider + // this is operator's fault. + return getArgumentValue("--type", + () -> { + throw new IllegalStateException("Package type not set"); + }, PACKAGE_TYPES::get); + } + + public Path outputDir() { + return getArgumentValue("--dest", () -> Path.of("."), Path::of); + } + + public Path inputDir() { + return getArgumentValue("--input", () -> null, Path::of); + } + + public String version() { + return getArgumentValue("--app-version", () -> "1.0"); + } + + public String name() { + return getArgumentValue("--name", () -> getArgumentValue("--main-class")); + } + + public boolean isRuntime() { + return hasArgument("--runtime-image") + && !hasArgument("--main-jar") + && !hasArgument("--module") + && !hasArgument("--app-image"); + } + + public JPackageCommand setDefaultInputOutput() { + addArguments("--input", TKit.defaultInputDir()); + addArguments("--dest", TKit.defaultOutputDir()); + return this; + } + + public JPackageCommand setFakeRuntime() { + verifyMutable(); + + ThrowingConsumer createBulkFile = path -> { + Files.createDirectories(path.getParent()); + try (FileOutputStream out = new FileOutputStream(path.toFile())) { + byte[] bytes = new byte[4 * 1024]; + new SecureRandom().nextBytes(bytes); + out.write(bytes); + } + }; + + addAction(cmd -> { + Path fakeRuntimeDir = TKit.workDir().resolve("fake_runtime"); + + TKit.trace(String.format("Init fake runtime in [%s] directory", + fakeRuntimeDir)); + + Files.createDirectories(fakeRuntimeDir); + + if (TKit.isWindows() || TKit.isLinux()) { + // Needed to make WindowsAppBundler happy as it copies MSVC dlls + // from `bin` directory. + // Need to make the code in rpm spec happy as it assumes there is + // always something in application image. + fakeRuntimeDir.resolve("bin").toFile().mkdir(); + } + + if (TKit.isOSX()) { + // Make MacAppImageBuilder happy + createBulkFile.accept(fakeRuntimeDir.resolve(Path.of( + "Contents/Home/lib/jli/libjli.dylib"))); + } + + // Mak sure fake runtime takes some disk space. + // Package bundles with 0KB size are unexpected and considered + // an error by PackageTest. + createBulkFile.accept(fakeRuntimeDir.resolve(Path.of("bin", "bulk"))); + + cmd.addArguments("--runtime-image", fakeRuntimeDir); + }); + + return this; + } + + JPackageCommand addAction(ThrowingConsumer action) { + verifyMutable(); + actions.add(ThrowingConsumer.toConsumer(action)); + return this; + } + + /** + * Shorthand for {@code helloAppImage(null)}. + */ + public static JPackageCommand helloAppImage() { + JavaAppDesc javaAppDesc = null; + return helloAppImage(javaAppDesc); + } + + /** + * Creates new JPackageCommand instance configured with the test Java app. + * For the explanation of `javaAppDesc` parameter, see documentation for + * #JavaAppDesc.parse() method. + * + * @param javaAppDesc Java application description + * @return this + */ + public static JPackageCommand helloAppImage(String javaAppDesc) { + final JavaAppDesc appDesc; + if (javaAppDesc == null) { + appDesc = null; + } else { + appDesc = JavaAppDesc.parse(javaAppDesc); + } + return helloAppImage(appDesc); + } + + public static JPackageCommand helloAppImage(JavaAppDesc javaAppDesc) { + JPackageCommand cmd = new JPackageCommand(); + cmd.setDefaultInputOutput().setDefaultAppName(); + PackageType.IMAGE.applyTo(cmd); + new HelloApp(javaAppDesc).addTo(cmd); + return cmd; + } + + public JPackageCommand setPackageType(PackageType type) { + verifyMutable(); + type.applyTo(this); + return this; + } + + JPackageCommand setDefaultAppName() { + return addArguments("--name", TKit.getCurrentDefaultAppName()); + } + + /** + * Returns path to output bundle of configured jpackage command. + * + * If this is build image command, returns path to application image directory. + */ + public Path outputBundle() { + final String bundleName; + if (isImagePackageType()) { + String dirName = name(); + if (TKit.isOSX()) { + dirName = dirName + ".app"; + } + bundleName = dirName; + } else if (TKit.isLinux()) { + bundleName = LinuxHelper.getBundleName(this); + } else if (TKit.isWindows()) { + bundleName = WindowsHelper.getBundleName(this); + } else if (TKit.isOSX()) { + bundleName = MacHelper.getBundleName(this); + } else { + throw TKit.throwUnknownPlatformError(); + } + + return outputDir().resolve(bundleName); + } + + /** + * Returns application layout. + * + * If this is build image command, returns application image layout of the + * output bundle relative to output directory. Otherwise returns layout of + * installed application relative to the root directory. + * + * If this command builds Java runtime, not an application, returns + * corresponding layout. + */ + public ApplicationLayout appLayout() { + final ApplicationLayout layout; + if (isRuntime()) { + layout = ApplicationLayout.javaRuntime(); + } else { + layout = ApplicationLayout.platformAppImage(); + } + + if (isImagePackageType()) { + return layout.resolveAt(outputBundle()); + } + + return layout.resolveAt(appInstallationDirectory()); + } + + /** + * Returns path to directory where application will be installed or null if + * this is build image command. + * + * E.g. on Linux for app named Foo default the function will return + * `/opt/foo` + */ + public Path appInstallationDirectory() { + if (isImagePackageType()) { + return null; + } + + if (TKit.isLinux()) { + if (isRuntime()) { + // Not fancy, but OK. + return Path.of(getArgumentValue("--install-dir", () -> "/opt"), + LinuxHelper.getPackageName(this)); + } + + // Launcher is in "bin" subfolder of the installation directory. + return appLauncherPath().getParent().getParent(); + } + + if (TKit.isWindows()) { + return WindowsHelper.getInstallationDirectory(this); + } + + if (TKit.isOSX()) { + return MacHelper.getInstallationDirectory(this); + } + + throw TKit.throwUnknownPlatformError(); + } + + /** + * Returns path to application's Java runtime. + * If the command will package Java runtime only, returns correct path to + * runtime directory. + * + * E.g.: + * [jpackage --name Foo --type rpm] -> `/opt/foo/lib/runtime` + * [jpackage --name Foo --type app-image --dest bar] -> `bar/Foo/lib/runtime` + * [jpackage --name Foo --type rpm --runtime-image java] -> `/opt/foo` + */ + public Path appRuntimeDirectory() { + return appLayout().runtimeDirectory(); + } + + /** + * Returns path for application launcher with the given name. + * + * E.g.: [jpackage --name Foo --type rpm] -> `/opt/foo/bin/Foo` + * [jpackage --name Foo --type app-image --dest bar] -> + * `bar/Foo/bin/Foo` + * + * @param launcherName name of launcher or {@code null} for the main + * launcher + * + * @throws IllegalArgumentException if the command is configured for + * packaging Java runtime + */ + public Path appLauncherPath(String launcherName) { + verifyNotRuntime(); + if (launcherName == null) { + launcherName = name(); + } + + if (TKit.isWindows()) { + launcherName = launcherName + ".exe"; + } + + if (isImagePackageType()) { + return appLayout().launchersDirectory().resolve(launcherName); + } + + if (TKit.isLinux()) { + return LinuxHelper.getLauncherPath(this).getParent().resolve(launcherName); + } + + return appLayout().launchersDirectory().resolve(launcherName); + } + + /** + * Shorthand for {@code appLauncherPath(null)}. + */ + public Path appLauncherPath() { + return appLauncherPath(null); + } + + private void verifyNotRuntime() { + if (isRuntime()) { + throw new IllegalArgumentException("Java runtime packaging"); + } + } + + /** + * Returns path to .cfg file of the given application launcher. + * + * E.g.: + * [jpackage --name Foo --type rpm] -> `/opt/foo/lib/app/Foo.cfg` + * [jpackage --name Foo --type app-image --dest bar] -> `bar/Foo/lib/app/Foo.cfg` + * + * @param launcher name of launcher or {@code null} for the main launcher + * + * @throws IllegalArgumentException if the command is configured for + * packaging Java runtime + */ + public Path appLauncherCfgPath(String launcherName) { + verifyNotRuntime(); + if (launcherName == null) { + launcherName = name(); + } + return appLayout().appDirectory().resolve(launcherName + ".cfg"); + } + + public boolean isFakeRuntime(String msg) { + Path runtimeDir = appRuntimeDirectory(); + + final Collection criticalRuntimeFiles; + if (TKit.isWindows()) { + criticalRuntimeFiles = WindowsHelper.CRITICAL_RUNTIME_FILES; + } else if (TKit.isLinux()) { + criticalRuntimeFiles = LinuxHelper.CRITICAL_RUNTIME_FILES; + } else if (TKit.isOSX()) { + criticalRuntimeFiles = MacHelper.CRITICAL_RUNTIME_FILES; + } else { + throw TKit.throwUnknownPlatformError(); + } + + if (criticalRuntimeFiles.stream().filter( + v -> runtimeDir.resolve(v).toFile().exists()).findFirst().orElse( + null) == null) { + // Fake runtime + TKit.trace(String.format( + "%s because application runtime directory [%s] is incomplete", + msg, runtimeDir)); + return true; + } + return false; + } + + public static void useToolProviderByDefault() { + defaultWithToolProvider = true; + } + + public static void useExecutableByDefault() { + defaultWithToolProvider = false; + } + + public JPackageCommand useToolProvider(boolean v) { + verifyMutable(); + withToolProvider = v; + return this; + } + + public JPackageCommand saveConsoleOutput(boolean v) { + verifyMutable(); + saveConsoleOutput = v; + return this; + } + + public JPackageCommand dumpOutput(boolean v) { + verifyMutable(); + suppressOutput = !v; + return this; + } + + public JPackageCommand ignoreDefaultRuntime(boolean v) { + verifyMutable(); + ignoreDefaultRuntime = v; + return this; + } + + public boolean isWithToolProvider() { + return Optional.ofNullable(withToolProvider).orElse( + defaultWithToolProvider); + } + + public JPackageCommand executePrerequisiteActions() { + verifyMutable(); + if (!actionsExecuted) { + actionsExecuted = true; + if (actions != null) { + actions.stream().forEach(r -> r.accept(this)); + } + } + return this; + } + + public Executor createExecutor() { + verifyMutable(); + Executor exec = new Executor() + .saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput) + .addArguments(args); + + if (isWithToolProvider()) { + exec.setToolProvider(JavaTool.JPACKAGE); + } else { + exec.setExecutable(JavaTool.JPACKAGE); + } + + return exec; + } + + public Executor.Result execute() { + executePrerequisiteActions(); + + if (isImagePackageType()) { + TKit.deleteDirectoryContentsRecursive(outputDir()); + } + + return new JPackageCommand(this) + .adjustArgumentsBeforeExecution() + .createExecutor() + .execute(); + } + + public JPackageCommand executeAndAssertHelloAppImageCreated() { + executeAndAssertImageCreated(); + HelloApp.executeLauncherAndVerifyOutput(this); + return this; + } + + public JPackageCommand executeAndAssertImageCreated() { + execute().assertExitCodeIsZero(); + return assertImageCreated(); + } + + public JPackageCommand assertImageCreated() { + verifyIsOfType(PackageType.IMAGE); + TKit.assertDirectoryExists(appRuntimeDirectory()); + + if (!isRuntime()) { + TKit.assertExecutableFileExists(appLauncherPath()); + TKit.assertFileExists(appLauncherCfgPath(null)); + } + + return this; + } + + private JPackageCommand adjustArgumentsBeforeExecution() { + if (!hasArgument("--runtime-image") && !hasArgument("--app-image") && DEFAULT_RUNTIME_IMAGE != null && !ignoreDefaultRuntime) { + addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE); + } + + if (!hasArgument("--verbose") && TKit.VERBOSE_JPACKAGE) { + addArgument("--verbose"); + } + + return this; + } + + String getPrintableCommandLine() { + return new Executor() + .setExecutable(JavaTool.JPACKAGE) + .addArguments(args) + .getPrintableCommandLine(); + } + + public void verifyIsOfType(Collection types) { + verifyIsOfType(types.toArray(PackageType[]::new)); + } + + public void verifyIsOfType(PackageType ... types) { + final var typesSet = Stream.of(types).collect(Collectors.toSet()); + if (!hasArgument("--type")) { + if (!isImagePackageType()) { + if (TKit.isLinux() && typesSet.equals(PackageType.LINUX)) { + return; + } + + if (TKit.isWindows() && typesSet.equals(PackageType.WINDOWS)) { + return; + } + + if (TKit.isOSX() && typesSet.equals(PackageType.MAC)) { + return; + } + } else if (typesSet.equals(Set.of(PackageType.IMAGE))) { + return; + } + } + + if (!typesSet.contains(packageType())) { + throw new IllegalArgumentException("Unexpected type"); + } + } + + public CfgFile readLaunherCfgFile() { + return readLaunherCfgFile(null); + } + + public CfgFile readLaunherCfgFile(String launcherName) { + verifyIsOfType(PackageType.IMAGE); + if (isRuntime()) { + return null; + } + return ThrowingFunction.toFunction(CfgFile::readFromFile).apply( + appLauncherCfgPath(launcherName)); + } + + public static String escapeAndJoin(String... args) { + return escapeAndJoin(List.of(args)); + } + + public static String escapeAndJoin(List args) { + Pattern whitespaceRegexp = Pattern.compile("\\s"); + + return args.stream().map(v -> { + String str = v; + // Escape quotes. + str = str.replace("\"", "\\\""); + // Escape backslashes. + str = str.replace("\\", "\\\\"); + // If value contains whitespace characters, put the value in quotes + if (whitespaceRegexp.matcher(str).find()) { + str = "\"" + str + "\""; + } + return str; + }).collect(Collectors.joining(" ")); + } + + public static Path relativePathInRuntime(JavaTool tool) { + Path path = tool.relativePathInJavaHome(); + if (TKit.isOSX()) { + path = Path.of("Contents/Home").resolve(path); + } + return path; + } + + public static Stream filterOutput(Stream jpackageOutput) { + // Skip "WARNING: Using incubator ..." first line of output + return jpackageOutput.skip(1); + } + + public static List filterOutput(List jpackageOutput) { + return filterOutput(jpackageOutput.stream()).collect(Collectors.toList()); + } + + @Override + protected boolean isMutable() { + return !immutable; + } + + private Boolean withToolProvider; + private boolean saveConsoleOutput; + private boolean suppressOutput; + private boolean ignoreDefaultRuntime; + private boolean immutable; + private boolean actionsExecuted; + private final List> actions; + private static boolean defaultWithToolProvider; + + private final static Map PACKAGE_TYPES = Functional.identity( + () -> { + Map reply = new HashMap<>(); + for (PackageType type : PackageType.values()) { + reply.put(type.getName(), type); + } + return reply; + }).get(); + + public final static Path DEFAULT_RUNTIME_IMAGE = Functional.identity(() -> { + // Set the property to the path of run-time image to speed up + // building app images and platform bundles by avoiding running jlink + // The value of the property will be automativcally appended to + // jpackage command line if the command line doesn't have + // `--runtime-image` parameter set. + String val = TKit.getConfigProperty("runtime-image"); + if (val != null) { + return Path.of(val); + } + return null; + }).get(); +}