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