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