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