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