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