1 /*
   2  * Copyright (c) 2015, 2016, 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.builders.mac;
  26 
  27 import com.oracle.tools.packager.BundlerParamInfo;
  28 import com.oracle.tools.packager.IOUtils;
  29 import com.oracle.tools.packager.Log;
  30 import com.oracle.tools.packager.RelativeFileSet;
  31 import com.oracle.tools.packager.StandardBundlerParam;
  32 import com.oracle.tools.packager.mac.MacResources;
  33 import jdk.packager.builders.AbstractAppImageBuilder;
  34 import jdk.tools.jlink.plugin.Pool;
  35 
  36 import java.io.BufferedWriter;
  37 import java.io.File;
  38 import java.io.FileInputStream;
  39 import java.io.FileOutputStream;
  40 import java.io.FileWriter;
  41 import java.io.IOException;
  42 import java.io.InputStream;
  43 import java.io.OutputStream;
  44 import java.io.OutputStreamWriter;
  45 import java.io.UncheckedIOException;
  46 import java.io.Writer;
  47 import java.math.BigInteger;
  48 import java.nio.file.Files;
  49 import java.nio.file.Path;
  50 import java.nio.file.attribute.PosixFilePermission;
  51 import java.text.MessageFormat;
  52 import java.util.ArrayList;
  53 import java.util.Arrays;
  54 import java.util.EnumSet;
  55 import java.util.HashMap;
  56 import java.util.List;
  57 import java.util.Map;
  58 import java.util.Objects;
  59 import java.util.Optional;
  60 import java.util.ResourceBundle;
  61 import java.util.Set;
  62 import java.util.concurrent.atomic.AtomicReference;
  63 import java.util.function.Consumer;
  64 
  65 import static com.oracle.tools.packager.StandardBundlerParam.*;
  66 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.*;
  67 import static com.oracle.tools.packager.mac.MacAppBundler.*;
  68 /**
  69  *
  70  */
  71 public class MacAppImageBuilder extends AbstractAppImageBuilder {
  72 
  73     private static final ResourceBundle I18N =
  74             ResourceBundle.getBundle(MacAppImageBuilder.class.getName());
  75 
  76     private static final String EXECUTABLE_NAME = "JavaAppLauncher";
  77     private static final String LIBRARY_NAME = "libpackager.dylib";
  78     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  79     private static final String OS_TYPE_CODE = "APPL";
  80     private static final String TEMPLATE_INFO_PLIST_LITE = "Info-lite.plist.template";
  81     private static final String TEMPLATE_RUNTIME_INFO_PLIST = "Runtime-Info.plist.template";
  82 
  83     private final Path root;
  84     private final Path contentsDir;
  85     private final Path javaDir;
  86     private final Path resourcesDir;
  87     private final Path macOSDir;
  88     private final Path runtimeDir;
  89     private final Path runtimeRoot;
  90     private final Path mdir;
  91 
  92     private final Map<String, ? super Object> params;
  93 
  94     private static Map<String, String> getMacCategories() {
  95         Map<String, String> map = new HashMap<>();
  96         map.put("Business", "public.app-category.business");
  97         map.put("Developer Tools", "public.app-category.developer-tools");
  98         map.put("Education", "public.app-category.education");
  99         map.put("Entertainment", "public.app-category.entertainment");
 100         map.put("Finance", "public.app-category.finance");
 101         map.put("Games", "public.app-category.games");
 102         map.put("Graphics & Design", "public.app-category.graphics-design");
 103         map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
 104         map.put("Lifestyle", "public.app-category.lifestyle");
 105         map.put("Medical", "public.app-category.medical");
 106         map.put("Music", "public.app-category.music");
 107         map.put("News", "public.app-category.news");
 108         map.put("Photography", "public.app-category.photography");
 109         map.put("Productivity", "public.app-category.productivity");
 110         map.put("Reference", "public.app-category.reference");
 111         map.put("Social Networking", "public.app-category.social-networking");
 112         map.put("Sports", "public.app-category.sports");
 113         map.put("Travel", "public.app-category.travel");
 114         map.put("Utilities", "public.app-category.utilities");
 115         map.put("Video", "public.app-category.video");
 116         map.put("Weather", "public.app-category.weather");
 117 
 118         map.put("Action Games", "public.app-category.action-games");
 119         map.put("Adventure Games", "public.app-category.adventure-games");
 120         map.put("Arcade Games", "public.app-category.arcade-games");
 121         map.put("Board Games", "public.app-category.board-games");
 122         map.put("Card Games", "public.app-category.card-games");
 123         map.put("Casino Games", "public.app-category.casino-games");
 124         map.put("Dice Games", "public.app-category.dice-games");
 125         map.put("Educational Games", "public.app-category.educational-games");
 126         map.put("Family Games", "public.app-category.family-games");
 127         map.put("Kids Games", "public.app-category.kids-games");
 128         map.put("Music Games", "public.app-category.music-games");
 129         map.put("Puzzle Games", "public.app-category.puzzle-games");
 130         map.put("Racing Games", "public.app-category.racing-games");
 131         map.put("Role Playing Games", "public.app-category.role-playing-games");
 132         map.put("Simulation Games", "public.app-category.simulation-games");
 133         map.put("Sports Games", "public.app-category.sports-games");
 134         map.put("Strategy Games", "public.app-category.strategy-games");
 135         map.put("Trivia Games", "public.app-category.trivia-games");
 136         map.put("Word Games", "public.app-category.word-games");
 137 
 138         return map;
 139     }
 140 
 141     public static final BundlerParamInfo<Boolean> MAC_CONFIGURE_LAUNCHER_IN_PLIST =
 142             new StandardBundlerParam<>(
 143                     I18N.getString("param.configure-launcher-in-plist"),
 144                     I18N.getString("param.configure-launcher-in-plist.description"),
 145                     "mac.configure-launcher-in-plist",
 146                     Boolean.class,
 147                     params -> Boolean.FALSE,
 148                     (s, p) -> Boolean.valueOf(s));
 149 
 150     public static final BundlerParamInfo<String> MAC_CATEGORY =
 151             new StandardBundlerParam<>(
 152                     I18N.getString("param.category-name"),
 153                     I18N.getString("param.category-name.description"),
 154                     "mac.category",
 155                     String.class,
 156                     params -> "public.app-category.developer-tools", // this category is almost certianly wrong, encouraging the user to set a value
 157                     (s, p) -> s
 158             );
 159 
 160     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
 161             new StandardBundlerParam<>(
 162                     I18N.getString("param.cfbundle-name.name"),
 163                     I18N.getString("param.cfbundle-name.description"),
 164                     "mac.CFBundleName",
 165                     String.class,
 166                     params -> null,
 167                     (s, p) -> s);
 168 
 169     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER =
 170             new StandardBundlerParam<>(
 171                     I18N.getString("param.cfbundle-identifier.name"),
 172                     I18N.getString("param.cfbundle-identifier.description"),
 173                     "mac.CFBundleIdentifier",
 174                     String.class,
 175                     params -> "com.example.bad", //FIXME
 176                     (s, p) -> s);
 177 
 178     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION =
 179             new StandardBundlerParam<>(
 180                     I18N.getString("param.cfbundle-version.name"),
 181                     I18N.getString("param.cfbundle-version.description"),
 182                     "mac.CFBundleVersion",
 183                     String.class,
 184                     p -> {
 185                         String s = VERSION.fetchFrom(p);
 186                         if (validCFBundleVersion(s)) {
 187                             return s;
 188                         } else {
 189                             return "100";
 190                         }
 191                     },
 192                     (s, p) -> s);
 193 
 194     public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
 195             I18N.getString("param.config-root.name"),
 196             I18N.getString("param.config-root.description"),
 197             "configRoot",
 198             File.class,
 199             params -> {
 200                 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
 201                 configRoot.mkdirs();
 202                 return configRoot;
 203             },
 204             (s, p) -> new File(s));
 205 
 206     public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
 207             I18N.getString("param.default-icon-icns"),
 208             I18N.getString("param.default-icon-icns.description"),
 209             ".mac.default.icns",
 210             String.class,
 211             params -> TEMPLATE_BUNDLE_ICON,
 212             (s, p) -> s);
 213 
 214 //    public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>(
 215 //            I18N.getString("param.signing-key-developer-id-app.name"),
 216 //            I18N.getString("param.signing-key-developer-id-app.description"),
 217 //            "mac.signing-key-developer-id-app",
 218 //            String.class,
 219 //            params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)),
 220 //            (s, p) -> s);
 221 
 222     //    public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>(
 223 //            I18N.getString("param.bundle-id-signing-prefix.name"),
 224 //            I18N.getString("param.bundle-id-signing-prefix.description"),
 225 //            "mac.bundle-id-signing-prefix",
 226 //            String.class,
 227 //            params -> IDENTIFIER.fetchFrom(params) + ".",
 228 //            (s, p) -> s);
 229 //
 230     public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>(
 231             I18N.getString("param.icon-icns.name"),
 232             I18N.getString("param.icon-icns.description"),
 233             "icon.icns",
 234             File.class,
 235             params -> {
 236                 File f = ICON.fetchFrom(params);
 237                 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
 238                     Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f));
 239                     return null;
 240                 }
 241                 return f;
 242             },
 243             (s, p) -> new File(s));
 244 
 245     private static String extractAppName() {
 246         return "";
 247     }
 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         //@SuppressWarnings("unchecked")
 255         //String img = (String) config.get("jimage.name"); // FIXME constant
 256 
 257         this.params = config;
 258 
 259         this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
 260 
 261         this.contentsDir = root.resolve("Contents");
 262         this.javaDir = contentsDir.resolve("Java");
 263         this.resourcesDir = contentsDir.resolve("Resources");
 264         this.macOSDir = contentsDir.resolve("MacOS");
 265         this.runtimeDir = contentsDir.resolve("PlugIns/Java.runtime");
 266         this.runtimeRoot = runtimeDir.resolve("Contents/Home");
 267         this.mdir = runtimeRoot.resolve("lib");
 268         Files.createDirectories(javaDir);
 269         Files.createDirectories(resourcesDir);
 270         Files.createDirectories(macOSDir);
 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     protected InputStream getResourceAsStream(String name) {
 356         return MacResources.class.getResourceAsStream(name);
 357     }
 358 
 359 
 360     @Override
 361     protected void prepareApplicationFiles(Pool files, Set<String> modules) throws IOException {
 362         File f;
 363 
 364         // Generate PkgInfo
 365         File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo");
 366         pkgInfoFile.createNewFile();
 367         writePkgInfo(pkgInfoFile);
 368 
 369 
 370         // Copy executable to MacOS folder
 371         Path executable = macOSDir.resolve(getLauncherName(params));
 372         writeEntry(MacResources.class.getResourceAsStream(EXECUTABLE_NAME), executable);
 373         executable.toFile().setExecutable(true, false);
 374 
 375         // Copy library to the MacOS folder
 376         writeEntry(
 377                 MacResources.class.getResourceAsStream(LIBRARY_NAME),
 378                 macOSDir.resolve(LIBRARY_NAME)
 379         );
 380 
 381         // generate launcher config
 382 
 383         writeCfgFile(params, new File(root.toFile(), getLauncherCfgName(params)), "$APPDIR/PlugIns/Java.runtime");
 384 
 385         // Copy class path entries to Java folder
 386         copyClassPathEntries(javaDir);
 387 
 388         //TODO: Need to support adding native libraries.
 389         // Copy library path entries to MacOS folder
 390         //copyLibraryPathEntries(macOSDirectory);
 391 
 392         /*********** Take care of "config" files *******/
 393         // Copy icon to Resources folder
 394         File icon = ICON_ICNS.fetchFrom(params);
 395         InputStream in = locateResource("package/macosx/" + APP_NAME.fetchFrom(params) + ".icns",
 396                 "icon",
 397                 DEFAULT_ICNS_ICON.fetchFrom(params),
 398                 icon,
 399                 VERBOSE.fetchFrom(params),
 400                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 401         Files.copy(in, resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns"));
 402 
 403         // copy file association icons
 404         for (Map<String, ? super Object> fa : FILE_ASSOCIATIONS.fetchFrom(params)) {
 405             f = FA_ICON.fetchFrom(fa);
 406             if (f != null && f.exists()) {
 407                 try (InputStream in2 = new FileInputStream(f)) {
 408                     Files.copy(in2, resourcesDir.resolve(f.getName()));
 409                 }
 410 
 411             }
 412         }
 413 
 414         // Generate Info.plist
 415         writeInfoPlist(contentsDir.resolve("Info.plist").toFile());
 416 
 417         // generate java runtime info.plist
 418         writeRuntimeInfoPlist(runtimeDir.resolve("Contents/Info.plist").toFile());
 419 
 420         // copy library
 421         Path runtimeMacOSDir = Files.createDirectories(runtimeDir.resolve("Contents/MacOS"));
 422         Files.copy(runtimeRoot.resolve("lib/jli/libjli.dylib"), runtimeMacOSDir.resolve("libjli.dylib"));
 423 
 424         // maybe sign
 425         if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) {
 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         }
 431     }
 432 
 433 
 434     private String getLauncherName(Map<String, ? super Object> params) {
 435         if (APP_NAME.fetchFrom(params) != null) {
 436             return APP_NAME.fetchFrom(params);
 437         } else {
 438             return MAIN_CLASS.fetchFrom(params);
 439         }
 440     }
 441 
 442     public static String getLauncherCfgName(Map<String, ? super Object> p) {
 443         return "Contents/Java/" + APP_NAME.fetchFrom(p) + ".cfg";
 444     }
 445 
 446     private void copyClassPathEntries(Path javaDirectory) throws IOException {
 447         List<RelativeFileSet> resourcesList = APP_RESOURCES_LIST.fetchFrom(params);
 448         if (resourcesList == null) {
 449             throw new RuntimeException(I18N.getString("message.null-classpath"));
 450         }
 451 
 452         for (RelativeFileSet classPath : resourcesList) {
 453             File srcdir = classPath.getBaseDirectory();
 454             for (String fname : classPath.getIncludedFiles()) {
 455                 // use new File since fname can have file separators
 456                 Files.copy(new File(srcdir, fname).toPath(), new File(javaDirectory.toFile(), fname).toPath());
 457             }
 458         }
 459     }
 460 
 461     private String getBundleName(Map<String, ? super Object> params) {
 462         //TODO: Check to see what rules/limits are in place for CFBundleName
 463         if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
 464             String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
 465             if (bn.length() > 16) {
 466                 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn));
 467             }
 468             return MAC_CF_BUNDLE_NAME.fetchFrom(params);
 469         } else if (APP_NAME.fetchFrom(params) != null) {
 470             return APP_NAME.fetchFrom(params);
 471         } else {
 472             String nm = MAIN_CLASS.fetchFrom(params);
 473             if (nm.length() > 16) {
 474                 nm = nm.substring(0, 16);
 475             }
 476             return nm;
 477         }
 478     }
 479 
 480     private void writeRuntimeInfoPlist(File file) throws IOException {
 481         //FIXME //TODO these values are bogus.
 482         Map<String, String> data = new HashMap<>();
 483         data.put("CF_BUNDLE_INFO", "bundle info");
 484         data.put("CF_BUNDLE_IDENTIFIER", "com.oracle.java.8u60.jdk");
 485         data.put("CF_BUNDLE_NAME", "Java SE 9");
 486         data.put("CF_BUNDLE_VERSION", "1.8.0_60");
 487 
 488         Writer w = new BufferedWriter(new FileWriter(file));
 489         w.write(preprocessTextResource(
 490                 "package/macosx/Runtime-Info.plist",
 491                 I18N.getString("resource.runtime-info-plist"),
 492                 TEMPLATE_RUNTIME_INFO_PLIST,
 493                 data,
 494                 VERBOSE.fetchFrom(params),
 495                 DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
 496         w.close();
 497     }
 498 
 499     private void writeInfoPlist(File file) throws IOException {
 500         Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath()));
 501 
 502         //prepare config for exe
 503         //Note: do not need CFBundleDisplayName if we do not support localization
 504         Map<String, String> data = new HashMap<>();
 505         data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns");
 506         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 507                 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
 508         data.put("DEPLOY_BUNDLE_NAME",
 509                 getBundleName(params));
 510         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 511                 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
 512         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
 513         data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime");
 514         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 515                 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
 516         data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
 517                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
 518         data.put("DEPLOY_BUNDLE_CATEGORY",
 519                 //TODO parameters should provide set of values for IDEs
 520                 MAC_CATEGORY.fetchFrom(params));
 521 
 522         data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
 523 
 524         data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
 525 
 526         StringBuilder sb = new StringBuilder();
 527         List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
 528 
 529         String newline = ""; //So we don't add unneccessary extra line after last append
 530         for (String o : jvmOptions) {
 531             sb.append(newline).append("    <string>").append(o).append("</string>");
 532             newline = "\n";
 533         }
 534 
 535         Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
 536         for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
 537             sb.append(newline)
 538                     .append("    <string>-D")
 539                     .append(entry.getKey())
 540                     .append("=")
 541                     .append(entry.getValue())
 542                     .append("</string>");
 543             newline = "\n";
 544         }
 545 
 546         String preloader = PRELOADER_CLASS.fetchFrom(params);
 547         if (preloader != null) {
 548             sb.append(newline)
 549                     .append("    <string>-Djavafx.preloader=")
 550                     .append(preloader)
 551                     .append("</string>");
 552         }
 553 
 554         data.put("DEPLOY_JVM_OPTIONS", sb.toString());
 555 
 556         sb = new StringBuilder();
 557         List<String> args = ARGUMENTS.fetchFrom(params);
 558         newline = ""; //So we don't add unneccessary extra line after last append
 559         for (String o : args) {
 560             sb.append(newline).append("    <string>").append(o).append("</string>");
 561             newline = "\n";
 562         }
 563         data.put("DEPLOY_ARGUMENTS", sb.toString());
 564 
 565         newline = "";
 566         sb = new StringBuilder();
 567         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
 568         for (Map.Entry<String, String> arg : overridableJVMOptions.entrySet()) {
 569             sb.append(newline)
 570                     .append("      <key>").append(arg.getKey()).append("</key>\n")
 571                     .append("      <string>").append(arg.getValue()).append("</string>");
 572             newline = "\n";
 573         }
 574         data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
 575 
 576 
 577         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 578 
 579         StringBuilder macroedPath = new StringBuilder();
 580         for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) {
 581             macroedPath.append(s);
 582             macroedPath.append(":");
 583         }
 584         macroedPath.deleteCharAt(macroedPath.length() - 1);
 585 
 586         data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString());
 587 
 588         //TODO: Add remainder of the classpath
 589 
 590         StringBuilder bundleDocumentTypes = new StringBuilder();
 591         StringBuilder exportedTypes = new StringBuilder();
 592         for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
 593 
 594             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 595 
 596             if (extensions == null) {
 597                 Log.info(I18N.getString("message.creating-association-with-null-extension"));
 598             }
 599 
 600             List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 601             String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty())
 602                     ? "mime"
 603                     : extensions.get(0));
 604             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 605             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS
 606 
 607             bundleDocumentTypes.append("    <dict>\n")
 608                     .append("      <key>LSItemContentTypes</key>\n")
 609                     .append("      <array>\n")
 610                     .append("        <string>")
 611                     .append(itemContentType)
 612                     .append("</string>\n")
 613                     .append("      </array>\n")
 614                     .append("\n")
 615                     .append("      <key>CFBundleTypeName</key>\n")
 616                     .append("      <string>")
 617                     .append(description)
 618                     .append("</string>\n")
 619                     .append("\n")
 620                     .append("      <key>LSHandlerRank</key>\n")
 621                     .append("      <string>Owner</string>\n") //TODO make a bundler arg
 622                     .append("\n")
 623                     .append("      <key>CFBundleTypeRole</key>\n")
 624                     .append("      <string>Editor</string>\n") // TODO make a bundler arg
 625                     .append("\n")
 626                     .append("      <key>LSIsAppleDefaultForType</key>\n")
 627                     .append("      <true/>\n") // TODO make a bundler arg
 628                     .append("\n");
 629 
 630             if (icon != null && icon.exists()) {
 631                 //?
 632                 bundleDocumentTypes.append("      <key>CFBundleTypeIconFile</key>\n")
 633                         .append("      <string>")
 634                         .append(icon.getName())
 635                         .append("</string>\n");
 636             }
 637             bundleDocumentTypes.append("    </dict>\n");
 638 
 639             exportedTypes.append("    <dict>\n")
 640                     .append("      <key>UTTypeIdentifier</key>\n")
 641                     .append("      <string>")
 642                     .append(itemContentType)
 643                     .append("</string>\n")
 644                     .append("\n")
 645                     .append("      <key>UTTypeDescription</key>\n")
 646                     .append("      <string>")
 647                     .append(description)
 648                     .append("</string>\n")
 649                     .append("      <key>UTTypeConformsTo</key>\n")
 650                     .append("      <array>\n")
 651                     .append("          <string>public.data</string>\n") //TODO expose this?
 652                     .append("      </array>\n")
 653                     .append("\n");
 654 
 655             if (icon != null && icon.exists()) {
 656                 exportedTypes.append("      <key>UTTypeIconFile</key>\n")
 657                         .append("      <string>")
 658                         .append(icon.getName())
 659                         .append("</string>\n")
 660                         .append("\n");
 661             }
 662 
 663             exportedTypes.append("\n")
 664                     .append("      <key>UTTypeTagSpecification</key>\n")
 665                     .append("      <dict>\n")
 666                             //TODO expose via param? .append("        <key>com.apple.ostype</key>\n");
 667                             //TODO expose via param? .append("        <string>ABCD</string>\n")
 668                     .append("\n");
 669 
 670             if (extensions != null && !extensions.isEmpty()) {
 671                 exportedTypes.append("        <key>public.filename-extension</key>\n")
 672                         .append("        <array>\n");
 673 
 674                 for (String ext : extensions) {
 675                     exportedTypes.append("          <string>")
 676                             .append(ext)
 677                             .append("</string>\n");
 678                 }
 679                 exportedTypes.append("        </array>\n");
 680             }
 681             if (mimeTypes != null && !mimeTypes.isEmpty()) {
 682                 exportedTypes.append("        <key>public.mime-type</key>\n")
 683                         .append("        <array>\n");
 684 
 685                 for (String mime : mimeTypes) {
 686                     exportedTypes.append("          <string>")
 687                             .append(mime)
 688                             .append("</string>\n");
 689                 }
 690                 exportedTypes.append("        </array>\n");
 691             }
 692             exportedTypes.append("      </dict>\n")
 693                     .append("    </dict>\n");
 694         }
 695         String associationData;
 696         if (bundleDocumentTypes.length() > 0) {
 697             associationData = "\n  <key>CFBundleDocumentTypes</key>\n  <array>\n"
 698                     + bundleDocumentTypes.toString()
 699                     + "  </array>\n\n  <key>UTExportedTypeDeclarations</key>\n  <array>\n"
 700                     + exportedTypes.toString()
 701                     + "  </array>\n";
 702         } else {
 703             associationData = "";
 704         }
 705         data.put("DEPLOY_FILE_ASSOCIATIONS", associationData);
 706 
 707 
 708         Writer w = new BufferedWriter(new FileWriter(file));
 709         w.write(preprocessTextResource(
 710                 //MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(),
 711                 "package/macosx/Info.plist",
 712                 I18N.getString("resource.app-info-plist"),
 713                 TEMPLATE_INFO_PLIST_LITE,
 714                 data, VERBOSE.fetchFrom(params),
 715                 DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
 716         w.close();
 717 
 718     }
 719 
 720     private void writePkgInfo(File file) throws IOException {
 721         //hardcoded as it does not seem we need to change it ever
 722         String signature = "????";
 723 
 724         try (Writer out = new BufferedWriter(new FileWriter(file))) {
 725             out.write(OS_TYPE_CODE + signature);
 726             out.flush();
 727         }
 728     }
 729 
 730     public static void signAppBundle(Map<String, ? super Object> params, Path appLocation, String signingIdentity, String identifierPrefix, String entitlementsFile, String inheritedEntitlements) throws IOException {
 731         AtomicReference<IOException> toThrow = new AtomicReference<>();
 732         String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params);
 733         String keyChain = SIGNING_KEYCHAIN.fetchFrom(params);
 734 
 735         // sign all dylibs and jars
 736         Files.walk(appLocation)
 737                 // while we are searching let's fix permissions
 738                 .peek(path -> {
 739                     try {
 740                         Set<PosixFilePermission> pfp = Files.getPosixFilePermissions(path);
 741                         if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) {
 742                             pfp = EnumSet.copyOf(pfp);
 743                             pfp.add(PosixFilePermission.OWNER_WRITE);
 744                             Files.setPosixFilePermissions(path, pfp);
 745                         }
 746                     } catch (IOException e) {
 747                         Log.debug(e);
 748                     }
 749                 })
 750                 .filter(p -> Files.isRegularFile(p) &&
 751                                 !(p.toString().contains("/Contents/MacOS/libjli.dylib")
 752                                         || p.toString().contains("/Contents/MacOS/JavaAppletPlugin")
 753                                         || p.toString().endsWith(appExecutable))
 754                 ).forEach(p -> {
 755             //noinspection ThrowableResultOfMethodCallIgnored
 756             if (toThrow.get() != null) return;
 757 
 758             List<String> args = new ArrayList<>();
 759             args.addAll(Arrays.asList("codesign",
 760                     "-s", signingIdentity, // sign with this key
 761                     "--prefix", identifierPrefix, // use the identifier as a prefix
 762                     "-vvvv"));
 763             if (entitlementsFile != null &&
 764                     (p.toString().endsWith(".jar")
 765                             || p.toString().endsWith(".dylib"))) {
 766                 args.add("--entitlements");
 767                 args.add(entitlementsFile); // entitlements
 768             } else if (inheritedEntitlements != null && Files.isExecutable(p)) {
 769                 args.add("--entitlements");
 770                 args.add(inheritedEntitlements); // inherited entitlements for executable processes
 771             }
 772             if (keyChain != null && !keyChain.isEmpty()) {
 773                 args.add("--keychain");
 774                 args.add(keyChain);
 775             }
 776             args.add(p.toString());
 777 
 778             try {
 779                 Set<PosixFilePermission> oldPermissions = Files.getPosixFilePermissions(p);
 780                 File f = p.toFile();
 781                 f.setWritable(true, true);
 782 
 783                 ProcessBuilder pb = new ProcessBuilder(args);
 784                 IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 785 
 786                 Files.setPosixFilePermissions(p, oldPermissions);
 787             } catch (IOException ioe) {
 788                 toThrow.set(ioe);
 789             }
 790         });
 791 
 792         IOException ioe = toThrow.get();
 793         if (ioe != null) {
 794             throw ioe;
 795         }
 796 
 797         // sign all plugins and frameworks
 798         Consumer<? super Path> signIdentifiedByPList = path -> {
 799             //noinspection ThrowableResultOfMethodCallIgnored
 800             if (toThrow.get() != null) return;
 801 
 802             try {
 803                 List<String> args = new ArrayList<>();
 804                 args.addAll(Arrays.asList("codesign",
 805                         "-s", signingIdentity, // sign with this key
 806                         "--prefix", identifierPrefix, // use the identifier as a prefix
 807                         "-vvvv"));
 808                 if (keyChain != null && !keyChain.isEmpty()) {
 809                     args.add("--keychain");
 810                     args.add(keyChain);
 811                 }
 812                 args.add(path.toString());
 813                 ProcessBuilder pb = new ProcessBuilder(args);
 814                 IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 815 
 816                 args = new ArrayList<>();
 817                 args.addAll(Arrays.asList("codesign",
 818                         "-s", signingIdentity, // sign with this key
 819                         "--prefix", identifierPrefix, // use the identifier as a prefix
 820                         "-vvvv"));
 821                 if (keyChain != null && !keyChain.isEmpty()) {
 822                     args.add("--keychain");
 823                     args.add(keyChain);
 824                 }
 825                 args.add(path.toString() + "/Contents/_CodeSignature/CodeResources");
 826                 pb = new ProcessBuilder(args);
 827                 IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 828             } catch (IOException e) {
 829                 toThrow.set(e);
 830             }
 831         };
 832 
 833         Path pluginsPath = appLocation.resolve("Contents/PlugIns");
 834         if (Files.isDirectory(pluginsPath)) {
 835             Files.list(pluginsPath)
 836                     .forEach(signIdentifiedByPList);
 837 
 838             ioe = toThrow.get();
 839             if (ioe != null) {
 840                 throw ioe;
 841             }
 842         }
 843         Path frameworkPath = appLocation.resolve("Contents/Frameworks");
 844         if (Files.isDirectory(frameworkPath)) {
 845             Files.list(frameworkPath)
 846                     .forEach(signIdentifiedByPList);
 847 
 848             ioe = toThrow.get();
 849             if (ioe != null) {
 850                 throw ioe;
 851             }
 852         }
 853 
 854         // sign the app itself
 855         List<String> args = new ArrayList<>();
 856         args.addAll(Arrays.asList("codesign",
 857                 "-s", signingIdentity, // sign with this key
 858                 "-vvvv")); // super verbose output
 859         if (entitlementsFile != null) {
 860             args.add("--entitlements");
 861             args.add(entitlementsFile); // entitlements
 862         }
 863         if (keyChain != null && !keyChain.isEmpty()) {
 864             args.add("--keychain");
 865             args.add(keyChain);
 866         }
 867         args.add(appLocation.toString());
 868 
 869         ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[args.size()]));
 870         IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 871     }
 872 
 873 }