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.Supplier;
  34 import java.util.regex.Matcher;
  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 
  42 /**
  43  * jpackage command line with prerequisite actions. Prerequisite actions can be
  44  * anything. The simplest is to compile test application and pack in a jar for
  45  * use on jpackage command line.
  46  */
  47 public final class JPackageCommand extends CommandArguments<JPackageCommand> {
  48 
  49     public JPackageCommand() {
  50         actions = new ArrayList<>();
  51     }
  52 
  53     public JPackageCommand(JPackageCommand cmd) {
  54         this();
  55         args.addAll(cmd.args);
  56         withToolProvider = cmd.withToolProvider;
  57         saveConsoleOutput = cmd.saveConsoleOutput;
  58         suppressOutput = cmd.suppressOutput;
  59         ignoreDefaultRuntime = cmd.ignoreDefaultRuntime;
  60         immutable = cmd.immutable;
  61         actionsExecuted = cmd.actionsExecuted;
  62     }
  63 
  64     JPackageCommand createImmutableCopy() {
  65         JPackageCommand reply = new JPackageCommand(this);
  66         reply.immutable = true;
  67         return reply;
  68     }
  69 
  70     public JPackageCommand setArgumentValue(String argName, String newValue) {
  71         verifyMutable();
  72 
  73         String prevArg = null;
  74         ListIterator<String> it = args.listIterator();
  75         while (it.hasNext()) {
  76             String value = it.next();
  77             if (prevArg != null && prevArg.equals(argName)) {
  78                 if (newValue != null) {
  79                     it.set(newValue);
  80                 } else {
  81                     it.remove();
  82                     it.previous();
  83                     it.remove();
  84                 }
  85                 return this;
  86             }
  87             prevArg = value;
  88         }
  89 
  90         if (newValue != null) {
  91             addArguments(argName, newValue);
  92         }
  93 
  94         return this;
  95     }
  96 
  97     public JPackageCommand setArgumentValue(String argName, Path newValue) {
  98         return setArgumentValue(argName, newValue.toString());
  99     }
 100 
 101     public JPackageCommand removeArgumentWithValue(String argName) {
 102         return setArgumentValue(argName, (String)null);
 103     }
 104 
 105     public JPackageCommand removeArgument(String argName) {
 106         args = args.stream().filter(arg -> !arg.equals(argName)).collect(
 107                 Collectors.toList());
 108         return this;
 109     }
 110 
 111     public boolean hasArgument(String argName) {
 112         return args.contains(argName);
 113     }
 114 
 115     public <T> T getArgumentValue(String argName,
 116             Function<JPackageCommand, T> defaultValueSupplier,
 117             Function<String, T> stringConverter) {
 118         String prevArg = null;
 119         for (String arg : args) {
 120             if (prevArg != null && prevArg.equals(argName)) {
 121                 return stringConverter.apply(arg);
 122             }
 123             prevArg = arg;
 124         }
 125         if (defaultValueSupplier != null) {
 126             return defaultValueSupplier.apply(this);
 127         }
 128         return null;
 129     }
 130 
 131     public String getArgumentValue(String argName,
 132             Function<JPackageCommand, String> defaultValueSupplier) {
 133         return getArgumentValue(argName, defaultValueSupplier, v -> v);
 134     }
 135 
 136     public <T> T getArgumentValue(String argName,
 137             Supplier<T> defaultValueSupplier,
 138             Function<String, T> stringConverter) {
 139         return getArgumentValue(argName, (unused) -> defaultValueSupplier.get(),
 140                 stringConverter);
 141     }
 142 
 143     public String getArgumentValue(String argName,
 144             Supplier<String> defaultValueSupplier) {
 145         return getArgumentValue(argName, defaultValueSupplier, v -> v);
 146     }
 147 
 148     public String getArgumentValue(String argName) {
 149         return getArgumentValue(argName, (Supplier<String>)null);
 150     }
 151 
 152     public String[] getAllArgumentValues(String argName) {
 153         List<String> values = new ArrayList<>();
 154         String prevArg = null;
 155         for (String arg : args) {
 156             if (prevArg != null && prevArg.equals(argName)) {
 157                 values.add(arg);
 158             }
 159             prevArg = arg;
 160         }
 161         return values.toArray(String[]::new);
 162     }
 163 
 164     public JPackageCommand addArguments(String name, Path value) {
 165         return addArguments(name, value.toString());
 166     }
 167 
 168     public boolean isImagePackageType() {
 169         return PackageType.IMAGE == getArgumentValue("--type",
 170                 () -> null, PACKAGE_TYPES::get);
 171     }
 172 
 173     public PackageType packageType() {
 174         // Don't try to be in sync with jpackage defaults. Keep it simple:
 175         // if no `--type` explicitely set on the command line, consider
 176         // this is operator's fault.
 177         return getArgumentValue("--type",
 178                 () -> {
 179                     throw new IllegalStateException("Package type not set");
 180                 }, PACKAGE_TYPES::get);
 181     }
 182 
 183     public Path outputDir() {
 184         return getArgumentValue("--dest", () -> Path.of("."), Path::of);
 185     }
 186 
 187     public Path inputDir() {
 188         return getArgumentValue("--input", () -> null, Path::of);
 189     }
 190 
 191     public String version() {
 192         return getArgumentValue("--app-version", () -> "1.0");
 193     }
 194 
 195     public String name() {
 196         return getArgumentValue("--name", () -> getArgumentValue("--main-class"));
 197     }
 198 
 199     public boolean isRuntime() {
 200         return  hasArgument("--runtime-image")
 201                 && !hasArgument("--main-jar")
 202                 && !hasArgument("--module")
 203                 && !hasArgument("--app-image");
 204     }
 205 
 206     public JPackageCommand setDefaultInputOutput() {
 207         addArguments("--input", TKit.defaultInputDir());
 208         addArguments("--dest", TKit.defaultOutputDir());
 209         return this;
 210     }
 211 
 212     public JPackageCommand setFakeRuntime() {
 213         verifyMutable();
 214 
 215         ThrowingConsumer<Path> createBulkFile = path -> {
 216             Files.createDirectories(path.getParent());
 217             try (FileOutputStream out = new FileOutputStream(path.toFile())) {
 218                 byte[] bytes = new byte[4 * 1024];
 219                 new SecureRandom().nextBytes(bytes);
 220                 out.write(bytes);
 221             }
 222         };
 223 
 224         addAction(cmd -> {
 225             Path fakeRuntimeDir = TKit.workDir().resolve("fake_runtime");
 226 
 227             TKit.trace(String.format("Init fake runtime in [%s] directory",
 228                     fakeRuntimeDir));
 229 
 230             Files.createDirectories(fakeRuntimeDir);
 231 
 232             if (TKit.isWindows() || TKit.isLinux()) {
 233                 // Needed to make WindowsAppBundler happy as it copies MSVC dlls
 234                 // from `bin` directory.
 235                 // Need to make the code in rpm spec happy as it assumes there is
 236                 // always something in application image.
 237                 fakeRuntimeDir.resolve("bin").toFile().mkdir();
 238             }
 239 
 240             if (TKit.isOSX()) {
 241                 // Make MacAppImageBuilder happy
 242                 createBulkFile.accept(fakeRuntimeDir.resolve(Path.of(
 243                         "Contents/Home/lib/jli/libjli.dylib")));
 244             }
 245 
 246             // Mak sure fake runtime takes some disk space.
 247             // Package bundles with 0KB size are unexpected and considered
 248             // an error by PackageTest.
 249             createBulkFile.accept(fakeRuntimeDir.resolve(Path.of("bin", "bulk")));
 250 
 251             cmd.addArguments("--runtime-image", fakeRuntimeDir);
 252         });
 253 
 254         return this;
 255     }
 256 
 257     JPackageCommand addAction(ThrowingConsumer<JPackageCommand> action) {
 258         verifyMutable();
 259         actions.add(ThrowingConsumer.toConsumer(action));
 260         return this;
 261     }
 262 
 263     /**
 264      * Shorthand for {@code helloAppImage(null)}.
 265      */
 266     public static JPackageCommand helloAppImage() {
 267         JavaAppDesc javaAppDesc = null;
 268         return helloAppImage(javaAppDesc);
 269     }
 270 
 271     /**
 272      * Creates new JPackageCommand instance configured with the test Java app.
 273      * For the explanation of `javaAppDesc` parameter, see documentation for
 274      * #JavaAppDesc.parse() method.
 275      *
 276      * @param javaAppDesc Java application description
 277      * @return this
 278      */
 279     public static JPackageCommand helloAppImage(String javaAppDesc) {
 280         final JavaAppDesc appDesc;
 281         if (javaAppDesc == null) {
 282             appDesc = null;
 283         } else {
 284             appDesc = JavaAppDesc.parse(javaAppDesc);
 285         }
 286         return helloAppImage(appDesc);
 287     }
 288 
 289     public static JPackageCommand helloAppImage(JavaAppDesc javaAppDesc) {
 290         JPackageCommand cmd = new JPackageCommand();
 291         cmd.setDefaultInputOutput().setDefaultAppName();
 292         PackageType.IMAGE.applyTo(cmd);
 293         new HelloApp(javaAppDesc).addTo(cmd);
 294         return cmd;
 295     }
 296 
 297     public JPackageCommand setPackageType(PackageType type) {
 298         verifyMutable();
 299         type.applyTo(this);
 300         return this;
 301     }
 302 
 303     JPackageCommand setDefaultAppName() {
 304         return addArguments("--name", TKit.getCurrentDefaultAppName());
 305     }
 306 
 307     /**
 308      * Returns path to output bundle of configured jpackage command.
 309      *
 310      * If this is build image command, returns path to application image directory.
 311      */
 312     public Path outputBundle() {
 313         final String bundleName;
 314         if (isImagePackageType()) {
 315             String dirName = name();
 316             if (TKit.isOSX()) {
 317                 dirName = dirName + ".app";
 318             }
 319             bundleName = dirName;
 320         } else if (TKit.isLinux()) {
 321             bundleName = LinuxHelper.getBundleName(this);
 322         } else if (TKit.isWindows()) {
 323             bundleName = WindowsHelper.getBundleName(this);
 324         } else if (TKit.isOSX()) {
 325             bundleName = MacHelper.getBundleName(this);
 326         } else {
 327             throw TKit.throwUnknownPlatformError();
 328         }
 329 
 330         return outputDir().resolve(bundleName);
 331     }
 332 
 333     /**
 334      * Returns application layout.
 335      *
 336      * If this is build image command, returns application image layout of the
 337      * output bundle relative to output directory. Otherwise returns layout of
 338      * installed application relative to the root directory.
 339      *
 340      * If this command builds Java runtime, not an application, returns
 341      * corresponding layout.
 342      */
 343     public ApplicationLayout appLayout() {
 344         final ApplicationLayout layout;
 345         if (isRuntime()) {
 346             layout = ApplicationLayout.javaRuntime();
 347         } else {
 348             layout = ApplicationLayout.platformAppImage();
 349         }
 350 
 351         if (isImagePackageType()) {
 352             return layout.resolveAt(outputBundle());
 353         }
 354 
 355         return layout.resolveAt(appInstallationDirectory());
 356     }
 357 
 358     /**
 359      * Returns path to directory where application will be installed or null if
 360      * this is build image command.
 361      *
 362      * E.g. on Linux for app named Foo default the function will return
 363      * `/opt/foo`
 364      */
 365     public Path appInstallationDirectory() {
 366         if (isImagePackageType()) {
 367             return null;
 368         }
 369 
 370         if (TKit.isLinux()) {
 371             if (isRuntime()) {
 372                 // Not fancy, but OK.
 373                 return Path.of(getArgumentValue("--install-dir", () -> "/opt"),
 374                         LinuxHelper.getPackageName(this));
 375             }
 376 
 377             // Launcher is in "bin" subfolder of the installation directory.
 378             return appLauncherPath().getParent().getParent();
 379         }
 380 
 381         if (TKit.isWindows()) {
 382             return WindowsHelper.getInstallationDirectory(this);
 383         }
 384 
 385         if (TKit.isOSX()) {
 386             return MacHelper.getInstallationDirectory(this);
 387         }
 388 
 389         throw TKit.throwUnknownPlatformError();
 390     }
 391 
 392     /**
 393      * Returns path to application's Java runtime.
 394      * If the command will package Java runtime only, returns correct path to
 395      * runtime directory.
 396      *
 397      * E.g.:
 398      * [jpackage --name Foo --type rpm] -> `/opt/foo/lib/runtime`
 399      * [jpackage --name Foo --type app-image --dest bar] -> `bar/Foo/lib/runtime`
 400      * [jpackage --name Foo --type rpm --runtime-image java] -> `/opt/foo`
 401      */
 402     public Path appRuntimeDirectory() {
 403         return appLayout().runtimeDirectory();
 404     }
 405 
 406     /**
 407      * Returns path for application launcher with the given name.
 408      *
 409      * E.g.: [jpackage --name Foo --type rpm] -> `/opt/foo/bin/Foo`
 410      * [jpackage --name Foo --type app-image --dest bar] ->
 411      * `bar/Foo/bin/Foo`
 412      *
 413      * @param launcherName name of launcher or {@code null} for the main
 414      * launcher
 415      *
 416      * @throws IllegalArgumentException if the command is configured for
 417      * packaging Java runtime
 418      */
 419     public Path appLauncherPath(String launcherName) {
 420         verifyNotRuntime();
 421         if (launcherName == null) {
 422             launcherName = name();
 423         }
 424 
 425         if (TKit.isWindows()) {
 426             launcherName = launcherName + ".exe";
 427         }
 428 
 429         if (isImagePackageType()) {
 430             return appLayout().launchersDirectory().resolve(launcherName);
 431         }
 432 
 433         if (TKit.isLinux()) {
 434             return LinuxHelper.getLauncherPath(this).getParent().resolve(launcherName);
 435         }
 436 
 437         return appLayout().launchersDirectory().resolve(launcherName);
 438     }
 439 
 440     /**
 441      * Shorthand for {@code appLauncherPath(null)}.
 442      */
 443     public Path appLauncherPath() {
 444         return appLauncherPath(null);
 445     }
 446 
 447     private void verifyNotRuntime() {
 448         if (isRuntime()) {
 449             throw new IllegalArgumentException("Java runtime packaging");
 450         }
 451     }
 452 
 453     /**
 454      * Returns path to .cfg file of the given application launcher.
 455      *
 456      * E.g.:
 457      * [jpackage --name Foo --type rpm] -> `/opt/foo/lib/app/Foo.cfg`
 458      * [jpackage --name Foo --type app-image --dest bar] -> `bar/Foo/lib/app/Foo.cfg`
 459      *
 460      * @param launcher name of launcher or {@code null} for the main launcher
 461      *
 462      * @throws IllegalArgumentException if the command is configured for
 463      * packaging Java runtime
 464      */
 465     public Path appLauncherCfgPath(String launcherName) {
 466         verifyNotRuntime();
 467         if (launcherName == null) {
 468             launcherName = name();
 469         }
 470         return appLayout().appDirectory().resolve(launcherName + ".cfg");
 471     }
 472 
 473     public boolean isFakeRuntime(String msg) {
 474         Path runtimeDir = appRuntimeDirectory();
 475 
 476         final Collection<Path> criticalRuntimeFiles;
 477         if (TKit.isWindows()) {
 478             criticalRuntimeFiles = WindowsHelper.CRITICAL_RUNTIME_FILES;
 479         } else if (TKit.isLinux()) {
 480             criticalRuntimeFiles = LinuxHelper.CRITICAL_RUNTIME_FILES;
 481         } else if (TKit.isOSX()) {
 482             criticalRuntimeFiles = MacHelper.CRITICAL_RUNTIME_FILES;
 483         } else {
 484             throw TKit.throwUnknownPlatformError();
 485         }
 486 
 487         if (criticalRuntimeFiles.stream().filter(
 488                 v -> runtimeDir.resolve(v).toFile().exists()).findFirst().orElse(
 489                         null) == null) {
 490             // Fake runtime
 491             TKit.trace(String.format(
 492                     "%s because application runtime directory [%s] is incomplete",
 493                     msg, runtimeDir));
 494             return true;
 495         }
 496         return false;
 497     }
 498 
 499     public static void useToolProviderByDefault() {
 500         defaultWithToolProvider = true;
 501     }
 502 
 503     public static void useExecutableByDefault() {
 504         defaultWithToolProvider = false;
 505     }
 506 
 507     public JPackageCommand useToolProvider(boolean v) {
 508         verifyMutable();
 509         withToolProvider = v;
 510         return this;
 511     }
 512 
 513     public JPackageCommand saveConsoleOutput(boolean v) {
 514         verifyMutable();
 515         saveConsoleOutput = v;
 516         return this;
 517     }
 518 
 519     public JPackageCommand dumpOutput(boolean v) {
 520         verifyMutable();
 521         suppressOutput = !v;
 522         return this;
 523     }
 524 
 525     public JPackageCommand ignoreDefaultRuntime(boolean v) {
 526         verifyMutable();
 527         ignoreDefaultRuntime = v;
 528         return this;
 529     }
 530 
 531     public boolean isWithToolProvider() {
 532         return Optional.ofNullable(withToolProvider).orElse(
 533                 defaultWithToolProvider);
 534     }
 535 
 536     public JPackageCommand executePrerequisiteActions() {
 537         verifyMutable();
 538         if (!actionsExecuted) {
 539             actionsExecuted = true;
 540             if (actions != null) {
 541                 actions.stream().forEach(r -> r.accept(this));
 542             }
 543         }
 544         return this;
 545     }
 546 
 547     public Executor createExecutor() {
 548         verifyMutable();
 549         Executor exec = new Executor()
 550                 .saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput)
 551                 .addArguments(args);
 552 
 553         if (isWithToolProvider()) {
 554             exec.setToolProvider(JavaTool.JPACKAGE);
 555         } else {
 556             exec.setExecutable(JavaTool.JPACKAGE);
 557         }
 558 
 559         return exec;
 560     }
 561 
 562     public Executor.Result execute() {
 563         executePrerequisiteActions();
 564 
 565         if (isImagePackageType()) {
 566             TKit.deleteDirectoryContentsRecursive(outputDir());
 567         }
 568 
 569         return new JPackageCommand(this)
 570                 .adjustArgumentsBeforeExecution()
 571                 .createExecutor()
 572                 .execute();
 573     }
 574 
 575     public JPackageCommand executeAndAssertHelloAppImageCreated() {
 576         executeAndAssertImageCreated();
 577         HelloApp.executeLauncherAndVerifyOutput(this);
 578         return this;
 579     }
 580 
 581     public JPackageCommand executeAndAssertImageCreated() {
 582         execute().assertExitCodeIsZero();
 583         return assertImageCreated();
 584     }
 585 
 586     public JPackageCommand assertImageCreated() {
 587         verifyIsOfType(PackageType.IMAGE);
 588         TKit.assertDirectoryExists(appRuntimeDirectory());
 589 
 590         if (!isRuntime()) {
 591             TKit.assertExecutableFileExists(appLauncherPath());
 592             TKit.assertFileExists(appLauncherCfgPath(null));
 593         }
 594 
 595         return this;
 596     }
 597 
 598     private JPackageCommand adjustArgumentsBeforeExecution() {
 599         if (!hasArgument("--runtime-image") && !hasArgument("--app-image") && DEFAULT_RUNTIME_IMAGE != null && !ignoreDefaultRuntime) {
 600             addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE);
 601         }
 602 
 603         if (!hasArgument("--verbose") && TKit.VERBOSE_JPACKAGE) {
 604             addArgument("--verbose");
 605         }
 606 
 607         return this;
 608     }
 609 
 610     String getPrintableCommandLine() {
 611         return new Executor()
 612                 .setExecutable(JavaTool.JPACKAGE)
 613                 .addArguments(args)
 614                 .getPrintableCommandLine();
 615     }
 616 
 617     public void verifyIsOfType(Collection<PackageType> types) {
 618         verifyIsOfType(types.toArray(PackageType[]::new));
 619     }
 620 
 621     public void verifyIsOfType(PackageType ... types) {
 622         final var typesSet = Stream.of(types).collect(Collectors.toSet());
 623         if (!hasArgument("--type")) {
 624             if (!isImagePackageType()) {
 625                 if (TKit.isLinux() && typesSet.equals(PackageType.LINUX)) {
 626                     return;
 627                 }
 628 
 629                 if (TKit.isWindows() && typesSet.equals(PackageType.WINDOWS)) {
 630                     return;
 631                 }
 632 
 633                 if (TKit.isOSX() && typesSet.equals(PackageType.MAC)) {
 634                     return;
 635                 }
 636             } else if (typesSet.equals(Set.of(PackageType.IMAGE))) {
 637                 return;
 638             }
 639         }
 640 
 641         if (!typesSet.contains(packageType())) {
 642             throw new IllegalArgumentException("Unexpected type");
 643         }
 644     }
 645 
 646     public CfgFile readLaunherCfgFile() {
 647         return readLaunherCfgFile(null);
 648     }
 649 
 650     public CfgFile readLaunherCfgFile(String launcherName) {
 651         verifyIsOfType(PackageType.IMAGE);
 652         if (isRuntime()) {
 653             return null;
 654         }
 655         return ThrowingFunction.toFunction(CfgFile::readFromFile).apply(
 656                 appLauncherCfgPath(launcherName));
 657     }
 658 
 659     public static String escapeAndJoin(String... args) {
 660         return escapeAndJoin(List.of(args));
 661     }
 662 
 663     public static String escapeAndJoin(List<String> args) {
 664         Pattern whitespaceRegexp = Pattern.compile("\\s");
 665 
 666         return args.stream().map(v -> {
 667             String str = v;
 668             // Escape quotes.
 669             str = str.replace("\"", "\\\"");
 670             // Escape backslashes.
 671             str = str.replace("\\", "\\\\");
 672             // If value contains whitespace characters, put the value in quotes
 673             if (whitespaceRegexp.matcher(str).find()) {
 674                 str = "\"" + str + "\"";
 675             }
 676             return str;
 677         }).collect(Collectors.joining(" "));
 678     }
 679 
 680     public static Path relativePathInRuntime(JavaTool tool) {
 681         Path path = tool.relativePathInJavaHome();
 682         if (TKit.isOSX()) {
 683             path = Path.of("Contents/Home").resolve(path);
 684         }
 685         return path;
 686     }
 687 
 688     public static Stream<String> filterOutput(Stream<String> jpackageOutput) {
 689         // Skip "WARNING: Using incubator ..." first line of output
 690         return jpackageOutput.skip(1);
 691     }
 692 
 693     public static List<String> filterOutput(List<String> jpackageOutput) {
 694         return filterOutput(jpackageOutput.stream()).collect(Collectors.toList());
 695     }
 696 
 697     @Override
 698     protected boolean isMutable() {
 699         return !immutable;
 700     }
 701 
 702     private Boolean withToolProvider;
 703     private boolean saveConsoleOutput;
 704     private boolean suppressOutput;
 705     private boolean ignoreDefaultRuntime;
 706     private boolean immutable;
 707     private boolean actionsExecuted;
 708     private final List<Consumer<JPackageCommand>> actions;
 709     private static boolean defaultWithToolProvider;
 710 
 711     private final static Map<String, PackageType> PACKAGE_TYPES = Functional.identity(
 712             () -> {
 713                 Map<String, PackageType> reply = new HashMap<>();
 714                 for (PackageType type : PackageType.values()) {
 715                     reply.put(type.getName(), type);
 716                 }
 717                 return reply;
 718             }).get();
 719 
 720     public final static Path DEFAULT_RUNTIME_IMAGE = Functional.identity(() -> {
 721         // Set the property to the path of run-time image to speed up
 722         // building app images and platform bundles by avoiding running jlink
 723         // The value of the property will be automativcally appended to
 724         // jpackage command line if the command line doesn't have
 725         // `--runtime-image` parameter set.
 726         String val = TKit.getConfigProperty("runtime-image");
 727         if (val != null) {
 728             return Path.of(val);
 729         }
 730         return null;
 731     }).get();
 732 }