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