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