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 public File bundle(Map<String, ? super Object> params, 121 File outdir) throws PackagerException { 122 Log.verbose(MessageFormat.format(I18N.getString( 123 "message.building-bundle"), APP_NAME.fetchFrom(params))); 124 125 IOUtils.writableOutputDir(outdir.toPath()); 126 127 // first, load in some overrides 128 // icns needs @2 versions, so load in the @2 default 129 params.put(DEFAULT_ICNS_ICON.getID(), TEMPLATE_BUNDLE_ICON_HIDPI); 130 131 // now we create the app 132 File appImageDir = APP_IMAGE_TEMP_ROOT.fetchFrom(params); 133 try { 134 appImageDir.mkdirs(); 135 136 try { 137 MacAppImageBuilder.addNewKeychain(params); 138 } catch (InterruptedException e) { 139 Log.error(e.getMessage()); 140 } 141 // first, make sure we don't use the local signing key 142 params.put(DEVELOPER_ID_APP_SIGNING_KEY.getID(), null); 143 File appLocation = prepareAppBundle(params); 144 145 prepareEntitlements(params); 146 147 String signingIdentity = 148 MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(params); 149 String identifierPrefix = 150 BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params); 151 String entitlementsFile = 152 getConfig_Entitlements(params).toString(); 153 String inheritEntitlements = 154 getConfig_Inherit_Entitlements(params).toString(); 155 156 MacAppImageBuilder.signAppBundle(params, appLocation.toPath(), 157 signingIdentity, identifierPrefix, 158 entitlementsFile, inheritEntitlements); 159 MacAppImageBuilder.restoreKeychainList(params); 160 161 ProcessBuilder pb; 162 163 // create the final pkg file 164 File finalPKG = new File(outdir, INSTALLER_NAME.fetchFrom(params) 165 + INSTALLER_SUFFIX.fetchFrom(params) 166 + ".pkg"); 167 outdir.mkdirs(); 168 169 String installIdentify = 170 MAC_APP_STORE_PKG_SIGNING_KEY.fetchFrom(params); 171 172 List<String> buildOptions = new ArrayList<>(); 173 buildOptions.add("productbuild"); 174 buildOptions.add("--component"); 175 buildOptions.add(appLocation.toString()); 176 buildOptions.add("/Applications"); 177 buildOptions.add("--sign"); 178 buildOptions.add(installIdentify); 179 buildOptions.add("--product"); 180 buildOptions.add(appLocation + "/Contents/Info.plist"); 181 String keychainName = SIGNING_KEYCHAIN.fetchFrom(params); 182 if (keychainName != null && !keychainName.isEmpty()) { 183 buildOptions.add("--keychain"); 184 buildOptions.add(keychainName); 185 } 186 buildOptions.add(finalPKG.getAbsolutePath()); 187 188 pb = new ProcessBuilder(buildOptions); 189 190 IOUtils.exec(pb); 191 return finalPKG; 192 } catch (PackagerException pe) { 193 throw pe; 194 } catch (Exception ex) { 195 Log.verbose(ex); 196 throw new PackagerException(ex); 197 } 198 } 199 200 private File getConfig_Entitlements(Map<String, ? super Object> params) { 201 return new File(CONFIG_ROOT.fetchFrom(params), 202 APP_NAME.fetchFrom(params) + ".entitlements"); 203 } 204 205 private File getConfig_Inherit_Entitlements( 206 Map<String, ? super Object> params) { 207 return new File(CONFIG_ROOT.fetchFrom(params), 208 APP_NAME.fetchFrom(params) + "_Inherit.entitlements"); 209 } 210 211 private void prepareEntitlements(Map<String, ? super Object> params) 212 throws IOException { 213 File entitlements = MAC_APP_STORE_ENTITLEMENTS.fetchFrom(params); 214 if (entitlements == null || !entitlements.exists()) { 215 fetchResource(getEntitlementsFileName(params), 216 I18N.getString("resource.mac-app-store-entitlements"), 217 DEFAULT_ENTITLEMENTS, 218 getConfig_Entitlements(params), 219 VERBOSE.fetchFrom(params), 220 RESOURCE_DIR.fetchFrom(params)); 221 } else { 222 fetchResource(getEntitlementsFileName(params), 223 I18N.getString("resource.mac-app-store-entitlements"), 224 entitlements, 225 getConfig_Entitlements(params), 226 VERBOSE.fetchFrom(params), 227 RESOURCE_DIR.fetchFrom(params)); 228 } 229 fetchResource(getInheritEntitlementsFileName(params), 230 I18N.getString("resource.mac-app-store-inherit-entitlements"), 231 DEFAULT_INHERIT_ENTITLEMENTS, 232 getConfig_Inherit_Entitlements(params), 233 VERBOSE.fetchFrom(params), 234 RESOURCE_DIR.fetchFrom(params)); 235 } 236 237 private String getEntitlementsFileName(Map<String, ? super Object> params) { 238 return APP_NAME.fetchFrom(params) + ".entitlements"; 239 } 240 241 private String getInheritEntitlementsFileName( 242 Map<String, ? super Object> params) { 243 return APP_NAME.fetchFrom(params) + "_Inherit.entitlements"; 244 } 245 246 247 /////////////////////////////////////////////////////////////////////// 248 // Implement Bundler 249 /////////////////////////////////////////////////////////////////////// 250 251 @Override 252 public String getName() { 253 return I18N.getString("store.bundler.name"); 254 } 255 256 @Override 257 public String getID() { 258 return "mac.appStore"; 259 } 260 261 @Override 262 public boolean validate(Map<String, ? super Object> params) 263 throws ConfigException { 264 try { 265 if (params == null) { 266 throw new ConfigException( 267 I18N.getString("error.parameters-null"), 268 I18N.getString("error.parameters-null.advice")); 269 } 270 271 // hdiutil is always available so there's no need to test for 272 // availability. 273 // run basic validation to ensure requirements are met 274 275 // we are not interested in return code, only possible exception 276 validateAppImageAndBundeler(params); 277 278 // reject explicitly set to not sign 279 if (!Optional.ofNullable(MacAppImageBuilder. 280 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { 281 throw new ConfigException( 282 I18N.getString("error.must-sign-app-store"), 283 I18N.getString("error.must-sign-app-store.advice")); 284 } 285 286 // make sure we have settings for signatures 287 if (MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(params) == null) { 288 throw new ConfigException( 289 I18N.getString("error.no-app-signing-key"), 290 I18N.getString("error.no-app-signing-key.advice")); 291 } 292 if (MAC_APP_STORE_PKG_SIGNING_KEY.fetchFrom(params) == null) { 293 throw new ConfigException( 294 I18N.getString("error.no-pkg-signing-key"), 295 I18N.getString("error.no-pkg-signing-key.advice")); 296 } 297 298 // things we could check... 299 // check the icons, make sure it has hidpi icons 300 // check the category, 301 // make sure it fits in the list apple has provided 302 // validate bundle identifier is reverse dns 303 // check for \a+\.\a+\.. 304 305 return true; 306 } catch (RuntimeException re) { 307 if (re.getCause() instanceof ConfigException) { 308 throw (ConfigException) re.getCause(); 309 } else { 310 throw new ConfigException(re); 311 } 312 } 313 } 314 315 @Override 316 public File execute(Map<String, ? super Object> params, 317 File outputParentDir) throws PackagerException { 318 return bundle(params, outputParentDir); 319 } 320 321 @Override 322 public boolean supported(boolean runtimeInstaller) { 323 // return (!runtimeInstaller && 324 // Platform.getPlatform() == Platform.MAC); 325 return false; // mac-app-store not yet supported 326 } 327 }