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