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