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