modules/fxpackager/src/main/java/com/sun/javafx/tools/packager/bundlers/MacAppBundler.java

Print this page




   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.sun.javafx.tools.packager.bundlers;
  26 
  27 import com.oracle.bundlers.*;

  28 import com.sun.javafx.tools.packager.Log;
  29 import com.sun.javafx.tools.resource.mac.MacResources;
  30 
  31 import java.io.*;
  32 import java.net.MalformedURLException;
  33 import java.net.URL;

  34 import java.util.*;
  35 
  36 import static com.oracle.bundlers.StandardBundlerParam.*;
  37 import static com.oracle.bundlers.StandardBundlerParam.USER_JVM_OPTIONS;
  38 import static com.oracle.bundlers.StandardBundlerParam.VERSION;
  39 import static com.oracle.bundlers.mac.MacBaseInstallerBundler.getPredefinedImage;
  40 
  41 public class MacAppBundler extends AbstractBundler {
  42     private File configRoot = null;
  43     private Map<String, ? super Object> params;

  44 
  45     public final static String MAC_BUNDLER_PREFIX =
  46             BUNDLER_PREFIX + "macosx" + File.separator;
  47 
  48     private static final String EXECUTABLE_NAME      = "JavaAppLauncher";
  49     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  50     private static final String OS_TYPE_CODE         = "APPL";
  51     private static final String TEMPLATE_INFO_PLIST  = "Info.plist.template";
  52 
  53     private static Map<String, String> getMacCategories() {
  54         Map<String, String> map = new HashMap<>();
  55         map.put("Business", "public.app-category.business");
  56         map.put("Developer Tools", "public.app-category.developer-tools");
  57         map.put("Education", "public.app-category.education");
  58         map.put("Entertainment", "public.app-category.entertainment");
  59         map.put("Finance", "public.app-category.finance");
  60         map.put("Games", "public.app-category.games");
  61         map.put("Graphics & Design", "public.app-category.graphics-design");
  62         map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
  63         map.put("Lifestyle", "public.app-category.lifestyle");


  83         map.put("Dice Games", "public.app-category.dice-games");
  84         map.put("Educational Games", "public.app-category.educational-games");
  85         map.put("Family Games", "public.app-category.family-games");
  86         map.put("Kids Games", "public.app-category.kids-games");
  87         map.put("Music Games", "public.app-category.music-games");
  88         map.put("Puzzle Games", "public.app-category.puzzle-games");
  89         map.put("Racing Games", "public.app-category.racing-games");
  90         map.put("Role Playing Games", "public.app-category.role-playing-games");
  91         map.put("Simulation Games", "public.app-category.simulation-games");
  92         map.put("Sports Games", "public.app-category.sports-games");
  93         map.put("Strategy Games", "public.app-category.strategy-games");
  94         map.put("Trivia Games", "public.app-category.trivia-games");
  95         map.put("Word Games", "public.app-category.word-games");
  96 
  97         return map;
  98     }
  99 
 100     public static final EnumeratedBundlerParam<String> MAC_CATEGORY  =
 101             new EnumeratedBundlerParam<>(
 102                     "Category",
 103                     "Mac Categories. Note that the key is the string to display to the user and the value is the id of the category",
 104                     "LSApplicationCategoryType",
 105                     String.class,
 106                     null,
 107                     params -> "Unknown",
 108                     false,
 109                     s -> s,
 110                     getMacCategories(),
 111                     false //strict - for MacStoreBundler this should be strict
 112             );
 113 

























 114     public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>(
 115             "Launcher URL", "Override the packager default launcher with a custom launcher.", "mac.launcher.url",
 116             URL.class, null, params -> MacResources.class.getResource(EXECUTABLE_NAME),
 117             false, s -> {





 118         try {
 119             return new URL(s);
 120         } catch (MalformedURLException e) {
 121             Log.info(e.toString());
 122             return null;
 123         }
 124     });
 125 
 126     private void setBuildRoot(File dir) {
 127         configRoot = new File(dir, "macosx");
 128         configRoot.mkdirs();



















































































 129     }
 130 
 131     public MacAppBundler() {
 132         super();
 133         baseResourceLoader = MacResources.class;
 134     }
 135 
 136     @Override
 137     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {

 138         logParameters(params);
 139         return doValidate(params);



 140     }
 141 
 142     //to be used by chained bundlers, e.g. by EXE bundler to avoid
 143     // skipping validation if p.type does not include "image"
 144     public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 145         if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
 146             throw new UnsupportedPlatformException();
 147         }
 148 
 149         if (getPredefinedImage(p) != null) {
 150             return true;
 151         }
 152 
 153         if (StandardBundlerParam.MAIN_JAR.fetchFrom(p) == null) {
 154             throw new ConfigException(
 155                     "Main application jar is missing.",
 156                     "Make sure to use fx:jar task to create main application jar.");
 157         }
 158 
 159         //validate required inputs
 160         if (USE_FX_PACKAGING.fetchFrom(p)) {
 161             testRuntime(p, new String[] {"Contents/Home/jre/lib/ext/jfxrt.jar", "Contents/Home/jre/lib/jfxrt.jar"});
 162         }
 163 
 164         return true;
 165     }
 166 
 167 
 168     private File getConfig_InfoPlist() {
 169         return new File(configRoot, "Info.plist");
 170     }
 171 
 172     private File getConfig_Icon() {
 173         return new File(configRoot, NAME.fetchFrom(params) + ".icns");
 174     }
 175 
 176     private void prepareConfigFiles() throws IOException {
 177         File infoPlistFile = getConfig_InfoPlist();
 178         infoPlistFile.createNewFile();
 179         writeInfoPlist(infoPlistFile);
 180 
 181         // Copy icon to Resources folder
 182         prepareIcon();
 183     }
 184 
 185     public File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
 186         File rootDirectory = null;
 187         try {
 188             final File predefinedImage = getPredefinedImage(p);
 189             if (predefinedImage != null) {
 190                 return predefinedImage;
 191             }
 192             params = p;
 193 
 194             File file = BUILD_ROOT.fetchFrom(p);
 195             setBuildRoot(file);
 196 
 197             //prepare config resources (we will copy them to the bundle later)
 198             // NB: explicitly saving them to simplify customization
 199             prepareConfigFiles();
 200 
 201             // Create directory structure
 202             rootDirectory = new File(outputDirectory, NAME.fetchFrom(p) + ".app");
 203             IOUtils.deleteRecursive(rootDirectory);
 204             rootDirectory.mkdirs();
 205 
 206             if (!dependentTask) {
 207                 Log.info("Creating app bundle: " + rootDirectory.getAbsolutePath());
 208             }
 209 
 210             File contentsDirectory = new File(rootDirectory, "Contents");
 211             contentsDirectory.mkdirs();
 212 
 213             File macOSDirectory = new File(contentsDirectory, "MacOS");
 214             macOSDirectory.mkdirs();
 215 
 216             File javaDirectory = new File(contentsDirectory, "Java");
 217             javaDirectory.mkdirs();
 218 
 219             File plugInsDirectory = new File(contentsDirectory, "PlugIns");
 220 
 221             File resourcesDirectory = new File(contentsDirectory, "Resources");
 222             resourcesDirectory.mkdirs();
 223 
 224             // Generate PkgInfo
 225             File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
 226             pkgInfoFile.createNewFile();
 227             writePkgInfo(pkgInfoFile);
 228 
 229             // Copy executable to MacOS folder
 230             File executableFile = new File(macOSDirectory, getLauncherName());
 231             IOUtils.copyFromURL(
 232                     RAW_EXECUTABLE_URL.fetchFrom(p),
 233                     executableFile);
 234 
 235             executableFile.setExecutable(true, false);
 236 
 237             // Copy runtime to PlugIns folder
 238             copyRuntime(plugInsDirectory);
 239 
 240             // Copy class path entries to Java folder
 241             copyClassPathEntries(javaDirectory);
 242 
 243 //TODO: Need to support adding native libraries.
 244             // Copy library path entries to MacOS folder
 245             //copyLibraryPathEntries(macOSDirectory);
 246 
 247             /*********** Take care of "config" files *******/
 248             // Copy icon to Resources folder
 249             IOUtils.copyFile(getConfig_Icon(),
 250                     new File(resourcesDirectory, getConfig_Icon().getName()));
 251             // Generate Info.plist
 252             IOUtils.copyFile(getConfig_InfoPlist(),
 253                     new File(contentsDirectory, "Info.plist"));
 254         } catch (ConfigException e) {
 255             Log.info("Bundler " + getName() + " skipped because of a configuration problem: " + e.getMessage() + "\nAdvice to fix: " + e.getAdvice());
 256         } catch (IOException ex) {
 257             Log.verbose(ex);
 258             return null;
 259         } finally {
 260             if (!verbose) {
 261                 //cleanup
 262                 cleanupConfigFiles();
 263             } else {
 264                 Log.info("Config files are saved to " +
 265                         configRoot.getAbsolutePath()  +
 266                         ". Use them to customize package.");
 267             }
 268         }
 269         return rootDirectory;
 270     }
 271 
 272     public String getAppName() {
 273         return  NAME.fetchFrom(params) + ".app";
 274     }
 275 
 276     protected void cleanupConfigFiles() {
 277         //Since building the app can be bypassed, make sure configRoot was set
 278         if (configRoot != null) {
 279             if (getConfig_Icon() != null) {
 280                 getConfig_Icon().delete();
 281             }
 282             if (getConfig_InfoPlist() != null) {
 283                 getConfig_InfoPlist().delete();
 284             }
 285         }
 286     }
 287 
 288 
 289     private void copyClassPathEntries(File javaDirectory) throws IOException {
 290         RelativeFileSet classPath = APP_RESOURCES.fetchFrom(params);
 291         if (classPath == null) {
 292             throw new RuntimeException("Null app resources?");
 293         }
 294         File srcdir = classPath.getBaseDirectory();
 295         for (String fname : classPath.getIncludedFiles()) {
 296             IOUtils.copyFile(
 297                     new File(srcdir, fname), new File(javaDirectory, fname));
 298         }
 299     }
 300 
 301     private void copyRuntime(File plugInsDirectory) throws IOException {
 302         RelativeFileSet runTime = RUNTIME.fetchFrom(params);
 303         if (runTime == null) {
 304             //request to use system runtime => do not bundle
 305             return;
 306         }
 307         plugInsDirectory.mkdirs();
 308 
 309         File srcdir = runTime.getBaseDirectory();
 310         File destDir = new File(plugInsDirectory, srcdir.getName());
 311         Set<String> filesToCopy = runTime.getIncludedFiles();
 312 
 313         // We don't need the symlink to libjli or the JRE's info.plist.
 314         // We are going to load it directly.
 315         filesToCopy.remove("Contents/MacOS/libjli.dylib");
 316         filesToCopy.remove("Contents/Info.plist");
 317         for (String fname : filesToCopy) {
 318             IOUtils.copyFile(
 319                     new File(srcdir, fname), new File(destDir, fname));
 320         }
 321     }
 322 
 323 
 324     // get Name from bundle params
 325     private String NAME() {
 326         return NAME.fetchFrom(params);
 327     }
 328 
 329     private void prepareIcon() throws IOException {
 330         File icon = ICON.fetchFrom(params);
 331         if (icon == null || !icon.exists()) {
 332             fetchResource(MAC_BUNDLER_PREFIX+ NAME() +".icns",
 333                     "icon",
 334                     TEMPLATE_BUNDLE_ICON,
 335                     getConfig_Icon());

 336         } else {
 337             fetchResource(MAC_BUNDLER_PREFIX+ NAME() +".icns",
 338                     "icon",
 339                     icon,
 340                     getConfig_Icon());

 341         }
 342     }
 343 
 344     private String getLauncherName() {
 345         if (NAME() != null) {
 346             return NAME();
 347         } else {
 348             return MAIN_CLASS.fetchFrom(params);
 349         }
 350     }
 351 
 352     private String getBundleName() {
 353         //TODO: Check to see what rules/limits are in place for CFBundleName
 354         if (NAME() != null) {
 355             return NAME();






 356         } else {
 357             String nm = MAIN_CLASS.fetchFrom(params);
 358             if (nm.length() > 16) {
 359                 nm = nm.substring(0, 16);
 360             }
 361             return nm;
 362         }
 363     }
 364 
 365     private String getBundleIdentifier() {
 366         //TODO: Check to see what rules/limits are in place for CFBundleIdentifier
 367         return  IDENTIFIER.fetchFrom(params);
 368     }
 369 
 370     private void writeInfoPlist(File file) throws IOException {
 371         Log.verbose("Preparing Info.plist: "+file.getAbsolutePath());
 372 
 373         //prepare config for exe
 374         //Note: do not need CFBundleDisplayName if we do not support localization
 375         Map<String, String> data = new HashMap<>();
 376         data.put("DEPLOY_ICON_FILE", getConfig_Icon().getName());
 377         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 378                 getBundleIdentifier().toLowerCase());
 379         data.put("DEPLOY_BUNDLE_NAME",
 380                 getBundleName());
 381         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 382                 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
 383         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName());
 384         if (RUNTIME.fetchFrom(params) != null) {
 385             data.put("DEPLOY_JAVA_RUNTIME_NAME",
 386                     RUNTIME.fetchFrom(params).getBaseDirectory().getName());
 387         } else {
 388             data.put("DEPLOY_JAVA_RUNTIME_NAME", "");
 389         }
 390         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 391                 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
 392         data.put("DEPLOY_BUNDLE_CATEGORY",
 393                 //TODO parameters should provide set of values for IDEs
 394                 CATEGORY.fetchFrom(params) != null ?
 395                         CATEGORY.fetchFrom(params) : "unknown");
 396 
 397         //TODO NOT THE WAY TODO THIS but good enough for first pass
 398         data.put("DEPLOY_MAIN_JAR_NAME", new BundleParams(params).getMainApplicationJar());
 399 //        data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).toString());
 400 
 401         data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
 402 
 403         StringBuilder sb = new StringBuilder();
 404         List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
 405 
 406         String newline = ""; //So we don't add unneccessary extra line after last append
 407         for (String o : jvmOptions) {
 408             sb.append(newline).append("    <string>").append(o).append("</string>");
 409             newline = "\n";
 410         }
 411         data.put("DEPLOY_JVM_OPTIONS", sb.toString());
 412 
 413         newline = "";
 414         sb = new StringBuilder();
 415         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);


 418             sb.append("      <key>").append(arg.getKey()).append("</key>\n");
 419             sb.append("      <string>").append(arg.getValue()).append("</string>");
 420             newline = "\n";
 421         }
 422         data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
 423 
 424 
 425         //TODO UNLESS we are supporting building for jre7, this is unnecessary
 426 //        if (params.useJavaFXPackaging()) {
 427 //            data.put("DEPLOY_LAUNCHER_CLASS", JAVAFX_LAUNCHER_CLASS);
 428 //        } else {
 429         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 430 //        }
 431         // This will be an empty string for correctly packaged JavaFX apps
 432         data.put("DEPLOY_APP_CLASSPATH", MAIN_JAR_CLASSPATH.fetchFrom(params));
 433 
 434         //TODO: Add remainder of the classpath
 435 
 436         Writer w = new BufferedWriter(new FileWriter(file));
 437         w.write(preprocessTextResource(
 438                 MAC_BUNDLER_PREFIX + getConfig_InfoPlist().getName(),
 439                 "Bundle config file", TEMPLATE_INFO_PLIST, data));

 440         w.close();
 441 
 442     }
 443 
 444     private void writePkgInfo(File file) throws IOException {
 445 
 446         //hardcoded as it does not seem we need to change it ever
 447         String signature = "????";
 448 
 449         try (Writer out = new BufferedWriter(new FileWriter(file))) {
 450             out.write(OS_TYPE_CODE + signature);
 451             out.flush();
 452         }
 453     }
 454 
 455     //////////////////////////////////////////////////////////////////////////////////
 456     // Implement Bundler
 457     //////////////////////////////////////////////////////////////////////////////////
 458 
 459     @Override


 475     public BundleType getBundleType() {
 476         return BundleType.IMAGE;
 477     }
 478 
 479     @Override
 480     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 481         return getAppBundleParameters();
 482     }
 483 
 484     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
 485         return Arrays.asList(
 486                 APP_NAME,
 487                 APP_RESOURCES,
 488                 BUILD_ROOT,
 489                 JVM_OPTIONS,
 490                 MAIN_CLASS,
 491                 MAIN_JAR,
 492                 MAIN_JAR_CLASSPATH,
 493                 PREFERENCES_ID,
 494                 RAW_EXECUTABLE_URL,
 495                 RUNTIME,
 496                 USE_FX_PACKAGING,
 497                 USER_JVM_OPTIONS,
 498                 VERSION,
 499                 ICON,
 500                 MAC_CATEGORY
 501         );
 502     }
 503 
 504 
 505     @Override
 506     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 507         return doBundle(params, outputParentDir, false);
 508     }
 509 }


   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.sun.javafx.tools.packager.bundlers;
  26 
  27 import com.oracle.bundlers.*;
  28 import com.oracle.bundlers.JreUtils.Rule;
  29 import com.sun.javafx.tools.packager.Log;
  30 import com.sun.javafx.tools.resource.mac.MacResources;
  31 
  32 import java.io.*;
  33 import java.net.MalformedURLException;
  34 import java.net.URL;
  35 import java.text.MessageFormat;
  36 import java.util.*;
  37 
  38 import static com.oracle.bundlers.StandardBundlerParam.*;


  39 import static com.oracle.bundlers.mac.MacBaseInstallerBundler.getPredefinedImage;
  40 
  41 public class MacAppBundler extends AbstractBundler {
  42 
  43     private static final ResourceBundle I18N =
  44             ResourceBundle.getBundle("com.oracle.bundlers.mac.MacAppBundler");
  45 
  46     public final static String MAC_BUNDLER_PREFIX =
  47             BUNDLER_PREFIX + "macosx" + File.separator;
  48 
  49     private static final String EXECUTABLE_NAME      = "JavaAppLauncher";
  50     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  51     private static final String OS_TYPE_CODE         = "APPL";
  52     private static final String TEMPLATE_INFO_PLIST  = "Info.plist.template";
  53 
  54     private static Map<String, String> getMacCategories() {
  55         Map<String, String> map = new HashMap<>();
  56         map.put("Business", "public.app-category.business");
  57         map.put("Developer Tools", "public.app-category.developer-tools");
  58         map.put("Education", "public.app-category.education");
  59         map.put("Entertainment", "public.app-category.entertainment");
  60         map.put("Finance", "public.app-category.finance");
  61         map.put("Games", "public.app-category.games");
  62         map.put("Graphics & Design", "public.app-category.graphics-design");
  63         map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
  64         map.put("Lifestyle", "public.app-category.lifestyle");


  84         map.put("Dice Games", "public.app-category.dice-games");
  85         map.put("Educational Games", "public.app-category.educational-games");
  86         map.put("Family Games", "public.app-category.family-games");
  87         map.put("Kids Games", "public.app-category.kids-games");
  88         map.put("Music Games", "public.app-category.music-games");
  89         map.put("Puzzle Games", "public.app-category.puzzle-games");
  90         map.put("Racing Games", "public.app-category.racing-games");
  91         map.put("Role Playing Games", "public.app-category.role-playing-games");
  92         map.put("Simulation Games", "public.app-category.simulation-games");
  93         map.put("Sports Games", "public.app-category.sports-games");
  94         map.put("Strategy Games", "public.app-category.strategy-games");
  95         map.put("Trivia Games", "public.app-category.trivia-games");
  96         map.put("Word Games", "public.app-category.word-games");
  97 
  98         return map;
  99     }
 100 
 101     public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
 102             new EnumeratedBundlerParam<>(
 103                     "Category",
 104                     "Mac App Store Categories. Note that the key is the string to display to the user and the value is the id of the category",
 105                     "mac.category",
 106                     String.class,
 107                     new String[] {CATEGORY.getID()},
 108                     params -> "Unknown",
 109                     false,
 110                     (s, p) -> s,
 111                     getMacCategories(),
 112                     false //strict - for MacStoreBundler this should be strict
 113             );
 114 
 115     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
 116             new StandardBundlerParam<>(
 117                     "CFBundleName",
 118                     "The name of the app as it appears in the Menu Bar.  This can be different from the application name.  This name should be less than 16 characters long and be suitable for displaying in the menu bar and the app’s Info window.",
 119                     "mac.CFBundleName",
 120                     String.class,
 121                     null,
 122                     params -> null,
 123                     false,
 124                     (s, p) -> s);
 125 
 126     public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
 127             I18N.getString("param.config-root.name"),
 128             I18N.getString("param.config-root.description"),
 129             "configRoot",
 130             File.class,
 131             null,
 132             params -> {
 133                 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
 134                 configRoot.mkdirs();
 135                 return configRoot;
 136             },
 137             false,
 138             (s, p) -> new File(s));
 139 
 140     public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>(
 141             "Launcher URL",
 142             "Override the packager default launcher with a custom launcher.",
 143             "mac.launcher.url",
 144             URL.class,
 145             null,
 146             params -> MacResources.class.getResource(EXECUTABLE_NAME),
 147             false,
 148             (s, p) -> {
 149                 try {
 150                     return new URL(s);
 151                 } catch (MalformedURLException e) {
 152                     Log.info(e.toString());
 153                     return null;
 154                 }
 155             });
 156 
 157     public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
 158             "Default Icon",
 159             "The Default Icon for when a user does not specify an icns file.",
 160             ".mac.default.icns",
 161             String.class,
 162             null,
 163             params -> TEMPLATE_BUNDLE_ICON,
 164             false,
 165             (s, p) -> s);
 166 
 167     //Subsetting of JRE is restricted.
 168     //JRE README defines what is allowed to strip:
 169     //   http://www.oracle.com/technetwork/java/javase/jre-7-readme-430162.html //TODO update when 8 goes GA
 170     //
 171     public static final BundlerParamInfo<Rule[]> MAC_JDK_RULES = new StandardBundlerParam<>(
 172             "",
 173             "",
 174             ".mac-jdk.runtime.rules",
 175             Rule[].class,
 176             null,
 177             params -> new Rule[]{
 178                     Rule.suffixNeg("macos/libjli.dylib"),
 179                     Rule.suffixNeg("resources"),
 180                     Rule.suffixNeg("home/bin"),
 181                     Rule.suffixNeg("home/db"),
 182                     Rule.suffixNeg("home/demo"),
 183                     Rule.suffixNeg("home/include"),
 184                     Rule.suffixNeg("home/lib"),
 185                     Rule.suffixNeg("home/man"),
 186                     Rule.suffixNeg("home/release"),
 187                     Rule.suffixNeg("home/sample"),
 188                     Rule.suffixNeg("home/src.zip"),
 189                     //"home/rt" is not part of the official builds
 190                     // but we may be creating this symlink to make older NB projects
 191                     // happy. Make sure to not include it into final artifact
 192                     Rule.suffixNeg("home/rt"),
 193                     Rule.suffixNeg("jre/bin"),
 194                     Rule.suffixNeg("jre/bin/rmiregistry"),
 195                     Rule.suffixNeg("jre/bin/tnameserv"),
 196                     Rule.suffixNeg("jre/bin/keytool"),
 197                     Rule.suffixNeg("jre/bin/klist"),
 198                     Rule.suffixNeg("jre/bin/ktab"),
 199                     Rule.suffixNeg("jre/bin/policytool"),
 200                     Rule.suffixNeg("jre/bin/orbd"),
 201                     Rule.suffixNeg("jre/bin/servertool"),
 202                     Rule.suffixNeg("jre/bin/javaws"),
 203                     Rule.suffixNeg("jre/bin/java"),
 204                     //Rule.suffixNeg("jre/lib/ext"), //need some of jars there for https to work
 205                     Rule.suffixNeg("jre/lib/nibs"),
 206                     //keep core deploy APIs but strip plugin dll
 207                     //Rule.suffixNeg("jre/lib/deploy"),
 208                     //Rule.suffixNeg("jre/lib/deploy.jar"),
 209                     //Rule.suffixNeg("jre/lib/javaws.jar"),
 210                     //Rule.suffixNeg("jre/lib/libdeploy.dylib"),
 211                     //Rule.suffixNeg("jre/lib/plugin.jar"),
 212                     Rule.suffixNeg("jre/lib/libnpjp2.dylib"),
 213                     Rule.suffixNeg("jre/lib/security/javaws.policy"),
 214                     Rule.substrNeg("Contents/Info.plist")
 215             },
 216             false,
 217             (s, p) -> null
 218     );
 219 
 220     public static final BundlerParamInfo<RelativeFileSet> MAC_RUNTIME = new StandardBundlerParam<>(
 221             RUNTIME.getName(),
 222             RUNTIME.getDescription(),
 223             RUNTIME.getID(),
 224             RelativeFileSet.class,
 225             null,
 226             params -> extractMacRuntime(System.getProperty("java.home"), params),
 227             false,
 228             MacAppBundler::extractMacRuntime
 229     );
 230 
 231     public static RelativeFileSet extractMacRuntime(String base, Map<String, ? super Object> params) {
 232         if (base.endsWith("/Home")) {
 233             throw new IllegalArgumentException("Currently Macs require a JDK to package");
 234         } else if (base.endsWith("/Home/jre")) {
 235             File baseDir = new File(base).getParentFile().getParentFile().getParentFile();
 236             return JreUtils.extractJreAsRelativeFileSet(baseDir.toString(),
 237                     MAC_JDK_RULES.fetchFrom(params));
 238         } else {
 239             // for now presume we are pointed to the top of a JDK
 240             return JreUtils.extractJreAsRelativeFileSet(base,
 241                     MAC_JDK_RULES.fetchFrom(params));
 242         }
 243     }
 244 
 245     public MacAppBundler() {
 246         super();
 247         baseResourceLoader = MacResources.class;
 248     }
 249 
 250     @Override
 251     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
 252         try {
 253             logParameters(params);
 254             return doValidate(params);
 255         } catch (RuntimeException re) {
 256             throw new ConfigException(re);
 257         }
 258     }
 259 
 260     //to be used by chained bundlers, e.g. by EXE bundler to avoid
 261     // skipping validation if p.type does not include "image"
 262     public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 263         if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
 264             throw new UnsupportedPlatformException();
 265         }
 266 
 267         if (getPredefinedImage(p) != null) {
 268             return true;
 269         }
 270 
 271         if (MAIN_JAR.fetchFrom(p) == null) {
 272             throw new ConfigException(
 273                     "Main application jar is missing.",
 274                     "Make sure to use fx:jar task to create main application jar.");
 275         }
 276 
 277         //validate required inputs
 278         if (USE_FX_PACKAGING.fetchFrom(p)) {
 279             testRuntime(p, new String[] {"Contents/Home/jre/lib/ext/jfxrt.jar", "Contents/Home/jre/lib/jfxrt.jar"});
 280         }
 281 
 282         return true;
 283     }
 284 
 285 
 286     private File getConfig_InfoPlist(Map<String, ? super Object> params) {
 287         return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
 288     }
 289 
 290     private File getConfig_Icon(Map<String, ? super Object> params) {
 291         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns");
 292     }
 293 
 294     private void prepareConfigFiles(Map<String, ? super Object> params) throws IOException {
 295         File infoPlistFile = getConfig_InfoPlist(params);
 296         infoPlistFile.createNewFile();
 297         writeInfoPlist(infoPlistFile, params);
 298 
 299         // Copy icon to Resources folder
 300         prepareIcon(params);
 301     }
 302 
 303     public File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
 304         File rootDirectory = null;
 305         try {
 306             final File predefinedImage = getPredefinedImage(p);
 307             if (predefinedImage != null) {
 308                 return predefinedImage;
 309             }

 310 
 311             File file = BUILD_ROOT.fetchFrom(p);

 312 
 313             //prepare config resources (we will copy them to the bundle later)
 314             // NB: explicitly saving them to simplify customization
 315             prepareConfigFiles(p);
 316 
 317             // Create directory structure
 318             rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
 319             IOUtils.deleteRecursive(rootDirectory);
 320             rootDirectory.mkdirs();
 321 
 322             if (!dependentTask) {
 323                 Log.info("Creating app bundle: " + rootDirectory.getAbsolutePath());
 324             }
 325 
 326             File contentsDirectory = new File(rootDirectory, "Contents");
 327             contentsDirectory.mkdirs();
 328 
 329             File macOSDirectory = new File(contentsDirectory, "MacOS");
 330             macOSDirectory.mkdirs();
 331 
 332             File javaDirectory = new File(contentsDirectory, "Java");
 333             javaDirectory.mkdirs();
 334 
 335             File plugInsDirectory = new File(contentsDirectory, "PlugIns");
 336 
 337             File resourcesDirectory = new File(contentsDirectory, "Resources");
 338             resourcesDirectory.mkdirs();
 339 
 340             // Generate PkgInfo
 341             File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
 342             pkgInfoFile.createNewFile();
 343             writePkgInfo(pkgInfoFile);
 344 
 345             // Copy executable to MacOS folder
 346             File executableFile = new File(macOSDirectory, getLauncherName(p));
 347             IOUtils.copyFromURL(
 348                     RAW_EXECUTABLE_URL.fetchFrom(p),
 349                     executableFile);
 350 
 351             executableFile.setExecutable(true, false);
 352 
 353             // Copy runtime to PlugIns folder
 354             copyRuntime(plugInsDirectory, p);
 355 
 356             // Copy class path entries to Java folder
 357             copyClassPathEntries(javaDirectory, p);
 358 
 359 //TODO: Need to support adding native libraries.
 360             // Copy library path entries to MacOS folder
 361             //copyLibraryPathEntries(macOSDirectory);
 362 
 363             /*********** Take care of "config" files *******/
 364             // Copy icon to Resources folder
 365             IOUtils.copyFile(getConfig_Icon(p),
 366                     new File(resourcesDirectory, getConfig_Icon(p).getName()));
 367             // Generate Info.plist
 368             IOUtils.copyFile(getConfig_InfoPlist(p),
 369                     new File(contentsDirectory, "Info.plist"));


 370         } catch (IOException ex) {
 371             Log.verbose(ex);
 372             return null;
 373         } finally {
 374             if (!VERBOSE.fetchFrom(p)) {
 375                 //cleanup
 376                 cleanupConfigFiles(p);
 377             } else {
 378                 Log.info("Config files are saved to " +
 379                         CONFIG_ROOT.fetchFrom(p).getAbsolutePath()  +
 380                         ". Use them to customize package.");
 381             }
 382         }
 383         return rootDirectory;
 384     }
 385 
 386     public String getAppName(Map<String, ? super Object> params) {
 387         return APP_NAME.fetchFrom(params) + ".app";
 388     }
 389 
 390     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
 391         //Since building the app can be bypassed, make sure configRoot was set
 392         if (CONFIG_ROOT.fetchFrom(params) != null) {
 393             if (getConfig_Icon(params) != null) {
 394                 getConfig_Icon(params).delete();
 395             }
 396             if (getConfig_InfoPlist(params) != null) {
 397                 getConfig_InfoPlist(params).delete();
 398             }
 399         }
 400     }
 401 
 402 
 403     private void copyClassPathEntries(File javaDirectory, Map<String, ? super Object> params) throws IOException {
 404         RelativeFileSet classPath = APP_RESOURCES.fetchFrom(params);
 405         if (classPath == null) {
 406             throw new RuntimeException("Null app resources?");
 407         }
 408         File srcdir = classPath.getBaseDirectory();
 409         for (String fname : classPath.getIncludedFiles()) {
 410             IOUtils.copyFile(
 411                     new File(srcdir, fname), new File(javaDirectory, fname));
 412         }
 413     }
 414 
 415     private void copyRuntime(File plugInsDirectory, Map<String, ? super Object> params) throws IOException {
 416         RelativeFileSet runTime = MAC_RUNTIME.fetchFrom(params);
 417         if (runTime == null) {
 418             //request to use system runtime => do not bundle
 419             return;
 420         }
 421         plugInsDirectory.mkdirs();
 422 
 423         File srcdir = runTime.getBaseDirectory();
 424         File destDir = new File(plugInsDirectory, srcdir.getName());
 425         Set<String> filesToCopy = runTime.getIncludedFiles();
 426 




 427         for (String fname : filesToCopy) {
 428             IOUtils.copyFile(
 429                     new File(srcdir, fname), new File(destDir, fname));
 430         }
 431     }
 432 
 433     private void prepareIcon(Map<String, ? super Object> params) throws IOException {






 434         File icon = ICON.fetchFrom(params);
 435         if (icon == null || !icon.exists()) {
 436             fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
 437                     "icon",
 438                     DEFAULT_ICNS_ICON.fetchFrom(params),
 439                     getConfig_Icon(params),
 440                     VERBOSE.fetchFrom(params));
 441         } else {
 442             fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
 443                     "icon",
 444                     icon,
 445                     getConfig_Icon(params),
 446                     VERBOSE.fetchFrom(params));
 447         }
 448     }
 449 
 450     private String getLauncherName(Map<String, ? super Object> params) {
 451         if (APP_NAME.fetchFrom(params) != null) {
 452             return APP_NAME.fetchFrom(params);
 453         } else {
 454             return MAIN_CLASS.fetchFrom(params);
 455         }
 456     }
 457 
 458     private String getBundleName(Map<String, ? super Object> params) {
 459         //TODO: Check to see what rules/limits are in place for CFBundleName
 460         if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
 461             String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
 462             if (bn.length() > 16) {
 463                 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn));
 464             }
 465             return MAC_CF_BUNDLE_NAME.fetchFrom(params);
 466         } else if (APP_NAME.fetchFrom(params) != null) {
 467             return APP_NAME.fetchFrom(params);
 468         } else {
 469             String nm = MAIN_CLASS.fetchFrom(params);
 470             if (nm.length() > 16) {
 471                 nm = nm.substring(0, 16);
 472             }
 473             return nm;
 474         }
 475     }
 476 
 477     private String getBundleIdentifier(Map<String, ? super Object> params) {
 478         //TODO: Check to see what rules/limits are in place for CFBundleIdentifier
 479         return  IDENTIFIER.fetchFrom(params);
 480     }
 481 
 482     private void writeInfoPlist(File file, Map<String, ? super Object> params) throws IOException {
 483         Log.verbose("Preparing Info.plist: "+file.getAbsolutePath());
 484 
 485         //prepare config for exe
 486         //Note: do not need CFBundleDisplayName if we do not support localization
 487         Map<String, String> data = new HashMap<>();
 488         data.put("DEPLOY_ICON_FILE", getConfig_Icon(params).getName());
 489         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 490                 getBundleIdentifier(params));
 491         data.put("DEPLOY_BUNDLE_NAME",
 492                 getBundleName(params));
 493         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 494                 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
 495         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
 496         if (MAC_RUNTIME.fetchFrom(params) != null) {
 497             data.put("DEPLOY_JAVA_RUNTIME_NAME",
 498                     MAC_RUNTIME.fetchFrom(params).getBaseDirectory().getName());
 499         } else {
 500             data.put("DEPLOY_JAVA_RUNTIME_NAME", "");
 501         }
 502         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 503                 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
 504         data.put("DEPLOY_BUNDLE_CATEGORY",
 505                 //TODO parameters should provide set of values for IDEs
 506                 MAC_CATEGORY.validatedFetchFrom(params));

 507 
 508         //TODO NOT THE WAY TODO THIS but good enough for first pass
 509         data.put("DEPLOY_MAIN_JAR_NAME", new BundleParams(params).getMainApplicationJar());
 510 //        data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).toString());
 511 
 512         data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
 513 
 514         StringBuilder sb = new StringBuilder();
 515         List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
 516 
 517         String newline = ""; //So we don't add unneccessary extra line after last append
 518         for (String o : jvmOptions) {
 519             sb.append(newline).append("    <string>").append(o).append("</string>");
 520             newline = "\n";
 521         }
 522         data.put("DEPLOY_JVM_OPTIONS", sb.toString());
 523 
 524         newline = "";
 525         sb = new StringBuilder();
 526         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);


 529             sb.append("      <key>").append(arg.getKey()).append("</key>\n");
 530             sb.append("      <string>").append(arg.getValue()).append("</string>");
 531             newline = "\n";
 532         }
 533         data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
 534 
 535 
 536         //TODO UNLESS we are supporting building for jre7, this is unnecessary
 537 //        if (params.useJavaFXPackaging()) {
 538 //            data.put("DEPLOY_LAUNCHER_CLASS", JAVAFX_LAUNCHER_CLASS);
 539 //        } else {
 540         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 541 //        }
 542         // This will be an empty string for correctly packaged JavaFX apps
 543         data.put("DEPLOY_APP_CLASSPATH", MAIN_JAR_CLASSPATH.fetchFrom(params));
 544 
 545         //TODO: Add remainder of the classpath
 546 
 547         Writer w = new BufferedWriter(new FileWriter(file));
 548         w.write(preprocessTextResource(
 549                 MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(),
 550                 "Bundle config file", TEMPLATE_INFO_PLIST, data,
 551                 VERBOSE.fetchFrom(params)));
 552         w.close();
 553 
 554     }
 555 
 556     private void writePkgInfo(File file) throws IOException {
 557 
 558         //hardcoded as it does not seem we need to change it ever
 559         String signature = "????";
 560 
 561         try (Writer out = new BufferedWriter(new FileWriter(file))) {
 562             out.write(OS_TYPE_CODE + signature);
 563             out.flush();
 564         }
 565     }
 566 
 567     //////////////////////////////////////////////////////////////////////////////////
 568     // Implement Bundler
 569     //////////////////////////////////////////////////////////////////////////////////
 570 
 571     @Override


 587     public BundleType getBundleType() {
 588         return BundleType.IMAGE;
 589     }
 590 
 591     @Override
 592     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 593         return getAppBundleParameters();
 594     }
 595 
 596     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
 597         return Arrays.asList(
 598                 APP_NAME,
 599                 APP_RESOURCES,
 600                 BUILD_ROOT,
 601                 JVM_OPTIONS,
 602                 MAIN_CLASS,
 603                 MAIN_JAR,
 604                 MAIN_JAR_CLASSPATH,
 605                 PREFERENCES_ID,
 606                 RAW_EXECUTABLE_URL,
 607                 MAC_RUNTIME,
 608                 USE_FX_PACKAGING,
 609                 USER_JVM_OPTIONS,
 610                 VERSION,
 611                 ICON,
 612                 MAC_CATEGORY
 613         );
 614     }
 615 
 616 
 617     @Override
 618     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 619         return doBundle(params, outputParentDir, false);
 620     }
 621 }