1 /*
   2  * Copyright (c) 2014, 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 com.oracle.tools.packager.mac;
  27 
  28 import com.oracle.tools.packager.BundlerParamInfo;
  29 import com.oracle.tools.packager.JreUtils;
  30 import com.oracle.tools.packager.StandardBundlerParam;
  31 import com.oracle.tools.packager.Log;
  32 import com.oracle.tools.packager.ConfigException;
  33 import com.oracle.tools.packager.IOUtils;
  34 import com.oracle.tools.packager.UnsupportedPlatformException;
  35 
  36 import java.io.File;
  37 import java.io.FileNotFoundException;
  38 import java.io.IOException;
  39 import java.text.MessageFormat;
  40 import java.util.Arrays;
  41 import java.util.Collection;
  42 import java.util.LinkedHashSet;
  43 import java.util.Map;
  44 import java.util.ResourceBundle;
  45 
  46 import static com.oracle.tools.packager.JreUtils.Rule.suffix;
  47 import static com.oracle.tools.packager.JreUtils.Rule.suffixNeg;
  48 import static com.oracle.tools.packager.StandardBundlerParam.*;
  49 
  50 public class MacAppStoreBundler extends MacBaseInstallerBundler {
  51 
  52     private static final ResourceBundle I18N =
  53             ResourceBundle.getBundle(MacAppStoreBundler.class.getName());
  54 
  55     private static final String TEMPLATE_BUNDLE_ICON_HIDPI = "GenericAppHiDPI.icns";
  56     private final static String DEFAULT_ENTITLEMENTS = "MacAppStore.entitlements";
  57     private final static String DEFAULT_INHERIT_ENTITLEMENTS = "MacAppStore_Inherit.entitlements";
  58 
  59     //Subsetting of JRE is restricted.
  60     //JRE README defines what is allowed to strip:
  61     //   http://www.oracle.com/technetwork/java/javase/jre-7-readme-430162.html //TODO update when 8 goes GA
  62     //
  63     public static final JreUtils.Rule[] MAC_APP_STORE_JDK_RULES =  new JreUtils.Rule[]{
  64             suffixNeg("macos/libjli.dylib"),
  65             suffixNeg("resources"),
  66             suffixNeg("home/bin"),
  67             suffixNeg("home/db"),
  68             suffixNeg("home/demo"),
  69             suffixNeg("home/include"),
  70             suffixNeg("home/lib"),
  71             suffixNeg("home/man"),
  72             suffixNeg("home/release"),
  73             suffixNeg("home/sample"),
  74             suffixNeg("home/src.zip"),
  75             //"home/rt" is not part of the official builds
  76             // but we may be creating this symlink to make older NB projects
  77             // happy. Make sure to not include it into final artifact
  78             suffixNeg("home/rt"),
  79             suffixNeg("jre/bin"),
  80             suffixNeg("bin/rmiregistry"),
  81             suffixNeg("bin/tnameserv"),
  82             suffixNeg("bin/keytool"),
  83             suffixNeg("bin/klist"),
  84             suffixNeg("bin/ktab"),
  85             suffixNeg("bin/policytool"),
  86             suffixNeg("bin/orbd"),
  87             suffixNeg("bin/servertool"),
  88             suffixNeg("bin/javaws"),
  89             suffixNeg("bin/java"),
  90             //Rule.suffixNeg("jre/lib/ext"), //need some of jars there for https to work
  91             suffixNeg("jre/lib/nibs"),
  92             //keep core deploy APIs but strip plugin dll
  93             //Rule.suffixNeg("jre/lib/deploy"),
  94             //Rule.suffixNeg("jre/lib/deploy.jar"),
  95             //Rule.suffixNeg("jre/lib/javaws.jar"),
  96             //Rule.suffixNeg("jre/lib/libdeploy.dylib"),
  97             //Rule.suffixNeg("jre/lib/plugin.jar"),
  98             suffixNeg("lib/libnpjp2.dylib"),
  99             suffixNeg("lib/security/javaws.policy"),
 100 
 101             // jfxmedia uses QuickTime, which is not allowed as of OSX 10.9
 102             suffixNeg("lib/libjfxmedia.dylib"),
 103 
 104             // the plist is needed for signing
 105             suffix("Info.plist"),
 106 
 107     };
 108 
 109     public static final BundlerParamInfo<String> MAC_APP_STORE_APP_SIGNING_KEY = new StandardBundlerParam<>(
 110             I18N.getString("param.signing-key-app.name"),
 111             I18N.getString("param.signing-key-app.description"),
 112             "mac.signing-key-app",
 113             String.class,
 114             params -> {
 115                 String key = "3rd Party Mac Developer Application: " + SIGNING_KEY_USER.fetchFrom(params);
 116                 try {
 117                     IOUtils.exec(new ProcessBuilder("security", "find-certificate", "-c", key), VERBOSE.fetchFrom(params));
 118                     return key;
 119                 } catch (IOException ioe) {
 120                     return null;
 121                 }
 122             },
 123             (s, p) -> s);
 124 
 125     public static final BundlerParamInfo<String> MAC_APP_STORE_PKG_SIGNING_KEY = new StandardBundlerParam<>(
 126             I18N.getString("param.signing-key-pkg.name"),
 127             I18N.getString("param.signing-key-pkg.description"),
 128             "mac.signing-key-pkg",
 129             String.class,
 130             params -> {
 131                 String key = "3rd Party Mac Developer Installer: " + SIGNING_KEY_USER.fetchFrom(params);
 132                 try {
 133                     IOUtils.exec(new ProcessBuilder("security", "find-certificate", "-c", key), VERBOSE.fetchFrom(params));
 134                     return key;
 135                 } catch (IOException ioe) {
 136                     return null;
 137                 }
 138             },
 139             (s, p) -> s);
 140 
 141     public static final StandardBundlerParam<File> MAC_APP_STORE_ENTITLEMENTS  = new StandardBundlerParam<>(
 142             I18N.getString("param.mac-app-store-entitlements.name"),
 143             I18N.getString("param.mac-app-store-entitlements.description"),
 144             "mac.app-store-entitlements",
 145             File.class,
 146             params -> null,
 147             (s, p) -> new File(s));
 148 
 149     public MacAppStoreBundler() {
 150         super();
 151         baseResourceLoader = MacResources.class;
 152     }
 153 
 154     //@Override
 155     public File bundle(Map<String, ? super Object> p, File outdir) {
 156         Log.info(MessageFormat.format(I18N.getString("message.building-bundle"), APP_NAME.fetchFrom(p)));
 157         if (!outdir.isDirectory() && !outdir.mkdirs()) {
 158             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outdir.getAbsolutePath()));
 159         }
 160         if (!outdir.canWrite()) {
 161             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outdir.getAbsolutePath()));
 162         }
 163 
 164         // first, load in some overrides
 165         // icns needs @2 versions, so load in the @2 default
 166         p.put(MacAppBundler.DEFAULT_ICNS_ICON.getID(), TEMPLATE_BUNDLE_ICON_HIDPI);
 167 
 168         // next we need to change the jdk/jre stripping to strip gstreamer
 169         p.put(MacAppBundler.MAC_JDK_RULES.getID(), MAC_APP_STORE_JDK_RULES);
 170 
 171         // now we create the app
 172         File appImageDir = APP_IMAGE_BUILD_ROOT.fetchFrom(p);
 173         try {
 174             appImageDir.mkdirs();
 175 
 176             // first, make sure we don't use the local signing key
 177             p.put(MacAppBundler.DEVELOPER_ID_APP_SIGNING_KEY.getID(), null);
 178             File appLocation = prepareAppBundle(p);
 179 
 180             prepareEntitlements(p);
 181 
 182             String signingIdentity = MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(p);
 183             String identifierPrefix = MacAppBundler.BUNDLE_ID_SIGNING_PREFIX.fetchFrom(p);
 184             String entitlementsFile = getConfig_Entitlements(p).toString();
 185             String inheritEntitlements = getConfig_Inherit_Entitlements(p).toString();
 186 
 187             signAppBundle(p, appLocation, signingIdentity, identifierPrefix, entitlementsFile, inheritEntitlements);
 188             ProcessBuilder pb;
 189 
 190             // create the final pkg file
 191             File finalPKG = new File(outdir, INSTALLER_NAME.fetchFrom(p)+"-MacAppStore.pkg");
 192             outdir.mkdirs();
 193 
 194             pb = new ProcessBuilder("productbuild",
 195                     "--component", appLocation.toString(), "/Applications",
 196                     "--sign", MAC_APP_STORE_PKG_SIGNING_KEY.fetchFrom(p),
 197                     "--product", appLocation + "/Contents/Info.plist",
 198                     finalPKG.getAbsolutePath());
 199             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 200             return finalPKG;
 201         } catch (Exception ex) {
 202             Log.info("App Store Ready Bundle failed : " + ex.getMessage());
 203             ex.printStackTrace();
 204             Log.debug(ex);
 205             return null;
 206         } finally {
 207             try {
 208                 if (appImageDir != null && !Log.isDebug()) {
 209                     IOUtils.deleteRecursive(appImageDir);
 210                 } else if (appImageDir != null) {
 211                     Log.info(MessageFormat.format(I18N.getString("mesasge.intermediate-bundle-location"), appImageDir.getAbsolutePath()));
 212                 }
 213                 if (!VERBOSE.fetchFrom(p)) {
 214                     //cleanup
 215                     cleanupConfigFiles(p);
 216                 } else {
 217                     Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath()));
 218                 }
 219             } catch (FileNotFoundException ex) {
 220                 //noinspection ReturnInsideFinallyBlock
 221                 return null;
 222             }
 223         }
 224     }
 225 
 226     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
 227         if (getConfig_Entitlements(params) != null) {
 228             getConfig_Entitlements(params).delete();
 229         }
 230         if (getConfig_Inherit_Entitlements(params) != null) {
 231             getConfig_Inherit_Entitlements(params).delete();
 232         }
 233         if (MAC_APP_IMAGE.fetchFrom(params) == null) {
 234             APP_BUNDLER.fetchFrom(params).cleanupConfigFiles(params);
 235         }
 236     }
 237 
 238     private File getConfig_Entitlements(Map<String, ? super Object> params) {
 239         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".entitlements");
 240     }
 241 
 242     private File getConfig_Inherit_Entitlements(Map<String, ? super Object> params) {
 243         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "_Inherit.entitlements");
 244     }
 245 
 246     private void prepareEntitlements(Map<String, ? super Object> params) throws IOException {
 247         File entitlements = MAC_APP_STORE_ENTITLEMENTS.fetchFrom(params);
 248         if (entitlements == null || !entitlements.exists()) {
 249             fetchResource(getEntitlementsFileName(params),
 250                     I18N.getString("resource.mac-app-store-entitlements"),
 251                     DEFAULT_ENTITLEMENTS,
 252                     getConfig_Entitlements(params),
 253                     VERBOSE.fetchFrom(params));
 254         } else {
 255             fetchResource(getEntitlementsFileName(params),
 256                     I18N.getString("resource.mac-app-store-entitlements"),
 257                     entitlements,
 258                     getConfig_Entitlements(params),
 259                     VERBOSE.fetchFrom(params));
 260         }
 261         fetchResource(getInheritEntitlementsFileName(params),
 262                 I18N.getString("resource.mac-app-store-inherit-entitlements"),
 263                 DEFAULT_INHERIT_ENTITLEMENTS,
 264                 getConfig_Inherit_Entitlements(params),
 265                 VERBOSE.fetchFrom(params));
 266     }
 267 
 268     private String getEntitlementsFileName(Map<String, ? super Object> params) {
 269         return MacAppBundler.MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".entitlements";
 270     }
 271 
 272     private String getInheritEntitlementsFileName(Map<String, ? super Object> params) {
 273         return MacAppBundler.MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +"_Inherit.entitlements";
 274     }
 275 
 276     //////////////////////////////////////////////////////////////////////////////////
 277     // Implement Bundler
 278     //////////////////////////////////////////////////////////////////////////////////
 279 
 280     @Override
 281     public String getName() {
 282         return I18N.getString("bundler.name");
 283     }
 284 
 285     @Override
 286     public String getDescription() {
 287         return I18N.getString("bundler.description");
 288     }
 289 
 290     @Override
 291     public String getID() {
 292         return "mac.appStore";
 293     }
 294 
 295     @Override
 296     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 297         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 298         results.addAll(MacAppBundler.getAppBundleParameters());
 299         results.addAll(getPKGBundleParameters());
 300         return results;
 301     }
 302 
 303     public Collection<BundlerParamInfo<?>> getPKGBundleParameters() {
 304         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 305 
 306         results.addAll(MacAppBundler.getAppBundleParameters());
 307         results.remove(MacAppBundler.DEVELOPER_ID_APP_SIGNING_KEY);
 308         results.addAll(Arrays.asList(
 309                 MAC_APP_STORE_APP_SIGNING_KEY,
 310                 MAC_APP_STORE_ENTITLEMENTS,
 311                 MAC_APP_STORE_PKG_SIGNING_KEY
 312         ));
 313 
 314         return results;
 315     }
 316 
 317     @Override
 318     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
 319         try {
 320             if (params == null) throw new ConfigException(
 321                     I18N.getString("error.parameters-null"),
 322                     I18N.getString("error.parameters-null.advice"));
 323 
 324             // hdiutil is always available so there's no need to test for availability.
 325             //run basic validation to ensure requirements are met
 326 
 327             //run basic validation to ensure requirements are met
 328 
 329             //we need to change the jdk/jre stripping to strip gstreamer
 330             params.put(MacAppBundler.MAC_JDK_RULES.getID(), MAC_APP_STORE_JDK_RULES);
 331 
 332             //we are not interested in return code, only possible exception
 333             validateAppImageAndBundeler(params);
 334 
 335             // make sure we have settings for signatures
 336             if (MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(params) == null) {
 337                 throw new ConfigException(
 338                         I18N.getString("error.no-app-signing-key"),
 339                         I18N.getString("error.no-app-signing-key.advice"));
 340             }
 341             if (MAC_APP_STORE_PKG_SIGNING_KEY.fetchFrom(params) == null) {
 342                 throw new ConfigException(
 343                         I18N.getString("error.no-pkg-signing-key"),
 344                         I18N.getString("error.no-pkg-signing-key.advice"));
 345             }
 346 
 347             // things we could check...
 348             // check the icons, make sure it has hidpi icons
 349             // check the category, make sure it fits in the list apple has provided
 350             // validate bundle identifier is reverse dns
 351             //  check for \a+\.\a+\..
 352 
 353             return true;
 354         } catch (RuntimeException re) {
 355             if (re.getCause() instanceof ConfigException) {
 356                 throw (ConfigException) re.getCause();
 357             } else {
 358                 throw new ConfigException(re);
 359             }
 360         }
 361     }
 362 
 363     @Override
 364     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 365         return bundle(params, outputParentDir);
 366     }
 367 }