1 /*
   2  * Copyright (c) 2014, 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.BundlerParamInfo;
  29 import jdk.jpackager.internal.StandardBundlerParam;
  30 import jdk.jpackager.internal.Arguments;
  31 import jdk.jpackager.internal.Log;
  32 import jdk.jpackager.internal.ConfigException;
  33 import jdk.jpackager.internal.IOUtils;
  34 import jdk.jpackager.internal.Platform;
  35 import jdk.jpackager.internal.UnsupportedPlatformException;
  36 import jdk.jpackager.internal.builders.mac.MacAppImageBuilder;
  37 import jdk.jpackager.internal.resources.mac.MacResources;
  38 
  39 import java.io.File;
  40 import java.io.IOException;
  41 import java.text.MessageFormat;
  42 import java.util.ArrayList;
  43 import java.util.Arrays;
  44 import java.util.Collection;
  45 import java.util.LinkedHashSet;
  46 import java.util.List;
  47 import java.util.Map;
  48 import java.util.Optional;
  49 import java.util.ResourceBundle;
  50 
  51 import static jdk.jpackager.internal.StandardBundlerParam.*;
  52 import static jdk.jpackager.internal.mac.MacAppBundler.*;
  53 
  54 public class MacAppStoreBundler extends MacBaseInstallerBundler {
  55 
  56     private static final ResourceBundle I18N =
  57             ResourceBundle.getBundle(
  58             "jdk.jpackager.internal.resources.mac.MacAppStoreBundler");
  59 
  60     private static final String TEMPLATE_BUNDLE_ICON_HIDPI =
  61             "GenericAppHiDPI.icns";
  62     private final static String DEFAULT_ENTITLEMENTS =
  63             "MacAppStore.entitlements";
  64     private final static String DEFAULT_INHERIT_ENTITLEMENTS =
  65             "MacAppStore_Inherit.entitlements";
  66 
  67     public static final BundlerParamInfo<String> MAC_APP_STORE_APP_SIGNING_KEY =
  68             new StandardBundlerParam<>(
  69             I18N.getString("param.signing-key-app.name"),
  70             I18N.getString("param.signing-key-app.description"),
  71             "mac.signing-key-app",
  72             String.class,
  73             params -> {
  74                     String result = MacBaseInstallerBundler.findKey(
  75                             "3rd Party Mac Developer Application: " +
  76                                     SIGNING_KEY_USER.fetchFrom(params),
  77                             SIGNING_KEYCHAIN.fetchFrom(params),
  78                             VERBOSE.fetchFrom(params));
  79                     if (result != null) {
  80                         MacCertificate certificate = new MacCertificate(result,
  81                                 VERBOSE.fetchFrom(params));
  82 
  83                         if (!certificate.isValid()) {
  84                             Log.error(MessageFormat.format(
  85                                     I18N.getString("error.certificate.expired"),
  86                                     result));
  87                         }
  88                     }
  89 
  90                     return result;
  91                 },
  92             (s, p) -> s);
  93 
  94     public static final BundlerParamInfo<String> MAC_APP_STORE_PKG_SIGNING_KEY =
  95             new StandardBundlerParam<>(
  96             I18N.getString("param.signing-key-pkg.name"),
  97             I18N.getString("param.signing-key-pkg.description"),
  98             "mac.signing-key-pkg",
  99             String.class,
 100             params -> {
 101                     String result = MacBaseInstallerBundler.findKey(
 102                             "3rd Party Mac Developer Installer: " +
 103                                     SIGNING_KEY_USER.fetchFrom(params),
 104                             SIGNING_KEYCHAIN.fetchFrom(params),
 105                             VERBOSE.fetchFrom(params));
 106 
 107                     if (result != null) {
 108                         MacCertificate certificate = new MacCertificate(
 109                                 result, VERBOSE.fetchFrom(params));
 110 
 111                         if (!certificate.isValid()) {
 112                             Log.error(MessageFormat.format(
 113                                     I18N.getString("error.certificate.expired"),
 114                                     result));
 115                         }
 116                     }
 117 
 118                     return result;
 119                 },
 120             (s, p) -> s);
 121 
 122     public static final StandardBundlerParam<File> MAC_APP_STORE_ENTITLEMENTS  =
 123             new StandardBundlerParam<>(
 124             I18N.getString("param.mac-app-store-entitlements.name"),
 125             I18N.getString("param.mac-app-store-entitlements.description"),
 126             Arguments.CLIOptions.MAC_APP_STORE_ENTITLEMENTS.getId(),
 127             File.class,
 128             params -> null,
 129             (s, p) -> new File(s));
 130 
 131     public static final BundlerParamInfo<String> INSTALLER_SUFFIX =
 132             new StandardBundlerParam<> (
 133             I18N.getString("param.installer-suffix.name"),
 134             I18N.getString("param.installer-suffix.description"),
 135             "mac.app-store.installerName.suffix",
 136             String.class,
 137             params -> "-MacAppStore",
 138             (s, p) -> s);
 139 
 140     public MacAppStoreBundler() {
 141         super();
 142         baseResourceLoader = MacResources.class;
 143     }
 144 
 145     //@Override
 146     public File bundle(Map<String, ? super Object> p, File outdir) {
 147         Log.verbose(MessageFormat.format(I18N.getString(
 148                 "message.building-bundle"), APP_NAME.fetchFrom(p)));
 149         if (!outdir.isDirectory() && !outdir.mkdirs()) {
 150             throw new RuntimeException(MessageFormat.format(I18N.getString(
 151                     "error.cannot-create-output-dir"),
 152                      outdir.getAbsolutePath()));
 153         }
 154         if (!outdir.canWrite()) {
 155             throw new RuntimeException(MessageFormat.format(I18N.getString(
 156                     "error.cannot-write-to-output-dir"),
 157                     outdir.getAbsolutePath()));
 158         }
 159 
 160         // first, load in some overrides
 161         // icns needs @2 versions, so load in the @2 default
 162         p.put(DEFAULT_ICNS_ICON.getID(), TEMPLATE_BUNDLE_ICON_HIDPI);
 163 
 164         // now we create the app
 165         File appImageDir = APP_IMAGE_BUILD_ROOT.fetchFrom(p);
 166         try {
 167             appImageDir.mkdirs();
 168 
 169             try {
 170                 MacAppImageBuilder.addNewKeychain(p);
 171             } catch (InterruptedException e) {
 172                 Log.error(e.getMessage());
 173             }
 174             // first, make sure we don't use the local signing key
 175             p.put(DEVELOPER_ID_APP_SIGNING_KEY.getID(), null);
 176             File appLocation = prepareAppBundle(p, false);
 177 
 178             prepareEntitlements(p);
 179 
 180             String signingIdentity = MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(p);
 181             String identifierPrefix = BUNDLE_ID_SIGNING_PREFIX.fetchFrom(p);
 182             String entitlementsFile = getConfig_Entitlements(p).toString();
 183             String inheritEntitlements =
 184                     getConfig_Inherit_Entitlements(p).toString();
 185 
 186             MacAppImageBuilder.signAppBundle(p, appLocation.toPath(),
 187                     signingIdentity, identifierPrefix,
 188                     entitlementsFile, inheritEntitlements);
 189             MacAppImageBuilder.restoreKeychainList(p);
 190 
 191             ProcessBuilder pb;
 192 
 193             // create the final pkg file
 194             File finalPKG = new File(outdir, INSTALLER_NAME.fetchFrom(p)
 195                     + INSTALLER_SUFFIX.fetchFrom(p)
 196                     + ".pkg");
 197             outdir.mkdirs();
 198 
 199             String installIdentify =
 200                     MAC_APP_STORE_PKG_SIGNING_KEY.fetchFrom(p);
 201 
 202             List<String> buildOptions = new ArrayList<>();
 203             buildOptions.add("productbuild");
 204             buildOptions.add("--component");
 205             buildOptions.add(appLocation.toString());
 206             buildOptions.add("/Applications");
 207             buildOptions.add("--sign");
 208             buildOptions.add(installIdentify);
 209             buildOptions.add("--product");
 210             buildOptions.add(appLocation + "/Contents/Info.plist");
 211             String keychainName = SIGNING_KEYCHAIN.fetchFrom(p);
 212             if (keychainName != null && !keychainName.isEmpty()) {
 213                 buildOptions.add("--keychain");
 214                 buildOptions.add(keychainName);
 215             }
 216             buildOptions.add(finalPKG.getAbsolutePath());
 217 
 218             pb = new ProcessBuilder(buildOptions);
 219 
 220             IOUtils.exec(pb, false);
 221             return finalPKG;
 222         } catch (Exception ex) {
 223             Log.error("App Store Ready Bundle failed : " + ex.getMessage());
 224             Log.verbose(ex);
 225             return null;
 226         } finally {
 227             try {
 228                 if (appImageDir != null &&
 229                        PREDEFINED_APP_IMAGE.fetchFrom(p) == null &&
 230                        (PREDEFINED_RUNTIME_IMAGE.fetchFrom(p) == null ||
 231                        !Arguments.CREATE_JRE_INSTALLER.fetchFrom(p)) &&
 232                        !Log.isDebug()) {
 233                     IOUtils.deleteRecursive(appImageDir);
 234                 } else if (appImageDir != null) {
 235                     Log.verbose(MessageFormat.format(I18N.getString(
 236                             "mesasge.intermediate-bundle-location"),
 237                             appImageDir.getAbsolutePath()));
 238                 }
 239 
 240                 //cleanup
 241                 cleanupConfigFiles(p);
 242             } catch (IOException ex) {
 243                 //noinspection ReturnInsideFinallyBlock
 244                 Log.debug(ex.getMessage());
 245                 return null;
 246             }
 247         }
 248     }
 249 
 250     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
 251         if (getConfig_Entitlements(params) != null) {
 252             getConfig_Entitlements(params).delete();
 253         }
 254         if (getConfig_Inherit_Entitlements(params) != null) {
 255             getConfig_Inherit_Entitlements(params).delete();
 256         }
 257         if (PREDEFINED_APP_IMAGE.fetchFrom(params) == null) {
 258             APP_BUNDLER.fetchFrom(params).cleanupConfigFiles(params);
 259         }
 260     }
 261 
 262     private File getConfig_Entitlements(Map<String, ? super Object> params) {
 263         return new File(CONFIG_ROOT.fetchFrom(params),
 264                 APP_NAME.fetchFrom(params) + ".entitlements");
 265     }
 266 
 267     private File getConfig_Inherit_Entitlements(
 268             Map<String, ? super Object> params) {
 269         return new File(CONFIG_ROOT.fetchFrom(params),
 270                 APP_NAME.fetchFrom(params) + "_Inherit.entitlements");
 271     }
 272 
 273     private void prepareEntitlements(Map<String, ? super Object> params)
 274             throws IOException {
 275         File entitlements = MAC_APP_STORE_ENTITLEMENTS.fetchFrom(params);
 276         if (entitlements == null || !entitlements.exists()) {
 277             fetchResource(getEntitlementsFileName(params),
 278                     I18N.getString("resource.mac-app-store-entitlements"),
 279                     DEFAULT_ENTITLEMENTS,
 280                     getConfig_Entitlements(params),
 281                     VERBOSE.fetchFrom(params),
 282                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 283         } else {
 284             fetchResource(getEntitlementsFileName(params),
 285                     I18N.getString("resource.mac-app-store-entitlements"),
 286                     entitlements,
 287                     getConfig_Entitlements(params),
 288                     VERBOSE.fetchFrom(params),
 289                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 290         }
 291         fetchResource(getInheritEntitlementsFileName(params),
 292                 I18N.getString("resource.mac-app-store-inherit-entitlements"),
 293                 DEFAULT_INHERIT_ENTITLEMENTS,
 294                 getConfig_Inherit_Entitlements(params),
 295                 VERBOSE.fetchFrom(params),
 296                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 297     }
 298 
 299     private String getEntitlementsFileName(Map<String, ? super Object> params) {
 300         return MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) + ".entitlements";
 301     }
 302 
 303     private String getInheritEntitlementsFileName(
 304             Map<String, ? super Object> params) {
 305         return MAC_BUNDLER_PREFIX + APP_NAME.fetchFrom(params)
 306                 + "_Inherit.entitlements";
 307     }
 308 
 309 
 310     ///////////////////////////////////////////////////////////////////////
 311     // Implement Bundler
 312     ///////////////////////////////////////////////////////////////////////
 313 
 314     @Override
 315     public String getName() {
 316         return I18N.getString("bundler.name");
 317     }
 318 
 319     @Override
 320     public String getDescription() {
 321         return I18N.getString("bundler.description");
 322     }
 323 
 324     @Override
 325     public String getID() {
 326         return "mac.appStore";
 327     }
 328 
 329     @Override
 330     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 331         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 332         results.addAll(getAppBundleParameters());
 333         results.addAll(getMacAppStoreBundleParameters());
 334         return results;
 335     }
 336 
 337     public Collection<BundlerParamInfo<?>> getMacAppStoreBundleParameters() {
 338         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 339 
 340         results.addAll(getAppBundleParameters());
 341         results.remove(DEVELOPER_ID_APP_SIGNING_KEY);
 342         results.addAll(Arrays.asList(
 343                 INSTALLER_SUFFIX,
 344                 MAC_APP_STORE_APP_SIGNING_KEY,
 345                 MAC_APP_STORE_ENTITLEMENTS,
 346                 MAC_APP_STORE_PKG_SIGNING_KEY,
 347                 SIGNING_KEYCHAIN
 348         ));
 349 
 350         return results;
 351     }
 352 
 353     @Override
 354     public boolean validate(Map<String, ? super Object> params)
 355             throws UnsupportedPlatformException, ConfigException {
 356         try {
 357             if (Platform.getPlatform() != Platform.MAC) {
 358                 throw new UnsupportedPlatformException();
 359             }
 360 
 361             if (params == null) {
 362                 throw new ConfigException(
 363                         I18N.getString("error.parameters-null"),
 364                         I18N.getString("error.parameters-null.advice"));
 365             }
 366 
 367             // hdiutil is always available so there's no need to test for
 368             // availability.
 369             // run basic validation to ensure requirements are met
 370 
 371             // TODO Mac App Store apps cannot use the system runtime
 372 
 373             // we are not interested in return code, only possible exception
 374             validateAppImageAndBundeler(params);
 375 
 376             // reject explicitly set to not sign
 377             if (!Optional.ofNullable(MacAppImageBuilder.
 378                     SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) {
 379                 throw new ConfigException(
 380                         I18N.getString("error.must-sign-app-store"),
 381                         I18N.getString("error.must-sign-app-store.advice"));
 382             }
 383 
 384             // make sure we have settings for signatures
 385             if (MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(params) == null) {
 386                 throw new ConfigException(
 387                         I18N.getString("error.no-app-signing-key"),
 388                         I18N.getString("error.no-app-signing-key.advice"));
 389             }
 390             if (MAC_APP_STORE_PKG_SIGNING_KEY.fetchFrom(params) == null) {
 391                 throw new ConfigException(
 392                         I18N.getString("error.no-pkg-signing-key"),
 393                         I18N.getString("error.no-pkg-signing-key.advice"));
 394             }
 395 
 396             // things we could check...
 397             // check the icons, make sure it has hidpi icons
 398             // check the category,
 399             // make sure it fits in the list apple has provided
 400             // validate bundle identifier is reverse dns
 401             // check for \a+\.\a+\..
 402 
 403             return true;
 404         } catch (RuntimeException re) {
 405             if (re.getCause() instanceof ConfigException) {
 406                 throw (ConfigException) re.getCause();
 407             } else {
 408                 throw new ConfigException(re);
 409             }
 410         }
 411     }
 412 
 413     @Override
 414     public File execute(Map<String, ? super Object> params,
 415             File outputParentDir) {
 416         return bundle(params, outputParentDir);
 417     }
 418 
 419     @Override
 420     public boolean supported() {
 421         return !Arguments.isJreInstaller() &&
 422                 Platform.getPlatform() == Platform.MAC;
 423     }
 424 }