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