1 /*
   2  * Copyright (c) 2012, 2015, 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 com.oracle.tools.packager.mac;
  26 
  27 import com.oracle.tools.packager.AbstractImageBundler;
  28 import com.oracle.tools.packager.BundlerParamInfo;
  29 import com.oracle.tools.packager.EnumeratedBundlerParam;
  30 import com.oracle.tools.packager.JreUtils;
  31 import com.oracle.tools.packager.JreUtils.Rule;
  32 import com.oracle.tools.packager.StandardBundlerParam;
  33 import com.oracle.tools.packager.Log;
  34 import com.sun.javafx.tools.packager.bundlers.BundleParams;
  35 import com.oracle.tools.packager.ConfigException;
  36 import com.oracle.tools.packager.IOUtils;
  37 import com.oracle.tools.packager.RelativeFileSet;
  38 import com.oracle.tools.packager.UnsupportedPlatformException;
  39 
  40 import java.io.*;
  41 import java.math.BigInteger;
  42 import java.net.MalformedURLException;
  43 import java.net.URL;
  44 import java.text.MessageFormat;
  45 import java.util.*;
  46 
  47 import static com.oracle.tools.packager.StandardBundlerParam.*;
  48 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN;
  49 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEY_USER;
  50 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.getPredefinedImage;
  51 
  52 public class MacAppBundler extends AbstractImageBundler {
  53 
  54     private static final ResourceBundle I18N =
  55             ResourceBundle.getBundle(MacAppBundler.class.getName());
  56 
  57     public final static String MAC_BUNDLER_PREFIX =
  58             BUNDLER_PREFIX + "macosx" + File.separator;
  59 
  60     private static final String EXECUTABLE_NAME      = "JavaAppLauncher";
  61     private final static String LIBRARY_NAME         = "libpackager.dylib";
  62     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  63     private static final String OS_TYPE_CODE         = "APPL";
  64     private static final String TEMPLATE_INFO_PLIST_LEGACY  = "Info.plist.template";
  65     private static final String TEMPLATE_INFO_PLIST_LITE    = "Info-lite.plist.template";
  66 
  67     private static Map<String, String> getMacCategories() {
  68         Map<String, String> map = new HashMap<>();
  69         map.put("Business", "public.app-category.business");
  70         map.put("Developer Tools", "public.app-category.developer-tools");
  71         map.put("Education", "public.app-category.education");
  72         map.put("Entertainment", "public.app-category.entertainment");
  73         map.put("Finance", "public.app-category.finance");
  74         map.put("Games", "public.app-category.games");
  75         map.put("Graphics & Design", "public.app-category.graphics-design");
  76         map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
  77         map.put("Lifestyle", "public.app-category.lifestyle");
  78         map.put("Medical", "public.app-category.medical");
  79         map.put("Music", "public.app-category.music");
  80         map.put("News", "public.app-category.news");
  81         map.put("Photography", "public.app-category.photography");
  82         map.put("Productivity", "public.app-category.productivity");
  83         map.put("Reference", "public.app-category.reference");
  84         map.put("Social Networking", "public.app-category.social-networking");
  85         map.put("Sports", "public.app-category.sports");
  86         map.put("Travel", "public.app-category.travel");
  87         map.put("Utilities", "public.app-category.utilities");
  88         map.put("Video", "public.app-category.video");
  89         map.put("Weather", "public.app-category.weather");
  90 
  91         map.put("Action Games", "public.app-category.action-games");
  92         map.put("Adventure Games", "public.app-category.adventure-games");
  93         map.put("Arcade Games", "public.app-category.arcade-games");
  94         map.put("Board Games", "public.app-category.board-games");
  95         map.put("Card Games", "public.app-category.card-games");
  96         map.put("Casino Games", "public.app-category.casino-games");
  97         map.put("Dice Games", "public.app-category.dice-games");
  98         map.put("Educational Games", "public.app-category.educational-games");
  99         map.put("Family Games", "public.app-category.family-games");
 100         map.put("Kids Games", "public.app-category.kids-games");
 101         map.put("Music Games", "public.app-category.music-games");
 102         map.put("Puzzle Games", "public.app-category.puzzle-games");
 103         map.put("Racing Games", "public.app-category.racing-games");
 104         map.put("Role Playing Games", "public.app-category.role-playing-games");
 105         map.put("Simulation Games", "public.app-category.simulation-games");
 106         map.put("Sports Games", "public.app-category.sports-games");
 107         map.put("Strategy Games", "public.app-category.strategy-games");
 108         map.put("Trivia Games", "public.app-category.trivia-games");
 109         map.put("Word Games", "public.app-category.word-games");
 110 
 111         return map;
 112     }
 113 
 114     public static final BundlerParamInfo<Boolean> MAC_CONFIGURE_LAUNCHER_IN_PLIST =
 115             new StandardBundlerParam<>(
 116                     I18N.getString("param.configure-launcher-in-plist"),
 117                     I18N.getString("param.configure-launcher-in-plist.description"),
 118                     "mac.configure-launcher-in-plist",
 119                     Boolean.class,
 120                     params -> Boolean.FALSE,
 121                     (s, p) -> Boolean.valueOf(s));
 122 
 123     public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
 124             new EnumeratedBundlerParam<>(
 125                     I18N.getString("param.category-name"),
 126                     I18N.getString("param.category-name.description"),
 127                     "mac.category",
 128                     String.class,
 129                     params -> params.containsKey(CATEGORY.getID())
 130                             ? CATEGORY.fetchFrom(params)
 131                             : "Unknown",
 132                     (s, p) -> s,
 133                     getMacCategories(),
 134                     false //strict - for MacStoreBundler this should be strict
 135             );
 136 
 137     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
 138             new StandardBundlerParam<>(
 139                     I18N.getString("param.cfbundle-name.name"),
 140                     I18N.getString("param.cfbundle-name.description"),
 141                     "mac.CFBundleName",
 142                     String.class,
 143                     params -> null,
 144                     (s, p) -> s);
 145 
 146     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER =
 147             new StandardBundlerParam<>(
 148                     I18N.getString("param.cfbundle-identifier.name"),
 149                     I18N.getString("param.cfbundle-identifier.description"),
 150                     "mac.CFBundleIdentifier",
 151                     String.class,
 152                     IDENTIFIER::fetchFrom,
 153                     (s, p) -> s);
 154 
 155     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION =
 156             new StandardBundlerParam<>(
 157                     I18N.getString("param.cfbundle-version.name"),
 158                     I18N.getString("param.cfbundle-version.description"),
 159                     "mac.CFBundleVersion",
 160                     String.class,
 161                     p -> {
 162                         String s = VERSION.fetchFrom(p);
 163                         if (validCFBundleVersion(s)) {
 164                             return s;
 165                         } else {
 166                             return "100";
 167                         }
 168                     },
 169                     (s, p) -> s);
 170 
 171     public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
 172             I18N.getString("param.config-root.name"),
 173             I18N.getString("param.config-root.description"),
 174             "configRoot",
 175             File.class,
 176             params -> {
 177                 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
 178                 configRoot.mkdirs();
 179                 return configRoot;
 180             },
 181             (s, p) -> new File(s));
 182 
 183     public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>(
 184             I18N.getString("param.raw-executable-url.name"),
 185             I18N.getString("param.raw-executable-url.description"),
 186             "mac.launcher.url",
 187             URL.class,
 188             params -> MacResources.class.getResource(EXECUTABLE_NAME),
 189             (s, p) -> {
 190                 try {
 191                     return new URL(s);
 192                 } catch (MalformedURLException e) {
 193                     Log.info(e.toString());
 194                     return null;
 195                 }
 196             });
 197 
 198     public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
 199             I18N.getString("param.default-icon-icns"),
 200             I18N.getString("param.default-icon-icns.description"),
 201             ".mac.default.icns",
 202             String.class,
 203             params -> TEMPLATE_BUNDLE_ICON,
 204             (s, p) -> s);
 205 
 206     public static final BundlerParamInfo<Rule[]> MAC_RULES = new StandardBundlerParam<>(
 207             "",
 208             "",
 209             ".mac.runtime.rules",
 210             Rule[].class,
 211             MacAppBundler::createMacRuntimeRules,
 212             (s, p) -> null
 213     );
 214 
 215     public static final BundlerParamInfo<RelativeFileSet> MAC_RUNTIME = new StandardBundlerParam<>(
 216             I18N.getString("param.runtime.name"),
 217             I18N.getString("param.runtime.description"),
 218             BundleParams.PARAM_RUNTIME,
 219             RelativeFileSet.class,
 220             params -> extractMacRuntime(System.getProperty("java.home"), params),
 221             MacAppBundler::extractMacRuntime
 222     );
 223 
 224     public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>(
 225             I18N.getString("param.signing-key-developer-id-app.name"),
 226             I18N.getString("param.signing-key-developer-id-app.description"),
 227             "mac.signing-key-developer-id-app",
 228             String.class,
 229             params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)),
 230             (s, p) -> s);
 231 
 232     public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>(
 233             I18N.getString("param.bundle-id-signing-prefix.name"),
 234             I18N.getString("param.bundle-id-signing-prefix.description"),
 235             "mac.bundle-id-signing-prefix",
 236             String.class,
 237             params -> IDENTIFIER.fetchFrom(params) + ".",
 238             (s, p) -> s);
 239 
 240     public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>(
 241             I18N.getString("param.icon-icns.name"),
 242             I18N.getString("param.icon-icns.description"),
 243             "icon.icns",
 244             File.class,
 245             params -> {
 246                 File f = ICON.fetchFrom(params);
 247                 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
 248                     Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f));
 249                     return null;
 250                 }
 251                 return f;
 252             },
 253             (s, p) -> new File(s));
 254 
 255     public static RelativeFileSet extractMacRuntime(String base, Map<String, ? super Object> params) {
 256         if (base.isEmpty()) {
 257             return null;
 258         }
 259 
 260         File workingBase = new File(base);
 261         workingBase = workingBase.getAbsoluteFile();
 262         try {
 263             workingBase = workingBase.getCanonicalFile();
 264         } catch (IOException ignore) {
 265             // we tried, workingBase will remain absolute and not canonical.
 266         }
 267         
 268         if (workingBase.getName().equals("jre")) {
 269             workingBase = workingBase.getParentFile();
 270         }
 271         if (workingBase.getName().equals("Home")) {
 272             workingBase = workingBase.getParentFile();
 273         }
 274         if (workingBase.getName().equals("Contents")) {
 275             workingBase = workingBase.getParentFile();
 276         }
 277         return JreUtils.extractJreAsRelativeFileSet(workingBase.toString(),
 278                 MAC_RULES.fetchFrom(params), true);
 279     }
 280 
 281     public MacAppBundler() {
 282         super();
 283         baseResourceLoader = MacResources.class;
 284     }
 285 
 286     @Override
 287     protected String getCacheLocation(Map<String, ? super Object> params) {
 288         Boolean systemWide = SYSTEM_WIDE.fetchFrom(params);
 289         if (systemWide == null || systemWide) {
 290             return "/Library/Application Support/" + IDENTIFIER.fetchFrom(params) + "/cache/";
 291         } else {
 292             return "$CACHEDIR/";
 293         }
 294     }
 295 
 296 
 297     public static boolean validCFBundleVersion(String v) {
 298         // CFBundleVersion (String - iOS, OS X) specifies the build version
 299         // number of the bundle, which identifies an iteration (released or
 300         // unreleased) of the bundle. The build version number should be a
 301         // string comprised of three non-negative, period-separated integers
 302         // with the first integer being greater than zero. The string should
 303         // only contain numeric (0-9) and period (.) characters. Leading zeros
 304         // are truncated from each integer and will be ignored (that is,
 305         // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
 306 
 307         if (v == null) {
 308             return false;
 309         }
 310 
 311         String p[] = v.split("\\.");
 312         if (p.length > 3 || p.length < 1) {
 313             Log.verbose(I18N.getString("message.version-string-too-many-components"));
 314             return false;
 315         }
 316 
 317         try {
 318             BigInteger n = new BigInteger(p[0]);
 319             if (BigInteger.ONE.compareTo(n) > 0) {
 320                 Log.verbose(I18N.getString("message.version-string-first-number-not-zero"));
 321                 return false;
 322             }
 323             if (p.length > 1) {
 324                 n = new BigInteger(p[1]);
 325                 if (BigInteger.ZERO.compareTo(n) > 0) {
 326                     Log.verbose(I18N.getString("message.version-string-no-negative-numbers"));
 327                     return false;
 328                 }
 329             }
 330             if (p.length > 2) {
 331                 n = new BigInteger(p[2]);
 332                 if (BigInteger.ZERO.compareTo(n) > 0) {
 333                     Log.verbose(I18N.getString("message.version-string-no-negative-numbers"));
 334                     return false;
 335                 }
 336             }
 337         } catch (NumberFormatException ne) {
 338             Log.verbose(I18N.getString("message.version-string-numbers-only"));
 339             Log.verbose(ne);
 340             return false;
 341         }
 342 
 343         return true;
 344     }
 345 
 346     @Override
 347     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
 348         try {
 349             return doValidate(params);
 350         } catch (RuntimeException re) {
 351             if (re.getCause() instanceof ConfigException) {
 352                 throw (ConfigException) re.getCause();
 353             } else {
 354                 throw new ConfigException(re);
 355             }
 356         }
 357     }
 358 
 359     //to be used by chained bundlers, e.g. by EXE bundler to avoid
 360     // skipping validation if p.type does not include "image"
 361     public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 362         if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
 363             throw new UnsupportedPlatformException();
 364         }
 365 
 366         StandardBundlerParam.validateMainClassInfoFromAppResources(p);
 367 
 368         Map<String, String> userJvmOptions = USER_JVM_OPTIONS.fetchFrom(p);
 369         if (userJvmOptions != null) {
 370             for (Map.Entry<String, String> entry : userJvmOptions.entrySet()) {
 371                 if (entry.getValue() == null || entry.getValue().isEmpty()) {
 372                     throw new ConfigException(
 373                             MessageFormat.format(I18N.getString("error.empty-user-jvm-option-value"), entry.getKey()),
 374                             I18N.getString("error.empty-user-jvm-option-value.advice"));
 375                 }
 376             }
 377         }
 378 
 379         if (getPredefinedImage(p) != null) {
 380             return true;
 381         }
 382 
 383         if (MAIN_JAR.fetchFrom(p) == null) {
 384             throw new ConfigException(
 385                     I18N.getString("error.no-application-jar"),
 386                     I18N.getString("error.no-application-jar.advice"));
 387         }
 388 
 389         //validate required inputs
 390         testRuntime(MAC_RUNTIME.fetchFrom(p), new String[] {
 391                 "Contents/Home/(jre/)?lib/[^/]+/libjvm.dylib", // most reliable
 392                 "Contents/Home/(jre/)?lib/rt.jar", // fallback canary for JDK 8
 393         });
 394         if (USE_FX_PACKAGING.fetchFrom(p)) {
 395             testRuntime(MAC_RUNTIME.fetchFrom(p), new String[] {"Contents/Home/(jre/)?lib/ext/jfxrt.jar", "Contents/Home/(jre/)?lib/jfxrt.jar"});
 396         }
 397 
 398         // validate short version
 399         if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) {
 400             throw new ConfigException(
 401                     I18N.getString("error.invalid-cfbundle-version"),
 402                     I18N.getString("error.invalid-cfbundle-version.advice"));
 403         }
 404         
 405         // reject explicitly set sign to true and no valid signature key
 406         if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) {
 407             String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
 408             if (signingIdentity == null) {
 409                 throw new ConfigException(
 410                         I18N.getString("error.explicit-sign-no-cert"),
 411                         I18N.getString("error.explicit-sign-no-cert.advice"));
 412             }
 413         }
 414         
 415         return true;
 416     }
 417 
 418     private File getConfig_InfoPlist(Map<String, ? super Object> params) {
 419         return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
 420     }
 421 
 422     private File getConfig_Icon(Map<String, ? super Object> params) {
 423         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns");
 424     }
 425 
 426     private void prepareConfigFiles(Map<String, ? super Object> params) throws IOException {
 427         File infoPlistFile = getConfig_InfoPlist(params);
 428         infoPlistFile.createNewFile();
 429         writeInfoPlist(infoPlistFile, params);
 430 
 431         // Copy icon to Resources folder
 432         prepareIcon(params);
 433     }
 434 
 435     public File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
 436         File rootDirectory = null;
 437         Map<String, ? super Object> originalParams = new HashMap<>(p);
 438         
 439         if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) {
 440             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
 441         }
 442         if (!outputDirectory.canWrite()) {
 443             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
 444         }
 445 
 446         try {
 447             final File predefinedImage = getPredefinedImage(p);
 448             if (predefinedImage != null) {
 449                 return predefinedImage;
 450             }
 451 
 452             // side effect is temp dir is created if not specified
 453             BUILD_ROOT.fetchFrom(p);
 454 
 455             //prepare config resources (we will copy them to the bundle later)
 456             // NB: explicitly saving them to simplify customization
 457             prepareConfigFiles(p);
 458 
 459             // Create directory structure
 460             rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
 461             IOUtils.deleteRecursive(rootDirectory);
 462             rootDirectory.mkdirs();
 463 
 464             if (!dependentTask) {
 465                 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath()));
 466             }
 467 
 468             File contentsDirectory = new File(rootDirectory, "Contents");
 469             contentsDirectory.mkdirs();
 470 
 471             File macOSDirectory = new File(contentsDirectory, "MacOS");
 472             macOSDirectory.mkdirs();
 473 
 474             File javaDirectory = new File(contentsDirectory, "Java");
 475             javaDirectory.mkdirs();
 476 
 477             File plugInsDirectory = new File(contentsDirectory, "PlugIns");
 478 
 479             File resourcesDirectory = new File(contentsDirectory, "Resources");
 480             resourcesDirectory.mkdirs();
 481 
 482             // Generate PkgInfo
 483             File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
 484             pkgInfoFile.createNewFile();
 485             writePkgInfo(pkgInfoFile);
 486 
 487             // Copy executable to MacOS folder
 488             File executableFile = new File(macOSDirectory, getLauncherName(p));
 489             IOUtils.copyFromURL(
 490                     RAW_EXECUTABLE_URL.fetchFrom(p),
 491                     executableFile);
 492 
 493             // Copy library to the MacOS folder
 494             IOUtils.copyFromURL(
 495                     MacResources.class.getResource(LIBRARY_NAME),
 496                     new File(macOSDirectory, LIBRARY_NAME));
 497 
 498             // maybe generate launcher config
 499             if (!MAC_CONFIGURE_LAUNCHER_IN_PLIST.fetchFrom(p)) {
 500                 if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
 501                     writeCfgFile(p, rootDirectory);
 502                 } else {
 503                     writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime");
 504                 }
 505             }
 506 
 507             executableFile.setExecutable(true, false);
 508 
 509             // Copy runtime to PlugIns folder
 510             copyRuntime(plugInsDirectory, p);
 511 
 512             // Copy class path entries to Java folder
 513             copyClassPathEntries(javaDirectory, p);
 514 
 515             //TODO: Need to support adding native libraries.
 516             // Copy library path entries to MacOS folder
 517             //copyLibraryPathEntries(macOSDirectory);
 518 
 519             /*********** Take care of "config" files *******/
 520             // Copy icon to Resources folder
 521             IOUtils.copyFile(getConfig_Icon(p),
 522                     new File(resourcesDirectory, getConfig_Icon(p).getName()));
 523 
 524             // copy file association icons
 525             for (Map<String, ? super Object> fa : FILE_ASSOCIATIONS.fetchFrom(p)) {
 526                 File f = FA_ICON.fetchFrom(fa);
 527                 if (f != null && f.exists()) {
 528                     IOUtils.copyFile(f,
 529                             new File(resourcesDirectory, f.getName()));
 530                 }
 531             }
 532 
 533             // Generate Info.plist
 534             IOUtils.copyFile(getConfig_InfoPlist(p),
 535                     new File(contentsDirectory, "Info.plist"));
 536             
 537             // create the secondary launchers, if any
 538             List<Map<String, ? super Object>> entryPoints = StandardBundlerParam.SECONDARY_LAUNCHERS.fetchFrom(p);
 539             for (Map<String, ? super Object> entryPoint : entryPoints) {
 540                 Map<String, ? super Object> tmp = new HashMap<>(originalParams);
 541                 tmp.putAll(entryPoint);
 542                 createLauncherForEntryPoint(tmp, rootDirectory);
 543             }
 544 
 545             // maybe sign
 546             if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.TRUE)) {
 547                 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
 548                 if (signingIdentity != null) {
 549                     MacBaseInstallerBundler.signAppBundle(p, rootDirectory, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(p));
 550                 }
 551             }
 552         } catch (IOException ex) {
 553             Log.info(ex.toString());
 554             Log.verbose(ex);
 555             return null;
 556         } finally {
 557             if (!VERBOSE.fetchFrom(p)) {
 558                 //cleanup
 559                 cleanupConfigFiles(p);
 560             } else {
 561                 Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath()));
 562             }
 563         }
 564         return rootDirectory;
 565     }
 566 
 567     public void cleanupConfigFiles(Map<String, ? super Object> params) {
 568         //Since building the app can be bypassed, make sure configRoot was set
 569         if (CONFIG_ROOT.fetchFrom(params) != null) {
 570             getConfig_Icon(params).delete();
 571             getConfig_InfoPlist(params).delete();
 572         }
 573     }
 574 
 575     private void copyClassPathEntries(File javaDirectory, Map<String, ? super Object> params) throws IOException {
 576         List<RelativeFileSet> resourcesList = APP_RESOURCES_LIST.fetchFrom(params);
 577         if (resourcesList == null) {
 578             throw new RuntimeException(I18N.getString("message.null-classpath"));
 579         }
 580         
 581         for (RelativeFileSet classPath : resourcesList) {
 582             File srcdir = classPath.getBaseDirectory();
 583             for (String fname : classPath.getIncludedFiles()) {
 584                 IOUtils.copyFile(
 585                         new File(srcdir, fname), new File(javaDirectory, fname));
 586             }
 587         }
 588     }
 589 
 590     private void copyRuntime(File plugInsDirectory, Map<String, ? super Object> params) throws IOException {
 591         RelativeFileSet runTime = MAC_RUNTIME.fetchFrom(params);
 592         if (runTime == null) {
 593             //request to use system runtime => do not bundle
 594             return;
 595         }
 596         plugInsDirectory.mkdirs();
 597 
 598         File srcdir = runTime.getBaseDirectory();
 599         // the name in .../Contents/PlugIns/ must have a dot to be verified 
 600         // properly by the Mac App Store.
 601         File destDir = new File(plugInsDirectory, "Java.runtime");
 602         Set<String> filesToCopy = runTime.getIncludedFiles();
 603 
 604         for (String fname : filesToCopy) {
 605             IOUtils.copyFile(
 606                     new File(srcdir, fname), new File(destDir, fname));
 607         }
 608     }
 609 
 610     private void prepareIcon(Map<String, ? super Object> params) throws IOException {
 611         File icon = ICON_ICNS.fetchFrom(params);
 612         if (icon == null || !icon.exists()) {
 613             fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
 614                     "icon",
 615                     DEFAULT_ICNS_ICON.fetchFrom(params),
 616                     getConfig_Icon(params),
 617                     VERBOSE.fetchFrom(params),
 618                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 619         } else {
 620             fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
 621                     "icon",
 622                     icon,
 623                     getConfig_Icon(params),
 624                     VERBOSE.fetchFrom(params),
 625                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 626         }
 627     }
 628 
 629     private String getLauncherName(Map<String, ? super Object> params) {
 630         if (APP_NAME.fetchFrom(params) != null) {
 631             return APP_NAME.fetchFrom(params);
 632         } else {
 633             return MAIN_CLASS.fetchFrom(params);
 634         }
 635     }
 636 
 637     private String getBundleName(Map<String, ? super Object> params) {
 638         //TODO: Check to see what rules/limits are in place for CFBundleName
 639         if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
 640             String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
 641             if (bn.length() > 16) {
 642                 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn));
 643             }
 644             return MAC_CF_BUNDLE_NAME.fetchFrom(params);
 645         } else if (APP_NAME.fetchFrom(params) != null) {
 646             return APP_NAME.fetchFrom(params);
 647         } else {
 648             String nm = MAIN_CLASS.fetchFrom(params);
 649             if (nm.length() > 16) {
 650                 nm = nm.substring(0, 16);
 651             }
 652             return nm;
 653         }
 654     }
 655 
 656     private void writeInfoPlist(File file, Map<String, ? super Object> params) throws IOException {
 657         Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath()));
 658 
 659         //prepare config for exe
 660         //Note: do not need CFBundleDisplayName if we do not support localization
 661         Map<String, String> data = new HashMap<>();
 662         data.put("DEPLOY_ICON_FILE", getConfig_Icon(params).getName());
 663         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 664                 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
 665         data.put("DEPLOY_BUNDLE_NAME",
 666                 getBundleName(params));
 667         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 668                 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
 669         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
 670         if (MAC_RUNTIME.fetchFrom(params) != null) {
 671             data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime");
 672         } else {
 673             data.put("DEPLOY_JAVA_RUNTIME_NAME", "");
 674         }
 675         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 676                 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
 677         data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
 678                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
 679         data.put("DEPLOY_BUNDLE_CATEGORY",
 680                 //TODO parameters should provide set of values for IDEs
 681                 MAC_CATEGORY.validatedFetchFrom(params));
 682 
 683         data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
 684 
 685         data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
 686 
 687         StringBuilder sb = new StringBuilder();
 688         List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
 689 
 690         String newline = ""; //So we don't add unneccessary extra line after last append
 691         for (String o : jvmOptions) {
 692             sb.append(newline).append("    <string>").append(o).append("</string>");
 693             newline = "\n";
 694         }
 695 
 696         Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
 697         for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
 698             sb.append(newline)
 699                     .append("    <string>-D")
 700                     .append(entry.getKey())
 701                     .append("=")
 702                     .append(entry.getValue())
 703                     .append("</string>");
 704             newline = "\n";
 705         }
 706 
 707         String preloader = PRELOADER_CLASS.fetchFrom(params);
 708         if (preloader != null) {
 709             sb.append(newline)
 710                     .append("    <string>-Djavafx.preloader=")
 711                     .append(preloader)
 712                     .append("</string>");
 713             //newline = "\n";
 714         }
 715 
 716         data.put("DEPLOY_JVM_OPTIONS", sb.toString());
 717 
 718         sb = new StringBuilder();
 719         List<String> args = ARGUMENTS.fetchFrom(params);
 720         newline = ""; //So we don't add unneccessary extra line after last append
 721         for (String o : args) {
 722             sb.append(newline).append("    <string>").append(o).append("</string>");
 723             newline = "\n";
 724         }
 725         data.put("DEPLOY_ARGUMENTS", sb.toString());
 726 
 727         newline = "";
 728         sb = new StringBuilder();
 729         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
 730         for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
 731             sb.append(newline)
 732                 .append("      <key>").append(arg.getKey()).append("</key>\n")
 733                 .append("      <string>").append(arg.getValue()).append("</string>");
 734             newline = "\n";
 735         }
 736         data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
 737 
 738 
 739         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 740 
 741         StringBuilder macroedPath = new StringBuilder();
 742         for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) {
 743             macroedPath.append(s);
 744             macroedPath.append(":");
 745         }
 746         macroedPath.deleteCharAt(macroedPath.length() - 1);
 747 
 748         data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString());
 749 
 750         //TODO: Add remainder of the classpath
 751 
 752         StringBuilder bundleDocumentTypes = new StringBuilder();
 753         StringBuilder exportedTypes = new StringBuilder();
 754         for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
 755 
 756             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 757             
 758             if (extensions == null) {
 759                 Log.info(I18N.getString("message.creating-association-with-null-extension"));
 760             }
 761 
 762             List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 763             String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty())
 764                     ? "mime"
 765                     : extensions.get(0));
 766             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 767             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS
 768 
 769             bundleDocumentTypes.append("    <dict>\n")
 770                 .append("      <key>LSItemContentTypes</key>\n")
 771                 .append("      <array>\n")
 772                 .append("        <string>")
 773                 .append(itemContentType)
 774                 .append("</string>\n")
 775                 .append("      </array>\n")
 776                 .append("\n")
 777                 .append("      <key>CFBundleTypeName</key>\n")
 778                 .append("      <string>")
 779                 .append(description)
 780                 .append("</string>\n")
 781                 .append("\n")
 782                 .append("      <key>LSHandlerRank</key>\n")
 783                 .append("      <string>Owner</string>\n") //TODO make a bundler arg
 784                 .append("\n")
 785                 .append("      <key>CFBundleTypeRole</key>\n")
 786                 .append("      <string>Editor</string>\n") // TODO make a bundler arg
 787                 .append("\n")
 788                 .append("      <key>LSIsAppleDefaultForType</key>\n")
 789                 .append("      <true/>\n") // TODO make a bundler arg
 790                 .append("\n");
 791 
 792             if (icon != null && icon.exists()) {
 793                 //?
 794                 bundleDocumentTypes.append("      <key>CFBundleTypeIconFile</key>\n")
 795                         .append("      <string>")
 796                         .append(icon.getName())
 797                         .append("</string>\n");
 798             }
 799             bundleDocumentTypes.append("    </dict>\n");
 800 
 801             exportedTypes.append("    <dict>\n")
 802                 .append("      <key>UTTypeIdentifier</key>\n")
 803                 .append("      <string>")
 804                 .append(itemContentType)
 805                 .append("</string>\n")
 806                 .append("\n")
 807                 .append("      <key>UTTypeDescription</key>\n")
 808                 .append("      <string>")
 809                 .append(description)
 810                 .append("</string>\n")
 811                 .append("      <key>UTTypeConformsTo</key>\n")
 812                 .append("      <array>\n")
 813                 .append("          <string>public.data</string>\n") //TODO expose this?
 814                 .append("      </array>\n")
 815                 .append("\n");
 816             
 817             if (icon != null && icon.exists()) {
 818                 exportedTypes.append("      <key>UTTypeIconFile</key>\n")
 819                     .append("      <string>")
 820                     .append(icon.getName())
 821                     .append("</string>\n")
 822                     .append("\n");
 823             }
 824 
 825             exportedTypes.append("\n")
 826                 .append("      <key>UTTypeTagSpecification</key>\n")
 827                 .append("      <dict>\n")
 828             //TODO expose via param? .append("        <key>com.apple.ostype</key>\n");
 829             //TODO expose via param? .append("        <string>ABCD</string>\n")
 830                 .append("\n");
 831 
 832             if (extensions != null && !extensions.isEmpty()) {
 833                 exportedTypes.append("        <key>public.filename-extension</key>\n")
 834                     .append("        <array>\n");
 835 
 836                 for (String ext : extensions) {
 837                     exportedTypes.append("          <string>")
 838                         .append(ext)
 839                         .append("</string>\n");
 840                 }
 841                 exportedTypes.append("        </array>\n");
 842             }
 843             if (mimeTypes != null && !mimeTypes.isEmpty()) {
 844                 exportedTypes.append("        <key>public.mime-type</key>\n")
 845                     .append("        <array>\n");
 846 
 847                 for (String mime : mimeTypes) {
 848                     exportedTypes.append("          <string>")
 849                         .append(mime)
 850                         .append("</string>\n");
 851                 }
 852                 exportedTypes.append("        </array>\n");
 853             }
 854             exportedTypes.append("      </dict>\n")
 855                     .append("    </dict>\n");
 856         }
 857         String associationData;
 858         if (bundleDocumentTypes.length() > 0) {
 859             associationData = "\n  <key>CFBundleDocumentTypes</key>\n  <array>\n"
 860                     + bundleDocumentTypes.toString()
 861                     + "  </array>\n\n  <key>UTExportedTypeDeclarations</key>\n  <array>\n"
 862                     + exportedTypes.toString()
 863                     + "  </array>\n";
 864         } else {
 865             associationData = "";
 866         }
 867         data.put("DEPLOY_FILE_ASSOCIATIONS", associationData);
 868 
 869 
 870         Writer w = new BufferedWriter(new FileWriter(file));
 871         w.write(preprocessTextResource(
 872                 MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(),
 873                 I18N.getString("resource.bundle-config-file"),
 874                 MAC_CONFIGURE_LAUNCHER_IN_PLIST.fetchFrom(params)
 875                     ? TEMPLATE_INFO_PLIST_LEGACY
 876                     : TEMPLATE_INFO_PLIST_LITE,
 877                 data, VERBOSE.fetchFrom(params),
 878                 DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
 879         w.close();
 880 
 881     }
 882 
 883     private void writePkgInfo(File file) throws IOException {
 884 
 885         //hardcoded as it does not seem we need to change it ever
 886         String signature = "????";
 887 
 888         try (Writer out = new BufferedWriter(new FileWriter(file))) {
 889             out.write(OS_TYPE_CODE + signature);
 890             out.flush();
 891         }
 892     }
 893 
 894     public static Rule[] createMacRuntimeRules(Map<String, ? super Object> params) {
 895         if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
 896             // we will never get a sensible answer unless we are running on OSX,
 897             // so quit now and return null indicating 'no sensible value'
 898             return null;
 899         }
 900 
 901         //Subsetting of JRE is restricted.
 902         //JRE README defines what is allowed to strip:
 903         //   http://www.oracle.com/technetwork/java/javase/jre-8-readme-2095710.html
 904         //
 905 
 906         List<Rule> rules = new ArrayList<>();
 907 
 908         File baseDir;
 909 
 910         if (params.containsKey(MAC_RUNTIME.getID())) {
 911             Object o = params.get(MAC_RUNTIME.getID());
 912             if (o instanceof RelativeFileSet) {
 913 
 914                 baseDir = ((RelativeFileSet)o).getBaseDirectory();
 915             } else {
 916                 baseDir = new File(o.toString());
 917             }
 918         } else {
 919             baseDir = new File(System.getProperty("java.home"));
 920         }
 921 
 922         // we accept either pointing at the directories typically installed at:
 923         // /Libraries/Java/JavaVirtualMachine/jdk1.8.0_40/
 924         //   * .
 925         //   * Contents/Home
 926         //   * Contents/Home/jre
 927         // /Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/
 928         //   * .
 929         //   * /Contents/Home
 930         // version may change, and if we don't detect any Contents/Home or Contents/Home/jre we will
 931         // presume we are at a root.
 932 
 933         if (!baseDir.exists()) {
 934             throw new RuntimeException(I18N.getString("error.non-existent-runtime"),
 935                 new ConfigException(I18N.getString("error.non-existent-runtime"),
 936                     I18N.getString("error.non-existent-runtime.advice")));
 937         }
 938 
 939         boolean isJRE;
 940         boolean isJDK;
 941 
 942         try {
 943             String path = baseDir.getCanonicalPath();
 944             if (path.endsWith("/Contents/Home/jre")) {
 945                 baseDir = baseDir.getParentFile().getParentFile().getParentFile();
 946             } else if (path.endsWith("/Contents/Home")) {
 947                 baseDir = baseDir.getParentFile().getParentFile();
 948             }
 949 
 950             isJRE = new File(baseDir, "Contents/Home/lib/jli/libjli.dylib").exists();
 951             isJDK = new File(baseDir, "Contents/Home/jre/lib/jli/libjli.dylib").exists();
 952 
 953         } catch (IOException e) {
 954             throw new RuntimeException(e);
 955         }
 956 
 957         if (!(isJRE || isJDK)) {
 958             throw new RuntimeException(I18N.getString("error.cannot-detect-runtime-in-directory"),
 959                     new ConfigException(I18N.getString("error.cannot-detect-runtime-in-directory"),
 960                             I18N.getString("error.cannot-detect-runtime-in-directory.advice")));
 961         }
 962 
 963         // we need the Info.plist for signing
 964         rules.add(Rule.suffix("/contents/info.plist"));
 965         
 966         // Strip some JRE specific stuff
 967         if (isJRE) {
 968             rules.add(Rule.suffixNeg("/contents/disabled.plist"));
 969             rules.add(Rule.suffixNeg("/contents/enabled.plist"));
 970             rules.add(Rule.substrNeg("/contents/frameworks/"));
 971         }
 972         
 973         // strip out command line tools
 974         rules.add(Rule.suffixNeg("home/bin"));
 975         if (isJDK) {
 976             rules.add(Rule.suffixNeg("home/jre/bin"));
 977         }
 978 
 979         // strip out JRE stuff
 980         if (isJRE) {
 981             // update helper
 982             rules.add(Rule.suffixNeg("resources"));
 983             // interfacebuilder files
 984             rules.add(Rule.suffixNeg("lib/nibs"));
 985             // browser integration
 986             rules.add(Rule.suffixNeg("lib/libnpjp2.dylib"));
 987             // java webstart
 988             rules.add(Rule.suffixNeg("lib/security/javaws.policy"));
 989             rules.add(Rule.suffixNeg("lib/shortcuts"));
 990 
 991             // general deploy libraries
 992             rules.add(Rule.suffixNeg("lib/deploy"));
 993             rules.add(Rule.suffixNeg("lib/deploy.jar"));
 994             rules.add(Rule.suffixNeg("lib/javaws.jar"));
 995             rules.add(Rule.suffixNeg("lib/libdeploy.dylib"));
 996             rules.add(Rule.suffixNeg("lib/plugin.jar"));
 997         }
 998 
 999         // strip out man pages
1000         rules.add(Rule.suffixNeg("home/man"));
1001 
1002         // this is the build hashes, strip or keep?
1003         //rules.add(Rule.suffixNeg("home/release"));
1004 
1005         // strip out JDK stuff like JavaDB, JNI Headers, etc
1006         if (isJDK) {
1007             rules.add(Rule.suffixNeg("home/db"));
1008             rules.add(Rule.suffixNeg("home/demo"));
1009             rules.add(Rule.suffixNeg("home/include"));
1010             rules.add(Rule.suffixNeg("home/lib"));
1011             rules.add(Rule.suffixNeg("home/sample"));
1012             rules.add(Rule.suffixNeg("home/src.zip"));
1013             rules.add(Rule.suffixNeg("home/javafx-src.zip"));
1014         }
1015 
1016         //"home/rt" is not part of the official builds
1017         // but we may be creating this symlink to make older NB projects
1018         // happy. Make sure to not include it into final artifact
1019         rules.add(Rule.suffixNeg("home/rt"));
1020 
1021         //rules.add(Rule.suffixNeg("jre/lib/ext")); //need some of jars there for https to work
1022 
1023         // strip out flight recorder
1024         rules.add(Rule.suffixNeg("lib/jfr.jar"));
1025 
1026         return rules.toArray(new Rule[rules.size()]);
1027     }
1028 
1029     //////////////////////////////////////////////////////////////////////////////////
1030     // Implement Bundler
1031     //////////////////////////////////////////////////////////////////////////////////
1032 
1033     @Override
1034     public String getName() {
1035         return I18N.getString("bundler.name");
1036     }
1037 
1038     @Override
1039     public String getDescription() {
1040         return I18N.getString("bundler.description");
1041     }
1042 
1043     @Override
1044     public String getID() {
1045         return "mac.app";
1046     }
1047 
1048     @Override
1049     public String getBundleType() {
1050         return "IMAGE";
1051     }
1052 
1053     @Override
1054     public Collection<BundlerParamInfo<?>> getBundleParameters() {
1055         return getAppBundleParameters();
1056     }
1057 
1058     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
1059         return Arrays.asList(
1060                 APP_NAME,
1061                 APP_RESOURCES,
1062                 // APP_RESOURCES_LIST, // ??
1063                 ARGUMENTS,
1064                 BUNDLE_ID_SIGNING_PREFIX,
1065                 CLASSPATH,
1066                 DEVELOPER_ID_APP_SIGNING_KEY,
1067                 ICON_ICNS,
1068                 JVM_OPTIONS,
1069                 JVM_PROPERTIES,
1070                 MAC_CATEGORY,
1071                 MAC_CF_BUNDLE_IDENTIFIER,
1072                 MAC_CF_BUNDLE_NAME,
1073                 MAC_CF_BUNDLE_VERSION,
1074                 MAC_RUNTIME,
1075                 MAIN_CLASS,
1076                 MAIN_JAR,
1077                 PREFERENCES_ID,
1078                 PRELOADER_CLASS,
1079                 SIGNING_KEYCHAIN,
1080                 USER_JVM_OPTIONS,
1081                 VERSION
1082         );
1083     }
1084 
1085 
1086     @Override
1087     public File execute(Map<String, ? super Object> params, File outputParentDir) {
1088         return doBundle(params, outputParentDir, false);
1089     }
1090 
1091     private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException {
1092         prepareConfigFiles(p);
1093 
1094         if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
1095             writeCfgFile(p, rootDirectory);
1096         } else {
1097             writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime");
1098         }
1099 
1100         // Copy executable root folder
1101         File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p));
1102         IOUtils.copyFromURL(
1103                 RAW_EXECUTABLE_URL.fetchFrom(p),
1104                 executableFile);
1105         executableFile.setExecutable(true, false);
1106 
1107     }
1108 
1109     public static String getLauncherCfgName(Map<String, ? super Object> p) {
1110         return "Contents/Java/" + APP_NAME.fetchFrom(p) +".cfg";
1111     }
1112 
1113     private void writeCfgFile(Map<String, ? super Object> params, File rootDir) throws FileNotFoundException {
1114         File pkgInfoFile = new File(rootDir, getLauncherCfgName(params));
1115 
1116         pkgInfoFile.delete();
1117 
1118         PrintStream out = new PrintStream(pkgInfoFile);
1119         if (MAC_RUNTIME.fetchFrom(params) == null) {
1120             out.println("app.runtime=");
1121         } else {
1122             out.println("app.runtime=$APPDIR/PlugIns/Java.runtime");
1123         }
1124         out.println("app.mainjar=" + MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
1125         out.println("app.version=" + VERSION.fetchFrom(params));
1126         //for future AU support (to be able to find app in the registry)
1127         out.println("app.id=" + IDENTIFIER.fetchFrom(params));
1128         out.println("app.preferences.id=" + PREFERENCES_ID.fetchFrom(params));
1129         out.println("app.identifier=" + IDENTIFIER.fetchFrom(params));
1130 
1131         out.println("app.mainclass=" +
1132                 MAIN_CLASS.fetchFrom(params).replaceAll("\\.", "/"));
1133         out.println("app.classpath=" + CLASSPATH.fetchFrom(params));
1134 
1135         List<String> jvmargs = JVM_OPTIONS.fetchFrom(params);
1136         int idx = 1;
1137         for (String a : jvmargs) {
1138             out.println("jvmarg."+idx+"="+a);
1139             idx++;
1140         }
1141         Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
1142         for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
1143             out.println("jvmarg."+idx+"=-D"+entry.getKey()+"="+entry.getValue());
1144             idx++;
1145         }
1146 
1147         String preloader = PRELOADER_CLASS.fetchFrom(params);
1148         if (preloader != null) {
1149             out.println("jvmarg."+idx+"=-Djavafx.preloader="+preloader);
1150         }
1151 
1152         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
1153         idx = 1;
1154         for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
1155             if (arg.getKey() == null || arg.getValue() == null) {
1156                 Log.info(I18N.getString("message.jvm-user-arg-is-null"));
1157             }
1158             else {
1159                 out.println("jvmuserarg."+idx+".name="+arg.getKey());
1160                 out.println("jvmuserarg."+idx+".value="+arg.getValue());
1161             }
1162             idx++;
1163         }
1164 
1165         // add command line args
1166         List<String> args = ARGUMENTS.fetchFrom(params);
1167         idx = 1;
1168         for (String a : args) {
1169             out.println("arg."+idx+"="+a);
1170             idx++;
1171         }
1172 
1173         out.close();
1174     }
1175     
1176 }