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.RelativeFileSet;
  32 import com.oracle.tools.packager.StandardBundlerParam;
  33 import com.oracle.tools.packager.mac.MacResources;
  34 
  35 import jdk.packager.internal.legacy.builders.AbstractAppImageBuilder;
  36 import jdk.packager.internal.legacy.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         this.params = config;
 252         this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
 253         this.contentsDir = root.resolve("Contents");
 254         this.javaDir = contentsDir.resolve("Java");
 255         this.resourcesDir = contentsDir.resolve("Resources");
 256         this.macOSDir = contentsDir.resolve("MacOS");
 257         this.runtimeDir = contentsDir.resolve("PlugIns/Java.runtime");
 258         this.runtimeRoot = runtimeDir.resolve("Contents/Home");
 259         this.mdir = runtimeRoot.resolve("lib");
 260         Files.createDirectories(javaDir);
 261         Files.createDirectories(resourcesDir);
 262         Files.createDirectories(macOSDir);
 263         Files.createDirectories(runtimeDir);
 264     }
 265 
 266     private static String extractAppName() {
 267         return "";
 268     }
 269 
 270     private void writeEntry(InputStream in, Path dstFile) throws IOException {
 271         Files.createDirectories(dstFile.getParent());
 272         Files.copy(in, dstFile);
 273     }
 274 
 275     /**
 276      * chmod ugo+x file
 277      */
 278     private void setExecutable(Path file) {
 279         try {
 280             Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file);
 281             perms.add(PosixFilePermission.OWNER_EXECUTE);
 282             perms.add(PosixFilePermission.GROUP_EXECUTE);
 283             perms.add(PosixFilePermission.OTHERS_EXECUTE);
 284             Files.setPosixFilePermissions(file, perms);
 285         } catch (IOException ioe) {
 286             throw new UncheckedIOException(ioe);
 287         }
 288     }
 289 
 290     private static void createUtf8File(File file, String content) throws IOException {
 291         try (OutputStream fout = new FileOutputStream(file);
 292              Writer output = new OutputStreamWriter(fout, "UTF-8")) {
 293             output.write(content);
 294         }
 295     }
 296 
 297     @Override
 298     protected String getCacheLocation(Map<String, ? super Object> params) {
 299         return "$CACHEDIR/";
 300     }
 301 
 302     public static boolean validCFBundleVersion(String v) {
 303         // CFBundleVersion (String - iOS, OS X) specifies the build version
 304         // number of the bundle, which identifies an iteration (released or
 305         // unreleased) of the bundle. The build version number should be a
 306         // string comprised of three non-negative, period-separated integers
 307         // with the first integer being greater than zero. The string should
 308         // only contain numeric (0-9) and period (.) characters. Leading zeros
 309         // are truncated from each integer and will be ignored (that is,
 310         // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
 311 
 312         if (v == null) {
 313             return false;
 314         }
 315 
 316         String p[] = v.split("\\.");
 317         if (p.length > 3 || p.length < 1) {
 318             Log.verbose(I18N.getString("message.version-string-too-many-components"));
 319             return false;
 320         }
 321 
 322         try {
 323             BigInteger n = new BigInteger(p[0]);
 324             if (BigInteger.ONE.compareTo(n) > 0) {
 325                 Log.verbose(I18N.getString("message.version-string-first-number-not-zero"));
 326                 return false;
 327             }
 328             if (p.length > 1) {
 329                 n = new BigInteger(p[1]);
 330                 if (BigInteger.ZERO.compareTo(n) > 0) {
 331                     Log.verbose(I18N.getString("message.version-string-no-negative-numbers"));
 332                     return false;
 333                 }
 334             }
 335             if (p.length > 2) {
 336                 n = new BigInteger(p[2]);
 337                 if (BigInteger.ZERO.compareTo(n) > 0) {
 338                     Log.verbose(I18N.getString("message.version-string-no-negative-numbers"));
 339                     return false;
 340                 }
 341             }
 342         } catch (NumberFormatException ne) {
 343             Log.verbose(I18N.getString("message.version-string-numbers-only"));
 344             Log.verbose(ne);
 345             return false;
 346         }
 347 
 348         return true;
 349     }
 350 
 351     @Override
 352     public InputStream getResourceAsStream(String name) {
 353         return MacResources.class.getResourceAsStream(name);
 354     }
 355 
 356     @Override
 357     public void prepareApplicationFiles() throws IOException {
 358         File f;
 359 
 360         // Generate PkgInfo
 361         File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo");
 362         pkgInfoFile.createNewFile();
 363         writePkgInfo(pkgInfoFile);
 364 
 365 
 366         // Copy executable to MacOS folder
 367         Path executable = macOSDir.resolve(getLauncherName(params));
 368         writeEntry(MacResources.class.getResourceAsStream(EXECUTABLE_NAME), executable);
 369         executable.toFile().setExecutable(true, false);
 370 
 371         // Copy library to the MacOS folder
 372         writeEntry(
 373                 MacResources.class.getResourceAsStream(LIBRARY_NAME),
 374                 macOSDir.resolve(LIBRARY_NAME)
 375         );
 376 
 377         // generate launcher config
 378 
 379         writeCfgFile(params, new File(root.toFile(), getLauncherCfgName(params)), "$APPDIR/PlugIns/Java.runtime");
 380 
 381         // Copy class path entries to Java folder
 382         copyClassPathEntries(javaDir);
 383 
 384         /*********** Take care of "config" files *******/
 385         // Copy icon to Resources folder
 386         File icon = ICON_ICNS.fetchFrom(params);
 387         InputStream in = locateResource("package/macosx/" + APP_NAME.fetchFrom(params) + ".icns",
 388                 "icon",
 389                 DEFAULT_ICNS_ICON.fetchFrom(params),
 390                 icon,
 391                 VERBOSE.fetchFrom(params),
 392                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 393         Files.copy(in, resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns"));
 394 
 395         // copy file association icons
 396         for (Map<String, ? super Object> fa : FILE_ASSOCIATIONS.fetchFrom(params)) {
 397             f = FA_ICON.fetchFrom(fa);
 398             if (f != null && f.exists()) {
 399                 try (InputStream in2 = new FileInputStream(f)) {
 400                     Files.copy(in2, resourcesDir.resolve(f.getName()));
 401                 }
 402 
 403             }
 404         }
 405 
 406         // Generate Info.plist
 407         writeInfoPlist(contentsDir.resolve("Info.plist").toFile());
 408 
 409         // generate java runtime info.plist
 410         writeRuntimeInfoPlist(runtimeDir.resolve("Contents/Info.plist").toFile());
 411 
 412         // copy library
 413         Path runtimeMacOSDir = Files.createDirectories(runtimeDir.resolve("Contents/MacOS"));
 414         Files.copy(runtimeRoot.resolve("lib/jli/libjli.dylib"), runtimeMacOSDir.resolve("libjli.dylib"));
 415 
 416         // maybe sign
 417         if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) {
 418             String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params);
 419             if (signingIdentity != null) {
 420                 signAppBundle(params, root, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null);
 421             }
 422         }
 423     }
 424 
 425 
 426     private String getLauncherName(Map<String, ? super Object> params) {
 427         if (APP_NAME.fetchFrom(params) != null) {
 428             return APP_NAME.fetchFrom(params);
 429         } else {
 430             return MAIN_CLASS.fetchFrom(params);
 431         }
 432     }
 433 
 434     public static String getLauncherCfgName(Map<String, ? super Object> p) {
 435         return "Contents/Java/" + APP_NAME.fetchFrom(p) + ".cfg";
 436     }
 437 
 438     private void copyClassPathEntries(Path javaDirectory) throws IOException {
 439         List<RelativeFileSet> resourcesList = APP_RESOURCES_LIST.fetchFrom(params);
 440         if (resourcesList == null) {
 441             throw new RuntimeException(I18N.getString("message.null-classpath"));
 442         }
 443 
 444         for (RelativeFileSet classPath : resourcesList) {
 445             File srcdir = classPath.getBaseDirectory();
 446             for (String fname : classPath.getIncludedFiles()) {
 447                 // use new File since fname can have file separators
 448                 Files.copy(new File(srcdir, fname).toPath(), new File(javaDirectory.toFile(), fname).toPath());
 449             }
 450         }
 451     }
 452 
 453     private String getBundleName(Map<String, ? super Object> params) {
 454         if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
 455             String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
 456             if (bn.length() > 16) {
 457                 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn));
 458             }
 459             return MAC_CF_BUNDLE_NAME.fetchFrom(params);
 460         } else if (APP_NAME.fetchFrom(params) != null) {
 461             return APP_NAME.fetchFrom(params);
 462         } else {
 463             String nm = MAIN_CLASS.fetchFrom(params);
 464             if (nm.length() > 16) {
 465                 nm = nm.substring(0, 16);
 466             }
 467             return nm;
 468         }
 469     }
 470 
 471     private void writeRuntimeInfoPlist(File file) throws IOException {
 472         Map<String, String> data = new HashMap<>();
 473         data.put("CF_BUNDLE_IDENTIFIER", "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
 474         data.put("CF_BUNDLE_NAME", "Java Runtime Image");
 475         data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params));
 476         data.put("CF_BUNDLE_SHORT_VERSION_STRING", VERSION.fetchFrom(params));
 477 
 478         Writer w = new BufferedWriter(new FileWriter(file));
 479         w.write(preprocessTextResource(
 480                 "package/macosx/Runtime-Info.plist",
 481                 I18N.getString("resource.runtime-info-plist"),
 482                 TEMPLATE_RUNTIME_INFO_PLIST,
 483                 data,
 484                 VERBOSE.fetchFrom(params),
 485                 DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
 486         w.close();
 487     }
 488 
 489     private void writeInfoPlist(File file) throws IOException {
 490         Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath()));
 491 
 492         //prepare config for exe
 493         //Note: do not need CFBundleDisplayName if we do not support localization
 494         Map<String, String> data = new HashMap<>();
 495         data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns");
 496         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 497                 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
 498         data.put("DEPLOY_BUNDLE_NAME",
 499                 getBundleName(params));
 500         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 501                 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
 502         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
 503         data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime");
 504         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 505                 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
 506         data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
 507                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
 508         data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params));
 509 
 510         boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null;
 511         boolean hasMainModule = StandardBundlerParam.MODULE.fetchFrom(params) != null;
 512 
 513         if (hasMainJar) {
 514             data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
 515         }
 516         else if (hasMainModule) {
 517             data.put("DEPLOY_MODULE_NAME", StandardBundlerParam.MODULE.fetchFrom(params));
 518         }
 519 
 520         data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
 521 
 522         StringBuilder sb = new StringBuilder();
 523         List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
 524 
 525         String newline = ""; //So we don't add unneccessary extra line after last append
 526         for (String o : jvmOptions) {
 527             sb.append(newline).append("    <string>").append(o).append("</string>");
 528             newline = "\n";
 529         }
 530 
 531         Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
 532         for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
 533             sb.append(newline)
 534                     .append("    <string>-D")
 535                     .append(entry.getKey())
 536                     .append("=")
 537                     .append(entry.getValue())
 538                     .append("</string>");
 539             newline = "\n";
 540         }
 541 
 542         String preloader = PRELOADER_CLASS.fetchFrom(params);
 543         if (preloader != null) {
 544             sb.append(newline)
 545                     .append("    <string>-Djavafx.preloader=")
 546                     .append(preloader)
 547                     .append("</string>");
 548         }
 549 
 550         data.put("DEPLOY_JVM_OPTIONS", sb.toString());
 551 
 552         sb = new StringBuilder();
 553         List<String> args = ARGUMENTS.fetchFrom(params);
 554         newline = ""; //So we don't add unneccessary extra line after last append
 555         for (String o : args) {
 556             sb.append(newline).append("    <string>").append(o).append("</string>");
 557             newline = "\n";
 558         }
 559         data.put("DEPLOY_ARGUMENTS", sb.toString());
 560 
 561         newline = "";
 562         sb = new StringBuilder();
 563         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
 564         for (Map.Entry<String, String> arg : overridableJVMOptions.entrySet()) {
 565             sb.append(newline)
 566                     .append("      <key>").append(arg.getKey()).append("</key>\n")
 567                     .append("      <string>").append(arg.getValue()).append("</string>");
 568             newline = "\n";
 569         }
 570         data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
 571 
 572 
 573         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 574 
 575         StringBuilder macroedPath = new StringBuilder();
 576         for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) {
 577             macroedPath.append(s);
 578             macroedPath.append(":");
 579         }
 580         macroedPath.deleteCharAt(macroedPath.length() - 1);
 581 
 582         data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString());
 583 
 584         StringBuilder bundleDocumentTypes = new StringBuilder();
 585         StringBuilder exportedTypes = new StringBuilder();
 586         for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
 587 
 588             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 589 
 590             if (extensions == null) {
 591                 Log.info(I18N.getString("message.creating-association-with-null-extension"));
 592             }
 593 
 594             List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 595             String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty())
 596                     ? "mime"
 597                     : extensions.get(0));
 598             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 599             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS
 600 
 601             bundleDocumentTypes.append("    <dict>\n")
 602                     .append("      <key>LSItemContentTypes</key>\n")
 603                     .append("      <array>\n")
 604                     .append("        <string>")
 605                     .append(itemContentType)
 606                     .append("</string>\n")
 607                     .append("      </array>\n")
 608                     .append("\n")
 609                     .append("      <key>CFBundleTypeName</key>\n")
 610                     .append("      <string>")
 611                     .append(description)
 612                     .append("</string>\n")
 613                     .append("\n")
 614                     .append("      <key>LSHandlerRank</key>\n")
 615                     .append("      <string>Owner</string>\n") //TODO make a bundler arg
 616                     .append("\n")
 617                     .append("      <key>CFBundleTypeRole</key>\n")
 618                     .append("      <string>Editor</string>\n") // TODO make a bundler arg
 619                     .append("\n")
 620                     .append("      <key>LSIsAppleDefaultForType</key>\n")
 621                     .append("      <true/>\n") // TODO make a bundler arg
 622                     .append("\n");
 623 
 624             if (icon != null && icon.exists()) {
 625                 //?
 626                 bundleDocumentTypes.append("      <key>CFBundleTypeIconFile</key>\n")
 627                         .append("      <string>")
 628                         .append(icon.getName())
 629                         .append("</string>\n");
 630             }
 631             bundleDocumentTypes.append("    </dict>\n");
 632 
 633             exportedTypes.append("    <dict>\n")
 634                     .append("      <key>UTTypeIdentifier</key>\n")
 635                     .append("      <string>")
 636                     .append(itemContentType)
 637                     .append("</string>\n")
 638                     .append("\n")
 639                     .append("      <key>UTTypeDescription</key>\n")
 640                     .append("      <string>")
 641                     .append(description)
 642                     .append("</string>\n")
 643                     .append("      <key>UTTypeConformsTo</key>\n")
 644                     .append("      <array>\n")
 645                     .append("          <string>public.data</string>\n") //TODO expose this?
 646                     .append("      </array>\n")
 647                     .append("\n");
 648 
 649             if (icon != null && icon.exists()) {
 650                 exportedTypes.append("      <key>UTTypeIconFile</key>\n")
 651                         .append("      <string>")
 652                         .append(icon.getName())
 653                         .append("</string>\n")
 654                         .append("\n");
 655             }
 656 
 657             exportedTypes.append("\n")
 658                     .append("      <key>UTTypeTagSpecification</key>\n")
 659                     .append("      <dict>\n")
 660                             //TODO expose via param? .append("        <key>com.apple.ostype</key>\n");
 661                             //TODO expose via param? .append("        <string>ABCD</string>\n")
 662                     .append("\n");
 663 
 664             if (extensions != null && !extensions.isEmpty()) {
 665                 exportedTypes.append("        <key>public.filename-extension</key>\n")
 666                         .append("        <array>\n");
 667 
 668                 for (String ext : extensions) {
 669                     exportedTypes.append("          <string>")
 670                             .append(ext)
 671                             .append("</string>\n");
 672                 }
 673                 exportedTypes.append("        </array>\n");
 674             }
 675             if (mimeTypes != null && !mimeTypes.isEmpty()) {
 676                 exportedTypes.append("        <key>public.mime-type</key>\n")
 677                         .append("        <array>\n");
 678 
 679                 for (String mime : mimeTypes) {
 680                     exportedTypes.append("          <string>")
 681                             .append(mime)
 682                             .append("</string>\n");
 683                 }
 684                 exportedTypes.append("        </array>\n");
 685             }
 686             exportedTypes.append("      </dict>\n")
 687                     .append("    </dict>\n");
 688         }
 689         String associationData;
 690         if (bundleDocumentTypes.length() > 0) {
 691             associationData = "\n  <key>CFBundleDocumentTypes</key>\n  <array>\n"
 692                     + bundleDocumentTypes.toString()
 693                     + "  </array>\n\n  <key>UTExportedTypeDeclarations</key>\n  <array>\n"
 694                     + exportedTypes.toString()
 695                     + "  </array>\n";
 696         } else {
 697             associationData = "";
 698         }
 699         data.put("DEPLOY_FILE_ASSOCIATIONS", associationData);
 700 
 701 
 702         Writer w = new BufferedWriter(new FileWriter(file));
 703         w.write(preprocessTextResource(
 704                 //MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(),
 705                 "package/macosx/Info.plist",
 706                 I18N.getString("resource.app-info-plist"),
 707                 TEMPLATE_INFO_PLIST_LITE,
 708                 data, VERBOSE.fetchFrom(params),
 709                 DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
 710         w.close();
 711     }
 712 
 713     private void writePkgInfo(File file) throws IOException {
 714         //hardcoded as it does not seem we need to change it ever
 715         String signature = "????";
 716 
 717         try (Writer out = new BufferedWriter(new FileWriter(file))) {
 718             out.write(OS_TYPE_CODE + signature);
 719             out.flush();
 720         }
 721     }
 722 
 723     public static void signAppBundle(Map<String, ? super Object> params, Path appLocation, String signingIdentity, String identifierPrefix, String entitlementsFile, String inheritedEntitlements) throws IOException {
 724         AtomicReference<IOException> toThrow = new AtomicReference<>();
 725         String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params);
 726         String keyChain = SIGNING_KEYCHAIN.fetchFrom(params);
 727 
 728         // sign all dylibs and jars
 729         Files.walk(appLocation)
 730                 // fix permissions
 731                 .peek(path -> {
 732                     try {
 733                         Set<PosixFilePermission> pfp = Files.getPosixFilePermissions(path);
 734                         if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) {
 735                             pfp = EnumSet.copyOf(pfp);
 736                             pfp.add(PosixFilePermission.OWNER_WRITE);
 737                             Files.setPosixFilePermissions(path, pfp);
 738                         }
 739                     } catch (IOException e) {
 740                         Log.debug(e);
 741                     }
 742                 })
 743                 .filter(p -> Files.isRegularFile(p) &&
 744                                 !(p.toString().contains("/Contents/MacOS/libjli.dylib")
 745                                         || p.toString().contains("/Contents/MacOS/JavaAppletPlugin")
 746                                         || p.toString().endsWith(appExecutable))
 747                 ).forEach(p -> {
 748             //noinspection ThrowableResultOfMethodCallIgnored
 749             if (toThrow.get() != null) return;
 750 
 751             // If p is a symlink then skip the signing process.
 752             if (Files.isSymbolicLink(p)) {
 753                 if (VERBOSE.fetchFrom(params)) {
 754                     Log.verbose(MessageFormat.format(I18N.getString("message.ignoring.symlink"), p.toString()));
 755                 }
 756             }
 757             else {
 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 
 793         IOException ioe = toThrow.get();
 794         if (ioe != null) {
 795             throw ioe;
 796         }
 797 
 798         // sign all plugins and frameworks
 799         Consumer<? super Path> signIdentifiedByPList = path -> {
 800             //noinspection ThrowableResultOfMethodCallIgnored
 801             if (toThrow.get() != null) return;
 802 
 803             try {
 804                 List<String> args = new ArrayList<>();
 805                 args.addAll(Arrays.asList("codesign",
 806                         "-s", signingIdentity, // sign with this key
 807                         "--prefix", identifierPrefix, // use the identifier as a prefix
 808                         "-vvvv"));
 809                 if (keyChain != null && !keyChain.isEmpty()) {
 810                     args.add("--keychain");
 811                     args.add(keyChain);
 812                 }
 813                 args.add(path.toString());
 814                 ProcessBuilder pb = new ProcessBuilder(args);
 815                 IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 816 
 817                 args = new ArrayList<>();
 818                 args.addAll(Arrays.asList("codesign",
 819                         "-s", signingIdentity, // sign with this key
 820                         "--prefix", identifierPrefix, // use the identifier as a prefix
 821                         "-vvvv"));
 822                 if (keyChain != null && !keyChain.isEmpty()) {
 823                     args.add("--keychain");
 824                     args.add(keyChain);
 825                 }
 826                 args.add(path.toString() + "/Contents/_CodeSignature/CodeResources");
 827                 pb = new ProcessBuilder(args);
 828                 IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 829             } catch (IOException e) {
 830                 toThrow.set(e);
 831             }
 832         };
 833 
 834         Path pluginsPath = appLocation.resolve("Contents/PlugIns");
 835         if (Files.isDirectory(pluginsPath)) {
 836             Files.list(pluginsPath)
 837                     .forEach(signIdentifiedByPList);
 838 
 839             ioe = toThrow.get();
 840             if (ioe != null) {
 841                 throw ioe;
 842             }
 843         }
 844         Path frameworkPath = appLocation.resolve("Contents/Frameworks");
 845         if (Files.isDirectory(frameworkPath)) {
 846             Files.list(frameworkPath)
 847                     .forEach(signIdentifiedByPList);
 848 
 849             ioe = toThrow.get();
 850             if (ioe != null) {
 851                 throw ioe;
 852             }
 853         }
 854 
 855         // sign the app itself
 856         List<String> args = new ArrayList<>();
 857         args.addAll(Arrays.asList("codesign",
 858                 "-s", signingIdentity, // sign with this key
 859                 "-vvvv")); // super verbose output
 860         if (entitlementsFile != null) {
 861             args.add("--entitlements");
 862             args.add(entitlementsFile); // entitlements
 863         }
 864         if (keyChain != null && !keyChain.isEmpty()) {
 865             args.add("--keychain");
 866             args.add(keyChain);
 867         }
 868         args.add(appLocation.toString());
 869 
 870         ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[args.size()]));
 871         IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 872     }
 873 
 874 }