1 /* 2 * Copyright (c) 2012, 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 package com.oracle.tools.packager.mac; 26 27 import com.oracle.tools.packager.AbstractImageBundler; 28 import com.oracle.tools.packager.BundlerParamInfo; 29 import com.oracle.tools.packager.ConfigException; 30 import com.oracle.tools.packager.EnumeratedBundlerParam; 31 import com.oracle.tools.packager.IOUtils; 32 import com.oracle.tools.packager.Log; 33 import com.oracle.tools.packager.Platform; 34 import com.oracle.tools.packager.StandardBundlerParam; 35 import com.oracle.tools.packager.UnsupportedPlatformException; 36 import jdk.packager.builders.mac.MacAppImageBuilder; 37 38 import jdk.packager.internal.JLinkBundlerHelper; 39 40 import java.io.File; 41 import java.io.IOException; 42 import java.math.BigInteger; 43 import java.text.MessageFormat; 44 import java.util.Arrays; 45 import java.util.Collection; 46 import java.util.HashMap; 47 import java.util.Map; 48 import java.util.Optional; 49 import java.util.ResourceBundle; 50 51 import static com.oracle.tools.packager.StandardBundlerParam.*; 52 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.*; 53 import jdk.packager.builders.AbstractAppImageBuilder; 54 import jdk.packager.internal.mac.MacCertificate; 55 56 public class MacAppBundler extends AbstractImageBundler { 57 58 private static final ResourceBundle I18N = 59 ResourceBundle.getBundle(MacAppBundler.class.getName()); 60 61 public final static String MAC_BUNDLER_PREFIX = 62 BUNDLER_PREFIX + "macosx" + File.separator; 63 64 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns"; 65 66 private static Map<String, String> getMacCategories() { 67 Map<String, String> map = new HashMap<>(); 68 map.put("Business", "public.app-category.business"); 69 map.put("Developer Tools", "public.app-category.developer-tools"); 70 map.put("Education", "public.app-category.education"); 71 map.put("Entertainment", "public.app-category.entertainment"); 72 map.put("Finance", "public.app-category.finance"); 73 map.put("Games", "public.app-category.games"); 74 map.put("Graphics & Design", "public.app-category.graphics-design"); 75 map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness"); 76 map.put("Lifestyle", "public.app-category.lifestyle"); 77 map.put("Medical", "public.app-category.medical"); 78 map.put("Music", "public.app-category.music"); 79 map.put("News", "public.app-category.news"); 80 map.put("Photography", "public.app-category.photography"); 81 map.put("Productivity", "public.app-category.productivity"); 82 map.put("Reference", "public.app-category.reference"); 83 map.put("Social Networking", "public.app-category.social-networking"); 84 map.put("Sports", "public.app-category.sports"); 85 map.put("Travel", "public.app-category.travel"); 86 map.put("Utilities", "public.app-category.utilities"); 87 map.put("Video", "public.app-category.video"); 88 map.put("Weather", "public.app-category.weather"); 89 90 map.put("Action Games", "public.app-category.action-games"); 91 map.put("Adventure Games", "public.app-category.adventure-games"); 92 map.put("Arcade Games", "public.app-category.arcade-games"); 93 map.put("Board Games", "public.app-category.board-games"); 94 map.put("Card Games", "public.app-category.card-games"); 95 map.put("Casino Games", "public.app-category.casino-games"); 96 map.put("Dice Games", "public.app-category.dice-games"); 97 map.put("Educational Games", "public.app-category.educational-games"); 98 map.put("Family Games", "public.app-category.family-games"); 99 map.put("Kids Games", "public.app-category.kids-games"); 100 map.put("Music Games", "public.app-category.music-games"); 101 map.put("Puzzle Games", "public.app-category.puzzle-games"); 102 map.put("Racing Games", "public.app-category.racing-games"); 103 map.put("Role Playing Games", "public.app-category.role-playing-games"); 104 map.put("Simulation Games", "public.app-category.simulation-games"); 105 map.put("Sports Games", "public.app-category.sports-games"); 106 map.put("Strategy Games", "public.app-category.strategy-games"); 107 map.put("Trivia Games", "public.app-category.trivia-games"); 108 map.put("Word Games", "public.app-category.word-games"); 109 110 return map; 111 } 112 113 public static final EnumeratedBundlerParam<String> MAC_CATEGORY = 114 new EnumeratedBundlerParam<>( 115 I18N.getString("param.category-name"), 116 I18N.getString("param.category-name.description"), 117 "mac.category", 118 String.class, 119 params -> params.containsKey(CATEGORY.getID()) 120 ? CATEGORY.fetchFrom(params) 121 : "Unknown", 122 (s, p) -> s, 123 getMacCategories(), 124 false //strict - for MacStoreBundler this should be strict 125 ); 126 127 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 128 new StandardBundlerParam<>( 129 I18N.getString("param.cfbundle-name.name"), 130 I18N.getString("param.cfbundle-name.description"), 131 "mac.CFBundleName", 132 String.class, 133 params -> null, 134 (s, p) -> s); 135 136 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 137 new StandardBundlerParam<>( 138 I18N.getString("param.cfbundle-identifier.name"), 139 I18N.getString("param.cfbundle-identifier.description"), 140 "mac.CFBundleIdentifier", 141 String.class, 142 IDENTIFIER::fetchFrom, 143 (s, p) -> s); 144 145 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 146 new StandardBundlerParam<>( 147 I18N.getString("param.cfbundle-version.name"), 148 I18N.getString("param.cfbundle-version.description"), 149 "mac.CFBundleVersion", 150 String.class, 151 p -> { 152 String s = VERSION.fetchFrom(p); 153 if (validCFBundleVersion(s)) { 154 return s; 155 } else { 156 return "100"; 157 } 158 }, 159 (s, p) -> s); 160 161 public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>( 162 I18N.getString("param.config-root.name"), 163 I18N.getString("param.config-root.description"), 164 "configRoot", 165 File.class, 166 params -> { 167 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx"); 168 configRoot.mkdirs(); 169 return configRoot; 170 }, 171 (s, p) -> new File(s)); 172 173 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>( 174 I18N.getString("param.default-icon-icns"), 175 I18N.getString("param.default-icon-icns.description"), 176 ".mac.default.icns", 177 String.class, 178 params -> TEMPLATE_BUNDLE_ICON, 179 (s, p) -> s); 180 181 public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>( 182 I18N.getString("param.signing-key-developer-id-app.name"), 183 I18N.getString("param.signing-key-developer-id-app.description"), 184 "mac.signing-key-developer-id-app", 185 String.class, 186 params -> { 187 String result = MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), 188 SIGNING_KEYCHAIN.fetchFrom(params), 189 VERBOSE.fetchFrom(params)); 190 if (result != null) { 191 MacCertificate certificate = new MacCertificate(result, VERBOSE.fetchFrom(params)); 192 193 if (!certificate.isValid()) { 194 Log.info(MessageFormat.format(I18N.getString("error.certificate.expired"), result)); 195 } 196 } 197 198 return result; 199 }, 200 (s, p) -> s); 201 202 public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>( 203 I18N.getString("param.bundle-id-signing-prefix.name"), 204 I18N.getString("param.bundle-id-signing-prefix.description"), 205 "mac.bundle-id-signing-prefix", 206 String.class, 207 params -> IDENTIFIER.fetchFrom(params) + ".", 208 (s, p) -> s); 209 210 public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>( 211 I18N.getString("param.icon-icns.name"), 212 I18N.getString("param.icon-icns.description"), 213 "icon.icns", 214 File.class, 215 params -> { 216 File f = ICON.fetchFrom(params); 217 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 218 Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f)); 219 return null; 220 } 221 return f; 222 }, 223 (s, p) -> new File(s)); 224 225 public MacAppBundler() { 226 super(); 227 baseResourceLoader = MacResources.class; 228 } 229 230 public static boolean validCFBundleVersion(String v) { 231 // CFBundleVersion (String - iOS, OS X) specifies the build version 232 // number of the bundle, which identifies an iteration (released or 233 // unreleased) of the bundle. The build version number should be a 234 // string comprised of three non-negative, period-separated integers 235 // with the first integer being greater than zero. The string should 236 // only contain numeric (0-9) and period (.) characters. Leading zeros 237 // are truncated from each integer and will be ignored (that is, 238 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 239 240 if (v == null) { 241 return false; 242 } 243 244 String p[] = v.split("\\."); 245 if (p.length > 3 || p.length < 1) { 246 Log.verbose(I18N.getString("message.version-string-too-many-components")); 247 return false; 248 } 249 250 try { 251 BigInteger n = new BigInteger(p[0]); 252 if (BigInteger.ONE.compareTo(n) > 0) { 253 Log.verbose(I18N.getString("message.version-string-first-number-not-zero")); 254 return false; 255 } 256 if (p.length > 1) { 257 n = new BigInteger(p[1]); 258 if (BigInteger.ZERO.compareTo(n) > 0) { 259 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 260 return false; 261 } 262 } 263 if (p.length > 2) { 264 n = new BigInteger(p[2]); 265 if (BigInteger.ZERO.compareTo(n) > 0) { 266 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 267 return false; 268 } 269 } 270 } catch (NumberFormatException ne) { 271 Log.verbose(I18N.getString("message.version-string-numbers-only")); 272 Log.verbose(ne); 273 return false; 274 } 275 276 return true; 277 } 278 279 @Override 280 public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException { 281 try { 282 return doValidate(params); 283 } catch (RuntimeException re) { 284 if (re.getCause() instanceof ConfigException) { 285 throw (ConfigException) re.getCause(); 286 } else { 287 throw new ConfigException(re); 288 } 289 } 290 } 291 292 //to be used by chained bundlers, e.g. by EXE bundler to avoid 293 // skipping validation if p.type does not include "image" 294 public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException { 295 if (Platform.getPlatform() != Platform.MAC) { 296 throw new UnsupportedPlatformException(); 297 } 298 299 imageBundleValidation(p); 300 301 if (getPredefinedImage(p) != null) { 302 return true; 303 } 304 305 //TODO warn if MAC_RUNTIME is set 306 307 // validate short version 308 if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) { 309 throw new ConfigException( 310 I18N.getString("error.invalid-cfbundle-version"), 311 I18N.getString("error.invalid-cfbundle-version.advice")); 312 } 313 314 // reject explicitly set sign to true and no valid signature key 315 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) { 316 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p); 317 if (signingIdentity == null) { 318 throw new ConfigException( 319 I18N.getString("error.explicit-sign-no-cert"), 320 I18N.getString("error.explicit-sign-no-cert.advice")); 321 } 322 } 323 324 return true; 325 } 326 327 private File getConfig_InfoPlist(Map<String, ? super Object> params) { 328 return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist"); 329 } 330 331 private File getConfig_Icon(Map<String, ? super Object> params) { 332 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns"); 333 } 334 335 336 File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) { 337 try { 338 if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) { 339 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath())); 340 } 341 if (!outputDirectory.canWrite()) { 342 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath())); 343 } 344 345 // Create directory structure 346 File rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app"); 347 IOUtils.deleteRecursive(rootDirectory); 348 rootDirectory.mkdirs(); 349 350 if (!dependentTask) { 351 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath())); 352 } 353 354 if (!p.containsKey(JLinkBundlerHelper.JLINK_BUILDER.getID())) { 355 p.put(JLinkBundlerHelper.JLINK_BUILDER.getID(), "macapp-image-builder"); 356 } 357 358 AbstractAppImageBuilder appBuilder = new MacAppImageBuilder(p, outputDirectory.toPath()); 359 JLinkBundlerHelper.execute(p, appBuilder); 360 return rootDirectory; 361 } catch (IOException ex) { 362 Log.info(ex.toString()); 363 Log.verbose(ex); 364 return null; 365 } catch (Exception ex) { 366 Log.info("Exception: "+ex); 367 Log.debug(ex); 368 return null; 369 } 370 } 371 372 public void cleanupConfigFiles(Map<String, ? super Object> params) { 373 //Since building the app can be bypassed, make sure configRoot was set 374 if (CONFIG_ROOT.fetchFrom(params) != null) { 375 getConfig_Icon(params).delete(); 376 getConfig_InfoPlist(params).delete(); 377 } 378 } 379 380 ////////////////////////////////////////////////////////////////////////////////// 381 // Implement Bundler 382 ////////////////////////////////////////////////////////////////////////////////// 383 384 @Override 385 public String getName() { 386 return I18N.getString("bundler.name"); 387 } 388 389 @Override 390 public String getDescription() { 391 return I18N.getString("bundler.description"); 392 } 393 394 @Override 395 public String getID() { 396 return "mac.app"; 397 } 398 399 @Override 400 public String getBundleType() { 401 return "IMAGE"; 402 } 403 404 @Override 405 public Collection<BundlerParamInfo<?>> getBundleParameters() { 406 return getAppBundleParameters(); 407 } 408 409 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() { 410 return Arrays.asList( 411 APP_NAME, 412 APP_RESOURCES, 413 ARGUMENTS, 414 BUNDLE_ID_SIGNING_PREFIX, 415 CLASSPATH, 416 DEVELOPER_ID_APP_SIGNING_KEY, 417 ICON_ICNS, 418 JVM_OPTIONS, 419 JVM_PROPERTIES, 420 MAC_CATEGORY, 421 MAC_CF_BUNDLE_IDENTIFIER, 422 MAC_CF_BUNDLE_NAME, 423 MAC_CF_BUNDLE_VERSION, 424 MAIN_CLASS, 425 MAIN_JAR, 426 PREFERENCES_ID, 427 PRELOADER_CLASS, 428 SIGNING_KEYCHAIN, 429 USER_JVM_OPTIONS, 430 VERSION 431 ); 432 } 433 434 435 @Override 436 public File execute(Map<String, ? super Object> params, File outputParentDir) { 437 return doBundle(params, outputParentDir, false); 438 } 439 440 // private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException { 441 // prepareConfigFiles(p); 442 // 443 // if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) { 444 // writeCfgFile(p, rootDirectory); 445 // } else { 446 // writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime"); 447 // } 448 // 449 // // Copy executable root folder 450 // File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p)); 451 // IOUtils.copyFromURL( 452 // RAW_EXECUTABLE_URL.fetchFrom(p), 453 // executableFile); 454 // executableFile.setExecutable(true, false); 455 // 456 // } 457 // 458 459 }