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