1 /*
   2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 package jdk.jpackage.test;
  24 
  25 import java.io.FileOutputStream;
  26 import java.io.IOException;
  27 import java.nio.file.Files;
  28 import java.nio.file.Path;
  29 import java.security.SecureRandom;
  30 import java.util.*;
  31 import java.util.function.Consumer;
  32 import java.util.function.Function;
  33 import java.util.function.Predicate;
  34 import java.util.function.Supplier;
  35 import java.util.regex.Pattern;
  36 import java.util.stream.Collectors;
  37 import java.util.stream.Stream;
  38 import jdk.incubator.jpackage.internal.ApplicationLayout;
  39 import jdk.jpackage.test.Functional.ThrowingConsumer;
  40 import jdk.jpackage.test.Functional.ThrowingFunction;
  41 import jdk.jpackage.test.Functional.ThrowingSupplier;
  42 
  43 /**
  44  * jpackage command line with prerequisite actions. Prerequisite actions can be
  45  * anything. The simplest is to compile test application and pack in a jar for
  46  * use on jpackage command line.
  47  */
  48 public final class JPackageCommand extends CommandArguments<JPackageCommand> {
  49 
  50     public JPackageCommand() {
  51         prerequisiteActions = new Actions();
  52         verifyActions = new Actions();
  53     }
  54 
  55     public JPackageCommand(JPackageCommand cmd) {
  56         args.addAll(cmd.args);
  57         withToolProvider = cmd.withToolProvider;
  58         saveConsoleOutput = cmd.saveConsoleOutput;
  59         suppressOutput = cmd.suppressOutput;
  60         ignoreDefaultRuntime = cmd.ignoreDefaultRuntime;
  61         ignoreDefaultVerbose = cmd.ignoreDefaultVerbose;
  62         immutable = cmd.immutable;
  63         prerequisiteActions = new Actions(cmd.prerequisiteActions);
  64         verifyActions = new Actions(cmd.verifyActions);
  65     }
  66 
  67     JPackageCommand createImmutableCopy() {
  68         JPackageCommand reply = new JPackageCommand(this);
  69         reply.immutable = true;
  70         return reply;
  71     }
  72 
  73     public JPackageCommand setArgumentValue(String argName, String newValue) {
  74         verifyMutable();
  75 
  76         String prevArg = null;
  77         ListIterator<String> it = args.listIterator();
  78         while (it.hasNext()) {
  79             String value = it.next();
  80             if (prevArg != null && prevArg.equals(argName)) {
  81                 if (newValue != null) {
  82                     it.set(newValue);
  83                 } else {
  84                     it.remove();
  85                     it.previous();
  86                     it.remove();
  87                 }
  88                 return this;
  89             }
  90             prevArg = value;
  91         }
  92 
  93         if (newValue != null) {
  94             addArguments(argName, newValue);
  95         }
  96 
  97         return this;
  98     }
  99 
 100     public JPackageCommand setArgumentValue(String argName, Path newValue) {
 101         return setArgumentValue(argName, newValue.toString());
 102     }
 103 
 104     public JPackageCommand removeArgumentWithValue(String argName) {
 105         return setArgumentValue(argName, (String)null);
 106     }
 107 
 108     public JPackageCommand removeArgument(String argName) {
 109         args = args.stream().filter(arg -> !arg.equals(argName)).collect(
 110                 Collectors.toList());
 111         return this;
 112     }
 113 
 114     public boolean hasArgument(String argName) {
 115         return args.contains(argName);
 116     }
 117 
 118     public <T> T getArgumentValue(String argName,
 119             Function<JPackageCommand, T> defaultValueSupplier,
 120             Function<String, T> stringConverter) {
 121         String prevArg = null;
 122         for (String arg : args) {
 123             if (prevArg != null && prevArg.equals(argName)) {
 124                 return stringConverter.apply(arg);
 125             }
 126             prevArg = arg;
 127         }
 128         if (defaultValueSupplier != null) {
 129             return defaultValueSupplier.apply(this);
 130         }
 131         return null;
 132     }
 133 
 134     public String getArgumentValue(String argName,
 135             Function<JPackageCommand, String> defaultValueSupplier) {
 136         return getArgumentValue(argName, defaultValueSupplier, v -> v);
 137     }
 138 
 139     public <T> T getArgumentValue(String argName,
 140             Supplier<T> defaultValueSupplier,
 141             Function<String, T> stringConverter) {
 142         return getArgumentValue(argName, (unused) -> defaultValueSupplier.get(),
 143                 stringConverter);
 144     }
 145 
 146     public String getArgumentValue(String argName,
 147             Supplier<String> defaultValueSupplier) {
 148         return getArgumentValue(argName, defaultValueSupplier, v -> v);
 149     }
 150 
 151     public String getArgumentValue(String argName) {
 152         return getArgumentValue(argName, (Supplier<String>)null);
 153     }
 154 
 155     public String[] getAllArgumentValues(String argName) {
 156         List<String> values = new ArrayList<>();
 157         String prevArg = null;
 158         for (String arg : args) {
 159             if (prevArg != null && prevArg.equals(argName)) {
 160                 values.add(arg);
 161             }
 162             prevArg = arg;
 163         }
 164         return values.toArray(String[]::new);
 165     }
 166 
 167     public JPackageCommand addArguments(String name, Path value) {
 168         return addArguments(name, value.toString());
 169     }
 170 
 171     public boolean isImagePackageType() {
 172         return PackageType.IMAGE == getArgumentValue("--type",
 173                 () -> null, PACKAGE_TYPES::get);
 174     }
 175 
 176     public PackageType packageType() {
 177         // Don't try to be in sync with jpackage defaults. Keep it simple:
 178         // if no `--type` explicitely set on the command line, consider
 179         // this is operator's fault.
 180         return getArgumentValue("--type",
 181                 () -> {
 182                     throw new IllegalStateException("Package type not set");
 183                 }, PACKAGE_TYPES::get);
 184     }
 185 
 186     public Path outputDir() {
 187         return getArgumentValue("--dest", () -> Path.of("."), Path::of);
 188     }
 189 
 190     public Path inputDir() {
 191         return getArgumentValue("--input", () -> null, Path::of);
 192     }
 193 
 194     public String version() {
 195         return getArgumentValue("--app-version", () -> "1.0");
 196     }
 197 
 198     public String name() {
 199         return getArgumentValue("--name", () -> getArgumentValue("--main-class"));
 200     }
 201 
 202     public boolean isRuntime() {
 203         return  hasArgument("--runtime-image")
 204                 && !hasArgument("--main-jar")
 205                 && !hasArgument("--module")
 206                 && !hasArgument("--app-image");
 207     }
 208 
 209     public JPackageCommand setDefaultInputOutput() {
 210         setArgumentValue("--input", TKit.workDir().resolve("input"));
 211         setArgumentValue("--dest", TKit.workDir().resolve("output"));
 212         return this;
 213     }
 214 
 215     public JPackageCommand setFakeRuntime() {
 216         verifyMutable();
 217 
 218         ThrowingConsumer<Path> createBulkFile = path -> {
 219             Files.createDirectories(path.getParent());
 220             try (FileOutputStream out = new FileOutputStream(path.toFile())) {
 221                 byte[] bytes = new byte[4 * 1024];
 222                 new SecureRandom().nextBytes(bytes);
 223                 out.write(bytes);
 224             }
 225         };
 226 
 227         addPrerequisiteAction(cmd -> {
 228             Path fakeRuntimeDir = TKit.workDir().resolve("fake_runtime");
 229 
 230             TKit.trace(String.format("Init fake runtime in [%s] directory",
 231                     fakeRuntimeDir));
 232 
 233             Files.createDirectories(fakeRuntimeDir);
 234 
 235             if (TKit.isWindows() || TKit.isLinux()) {
 236                 // Needed to make WindowsAppBundler happy as it copies MSVC dlls
 237                 // from `bin` directory.
 238                 // Need to make the code in rpm spec happy as it assumes there is
 239                 // always something in application image.
 240                 fakeRuntimeDir.resolve("bin").toFile().mkdir();
 241             }
 242 
 243             if (TKit.isOSX()) {
 244                 // Make MacAppImageBuilder happy
 245                 createBulkFile.accept(fakeRuntimeDir.resolve(Path.of(
 246                         "Contents/Home/lib/jli/libjli.dylib")));
 247             }
 248 
 249             // Mak sure fake runtime takes some disk space.
 250             // Package bundles with 0KB size are unexpected and considered
 251             // an error by PackageTest.
 252             createBulkFile.accept(fakeRuntimeDir.resolve(Path.of("bin", "bulk")));
 253 
 254             cmd.addArguments("--runtime-image", fakeRuntimeDir);
 255         });
 256 
 257         return this;
 258     }
 259 
 260     JPackageCommand addPrerequisiteAction(ThrowingConsumer<JPackageCommand> action) {
 261         verifyMutable();
 262         prerequisiteActions.add(action);
 263         return this;
 264     }
 265 
 266     JPackageCommand addVerifyAction(ThrowingConsumer<JPackageCommand> action) {
 267         verifyMutable();
 268         verifyActions.add(action);
 269         return this;
 270     }
 271 
 272     /**
 273      * Shorthand for {@code helloAppImage(null)}.
 274      */
 275     public static JPackageCommand helloAppImage() {
 276         JavaAppDesc javaAppDesc = null;
 277         return helloAppImage(javaAppDesc);
 278     }
 279 
 280     /**
 281      * Creates new JPackageCommand instance configured with the test Java app.
 282      * For the explanation of `javaAppDesc` parameter, see documentation for
 283      * #JavaAppDesc.parse() method.
 284      *
 285      * @param javaAppDesc Java application description
 286      * @return this
 287      */
 288     public static JPackageCommand helloAppImage(String javaAppDesc) {
 289         final JavaAppDesc appDesc;
 290         if (javaAppDesc == null) {
 291             appDesc = null;
 292         } else {
 293             appDesc = JavaAppDesc.parse(javaAppDesc);
 294         }
 295         return helloAppImage(appDesc);
 296     }
 297 
 298     public static JPackageCommand helloAppImage(JavaAppDesc javaAppDesc) {
 299         JPackageCommand cmd = new JPackageCommand();
 300         cmd.setDefaultInputOutput().setDefaultAppName();
 301         PackageType.IMAGE.applyTo(cmd);
 302         new HelloApp(javaAppDesc).addTo(cmd);
 303         return cmd;
 304     }
 305 
 306     public JPackageCommand setPackageType(PackageType type) {
 307         verifyMutable();
 308         type.applyTo(this);
 309         return this;
 310     }
 311 
 312     JPackageCommand setDefaultAppName() {
 313         return addArguments("--name", TKit.getCurrentDefaultAppName());
 314     }
 315 
 316     /**
 317      * Returns path to output bundle of configured jpackage command.
 318      *
 319      * If this is build image command, returns path to application image directory.
 320      */
 321     public Path outputBundle() {
 322         final String bundleName;
 323         if (isImagePackageType()) {
 324             String dirName = name();
 325             if (TKit.isOSX()) {
 326                 dirName = dirName + ".app";
 327             }
 328             bundleName = dirName;
 329         } else if (TKit.isLinux()) {
 330             bundleName = LinuxHelper.getBundleName(this);
 331         } else if (TKit.isWindows()) {
 332             bundleName = WindowsHelper.getBundleName(this);
 333         } else if (TKit.isOSX()) {
 334             bundleName = MacHelper.getBundleName(this);
 335         } else {
 336             throw TKit.throwUnknownPlatformError();
 337         }
 338 
 339         return outputDir().resolve(bundleName);
 340     }
 341 
 342     /**
 343      * Returns application layout.
 344      *
 345      * If this is build image command, returns application image layout of the
 346      * output bundle relative to output directory. Otherwise returns layout of
 347      * installed application relative to the root directory.
 348      *
 349      * If this command builds Java runtime, not an application, returns
 350      * corresponding layout.
 351      */
 352     public ApplicationLayout appLayout() {
 353         final ApplicationLayout layout;
 354         if (isRuntime()) {
 355             layout = ApplicationLayout.javaRuntime();
 356         } else {
 357             layout = ApplicationLayout.platformAppImage();
 358         }
 359 
 360         if (isImagePackageType()) {
 361             return layout.resolveAt(outputBundle());
 362         }
 363 
 364         return layout.resolveAt(appInstallationDirectory());
 365     }
 366 
 367     /**
 368      * Returns path to directory where application will be installed or null if
 369      * this is build image command.
 370      *
 371      * E.g. on Linux for app named Foo default the function will return
 372      * `/opt/foo`
 373      */
 374     public Path appInstallationDirectory() {
 375         Path unpackedDir = getArgumentValue(UNPACKED_PATH_ARGNAME, () -> null,
 376                 Path::of);
 377         if (unpackedDir != null) {
 378             return unpackedDir;
 379         }
 380 
 381         if (isImagePackageType()) {
 382             return null;
 383         }
 384 
 385         if (TKit.isLinux()) {
 386             if (isRuntime()) {
 387                 // Not fancy, but OK.
 388                 return Path.of(getArgumentValue("--install-dir", () -> "/opt"),
 389                         LinuxHelper.getPackageName(this));
 390             }
 391 
 392             // Launcher is in "bin" subfolder of the installation directory.
 393             return appLauncherPath().getParent().getParent();
 394         }
 395 
 396         if (TKit.isWindows()) {
 397             return WindowsHelper.getInstallationDirectory(this);
 398         }
 399 
 400         if (TKit.isOSX()) {
 401             return MacHelper.getInstallationDirectory(this);
 402         }
 403 
 404         throw TKit.throwUnknownPlatformError();
 405     }
 406 
 407     /**
 408      * Returns path to application's Java runtime.
 409      * If the command will package Java runtime only, returns correct path to
 410      * runtime directory.
 411      *
 412      * E.g.:
 413      * [jpackage --name Foo --type rpm] -> `/opt/foo/lib/runtime`
 414      * [jpackage --name Foo --type app-image --dest bar] -> `bar/Foo/lib/runtime`
 415      * [jpackage --name Foo --type rpm --runtime-image java] -> `/opt/foo`
 416      */
 417     public Path appRuntimeDirectory() {
 418         return appLayout().runtimeDirectory();
 419     }
 420 
 421     /**
 422      * Returns path for application launcher with the given name.
 423      *
 424      * E.g.: [jpackage --name Foo --type rpm] -> `/opt/foo/bin/Foo`
 425      * [jpackage --name Foo --type app-image --dest bar] ->
 426      * `bar/Foo/bin/Foo`
 427      *
 428      * @param launcherName name of launcher or {@code null} for the main
 429      * launcher
 430      *
 431      * @throws IllegalArgumentException if the command is configured for
 432      * packaging Java runtime
 433      */
 434     public Path appLauncherPath(String launcherName) {
 435         verifyNotRuntime();
 436         if (launcherName == null) {
 437             launcherName = name();
 438         }
 439 
 440         if (TKit.isWindows()) {
 441             launcherName = launcherName + ".exe";
 442         }
 443 
 444         if (isImagePackageType() || isPackageUnpacked()) {
 445             return appLayout().launchersDirectory().resolve(launcherName);
 446         }
 447 
 448         if (TKit.isLinux()) {
 449             return LinuxHelper.getLauncherPath(this).getParent().resolve(launcherName);
 450         }
 451 
 452         return appLayout().launchersDirectory().resolve(launcherName);
 453     }
 454 
 455     /**
 456      * Shorthand for {@code appLauncherPath(null)}.
 457      */
 458     public Path appLauncherPath() {
 459         return appLauncherPath(null);
 460     }
 461 
 462     private void verifyNotRuntime() {
 463         if (isRuntime()) {
 464             throw new IllegalArgumentException("Java runtime packaging");
 465         }
 466     }
 467 
 468     /**
 469      * Returns path to .cfg file of the given application launcher.
 470      *
 471      * E.g.:
 472      * [jpackage --name Foo --type rpm] -> `/opt/foo/lib/app/Foo.cfg`
 473      * [jpackage --name Foo --type app-image --dest bar] -> `bar/Foo/lib/app/Foo.cfg`
 474      *
 475      * @param launcher name of launcher or {@code null} for the main launcher
 476      *
 477      * @throws IllegalArgumentException if the command is configured for
 478      * packaging Java runtime
 479      */
 480     public Path appLauncherCfgPath(String launcherName) {
 481         verifyNotRuntime();
 482         if (launcherName == null) {
 483             launcherName = name();
 484         }
 485         return appLayout().appDirectory().resolve(launcherName + ".cfg");
 486     }
 487 
 488     public boolean isFakeRuntime(String msg) {
 489         Path runtimeDir = appRuntimeDirectory();
 490 
 491         final Collection<Path> criticalRuntimeFiles;
 492         if (TKit.isWindows()) {
 493             criticalRuntimeFiles = WindowsHelper.CRITICAL_RUNTIME_FILES;
 494         } else if (TKit.isLinux()) {
 495             criticalRuntimeFiles = LinuxHelper.CRITICAL_RUNTIME_FILES;
 496         } else if (TKit.isOSX()) {
 497             criticalRuntimeFiles = MacHelper.CRITICAL_RUNTIME_FILES;
 498         } else {
 499             throw TKit.throwUnknownPlatformError();
 500         }
 501 
 502         if (criticalRuntimeFiles.stream().filter(
 503                 v -> runtimeDir.resolve(v).toFile().exists()).findFirst().orElse(
 504                         null) == null) {
 505             // Fake runtime
 506             TKit.trace(String.format(
 507                     "%s because application runtime directory [%s] is incomplete",
 508                     msg, runtimeDir));
 509             return true;
 510         }
 511         return false;
 512     }
 513 
 514     public boolean isPackageUnpacked(String msg) {
 515         if (isPackageUnpacked()) {
 516             TKit.trace(String.format(
 517                     "%s because package was unpacked, not installed", msg));
 518             return true;
 519         }
 520         return false;
 521     }
 522 
 523     boolean isPackageUnpacked() {
 524         return hasArgument(UNPACKED_PATH_ARGNAME);
 525     }
 526 
 527     public static void useToolProviderByDefault() {
 528         defaultWithToolProvider = true;
 529     }
 530 
 531     public static void useExecutableByDefault() {
 532         defaultWithToolProvider = false;
 533     }
 534 
 535     public JPackageCommand useToolProvider(boolean v) {
 536         verifyMutable();
 537         withToolProvider = v;
 538         return this;
 539     }
 540 
 541     public JPackageCommand saveConsoleOutput(boolean v) {
 542         verifyMutable();
 543         saveConsoleOutput = v;
 544         return this;
 545     }
 546 
 547     public JPackageCommand dumpOutput(boolean v) {
 548         verifyMutable();
 549         suppressOutput = !v;
 550         return this;
 551     }
 552 
 553     public JPackageCommand ignoreDefaultRuntime(boolean v) {
 554         verifyMutable();
 555         ignoreDefaultRuntime = v;
 556         return this;
 557     }
 558 
 559     public JPackageCommand ignoreDefaultVerbose(boolean v) {
 560         verifyMutable();
 561         ignoreDefaultVerbose = v;
 562         return this;
 563     }
 564 
 565     public boolean isWithToolProvider() {
 566         return Optional.ofNullable(withToolProvider).orElse(
 567                 defaultWithToolProvider);
 568     }
 569 
 570     public JPackageCommand executePrerequisiteActions() {
 571         prerequisiteActions.run();
 572         return this;
 573     }
 574 
 575     public JPackageCommand executeVerifyActions() {
 576         verifyActions.run();
 577         return this;
 578     }
 579 
 580     private Executor createExecutor() {
 581         Executor exec = new Executor()
 582                 .saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput)
 583                 .addArguments(args);
 584 
 585         if (isWithToolProvider()) {
 586             exec.setToolProvider(JavaTool.JPACKAGE);
 587         } else {
 588             exec.setExecutable(JavaTool.JPACKAGE);
 589         }
 590 
 591         return exec;
 592     }
 593 
 594     public Executor.Result execute() {
 595         return execute(0);
 596     }
 597 
 598     public Executor.Result execute(int expectedExitCode) {
 599         executePrerequisiteActions();
 600 
 601         if (isImagePackageType()) {
 602             TKit.deleteDirectoryContentsRecursive(outputDir());
 603         } else if (ThrowingSupplier.toSupplier(() -> Files.deleteIfExists(
 604                 outputBundle())).get()) {
 605             TKit.trace(
 606                     String.format("Deleted [%s] file before running jpackage",
 607                             outputBundle()));
 608         }
 609 
 610         Path resourceDir = getArgumentValue("--resource-dir", () -> null, Path::of);
 611         if (resourceDir != null && Files.isDirectory(resourceDir)) {
 612             TKit.trace(String.format("Files in [%s] resource dir:",
 613                     resourceDir));
 614             try (var files = Files.walk(resourceDir, 1)) {
 615                 files.sequential()
 616                         .filter(Predicate.not(resourceDir::equals))
 617                         .map(path -> String.format("[%s]", path.getFileName()))
 618                         .forEachOrdered(TKit::trace);
 619                 TKit.trace("Done");
 620             } catch (IOException ex) {
 621                 TKit.trace(String.format(
 622                         "Failed to list files in [%s] resource directory: %s",
 623                         resourceDir, ex));
 624             }
 625         }
 626 
 627         Executor.Result result = new JPackageCommand(this)
 628                 .adjustArgumentsBeforeExecution()
 629                 .createExecutor()
 630                 .execute(expectedExitCode);
 631 
 632         if (result.exitCode == 0) {
 633             executeVerifyActions();
 634         }
 635 
 636         return result;
 637     }
 638 
 639     public Executor.Result executeAndAssertHelloAppImageCreated() {
 640         Executor.Result result = executeAndAssertImageCreated();
 641         HelloApp.executeLauncherAndVerifyOutput(this);
 642         return result;
 643     }
 644 
 645     public Executor.Result executeAndAssertImageCreated() {
 646         Executor.Result result = execute();
 647         assertImageCreated();
 648         return result;
 649     }
 650 
 651     public JPackageCommand assertImageCreated() {
 652         verifyIsOfType(PackageType.IMAGE);
 653         TKit.assertDirectoryExists(appRuntimeDirectory());
 654 
 655         if (!isRuntime()) {
 656             TKit.assertExecutableFileExists(appLauncherPath());
 657             TKit.assertFileExists(appLauncherCfgPath(null));
 658         }
 659 
 660         return this;
 661     }
 662 
 663     JPackageCommand setUnpackedPackageLocation(Path path) {
 664         verifyIsOfType(PackageType.NATIVE);
 665         setArgumentValue(UNPACKED_PATH_ARGNAME, path);
 666         return this;
 667     }
 668 
 669     private JPackageCommand adjustArgumentsBeforeExecution() {
 670         if (!hasArgument("--runtime-image") && !hasArgument("--app-image") && DEFAULT_RUNTIME_IMAGE != null && !ignoreDefaultRuntime) {
 671             addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE);
 672         }
 673 
 674         if (!hasArgument("--verbose") && TKit.VERBOSE_JPACKAGE && !ignoreDefaultVerbose) {
 675             addArgument("--verbose");
 676         }
 677 
 678         return this;
 679     }
 680 
 681     public String getPrintableCommandLine() {
 682         return createExecutor().getPrintableCommandLine();
 683     }
 684 
 685     public void verifyIsOfType(Collection<PackageType> types) {
 686         verifyIsOfType(types.toArray(PackageType[]::new));
 687     }
 688 
 689     public void verifyIsOfType(PackageType ... types) {
 690         final var typesSet = Stream.of(types).collect(Collectors.toSet());
 691         if (!hasArgument("--type")) {
 692             if (!isImagePackageType()) {
 693                 if (TKit.isLinux() && typesSet.equals(PackageType.LINUX)) {
 694                     return;
 695                 }
 696 
 697                 if (TKit.isWindows() && typesSet.equals(PackageType.WINDOWS)) {
 698                     return;
 699                 }
 700 
 701                 if (TKit.isOSX() && typesSet.equals(PackageType.MAC)) {
 702                     return;
 703                 }
 704             } else if (typesSet.equals(Set.of(PackageType.IMAGE))) {
 705                 return;
 706             }
 707         }
 708 
 709         if (!typesSet.contains(packageType())) {
 710             throw new IllegalArgumentException("Unexpected type");
 711         }
 712     }
 713 
 714     public CfgFile readLaunherCfgFile() {
 715         return readLaunherCfgFile(null);
 716     }
 717 
 718     public CfgFile readLaunherCfgFile(String launcherName) {
 719         verifyIsOfType(PackageType.IMAGE);
 720         if (isRuntime()) {
 721             return null;
 722         }
 723         return ThrowingFunction.toFunction(CfgFile::readFromFile).apply(
 724                 appLauncherCfgPath(launcherName));
 725     }
 726 
 727     public static String escapeAndJoin(String... args) {
 728         return escapeAndJoin(List.of(args));
 729     }
 730 
 731     public static String escapeAndJoin(List<String> args) {
 732         Pattern whitespaceRegexp = Pattern.compile("\\s");
 733 
 734         return args.stream().map(v -> {
 735             String str = v;
 736             // Escape quotes.
 737             str = str.replace("\"", "\\\"");
 738             // Escape backslashes.
 739             str = str.replace("\\", "\\\\");
 740             // If value contains whitespace characters, put the value in quotes
 741             if (whitespaceRegexp.matcher(str).find()) {
 742                 str = "\"" + str + "\"";
 743             }
 744             return str;
 745         }).collect(Collectors.joining(" "));
 746     }
 747 
 748     public static Path relativePathInRuntime(JavaTool tool) {
 749         Path path = tool.relativePathInJavaHome();
 750         if (TKit.isOSX()) {
 751             path = Path.of("Contents/Home").resolve(path);
 752         }
 753         return path;
 754     }
 755 
 756     public static Stream<String> filterOutput(Stream<String> jpackageOutput) {
 757         // Skip "WARNING: Using incubator ..." first line of output
 758         return jpackageOutput.skip(1);
 759     }
 760 
 761     public static List<String> filterOutput(List<String> jpackageOutput) {
 762         return filterOutput(jpackageOutput.stream()).collect(Collectors.toList());
 763     }
 764 
 765     @Override
 766     protected boolean isMutable() {
 767         return !immutable;
 768     }
 769 
 770     private final class Actions implements Runnable {
 771         Actions() {
 772             actions = new ArrayList<>();
 773         }
 774 
 775         Actions(Actions other) {
 776             this();
 777             actions.addAll(other.actions);
 778         }
 779 
 780         void add(ThrowingConsumer<JPackageCommand> action) {
 781             Objects.requireNonNull(action);
 782             verifyMutable();
 783             actions.add(new Consumer<JPackageCommand>() {
 784                 @Override
 785                 public void accept(JPackageCommand t) {
 786                     if (!executed) {
 787                         executed = true;
 788                         ThrowingConsumer.toConsumer(action).accept(t);
 789                     }
 790                 }
 791                 private boolean executed;
 792             });
 793         }
 794 
 795         @Override
 796         public void run() {
 797             verifyMutable();
 798             actions.forEach(action -> action.accept(JPackageCommand.this));
 799         }
 800 
 801         private final List<Consumer<JPackageCommand>> actions;
 802     }
 803 
 804     private Boolean withToolProvider;
 805     private boolean saveConsoleOutput;
 806     private boolean suppressOutput;
 807     private boolean ignoreDefaultRuntime;
 808     private boolean ignoreDefaultVerbose;
 809     private boolean immutable;
 810     private final Actions prerequisiteActions;
 811     private final Actions verifyActions;
 812     private static boolean defaultWithToolProvider;
 813 
 814     private final static Map<String, PackageType> PACKAGE_TYPES = Functional.identity(
 815             () -> {
 816                 Map<String, PackageType> reply = new HashMap<>();
 817                 for (PackageType type : PackageType.values()) {
 818                     reply.put(type.getName(), type);
 819                 }
 820                 return reply;
 821             }).get();
 822 
 823     public final static Path DEFAULT_RUNTIME_IMAGE = Functional.identity(() -> {
 824         // Set the property to the path of run-time image to speed up
 825         // building app images and platform bundles by avoiding running jlink
 826         // The value of the property will be automativcally appended to
 827         // jpackage command line if the command line doesn't have
 828         // `--runtime-image` parameter set.
 829         String val = TKit.getConfigProperty("runtime-image");
 830         if (val != null) {
 831             return Path.of(val);
 832         }
 833         return null;
 834     }).get();
 835 
 836     private final static String UNPACKED_PATH_ARGNAME = "jpt-unpacked-folder";
 837 }