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 26 package jdk.jpackage.internal; 27 28 import java.io.BufferedWriter; 29 import java.io.File; 30 import java.io.FileInputStream; 31 import java.io.FileOutputStream; 32 import java.io.FileWriter; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.OutputStream; 36 import java.io.OutputStreamWriter; 37 import java.io.UncheckedIOException; 38 import java.io.Writer; 39 import java.math.BigInteger; 40 import java.nio.file.Files; 41 import java.nio.file.Path; 42 import java.nio.file.StandardCopyOption; 43 import java.nio.file.attribute.PosixFilePermission; 44 import java.text.MessageFormat; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.EnumSet; 48 import java.util.HashMap; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.Objects; 52 import java.util.Optional; 53 import java.util.ResourceBundle; 54 import java.util.Set; 55 import java.util.concurrent.atomic.AtomicReference; 56 import java.util.function.Consumer; 57 58 import static jdk.jpackage.internal.StandardBundlerParam.*; 59 import static jdk.jpackage.internal.MacBaseInstallerBundler.*; 60 import static jdk.jpackage.internal.MacAppBundler.*; 61 62 public class MacAppImageBuilder extends AbstractAppImageBuilder { 63 64 private static final ResourceBundle I18N = ResourceBundle.getBundle( 65 "jdk.jpackage.internal.resources.MacResources"); 66 67 private static final String LIBRARY_NAME = "libapplauncher.dylib"; 68 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns"; 69 private static final String OS_TYPE_CODE = "APPL"; 70 private static final String TEMPLATE_INFO_PLIST_LITE = 71 "Info-lite.plist.template"; 72 private static final String TEMPLATE_RUNTIME_INFO_PLIST = 73 "Runtime-Info.plist.template"; 74 75 private final Path root; 76 private final Path contentsDir; 77 private final Path javaDir; 78 private final Path javaModsDir; 79 private final Path resourcesDir; 80 private final Path macOSDir; 81 private final Path runtimeDir; 82 private final Path runtimeRoot; 83 private final Path mdir; 84 85 private final Map<String, ? super Object> params; 86 87 private static List<String> keyChains; 88 89 public static final BundlerParamInfo<Boolean> 90 MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>( 91 "mac.configure-launcher-in-plist", 92 Boolean.class, 93 params -> Boolean.FALSE, 94 (s, p) -> Boolean.valueOf(s)); 95 96 public static final EnumeratedBundlerParam<String> MAC_CATEGORY = 97 new EnumeratedBundlerParam<>( 98 Arguments.CLIOptions.MAC_APP_STORE_CATEGORY.getId(), 99 String.class, 100 params -> "Unknown", 101 (s, p) -> s, 102 MacAppBundler.getMacCategories(), 103 false //strict - for MacStoreBundler this should be strict 104 ); 105 106 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 107 new StandardBundlerParam<>( 108 "mac.CFBundleName", 109 String.class, 110 params -> null, 111 (s, p) -> s); 112 113 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 114 new StandardBundlerParam<>( 115 Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(), 116 String.class, 117 IDENTIFIER::fetchFrom, 118 (s, p) -> s); 119 120 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 121 new StandardBundlerParam<>( 122 "mac.CFBundleVersion", 123 String.class, 124 p -> { 125 String s = VERSION.fetchFrom(p); 126 if (validCFBundleVersion(s)) { 127 return s; 128 } else { 129 return "100"; 130 } 131 }, 132 (s, p) -> s); 133 134 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = 135 new StandardBundlerParam<>( 136 ".mac.default.icns", 137 String.class, 138 params -> TEMPLATE_BUNDLE_ICON, 139 (s, p) -> s); 140 141 public static final BundlerParamInfo<File> ICON_ICNS = 142 new StandardBundlerParam<>( 143 "icon.icns", 144 File.class, 145 params -> { 146 File f = ICON.fetchFrom(params); 147 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 148 Log.error(MessageFormat.format( 149 I18N.getString("message.icon-not-icns"), f)); 150 return null; 151 } 152 return f; 153 }, 154 (s, p) -> new File(s)); 155 156 public static final StandardBundlerParam<Boolean> SIGN_BUNDLE = 157 new StandardBundlerParam<>( 158 Arguments.CLIOptions.MAC_SIGN.getId(), 159 Boolean.class, 160 params -> false, 161 // valueOf(null) is false, we actually do want null in some cases 162 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? 163 null : Boolean.valueOf(s) 164 ); 165 166 public MacAppImageBuilder(Map<String, Object> config, Path imageOutDir) 167 throws IOException { 168 super(config, imageOutDir.resolve(APP_NAME.fetchFrom(config) 169 + ".app/Contents/runtime/Contents/Home")); 170 171 Objects.requireNonNull(imageOutDir); 172 173 this.params = config; 174 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app"); 175 this.contentsDir = root.resolve("Contents"); 176 this.javaDir = contentsDir.resolve("Java"); 177 this.javaModsDir = javaDir.resolve("mods"); 178 this.resourcesDir = contentsDir.resolve("Resources"); 179 this.macOSDir = contentsDir.resolve("MacOS"); 180 this.runtimeDir = contentsDir.resolve("runtime"); 181 this.runtimeRoot = runtimeDir.resolve("Contents/Home"); 182 this.mdir = runtimeRoot.resolve("lib"); 183 Files.createDirectories(javaDir); 184 Files.createDirectories(resourcesDir); 185 Files.createDirectories(macOSDir); 186 Files.createDirectories(runtimeDir); 187 } 188 189 public MacAppImageBuilder(Map<String, Object> config, String jreName, 190 Path imageOutDir) throws IOException { 191 super(null, imageOutDir.resolve(jreName + "/Contents/Home")); 192 193 Objects.requireNonNull(imageOutDir); 194 195 this.params = config; 196 this.root = imageOutDir.resolve(jreName ); 197 this.contentsDir = root.resolve("Contents"); 198 this.javaDir = null; 199 this.javaModsDir = null; 200 this.resourcesDir = null; 201 this.macOSDir = null; 202 this.runtimeDir = this.root; 203 this.runtimeRoot = runtimeDir.resolve("Contents/Home"); 204 this.mdir = runtimeRoot.resolve("lib"); 205 206 Files.createDirectories(runtimeDir); 207 } 208 209 private void writeEntry(InputStream in, Path dstFile) throws IOException { 210 Files.createDirectories(dstFile.getParent()); 211 Files.copy(in, dstFile); 212 } 213 214 // chmod ugo+x file 215 private void setExecutable(Path file) { 216 try { 217 Set<PosixFilePermission> perms = 218 Files.getPosixFilePermissions(file); 219 perms.add(PosixFilePermission.OWNER_EXECUTE); 220 perms.add(PosixFilePermission.GROUP_EXECUTE); 221 perms.add(PosixFilePermission.OTHERS_EXECUTE); 222 Files.setPosixFilePermissions(file, perms); 223 } catch (IOException ioe) { 224 throw new UncheckedIOException(ioe); 225 } 226 } 227 228 private static void createUtf8File(File file, String content) 229 throws IOException { 230 try (OutputStream fout = new FileOutputStream(file); 231 Writer output = new OutputStreamWriter(fout, "UTF-8")) { 232 output.write(content); 233 } 234 } 235 236 public static boolean validCFBundleVersion(String v) { 237 // CFBundleVersion (String - iOS, OS X) specifies the build version 238 // number of the bundle, which identifies an iteration (released or 239 // unreleased) of the bundle. The build version number should be a 240 // string comprised of three non-negative, period-separated integers 241 // with the first integer being greater than zero. The string should 242 // only contain numeric (0-9) and period (.) characters. Leading zeros 243 // are truncated from each integer and will be ignored (that is, 244 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 245 246 if (v == null) { 247 return false; 248 } 249 250 String p[] = v.split("\\."); 251 if (p.length > 3 || p.length < 1) { 252 Log.verbose(I18N.getString( 253 "message.version-string-too-many-components")); 254 return false; 255 } 271 } 272 if (p.length > 2) { 273 n = new BigInteger(p[2]); 274 if (BigInteger.ZERO.compareTo(n) > 0) { 275 Log.verbose(I18N.getString( 276 "message.version-string-no-negative-numbers")); 277 return false; 278 } 279 } 280 } catch (NumberFormatException ne) { 281 Log.verbose(I18N.getString("message.version-string-numbers-only")); 282 Log.verbose(ne); 283 return false; 284 } 285 286 return true; 287 } 288 289 @Override 290 public Path getAppDir() { 291 return javaDir; 292 } 293 294 @Override 295 public Path getAppModsDir() { 296 return javaModsDir; 297 } 298 299 @Override 300 public void prepareApplicationFiles() throws IOException { 301 Map<String, ? super Object> originalParams = new HashMap<>(params); 302 // Generate PkgInfo 303 File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo"); 304 pkgInfoFile.createNewFile(); 305 writePkgInfo(pkgInfoFile); 306 307 Path executable = macOSDir.resolve(getLauncherName(params)); 308 309 // create the main app launcher 310 try (InputStream is_launcher = 311 getResourceAsStream("jpackageapplauncher"); 312 InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) { 313 // Copy executable and library to MacOS folder 314 writeEntry(is_launcher, executable); 315 writeEntry(is_lib, macOSDir.resolve(LIBRARY_NAME)); 316 } 317 executable.toFile().setExecutable(true, false); 318 // generate main app launcher config file 319 File cfg = new File(root.toFile(), getLauncherCfgName(params)); 320 writeCfgFile(params, cfg, "$APPDIR/runtime"); 321 322 // create additional app launcher(s) and config file(s) 323 List<Map<String, ? super Object>> entryPoints = 324 StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params); 325 for (Map<String, ? super Object> entryPoint : entryPoints) { 326 Map<String, ? super Object> tmp = 327 AddLauncherArguments.merge(originalParams, entryPoint); 328 329 // add executable for add launcher 330 Path addExecutable = macOSDir.resolve(getLauncherName(tmp)); 331 try (InputStream is = getResourceAsStream("jpackageapplauncher");) { 332 writeEntry(is, addExecutable); 333 } 334 addExecutable.toFile().setExecutable(true, false); 335 336 // add config file for add launcher 337 cfg = new File(root.toFile(), getLauncherCfgName(tmp)); 338 writeCfgFile(tmp, cfg, "$APPDIR/runtime"); 339 } 340 341 // Copy class path entries to Java folder 342 copyClassPathEntries(javaDir); 343 344 /*********** Take care of "config" files *******/ 345 File icon = ICON_ICNS.fetchFrom(params); 346 347 InputStream in = locateResource( 348 APP_NAME.fetchFrom(params) + ".icns", 349 "icon", 350 DEFAULT_ICNS_ICON.fetchFrom(params), 351 icon, 352 VERBOSE.fetchFrom(params), 353 RESOURCE_DIR.fetchFrom(params)); 354 Files.copy(in, 355 resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns"), 356 StandardCopyOption.REPLACE_EXISTING); 357 358 // copy file association icons 359 for (Map<String, ? 360 super Object> fa : FILE_ASSOCIATIONS.fetchFrom(params)) { 361 File f = FA_ICON.fetchFrom(fa); 362 if (f != null && f.exists()) { 363 try (InputStream in2 = new FileInputStream(f)) { 364 Files.copy(in2, resourcesDir.resolve(f.getName())); 365 } 366 367 } 368 } 369 370 copyRuntimeFiles(); 371 sign(); 372 } 373 374 @Override 375 public void prepareJreFiles() throws IOException { 376 copyRuntimeFiles(); 377 sign(); 378 } 379 380 private void copyRuntimeFiles() throws IOException { 381 // Generate Info.plist 382 writeInfoPlist(contentsDir.resolve("Info.plist").toFile()); 383 384 // generate java runtime info.plist 385 writeRuntimeInfoPlist( 386 runtimeDir.resolve("Contents/Info.plist").toFile()); 387 388 // copy library 389 Path runtimeMacOSDir = Files.createDirectories( 390 runtimeDir.resolve("Contents/MacOS")); 391 392 // JDK 9, 10, and 11 have extra '/jli/' subdir 393 Path jli = runtimeRoot.resolve("lib/libjli.dylib"); 394 if (!Files.exists(jli)) { 395 jli = runtimeRoot.resolve("lib/jli/libjli.dylib"); 396 } 397 398 Files.copy(jli, runtimeMacOSDir.resolve("libjli.dylib")); 399 } 400 401 private void sign() throws IOException { 402 if (Optional.ofNullable( 403 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { 404 try { 405 addNewKeychain(params); 406 } catch (InterruptedException e) { 407 Log.error(e.getMessage()); 408 } 409 String signingIdentity = 410 DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params); 411 if (signingIdentity != null) { 412 signAppBundle(params, root, signingIdentity, 413 BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null); 414 } 415 restoreKeychainList(params); 416 } 417 } 418 419 private String getLauncherName(Map<String, ? super Object> params) { 420 if (APP_NAME.fetchFrom(params) != null) { 421 return APP_NAME.fetchFrom(params); 422 } else { 423 return MAIN_CLASS.fetchFrom(params); 424 } 425 } 426 427 public static String getLauncherCfgName(Map<String, ? super Object> p) { 428 return "Contents/Java/" + APP_NAME.fetchFrom(p) + ".cfg"; 429 } 430 431 private void copyClassPathEntries(Path javaDirectory) throws IOException { 432 List<RelativeFileSet> resourcesList = 433 APP_RESOURCES_LIST.fetchFrom(params); 434 if (resourcesList == null) { 435 throw new RuntimeException( 436 I18N.getString("message.null-classpath")); 437 } 438 439 for (RelativeFileSet classPath : resourcesList) { 440 File srcdir = classPath.getBaseDirectory(); 441 for (String fname : classPath.getIncludedFiles()) { 442 copyEntry(javaDirectory, srcdir, fname); 443 } 444 } 445 } 446 447 private String getBundleName(Map<String, ? super Object> params) { 448 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { 449 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); 450 if (bn.length() > 16) { 451 Log.error(MessageFormat.format(I18N.getString( 452 "message.bundle-name-too-long-warning"), 453 MAC_CF_BUNDLE_NAME.getID(), bn)); 454 } 455 return MAC_CF_BUNDLE_NAME.fetchFrom(params); 456 } else if (APP_NAME.fetchFrom(params) != null) { 457 return APP_NAME.fetchFrom(params); 458 } else { 459 String nm = MAIN_CLASS.fetchFrom(params); 460 if (nm.length() > 16) { 461 nm = nm.substring(0, 16); 462 } 463 return nm; 464 } 465 } 466 467 private void writeRuntimeInfoPlist(File file) throws IOException { 468 Map<String, String> data = new HashMap<>(); 469 String identifier = StandardBundlerParam.isRuntimeInstaller(params) ? 470 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) : 471 "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params); 472 data.put("CF_BUNDLE_IDENTIFIER", identifier); 473 String name = StandardBundlerParam.isRuntimeInstaller(params) ? 474 getBundleName(params): "Java Runtime Image"; 475 data.put("CF_BUNDLE_NAME", name); 476 data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params)); 477 data.put("CF_BUNDLE_SHORT_VERSION_STRING", VERSION.fetchFrom(params)); 478 479 Writer w = new BufferedWriter(new FileWriter(file)); 480 w.write(preprocessTextResource("Runtime-Info.plist", 481 I18N.getString("resource.runtime-info-plist"), 482 TEMPLATE_RUNTIME_INFO_PLIST, 483 data, 484 VERBOSE.fetchFrom(params), 485 RESOURCE_DIR.fetchFrom(params))); 486 w.close(); 487 } 488 489 private void writeInfoPlist(File file) throws IOException { 490 Log.verbose(MessageFormat.format(I18N.getString( 491 "message.preparing-info-plist"), file.getAbsolutePath())); 492 493 //prepare config for exe 494 //Note: do not need CFBundleDisplayName if we don't support localization 495 Map<String, String> data = new HashMap<>(); 496 data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns"); 497 data.put("DEPLOY_BUNDLE_IDENTIFIER", 498 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 499 data.put("DEPLOY_BUNDLE_NAME", 500 getBundleName(params)); 501 data.put("DEPLOY_BUNDLE_COPYRIGHT", 502 COPYRIGHT.fetchFrom(params) != null ? 503 COPYRIGHT.fetchFrom(params) : "Unknown"); 504 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); 505 data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/runtime"); 506 data.put("DEPLOY_BUNDLE_SHORT_VERSION", 507 VERSION.fetchFrom(params) != null ? 508 VERSION.fetchFrom(params) : "1.0.0"); 509 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", 510 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? 511 MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); 512 data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params)); 513 514 boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null; 515 boolean hasMainModule = 516 StandardBundlerParam.MODULE.fetchFrom(params) != null; 517 518 if (hasMainJar) { 519 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params). 520 getIncludedFiles().iterator().next()); 521 } 522 else if (hasMainModule) { 523 data.put("DEPLOY_MODULE_NAME", 524 StandardBundlerParam.MODULE.fetchFrom(params)); 525 } 526 527 StringBuilder sb = new StringBuilder(); 528 List<String> jvmOptions = JAVA_OPTIONS.fetchFrom(params); 529 530 String newline = ""; //So we don't add extra line after last append 531 for (String o : jvmOptions) { 532 sb.append(newline).append( 535 } 536 537 data.put("DEPLOY_JAVA_OPTIONS", sb.toString()); 538 539 sb = new StringBuilder(); 540 List<String> args = ARGUMENTS.fetchFrom(params); 541 newline = ""; 542 // So we don't add unneccessary extra line after last append 543 544 for (String o : args) { 545 sb.append(newline).append(" <string>").append(o).append( 546 "</string>"); 547 newline = "\n"; 548 } 549 data.put("DEPLOY_ARGUMENTS", sb.toString()); 550 551 newline = ""; 552 553 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); 554 555 StringBuilder macroedPath = new StringBuilder(); 556 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { 557 macroedPath.append(s); 558 macroedPath.append(":"); 559 } 560 macroedPath.deleteCharAt(macroedPath.length() - 1); 561 562 data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString()); 563 564 StringBuilder bundleDocumentTypes = new StringBuilder(); 565 StringBuilder exportedTypes = new StringBuilder(); 566 for (Map<String, ? super Object> 567 fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { 568 569 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 570 571 if (extensions == null) { 572 Log.verbose(I18N.getString( 573 "message.creating-association-with-null-extension")); 574 } 575 576 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); 577 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) 578 + "." + ((extensions == null || extensions.isEmpty()) 579 ? "mime" : extensions.get(0)); 580 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 581 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS 582 583 bundleDocumentTypes.append(" <dict>\n") 584 .append(" <key>LSItemContentTypes</key>\n") 585 .append(" <array>\n") 586 .append(" <string>") 587 .append(itemContentType) 588 .append("</string>\n") 589 .append(" </array>\n") 590 .append("\n") 591 .append(" <key>CFBundleTypeName</key>\n") 592 .append(" <string>") 593 .append(description) 594 .append("</string>\n") 595 .append("\n") 596 .append(" <key>LSHandlerRank</key>\n") 597 .append(" <string>Owner</string>\n") 598 // TODO make a bundler arg 599 .append("\n") 600 .append(" <key>CFBundleTypeRole</key>\n") 601 .append(" <string>Editor</string>\n") 672 } 673 exportedTypes.append(" </array>\n"); 674 } 675 exportedTypes.append(" </dict>\n") 676 .append(" </dict>\n"); 677 } 678 String associationData; 679 if (bundleDocumentTypes.length() > 0) { 680 associationData = 681 "\n <key>CFBundleDocumentTypes</key>\n <array>\n" 682 + bundleDocumentTypes.toString() 683 + " </array>\n\n" 684 + " <key>UTExportedTypeDeclarations</key>\n <array>\n" 685 + exportedTypes.toString() 686 + " </array>\n"; 687 } else { 688 associationData = ""; 689 } 690 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); 691 692 693 Writer w = new BufferedWriter(new FileWriter(file)); 694 w.write(preprocessTextResource( 695 // getConfig_InfoPlist(params).getName(), 696 "Info.plist", 697 I18N.getString("resource.app-info-plist"), 698 TEMPLATE_INFO_PLIST_LITE, 699 data, VERBOSE.fetchFrom(params), 700 RESOURCE_DIR.fetchFrom(params))); 701 w.close(); 702 } 703 704 private void writePkgInfo(File file) throws IOException { 705 //hardcoded as it does not seem we need to change it ever 706 String signature = "????"; 707 708 try (Writer out = new BufferedWriter(new FileWriter(file))) { 709 out.write(OS_TYPE_CODE + signature); 710 out.flush(); 711 } 712 } 713 714 public static void addNewKeychain(Map<String, ? super Object> params) 715 throws IOException, InterruptedException { 716 if (Platform.getMajorVersion() < 10 || 717 (Platform.getMajorVersion() == 10 && 718 Platform.getMinorVersion() < 12)) { 719 // we need this for OS X 10.12+ 720 return; 721 } 722 723 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 724 if (keyChain == null || keyChain.isEmpty()) { 725 return; 726 } 727 728 // get current keychain list 744 745 keyChains = new ArrayList<>(); 746 // remove " 747 keychainList.forEach((String s) -> { 748 String path = s.trim(); 749 if (path.startsWith("\"") && path.endsWith("\"")) { 750 path = path.substring(1, path.length()-1); 751 } 752 keyChains.add(path); 753 }); 754 755 List<String> args = new ArrayList<>(); 756 args.add("security"); 757 args.add("list-keychains"); 758 args.add("-s"); 759 760 args.addAll(keyChains); 761 args.add(keyChain); 762 763 ProcessBuilder pb = new ProcessBuilder(args); 764 IOUtils.exec(pb, false); 765 } 766 767 public static void restoreKeychainList(Map<String, ? super Object> params) 768 throws IOException{ 769 if (Platform.getMajorVersion() < 10 || 770 (Platform.getMajorVersion() == 10 && 771 Platform.getMinorVersion() < 12)) { 772 // we need this for OS X 10.12+ 773 return; 774 } 775 776 if (keyChains == null || keyChains.isEmpty()) { 777 return; 778 } 779 780 List<String> args = new ArrayList<>(); 781 args.add("security"); 782 args.add("list-keychains"); 783 args.add("-s"); 784 785 args.addAll(keyChains); 786 787 ProcessBuilder pb = new ProcessBuilder(args); 788 IOUtils.exec(pb, false); 789 } 790 791 public static void signAppBundle( 792 Map<String, ? super Object> params, Path appLocation, 793 String signingIdentity, String identifierPrefix, 794 String entitlementsFile, String inheritedEntitlements) 795 throws IOException { 796 AtomicReference<IOException> toThrow = new AtomicReference<>(); 797 String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); 798 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 799 800 // sign all dylibs and jars 801 Files.walk(appLocation) 802 // fix permissions 803 .peek(path -> { 804 try { 805 Set<PosixFilePermission> pfp = 806 Files.getPosixFilePermissions(path); 807 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { 808 pfp = EnumSet.copyOf(pfp); 809 pfp.add(PosixFilePermission.OWNER_WRITE); 810 Files.setPosixFilePermissions(path, pfp); 811 } 812 } catch (IOException e) { 813 Log.debug(e); 814 } 815 }) 816 .filter(p -> Files.isRegularFile(p) && 817 !(p.toString().contains("/Contents/MacOS/libjli.dylib") 818 || p.toString().contains( 819 "/Contents/MacOS/JavaAppletPlugin") 820 || p.toString().endsWith(appExecutable)) 821 ).forEach(p -> { 822 //noinspection ThrowableResultOfMethodCallIgnored 823 if (toThrow.get() != null) return; 824 825 // If p is a symlink then skip the signing process. 826 if (Files.isSymbolicLink(p)) { 827 if (VERBOSE.fetchFrom(params)) { 828 Log.verbose(MessageFormat.format(I18N.getString( 829 "message.ignoring.symlink"), p.toString())); 830 } 831 } 832 else { 833 List<String> args = new ArrayList<>(); 834 args.addAll(Arrays.asList("codesign", 835 "-s", signingIdentity, // sign with this key 836 "--prefix", identifierPrefix, 837 // use the identifier as a prefix 838 "-vvvv")); 839 if (entitlementsFile != null && 840 (p.toString().endsWith(".jar") 841 || p.toString().endsWith(".dylib"))) { 842 args.add("--entitlements"); 843 args.add(entitlementsFile); // entitlements 844 } else if (inheritedEntitlements != null && 845 Files.isExecutable(p)) { 846 args.add("--entitlements"); 847 args.add(inheritedEntitlements); 848 // inherited entitlements for executable processes 849 } 850 if (keyChain != null && !keyChain.isEmpty()) { 851 args.add("--keychain"); 852 args.add(keyChain); 853 } 854 args.add(p.toString()); 855 856 try { 857 Set<PosixFilePermission> oldPermissions = 858 Files.getPosixFilePermissions(p); 859 File f = p.toFile(); 860 f.setWritable(true, true); 861 862 ProcessBuilder pb = new ProcessBuilder(args); 863 IOUtils.exec(pb, false); 864 865 Files.setPosixFilePermissions(p, oldPermissions); 866 } catch (IOException ioe) { 867 toThrow.set(ioe); 868 } 869 } 870 }); 871 872 IOException ioe = toThrow.get(); 873 if (ioe != null) { 874 throw ioe; 875 } 876 877 // sign all runtime and frameworks 878 Consumer<? super Path> signIdentifiedByPList = path -> { 879 //noinspection ThrowableResultOfMethodCallIgnored 880 if (toThrow.get() != null) return; 881 882 try { 883 List<String> args = new ArrayList<>(); 884 args.addAll(Arrays.asList("codesign", 885 "-s", signingIdentity, // sign with this key 886 "--prefix", identifierPrefix, 887 // use the identifier as a prefix 888 "-vvvv")); 889 if (keyChain != null && !keyChain.isEmpty()) { 890 args.add("--keychain"); 891 args.add(keyChain); 892 } 893 args.add(path.toString()); 894 ProcessBuilder pb = new ProcessBuilder(args); 895 IOUtils.exec(pb, false); 896 897 args = new ArrayList<>(); 898 args.addAll(Arrays.asList("codesign", 899 "-s", signingIdentity, // sign with this key 900 "--prefix", identifierPrefix, 901 // use the identifier as a prefix 902 "-vvvv")); 903 if (keyChain != null && !keyChain.isEmpty()) { 904 args.add("--keychain"); 905 args.add(keyChain); 906 } 907 args.add(path.toString() 908 + "/Contents/_CodeSignature/CodeResources"); 909 pb = new ProcessBuilder(args); 910 IOUtils.exec(pb, false); 911 } catch (IOException e) { 912 toThrow.set(e); 913 } 914 }; 915 916 Path javaPath = appLocation.resolve("Contents/runtime"); 917 if (Files.isDirectory(javaPath)) { 918 Files.list(javaPath) 919 .forEach(signIdentifiedByPList); 920 921 ioe = toThrow.get(); 922 if (ioe != null) { 923 throw ioe; 924 } 925 } 926 Path frameworkPath = appLocation.resolve("Contents/Frameworks"); 927 if (Files.isDirectory(frameworkPath)) { 928 Files.list(frameworkPath) 929 .forEach(signIdentifiedByPList); 930 931 ioe = toThrow.get(); 932 if (ioe != null) { 933 throw ioe; 934 } 935 } 936 937 // sign the app itself 938 List<String> args = new ArrayList<>(); 939 args.addAll(Arrays.asList("codesign", 940 "-s", signingIdentity, // sign with this key 941 "-vvvv")); // super verbose output 942 if (entitlementsFile != null) { 943 args.add("--entitlements"); 944 args.add(entitlementsFile); // entitlements 945 } 946 if (keyChain != null && !keyChain.isEmpty()) { 947 args.add("--keychain"); 948 args.add(keyChain); 949 } 950 args.add(appLocation.toString()); 951 952 ProcessBuilder pb = 953 new ProcessBuilder(args.toArray(new String[args.size()])); 954 IOUtils.exec(pb, false); 955 } 956 957 } | 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 26 package jdk.incubator.jpackage.internal; 27 28 import java.io.File; 29 import java.io.FileInputStream; 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.io.Writer; 33 import java.math.BigInteger; 34 import java.nio.file.Files; 35 import java.nio.file.Path; 36 import java.nio.file.StandardCopyOption; 37 import java.nio.file.attribute.PosixFilePermission; 38 import java.text.MessageFormat; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.EnumSet; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Objects; 46 import java.util.Optional; 47 import java.util.ResourceBundle; 48 import java.util.Set; 49 import java.util.concurrent.atomic.AtomicReference; 50 import java.util.function.Consumer; 51 import java.util.stream.Stream; 52 import javax.xml.parsers.DocumentBuilder; 53 import javax.xml.parsers.DocumentBuilderFactory; 54 import javax.xml.xpath.XPath; 55 import javax.xml.xpath.XPathConstants; 56 import javax.xml.xpath.XPathFactory; 57 58 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; 59 import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.*; 60 import static jdk.incubator.jpackage.internal.MacAppBundler.*; 61 import static jdk.incubator.jpackage.internal.OverridableResource.createResource; 62 63 public class MacAppImageBuilder extends AbstractAppImageBuilder { 64 65 private static final ResourceBundle I18N = ResourceBundle.getBundle( 66 "jdk.incubator.jpackage.internal.resources.MacResources"); 67 68 private static final String LIBRARY_NAME = "libapplauncher.dylib"; 69 private static final String TEMPLATE_BUNDLE_ICON = "java.icns"; 70 private static final String OS_TYPE_CODE = "APPL"; 71 private static final String TEMPLATE_INFO_PLIST_LITE = 72 "Info-lite.plist.template"; 73 private static final String TEMPLATE_RUNTIME_INFO_PLIST = 74 "Runtime-Info.plist.template"; 75 76 private final Path root; 77 private final Path contentsDir; 78 private final Path appDir; 79 private final Path javaModsDir; 80 private final Path resourcesDir; 81 private final Path macOSDir; 82 private final Path runtimeDir; 83 private final Path runtimeRoot; 84 private final Path mdir; 85 86 private static List<String> keyChains; 87 88 public static final BundlerParamInfo<Boolean> 89 MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>( 90 "mac.configure-launcher-in-plist", 91 Boolean.class, 92 params -> Boolean.FALSE, 93 (s, p) -> Boolean.valueOf(s)); 94 95 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 96 new StandardBundlerParam<>( 97 Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(), 98 String.class, 99 params -> null, 100 (s, p) -> s); 101 102 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 103 new StandardBundlerParam<>( 104 Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(), 105 String.class, 106 params -> { 107 // Get identifier from app image if user provided 108 // app image and did not provide the identifier via CLI. 109 String identifier = extractBundleIdentifier(params); 110 if (identifier != null) { 111 return identifier; 112 } 113 114 return IDENTIFIER.fetchFrom(params); 115 }, 116 (s, p) -> s); 117 118 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 119 new StandardBundlerParam<>( 120 "mac.CFBundleVersion", 121 String.class, 122 p -> { 123 String s = VERSION.fetchFrom(p); 124 if (validCFBundleVersion(s)) { 125 return s; 126 } else { 127 return "100"; 128 } 129 }, 130 (s, p) -> s); 131 132 public static final BundlerParamInfo<File> ICON_ICNS = 133 new StandardBundlerParam<>( 134 "icon.icns", 135 File.class, 136 params -> { 137 File f = ICON.fetchFrom(params); 138 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 139 Log.error(MessageFormat.format( 140 I18N.getString("message.icon-not-icns"), f)); 141 return null; 142 } 143 return f; 144 }, 145 (s, p) -> new File(s)); 146 147 public static final StandardBundlerParam<Boolean> SIGN_BUNDLE = 148 new StandardBundlerParam<>( 149 Arguments.CLIOptions.MAC_SIGN.getId(), 150 Boolean.class, 151 params -> false, 152 // valueOf(null) is false, we actually do want null in some cases 153 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? 154 null : Boolean.valueOf(s) 155 ); 156 157 public MacAppImageBuilder(Map<String, Object> params, Path imageOutDir) 158 throws IOException { 159 super(params, imageOutDir.resolve(APP_NAME.fetchFrom(params) 160 + ".app/Contents/runtime/Contents/Home")); 161 162 Objects.requireNonNull(imageOutDir); 163 164 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app"); 165 this.contentsDir = root.resolve("Contents"); 166 this.appDir = contentsDir.resolve("app"); 167 this.javaModsDir = appDir.resolve("mods"); 168 this.resourcesDir = contentsDir.resolve("Resources"); 169 this.macOSDir = contentsDir.resolve("MacOS"); 170 this.runtimeDir = contentsDir.resolve("runtime"); 171 this.runtimeRoot = runtimeDir.resolve("Contents/Home"); 172 this.mdir = runtimeRoot.resolve("lib"); 173 Files.createDirectories(appDir); 174 Files.createDirectories(resourcesDir); 175 Files.createDirectories(macOSDir); 176 Files.createDirectories(runtimeDir); 177 } 178 179 private void writeEntry(InputStream in, Path dstFile) throws IOException { 180 Files.createDirectories(dstFile.getParent()); 181 Files.copy(in, dstFile); 182 } 183 184 public static boolean validCFBundleVersion(String v) { 185 // CFBundleVersion (String - iOS, OS X) specifies the build version 186 // number of the bundle, which identifies an iteration (released or 187 // unreleased) of the bundle. The build version number should be a 188 // string comprised of three non-negative, period-separated integers 189 // with the first integer being greater than zero. The string should 190 // only contain numeric (0-9) and period (.) characters. Leading zeros 191 // are truncated from each integer and will be ignored (that is, 192 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 193 194 if (v == null) { 195 return false; 196 } 197 198 String p[] = v.split("\\."); 199 if (p.length > 3 || p.length < 1) { 200 Log.verbose(I18N.getString( 201 "message.version-string-too-many-components")); 202 return false; 203 } 219 } 220 if (p.length > 2) { 221 n = new BigInteger(p[2]); 222 if (BigInteger.ZERO.compareTo(n) > 0) { 223 Log.verbose(I18N.getString( 224 "message.version-string-no-negative-numbers")); 225 return false; 226 } 227 } 228 } catch (NumberFormatException ne) { 229 Log.verbose(I18N.getString("message.version-string-numbers-only")); 230 Log.verbose(ne); 231 return false; 232 } 233 234 return true; 235 } 236 237 @Override 238 public Path getAppDir() { 239 return appDir; 240 } 241 242 @Override 243 public Path getAppModsDir() { 244 return javaModsDir; 245 } 246 247 @Override 248 public void prepareApplicationFiles(Map<String, ? super Object> params) 249 throws IOException { 250 Map<String, ? super Object> originalParams = new HashMap<>(params); 251 // Generate PkgInfo 252 File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo"); 253 pkgInfoFile.createNewFile(); 254 writePkgInfo(pkgInfoFile); 255 256 Path executable = macOSDir.resolve(getLauncherName(params)); 257 258 // create the main app launcher 259 try (InputStream is_launcher = 260 getResourceAsStream("jpackageapplauncher"); 261 InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) { 262 // Copy executable and library to MacOS folder 263 writeEntry(is_launcher, executable); 264 writeEntry(is_lib, macOSDir.resolve(LIBRARY_NAME)); 265 } 266 executable.toFile().setExecutable(true, false); 267 // generate main app launcher config file 268 File cfg = new File(root.toFile(), getLauncherCfgName(params)); 269 writeCfgFile(params, cfg); 270 271 // create additional app launcher(s) and config file(s) 272 List<Map<String, ? super Object>> entryPoints = 273 StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params); 274 for (Map<String, ? super Object> entryPoint : entryPoints) { 275 Map<String, ? super Object> tmp = 276 AddLauncherArguments.merge(originalParams, entryPoint); 277 278 // add executable for add launcher 279 Path addExecutable = macOSDir.resolve(getLauncherName(tmp)); 280 try (InputStream is = getResourceAsStream("jpackageapplauncher");) { 281 writeEntry(is, addExecutable); 282 } 283 addExecutable.toFile().setExecutable(true, false); 284 285 // add config file for add launcher 286 cfg = new File(root.toFile(), getLauncherCfgName(tmp)); 287 writeCfgFile(tmp, cfg); 288 } 289 290 // Copy class path entries to Java folder 291 copyClassPathEntries(appDir, params); 292 293 /*********** Take care of "config" files *******/ 294 295 createResource(TEMPLATE_BUNDLE_ICON, params) 296 .setCategory("icon") 297 .setExternal(ICON_ICNS.fetchFrom(params)) 298 .saveToFile(resourcesDir.resolve(APP_NAME.fetchFrom(params) 299 + ".icns")); 300 301 // copy file association icons 302 for (Map<String, ? 303 super Object> fa : FILE_ASSOCIATIONS.fetchFrom(params)) { 304 File f = FA_ICON.fetchFrom(fa); 305 if (f != null && f.exists()) { 306 try (InputStream in2 = new FileInputStream(f)) { 307 Files.copy(in2, resourcesDir.resolve(f.getName())); 308 } 309 310 } 311 } 312 313 copyRuntimeFiles(params); 314 sign(params); 315 } 316 317 @Override 318 public void prepareJreFiles(Map<String, ? super Object> params) 319 throws IOException { 320 copyRuntimeFiles(params); 321 sign(params); 322 } 323 324 @Override 325 File getRuntimeImageDir(File runtimeImageTop) { 326 File home = new File(runtimeImageTop, "Contents/Home"); 327 return (home.exists() ? home : runtimeImageTop); 328 } 329 330 private void copyRuntimeFiles(Map<String, ? super Object> params) 331 throws IOException { 332 // Generate Info.plist 333 writeInfoPlist(contentsDir.resolve("Info.plist").toFile(), params); 334 335 // generate java runtime info.plist 336 writeRuntimeInfoPlist( 337 runtimeDir.resolve("Contents/Info.plist").toFile(), params); 338 339 // copy library 340 Path runtimeMacOSDir = Files.createDirectories( 341 runtimeDir.resolve("Contents/MacOS")); 342 343 // JDK 9, 10, and 11 have extra '/jli/' subdir 344 Path jli = runtimeRoot.resolve("lib/libjli.dylib"); 345 if (!Files.exists(jli)) { 346 jli = runtimeRoot.resolve("lib/jli/libjli.dylib"); 347 } 348 349 Files.copy(jli, runtimeMacOSDir.resolve("libjli.dylib")); 350 } 351 352 private void sign(Map<String, ? super Object> params) throws IOException { 353 if (Optional.ofNullable( 354 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { 355 try { 356 addNewKeychain(params); 357 } catch (InterruptedException e) { 358 Log.error(e.getMessage()); 359 } 360 String signingIdentity = 361 DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params); 362 if (signingIdentity != null) { 363 signAppBundle(params, root, signingIdentity, 364 BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null); 365 } 366 restoreKeychainList(params); 367 } 368 } 369 370 private String getLauncherName(Map<String, ? super Object> params) { 371 if (APP_NAME.fetchFrom(params) != null) { 372 return APP_NAME.fetchFrom(params); 373 } else { 374 return MAIN_CLASS.fetchFrom(params); 375 } 376 } 377 378 public static String getLauncherCfgName( 379 Map<String, ? super Object> params) { 380 return "Contents/app/" + APP_NAME.fetchFrom(params) + ".cfg"; 381 } 382 383 private void copyClassPathEntries(Path javaDirectory, 384 Map<String, ? super Object> params) throws IOException { 385 List<RelativeFileSet> resourcesList = 386 APP_RESOURCES_LIST.fetchFrom(params); 387 if (resourcesList == null) { 388 throw new RuntimeException( 389 I18N.getString("message.null-classpath")); 390 } 391 392 for (RelativeFileSet classPath : resourcesList) { 393 File srcdir = classPath.getBaseDirectory(); 394 for (String fname : classPath.getIncludedFiles()) { 395 copyEntry(javaDirectory, srcdir, fname); 396 } 397 } 398 } 399 400 private String getBundleName(Map<String, ? super Object> params) { 401 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { 402 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); 403 if (bn.length() > 16) { 404 Log.error(MessageFormat.format(I18N.getString( 405 "message.bundle-name-too-long-warning"), 406 MAC_CF_BUNDLE_NAME.getID(), bn)); 407 } 408 return MAC_CF_BUNDLE_NAME.fetchFrom(params); 409 } else if (APP_NAME.fetchFrom(params) != null) { 410 return APP_NAME.fetchFrom(params); 411 } else { 412 String nm = MAIN_CLASS.fetchFrom(params); 413 if (nm.length() > 16) { 414 nm = nm.substring(0, 16); 415 } 416 return nm; 417 } 418 } 419 420 private void writeRuntimeInfoPlist(File file, 421 Map<String, ? super Object> params) throws IOException { 422 Map<String, String> data = new HashMap<>(); 423 String identifier = StandardBundlerParam.isRuntimeInstaller(params) ? 424 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) : 425 "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params); 426 data.put("CF_BUNDLE_IDENTIFIER", identifier); 427 String name = StandardBundlerParam.isRuntimeInstaller(params) ? 428 getBundleName(params): "Java Runtime Image"; 429 data.put("CF_BUNDLE_NAME", name); 430 data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params)); 431 data.put("CF_BUNDLE_SHORT_VERSION_STRING", VERSION.fetchFrom(params)); 432 433 createResource(TEMPLATE_RUNTIME_INFO_PLIST, params) 434 .setPublicName("Runtime-Info.plist") 435 .setCategory(I18N.getString("resource.runtime-info-plist")) 436 .setSubstitutionData(data) 437 .saveToFile(file); 438 } 439 440 private void writeInfoPlist(File file, Map<String, ? super Object> params) 441 throws IOException { 442 Log.verbose(MessageFormat.format(I18N.getString( 443 "message.preparing-info-plist"), file.getAbsolutePath())); 444 445 //prepare config for exe 446 //Note: do not need CFBundleDisplayName if we don't support localization 447 Map<String, String> data = new HashMap<>(); 448 data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns"); 449 data.put("DEPLOY_BUNDLE_IDENTIFIER", 450 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 451 data.put("DEPLOY_BUNDLE_NAME", 452 getBundleName(params)); 453 data.put("DEPLOY_BUNDLE_COPYRIGHT", 454 COPYRIGHT.fetchFrom(params) != null ? 455 COPYRIGHT.fetchFrom(params) : "Unknown"); 456 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); 457 data.put("DEPLOY_BUNDLE_SHORT_VERSION", 458 VERSION.fetchFrom(params) != null ? 459 VERSION.fetchFrom(params) : "1.0.0"); 460 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", 461 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? 462 MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); 463 464 boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null; 465 boolean hasMainModule = 466 StandardBundlerParam.MODULE.fetchFrom(params) != null; 467 468 if (hasMainJar) { 469 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params). 470 getIncludedFiles().iterator().next()); 471 } 472 else if (hasMainModule) { 473 data.put("DEPLOY_MODULE_NAME", 474 StandardBundlerParam.MODULE.fetchFrom(params)); 475 } 476 477 StringBuilder sb = new StringBuilder(); 478 List<String> jvmOptions = JAVA_OPTIONS.fetchFrom(params); 479 480 String newline = ""; //So we don't add extra line after last append 481 for (String o : jvmOptions) { 482 sb.append(newline).append( 485 } 486 487 data.put("DEPLOY_JAVA_OPTIONS", sb.toString()); 488 489 sb = new StringBuilder(); 490 List<String> args = ARGUMENTS.fetchFrom(params); 491 newline = ""; 492 // So we don't add unneccessary extra line after last append 493 494 for (String o : args) { 495 sb.append(newline).append(" <string>").append(o).append( 496 "</string>"); 497 newline = "\n"; 498 } 499 data.put("DEPLOY_ARGUMENTS", sb.toString()); 500 501 newline = ""; 502 503 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); 504 505 data.put("DEPLOY_APP_CLASSPATH", 506 getCfgClassPath(CLASSPATH.fetchFrom(params))); 507 508 StringBuilder bundleDocumentTypes = new StringBuilder(); 509 StringBuilder exportedTypes = new StringBuilder(); 510 for (Map<String, ? super Object> 511 fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { 512 513 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 514 515 if (extensions == null) { 516 Log.verbose(I18N.getString( 517 "message.creating-association-with-null-extension")); 518 } 519 520 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); 521 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) 522 + "." + ((extensions == null || extensions.isEmpty()) 523 ? "mime" : extensions.get(0)); 524 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 525 File icon = FA_ICON.fetchFrom(fileAssociation); 526 527 bundleDocumentTypes.append(" <dict>\n") 528 .append(" <key>LSItemContentTypes</key>\n") 529 .append(" <array>\n") 530 .append(" <string>") 531 .append(itemContentType) 532 .append("</string>\n") 533 .append(" </array>\n") 534 .append("\n") 535 .append(" <key>CFBundleTypeName</key>\n") 536 .append(" <string>") 537 .append(description) 538 .append("</string>\n") 539 .append("\n") 540 .append(" <key>LSHandlerRank</key>\n") 541 .append(" <string>Owner</string>\n") 542 // TODO make a bundler arg 543 .append("\n") 544 .append(" <key>CFBundleTypeRole</key>\n") 545 .append(" <string>Editor</string>\n") 616 } 617 exportedTypes.append(" </array>\n"); 618 } 619 exportedTypes.append(" </dict>\n") 620 .append(" </dict>\n"); 621 } 622 String associationData; 623 if (bundleDocumentTypes.length() > 0) { 624 associationData = 625 "\n <key>CFBundleDocumentTypes</key>\n <array>\n" 626 + bundleDocumentTypes.toString() 627 + " </array>\n\n" 628 + " <key>UTExportedTypeDeclarations</key>\n <array>\n" 629 + exportedTypes.toString() 630 + " </array>\n"; 631 } else { 632 associationData = ""; 633 } 634 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); 635 636 createResource(TEMPLATE_INFO_PLIST_LITE, params) 637 .setCategory(I18N.getString("resource.app-info-plist")) 638 .setSubstitutionData(data) 639 .setPublicName("Info.plist") 640 .saveToFile(file); 641 } 642 643 private void writePkgInfo(File file) throws IOException { 644 //hardcoded as it does not seem we need to change it ever 645 String signature = "????"; 646 647 try (Writer out = Files.newBufferedWriter(file.toPath())) { 648 out.write(OS_TYPE_CODE + signature); 649 out.flush(); 650 } 651 } 652 653 public static void addNewKeychain(Map<String, ? super Object> params) 654 throws IOException, InterruptedException { 655 if (Platform.getMajorVersion() < 10 || 656 (Platform.getMajorVersion() == 10 && 657 Platform.getMinorVersion() < 12)) { 658 // we need this for OS X 10.12+ 659 return; 660 } 661 662 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 663 if (keyChain == null || keyChain.isEmpty()) { 664 return; 665 } 666 667 // get current keychain list 683 684 keyChains = new ArrayList<>(); 685 // remove " 686 keychainList.forEach((String s) -> { 687 String path = s.trim(); 688 if (path.startsWith("\"") && path.endsWith("\"")) { 689 path = path.substring(1, path.length()-1); 690 } 691 keyChains.add(path); 692 }); 693 694 List<String> args = new ArrayList<>(); 695 args.add("security"); 696 args.add("list-keychains"); 697 args.add("-s"); 698 699 args.addAll(keyChains); 700 args.add(keyChain); 701 702 ProcessBuilder pb = new ProcessBuilder(args); 703 IOUtils.exec(pb); 704 } 705 706 public static void restoreKeychainList(Map<String, ? super Object> params) 707 throws IOException{ 708 if (Platform.getMajorVersion() < 10 || 709 (Platform.getMajorVersion() == 10 && 710 Platform.getMinorVersion() < 12)) { 711 // we need this for OS X 10.12+ 712 return; 713 } 714 715 if (keyChains == null || keyChains.isEmpty()) { 716 return; 717 } 718 719 List<String> args = new ArrayList<>(); 720 args.add("security"); 721 args.add("list-keychains"); 722 args.add("-s"); 723 724 args.addAll(keyChains); 725 726 ProcessBuilder pb = new ProcessBuilder(args); 727 IOUtils.exec(pb); 728 } 729 730 public static void signAppBundle( 731 Map<String, ? super Object> params, Path appLocation, 732 String signingIdentity, String identifierPrefix, 733 String entitlementsFile, String inheritedEntitlements) 734 throws IOException { 735 AtomicReference<IOException> toThrow = new AtomicReference<>(); 736 String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); 737 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 738 739 // sign all dylibs and jars 740 try (Stream<Path> stream = Files.walk(appLocation)) { 741 stream.peek(path -> { // fix permissions 742 try { 743 Set<PosixFilePermission> pfp = 744 Files.getPosixFilePermissions(path); 745 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { 746 pfp = EnumSet.copyOf(pfp); 747 pfp.add(PosixFilePermission.OWNER_WRITE); 748 Files.setPosixFilePermissions(path, pfp); 749 } 750 } catch (IOException e) { 751 Log.verbose(e); 752 } 753 }).filter(p -> Files.isRegularFile(p) 754 && !(p.toString().contains("/Contents/MacOS/libjli.dylib") 755 || p.toString().endsWith(appExecutable) 756 || p.toString().contains("/Contents/runtime") 757 || p.toString().contains("/Contents/Frameworks"))).forEach(p -> { 758 //noinspection ThrowableResultOfMethodCallIgnored 759 if (toThrow.get() != null) return; 760 761 // If p is a symlink then skip the signing process. 762 if (Files.isSymbolicLink(p)) { 763 if (VERBOSE.fetchFrom(params)) { 764 Log.verbose(MessageFormat.format(I18N.getString( 765 "message.ignoring.symlink"), p.toString())); 766 } 767 } else { 768 if (p.toString().endsWith(LIBRARY_NAME)) { 769 if (isFileSigned(p)) { 770 return; 771 } 772 } 773 774 List<String> args = new ArrayList<>(); 775 args.addAll(Arrays.asList("codesign", 776 "-s", signingIdentity, // sign with this key 777 "--prefix", identifierPrefix, 778 // use the identifier as a prefix 779 "-vvvv")); 780 if (entitlementsFile != null && 781 (p.toString().endsWith(".jar") 782 || p.toString().endsWith(".dylib"))) { 783 args.add("--entitlements"); 784 args.add(entitlementsFile); // entitlements 785 } else if (inheritedEntitlements != null && 786 Files.isExecutable(p)) { 787 args.add("--entitlements"); 788 args.add(inheritedEntitlements); 789 // inherited entitlements for executable processes 790 } 791 if (keyChain != null && !keyChain.isEmpty()) { 792 args.add("--keychain"); 793 args.add(keyChain); 794 } 795 args.add(p.toString()); 796 797 try { 798 Set<PosixFilePermission> oldPermissions = 799 Files.getPosixFilePermissions(p); 800 File f = p.toFile(); 801 f.setWritable(true, true); 802 803 ProcessBuilder pb = new ProcessBuilder(args); 804 IOUtils.exec(pb); 805 806 Files.setPosixFilePermissions(p, oldPermissions); 807 } catch (IOException ioe) { 808 toThrow.set(ioe); 809 } 810 } 811 }); 812 } 813 IOException ioe = toThrow.get(); 814 if (ioe != null) { 815 throw ioe; 816 } 817 818 // sign all runtime and frameworks 819 Consumer<? super Path> signIdentifiedByPList = path -> { 820 //noinspection ThrowableResultOfMethodCallIgnored 821 if (toThrow.get() != null) return; 822 823 try { 824 List<String> args = new ArrayList<>(); 825 args.addAll(Arrays.asList("codesign", 826 "-s", signingIdentity, // sign with this key 827 "--prefix", identifierPrefix, 828 // use the identifier as a prefix 829 "-vvvv")); 830 if (keyChain != null && !keyChain.isEmpty()) { 831 args.add("--keychain"); 832 args.add(keyChain); 833 } 834 args.add(path.toString()); 835 ProcessBuilder pb = new ProcessBuilder(args); 836 IOUtils.exec(pb); 837 838 args = new ArrayList<>(); 839 args.addAll(Arrays.asList("codesign", 840 "-s", signingIdentity, // sign with this key 841 "--prefix", identifierPrefix, 842 // use the identifier as a prefix 843 "-vvvv")); 844 if (keyChain != null && !keyChain.isEmpty()) { 845 args.add("--keychain"); 846 args.add(keyChain); 847 } 848 args.add(path.toString() 849 + "/Contents/_CodeSignature/CodeResources"); 850 pb = new ProcessBuilder(args); 851 IOUtils.exec(pb); 852 } catch (IOException e) { 853 toThrow.set(e); 854 } 855 }; 856 857 Path javaPath = appLocation.resolve("Contents/runtime"); 858 if (Files.isDirectory(javaPath)) { 859 signIdentifiedByPList.accept(javaPath); 860 861 ioe = toThrow.get(); 862 if (ioe != null) { 863 throw ioe; 864 } 865 } 866 Path frameworkPath = appLocation.resolve("Contents/Frameworks"); 867 if (Files.isDirectory(frameworkPath)) { 868 Files.list(frameworkPath) 869 .forEach(signIdentifiedByPList); 870 871 ioe = toThrow.get(); 872 if (ioe != null) { 873 throw ioe; 874 } 875 } 876 877 // sign the app itself 878 List<String> args = new ArrayList<>(); 879 args.addAll(Arrays.asList("codesign", 880 "-s", signingIdentity, // sign with this key 881 "-vvvv")); // super verbose output 882 if (entitlementsFile != null) { 883 args.add("--entitlements"); 884 args.add(entitlementsFile); // entitlements 885 } 886 if (keyChain != null && !keyChain.isEmpty()) { 887 args.add("--keychain"); 888 args.add(keyChain); 889 } 890 args.add(appLocation.toString()); 891 892 ProcessBuilder pb = 893 new ProcessBuilder(args.toArray(new String[args.size()])); 894 IOUtils.exec(pb); 895 } 896 897 private static boolean isFileSigned(Path file) { 898 ProcessBuilder pb = 899 new ProcessBuilder("codesign", "--verify", file.toString()); 900 901 try { 902 IOUtils.exec(pb); 903 } catch (IOException ex) { 904 return false; 905 } 906 907 return true; 908 } 909 910 private static String extractBundleIdentifier(Map<String, Object> params) { 911 if (PREDEFINED_APP_IMAGE.fetchFrom(params) == null) { 912 return null; 913 } 914 915 try { 916 File infoPList = new File(PREDEFINED_APP_IMAGE.fetchFrom(params) + 917 File.separator + "Contents" + 918 File.separator + "Info.plist"); 919 920 DocumentBuilderFactory dbf 921 = DocumentBuilderFactory.newDefaultInstance(); 922 dbf.setFeature("http://apache.org/xml/features/" + 923 "nonvalidating/load-external-dtd", false); 924 DocumentBuilder b = dbf.newDocumentBuilder(); 925 org.w3c.dom.Document doc = b.parse(new FileInputStream( 926 infoPList.getAbsolutePath())); 927 928 XPath xPath = XPathFactory.newInstance().newXPath(); 929 // Query for the value of <string> element preceding <key> 930 // element with value equal to CFBundleIdentifier 931 String v = (String) xPath.evaluate( 932 "//string[preceding-sibling::key = \"CFBundleIdentifier\"][1]", 933 doc, XPathConstants.STRING); 934 935 if (v != null && !v.isEmpty()) { 936 return v; 937 } 938 } catch (Exception ex) { 939 Log.verbose(ex); 940 } 941 942 return null; 943 } 944 945 } |