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