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 }