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