1 /* 2 * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.jpackage.internal; 27 28 import java.io.BufferedWriter; 29 import java.io.File; 30 import java.io.FileInputStream; 31 import java.io.FileOutputStream; 32 import java.io.FileWriter; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.OutputStream; 36 import java.io.OutputStreamWriter; 37 import java.io.UncheckedIOException; 38 import java.io.Writer; 39 import java.math.BigInteger; 40 import java.nio.file.Files; 41 import java.nio.file.Path; 42 import java.nio.file.StandardCopyOption; 43 import java.nio.file.attribute.PosixFilePermission; 44 import java.text.MessageFormat; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.EnumSet; 48 import java.util.HashMap; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.Objects; 52 import java.util.Optional; 53 import java.util.ResourceBundle; 54 import java.util.Set; 55 import java.util.concurrent.atomic.AtomicReference; 56 import java.util.function.Consumer; 57 58 import static jdk.jpackage.internal.StandardBundlerParam.*; 59 import static jdk.jpackage.internal.MacBaseInstallerBundler.*; 60 import static jdk.jpackage.internal.MacAppBundler.*; 61 62 public class MacAppImageBuilder extends AbstractAppImageBuilder { 63 64 private static final ResourceBundle I18N = ResourceBundle.getBundle( 65 "jdk.jpackage.internal.resources.MacResources"); 66 67 private static final String LIBRARY_NAME = "libapplauncher.dylib"; 68 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns"; 69 private static final String OS_TYPE_CODE = "APPL"; 70 private static final String TEMPLATE_INFO_PLIST_LITE = 71 "Info-lite.plist.template"; 72 private static final String TEMPLATE_RUNTIME_INFO_PLIST = 73 "Runtime-Info.plist.template"; 74 75 private final Path root; 76 private final Path contentsDir; 77 private final Path javaDir; 78 private final Path javaModsDir; 79 private final Path resourcesDir; 80 private final Path macOSDir; 81 private final Path runtimeDir; 82 private final Path runtimeRoot; 83 private final Path mdir; 84 85 private final Map<String, ? super Object> params; 86 87 private static List<String> keyChains; 88 89 public static final BundlerParamInfo<Boolean> 90 MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>( 91 "mac.configure-launcher-in-plist", 92 Boolean.class, 93 params -> Boolean.FALSE, 94 (s, p) -> Boolean.valueOf(s)); 95 96 public static final EnumeratedBundlerParam<String> MAC_CATEGORY = 97 new EnumeratedBundlerParam<>( 98 Arguments.CLIOptions.MAC_APP_STORE_CATEGORY.getId(), 99 String.class, 100 params -> "Unknown", 101 (s, p) -> s, 102 MacAppBundler.getMacCategories(), 103 false //strict - for MacStoreBundler this should be strict 104 ); 105 106 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 107 new StandardBundlerParam<>( 108 Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(), 109 String.class, 110 params -> null, 111 (s, p) -> s); 112 113 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 114 new StandardBundlerParam<>( 115 Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(), 116 String.class, 117 IDENTIFIER::fetchFrom, 118 (s, p) -> s); 119 120 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 121 new StandardBundlerParam<>( 122 "mac.CFBundleVersion", 123 String.class, 124 p -> { 125 String s = VERSION.fetchFrom(p); 126 if (validCFBundleVersion(s)) { 127 return s; 128 } else { 129 return "100"; 130 } 131 }, 132 (s, p) -> s); 133 134 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = 135 new StandardBundlerParam<>( 136 ".mac.default.icns", 137 String.class, 138 params -> TEMPLATE_BUNDLE_ICON, 139 (s, p) -> s); 140 141 public static final BundlerParamInfo<File> ICON_ICNS = 142 new StandardBundlerParam<>( 143 "icon.icns", 144 File.class, 145 params -> { 146 File f = ICON.fetchFrom(params); 147 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 148 Log.error(MessageFormat.format( 149 I18N.getString("message.icon-not-icns"), f)); 150 return null; 151 } 152 return f; 153 }, 154 (s, p) -> new File(s)); 155 156 public static final StandardBundlerParam<Boolean> SIGN_BUNDLE = 157 new StandardBundlerParam<>( 158 Arguments.CLIOptions.MAC_SIGN.getId(), 159 Boolean.class, 160 params -> false, 161 // valueOf(null) is false, we actually do want null in some cases 162 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? 163 null : Boolean.valueOf(s) 164 ); 165 166 public MacAppImageBuilder(Map<String, Object> config, Path imageOutDir) 167 throws IOException { 168 super(config, imageOutDir.resolve(APP_NAME.fetchFrom(config) 169 + ".app/Contents/runtime/Contents/Home")); 170 171 Objects.requireNonNull(imageOutDir); 172 173 this.params = config; 174 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app"); 175 this.contentsDir = root.resolve("Contents"); 176 this.javaDir = contentsDir.resolve("Java"); 177 this.javaModsDir = javaDir.resolve("mods"); 178 this.resourcesDir = contentsDir.resolve("Resources"); 179 this.macOSDir = contentsDir.resolve("MacOS"); 180 this.runtimeDir = contentsDir.resolve("runtime"); 181 this.runtimeRoot = runtimeDir.resolve("Contents/Home"); 182 this.mdir = runtimeRoot.resolve("lib"); 183 Files.createDirectories(javaDir); 184 Files.createDirectories(resourcesDir); 185 Files.createDirectories(macOSDir); 186 Files.createDirectories(runtimeDir); 187 } 188 189 public MacAppImageBuilder(Map<String, Object> config, String jreName, 190 Path imageOutDir) throws IOException { 191 super(null, imageOutDir.resolve(jreName + "/Contents/Home")); 192 193 Objects.requireNonNull(imageOutDir); 194 195 this.params = config; 196 this.root = imageOutDir.resolve(jreName ); 197 this.contentsDir = root.resolve("Contents"); 198 this.javaDir = null; 199 this.javaModsDir = null; 200 this.resourcesDir = null; 201 this.macOSDir = null; 202 this.runtimeDir = this.root; 203 this.runtimeRoot = runtimeDir.resolve("Contents/Home"); 204 this.mdir = runtimeRoot.resolve("lib"); 205 206 Files.createDirectories(runtimeDir); 207 } 208 209 private void writeEntry(InputStream in, Path dstFile) throws IOException { 210 Files.createDirectories(dstFile.getParent()); 211 Files.copy(in, dstFile); 212 } 213 214 public static boolean validCFBundleVersion(String v) { 215 // CFBundleVersion (String - iOS, OS X) specifies the build version 216 // number of the bundle, which identifies an iteration (released or 217 // unreleased) of the bundle. The build version number should be a 218 // string comprised of three non-negative, period-separated integers 219 // with the first integer being greater than zero. The string should 220 // only contain numeric (0-9) and period (.) characters. Leading zeros 221 // are truncated from each integer and will be ignored (that is, 222 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 223 224 if (v == null) { 225 return false; 226 } 227 228 String p[] = v.split("\\."); 229 if (p.length > 3 || p.length < 1) { 230 Log.verbose(I18N.getString( 231 "message.version-string-too-many-components")); 232 return false; 233 } 234 235 try { 236 BigInteger n = new BigInteger(p[0]); 237 if (BigInteger.ONE.compareTo(n) > 0) { 238 Log.verbose(I18N.getString( 239 "message.version-string-first-number-not-zero")); 240 return false; 241 } 242 if (p.length > 1) { 243 n = new BigInteger(p[1]); 244 if (BigInteger.ZERO.compareTo(n) > 0) { 245 Log.verbose(I18N.getString( 246 "message.version-string-no-negative-numbers")); 247 return false; 248 } 249 } 250 if (p.length > 2) { 251 n = new BigInteger(p[2]); 252 if (BigInteger.ZERO.compareTo(n) > 0) { 253 Log.verbose(I18N.getString( 254 "message.version-string-no-negative-numbers")); 255 return false; 256 } 257 } 258 } catch (NumberFormatException ne) { 259 Log.verbose(I18N.getString("message.version-string-numbers-only")); 260 Log.verbose(ne); 261 return false; 262 } 263 264 return true; 265 } 266 267 @Override 268 public Path getAppDir() { 269 return javaDir; 270 } 271 272 @Override 273 public Path getAppModsDir() { 274 return javaModsDir; 275 } 276 277 @Override 278 public void prepareApplicationFiles() 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(javaDir); 321 322 /*********** Take care of "config" files *******/ 323 File icon = ICON_ICNS.fetchFrom(params); 324 325 InputStream in = locateResource( 326 APP_NAME.fetchFrom(params) + ".icns", 327 "icon", 328 DEFAULT_ICNS_ICON.fetchFrom(params), 329 icon, 330 VERBOSE.fetchFrom(params), 331 RESOURCE_DIR.fetchFrom(params)); 332 Files.copy(in, 333 resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns"), 334 StandardCopyOption.REPLACE_EXISTING); 335 336 // copy file association icons 337 for (Map<String, ? 338 super Object> fa : FILE_ASSOCIATIONS.fetchFrom(params)) { 339 File f = FA_ICON.fetchFrom(fa); 340 if (f != null && f.exists()) { 341 try (InputStream in2 = new FileInputStream(f)) { 342 Files.copy(in2, resourcesDir.resolve(f.getName())); 343 } 344 345 } 346 } 347 348 copyRuntimeFiles(); 349 sign(); 350 } 351 352 @Override 353 public void prepareJreFiles() throws IOException { 354 copyRuntimeFiles(); 355 sign(); 356 } 357 358 @Override 359 File getRuntimeImageDir(File runtimeImageTop) { 360 File home = new File(runtimeImageTop, "Contents/Home"); 361 return (home.exists() ? home : runtimeImageTop); 362 } 363 364 private void copyRuntimeFiles() throws IOException { 365 // Generate Info.plist 366 writeInfoPlist(contentsDir.resolve("Info.plist").toFile()); 367 368 // generate java runtime info.plist 369 writeRuntimeInfoPlist( 370 runtimeDir.resolve("Contents/Info.plist").toFile()); 371 372 // copy library 373 Path runtimeMacOSDir = Files.createDirectories( 374 runtimeDir.resolve("Contents/MacOS")); 375 376 // JDK 9, 10, and 11 have extra '/jli/' subdir 377 Path jli = runtimeRoot.resolve("lib/libjli.dylib"); 378 if (!Files.exists(jli)) { 379 jli = runtimeRoot.resolve("lib/jli/libjli.dylib"); 380 } 381 382 Files.copy(jli, runtimeMacOSDir.resolve("libjli.dylib")); 383 } 384 385 private void sign() throws IOException { 386 if (Optional.ofNullable( 387 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { 388 try { 389 addNewKeychain(params); 390 } catch (InterruptedException e) { 391 Log.error(e.getMessage()); 392 } 393 String signingIdentity = 394 DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params); 395 if (signingIdentity != null) { 396 signAppBundle(params, root, signingIdentity, 397 BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null); 398 } 399 restoreKeychainList(params); 400 } 401 } 402 403 private String getLauncherName(Map<String, ? super Object> params) { 404 if (APP_NAME.fetchFrom(params) != null) { 405 return APP_NAME.fetchFrom(params); 406 } else { 407 return MAIN_CLASS.fetchFrom(params); 408 } 409 } 410 411 public static String getLauncherCfgName( 412 Map<String, ? super Object> params) { 413 return "Contents/Java/" + APP_NAME.fetchFrom(params) + ".cfg"; 414 } 415 416 private void copyClassPathEntries(Path javaDirectory) throws IOException { 417 List<RelativeFileSet> resourcesList = 418 APP_RESOURCES_LIST.fetchFrom(params); 419 if (resourcesList == null) { 420 throw new RuntimeException( 421 I18N.getString("message.null-classpath")); 422 } 423 424 for (RelativeFileSet classPath : resourcesList) { 425 File srcdir = classPath.getBaseDirectory(); 426 for (String fname : classPath.getIncludedFiles()) { 427 copyEntry(javaDirectory, srcdir, fname); 428 } 429 } 430 } 431 432 private String getBundleName(Map<String, ? super Object> params) { 433 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { 434 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); 435 if (bn.length() > 16) { 436 Log.error(MessageFormat.format(I18N.getString( 437 "message.bundle-name-too-long-warning"), 438 MAC_CF_BUNDLE_NAME.getID(), bn)); 439 } 440 return MAC_CF_BUNDLE_NAME.fetchFrom(params); 441 } else if (APP_NAME.fetchFrom(params) != null) { 442 return APP_NAME.fetchFrom(params); 443 } else { 444 String nm = MAIN_CLASS.fetchFrom(params); 445 if (nm.length() > 16) { 446 nm = nm.substring(0, 16); 447 } 448 return nm; 449 } 450 } 451 452 private void writeRuntimeInfoPlist(File file) throws IOException { 453 Map<String, String> data = new HashMap<>(); 454 String identifier = StandardBundlerParam.isRuntimeInstaller(params) ? 455 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) : 456 "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params); 457 data.put("CF_BUNDLE_IDENTIFIER", identifier); 458 String name = StandardBundlerParam.isRuntimeInstaller(params) ? 459 getBundleName(params): "Java Runtime Image"; 460 data.put("CF_BUNDLE_NAME", name); 461 data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params)); 462 data.put("CF_BUNDLE_SHORT_VERSION_STRING", VERSION.fetchFrom(params)); 463 464 try (Writer w = Files.newBufferedWriter(file.toPath())) { 465 w.write(preprocessTextResource("Runtime-Info.plist", 466 I18N.getString("resource.runtime-info-plist"), 467 TEMPLATE_RUNTIME_INFO_PLIST, 468 data, 469 VERBOSE.fetchFrom(params), 470 RESOURCE_DIR.fetchFrom(params))); 471 } 472 } 473 474 private void writeInfoPlist(File file) throws IOException { 475 Log.verbose(MessageFormat.format(I18N.getString( 476 "message.preparing-info-plist"), file.getAbsolutePath())); 477 478 //prepare config for exe 479 //Note: do not need CFBundleDisplayName if we don't support localization 480 Map<String, String> data = new HashMap<>(); 481 data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns"); 482 data.put("DEPLOY_BUNDLE_IDENTIFIER", 483 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 484 data.put("DEPLOY_BUNDLE_NAME", 485 getBundleName(params)); 486 data.put("DEPLOY_BUNDLE_COPYRIGHT", 487 COPYRIGHT.fetchFrom(params) != null ? 488 COPYRIGHT.fetchFrom(params) : "Unknown"); 489 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); 490 data.put("DEPLOY_JAVA_RUNTIME_NAME", getCfgRuntimeDir()); 491 data.put("DEPLOY_BUNDLE_SHORT_VERSION", 492 VERSION.fetchFrom(params) != null ? 493 VERSION.fetchFrom(params) : "1.0.0"); 494 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", 495 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? 496 MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); 497 data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params)); 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 672 try (Writer w = Files.newBufferedWriter(file.toPath())) { 673 w.write(preprocessTextResource( 674 // getConfig_InfoPlist(params).getName(), 675 "Info.plist", 676 I18N.getString("resource.app-info-plist"), 677 TEMPLATE_INFO_PLIST_LITE, 678 data, VERBOSE.fetchFrom(params), 679 RESOURCE_DIR.fetchFrom(params))); 680 } 681 } 682 683 private void writePkgInfo(File file) throws IOException { 684 //hardcoded as it does not seem we need to change it ever 685 String signature = "????"; 686 687 try (Writer out = Files.newBufferedWriter(file.toPath())) { 688 out.write(OS_TYPE_CODE + signature); 689 out.flush(); 690 } 691 } 692 693 public static void addNewKeychain(Map<String, ? super Object> params) 694 throws IOException, InterruptedException { 695 if (Platform.getMajorVersion() < 10 || 696 (Platform.getMajorVersion() == 10 && 697 Platform.getMinorVersion() < 12)) { 698 // we need this for OS X 10.12+ 699 return; 700 } 701 702 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 703 if (keyChain == null || keyChain.isEmpty()) { 704 return; 705 } 706 707 // get current keychain list 708 String keyChainPath = new File (keyChain).getAbsolutePath().toString(); 709 List<String> keychainList = new ArrayList<>(); 710 int ret = IOUtils.getProcessOutput( 711 keychainList, "security", "list-keychains"); 712 if (ret != 0) { 713 Log.error(I18N.getString("message.keychain.error")); 714 return; 715 } 716 717 boolean contains = keychainList.stream().anyMatch( 718 str -> str.trim().equals("\""+keyChainPath.trim()+"\"")); 719 if (contains) { 720 // keychain is already added in the search list 721 return; 722 } 723 724 keyChains = new ArrayList<>(); 725 // remove " 726 keychainList.forEach((String s) -> { 727 String path = s.trim(); 728 if (path.startsWith("\"") && path.endsWith("\"")) { 729 path = path.substring(1, path.length()-1); 730 } 731 keyChains.add(path); 732 }); 733 734 List<String> args = new ArrayList<>(); 735 args.add("security"); 736 args.add("list-keychains"); 737 args.add("-s"); 738 739 args.addAll(keyChains); 740 args.add(keyChain); 741 742 ProcessBuilder pb = new ProcessBuilder(args); 743 IOUtils.exec(pb); 744 } 745 746 public static void restoreKeychainList(Map<String, ? super Object> params) 747 throws IOException{ 748 if (Platform.getMajorVersion() < 10 || 749 (Platform.getMajorVersion() == 10 && 750 Platform.getMinorVersion() < 12)) { 751 // we need this for OS X 10.12+ 752 return; 753 } 754 755 if (keyChains == null || keyChains.isEmpty()) { 756 return; 757 } 758 759 List<String> args = new ArrayList<>(); 760 args.add("security"); 761 args.add("list-keychains"); 762 args.add("-s"); 763 764 args.addAll(keyChains); 765 766 ProcessBuilder pb = new ProcessBuilder(args); 767 IOUtils.exec(pb); 768 } 769 770 public static void signAppBundle( 771 Map<String, ? super Object> params, Path appLocation, 772 String signingIdentity, String identifierPrefix, 773 String entitlementsFile, String inheritedEntitlements) 774 throws IOException { 775 AtomicReference<IOException> toThrow = new AtomicReference<>(); 776 String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); 777 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 778 779 // sign all dylibs and jars 780 Files.walk(appLocation) 781 // fix permissions 782 .peek(path -> { 783 try { 784 Set<PosixFilePermission> pfp = 785 Files.getPosixFilePermissions(path); 786 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { 787 pfp = EnumSet.copyOf(pfp); 788 pfp.add(PosixFilePermission.OWNER_WRITE); 789 Files.setPosixFilePermissions(path, pfp); 790 } 791 } catch (IOException e) { 792 Log.verbose(e); 793 } 794 }) 795 .filter(p -> Files.isRegularFile(p) && 796 !(p.toString().contains("/Contents/MacOS/libjli.dylib") 797 || p.toString().endsWith(appExecutable)) 798 ).forEach(p -> { 799 //noinspection ThrowableResultOfMethodCallIgnored 800 if (toThrow.get() != null) return; 801 802 // If p is a symlink then skip the signing process. 803 if (Files.isSymbolicLink(p)) { 804 if (VERBOSE.fetchFrom(params)) { 805 Log.verbose(MessageFormat.format(I18N.getString( 806 "message.ignoring.symlink"), p.toString())); 807 } 808 } 809 else { 810 List<String> args = new ArrayList<>(); 811 args.addAll(Arrays.asList("codesign", 812 "-s", signingIdentity, // sign with this key 813 "--prefix", identifierPrefix, 814 // use the identifier as a prefix 815 "-vvvv")); 816 if (entitlementsFile != null && 817 (p.toString().endsWith(".jar") 818 || p.toString().endsWith(".dylib"))) { 819 args.add("--entitlements"); 820 args.add(entitlementsFile); // entitlements 821 } else if (inheritedEntitlements != null && 822 Files.isExecutable(p)) { 823 args.add("--entitlements"); 824 args.add(inheritedEntitlements); 825 // inherited entitlements for executable processes 826 } 827 if (keyChain != null && !keyChain.isEmpty()) { 828 args.add("--keychain"); 829 args.add(keyChain); 830 } 831 args.add(p.toString()); 832 833 try { 834 Set<PosixFilePermission> oldPermissions = 835 Files.getPosixFilePermissions(p); 836 File f = p.toFile(); 837 f.setWritable(true, true); 838 839 ProcessBuilder pb = new ProcessBuilder(args); 840 IOUtils.exec(pb); 841 842 Files.setPosixFilePermissions(p, oldPermissions); 843 } catch (IOException ioe) { 844 toThrow.set(ioe); 845 } 846 } 847 }); 848 849 IOException ioe = toThrow.get(); 850 if (ioe != null) { 851 throw ioe; 852 } 853 854 // sign all runtime and frameworks 855 Consumer<? super Path> signIdentifiedByPList = path -> { 856 //noinspection ThrowableResultOfMethodCallIgnored 857 if (toThrow.get() != null) return; 858 859 try { 860 List<String> args = new ArrayList<>(); 861 args.addAll(Arrays.asList("codesign", 862 "-s", signingIdentity, // sign with this key 863 "--prefix", identifierPrefix, 864 // use the identifier as a prefix 865 "-vvvv")); 866 if (keyChain != null && !keyChain.isEmpty()) { 867 args.add("--keychain"); 868 args.add(keyChain); 869 } 870 args.add(path.toString()); 871 ProcessBuilder pb = new ProcessBuilder(args); 872 IOUtils.exec(pb); 873 874 args = new ArrayList<>(); 875 args.addAll(Arrays.asList("codesign", 876 "-s", signingIdentity, // sign with this key 877 "--prefix", identifierPrefix, 878 // use the identifier as a prefix 879 "-vvvv")); 880 if (keyChain != null && !keyChain.isEmpty()) { 881 args.add("--keychain"); 882 args.add(keyChain); 883 } 884 args.add(path.toString() 885 + "/Contents/_CodeSignature/CodeResources"); 886 pb = new ProcessBuilder(args); 887 IOUtils.exec(pb); 888 } catch (IOException e) { 889 toThrow.set(e); 890 } 891 }; 892 893 Path javaPath = appLocation.resolve("Contents/runtime"); 894 if (Files.isDirectory(javaPath)) { 895 signIdentifiedByPList.accept(javaPath); 896 897 ioe = toThrow.get(); 898 if (ioe != null) { 899 throw ioe; 900 } 901 } 902 Path frameworkPath = appLocation.resolve("Contents/Frameworks"); 903 if (Files.isDirectory(frameworkPath)) { 904 Files.list(frameworkPath) 905 .forEach(signIdentifiedByPList); 906 907 ioe = toThrow.get(); 908 if (ioe != null) { 909 throw ioe; 910 } 911 } 912 913 // sign the app itself 914 List<String> args = new ArrayList<>(); 915 args.addAll(Arrays.asList("codesign", 916 "-s", signingIdentity, // sign with this key 917 "-vvvv")); // super verbose output 918 if (entitlementsFile != null) { 919 args.add("--entitlements"); 920 args.add(entitlementsFile); // entitlements 921 } 922 if (keyChain != null && !keyChain.isEmpty()) { 923 args.add("--keychain"); 924 args.add(keyChain); 925 } 926 args.add(appLocation.toString()); 927 928 ProcessBuilder pb = 929 new ProcessBuilder(args.toArray(new String[args.size()])); 930 IOUtils.exec(pb); 931 } 932 933 }