1 /* 2 * Copyright (c) 2018, 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. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package jdk.jpackage.internal; 26 27 import java.io.File; 28 import java.io.FileInputStream; 29 import java.io.IOException; 30 import java.nio.file.Files; 31 import java.nio.file.Path; 32 import java.text.MessageFormat; 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.Collection; 36 import java.util.EnumSet; 37 import java.util.HashMap; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Set; 42 import java.util.Properties; 43 import java.util.ResourceBundle; 44 import java.util.jar.Attributes; 45 import java.util.jar.JarFile; 46 import java.util.jar.Manifest; 47 import java.util.stream.Stream; 48 import java.util.regex.Matcher; 49 import java.util.regex.Pattern; 50 51 /** 52 * Arguments 53 * 54 * This class encapsulates and processes the command line arguments, 55 * in effect, implementing all the work of jpackage tool. 56 * 57 * The primary entry point, processArguments(): 58 * Processes and validates command line arguments, constructing DeployParams. 59 * Validates the DeployParams, and generate the BundleParams. 60 * Generates List of Bundlers from BundleParams valid for this platform. 61 * Executes each Bundler in the list. 62 */ 63 public class Arguments { 64 private static final ResourceBundle I18N = ResourceBundle.getBundle( 65 "jdk.jpackage.internal.resources.MainResources"); 66 67 private static final String IMAGE_MODE = "image"; 68 private static final String INSTALLER_MODE = "installer"; 69 private static final String JRE_INSTALLER_MODE = "jre-installer"; 70 71 private static final String FA_EXTENSIONS = "extension"; 72 private static final String FA_CONTENT_TYPE = "mime-type"; 73 private static final String FA_DESCRIPTION = "description"; 74 private static final String FA_ICON = "icon"; 75 76 public static final BundlerParamInfo<Boolean> CREATE_IMAGE = 77 new StandardBundlerParam<>( 78 I18N.getString("param.create-image.name"), 79 I18N.getString("param.create-image.description"), 80 IMAGE_MODE, 81 Boolean.class, 82 p -> Boolean.FALSE, 83 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? 84 true : Boolean.valueOf(s)); 85 86 public static final BundlerParamInfo<Boolean> CREATE_INSTALLER = 87 new StandardBundlerParam<>( 88 I18N.getString("param.create-installer.name"), 89 I18N.getString("param.create-installer.description"), 90 INSTALLER_MODE, 91 Boolean.class, 92 p -> Boolean.FALSE, 93 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? 94 true : Boolean.valueOf(s)); 95 96 public static final BundlerParamInfo<Boolean> CREATE_JRE_INSTALLER = 97 new StandardBundlerParam<>( 98 I18N.getString("param.create-jre-installer.name"), 99 I18N.getString("param.create-jre-installer.description"), 100 JRE_INSTALLER_MODE, 101 Boolean.class, 102 p -> Boolean.FALSE, 103 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? 104 true : Boolean.valueOf(s)); 105 106 // regexp for parsing args (for example, for secondary launchers) 107 private static Pattern pattern = Pattern.compile( 108 "(?:(?:([\"'])(?:\\\\\\1|.)*?(?:\\1|$))|(?:\\\\[\"'\\s]|[^\\s]))++"); 109 110 private DeployParams deployParams = null; 111 private BundlerType bundleType = null; 112 113 private int pos = 0; 114 private List<String> argList = null; 115 116 private List<CLIOptions> allOptions = null; 117 118 private ArrayList<String> files = null; 119 120 private String input = null; 121 private String output = null; 122 123 private boolean hasMainJar = false; 124 private boolean hasMainClass = false; 125 private boolean hasMainModule = false; 126 private boolean hasTargetFormat = false; 127 private boolean hasAppImage = false; 128 private boolean retainBuildRoot = false; 129 130 private String buildRoot = null; 131 private String mainJarPath = null; 132 133 private static boolean jreInstaller = false; 134 135 private List<jdk.jpackage.internal.Bundler> platformBundlers = null; 136 137 private List<SecondaryLauncherArguments> secondaryLaunchers = null; 138 139 private static Map<String, CLIOptions> argIds = new HashMap<>(); 140 private static Map<String, CLIOptions> argShortIds = new HashMap<>(); 141 142 { 143 // init maps for parsing arguments 144 EnumSet<CLIOptions> options = EnumSet.allOf(CLIOptions.class); 145 146 options.forEach(option -> { 147 argIds.put(option.getIdWithPrefix(), option); 148 if (option.getShortIdWithPrefix() != null) { 149 argShortIds.put(option.getShortIdWithPrefix(), option); 150 } 151 }); 152 } 153 154 public Arguments(String[] args) { 155 initArgumentList(args); 156 } 157 158 // CLIOptions is public for DeployParamsTest 159 public enum CLIOptions { 160 CREATE_IMAGE(IMAGE_MODE, OptionCategories.MODE, () -> { 161 context().bundleType = BundlerType.IMAGE; 162 context().deployParams.setTargetFormat("image"); 163 setOptionValue(IMAGE_MODE, true); 164 }), 165 166 CREATE_INSTALLER(INSTALLER_MODE, OptionCategories.MODE, () -> { 167 setOptionValue(INSTALLER_MODE, true); 168 context().bundleType = BundlerType.INSTALLER; 169 String format = "installer"; 170 if (hasNextArg()) { 171 String arg = popArg(); 172 if (!arg.startsWith("-")) { 173 format = arg.toLowerCase(); 174 context().hasTargetFormat = true; 175 } else { 176 prevArg(); 177 } 178 } 179 context().deployParams.setTargetFormat(format); 180 }), 181 182 CREATE_JRE_INSTALLER(JRE_INSTALLER_MODE, OptionCategories.MODE, () -> { 183 setOptionValue(JRE_INSTALLER_MODE, true); 184 context().bundleType = BundlerType.INSTALLER; 185 String format = "installer"; 186 if (hasNextArg()) { 187 String arg = popArg(); 188 if (!arg.startsWith("-")) { 189 format = arg.toLowerCase(); 190 context().hasTargetFormat = true; 191 } else { 192 prevArg(); 193 } 194 } 195 jreInstaller = true; 196 context().deployParams.setTargetFormat(format); 197 context().deployParams.setJreInstaller(true); 198 }), 199 200 INPUT ("input", "i", OptionCategories.PROPERTY, () -> { 201 context().input = popArg(); 202 setOptionValue("input", context().input); 203 }), 204 205 OUTPUT ("output", "o", OptionCategories.PROPERTY, () -> { 206 context().output = popArg(); 207 context().deployParams.setOutput(new File(context().output)); 208 }), 209 210 DESCRIPTION ("description", "d", OptionCategories.PROPERTY), 211 212 VENDOR ("vendor", OptionCategories.PROPERTY), 213 214 APPCLASS ("class", "c", OptionCategories.PROPERTY, () -> { 215 context().hasMainClass = true; 216 setOptionValue("class", popArg()); 217 }), 218 219 NAME ("name", "n", OptionCategories.PROPERTY), 220 221 IDENTIFIER ("identifier", OptionCategories.PROPERTY), 222 223 VERBOSE ("verbose", OptionCategories.PROPERTY, () -> { 224 setOptionValue("verbose", true); 225 Log.setVerbose(true); 226 }), 227 228 FORCE ("force", OptionCategories.PROPERTY, () -> { 229 setOptionValue("force", true); 230 }), 231 232 RESOURCE_DIR("resource-dir", 233 OptionCategories.PROPERTY, () -> { 234 String resourceDir = popArg(); 235 setOptionValue("resource-dir", resourceDir); 236 }), 237 238 FILES ("files", "f", OptionCategories.PROPERTY, () -> { 239 context().files = new ArrayList<>(); 240 String files = popArg(); 241 context().files.addAll( 242 Arrays.asList(files.split(File.pathSeparator))); 243 }), 244 245 ARGUMENTS ("arguments", "a", OptionCategories.PROPERTY, () -> { 246 List<String> arguments = getArgumentList(popArg()); 247 setOptionValue("arguments", arguments); 248 }), 249 250 STRIP_NATIVE_COMMANDS ("strip-native-commands", 251 OptionCategories.PROPERTY, () -> { 252 setOptionValue("strip-native-commands", true); 253 }), 254 255 ICON ("icon", OptionCategories.PROPERTY), 256 CATEGORY ("category", OptionCategories.PROPERTY), 257 COPYRIGHT ("copyright", OptionCategories.PROPERTY), 258 259 LICENSE_FILE ("license-file", OptionCategories.PROPERTY), 260 261 VERSION ("app-version", OptionCategories.PROPERTY), 262 263 JVM_ARGS ("jvm-args", OptionCategories.PROPERTY, () -> { 264 List<String> args = getArgumentList(popArg()); 265 args.forEach(a -> setOptionValue("jvm-args", a)); 266 }), 267 268 FILE_ASSOCIATIONS ("file-associations", 269 OptionCategories.PROPERTY, () -> { 270 Map<String, ? super Object> args = new HashMap<>(); 271 272 // load .properties file 273 Map<String, String> initialMap = getPropertiesFromFile(popArg()); 274 275 String ext = initialMap.get(FA_EXTENSIONS); 276 if (ext != null) { 277 args.put(StandardBundlerParam.FA_EXTENSIONS.getID(), ext); 278 } 279 280 String type = initialMap.get(FA_CONTENT_TYPE); 281 if (type != null) { 282 args.put(StandardBundlerParam.FA_CONTENT_TYPE.getID(), type); 283 } 284 285 String desc = initialMap.get(FA_DESCRIPTION); 286 if (desc != null) { 287 args.put(StandardBundlerParam.FA_DESCRIPTION.getID(), desc); 288 } 289 290 String icon = initialMap.get(FA_ICON); 291 if (icon != null) { 292 args.put(StandardBundlerParam.FA_ICON.getID(), icon); 293 } 294 295 ArrayList<Map<String, ? super Object>> associationList = 296 new ArrayList<Map<String, ? super Object>>(); 297 298 associationList.add(args); 299 300 // check that we really add _another_ value to the list 301 setOptionValue("file-associations", associationList); 302 303 }), 304 305 SECONDARY_LAUNCHER ("secondary-launcher", 306 OptionCategories.PROPERTY, () -> { 307 context().secondaryLaunchers.add( 308 new SecondaryLauncherArguments(popArg())); 309 }), 310 311 BUILD_ROOT ("build-root", OptionCategories.PROPERTY, () -> { 312 context().buildRoot = popArg(); 313 context().retainBuildRoot = true; 314 setOptionValue("build-root", context().buildRoot); 315 }), 316 317 INSTALL_DIR ("install-dir", OptionCategories.PROPERTY), 318 319 PREDEFINED_APP_IMAGE ("app-image", OptionCategories.PROPERTY, ()-> { 320 setOptionValue("app-image", popArg()); 321 context().hasAppImage = true; 322 }), 323 324 PREDEFINED_RUNTIME_IMAGE ("runtime-image", OptionCategories.PROPERTY), 325 326 MAIN_JAR ("main-jar", "j", OptionCategories.PROPERTY, () -> { 327 context().mainJarPath = popArg(); 328 context().hasMainJar = true; 329 setOptionValue("main-jar", context().mainJarPath); 330 }), 331 332 MODULE ("module", "m", OptionCategories.MODULAR, () -> { 333 context().hasMainModule = true; 334 setOptionValue("module", popArg()); 335 }), 336 337 ADD_MODULES ("add-modules", OptionCategories.MODULAR), 338 339 MODULE_PATH ("module-path", "p", OptionCategories.MODULAR), 340 341 LIMIT_MODULES ("limit-modules", OptionCategories.MODULAR), 342 343 MAC_SIGN ("mac-sign", "s", OptionCategories.PLATFORM_MAC, () -> { 344 setOptionValue("mac-sign", true); 345 }), 346 347 MAC_BUNDLE_NAME ("mac-bundle-name", OptionCategories.PLATFORM_MAC), 348 349 MAC_BUNDLE_IDENTIFIER("mac-bundle-identifier", 350 OptionCategories.PLATFORM_MAC), 351 352 MAC_APP_STORE_CATEGORY ("mac-app-store-category", 353 OptionCategories.PLATFORM_MAC), 354 355 MAC_BUNDLE_SIGNING_PREFIX ("mac-bundle-signing-prefix", 356 OptionCategories.PLATFORM_MAC), 357 358 MAC_SIGNING_KEY_NAME ("mac-signing-key-user-name", 359 OptionCategories.PLATFORM_MAC), 360 361 MAC_SIGNING_KEYCHAIN ("mac-signing-keychain", 362 OptionCategories.PLATFORM_MAC), 363 364 MAC_APP_STORE_ENTITLEMENTS ("mac-app-store-entitlements", 365 OptionCategories.PLATFORM_MAC), 366 367 WIN_MENU_HINT ("win-menu", OptionCategories.PLATFORM_WIN, () -> { 368 setOptionValue("win-menu", true); 369 }), 370 371 WIN_MENU_GROUP ("win-menu-group", OptionCategories.PLATFORM_WIN), 372 373 WIN_SHORTCUT_HINT ("win-shortcut", 374 OptionCategories.PLATFORM_WIN, () -> { 375 setOptionValue("win-shortcut", true); 376 }), 377 378 WIN_PER_USER_INSTALLATION ("win-per-user-install", 379 OptionCategories.PLATFORM_WIN, () -> { 380 setOptionValue("win-per-user-install", false); 381 }), 382 383 WIN_DIR_CHOOSER ("win-dir-chooser", 384 OptionCategories.PLATFORM_WIN, () -> { 385 setOptionValue("win-dir-chooser", true); 386 }), 387 388 WIN_REGISTRY_NAME ("win-registry-name", OptionCategories.PLATFORM_WIN), 389 390 WIN_UPGRADE_UUID ("win-upgrade-uuid", 391 OptionCategories.PLATFORM_WIN), 392 393 WIN_CONSOLE_HINT ("win-console", OptionCategories.PLATFORM_WIN, () -> { 394 setOptionValue("win-console", true); 395 }), 396 397 LINUX_BUNDLE_NAME ("linux-bundle-name", 398 OptionCategories.PLATFORM_LINUX), 399 400 LINUX_DEB_MAINTAINER ("linux-deb-maintainer", 401 OptionCategories.PLATFORM_LINUX), 402 403 LINUX_RPM_LICENSE_TYPE ("linux-rpm-license-type", 404 OptionCategories.PLATFORM_LINUX), 405 406 LINUX_PACKAGE_DEPENDENCIES ("linux-package-deps", 407 OptionCategories.PLATFORM_LINUX); 408 409 private final String id; 410 private final String shortId; 411 private final OptionCategories category; 412 private final ArgAction action; 413 private static Arguments argContext; 414 415 private CLIOptions(String id, OptionCategories category) { 416 this(id, null, category, null); 417 } 418 419 private CLIOptions(String id, String shortId, 420 OptionCategories category) { 421 this(id, shortId, category, null); 422 } 423 424 private CLIOptions(String id, 425 OptionCategories category, ArgAction action) { 426 this(id, null, category, action); 427 } 428 429 private CLIOptions(String id, String shortId, 430 OptionCategories category, ArgAction action) { 431 this.id = id; 432 this.shortId = shortId; 433 this.action = action; 434 this.category = category; 435 } 436 437 static void setContext(Arguments context) { 438 argContext = context; 439 } 440 441 private static Arguments context() { 442 if (argContext != null) { 443 return argContext; 444 } else { 445 throw new RuntimeException("Argument context is not set."); 446 } 447 } 448 449 public String getId() { 450 return this.id; 451 } 452 453 String getIdWithPrefix() { 454 String prefix = isMode() ? "create-" : "--"; 455 return prefix + this.id; 456 } 457 458 String getShortIdWithPrefix() { 459 return this.shortId == null ? null : "-" + this.shortId; 460 } 461 462 void execute() { 463 if (action != null) { 464 action.execute(); 465 } else { 466 defaultAction(); 467 } 468 } 469 470 boolean isMode() { 471 return category == OptionCategories.MODE; 472 } 473 474 OptionCategories getCategory() { 475 return category; 476 } 477 478 private void defaultAction() { 479 context().deployParams.addBundleArgument(id, popArg()); 480 } 481 482 private static void setOptionValue(String option, Object value) { 483 context().deployParams.addBundleArgument(option, value); 484 } 485 486 private static String popArg() { 487 nextArg(); 488 return (context().pos >= context().argList.size()) ? 489 "" : context().argList.get(context().pos); 490 } 491 492 private static String getArg() { 493 return (context().pos >= context().argList.size()) ? 494 "" : context().argList.get(context().pos); 495 } 496 497 private static void nextArg() { 498 context().pos++; 499 } 500 501 private static void prevArg() { 502 context().pos--; 503 } 504 505 private static boolean hasNextArg() { 506 return context().pos < context().argList.size(); 507 } 508 } 509 510 enum OptionCategories { 511 MODE, 512 MODULAR, 513 PROPERTY, 514 PLATFORM_MAC, 515 PLATFORM_WIN, 516 PLATFORM_LINUX; 517 } 518 519 private void initArgumentList(String[] args) { 520 argList = new ArrayList<String>(args.length); 521 for (String arg : args) { 522 if (arg.startsWith("@")) { 523 if (arg.length() > 1) { 524 String filename = arg.substring(1); 525 argList.addAll(extractArgList(filename)); 526 } else { 527 Log.error("invalid option ["+arg+"]"); 528 } 529 } else { 530 argList.add(arg); 531 } 532 } 533 Log.debug ("\nJPackage argument list: \n" + argList + "\n"); 534 pos = 0; 535 536 deployParams = new DeployParams(); 537 bundleType = BundlerType.NONE; 538 539 allOptions = new ArrayList<>(); 540 541 secondaryLaunchers = new ArrayList<>(); 542 } 543 544 private List<String> extractArgList(String filename) { 545 List<String> args = new ArrayList<String>(); 546 try { 547 File f = new File(filename); 548 if (f.exists()) { 549 List<String> lines = Files.readAllLines(f.toPath()); 550 for (String line : lines) { 551 String [] qsplit; 552 String quote = "\""; 553 if (line.contains("\"")) { 554 qsplit = line.split("\""); 555 } else { 556 qsplit = line.split("\'"); 557 quote = "\'"; 558 } 559 for (int i=0; i<qsplit.length; i++) { 560 // every other qsplit of line is a quoted string 561 if ((i & 1) == 0) { 562 // non-quoted string - split by whitespace 563 String [] newargs = qsplit[i].split("\\s"); 564 for (String newarg : newargs) { 565 args.add(newarg); 566 } 567 } else { 568 // quoted string - don't split by whitespace 569 args.add(qsplit[i]); 570 } 571 } 572 } 573 } else { 574 Log.error("Can not find argument file: " + f); 575 } 576 } catch (IOException ioe) { 577 Log.verbose(ioe.getMessage()); 578 Log.verbose(ioe); 579 } 580 return args; 581 } 582 583 584 public boolean processArguments() throws Exception { 585 try { 586 587 // init context of arguments 588 CLIOptions.setContext(this); 589 590 // parse cmd line 591 String arg; 592 CLIOptions option; 593 for (; CLIOptions.hasNextArg(); CLIOptions.nextArg()) { 594 arg = CLIOptions.getArg(); 595 if ((option = toCLIOption(arg)) != null) { 596 // found a CLI option 597 allOptions.add(option); 598 option.execute(); 599 } else { 600 Log.error("invalid option ["+arg+"]"); 601 } 602 } 603 604 if (allOptions.isEmpty() || !allOptions.get(0).isMode()) { 605 // first argument should always be a mode. 606 Log.error("ERROR: Mode is not specified"); 607 return false; 608 } 609 610 if (!hasAppImage && !hasMainJar && !hasMainModule && 611 !hasMainClass && !jreInstaller) { 612 Log.error("ERROR: Main jar, main class, main module, " 613 + "or app-image must be specified."); 614 } else if (!hasMainModule && !hasMainClass) { 615 // try to get main-class from manifest 616 String mainClass = getMainClassFromManifest(); 617 if (mainClass != null) { 618 CLIOptions.setOptionValue( 619 CLIOptions.APPCLASS.getId(), mainClass); 620 } 621 } 622 623 // display warning for arguments that are not supported 624 // for current configuration. 625 626 validateArguments(); 627 628 addResources(deployParams, input, files); 629 630 deployParams.setBundleType(bundleType); 631 632 List<Map<String, ? super Object>> launchersAsMap = 633 new ArrayList<>(); 634 635 for (SecondaryLauncherArguments sl : secondaryLaunchers) { 636 launchersAsMap.add(sl.getLauncherMap()); 637 } 638 639 deployParams.addBundleArgument( 640 StandardBundlerParam.SECONDARY_LAUNCHERS.getID(), 641 launchersAsMap); 642 643 // at this point deployParams should be already configured 644 645 deployParams.validate(); 646 647 BundleParams bp = deployParams.getBundleParams(); 648 649 // validate name(s) 650 ArrayList<String> usedNames = new ArrayList<String>(); 651 usedNames.add(bp.getName()); // add main app name 652 653 for (SecondaryLauncherArguments sl : secondaryLaunchers) { 654 Map<String, ? super Object> slMap = sl.getLauncherMap(); 655 String slName = 656 (String) slMap.get(Arguments.CLIOptions.NAME.getId()); 657 if (slName == null) { 658 throw new PackagerException("ERR_NoSecondaryLauncherName"); 659 } 660 // same rules apply to secondary launcher names as app name 661 DeployParams.validateName(slName, false); 662 for (String usedName : usedNames) { 663 if (slName.equals(usedName)) { 664 throw new PackagerException("ERR_NoUniqueName"); 665 } 666 } 667 usedNames.add(slName); 668 } 669 if (jreInstaller && bp.getName() == null) { 670 throw new PackagerException("ERR_NoJreInstallerName"); 671 } 672 673 return generateBundle(bp.getBundleParamsAsMap()); 674 } catch (Exception e) { 675 if (Log.isVerbose()) { 676 throw e; 677 } else { 678 Log.error(e.getMessage()); 679 if (e.getCause() != null && e.getCause() != e) { 680 Log.error(e.getCause().getMessage()); 681 } 682 return false; 683 } 684 } 685 } 686 687 private void validateArguments() { 688 CLIOptions mode = allOptions.get(0); 689 for (CLIOptions option : allOptions) { 690 if(!ValidOptions.checkIfSupported(mode, option)) { 691 System.out.println("WARNING: argument [" 692 + option.getId() + "] is not " 693 + "supported for current configuration."); 694 } 695 } 696 } 697 698 private List<jdk.jpackage.internal.Bundler> getPlatformBundlers() { 699 700 if (platformBundlers != null) { 701 return platformBundlers; 702 } 703 704 platformBundlers = new ArrayList<>(); 705 for (jdk.jpackage.internal.Bundler bundler : 706 Bundlers.createBundlersInstance().getBundlers( 707 bundleType.toString())) { 708 if (hasTargetFormat && deployParams.getTargetFormat() != null && 709 !deployParams.getTargetFormat().equalsIgnoreCase( 710 bundler.getID())) { 711 continue; 712 } 713 if (bundler.supported()) { 714 platformBundlers.add(bundler); 715 } 716 } 717 718 return platformBundlers; 719 } 720 721 private boolean generateBundle(Map<String,? super Object> params) 722 throws PackagerException { 723 724 boolean bundleCreated = false; 725 726 // the build-root needs to be fetched from the params early, 727 // to prevent each copy of the params (such as may be used for 728 // secondary launchers) from generating a separate build-root when 729 // the default is used (the default is a new temp directory) 730 // The bundler.cleanup() below would not otherwise be able to 731 // clean these extra (and unneeded) temp directories. 732 StandardBundlerParam.BUILD_ROOT.fetchFrom(params); 733 734 for (jdk.jpackage.internal.Bundler bundler : getPlatformBundlers()) { 735 Map<String, ? super Object> localParams = new HashMap<>(params); 736 try { 737 if (bundler.validate(localParams)) { 738 File result = 739 bundler.execute(localParams, deployParams.outdir); 740 if (!retainBuildRoot) { 741 bundler.cleanup(localParams); 742 } 743 if (result == null) { 744 throw new PackagerException("MSG_BundlerFailed", 745 bundler.getID(), bundler.getName()); 746 } 747 bundleCreated = true; // at least one bundle was created 748 } 749 } catch (UnsupportedPlatformException e) { 750 Log.debug(MessageFormat.format( 751 I18N.getString("MSG_BundlerPlatformException"), 752 bundler.getName())); 753 } catch (ConfigException e) { 754 Log.debug(e); 755 if (e.getAdvice() != null) { 756 Log.error(MessageFormat.format( 757 I18N.getString("MSG_BundlerConfigException"), 758 bundler.getName(), e.getMessage(), e.getAdvice())); 759 } else { 760 Log.error(MessageFormat.format(I18N.getString( 761 "MSG_BundlerConfigExceptionNoAdvice"), 762 bundler.getName(), e.getMessage())); 763 } 764 } catch (RuntimeException re) { 765 Log.error(MessageFormat.format( 766 I18N.getString("MSG_BundlerRuntimeException"), 767 bundler.getName(), re.toString())); 768 Log.debug(re); 769 } finally { 770 if (retainBuildRoot) { 771 Log.verbose(MessageFormat.format( 772 I18N.getString("message.debug-working-directory"), 773 (new File(buildRoot)).getAbsolutePath())); 774 } 775 } 776 } 777 778 return bundleCreated; 779 } 780 781 private void addResources(DeployParams deployParams, 782 String inputdir, List<String> inputfiles) { 783 784 if (inputdir == null || inputdir.isEmpty()) { 785 return; 786 } 787 788 File baseDir = new File(inputdir); 789 790 if (!baseDir.isDirectory()) { 791 Log.error( 792 "Unable to add resources: \"--input\" is not a directory."); 793 return; 794 } 795 796 List<String> fileNames; 797 if (inputfiles != null) { 798 fileNames = inputfiles; 799 } else { 800 // "-files" is omitted, all files in input cdir (which 801 // is a mandatory argument in this case) will be packaged. 802 fileNames = new ArrayList<>(); 803 try (Stream<Path> files = Files.list(baseDir.toPath())) { 804 files.forEach(file -> fileNames.add( 805 file.getFileName().toString())); 806 } catch (IOException e) { 807 Log.error("Unable to add resources: " + e.getMessage()); 808 } 809 } 810 fileNames.forEach(file -> deployParams.addResource(baseDir, file)); 811 812 deployParams.setClasspath(); 813 } 814 815 static boolean isCLIOption(String arg) { 816 return toCLIOption(arg) != null; 817 } 818 819 static CLIOptions toCLIOption(String arg) { 820 CLIOptions option; 821 if ((option = argIds.get(arg)) == null) { 822 option = argShortIds.get(arg); 823 } 824 return option; 825 } 826 827 static Map<String, String> getArgumentMap(String inputString) { 828 Map<String, String> map = new HashMap<>(); 829 List<String> list = getArgumentList(inputString); 830 for (String pair : list) { 831 int equals = pair.indexOf("="); 832 if (equals != -1) { 833 String key = pair.substring(0, equals); 834 String value = pair.substring(equals+1, pair.length()); 835 map.put(key, value); 836 } 837 } 838 return map; 839 } 840 841 static Map<String, String> getPropertiesFromFile(String filename) { 842 Map<String, String> map = new HashMap<>(); 843 // load properties file 844 File file = new File(filename); 845 Properties properties = new Properties(); 846 try (FileInputStream in = new FileInputStream(file)) { 847 properties.load(in); 848 } catch (IOException e) { 849 Log.error("Exception: " + e.getMessage()); 850 } 851 852 for (final String name: properties.stringPropertyNames()) { 853 map.put(name, properties.getProperty(name)); 854 } 855 856 return map; 857 } 858 859 static List<String> getArgumentList(String inputString) { 860 List<String> list = new ArrayList<>(); 861 if (inputString == null || inputString.isEmpty()) { 862 return list; 863 } 864 865 // The "pattern" regexp attempts to abide to the rule that 866 // strings are delimited by whitespace unless surrounded by 867 // quotes, then it is anything (including spaces) in the quotes. 868 Matcher m = pattern.matcher(inputString); 869 while (m.find()) { 870 String s = inputString.substring(m.start(), m.end()).trim(); 871 // Ensure we do not have an empty string. trim() will take care of 872 // whitespace only strings. The regex preserves quotes and escaped 873 // chars so we need to clean them before adding to the List 874 if (!s.isEmpty()) { 875 list.add(unquoteIfNeeded(s)); 876 } 877 } 878 return list; 879 } 880 881 private static String unquoteIfNeeded(String in) { 882 if (in == null) { 883 return null; 884 } 885 886 if (in.isEmpty()) { 887 return ""; 888 } 889 890 // Use code points to preserve non-ASCII chars 891 StringBuilder sb = new StringBuilder(); 892 int codeLen = in.codePointCount(0, in.length()); 893 int quoteChar = -1; 894 for (int i = 0; i < codeLen; i++) { 895 int code = in.codePointAt(i); 896 if (code == '"' || code == '\'') { 897 // If quote is escaped make sure to copy it 898 if (i > 0 && in.codePointAt(i - 1) == '\\') { 899 sb.deleteCharAt(sb.length() - 1); 900 sb.appendCodePoint(code); 901 continue; 902 } 903 if (quoteChar != -1) { 904 if (code == quoteChar) { 905 // close quote, skip char 906 quoteChar = -1; 907 } else { 908 sb.appendCodePoint(code); 909 } 910 } else { 911 // opening quote, skip char 912 quoteChar = code; 913 } 914 } else { 915 sb.appendCodePoint(code); 916 } 917 } 918 return sb.toString(); 919 } 920 921 private String getMainClassFromManifest() { 922 if (mainJarPath == null || 923 input == null ) { 924 return null; 925 } 926 927 JarFile jf; 928 try { 929 File file = new File(input, mainJarPath); 930 if (!file.exists()) { 931 return null; 932 } 933 jf = new JarFile(file); 934 Manifest m = jf.getManifest(); 935 Attributes attrs = (m != null) ? m.getMainAttributes() : null; 936 if (attrs != null) { 937 return attrs.getValue(Attributes.Name.MAIN_CLASS); 938 } 939 } catch (IOException ignore) {} 940 return null; 941 } 942 943 static boolean isJreInstaller() { 944 return jreInstaller; 945 } 946 }