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