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