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)), "$APPDIR/PlugIns/Java.runtime");
 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 void writeInfoPlist(File file, Map<String, ? super Object> params) throws IOException {
 666         Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath()));
 667 
 668         //prepare config for exe
 669         //Note: do not need CFBundleDisplayName if we do not support localization
 670         Map<String, String> data = new HashMap<>();
 671         data.put("DEPLOY_ICON_FILE", getConfig_Icon(params).getName());
 672         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 673                 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
 674         data.put("DEPLOY_BUNDLE_NAME",
 675                 getBundleName(params));
 676         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 677                 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
 678         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
 679         if (MAC_RUNTIME.fetchFrom(params) != null) {
 680             data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime");
 681         } else {
 682             data.put("DEPLOY_JAVA_RUNTIME_NAME", "");
 683         }
 684         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 685                 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
 686         data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
 687                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
 688         data.put("DEPLOY_BUNDLE_CATEGORY",
 689                 //TODO parameters should provide set of values for IDEs
 690                 MAC_CATEGORY.validatedFetchFrom(params));
 691 
 692         data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
 693 
 694         data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
 695 
 696         StringBuilder sb = new StringBuilder();
 697         List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
 698 
 699         String newline = ""; //So we don't add unneccessary extra line after last append
 700         for (String o : jvmOptions) {
 701             sb.append(newline).append("    <string>").append(o).append("</string>");
 702             newline = "\n";
 703         }
 704 
 705         Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
 706         for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
 707             sb.append(newline)
 708                     .append("    <string>-D")
 709                     .append(entry.getKey())
 710                     .append("=")
 711                     .append(entry.getValue())
 712                     .append("</string>");
 713             newline = "\n";
 714         }
 715 
 716         String preloader = PRELOADER_CLASS.fetchFrom(params);
 717         if (preloader != null) {
 718             sb.append(newline)
 719                     .append("    <string>-Djavafx.preloader=")
 720                     .append(preloader)
 721                     .append("</string>");
 722             //newline = "\n";
 723         }
 724 
 725         data.put("DEPLOY_JVM_OPTIONS", sb.toString());
 726 
 727         sb = new StringBuilder();
 728         List<String> args = ARGUMENTS.fetchFrom(params);
 729         newline = ""; //So we don't add unneccessary extra line after last append
 730         for (String o : args) {
 731             sb.append(newline).append("    <string>").append(o).append("</string>");
 732             newline = "\n";
 733         }
 734         data.put("DEPLOY_ARGUMENTS", sb.toString());
 735 
 736         newline = "";
 737         sb = new StringBuilder();
 738         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
 739         for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
 740             sb.append(newline)
 741                 .append("      <key>").append(arg.getKey()).append("</key>\n")
 742                 .append("      <string>").append(arg.getValue()).append("</string>");
 743             newline = "\n";
 744         }
 745         data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
 746 
 747 
 748         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 749 
 750         StringBuilder macroedPath = new StringBuilder();
 751         for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) {
 752             macroedPath.append(s);
 753             macroedPath.append(":");
 754         }
 755         macroedPath.deleteCharAt(macroedPath.length() - 1);
 756 
 757         data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString());
 758 
 759         //TODO: Add remainder of the classpath
 760 
 761         StringBuilder bundleDocumentTypes = new StringBuilder();
 762         StringBuilder exportedTypes = new StringBuilder();
 763         for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
 764 
 765             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 766             
 767             if (extensions == null) {
 768                 Log.info(I18N.getString("message.creating-association-with-null-extension"));
 769             }
 770 
 771             List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 772             String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty())
 773                     ? "mime"
 774                     : extensions.get(0));
 775             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 776             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS
 777 
 778             bundleDocumentTypes.append("    <dict>\n")
 779                 .append("      <key>LSItemContentTypes</key>\n")
 780                 .append("      <array>\n")
 781                 .append("        <string>")
 782                 .append(itemContentType)
 783                 .append("</string>\n")
 784                 .append("      </array>\n")
 785                 .append("\n")
 786                 .append("      <key>CFBundleTypeName</key>\n")
 787                 .append("      <string>")
 788                 .append(description)
 789                 .append("</string>\n")
 790                 .append("\n")
 791                 .append("      <key>LSHandlerRank</key>\n")
 792                 .append("      <string>Owner</string>\n") //TODO make a bundler arg
 793                 .append("\n")
 794                 .append("      <key>CFBundleTypeRole</key>\n")
 795                 .append("      <string>Editor</string>\n") // TODO make a bundler arg
 796                 .append("\n")
 797                 .append("      <key>LSIsAppleDefaultForType</key>\n")
 798                 .append("      <true/>\n") // TODO make a bundler arg
 799                 .append("\n");
 800 
 801             if (icon != null && icon.exists()) {
 802                 //?
 803                 bundleDocumentTypes.append("      <key>CFBundleTypeIconFile</key>\n")
 804                         .append("      <string>")
 805                         .append(icon.getName())
 806                         .append("</string>\n");
 807             }
 808             bundleDocumentTypes.append("    </dict>\n");
 809 
 810             exportedTypes.append("    <dict>\n")
 811                 .append("      <key>UTTypeIdentifier</key>\n")
 812                 .append("      <string>")
 813                 .append(itemContentType)
 814                 .append("</string>\n")
 815                 .append("\n")
 816                 .append("      <key>UTTypeDescription</key>\n")
 817                 .append("      <string>")
 818                 .append(description)
 819                 .append("</string>\n")
 820                 .append("      <key>UTTypeConformsTo</key>\n")
 821                 .append("      <array>\n")
 822                 .append("          <string>public.data</string>\n") //TODO expose this?
 823                 .append("      </array>\n")
 824                 .append("\n");
 825             
 826             if (icon != null && icon.exists()) {
 827                 exportedTypes.append("      <key>UTTypeIconFile</key>\n")
 828                     .append("      <string>")
 829                     .append(icon.getName())
 830                     .append("</string>\n")
 831                     .append("\n");
 832             }
 833 
 834             exportedTypes.append("\n")
 835                 .append("      <key>UTTypeTagSpecification</key>\n")
 836                 .append("      <dict>\n")
 837             //TODO expose via param? .append("        <key>com.apple.ostype</key>\n");
 838             //TODO expose via param? .append("        <string>ABCD</string>\n")
 839                 .append("\n");
 840 
 841             if (extensions != null && !extensions.isEmpty()) {
 842                 exportedTypes.append("        <key>public.filename-extension</key>\n")
 843                     .append("        <array>\n");
 844 
 845                 for (String ext : extensions) {
 846                     exportedTypes.append("          <string>")
 847                         .append(ext)
 848                         .append("</string>\n");
 849                 }
 850                 exportedTypes.append("        </array>\n");
 851             }
 852             if (mimeTypes != null && !mimeTypes.isEmpty()) {
 853                 exportedTypes.append("        <key>public.mime-type</key>\n")
 854                     .append("        <array>\n");
 855 
 856                 for (String mime : mimeTypes) {
 857                     exportedTypes.append("          <string>")
 858                         .append(mime)
 859                         .append("</string>\n");
 860                 }
 861                 exportedTypes.append("        </array>\n");
 862             }
 863             exportedTypes.append("      </dict>\n")
 864                     .append("    </dict>\n");
 865         }
 866         String associationData;
 867         if (bundleDocumentTypes.length() > 0) {
 868             associationData = "\n  <key>CFBundleDocumentTypes</key>\n  <array>\n"
 869                     + bundleDocumentTypes.toString()
 870                     + "  </array>\n\n  <key>UTExportedTypeDeclarations</key>\n  <array>\n"
 871                     + exportedTypes.toString()
 872                     + "  </array>\n";
 873         } else {
 874             associationData = "";
 875         }
 876         data.put("DEPLOY_FILE_ASSOCIATIONS", associationData);
 877 
 878 
 879         Writer w = new BufferedWriter(new FileWriter(file));
 880         w.write(preprocessTextResource(
 881                 MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(),
 882                 I18N.getString("resource.bundle-config-file"),
 883                 MAC_CONFIGURE_LAUNCHER_IN_PLIST.fetchFrom(params)
 884                     ? TEMPLATE_INFO_PLIST_LEGACY
 885                     : TEMPLATE_INFO_PLIST_LITE,
 886                 data, VERBOSE.fetchFrom(params),
 887                 DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
 888         w.close();
 889 
 890     }
 891 
 892     private void writePkgInfo(File file) throws IOException {
 893 
 894         //hardcoded as it does not seem we need to change it ever
 895         String signature = "????";
 896 
 897         try (Writer out = new BufferedWriter(new FileWriter(file))) {
 898             out.write(OS_TYPE_CODE + signature);
 899             out.flush();
 900         }
 901     }
 902 
 903     public static Rule[] createMacRuntimeRules(Map<String, ? super Object> params) {
 904         if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
 905             // we will never get a sensible answer unless we are running on OSX,
 906             // so quit now and return null indicating 'no sensible value'
 907             return null;
 908         }
 909 
 910         //Subsetting of JRE is restricted.
 911         //JRE README defines what is allowed to strip:
 912         //   ´╗┐http://www.oracle.com/technetwork/java/javase/jre-8-readme-2095710.html
 913         //
 914 
 915         List<Rule> rules = new ArrayList<>();
 916 
 917         File baseDir;
 918 
 919         if (params.containsKey(MAC_RUNTIME.getID())) {
 920             Object o = params.get(MAC_RUNTIME.getID());
 921             if (o instanceof RelativeFileSet) {
 922                 baseDir = ((RelativeFileSet)o).getBaseDirectory();
 923             } else {
 924                 baseDir = new File(o.toString());
 925             }
 926         } else {
 927             baseDir = new File(System.getProperty("java.home"));
 928         }
 929 
 930         // we accept either pointing at the directories typically installed at:
 931         // /Libraries/Java/JavaVirtualMachine/jdk1.8.0_40/
 932         //   * .
 933         //   * Contents/Home
 934         //   * Contents/Home/jre
 935         // /Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/
 936         //   * .
 937         //   * /Contents/Home
 938         // version may change, and if we don't detect any Contents/Home or Contents/Home/jre we will
 939         // presume we are at a root.
 940 
 941         if (!baseDir.exists()) {
 942             throw new RuntimeException(I18N.getString("error.non-existent-runtime"),
 943                 new ConfigException(I18N.getString("error.non-existent-runtime"),
 944                     I18N.getString("error.non-existent-runtime.advice")));
 945         }
 946 
 947         boolean isJRE;
 948         boolean isJDK;
 949 
 950         try {
 951             String path = baseDir.getCanonicalPath();
 952             if (path.endsWith("/Contents/Home/jre")) {
 953                 baseDir = baseDir.getParentFile().getParentFile().getParentFile();
 954             } else if (path.endsWith("/Contents/Home")) {
 955                 baseDir = baseDir.getParentFile().getParentFile();
 956             }
 957 
 958             isJRE = new File(baseDir, "Contents/Home/lib/jli/libjli.dylib").exists();
 959             isJDK = new File(baseDir, "Contents/Home/jre/lib/jli/libjli.dylib").exists();
 960 
 961         } catch (IOException e) {
 962             throw new RuntimeException(e);
 963         }
 964 
 965         if (!(isJRE || isJDK)) {
 966             throw new RuntimeException(I18N.getString("error.cannot-detect-runtime-in-directory"),
 967                     new ConfigException(I18N.getString("error.cannot-detect-runtime-in-directory"),
 968                             I18N.getString("error.cannot-detect-runtime-in-directory.advice")));
 969         }
 970 
 971         // we need the Info.plist for signing
 972         rules.add(Rule.suffix("/contents/info.plist"));
 973         
 974         // Strip some JRE specific stuff
 975         if (isJRE) {
 976             rules.add(Rule.suffixNeg("/contents/disabled.plist"));
 977             rules.add(Rule.suffixNeg("/contents/enabled.plist"));
 978             rules.add(Rule.substrNeg("/contents/frameworks/"));
 979         }
 980         
 981         // strip out command line tools
 982         rules.add(Rule.suffixNeg("home/bin"));
 983         if (isJDK) {
 984             rules.add(Rule.suffixNeg("home/jre/bin"));
 985         }
 986 
 987         // strip out JRE stuff
 988         if (isJRE) {
 989             // update helper
 990             rules.add(Rule.suffixNeg("resources"));
 991             // interfacebuilder files
 992             rules.add(Rule.suffixNeg("lib/nibs"));
 993             // browser integration
 994             rules.add(Rule.suffixNeg("lib/libnpjp2.dylib"));
 995             // java webstart
 996             rules.add(Rule.suffixNeg("lib/security/javaws.policy"));
 997             rules.add(Rule.suffixNeg("lib/shortcuts"));
 998 
 999             // general deploy libraries
1000             rules.add(Rule.suffixNeg("lib/deploy"));
1001             rules.add(Rule.suffixNeg("lib/deploy.jar"));
1002             rules.add(Rule.suffixNeg("lib/javaws.jar"));
1003             rules.add(Rule.suffixNeg("lib/libdeploy.dylib"));
1004             rules.add(Rule.suffixNeg("lib/plugin.jar"));
1005         }
1006 
1007         // strip out man pages
1008         rules.add(Rule.suffixNeg("home/man"));
1009 
1010         // this is the build hashes, strip or keep?
1011         //rules.add(Rule.suffixNeg("home/release"));
1012 
1013         // strip out JDK stuff like JavaDB, JNI Headers, etc
1014         if (isJDK) {
1015             rules.add(Rule.suffixNeg("home/db"));
1016             rules.add(Rule.suffixNeg("home/demo"));
1017             rules.add(Rule.suffixNeg("home/include"));
1018             rules.add(Rule.suffixNeg("home/lib"));
1019             rules.add(Rule.suffixNeg("home/sample"));
1020             rules.add(Rule.suffixNeg("home/src.zip"));
1021             rules.add(Rule.suffixNeg("home/javafx-src.zip"));
1022         }
1023 
1024         //"home/rt" is not part of the official builds
1025         // but we may be creating this symlink to make older NB projects
1026         // happy. Make sure to not include it into final artifact
1027         rules.add(Rule.suffixNeg("home/rt"));
1028 
1029         //rules.add(Rule.suffixNeg("jre/lib/ext")); //need some of jars there for https to work
1030 
1031         // strip out flight recorder
1032         rules.add(Rule.suffixNeg("lib/jfr.jar"));
1033 
1034         return rules.toArray(new Rule[rules.size()]);
1035     }
1036 
1037     //////////////////////////////////////////////////////////////////////////////////
1038     // Implement Bundler
1039     //////////////////////////////////////////////////////////////////////////////////
1040 
1041     @Override
1042     public String getName() {
1043         return I18N.getString("bundler.name");
1044     }
1045 
1046     @Override
1047     public String getDescription() {
1048         return I18N.getString("bundler.description");
1049     }
1050 
1051     @Override
1052     public String getID() {
1053         return "mac.app";
1054     }
1055 
1056     @Override
1057     public String getBundleType() {
1058         return "IMAGE";
1059     }
1060 
1061     @Override
1062     public Collection<BundlerParamInfo<?>> getBundleParameters() {
1063         return getAppBundleParameters();
1064     }
1065 
1066     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
1067         return Arrays.asList(
1068                 APP_NAME,
1069                 APP_RESOURCES,
1070                 // APP_RESOURCES_LIST, // ??
1071                 ARGUMENTS,
1072                 BUNDLE_ID_SIGNING_PREFIX,
1073                 CLASSPATH,
1074                 DEVELOPER_ID_APP_SIGNING_KEY,
1075                 ICON_ICNS,
1076                 JVM_OPTIONS,
1077                 JVM_PROPERTIES,
1078                 MAC_CATEGORY,
1079                 MAC_CF_BUNDLE_IDENTIFIER,
1080                 MAC_CF_BUNDLE_NAME,
1081                 MAC_CF_BUNDLE_VERSION,
1082                 MAC_RUNTIME,
1083                 MAIN_CLASS,
1084                 MAIN_JAR,
1085                 PREFERENCES_ID,
1086                 PRELOADER_CLASS,
1087                 SIGNING_KEYCHAIN,
1088                 USER_JVM_OPTIONS,
1089                 VERSION
1090         );
1091     }
1092 
1093 
1094     @Override
1095     public File execute(Map<String, ? super Object> params, File outputParentDir) {
1096         return doBundle(params, outputParentDir, false);
1097     }
1098 
1099     private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException {
1100         prepareConfigFiles(p);
1101 
1102         if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
1103             writeCfgFile(p, rootDirectory);
1104         } else {
1105             writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime");
1106         }
1107 
1108         // Copy executable root folder
1109         File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p));
1110         IOUtils.copyFromURL(
1111                 RAW_EXECUTABLE_URL.fetchFrom(p),
1112                 executableFile);
1113         executableFile.setExecutable(true, false);
1114 
1115     }
1116 
1117     public static String getLauncherCfgName(Map<String, ? super Object> p) {
1118         return "Contents/Java/" + APP_NAME.fetchFrom(p) +".cfg";
1119     }
1120 
1121     private void writeCfgFile(Map<String, ? super Object> params, File rootDir) throws FileNotFoundException {
1122         File pkgInfoFile = new File(rootDir, getLauncherCfgName(params));
1123 
1124         pkgInfoFile.delete();
1125 
1126         PrintStream out = new PrintStream(pkgInfoFile);
1127         if (MAC_RUNTIME.fetchFrom(params) == null) {
1128             out.println("app.runtime=");
1129         } else {
1130             out.println("app.runtime=$APPDIR/PlugIns/Java.runtime");
1131         }
1132         out.println("app.mainjar=" + MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
1133         out.println("app.version=" + VERSION.fetchFrom(params));
1134         //for future AU support (to be able to find app in the registry)
1135         out.println("app.id=" + IDENTIFIER.fetchFrom(params));
1136         out.println("app.preferences.id=" + PREFERENCES_ID.fetchFrom(params));
1137         out.println("app.identifier=" + IDENTIFIER.fetchFrom(params));
1138 
1139         out.println("app.mainclass=" +
1140                 MAIN_CLASS.fetchFrom(params).replaceAll("\\.", "/"));
1141         out.println("app.classpath=" + CLASSPATH.fetchFrom(params));
1142 
1143         List<String> jvmargs = JVM_OPTIONS.fetchFrom(params);
1144         int idx = 1;
1145         for (String a : jvmargs) {
1146             out.println("jvmarg."+idx+"="+a);
1147             idx++;
1148         }
1149         Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
1150         for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
1151             out.println("jvmarg."+idx+"=-D"+entry.getKey()+"="+entry.getValue());
1152             idx++;
1153         }
1154 
1155         String preloader = PRELOADER_CLASS.fetchFrom(params);
1156         if (preloader != null) {
1157             out.println("jvmarg."+idx+"=-Djavafx.preloader="+preloader);
1158         }
1159 
1160         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
1161         idx = 1;
1162         for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
1163             if (arg.getKey() == null || arg.getValue() == null) {
1164                 Log.info(I18N.getString("message.jvm-user-arg-is-null"));
1165             }
1166             else {
1167                 out.println("jvmuserarg."+idx+".name="+arg.getKey());
1168                 out.println("jvmuserarg."+idx+".value="+arg.getValue());
1169             }
1170             idx++;
1171         }
1172 
1173         // add command line args
1174         List<String> args = ARGUMENTS.fetchFrom(params);
1175         idx = 1;
1176         for (String a : args) {
1177             out.println("arg."+idx+"="+a);
1178             idx++;
1179         }
1180 
1181         out.close();
1182     }
1183     @Override
1184     public void extractRuntimeFlags(Map<String, ? super Object> params) {
1185         if (params.containsKey(".runtime.autodetect")) return;
1186 
1187         params.put(".runtime.autodetect", "attempted");
1188         RelativeFileSet runtime = MAC_RUNTIME.fetchFrom(params);
1189         String commandline;
1190         if (runtime == null) {
1191             //System JRE, report nothing useful
1192             params.put(".runtime.autodetect", "systemjre");
1193         } else {
1194             File workingBase = runtime.getBaseDirectory();
1195             if (workingBase.getName().equals("jre")) {
1196                 workingBase = workingBase.getParentFile();
1197             }
1198             if (workingBase.getName().equals("Home")) {
1199                 workingBase = workingBase.getParentFile();
1200             }
1201             if (workingBase.getName().equals("Contents")) {
1202                 workingBase = workingBase.getParentFile();
1203             }
1204 
1205 
1206             try {
1207                 byte[] infoPlistBytes = Files.readAllBytes(workingBase.toPath().resolve(Paths.get("Contents", "Info.plist")));
1208                 String infoPlist = new String(infoPlistBytes);
1209 
1210                 Pattern cfBundleVersionMatcher = Pattern.compile("<key>CFBundleVersion</key>\\s*<string>([^<]+)</string>");
1211                 Matcher m = cfBundleVersionMatcher.matcher(infoPlist);
1212                 if (m.find()) {
1213                     AbstractImageBundler.extractFlagsFromVersion(params, "java version \"" + m.group(1) + "\"\n");
1214                     params.put(".runtime.autodetect", "succeeded");
1215                 } else {
1216                     params.put(".runtime.autodetect", "failed");
1217                 }
1218             } catch (IOException e) {
1219                 e.printStackTrace();
1220                 params.put(".runtime.autodetect", "failed");
1221             }
1222         }
1223     }
1224 }