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