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