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 signAppBundle(params, root, signingIdentity, 372 BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null); 373 } 374 restoreKeychainList(params); 375 } 376 } 377 378 private String getLauncherName(Map<String, ? super Object> params) { 379 if (APP_NAME.fetchFrom(params) != null) { 380 return APP_NAME.fetchFrom(params); 381 } else { 382 return MAIN_CLASS.fetchFrom(params); 383 } 384 } 385 386 public static String getLauncherCfgName( 387 Map<String, ? super Object> params) { 388 return "Contents/app/" + APP_NAME.fetchFrom(params) + ".cfg"; 389 } 390 391 private void copyClassPathEntries(Path javaDirectory, 392 Map<String, ? super Object> params) throws IOException { 393 List<RelativeFileSet> resourcesList = 394 APP_RESOURCES_LIST.fetchFrom(params); 395 if (resourcesList == null) { 396 throw new RuntimeException( 397 I18N.getString("message.null-classpath")); 398 } 399 400 for (RelativeFileSet classPath : resourcesList) { 401 File srcdir = classPath.getBaseDirectory(); 402 for (String fname : classPath.getIncludedFiles()) { 403 copyEntry(javaDirectory, srcdir, fname); 404 } 405 } 406 } 407 408 private String getBundleName(Map<String, ? super Object> params) { 409 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { 410 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); 411 if (bn.length() > 16) { 412 Log.error(MessageFormat.format(I18N.getString( 413 "message.bundle-name-too-long-warning"), 414 MAC_CF_BUNDLE_NAME.getID(), bn)); 415 } 416 return MAC_CF_BUNDLE_NAME.fetchFrom(params); 417 } else if (APP_NAME.fetchFrom(params) != null) { 418 return APP_NAME.fetchFrom(params); 419 } else { 420 String nm = MAIN_CLASS.fetchFrom(params); 421 if (nm.length() > 16) { 422 nm = nm.substring(0, 16); 423 } 424 return nm; 425 } 426 } 427 428 private void writeRuntimeInfoPlist(File file, 429 Map<String, ? super Object> params) throws IOException { 430 Map<String, String> data = new HashMap<>(); 431 String identifier = StandardBundlerParam.isRuntimeInstaller(params) ? 432 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) : 433 "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params); 434 data.put("CF_BUNDLE_IDENTIFIER", identifier); 435 String name = StandardBundlerParam.isRuntimeInstaller(params) ? 436 getBundleName(params): "Java Runtime Image"; 437 data.put("CF_BUNDLE_NAME", name); 438 data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params)); 439 data.put("CF_BUNDLE_SHORT_VERSION_STRING", VERSION.fetchFrom(params)); 440 441 createResource(TEMPLATE_RUNTIME_INFO_PLIST, params) 442 .setPublicName("Runtime-Info.plist") 443 .setCategory(I18N.getString("resource.runtime-info-plist")) 444 .setSubstitutionData(data) 445 .saveToFile(file); 446 } 447 448 private void writeInfoPlist(File file, Map<String, ? super Object> params) 449 throws IOException { 450 Log.verbose(MessageFormat.format(I18N.getString( 451 "message.preparing-info-plist"), file.getAbsolutePath())); 452 453 //prepare config for exe 454 //Note: do not need CFBundleDisplayName if we don't support localization 455 Map<String, String> data = new HashMap<>(); 456 data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns"); 457 data.put("DEPLOY_BUNDLE_IDENTIFIER", 458 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 459 data.put("DEPLOY_BUNDLE_NAME", 460 getBundleName(params)); 461 data.put("DEPLOY_BUNDLE_COPYRIGHT", 462 COPYRIGHT.fetchFrom(params) != null ? 463 COPYRIGHT.fetchFrom(params) : "Unknown"); 464 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); 465 data.put("DEPLOY_BUNDLE_SHORT_VERSION", 466 VERSION.fetchFrom(params) != null ? 467 VERSION.fetchFrom(params) : "1.0.0"); 468 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", 469 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? 470 MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); 471 472 boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null; 473 boolean hasMainModule = 474 StandardBundlerParam.MODULE.fetchFrom(params) != null; 475 476 if (hasMainJar) { 477 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params). 478 getIncludedFiles().iterator().next()); 479 } 480 else if (hasMainModule) { 481 data.put("DEPLOY_MODULE_NAME", 482 StandardBundlerParam.MODULE.fetchFrom(params)); 483 } 484 485 StringBuilder sb = new StringBuilder(); 486 List<String> jvmOptions = JAVA_OPTIONS.fetchFrom(params); 487 488 String newline = ""; //So we don't add extra line after last append 489 for (String o : jvmOptions) { 490 sb.append(newline).append( 491 " <string>").append(o).append("</string>"); 492 newline = "\n"; 493 } 494 495 data.put("DEPLOY_JAVA_OPTIONS", sb.toString()); 496 497 sb = new StringBuilder(); 498 List<String> args = ARGUMENTS.fetchFrom(params); 499 newline = ""; 500 // So we don't add unneccessary extra line after last append 501 502 for (String o : args) { 503 sb.append(newline).append(" <string>").append(o).append( 504 "</string>"); 505 newline = "\n"; 506 } 507 data.put("DEPLOY_ARGUMENTS", sb.toString()); 508 509 newline = ""; 510 511 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); 512 513 data.put("DEPLOY_APP_CLASSPATH", 514 getCfgClassPath(CLASSPATH.fetchFrom(params))); 515 516 StringBuilder bundleDocumentTypes = new StringBuilder(); 517 StringBuilder exportedTypes = new StringBuilder(); 518 for (Map<String, ? super Object> 519 fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { 520 521 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 522 523 if (extensions == null) { 524 Log.verbose(I18N.getString( 525 "message.creating-association-with-null-extension")); 526 } 527 528 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); 529 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) 530 + "." + ((extensions == null || extensions.isEmpty()) 531 ? "mime" : extensions.get(0)); 532 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 533 File icon = FA_ICON.fetchFrom(fileAssociation); 534 535 bundleDocumentTypes.append(" <dict>\n") 536 .append(" <key>LSItemContentTypes</key>\n") 537 .append(" <array>\n") 538 .append(" <string>") 539 .append(itemContentType) 540 .append("</string>\n") 541 .append(" </array>\n") 542 .append("\n") 543 .append(" <key>CFBundleTypeName</key>\n") 544 .append(" <string>") 545 .append(description) 546 .append("</string>\n") 547 .append("\n") 548 .append(" <key>LSHandlerRank</key>\n") 549 .append(" <string>Owner</string>\n") 550 // TODO make a bundler arg 551 .append("\n") 552 .append(" <key>CFBundleTypeRole</key>\n") 553 .append(" <string>Editor</string>\n") 554 // TODO make a bundler arg 555 .append("\n") 556 .append(" <key>LSIsAppleDefaultForType</key>\n") 557 .append(" <true/>\n") 558 // TODO make a bundler arg 559 .append("\n"); 560 561 if (icon != null && icon.exists()) { 562 bundleDocumentTypes 563 .append(" <key>CFBundleTypeIconFile</key>\n") 564 .append(" <string>") 565 .append(icon.getName()) 566 .append("</string>\n"); 567 } 568 bundleDocumentTypes.append(" </dict>\n"); 569 570 exportedTypes.append(" <dict>\n") 571 .append(" <key>UTTypeIdentifier</key>\n") 572 .append(" <string>") 573 .append(itemContentType) 574 .append("</string>\n") 575 .append("\n") 576 .append(" <key>UTTypeDescription</key>\n") 577 .append(" <string>") 578 .append(description) 579 .append("</string>\n") 580 .append(" <key>UTTypeConformsTo</key>\n") 581 .append(" <array>\n") 582 .append(" <string>public.data</string>\n") 583 //TODO expose this? 584 .append(" </array>\n") 585 .append("\n"); 586 587 if (icon != null && icon.exists()) { 588 exportedTypes.append(" <key>UTTypeIconFile</key>\n") 589 .append(" <string>") 590 .append(icon.getName()) 591 .append("</string>\n") 592 .append("\n"); 593 } 594 595 exportedTypes.append("\n") 596 .append(" <key>UTTypeTagSpecification</key>\n") 597 .append(" <dict>\n") 598 // TODO expose via param? .append( 599 // " <key>com.apple.ostype</key>\n"); 600 // TODO expose via param? .append( 601 // " <string>ABCD</string>\n") 602 .append("\n"); 603 604 if (extensions != null && !extensions.isEmpty()) { 605 exportedTypes.append( 606 " <key>public.filename-extension</key>\n") 607 .append(" <array>\n"); 608 609 for (String ext : extensions) { 610 exportedTypes.append(" <string>") 611 .append(ext) 612 .append("</string>\n"); 613 } 614 exportedTypes.append(" </array>\n"); 615 } 616 if (mimeTypes != null && !mimeTypes.isEmpty()) { 617 exportedTypes.append(" <key>public.mime-type</key>\n") 618 .append(" <array>\n"); 619 620 for (String mime : mimeTypes) { 621 exportedTypes.append(" <string>") 622 .append(mime) 623 .append("</string>\n"); 624 } 625 exportedTypes.append(" </array>\n"); 626 } 627 exportedTypes.append(" </dict>\n") 628 .append(" </dict>\n"); 629 } 630 String associationData; 631 if (bundleDocumentTypes.length() > 0) { 632 associationData = 633 "\n <key>CFBundleDocumentTypes</key>\n <array>\n" 634 + bundleDocumentTypes.toString() 635 + " </array>\n\n" 636 + " <key>UTExportedTypeDeclarations</key>\n <array>\n" 637 + exportedTypes.toString() 638 + " </array>\n"; 639 } else { 640 associationData = ""; 641 } 642 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); 643 644 createResource(TEMPLATE_INFO_PLIST_LITE, params) 645 .setCategory(I18N.getString("resource.app-info-plist")) 646 .setSubstitutionData(data) 647 .setPublicName("Info.plist") 648 .saveToFile(file); 649 } 650 651 private void writePkgInfo(File file) throws IOException { 652 //hardcoded as it does not seem we need to change it ever 653 String signature = "????"; 654 655 try (Writer out = Files.newBufferedWriter(file.toPath())) { 656 out.write(OS_TYPE_CODE + signature); 657 out.flush(); 658 } 659 } 660 661 public static void addNewKeychain(Map<String, ? super Object> params) 662 throws IOException, InterruptedException { 663 if (Platform.getMajorVersion() < 10 || 664 (Platform.getMajorVersion() == 10 && 665 Platform.getMinorVersion() < 12)) { 666 // we need this for OS X 10.12+ 667 return; 668 } 669 670 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 671 if (keyChain == null || keyChain.isEmpty()) { 672 return; 673 } 674 675 // get current keychain list 676 String keyChainPath = new File (keyChain).getAbsolutePath().toString(); 677 List<String> keychainList = new ArrayList<>(); 678 int ret = IOUtils.getProcessOutput( 679 keychainList, "security", "list-keychains"); 680 if (ret != 0) { 681 Log.error(I18N.getString("message.keychain.error")); 682 return; 683 } 684 685 boolean contains = keychainList.stream().anyMatch( 686 str -> str.trim().equals("\""+keyChainPath.trim()+"\"")); 687 if (contains) { 688 // keychain is already added in the search list 689 return; 690 } 691 692 keyChains = new ArrayList<>(); 693 // remove " 694 keychainList.forEach((String s) -> { 695 String path = s.trim(); 696 if (path.startsWith("\"") && path.endsWith("\"")) { 697 path = path.substring(1, path.length()-1); 698 } 699 keyChains.add(path); 700 }); 701 702 List<String> args = new ArrayList<>(); 703 args.add("security"); 704 args.add("list-keychains"); 705 args.add("-s"); 706 707 args.addAll(keyChains); 708 args.add(keyChain); 709 710 ProcessBuilder pb = new ProcessBuilder(args); 711 IOUtils.exec(pb); 712 } 713 714 public static void restoreKeychainList(Map<String, ? super Object> params) 715 throws IOException{ 716 if (Platform.getMajorVersion() < 10 || 717 (Platform.getMajorVersion() == 10 && 718 Platform.getMinorVersion() < 12)) { 719 // we need this for OS X 10.12+ 720 return; 721 } 722 723 if (keyChains == null || keyChains.isEmpty()) { 724 return; 725 } 726 727 List<String> args = new ArrayList<>(); 728 args.add("security"); 729 args.add("list-keychains"); 730 args.add("-s"); 731 732 args.addAll(keyChains); 733 734 ProcessBuilder pb = new ProcessBuilder(args); 735 IOUtils.exec(pb); 736 } 737 738 public static void signAppBundle( 739 Map<String, ? super Object> params, Path appLocation, 740 String signingIdentity, String identifierPrefix, 741 String entitlementsFile, String inheritedEntitlements) 742 throws IOException { 743 AtomicReference<IOException> toThrow = new AtomicReference<>(); 744 String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); 745 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 746 747 // sign all dylibs and jars 748 try (Stream<Path> stream = Files.walk(appLocation)) { 749 stream.peek(path -> { // fix permissions 750 try { 751 Set<PosixFilePermission> pfp = 752 Files.getPosixFilePermissions(path); 753 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { 754 pfp = EnumSet.copyOf(pfp); 755 pfp.add(PosixFilePermission.OWNER_WRITE); 756 Files.setPosixFilePermissions(path, pfp); 757 } 758 } catch (IOException e) { 759 Log.verbose(e); 760 } 761 }).filter(p -> Files.isRegularFile(p) 762 && !(p.toString().contains("/Contents/MacOS/libjli.dylib") 763 || p.toString().endsWith(appExecutable) 764 || p.toString().contains("/Contents/runtime") 765 || p.toString().contains("/Contents/Frameworks"))).forEach(p -> { 766 //noinspection ThrowableResultOfMethodCallIgnored 767 if (toThrow.get() != null) return; 768 769 // If p is a symlink then skip the signing process. 770 if (Files.isSymbolicLink(p)) { 771 if (VERBOSE.fetchFrom(params)) { 772 Log.verbose(MessageFormat.format(I18N.getString( 773 "message.ignoring.symlink"), p.toString())); 774 } 775 } else { 776 if (p.toString().endsWith(LIBRARY_NAME)) { 777 if (isFileSigned(p)) { 778 return; 779 } 780 } 781 782 List<String> args = new ArrayList<>(); 783 args.addAll(Arrays.asList("codesign", 784 "-s", signingIdentity, // sign with this key 785 "--prefix", identifierPrefix, 786 // use the identifier as a prefix 787 "-vvvv")); 788 if (entitlementsFile != null && 789 (p.toString().endsWith(".jar") 790 || p.toString().endsWith(".dylib"))) { 791 args.add("--entitlements"); 792 args.add(entitlementsFile); // entitlements 793 } else if (inheritedEntitlements != null && 794 Files.isExecutable(p)) { 795 args.add("--entitlements"); 796 args.add(inheritedEntitlements); 797 // inherited entitlements for executable processes 798 } 799 if (keyChain != null && !keyChain.isEmpty()) { 800 args.add("--keychain"); 801 args.add(keyChain); 802 } 803 args.add(p.toString()); 804 805 try { 806 Set<PosixFilePermission> oldPermissions = 807 Files.getPosixFilePermissions(p); 808 File f = p.toFile(); 809 f.setWritable(true, true); 810 811 ProcessBuilder pb = new ProcessBuilder(args); 812 IOUtils.exec(pb); 813 814 Files.setPosixFilePermissions(p, oldPermissions); 815 } catch (IOException ioe) { 816 toThrow.set(ioe); 817 } 818 } 819 }); 820 } 821 IOException ioe = toThrow.get(); 822 if (ioe != null) { 823 throw ioe; 824 } 825 826 // sign all runtime and frameworks 827 Consumer<? super Path> signIdentifiedByPList = path -> { 828 //noinspection ThrowableResultOfMethodCallIgnored 829 if (toThrow.get() != null) return; 830 831 try { 832 List<String> args = new ArrayList<>(); 833 args.addAll(Arrays.asList("codesign", 834 "-f", 835 "-s", signingIdentity, // sign with this key 836 "--prefix", identifierPrefix, 837 // use the identifier as a prefix 838 "-vvvv")); 839 if (keyChain != null && !keyChain.isEmpty()) { 840 args.add("--keychain"); 841 args.add(keyChain); 842 } 843 args.add(path.toString()); 844 ProcessBuilder pb = new ProcessBuilder(args); 845 IOUtils.exec(pb); 846 847 args = new ArrayList<>(); 848 args.addAll(Arrays.asList("codesign", 849 "-s", signingIdentity, // sign with this key 850 "--prefix", identifierPrefix, 851 // use the identifier as a prefix 852 "-vvvv")); 853 if (keyChain != null && !keyChain.isEmpty()) { 854 args.add("--keychain"); 855 args.add(keyChain); 856 } 857 args.add(path.toString() 858 + "/Contents/_CodeSignature/CodeResources"); 859 pb = new ProcessBuilder(args); 860 IOUtils.exec(pb); 861 } catch (IOException e) { 862 toThrow.set(e); 863 } 864 }; 865 866 Path javaPath = appLocation.resolve("Contents/runtime"); 867 if (Files.isDirectory(javaPath)) { 868 signIdentifiedByPList.accept(javaPath); 869 870 ioe = toThrow.get(); 871 if (ioe != null) { 872 throw ioe; 873 } 874 } 875 Path frameworkPath = appLocation.resolve("Contents/Frameworks"); 876 if (Files.isDirectory(frameworkPath)) { 877 Files.list(frameworkPath) 878 .forEach(signIdentifiedByPList); 879 880 ioe = toThrow.get(); 881 if (ioe != null) { 882 throw ioe; 883 } 884 } 885 886 // sign the app itself 887 List<String> args = new ArrayList<>(); 888 args.addAll(Arrays.asList("codesign", 889 "-s", signingIdentity, // sign with this key 890 "-vvvv")); // super verbose output 891 if (entitlementsFile != null) { 892 args.add("--entitlements"); 893 args.add(entitlementsFile); // entitlements 894 } 895 if (keyChain != null && !keyChain.isEmpty()) { 896 args.add("--keychain"); 897 args.add(keyChain); 898 } 899 args.add(appLocation.toString()); 900 901 ProcessBuilder pb = 902 new ProcessBuilder(args.toArray(new String[args.size()])); 903 IOUtils.exec(pb); 904 } 905 906 private static boolean isFileSigned(Path file) { 907 ProcessBuilder pb = 908 new ProcessBuilder("codesign", "--verify", file.toString()); 909 910 try { 911 IOUtils.exec(pb); 912 } catch (IOException ex) { 913 return false; 914 } 915 916 return true; 917 } 918 919 private static String extractBundleIdentifier(Map<String, Object> params) { 920 if (PREDEFINED_APP_IMAGE.fetchFrom(params) == null) { 921 return null; 922 } 923 924 try { 925 File infoPList = new File(PREDEFINED_APP_IMAGE.fetchFrom(params) + 926 File.separator + "Contents" + 927 File.separator + "Info.plist"); 928 929 DocumentBuilderFactory dbf 930 = DocumentBuilderFactory.newDefaultInstance(); 931 dbf.setFeature("http://apache.org/xml/features/" + 932 "nonvalidating/load-external-dtd", false); 933 DocumentBuilder b = dbf.newDocumentBuilder(); 934 org.w3c.dom.Document doc = b.parse(new FileInputStream( 935 infoPList.getAbsolutePath())); 936 937 XPath xPath = XPathFactory.newInstance().newXPath(); 938 // Query for the value of <string> element preceding <key> 939 // element with value equal to CFBundleIdentifier 940 String v = (String) xPath.evaluate( 941 "//string[preceding-sibling::key = \"CFBundleIdentifier\"][1]", 942 doc, XPathConstants.STRING); 943 944 if (v != null && !v.isEmpty()) { 945 return v; 946 } 947 } catch (Exception ex) { 948 Log.verbose(ex); 949 } 950 951 return null; 952 } 953 954 }