1 /*
   2  * Copyright (c) 2012, 2018, 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 
  26 package jdk.jpackager.internal.mac;
  27 
  28 import jdk.jpackager.internal.AbstractImageBundler;
  29 import jdk.jpackager.internal.BundlerParamInfo;
  30 import jdk.jpackager.internal.ConfigException;
  31 import jdk.jpackager.internal.EnumeratedBundlerParam;
  32 import jdk.jpackager.internal.IOUtils;
  33 import jdk.jpackager.internal.Log;
  34 import jdk.jpackager.internal.Platform;
  35 import jdk.jpackager.internal.StandardBundlerParam;
  36 import jdk.jpackager.internal.Arguments;
  37 import jdk.jpackager.internal.UnsupportedPlatformException;
  38 import jdk.jpackager.internal.builders.mac.MacAppImageBuilder;
  39 import jdk.jpackager.internal.resources.mac.MacResources;
  40 import jdk.jpackager.internal.JLinkBundlerHelper;
  41 
  42 import java.io.File;
  43 import java.io.IOException;
  44 import java.math.BigInteger;
  45 import java.text.MessageFormat;
  46 import java.util.Arrays;
  47 import java.util.Collection;
  48 import java.util.HashMap;
  49 import java.util.Map;
  50 import java.util.Optional;
  51 import java.util.ResourceBundle;
  52 
  53 import static jdk.jpackager.internal.StandardBundlerParam.*;
  54 import static jdk.jpackager.internal.mac.MacBaseInstallerBundler.*;
  55 import jdk.jpackager.internal.builders.AbstractAppImageBuilder;
  56 
  57 public class MacAppBundler extends AbstractImageBundler {
  58 
  59     private static final ResourceBundle I18N =
  60             ResourceBundle.getBundle(
  61                     "jdk.jpackager.internal.resources.mac.MacAppBundler");
  62 
  63     public final static String MAC_BUNDLER_PREFIX =
  64             BUNDLER_PREFIX + "macosx" + File.separator;
  65 
  66     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  67 
  68     private static Map<String, String> getMacCategories() {
  69         Map<String, String> map = new HashMap<>();
  70         map.put("Business", "public.app-category.business");
  71         map.put("Developer Tools", "public.app-category.developer-tools");
  72         map.put("Education", "public.app-category.education");
  73         map.put("Entertainment", "public.app-category.entertainment");
  74         map.put("Finance", "public.app-category.finance");
  75         map.put("Games", "public.app-category.games");
  76         map.put("Graphics & Design", "public.app-category.graphics-design");
  77         map.put("Healthcare & Fitness",
  78                 "public.app-category.healthcare-fitness");
  79         map.put("Lifestyle", "public.app-category.lifestyle");
  80         map.put("Medical", "public.app-category.medical");
  81         map.put("Music", "public.app-category.music");
  82         map.put("News", "public.app-category.news");
  83         map.put("Photography", "public.app-category.photography");
  84         map.put("Productivity", "public.app-category.productivity");
  85         map.put("Reference", "public.app-category.reference");
  86         map.put("Social Networking", "public.app-category.social-networking");
  87         map.put("Sports", "public.app-category.sports");
  88         map.put("Travel", "public.app-category.travel");
  89         map.put("Utilities", "public.app-category.utilities");
  90         map.put("Video", "public.app-category.video");
  91         map.put("Weather", "public.app-category.weather");
  92 
  93         map.put("Action Games", "public.app-category.action-games");
  94         map.put("Adventure Games", "public.app-category.adventure-games");
  95         map.put("Arcade Games", "public.app-category.arcade-games");
  96         map.put("Board Games", "public.app-category.board-games");
  97         map.put("Card Games", "public.app-category.card-games");
  98         map.put("Casino Games", "public.app-category.casino-games");
  99         map.put("Dice Games", "public.app-category.dice-games");
 100         map.put("Educational Games", "public.app-category.educational-games");
 101         map.put("Family Games", "public.app-category.family-games");
 102         map.put("Kids Games", "public.app-category.kids-games");
 103         map.put("Music Games", "public.app-category.music-games");
 104         map.put("Puzzle Games", "public.app-category.puzzle-games");
 105         map.put("Racing Games", "public.app-category.racing-games");
 106         map.put("Role Playing Games", "public.app-category.role-playing-games");
 107         map.put("Simulation Games", "public.app-category.simulation-games");
 108         map.put("Sports Games", "public.app-category.sports-games");
 109         map.put("Strategy Games", "public.app-category.strategy-games");
 110         map.put("Trivia Games", "public.app-category.trivia-games");
 111         map.put("Word Games", "public.app-category.word-games");
 112 
 113         return map;
 114     }
 115 
 116     public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
 117             new EnumeratedBundlerParam<>(
 118                     I18N.getString("param.category-name"),
 119                     I18N.getString("param.category-name.description"),
 120                     Arguments.CLIOptions.MAC_APP_STORE_CATEGORY.getId(),
 121                     String.class,
 122                     params -> params.containsKey(CATEGORY.getID())
 123                             ? CATEGORY.fetchFrom(params)
 124                             : "Unknown",
 125                     (s, p) -> s,
 126                     getMacCategories(),
 127                     false //strict - for MacStoreBundler this should be strict
 128             );
 129 
 130     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
 131             new StandardBundlerParam<>(
 132                     I18N.getString("param.cfbundle-name.name"),
 133                     I18N.getString("param.cfbundle-name.description"),
 134                     Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(),
 135                     String.class,
 136                     params -> null,
 137                     (s, p) -> s);
 138 
 139     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER =
 140             new StandardBundlerParam<>(
 141                     I18N.getString("param.cfbundle-identifier.name"),
 142                     I18N.getString("param.cfbundle-identifier.description"),
 143                     Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(),
 144                     String.class,
 145                     IDENTIFIER::fetchFrom,
 146                     (s, p) -> s);
 147 
 148     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION =
 149             new StandardBundlerParam<>(
 150                     I18N.getString("param.cfbundle-version.name"),
 151                     I18N.getString("param.cfbundle-version.description"),
 152                     "mac.CFBundleVersion",
 153                     String.class,
 154                     p -> {
 155                         String s = VERSION.fetchFrom(p);
 156                         if (validCFBundleVersion(s)) {
 157                             return s;
 158                         } else {
 159                             return "100";
 160                         }
 161                     },
 162                     (s, p) -> s);
 163 
 164     public static final BundlerParamInfo<File> CONFIG_ROOT =
 165             new StandardBundlerParam<>(
 166             I18N.getString("param.config-root.name"),
 167             I18N.getString("param.config-root.description"),
 168             "configRoot",
 169             File.class,
 170             params -> {
 171                 File configRoot =
 172                         new File(BUILD_ROOT.fetchFrom(params), "macosx");
 173                 configRoot.mkdirs();
 174                 return configRoot;
 175             },
 176             (s, p) -> new File(s));
 177 
 178     public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON =
 179             new StandardBundlerParam<>(
 180             I18N.getString("param.default-icon-icns"),
 181             I18N.getString("param.default-icon-icns.description"),
 182             ".mac.default.icns",
 183             String.class,
 184             params -> TEMPLATE_BUNDLE_ICON,
 185             (s, p) -> s);
 186 
 187     public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY =
 188             new StandardBundlerParam<>(
 189             I18N.getString("param.signing-key-developer-id-app.name"),
 190             I18N.getString("param.signing-key-developer-id-app.description"),
 191             "mac.signing-key-developer-id-app",
 192             String.class,
 193             params -> {
 194                     String result = MacBaseInstallerBundler.findKey(
 195                             "Developer ID Application: "
 196                             + SIGNING_KEY_USER.fetchFrom(params),
 197                             SIGNING_KEYCHAIN.fetchFrom(params),
 198                             VERBOSE.fetchFrom(params));
 199                     if (result != null) {
 200                         MacCertificate certificate = new MacCertificate(result,
 201                                 VERBOSE.fetchFrom(params));
 202 
 203                         if (!certificate.isValid()) {
 204                             Log.error(MessageFormat.format(I18N.getString(
 205                                     "error.certificate.expired"), result));
 206                         }
 207                     }
 208 
 209                     return result;
 210                 },
 211             (s, p) -> s);
 212 
 213     public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX =
 214             new StandardBundlerParam<>(
 215             I18N.getString("param.bundle-id-signing-prefix.name"),
 216             I18N.getString("param.bundle-id-signing-prefix.description"),
 217             Arguments.CLIOptions.MAC_BUNDLE_SIGNING_PREFIX.getId(),
 218             String.class,
 219             params -> IDENTIFIER.fetchFrom(params) + ".",
 220             (s, p) -> s);
 221 
 222     public static final BundlerParamInfo<File> ICON_ICNS =
 223             new StandardBundlerParam<>(
 224             I18N.getString("param.icon-icns.name"),
 225             I18N.getString("param.icon-icns.description"),
 226             "icon.icns",
 227             File.class,
 228             params -> {
 229                 File f = ICON.fetchFrom(params);
 230                 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
 231                     Log.error(MessageFormat.format(
 232                             I18N.getString("message.icon-not-icns"), f));
 233                     return null;
 234                 }
 235                 return f;
 236             },
 237             (s, p) -> new File(s));
 238 
 239     public MacAppBundler() {
 240         super();
 241         baseResourceLoader = MacResources.class;
 242     }
 243 
 244     public static boolean validCFBundleVersion(String v) {
 245         // CFBundleVersion (String - iOS, OS X) specifies the build version
 246         // number of the bundle, which identifies an iteration (released or
 247         // unreleased) of the bundle. The build version number should be a
 248         // string comprised of three non-negative, period-separated integers
 249         // with the first integer being greater than zero. The string should
 250         // only contain numeric (0-9) and period (.) characters. Leading zeros
 251         // are truncated from each integer and will be ignored (that is,
 252         // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
 253 
 254         if (v == null) {
 255             return false;
 256         }
 257 
 258         String p[] = v.split("\\.");
 259         if (p.length > 3 || p.length < 1) {
 260             Log.verbose(I18N.getString(
 261                     "message.version-string-too-many-components"));
 262             return false;
 263         }
 264 
 265         try {
 266             BigInteger n = new BigInteger(p[0]);
 267             if (BigInteger.ONE.compareTo(n) > 0) {
 268                 Log.verbose(I18N.getString(
 269                         "message.version-string-first-number-not-zero"));
 270                 return false;
 271             }
 272             if (p.length > 1) {
 273                 n = new BigInteger(p[1]);
 274                 if (BigInteger.ZERO.compareTo(n) > 0) {
 275                     Log.verbose(I18N.getString(
 276                             "message.version-string-no-negative-numbers"));
 277                     return false;
 278                 }
 279             }
 280             if (p.length > 2) {
 281                 n = new BigInteger(p[2]);
 282                 if (BigInteger.ZERO.compareTo(n) > 0) {
 283                     Log.verbose(I18N.getString(
 284                             "message.version-string-no-negative-numbers"));
 285                     return false;
 286                 }
 287             }
 288         } catch (NumberFormatException ne) {
 289             Log.verbose(I18N.getString("message.version-string-numbers-only"));
 290             Log.verbose(ne);
 291             return false;
 292         }
 293 
 294         return true;
 295     }
 296 
 297     @Override
 298     public boolean validate(Map<String, ? super Object> params)
 299             throws UnsupportedPlatformException, ConfigException {
 300         try {
 301             return doValidate(params);
 302         } catch (RuntimeException re) {
 303             if (re.getCause() instanceof ConfigException) {
 304                 throw (ConfigException) re.getCause();
 305             } else {
 306                 throw new ConfigException(re);
 307             }
 308         }
 309     }
 310 
 311     // to be used by chained bundlers, e.g. by EXE bundler to avoid
 312     // skipping validation if p.type does not include "image"
 313     public boolean doValidate(Map<String, ? super Object> p)
 314             throws UnsupportedPlatformException, ConfigException {
 315         if (Platform.getPlatform() != Platform.MAC) {
 316             throw new UnsupportedPlatformException();
 317         }
 318 
 319         imageBundleValidation(p);
 320 
 321         if (StandardBundlerParam.getPredefinedAppImage(p) != null) {
 322             return true;
 323         }
 324 
 325         // validate short version
 326         if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) {
 327             throw new ConfigException(
 328                     I18N.getString("error.invalid-cfbundle-version"),
 329                     I18N.getString("error.invalid-cfbundle-version.advice"));
 330         }
 331 
 332         // reject explicitly set sign to true and no valid signature key
 333         if (Optional.ofNullable(MacAppImageBuilder.
 334                     SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) {
 335             String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
 336             if (signingIdentity == null) {
 337                 throw new ConfigException(
 338                         I18N.getString("error.explicit-sign-no-cert"),
 339                         I18N.getString("error.explicit-sign-no-cert.advice"));
 340             }
 341         }
 342 
 343         return true;
 344     }
 345 
 346     private File getConfig_InfoPlist(Map<String, ? super Object> params) {
 347         return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
 348     }
 349 
 350     private File getConfig_Icon(Map<String, ? super Object> params) {
 351         return new File(CONFIG_ROOT.fetchFrom(params),
 352                 APP_NAME.fetchFrom(params) + ".icns");
 353     }
 354 
 355     File doBundle(Map<String, ? super Object> p, File outputDirectory,
 356             boolean dependentTask) {
 357         if (Arguments.CREATE_JRE_INSTALLER.fetchFrom(p)) {
 358             return doJreBundle(p, outputDirectory, dependentTask);
 359         } else {
 360             return doAppBundle(p, outputDirectory, dependentTask);
 361         }
 362     }
 363 
 364     File doJreBundle(Map<String, ? super Object> p,
 365             File outputDirectory, boolean dependentTask) {
 366         try {
 367             File rootDirectory = createRoot(p, outputDirectory, dependentTask,
 368                     APP_NAME.fetchFrom(p), "macapp-image-builder");
 369             AbstractAppImageBuilder appBuilder = new MacAppImageBuilder(p,
 370                     APP_NAME.fetchFrom(p), outputDirectory.toPath());
 371             File predefined = PREDEFINED_RUNTIME_IMAGE.fetchFrom(p);
 372             if (predefined == null ) {
 373                 JLinkBundlerHelper.generateServerJre(p, appBuilder);
 374             } else {
 375                 return predefined;
 376             }
 377             return rootDirectory;
 378         } catch (Exception ex) {
 379             Log.error("Exception: "+ex);
 380             Log.verbose(ex);
 381             return null;
 382         }
 383     }
 384 
 385     File doAppBundle(Map<String, ? super Object> p, File outputDirectory,
 386             boolean dependentTask) {
 387         try {
 388             File rootDirectory = createRoot(p, outputDirectory, dependentTask,
 389                     APP_NAME.fetchFrom(p) + ".app", "macapp-image-builder");
 390             AbstractAppImageBuilder appBuilder =
 391                     new MacAppImageBuilder(p, outputDirectory.toPath());
 392             if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(p) == null ) {
 393                 JLinkBundlerHelper.execute(p, appBuilder);
 394             } else {
 395                 StandardBundlerParam.copyPredefinedRuntimeImage(p, appBuilder);
 396             }
 397             return rootDirectory;
 398         } catch (Exception ex) {
 399             Log.error("Exception: "+ex);
 400             Log.verbose(ex);
 401             return null;
 402         }
 403     }
 404 
 405     public void cleanupConfigFiles(Map<String, ? super Object> params) {
 406         if (CONFIG_ROOT.fetchFrom(params) != null) {
 407             getConfig_Icon(params).delete();
 408             getConfig_InfoPlist(params).delete();
 409         }
 410     }
 411 
 412     /////////////////////////////////////////////////////////////////////////
 413     // Implement Bundler
 414     /////////////////////////////////////////////////////////////////////////
 415 
 416     @Override
 417     public String getName() {
 418         return I18N.getString("bundler.name");
 419     }
 420 
 421     @Override
 422     public String getDescription() {
 423         return I18N.getString("bundler.description");
 424     }
 425 
 426     @Override
 427     public String getID() {
 428         return "mac.app";
 429     }
 430 
 431     @Override
 432     public String getBundleType() {
 433         return "IMAGE";
 434     }
 435 
 436     @Override
 437     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 438         return getAppBundleParameters();
 439     }
 440 
 441     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
 442         return Arrays.asList(
 443                 APP_NAME,
 444                 APP_RESOURCES,
 445                 ARGUMENTS,
 446                 BUNDLE_ID_SIGNING_PREFIX,
 447                 CLASSPATH,
 448                 DEVELOPER_ID_APP_SIGNING_KEY,
 449                 ICON_ICNS,
 450                 JVM_OPTIONS,
 451                 MAC_CATEGORY,
 452                 MAC_CF_BUNDLE_IDENTIFIER,
 453                 MAC_CF_BUNDLE_NAME,
 454                 MAC_CF_BUNDLE_VERSION,
 455                 MAIN_CLASS,
 456                 MAIN_JAR,
 457                 PREFERENCES_ID,
 458                 SIGNING_KEYCHAIN,
 459                 VERSION,
 460                 VERBOSE
 461         );
 462     }
 463 
 464 
 465     @Override
 466     public File execute(Map<String, ? super Object> params,
 467             File outputParentDir) {
 468         return doBundle(params, outputParentDir, false);
 469     }
 470 
 471     @Override
 472     public boolean supported() {
 473         return Platform.getPlatform() == Platform.MAC;
 474     }
 475 
 476 }