1 /*
   2  * Copyright (c) 2012, 2016, 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.Log;
  33 import com.oracle.tools.packager.Platform;
  34 import com.oracle.tools.packager.StandardBundlerParam;
  35 import com.oracle.tools.packager.UnsupportedPlatformException;
  36 import jdk.packager.builders.mac.MacAppImageBuilder;
  37 
  38 import jdk.packager.internal.JLinkBundlerHelper;
  39 
  40 import java.io.File;
  41 import java.io.IOException;
  42 import java.math.BigInteger;
  43 import java.text.MessageFormat;
  44 import java.util.Arrays;
  45 import java.util.Collection;
  46 import java.util.HashMap;
  47 import java.util.Map;
  48 import java.util.Optional;
  49 import java.util.ResourceBundle;
  50 
  51 import static com.oracle.tools.packager.StandardBundlerParam.*;
  52 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.*;
  53 import jdk.packager.builders.AbstractAppImageBuilder;
  54 import jdk.packager.internal.mac.MacCertificate;
  55 
  56 public class MacAppBundler extends AbstractImageBundler {
  57 
  58     private static final ResourceBundle I18N =
  59             ResourceBundle.getBundle(MacAppBundler.class.getName());
  60 
  61     public final static String MAC_BUNDLER_PREFIX =
  62             BUNDLER_PREFIX + "macosx" + File.separator;
  63 
  64     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  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<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
 174             I18N.getString("param.default-icon-icns"),
 175             I18N.getString("param.default-icon-icns.description"),
 176             ".mac.default.icns",
 177             String.class,
 178             params -> TEMPLATE_BUNDLE_ICON,
 179             (s, p) -> s);
 180 
 181     public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>(
 182             I18N.getString("param.signing-key-developer-id-app.name"),
 183             I18N.getString("param.signing-key-developer-id-app.description"),
 184             "mac.signing-key-developer-id-app",
 185             String.class,
 186             params -> {
 187                     String result = MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params),
 188                                                                     SIGNING_KEYCHAIN.fetchFrom(params),
 189                                                                     VERBOSE.fetchFrom(params));
 190                     if (result != null) {
 191                         MacCertificate certificate = new MacCertificate(result, VERBOSE.fetchFrom(params));
 192 
 193                         if (!certificate.isValid()) {
 194                             Log.info(MessageFormat.format(I18N.getString("error.certificate.expired"), result));
 195                         }
 196                     }
 197 
 198                     return result;
 199                 },
 200             (s, p) -> s);
 201 
 202     public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>(
 203             I18N.getString("param.bundle-id-signing-prefix.name"),
 204             I18N.getString("param.bundle-id-signing-prefix.description"),
 205             "mac.bundle-id-signing-prefix",
 206             String.class,
 207             params -> IDENTIFIER.fetchFrom(params) + ".",
 208             (s, p) -> s);
 209 
 210     public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>(
 211             I18N.getString("param.icon-icns.name"),
 212             I18N.getString("param.icon-icns.description"),
 213             "icon.icns",
 214             File.class,
 215             params -> {
 216                 File f = ICON.fetchFrom(params);
 217                 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
 218                     Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f));
 219                     return null;
 220                 }
 221                 return f;
 222             },
 223             (s, p) -> new File(s));
 224 
 225     public MacAppBundler() {
 226         super();
 227         baseResourceLoader = MacResources.class;
 228     }
 229 
 230     public static boolean validCFBundleVersion(String v) {
 231         // CFBundleVersion (String - iOS, OS X) specifies the build version
 232         // number of the bundle, which identifies an iteration (released or
 233         // unreleased) of the bundle. The build version number should be a
 234         // string comprised of three non-negative, period-separated integers
 235         // with the first integer being greater than zero. The string should
 236         // only contain numeric (0-9) and period (.) characters. Leading zeros
 237         // are truncated from each integer and will be ignored (that is,
 238         // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
 239 
 240         if (v == null) {
 241             return false;
 242         }
 243 
 244         String p[] = v.split("\\.");
 245         if (p.length > 3 || p.length < 1) {
 246             Log.verbose(I18N.getString("message.version-string-too-many-components"));
 247             return false;
 248         }
 249 
 250         try {
 251             BigInteger n = new BigInteger(p[0]);
 252             if (BigInteger.ONE.compareTo(n) > 0) {
 253                 Log.verbose(I18N.getString("message.version-string-first-number-not-zero"));
 254                 return false;
 255             }
 256             if (p.length > 1) {
 257                 n = new BigInteger(p[1]);
 258                 if (BigInteger.ZERO.compareTo(n) > 0) {
 259                     Log.verbose(I18N.getString("message.version-string-no-negative-numbers"));
 260                     return false;
 261                 }
 262             }
 263             if (p.length > 2) {
 264                 n = new BigInteger(p[2]);
 265                 if (BigInteger.ZERO.compareTo(n) > 0) {
 266                     Log.verbose(I18N.getString("message.version-string-no-negative-numbers"));
 267                     return false;
 268                 }
 269             }
 270         } catch (NumberFormatException ne) {
 271             Log.verbose(I18N.getString("message.version-string-numbers-only"));
 272             Log.verbose(ne);
 273             return false;
 274         }
 275 
 276         return true;
 277     }
 278 
 279     @Override
 280     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
 281         try {
 282             return doValidate(params);
 283         } catch (RuntimeException re) {
 284             if (re.getCause() instanceof ConfigException) {
 285                 throw (ConfigException) re.getCause();
 286             } else {
 287                 throw new ConfigException(re);
 288             }
 289         }
 290     }
 291 
 292     //to be used by chained bundlers, e.g. by EXE bundler to avoid
 293     // skipping validation if p.type does not include "image"
 294     public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 295         if (Platform.getPlatform() != Platform.MAC) {
 296             throw new UnsupportedPlatformException();
 297         }
 298 
 299         imageBundleValidation(p);
 300 
 301         if (getPredefinedImage(p) != null) {
 302             return true;
 303         }
 304 
 305         //TODO warn if MAC_RUNTIME is set
 306 
 307         // validate short version
 308         if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) {
 309             throw new ConfigException(
 310                     I18N.getString("error.invalid-cfbundle-version"),
 311                     I18N.getString("error.invalid-cfbundle-version.advice"));
 312         }
 313 
 314         // reject explicitly set sign to true and no valid signature key
 315         if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) {
 316             String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
 317             if (signingIdentity == null) {
 318                 throw new ConfigException(
 319                         I18N.getString("error.explicit-sign-no-cert"),
 320                         I18N.getString("error.explicit-sign-no-cert.advice"));
 321             }
 322         }
 323 
 324         return true;
 325     }
 326 
 327     private File getConfig_InfoPlist(Map<String, ? super Object> params) {
 328         return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
 329     }
 330 
 331     private File getConfig_Icon(Map<String, ? super Object> params) {
 332         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns");
 333     }
 334 
 335 
 336     File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
 337         try {
 338             if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) {
 339                 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
 340             }
 341             if (!outputDirectory.canWrite()) {
 342                 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
 343             }
 344 
 345             // Create directory structure
 346             File rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
 347             IOUtils.deleteRecursive(rootDirectory);
 348             rootDirectory.mkdirs();
 349 
 350             if (!dependentTask) {
 351                 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath()));
 352             }
 353 
 354             if (!p.containsKey(JLinkBundlerHelper.JLINK_BUILDER.getID())) {
 355                 p.put(JLinkBundlerHelper.JLINK_BUILDER.getID(), "macapp-image-builder");
 356             }
 357 
 358             AbstractAppImageBuilder appBuilder = new MacAppImageBuilder(p, outputDirectory.toPath());
 359             JLinkBundlerHelper.execute(p, appBuilder);
 360             return rootDirectory;
 361         } catch (IOException ex) {
 362             Log.info(ex.toString());
 363             Log.verbose(ex);
 364             return null;
 365         } catch (Exception ex) {
 366             Log.info("Exception: "+ex);
 367             Log.debug(ex);
 368             return null;
 369         }
 370     }
 371 
 372     public void cleanupConfigFiles(Map<String, ? super Object> params) {
 373         //Since building the app can be bypassed, make sure configRoot was set
 374         if (CONFIG_ROOT.fetchFrom(params) != null) {
 375             getConfig_Icon(params).delete();
 376             getConfig_InfoPlist(params).delete();
 377         }
 378     }
 379 
 380     //////////////////////////////////////////////////////////////////////////////////
 381     // Implement Bundler
 382     //////////////////////////////////////////////////////////////////////////////////
 383 
 384     @Override
 385     public String getName() {
 386         return I18N.getString("bundler.name");
 387     }
 388 
 389     @Override
 390     public String getDescription() {
 391         return I18N.getString("bundler.description");
 392     }
 393 
 394     @Override
 395     public String getID() {
 396         return "mac.app";
 397     }
 398 
 399     @Override
 400     public String getBundleType() {
 401         return "IMAGE";
 402     }
 403 
 404     @Override
 405     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 406         return getAppBundleParameters();
 407     }
 408 
 409     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
 410         return Arrays.asList(
 411                 APP_NAME,
 412                 APP_RESOURCES,
 413                 ARGUMENTS,
 414                 BUNDLE_ID_SIGNING_PREFIX,
 415                 CLASSPATH,
 416                 DEVELOPER_ID_APP_SIGNING_KEY,
 417                 ICON_ICNS,
 418                 JVM_OPTIONS,
 419                 JVM_PROPERTIES,
 420                 MAC_CATEGORY,
 421                 MAC_CF_BUNDLE_IDENTIFIER,
 422                 MAC_CF_BUNDLE_NAME,
 423                 MAC_CF_BUNDLE_VERSION,
 424                 MAIN_CLASS,
 425                 MAIN_JAR,
 426                 PREFERENCES_ID,
 427                 PRELOADER_CLASS,
 428                 SIGNING_KEYCHAIN,
 429                 USER_JVM_OPTIONS,
 430                 VERSION
 431         );
 432     }
 433 
 434 
 435     @Override
 436     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 437         return doBundle(params, outputParentDir, false);
 438     }
 439 
 440 //    private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException {
 441 //        prepareConfigFiles(p);
 442 //
 443 //        if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
 444 //            writeCfgFile(p, rootDirectory);
 445 //        } else {
 446 //            writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime");
 447 //        }
 448 //
 449 //        // Copy executable root folder
 450 //        File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p));
 451 //        IOUtils.copyFromURL(
 452 //                RAW_EXECUTABLE_URL.fetchFrom(p),
 453 //                executableFile);
 454 //        executableFile.setExecutable(true, false);
 455 //
 456 //    }
 457 //
 458 
 459 }