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 }