1 /* 2 * Copyright (c) 2014, 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.bundlers.mac; 27 28 import com.oracle.bundlers.BundlerParamInfo; 29 import com.oracle.bundlers.JreUtils; 30 import com.oracle.bundlers.StandardBundlerParam; 31 import com.sun.javafx.tools.packager.Log; 32 import com.sun.javafx.tools.packager.bundlers.ConfigException; 33 import com.sun.javafx.tools.packager.bundlers.IOUtils; 34 import com.sun.javafx.tools.packager.bundlers.MacAppBundler; 35 import com.sun.javafx.tools.packager.bundlers.UnsupportedPlatformException; 36 import com.sun.javafx.tools.resource.mac.MacResources; 37 38 import java.io.ByteArrayOutputStream; 39 import java.io.File; 40 import java.io.IOException; 41 import java.io.PrintStream; 42 import java.nio.file.Files; 43 import java.nio.file.Path; 44 import java.util.ArrayList; 45 import java.util.Arrays; 46 import java.util.Collection; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.ResourceBundle; 50 import java.util.function.Consumer; 51 import java.util.regex.Matcher; 52 import java.util.regex.Pattern; 53 import java.util.stream.Collectors; 54 55 import static com.oracle.bundlers.JreUtils.Rule.suffix; 56 import static com.oracle.bundlers.JreUtils.Rule.suffixNeg; 57 import static com.oracle.bundlers.StandardBundlerParam.IDENTIFIER; 58 import static com.oracle.bundlers.StandardBundlerParam.APP_NAME; 59 import static com.oracle.bundlers.StandardBundlerParam.VERBOSE; 60 61 public class MacAppStoreBundler extends MacBaseInstallerBundler { 62 63 private static final ResourceBundle I18N = 64 ResourceBundle.getBundle("com.oracle.bundlers.mac.MacAppStoreBundler"); 65 66 private static final String TEMPLATE_BUNDLE_ICON_HIDPI = "GenericAppHiDPI.icns"; 67 private final static String DEFAULT_ENTITLEMENTS = "MacAppStore.entitlements"; 68 private final static String DEFAULT_INHERIT_ENTITLEMENTS = "MacAppStore_Inherit.entitlements"; 69 70 //Subsetting of JRE is restricted. 71 //JRE README defines what is allowed to strip: 72 // http://www.oracle.com/technetwork/java/javase/jre-7-readme-430162.html //TODO update when 8 goes GA 73 // 74 public static final JreUtils.Rule[] MAC_APP_STORE_JDK_RULES = new JreUtils.Rule[]{ 75 suffixNeg("macos/libjli.dylib"), 76 suffixNeg("resources"), 77 suffixNeg("home/bin"), 78 suffixNeg("home/db"), 79 suffixNeg("home/demo"), 80 suffixNeg("home/include"), 81 suffixNeg("home/lib"), 82 suffixNeg("home/man"), 83 suffixNeg("home/release"), 84 suffixNeg("home/sample"), 85 suffixNeg("home/src.zip"), 86 //"home/rt" is not part of the official builds 87 // but we may be creating this symlink to make older NB projects 88 // happy. Make sure to not include it into final artifact 89 suffixNeg("home/rt"), 90 suffixNeg("jre/bin"), 91 suffixNeg("bin/rmiregistry"), 92 suffixNeg("bin/tnameserv"), 93 suffixNeg("bin/keytool"), 94 suffixNeg("bin/klist"), 95 suffixNeg("bin/ktab"), 96 suffixNeg("bin/policytool"), 97 suffixNeg("bin/orbd"), 98 suffixNeg("bin/servertool"), 99 suffixNeg("bin/javaws"), 100 suffixNeg("bin/java"), 101 //Rule.suffixNeg("jre/lib/ext"), //need some of jars there for https to work 102 suffixNeg("jre/lib/nibs"), 103 //keep core deploy APIs but strip plugin dll 104 //Rule.suffixNeg("jre/lib/deploy"), 105 //Rule.suffixNeg("jre/lib/deploy.jar"), 106 //Rule.suffixNeg("jre/lib/javaws.jar"), 107 //Rule.suffixNeg("jre/lib/libdeploy.dylib"), 108 //Rule.suffixNeg("jre/lib/plugin.jar"), 109 suffixNeg("lib/libnpjp2.dylib"), 110 suffixNeg("lib/security/javaws.policy"), 111 112 // jfxmedia uses QuickTime, which is not allowed as of OSX 10.9 113 suffixNeg("lib/libjfxmedia.dylib"), 114 115 // the plist is needed for signing 116 suffix("Info.plist"), 117 118 }; 119 120 public static final BundlerParamInfo<String> MAC_APP_STORE_SIGNING_KEY_USER = new StandardBundlerParam<>( 121 I18N.getString("param.signing-key-name.name"), 122 I18N.getString("param.signing-key-name.description"), 123 "mac.signing-key-user-name", 124 String.class, 125 null, 126 params -> { 127 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos)) { 128 ProcessBuilder pb = new ProcessBuilder( 129 "dscacheutil", 130 "-q", "user", "-a", "name", System.getProperty("user.name")); 131 132 IOUtils.exec(pb, Log.isDebug(), false, ps); 133 134 String commandOutput = baos.toString(); 135 136 Pattern pattern = Pattern.compile(".*gecos: (.*)"); 137 Matcher matcher = pattern.matcher(commandOutput); 138 if (matcher.matches()) { 139 return (matcher.group(1)); 140 } 141 } catch (IOException ioe) { 142 Log.info("Error retrieving gecos name"); 143 Log.debug(ioe); 144 } 145 return null; 146 }, 147 false, 148 null); 149 150 public static final BundlerParamInfo<String> MAC_APP_STORE_APP_SIGNING_KEY = new StandardBundlerParam<>( 151 I18N.getString("param.signing-key-app.name"), 152 I18N.getString("param.signing-key-app.description"), 153 "mac.signing-key-app", 154 String.class, 155 null, 156 params -> "3rd Party Mac Developer Application: " + MAC_APP_STORE_SIGNING_KEY_USER.fetchFrom(params), 157 false, 158 (s, p) -> s); 159 160 public static final BundlerParamInfo<String> MAC_APP_STORE_PKG_SIGNING_KEY = new StandardBundlerParam<>( 161 I18N.getString("param.signing-key-pkg.name"), 162 I18N.getString("param.signing-key-pkg.description"), 163 "mac.signing-key-pkg", 164 String.class, 165 null, 166 params -> "3rd Party Mac Developer Installer: " + MAC_APP_STORE_SIGNING_KEY_USER.fetchFrom(params), 167 false, 168 (s, p) -> s); 169 170 public static final StandardBundlerParam<File> MAC_APP_STORE_ENTITLEMENTS = new StandardBundlerParam<>( 171 I18N.getString("param.mac-app-store-entitlements.name"), 172 I18N.getString("param.mac-app-store-entitlements.description"), 173 "mac.app-store-entitlements", 174 File.class, 175 null, 176 params -> null, 177 false, 178 (s, p) -> new File(s)); 179 180 public MacAppStoreBundler() { 181 super(); 182 baseResourceLoader = MacResources.class; 183 } 184 185 //@Override 186 public File bundle(Map<String, ? super Object> p, File outdir) { 187 Log.info("Building Mac App Store Bundle for " + APP_NAME.fetchFrom(p)); 188 189 // first, load in some overrides 190 // icns needs @2 versions, so load in the @2 default 191 p.put(MacAppBundler.DEFAULT_ICNS_ICON.getID(), TEMPLATE_BUNDLE_ICON_HIDPI); 192 193 // next we need to change the jdk/jre stripping to strip gstreamer 194 p.put(MacAppBundler.MAC_JDK_RULES.getID(), MAC_APP_STORE_JDK_RULES); 195 196 // now we create the app 197 File appImageDir = APP_IMAGE_BUILD_ROOT.fetchFrom(p); 198 try { 199 appImageDir.mkdirs(); 200 File appLocation = prepareAppBundle(p); 201 202 prepareEntitlements(p); 203 204 List<String> args = new ArrayList<>(); 205 args.addAll(Arrays.asList( 206 "codesign", 207 "-s", MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(p), // sign with this key 208 "-f", // replace all existing signatures 209 "--entitlements", getConfig_Entitlements(p).toString() // entitlements 210 )); 211 212 // sign all dylibs and jars 213 List<String> signTargets = Files.walk(appLocation.toPath()) 214 .map(Path::toString) 215 .filter(s -> (s.endsWith(".jar") 216 || s.endsWith(".dylib")) 217 ) 218 .collect(Collectors.toList()); 219 220 args.addAll(signTargets); 221 ProcessBuilder pb = new ProcessBuilder(args); 222 IOUtils.exec(pb, VERBOSE.fetchFrom(p)); 223 224 // sign all contained executables with an inherit entitlement 225 Files.find(appLocation.toPath().resolve("Contents"), Integer.MAX_VALUE, 226 (path, attr) -> (Files.isExecutable(path) && Files.isRegularFile(path))) 227 .filter(path -> (!path.toString().endsWith(".dylib"))) 228 .forEach(path -> { 229 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos)) { 230 ProcessBuilder pb2 = new ProcessBuilder("codesign", 231 "-s", MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(p), // sign with this key 232 "-f", // replace all existing signatures 233 "--prefix", IDENTIFIER.fetchFrom(p), // use the identifier as a prefix 234 "--entitlements", getConfig_Inherit_Entitlements(p).toString(), // entitlements 235 path.toString()); 236 IOUtils.exec(pb2, VERBOSE.fetchFrom(p)); 237 } catch (IOException e) { 238 e.printStackTrace(); 239 } 240 }); 241 242 // sign all plugins and frameworks 243 Consumer<? super Path> signIdentifiedByPList = path -> { 244 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos)) { 245 ProcessBuilder pb2 = new ProcessBuilder("/usr/libexec/PlistBuddy", 246 "-c", "Print :CFBundleIdentifier", path.resolve("Contents/Info.plist").toString()); 247 IOUtils.exec(pb2, VERBOSE.fetchFrom(p), false, ps); 248 String bundleID = baos.toString(); 249 250 pb2 = new ProcessBuilder("codesign", 251 "-s", MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(p), // sign with this key 252 "-f", // replace all existing signatures 253 //"-i", bundleID, // sign the bundle's CFBundleIdentifier 254 path.toString()); 255 IOUtils.exec(pb2, VERBOSE.fetchFrom(p)); 256 } catch (IOException e) { 257 e.printStackTrace(); 258 } 259 }; 260 Path pluginsPath = appLocation.toPath().resolve("Contents/PlugIns"); 261 if (Files.isDirectory(pluginsPath)) { 262 Files.list(pluginsPath) 263 .forEach(signIdentifiedByPList); 264 } 265 Path frameworkPath = appLocation.toPath().resolve("Contents/Frameworks"); 266 if (Files.isDirectory(frameworkPath)) { 267 Files.list(frameworkPath) 268 .forEach(signIdentifiedByPList); 269 } 270 271 // sign the app itself 272 pb = new ProcessBuilder("codesign", 273 "-s", MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(p), // sign with this key 274 "-f", // replace all existing signatures 275 "--entitlements", getConfig_Entitlements(p).toString(), // entitlements 276 appLocation.toString()); 277 IOUtils.exec(pb, VERBOSE.fetchFrom(p)); 278 279 // create the final pkg file 280 File finalPKG = new File(outdir, APP_NAME.fetchFrom(p)+".pkg"); 281 outdir.mkdirs(); 282 283 pb = new ProcessBuilder("productbuild", 284 "--component", appLocation.toString(), "/Applications", 285 "--sign", MAC_APP_STORE_PKG_SIGNING_KEY.fetchFrom(p), 286 "--product", appLocation + "/Contents/Info.plist", 287 finalPKG.getAbsolutePath()); 288 IOUtils.exec(pb, VERBOSE.fetchFrom(p)); 289 return finalPKG; 290 } catch (Exception ex) { 291 Log.info("App Store Ready Bundle failed : " + ex.getMessage()); 292 ex.printStackTrace(); 293 Log.debug(ex); 294 return null; 295 } 296 } 297 298 private File getConfig_Entitlements(Map<String, ? super Object> params) { 299 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".entitlements"); 300 } 301 302 private File getConfig_Inherit_Entitlements(Map<String, ? super Object> params) { 303 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "_Inherit.entitlements"); 304 } 305 306 private void prepareEntitlements(Map<String, ? super Object> params) throws IOException { 307 File entitlements = MAC_APP_STORE_ENTITLEMENTS.fetchFrom(params); 308 if (entitlements == null || !entitlements.exists()) { 309 fetchResource(getEntitlementsFileName(params), 310 I18N.getString("resource.mac-app-store-entitlements"), 311 DEFAULT_ENTITLEMENTS, 312 getConfig_Entitlements(params), 313 VERBOSE.fetchFrom(params)); 314 } else { 315 fetchResource(getEntitlementsFileName(params), 316 I18N.getString("resource.mac-app-store-entitlements"), 317 entitlements, 318 getConfig_Entitlements(params), 319 VERBOSE.fetchFrom(params)); 320 } 321 fetchResource(getInheritEntitlementsFileName(params), 322 I18N.getString("resource.mac-app-store-inherit-entitlements"), 323 DEFAULT_INHERIT_ENTITLEMENTS, 324 getConfig_Inherit_Entitlements(params), 325 VERBOSE.fetchFrom(params)); 326 } 327 328 private String getEntitlementsFileName(Map<String, ? super Object> params) { 329 return MacAppBundler.MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".entitlements"; 330 } 331 332 private String getInheritEntitlementsFileName(Map<String, ? super Object> params) { 333 return MacAppBundler.MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +"_Inherit.entitlements"; 334 } 335 336 ////////////////////////////////////////////////////////////////////////////////// 337 // Implement Bundler 338 ////////////////////////////////////////////////////////////////////////////////// 339 340 @Override 341 public String getName() { 342 return I18N.getString("bundler.name"); 343 } 344 345 @Override 346 public String getDescription() { 347 return I18N.getString("bundler.description"); 348 } 349 350 @Override 351 public String getID() { 352 return "mac.appStore"; 353 } 354 355 @Override 356 public Collection<BundlerParamInfo<?>> getBundleParameters() { 357 //Add PKG Specific parameters as required 358 return super.getBundleParameters(); 359 } 360 361 @Override 362 public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException { 363 try { 364 if (params == null) throw new ConfigException( 365 I18N.getString("error.parameters-null"), 366 I18N.getString("error.parameters-null.advice")); 367 368 // hdiutil is always available so there's no need to test for availability. 369 //run basic validation to ensure requirements are met 370 371 //run basic validation to ensure requirements are met 372 //we are not interested in return code, only possible exception 373 APP_BUNDLER.fetchFrom(params).doValidate(params); 374 375 // more stringent app store validations 376 // check the icons, make sure it has hidpi icons 377 // check the category, make sure it fits in the list apple has provided 378 // make sure we have settings for signatures 379 // validate bundle identifier is reverse dns 380 // check for \a+\.\a+\.. 381 382 return true; 383 } catch (RuntimeException re) { 384 throw new ConfigException(re); 385 } 386 } 387 388 @Override 389 public File execute(Map<String, ? super Object> params, File outputParentDir) { 390 return bundle(params, outputParentDir); 391 } 392 }