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