1 /*
   2  * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package com.oracle.tools.packager.mac;
  26 
  27 import com.oracle.tools.packager.AbstractImageBundler;
  28 import com.oracle.tools.packager.BundlerParamInfo;
  29 import com.oracle.tools.packager.ConfigException;
  30 import com.oracle.tools.packager.EnumeratedBundlerParam;
  31 import com.oracle.tools.packager.IOUtils;
  32 import com.oracle.tools.packager.JLinkBundlerHelper;
  33 import com.oracle.tools.packager.Log;
  34 import com.oracle.tools.packager.StandardBundlerParam;
  35 import com.oracle.tools.packager.UnsupportedPlatformException;
  36 import jdk.tools.jlink.builder.ImageBuilder;
  37 import jdk.packager.builders.mac.MacAppImageBuilder;
  38 
  39 import java.io.File;
  40 import java.io.IOException;
  41 import java.math.BigInteger;
  42 import java.text.MessageFormat;
  43 import java.util.Arrays;
  44 import java.util.Collection;
  45 import java.util.HashMap;
  46 import java.util.Map;
  47 import java.util.Optional;
  48 import java.util.ResourceBundle;
  49 
  50 import static com.oracle.tools.packager.StandardBundlerParam.*;
  51 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.*;
  52 
  53 public class MacAppBundler extends AbstractImageBundler {
  54 
  55     private static final ResourceBundle I18N =
  56             ResourceBundle.getBundle(MacAppBundler.class.getName());
  57 
  58     public final static String MAC_BUNDLER_PREFIX =
  59             BUNDLER_PREFIX + "macosx" + File.separator;
  60 
  61     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  62 
  63     private static Map<String, String> getMacCategories() {
  64         Map<String, String> map = new HashMap<>();
  65         map.put("Business", "public.app-category.business");
  66         map.put("Developer Tools", "public.app-category.developer-tools");
  67         map.put("Education", "public.app-category.education");
  68         map.put("Entertainment", "public.app-category.entertainment");
  69         map.put("Finance", "public.app-category.finance");
  70         map.put("Games", "public.app-category.games");
  71         map.put("Graphics & Design", "public.app-category.graphics-design");
  72         map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
  73         map.put("Lifestyle", "public.app-category.lifestyle");
  74         map.put("Medical", "public.app-category.medical");
  75         map.put("Music", "public.app-category.music");
  76         map.put("News", "public.app-category.news");
  77         map.put("Photography", "public.app-category.photography");
  78         map.put("Productivity", "public.app-category.productivity");
  79         map.put("Reference", "public.app-category.reference");
  80         map.put("Social Networking", "public.app-category.social-networking");
  81         map.put("Sports", "public.app-category.sports");
  82         map.put("Travel", "public.app-category.travel");
  83         map.put("Utilities", "public.app-category.utilities");
  84         map.put("Video", "public.app-category.video");
  85         map.put("Weather", "public.app-category.weather");
  86 
  87         map.put("Action Games", "public.app-category.action-games");
  88         map.put("Adventure Games", "public.app-category.adventure-games");
  89         map.put("Arcade Games", "public.app-category.arcade-games");
  90         map.put("Board Games", "public.app-category.board-games");
  91         map.put("Card Games", "public.app-category.card-games");
  92         map.put("Casino Games", "public.app-category.casino-games");
  93         map.put("Dice Games", "public.app-category.dice-games");
  94         map.put("Educational Games", "public.app-category.educational-games");
  95         map.put("Family Games", "public.app-category.family-games");
  96         map.put("Kids Games", "public.app-category.kids-games");
  97         map.put("Music Games", "public.app-category.music-games");
  98         map.put("Puzzle Games", "public.app-category.puzzle-games");
  99         map.put("Racing Games", "public.app-category.racing-games");
 100         map.put("Role Playing Games", "public.app-category.role-playing-games");
 101         map.put("Simulation Games", "public.app-category.simulation-games");
 102         map.put("Sports Games", "public.app-category.sports-games");
 103         map.put("Strategy Games", "public.app-category.strategy-games");
 104         map.put("Trivia Games", "public.app-category.trivia-games");
 105         map.put("Word Games", "public.app-category.word-games");
 106 
 107         return map;
 108     }
 109 
 110     public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
 111             new EnumeratedBundlerParam<>(
 112                     I18N.getString("param.category-name"),
 113                     I18N.getString("param.category-name.description"),
 114                     "mac.category",
 115                     String.class,
 116                     params -> params.containsKey(CATEGORY.getID())
 117                             ? CATEGORY.fetchFrom(params)
 118                             : "Unknown",
 119                     (s, p) -> s,
 120                     getMacCategories(),
 121                     false //strict - for MacStoreBundler this should be strict
 122             );
 123 
 124     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
 125             new StandardBundlerParam<>(
 126                     I18N.getString("param.cfbundle-name.name"),
 127                     I18N.getString("param.cfbundle-name.description"),
 128                     "mac.CFBundleName",
 129                     String.class,
 130                     params -> null,
 131                     (s, p) -> s);
 132 
 133     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER =
 134             new StandardBundlerParam<>(
 135                     I18N.getString("param.cfbundle-identifier.name"),
 136                     I18N.getString("param.cfbundle-identifier.description"),
 137                     "mac.CFBundleIdentifier",
 138                     String.class,
 139                     IDENTIFIER::fetchFrom,
 140                     (s, p) -> s);
 141 
 142     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION =
 143             new StandardBundlerParam<>(
 144                     I18N.getString("param.cfbundle-version.name"),
 145                     I18N.getString("param.cfbundle-version.description"),
 146                     "mac.CFBundleVersion",
 147                     String.class,
 148                     p -> {
 149                         String s = VERSION.fetchFrom(p);
 150                         if (validCFBundleVersion(s)) {
 151                             return s;
 152                         } else {
 153                             return "100";
 154                         }
 155                     },
 156                     (s, p) -> s);
 157 
 158     public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
 159             I18N.getString("param.config-root.name"),
 160             I18N.getString("param.config-root.description"),
 161             "configRoot",
 162             File.class,
 163             params -> {
 164                 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
 165                 configRoot.mkdirs();
 166                 return configRoot;
 167             },
 168             (s, p) -> new File(s));
 169 
 170     public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
 171             I18N.getString("param.default-icon-icns"),
 172             I18N.getString("param.default-icon-icns.description"),
 173             ".mac.default.icns",
 174             String.class,
 175             params -> TEMPLATE_BUNDLE_ICON,
 176             (s, p) -> s);
 177 
 178     public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>(
 179             I18N.getString("param.signing-key-developer-id-app.name"),
 180             I18N.getString("param.signing-key-developer-id-app.description"),
 181             "mac.signing-key-developer-id-app",
 182             String.class,
 183             params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)),
 184             (s, p) -> s);
 185 
 186     public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>(
 187             I18N.getString("param.bundle-id-signing-prefix.name"),
 188             I18N.getString("param.bundle-id-signing-prefix.description"),
 189             "mac.bundle-id-signing-prefix",
 190             String.class,
 191             params -> IDENTIFIER.fetchFrom(params) + ".",
 192             (s, p) -> s);
 193 
 194     public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>(
 195             I18N.getString("param.icon-icns.name"),
 196             I18N.getString("param.icon-icns.description"),
 197             "icon.icns",
 198             File.class,
 199             params -> {
 200                 File f = ICON.fetchFrom(params);
 201                 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
 202                     Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f));
 203                     return null;
 204                 }
 205                 return f;
 206             },
 207             (s, p) -> new File(s));
 208 
 209     public MacAppBundler() {
 210         super();
 211         baseResourceLoader = MacResources.class;
 212     }
 213 
 214     public static boolean validCFBundleVersion(String v) {
 215         // CFBundleVersion (String - iOS, OS X) specifies the build version
 216         // number of the bundle, which identifies an iteration (released or
 217         // unreleased) of the bundle. The build version number should be a
 218         // string comprised of three non-negative, period-separated integers
 219         // with the first integer being greater than zero. The string should
 220         // only contain numeric (0-9) and period (.) characters. Leading zeros
 221         // are truncated from each integer and will be ignored (that is,
 222         // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
 223 
 224         if (v == null) {
 225             return false;
 226         }
 227 
 228         String p[] = v.split("\\.");
 229         if (p.length > 3 || p.length < 1) {
 230             Log.verbose(I18N.getString("message.version-string-too-many-components"));
 231             return false;
 232         }
 233 
 234         try {
 235             BigInteger n = new BigInteger(p[0]);
 236             if (BigInteger.ONE.compareTo(n) > 0) {
 237                 Log.verbose(I18N.getString("message.version-string-first-number-not-zero"));
 238                 return false;
 239             }
 240             if (p.length > 1) {
 241                 n = new BigInteger(p[1]);
 242                 if (BigInteger.ZERO.compareTo(n) > 0) {
 243                     Log.verbose(I18N.getString("message.version-string-no-negative-numbers"));
 244                     return false;
 245                 }
 246             }
 247             if (p.length > 2) {
 248                 n = new BigInteger(p[2]);
 249                 if (BigInteger.ZERO.compareTo(n) > 0) {
 250                     Log.verbose(I18N.getString("message.version-string-no-negative-numbers"));
 251                     return false;
 252                 }
 253             }
 254         } catch (NumberFormatException ne) {
 255             Log.verbose(I18N.getString("message.version-string-numbers-only"));
 256             Log.verbose(ne);
 257             return false;
 258         }
 259 
 260         return true;
 261     }
 262 
 263     @Override
 264     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
 265         try {
 266             return doValidate(params);
 267         } catch (RuntimeException re) {
 268             if (re.getCause() instanceof ConfigException) {
 269                 throw (ConfigException) re.getCause();
 270             } else {
 271                 throw new ConfigException(re);
 272             }
 273         }
 274     }
 275 
 276     //to be used by chained bundlers, e.g. by EXE bundler to avoid
 277     // skipping validation if p.type does not include "image"
 278     public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 279         if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
 280             throw new UnsupportedPlatformException();
 281         }
 282 
 283         imageBundleValidation(p);
 284 
 285         if (getPredefinedImage(p) != null) {
 286             return true;
 287         }
 288 
 289         //TODO warn if MAC_RUNTIME is set
 290 
 291         // validate short version
 292         if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) {
 293             throw new ConfigException(
 294                     I18N.getString("error.invalid-cfbundle-version"),
 295                     I18N.getString("error.invalid-cfbundle-version.advice"));
 296         }
 297 
 298         // reject explicitly set sign to true and no valid signature key
 299         if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) {
 300             String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
 301             if (signingIdentity == null) {
 302                 throw new ConfigException(
 303                         I18N.getString("error.explicit-sign-no-cert"),
 304                         I18N.getString("error.explicit-sign-no-cert.advice"));
 305             }
 306         }
 307 
 308         return true;
 309     }
 310 
 311     private File getConfig_InfoPlist(Map<String, ? super Object> params) {
 312         return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
 313     }
 314 
 315     private File getConfig_Icon(Map<String, ? super Object> params) {
 316         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns");
 317     }
 318 
 319 
 320     File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
 321         try {
 322             if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) {
 323                 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
 324             }
 325             if (!outputDirectory.canWrite()) {
 326                 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
 327             }
 328 
 329             // Create directory structure
 330             File rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
 331             IOUtils.deleteRecursive(rootDirectory);
 332             rootDirectory.mkdirs();
 333 
 334             if (!dependentTask) {
 335                 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath()));
 336             }
 337 
 338             if (!p.containsKey(JLinkBundlerHelper.JLINK_BUILDER.getID())) {
 339                 p.put(JLinkBundlerHelper.JLINK_BUILDER.getID(), "macapp-image-builder");
 340             }
 341 
 342             ImageBuilder imageBuilder = new MacAppImageBuilder(p, outputDirectory.toPath());
 343             JLinkBundlerHelper.execute(p, outputDirectory, imageBuilder);
 344             return rootDirectory;
 345         } catch (IOException ex) {
 346             Log.info(ex.toString());
 347             Log.verbose(ex);
 348             return null;
 349         }
 350     }
 351 
 352     public void cleanupConfigFiles(Map<String, ? super Object> params) {
 353         //Since building the app can be bypassed, make sure configRoot was set
 354         if (CONFIG_ROOT.fetchFrom(params) != null) {
 355             getConfig_Icon(params).delete();
 356             getConfig_InfoPlist(params).delete();
 357         }
 358     }
 359 
 360     //////////////////////////////////////////////////////////////////////////////////
 361     // Implement Bundler
 362     //////////////////////////////////////////////////////////////////////////////////
 363 
 364     @Override
 365     public String getName() {
 366         return I18N.getString("bundler.name");
 367     }
 368 
 369     @Override
 370     public String getDescription() {
 371         return I18N.getString("bundler.description");
 372     }
 373 
 374     @Override
 375     public String getID() {
 376         return "mac.app";
 377     }
 378 
 379     @Override
 380     public String getBundleType() {
 381         return "IMAGE";
 382     }
 383 
 384     @Override
 385     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 386         return getAppBundleParameters();
 387     }
 388 
 389     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
 390         return Arrays.asList(
 391                 APP_NAME,
 392                 APP_RESOURCES,
 393                 // APP_RESOURCES_LIST, // ??
 394                 ARGUMENTS,
 395                 BUNDLE_ID_SIGNING_PREFIX,
 396                 CLASSPATH,
 397                 DEVELOPER_ID_APP_SIGNING_KEY,
 398                 ICON_ICNS,
 399                 JVM_OPTIONS,
 400                 JVM_PROPERTIES,
 401                 MAC_CATEGORY,
 402                 MAC_CF_BUNDLE_IDENTIFIER,
 403                 MAC_CF_BUNDLE_NAME,
 404                 MAC_CF_BUNDLE_VERSION,
 405 //                MAC_RUNTIME,
 406                 MAIN_CLASS,
 407                 MAIN_JAR,
 408                 PREFERENCES_ID,
 409                 PRELOADER_CLASS,
 410                 SIGNING_KEYCHAIN,
 411                 USER_JVM_OPTIONS,
 412                 VERSION
 413         );
 414     }
 415 
 416 
 417     @Override
 418     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 419         return doBundle(params, outputParentDir, false);
 420     }
 421 
 422 //    private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException {
 423 //        prepareConfigFiles(p);
 424 //
 425 //        if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
 426 //            writeCfgFile(p, rootDirectory);
 427 //        } else {
 428 //            writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime");
 429 //        }
 430 //
 431 //        // Copy executable root folder
 432 //        File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p));
 433 //        IOUtils.copyFromURL(
 434 //                RAW_EXECUTABLE_URL.fetchFrom(p),
 435 //                executableFile);
 436 //        executableFile.setExecutable(true, false);
 437 //
 438 //    }
 439 //
 440 
 441 }