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