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