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 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/PlugIns/Java.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("PlugIns/Java.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 } 256 257 try { 258 BigInteger n = new BigInteger(p[0]); 259 if (BigInteger.ONE.compareTo(n) > 0) { 260 Log.verbose(I18N.getString( 261 "message.version-string-first-number-not-zero")); 262 return false; 263 } 264 if (p.length > 1) { 265 n = new BigInteger(p[1]); 266 if (BigInteger.ZERO.compareTo(n) > 0) { 267 Log.verbose(I18N.getString( 268 "message.version-string-no-negative-numbers")); 269 return false; 270 } 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/PlugIns/Java.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 = new HashMap<>(originalParams); 327 tmp.putAll(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/PlugIns/Java.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/PlugIns/Java.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 data.put("DEPLOY_PREFERENCES_ID", 528 PREFERENCES_ID.fetchFrom(params).toLowerCase()); 529 530 StringBuilder sb = new StringBuilder(); 531 List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params); 532 533 String newline = ""; //So we don't add extra line after last append 534 for (String o : jvmOptions) { 535 sb.append(newline).append( 536 " <string>").append(o).append("</string>"); 537 newline = "\n"; 538 } 539 540 data.put("DEPLOY_JVM_OPTIONS", sb.toString()); 541 542 sb = new StringBuilder(); 543 List<String> args = ARGUMENTS.fetchFrom(params); 544 newline = ""; 545 // So we don't add unneccessary extra line after last append 546 547 for (String o : args) { 548 sb.append(newline).append(" <string>").append(o).append( 549 "</string>"); 550 newline = "\n"; 551 } 552 data.put("DEPLOY_ARGUMENTS", sb.toString()); 553 554 newline = ""; 555 556 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); 557 558 StringBuilder macroedPath = new StringBuilder(); 559 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { 560 macroedPath.append(s); 561 macroedPath.append(":"); 562 } 563 macroedPath.deleteCharAt(macroedPath.length() - 1); 564 565 data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString()); 566 567 StringBuilder bundleDocumentTypes = new StringBuilder(); 568 StringBuilder exportedTypes = new StringBuilder(); 569 for (Map<String, ? super Object> 570 fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { 571 572 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 573 574 if (extensions == null) { 575 Log.verbose(I18N.getString( 576 "message.creating-association-with-null-extension")); 577 } 578 579 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); 580 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) 581 + "." + ((extensions == null || extensions.isEmpty()) 582 ? "mime" : extensions.get(0)); 583 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 584 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS 585 586 bundleDocumentTypes.append(" <dict>\n") 587 .append(" <key>LSItemContentTypes</key>\n") 588 .append(" <array>\n") 589 .append(" <string>") 590 .append(itemContentType) 591 .append("</string>\n") 592 .append(" </array>\n") 593 .append("\n") 594 .append(" <key>CFBundleTypeName</key>\n") 595 .append(" <string>") 596 .append(description) 597 .append("</string>\n") 598 .append("\n") 599 .append(" <key>LSHandlerRank</key>\n") 600 .append(" <string>Owner</string>\n") 601 // TODO make a bundler arg 602 .append("\n") 603 .append(" <key>CFBundleTypeRole</key>\n") 604 .append(" <string>Editor</string>\n") 605 // TODO make a bundler arg 606 .append("\n") 607 .append(" <key>LSIsAppleDefaultForType</key>\n") 608 .append(" <true/>\n") 609 // TODO make a bundler arg 610 .append("\n"); 611 612 if (icon != null && icon.exists()) { 613 bundleDocumentTypes 614 .append(" <key>CFBundleTypeIconFile</key>\n") 615 .append(" <string>") 616 .append(icon.getName()) 617 .append("</string>\n"); 618 } 619 bundleDocumentTypes.append(" </dict>\n"); 620 621 exportedTypes.append(" <dict>\n") 622 .append(" <key>UTTypeIdentifier</key>\n") 623 .append(" <string>") 624 .append(itemContentType) 625 .append("</string>\n") 626 .append("\n") 627 .append(" <key>UTTypeDescription</key>\n") 628 .append(" <string>") 629 .append(description) 630 .append("</string>\n") 631 .append(" <key>UTTypeConformsTo</key>\n") 632 .append(" <array>\n") 633 .append(" <string>public.data</string>\n") 634 //TODO expose this? 635 .append(" </array>\n") 636 .append("\n"); 637 638 if (icon != null && icon.exists()) { 639 exportedTypes.append(" <key>UTTypeIconFile</key>\n") 640 .append(" <string>") 641 .append(icon.getName()) 642 .append("</string>\n") 643 .append("\n"); 644 } 645 646 exportedTypes.append("\n") 647 .append(" <key>UTTypeTagSpecification</key>\n") 648 .append(" <dict>\n") 649 // TODO expose via param? .append( 650 // " <key>com.apple.ostype</key>\n"); 651 // TODO expose via param? .append( 652 // " <string>ABCD</string>\n") 653 .append("\n"); 654 655 if (extensions != null && !extensions.isEmpty()) { 656 exportedTypes.append( 657 " <key>public.filename-extension</key>\n") 658 .append(" <array>\n"); 659 660 for (String ext : extensions) { 661 exportedTypes.append(" <string>") 662 .append(ext) 663 .append("</string>\n"); 664 } 665 exportedTypes.append(" </array>\n"); 666 } 667 if (mimeTypes != null && !mimeTypes.isEmpty()) { 668 exportedTypes.append(" <key>public.mime-type</key>\n") 669 .append(" <array>\n"); 670 671 for (String mime : mimeTypes) { 672 exportedTypes.append(" <string>") 673 .append(mime) 674 .append("</string>\n"); 675 } 676 exportedTypes.append(" </array>\n"); 677 } 678 exportedTypes.append(" </dict>\n") 679 .append(" </dict>\n"); 680 } 681 String associationData; 682 if (bundleDocumentTypes.length() > 0) { 683 associationData = 684 "\n <key>CFBundleDocumentTypes</key>\n <array>\n" 685 + bundleDocumentTypes.toString() 686 + " </array>\n\n" 687 + " <key>UTExportedTypeDeclarations</key>\n <array>\n" 688 + exportedTypes.toString() 689 + " </array>\n"; 690 } else { 691 associationData = ""; 692 } 693 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); 694 695 696 Writer w = new BufferedWriter(new FileWriter(file)); 697 w.write(preprocessTextResource( 698 // getConfig_InfoPlist(params).getName(), 699 "Info.plist", 700 I18N.getString("resource.app-info-plist"), 701 TEMPLATE_INFO_PLIST_LITE, 702 data, VERBOSE.fetchFrom(params), 703 RESOURCE_DIR.fetchFrom(params))); 704 w.close(); 705 } 706 707 private void writePkgInfo(File file) throws IOException { 708 //hardcoded as it does not seem we need to change it ever 709 String signature = "????"; 710 711 try (Writer out = new BufferedWriter(new FileWriter(file))) { 712 out.write(OS_TYPE_CODE + signature); 713 out.flush(); 714 } 715 } 716 717 public static void addNewKeychain(Map<String, ? super Object> params) 718 throws IOException, InterruptedException { 719 if (Platform.getMajorVersion() < 10 || 720 (Platform.getMajorVersion() == 10 && 721 Platform.getMinorVersion() < 12)) { 722 // we need this for OS X 10.12+ 723 return; 724 } 725 726 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 727 if (keyChain == null || keyChain.isEmpty()) { 728 return; 729 } 730 731 // get current keychain list 732 String keyChainPath = new File (keyChain).getAbsolutePath().toString(); 733 List<String> keychainList = new ArrayList<>(); 734 int ret = IOUtils.getProcessOutput( 735 keychainList, "security", "list-keychains"); 736 if (ret != 0) { 737 Log.error(I18N.getString("message.keychain.error")); 738 return; 739 } 740 741 boolean contains = keychainList.stream().anyMatch( 742 str -> str.trim().equals("\""+keyChainPath.trim()+"\"")); 743 if (contains) { 744 // keychain is already added in the search list 745 return; 746 } 747 748 keyChains = new ArrayList<>(); 749 // remove " 750 keychainList.forEach((String s) -> { 751 String path = s.trim(); 752 if (path.startsWith("\"") && path.endsWith("\"")) { 753 path = path.substring(1, path.length()-1); 754 } 755 keyChains.add(path); 756 }); 757 758 List<String> args = new ArrayList<>(); 759 args.add("security"); 760 args.add("list-keychains"); 761 args.add("-s"); 762 763 args.addAll(keyChains); 764 args.add(keyChain); 765 766 ProcessBuilder pb = new ProcessBuilder(args); 767 IOUtils.exec(pb, false); 768 } 769 770 public static void restoreKeychainList(Map<String, ? super Object> params) 771 throws IOException{ 772 if (Platform.getMajorVersion() < 10 || 773 (Platform.getMajorVersion() == 10 && 774 Platform.getMinorVersion() < 12)) { 775 // we need this for OS X 10.12+ 776 return; 777 } 778 779 if (keyChains == null || keyChains.isEmpty()) { 780 return; 781 } 782 783 List<String> args = new ArrayList<>(); 784 args.add("security"); 785 args.add("list-keychains"); 786 args.add("-s"); 787 788 args.addAll(keyChains); 789 790 ProcessBuilder pb = new ProcessBuilder(args); 791 IOUtils.exec(pb, false); 792 } 793 794 public static void signAppBundle( 795 Map<String, ? super Object> params, Path appLocation, 796 String signingIdentity, String identifierPrefix, 797 String entitlementsFile, String inheritedEntitlements) 798 throws IOException { 799 AtomicReference<IOException> toThrow = new AtomicReference<>(); 800 String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); 801 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 802 803 // sign all dylibs and jars 804 Files.walk(appLocation) 805 // fix permissions 806 .peek(path -> { 807 try { 808 Set<PosixFilePermission> pfp = 809 Files.getPosixFilePermissions(path); 810 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { 811 pfp = EnumSet.copyOf(pfp); 812 pfp.add(PosixFilePermission.OWNER_WRITE); 813 Files.setPosixFilePermissions(path, pfp); 814 } 815 } catch (IOException e) { 816 Log.debug(e); 817 } 818 }) 819 .filter(p -> Files.isRegularFile(p) && 820 !(p.toString().contains("/Contents/MacOS/libjli.dylib") 821 || p.toString().contains( 822 "/Contents/MacOS/JavaAppletPlugin") 823 || p.toString().endsWith(appExecutable)) 824 ).forEach(p -> { 825 //noinspection ThrowableResultOfMethodCallIgnored 826 if (toThrow.get() != null) return; 827 828 // If p is a symlink then skip the signing process. 829 if (Files.isSymbolicLink(p)) { 830 if (VERBOSE.fetchFrom(params)) { 831 Log.verbose(MessageFormat.format(I18N.getString( 832 "message.ignoring.symlink"), p.toString())); 833 } 834 } 835 else { 836 List<String> args = new ArrayList<>(); 837 args.addAll(Arrays.asList("codesign", 838 "-s", signingIdentity, // sign with this key 839 "--prefix", identifierPrefix, 840 // use the identifier as a prefix 841 "-vvvv")); 842 if (entitlementsFile != null && 843 (p.toString().endsWith(".jar") 844 || p.toString().endsWith(".dylib"))) { 845 args.add("--entitlements"); 846 args.add(entitlementsFile); // entitlements 847 } else if (inheritedEntitlements != null && 848 Files.isExecutable(p)) { 849 args.add("--entitlements"); 850 args.add(inheritedEntitlements); 851 // inherited entitlements for executable processes 852 } 853 if (keyChain != null && !keyChain.isEmpty()) { 854 args.add("--keychain"); 855 args.add(keyChain); 856 } 857 args.add(p.toString()); 858 859 try { 860 Set<PosixFilePermission> oldPermissions = 861 Files.getPosixFilePermissions(p); 862 File f = p.toFile(); 863 f.setWritable(true, true); 864 865 ProcessBuilder pb = new ProcessBuilder(args); 866 IOUtils.exec(pb, false); 867 868 Files.setPosixFilePermissions(p, oldPermissions); 869 } catch (IOException ioe) { 870 toThrow.set(ioe); 871 } 872 } 873 }); 874 875 IOException ioe = toThrow.get(); 876 if (ioe != null) { 877 throw ioe; 878 } 879 880 // sign all plugins and frameworks 881 Consumer<? super Path> signIdentifiedByPList = path -> { 882 //noinspection ThrowableResultOfMethodCallIgnored 883 if (toThrow.get() != null) return; 884 885 try { 886 List<String> args = new ArrayList<>(); 887 args.addAll(Arrays.asList("codesign", 888 "-s", signingIdentity, // sign with this key 889 "--prefix", identifierPrefix, 890 // use the identifier as a prefix 891 "-vvvv")); 892 if (keyChain != null && !keyChain.isEmpty()) { 893 args.add("--keychain"); 894 args.add(keyChain); 895 } 896 args.add(path.toString()); 897 ProcessBuilder pb = new ProcessBuilder(args); 898 IOUtils.exec(pb, false); 899 900 args = new ArrayList<>(); 901 args.addAll(Arrays.asList("codesign", 902 "-s", signingIdentity, // sign with this key 903 "--prefix", identifierPrefix, 904 // use the identifier as a prefix 905 "-vvvv")); 906 if (keyChain != null && !keyChain.isEmpty()) { 907 args.add("--keychain"); 908 args.add(keyChain); 909 } 910 args.add(path.toString() 911 + "/Contents/_CodeSignature/CodeResources"); 912 pb = new ProcessBuilder(args); 913 IOUtils.exec(pb, false); 914 } catch (IOException e) { 915 toThrow.set(e); 916 } 917 }; 918 919 Path pluginsPath = appLocation.resolve("Contents/PlugIns"); 920 if (Files.isDirectory(pluginsPath)) { 921 Files.list(pluginsPath) 922 .forEach(signIdentifiedByPList); 923 924 ioe = toThrow.get(); 925 if (ioe != null) { 926 throw ioe; 927 } 928 } 929 Path frameworkPath = appLocation.resolve("Contents/Frameworks"); 930 if (Files.isDirectory(frameworkPath)) { 931 Files.list(frameworkPath) 932 .forEach(signIdentifiedByPList); 933 934 ioe = toThrow.get(); 935 if (ioe != null) { 936 throw ioe; 937 } 938 } 939 940 // sign the app itself 941 List<String> args = new ArrayList<>(); 942 args.addAll(Arrays.asList("codesign", 943 "-s", signingIdentity, // sign with this key 944 "-vvvv")); // super verbose output 945 if (entitlementsFile != null) { 946 args.add("--entitlements"); 947 args.add(entitlementsFile); // entitlements 948 } 949 if (keyChain != null && !keyChain.isEmpty()) { 950 args.add("--keychain"); 951 args.add(keyChain); 952 } 953 args.add(appLocation.toString()); 954 955 ProcessBuilder pb = 956 new ProcessBuilder(args.toArray(new String[args.size()])); 957 IOUtils.exec(pb, false); 958 } 959 960 }