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