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