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