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