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