1 /* 2 * Copyright (c) 2012, 2015, 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.AbstractBundler; 28 import com.oracle.tools.packager.BundlerParamInfo; 29 import com.oracle.tools.packager.EnumeratedBundlerParam; 30 import com.oracle.tools.packager.JreUtils; 31 import com.oracle.tools.packager.JreUtils.Rule; 32 import com.oracle.tools.packager.StandardBundlerParam; 33 import com.oracle.tools.packager.Log; 34 import com.sun.javafx.tools.packager.bundlers.BundleParams; 35 import com.oracle.tools.packager.ConfigException; 36 import com.oracle.tools.packager.IOUtils; 37 import com.oracle.tools.packager.RelativeFileSet; 38 import com.oracle.tools.packager.UnsupportedPlatformException; 39 40 import java.io.*; 41 import java.math.BigInteger; 42 import java.net.MalformedURLException; 43 import java.net.URL; 44 import java.text.MessageFormat; 45 import java.util.*; 46 47 import static com.oracle.tools.packager.StandardBundlerParam.*; 48 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN; 49 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEY_USER; 50 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.getPredefinedImage; 51 52 public class MacAppBundler extends AbstractBundler { 53 54 private static final ResourceBundle I18N = 55 ResourceBundle.getBundle(MacAppBundler.class.getName()); 56 57 public final static String MAC_BUNDLER_PREFIX = 58 BUNDLER_PREFIX + "macosx" + File.separator; 59 60 private static final String EXECUTABLE_NAME = "JavaAppLauncher"; 61 private final static String LIBRARY_NAME = "libpackager.dylib"; 62 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns"; 63 private static final String OS_TYPE_CODE = "APPL"; 64 private static final String TEMPLATE_INFO_PLIST = "Info.plist.template"; 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<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>( 174 I18N.getString("param.raw-executable-url.name"), 175 I18N.getString("param.raw-executable-url.description"), 176 "mac.launcher.url", 177 URL.class, 178 params -> MacResources.class.getResource(EXECUTABLE_NAME), 179 (s, p) -> { 180 try { 181 return new URL(s); 182 } catch (MalformedURLException e) { 183 Log.info(e.toString()); 184 return null; 185 } 186 }); 187 188 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>( 189 I18N.getString("param.default-icon-icns"), 190 I18N.getString("param.default-icon-icns.description"), 191 ".mac.default.icns", 192 String.class, 193 params -> TEMPLATE_BUNDLE_ICON, 194 (s, p) -> s); 195 196 public static final BundlerParamInfo<Rule[]> MAC_RULES = new StandardBundlerParam<>( 197 "", 198 "", 199 ".mac.runtime.rules", 200 Rule[].class, 201 MacAppBundler::createMacRuntimeRules, 202 (s, p) -> null 203 ); 204 205 public static final BundlerParamInfo<RelativeFileSet> MAC_RUNTIME = new StandardBundlerParam<>( 206 I18N.getString("param.runtime.name"), 207 I18N.getString("param.runtime.description"), 208 BundleParams.PARAM_RUNTIME, 209 RelativeFileSet.class, 210 params -> extractMacRuntime(System.getProperty("java.home"), params), 211 MacAppBundler::extractMacRuntime 212 ); 213 214 public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>( 215 I18N.getString("param.signing-key-developer-id-app.name"), 216 I18N.getString("param.signing-key-developer-id-app.description"), 217 "mac.signing-key-developer-id-app", 218 String.class, 219 params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)), 220 (s, p) -> s); 221 222 public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>( 223 I18N.getString("param.bundle-id-signing-prefix.name"), 224 I18N.getString("param.bundle-id-signing-prefix.description"), 225 "mac.bundle-id-signing-prefix", 226 String.class, 227 params -> IDENTIFIER.fetchFrom(params) + ".", 228 (s, p) -> s); 229 230 public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>( 231 I18N.getString("param.icon-icns.name"), 232 I18N.getString("param.icon-icns.description"), 233 "icon.icns", 234 File.class, 235 params -> { 236 File f = ICON.fetchFrom(params); 237 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 238 Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f)); 239 return null; 240 } 241 return f; 242 }, 243 (s, p) -> new File(s)); 244 245 public static RelativeFileSet extractMacRuntime(String base, Map<String, ? super Object> params) { 246 if (base.isEmpty()) { 247 return null; 248 } 249 250 File workingBase = new File(base); 251 workingBase = workingBase.getAbsoluteFile(); 252 try { 253 workingBase = workingBase.getCanonicalFile(); 254 } catch (IOException ignore) { 255 // we tried, workingBase will remain absolute and not canonical. 256 } 257 258 if (workingBase.getName().equals("jre")) { 259 workingBase = workingBase.getParentFile(); 260 } 261 if (workingBase.getName().equals("Home")) { 262 workingBase = workingBase.getParentFile(); 263 } 264 if (workingBase.getName().equals("Contents")) { 265 workingBase = workingBase.getParentFile(); 266 } 267 return JreUtils.extractJreAsRelativeFileSet(workingBase.toString(), 268 MAC_RULES.fetchFrom(params), true); 269 } 270 271 public MacAppBundler() { 272 super(); 273 baseResourceLoader = MacResources.class; 274 } 275 276 public static boolean validCFBundleVersion(String v) { 277 // CFBundleVersion (String - iOS, OS X) specifies the build version 278 // number of the bundle, which identifies an iteration (released or 279 // unreleased) of the bundle. The build version number should be a 280 // string comprised of three non-negative, period-separated integers 281 // with the first integer being greater than zero. The string should 282 // only contain numeric (0-9) and period (.) characters. Leading zeros 283 // are truncated from each integer and will be ignored (that is, 284 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 285 286 if (v == null) { 287 return false; 288 } 289 290 String p[] = v.split("\\."); 291 if (p.length > 3 || p.length < 1) { 292 Log.verbose(I18N.getString("message.version-string-too-many-components")); 293 return false; 294 } 295 296 try { 297 BigInteger n = new BigInteger(p[0]); 298 if (BigInteger.ONE.compareTo(n) > 0) { 299 Log.verbose(I18N.getString("message.version-string-first-number-not-zero")); 300 return false; 301 } 302 if (p.length > 1) { 303 n = new BigInteger(p[1]); 304 if (BigInteger.ZERO.compareTo(n) > 0) { 305 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 306 return false; 307 } 308 } 309 if (p.length > 2) { 310 n = new BigInteger(p[2]); 311 if (BigInteger.ZERO.compareTo(n) > 0) { 312 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 313 return false; 314 } 315 } 316 } catch (NumberFormatException ne) { 317 Log.verbose(I18N.getString("message.version-string-numbers-only")); 318 Log.verbose(ne); 319 return false; 320 } 321 322 return true; 323 } 324 325 @Override 326 public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException { 327 try { 328 return doValidate(params); 329 } catch (RuntimeException re) { 330 if (re.getCause() instanceof ConfigException) { 331 throw (ConfigException) re.getCause(); 332 } else { 333 throw new ConfigException(re); 334 } 335 } 336 } 337 338 //to be used by chained bundlers, e.g. by EXE bundler to avoid 339 // skipping validation if p.type does not include "image" 340 public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException { 341 if (!System.getProperty("os.name").toLowerCase().contains("os x")) { 342 throw new UnsupportedPlatformException(); 343 } 344 345 StandardBundlerParam.validateMainClassInfoFromAppResources(p); 346 347 Map<String, String> userJvmOptions = USER_JVM_OPTIONS.fetchFrom(p); 348 if (userJvmOptions != null) { 349 for (Map.Entry<String, String> entry : userJvmOptions.entrySet()) { 350 if (entry.getValue() == null || entry.getValue().isEmpty()) { 351 throw new ConfigException( 352 MessageFormat.format(I18N.getString("error.empty-user-jvm-option-value"), entry.getKey()), 353 I18N.getString("error.empty-user-jvm-option-value.advice")); 354 } 355 } 356 } 357 358 if (getPredefinedImage(p) != null) { 359 return true; 360 } 361 362 if (MAIN_JAR.fetchFrom(p) == null) { 363 throw new ConfigException( 364 I18N.getString("error.no-application-jar"), 365 I18N.getString("error.no-application-jar.advice")); 366 } 367 368 //validate required inputs 369 testRuntime(MAC_RUNTIME.fetchFrom(p), new String[] { 370 "Contents/Home/(jre/)?lib/[^/]+/libjvm.dylib", // most reliable 371 "Contents/Home/(jre/)?lib/rt.jar", // fallback canary for JDK 8 372 }); 373 if (USE_FX_PACKAGING.fetchFrom(p)) { 374 testRuntime(MAC_RUNTIME.fetchFrom(p), new String[] {"Contents/Home/(jre/)?lib/ext/jfxrt.jar", "Contents/Home/(jre/)?lib/jfxrt.jar"}); 375 } 376 377 // validate short version 378 if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) { 379 throw new ConfigException( 380 I18N.getString("error.invalid-cfbundle-version"), 381 I18N.getString("error.invalid-cfbundle-version.advice")); 382 } 383 384 // reject explicitly set sign to true and no valid signature key 385 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) { 386 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p); 387 if (signingIdentity == null) { 388 throw new ConfigException( 389 I18N.getString("error.explicit-sign-no-cert"), 390 I18N.getString("error.explicit-sign-no-cert.advice")); 391 } 392 } 393 394 return true; 395 } 396 397 private File getConfig_InfoPlist(Map<String, ? super Object> params) { 398 return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist"); 399 } 400 401 private File getConfig_Icon(Map<String, ? super Object> params) { 402 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns"); 403 } 404 405 private void prepareConfigFiles(Map<String, ? super Object> params) throws IOException { 406 File infoPlistFile = getConfig_InfoPlist(params); 407 infoPlistFile.createNewFile(); 408 writeInfoPlist(infoPlistFile, params); 409 410 // Copy icon to Resources folder 411 prepareIcon(params); 412 } 413 414 public File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) { 415 File rootDirectory = null; 416 if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) { 417 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath())); 418 } 419 if (!outputDirectory.canWrite()) { 420 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath())); 421 } 422 423 try { 424 final File predefinedImage = getPredefinedImage(p); 425 if (predefinedImage != null) { 426 return predefinedImage; 427 } 428 429 // side effect is temp dir is created if not specified 430 BUILD_ROOT.fetchFrom(p); 431 432 //prepare config resources (we will copy them to the bundle later) 433 // NB: explicitly saving them to simplify customization 434 prepareConfigFiles(p); 435 436 // Create directory structure 437 rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app"); 438 IOUtils.deleteRecursive(rootDirectory); 439 rootDirectory.mkdirs(); 440 441 if (!dependentTask) { 442 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath())); 443 } 444 445 File contentsDirectory = new File(rootDirectory, "Contents"); 446 contentsDirectory.mkdirs(); 447 448 File macOSDirectory = new File(contentsDirectory, "MacOS"); 449 macOSDirectory.mkdirs(); 450 451 File javaDirectory = new File(contentsDirectory, "Java"); 452 javaDirectory.mkdirs(); 453 454 File plugInsDirectory = new File(contentsDirectory, "PlugIns"); 455 456 File resourcesDirectory = new File(contentsDirectory, "Resources"); 457 resourcesDirectory.mkdirs(); 458 459 // Generate PkgInfo 460 File pkgInfoFile = new File(contentsDirectory, "PkgInfo"); 461 pkgInfoFile.createNewFile(); 462 writePkgInfo(pkgInfoFile); 463 464 // Copy executable to MacOS folder 465 File executableFile = new File(macOSDirectory, getLauncherName(p)); 466 IOUtils.copyFromURL( 467 RAW_EXECUTABLE_URL.fetchFrom(p), 468 executableFile); 469 470 // Copy library to the MacOS folder 471 IOUtils.copyFromURL( 472 MacResources.class.getResource(LIBRARY_NAME), 473 new File(macOSDirectory, LIBRARY_NAME)); 474 475 executableFile.setExecutable(true, false); 476 477 // Copy runtime to PlugIns folder 478 copyRuntime(plugInsDirectory, p); 479 480 // Copy class path entries to Java folder 481 copyClassPathEntries(javaDirectory, p); 482 483 //TODO: Need to support adding native libraries. 484 // Copy library path entries to MacOS folder 485 //copyLibraryPathEntries(macOSDirectory); 486 487 /*********** Take care of "config" files *******/ 488 // Copy icon to Resources folder 489 IOUtils.copyFile(getConfig_Icon(p), 490 new File(resourcesDirectory, getConfig_Icon(p).getName())); 491 492 // copy file association icons 493 for (Map<String, ? super Object> fa : FILE_ASSOCIATIONS.fetchFrom(p)) { 494 File f = FA_ICON.fetchFrom(fa); 495 if (f != null && f.exists()) { 496 IOUtils.copyFile(f, 497 new File(resourcesDirectory, f.getName())); 498 } 499 } 500 501 502 // Generate Info.plist 503 IOUtils.copyFile(getConfig_InfoPlist(p), 504 new File(contentsDirectory, "Info.plist")); 505 506 // maybe sign 507 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.TRUE)) { 508 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p); 509 if (signingIdentity != null) { 510 MacBaseInstallerBundler.signAppBundle(p, rootDirectory, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(p)); 511 } 512 } 513 } catch (IOException ex) { 514 Log.info(ex.toString()); 515 Log.verbose(ex); 516 return null; 517 } finally { 518 if (!VERBOSE.fetchFrom(p)) { 519 //cleanup 520 cleanupConfigFiles(p); 521 } else { 522 Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath())); 523 } 524 } 525 return rootDirectory; 526 } 527 528 public void cleanupConfigFiles(Map<String, ? super Object> params) { 529 //Since building the app can be bypassed, make sure configRoot was set 530 if (CONFIG_ROOT.fetchFrom(params) != null) { 531 if (getConfig_Icon(params) != null) { 532 getConfig_Icon(params).delete(); 533 } 534 if (getConfig_InfoPlist(params) != null) { 535 getConfig_InfoPlist(params).delete(); 536 } 537 } 538 } 539 540 private void copyClassPathEntries(File javaDirectory, Map<String, ? super Object> params) throws IOException { 541 List<RelativeFileSet> resourcesList = APP_RESOURCES_LIST.fetchFrom(params); 542 if (resourcesList == null) { 543 throw new RuntimeException(I18N.getString("message.null-classpath")); 544 } 545 546 for (RelativeFileSet classPath : resourcesList) { 547 File srcdir = classPath.getBaseDirectory(); 548 for (String fname : classPath.getIncludedFiles()) { 549 IOUtils.copyFile( 550 new File(srcdir, fname), new File(javaDirectory, fname)); 551 } 552 } 553 } 554 555 private void copyRuntime(File plugInsDirectory, Map<String, ? super Object> params) throws IOException { 556 RelativeFileSet runTime = MAC_RUNTIME.fetchFrom(params); 557 if (runTime == null) { 558 //request to use system runtime => do not bundle 559 return; 560 } 561 plugInsDirectory.mkdirs(); 562 563 File srcdir = runTime.getBaseDirectory(); 564 File destDir = new File(plugInsDirectory, "Java"); 565 Set<String> filesToCopy = runTime.getIncludedFiles(); 566 567 for (String fname : filesToCopy) { 568 IOUtils.copyFile( 569 new File(srcdir, fname), new File(destDir, fname)); 570 } 571 } 572 573 private void prepareIcon(Map<String, ? super Object> params) throws IOException { 574 File icon = ICON_ICNS.fetchFrom(params); 575 if (icon == null || !icon.exists()) { 576 fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns", 577 "icon", 578 DEFAULT_ICNS_ICON.fetchFrom(params), 579 getConfig_Icon(params), 580 VERBOSE.fetchFrom(params), 581 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 582 } else { 583 fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns", 584 "icon", 585 icon, 586 getConfig_Icon(params), 587 VERBOSE.fetchFrom(params), 588 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 589 } 590 } 591 592 private String getLauncherName(Map<String, ? super Object> params) { 593 if (APP_NAME.fetchFrom(params) != null) { 594 return APP_NAME.fetchFrom(params); 595 } else { 596 return MAIN_CLASS.fetchFrom(params); 597 } 598 } 599 600 private String getBundleName(Map<String, ? super Object> params) { 601 //TODO: Check to see what rules/limits are in place for CFBundleName 602 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { 603 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); 604 if (bn.length() > 16) { 605 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn)); 606 } 607 return MAC_CF_BUNDLE_NAME.fetchFrom(params); 608 } else if (APP_NAME.fetchFrom(params) != null) { 609 return APP_NAME.fetchFrom(params); 610 } else { 611 String nm = MAIN_CLASS.fetchFrom(params); 612 if (nm.length() > 16) { 613 nm = nm.substring(0, 16); 614 } 615 return nm; 616 } 617 } 618 619 private void writeInfoPlist(File file, Map<String, ? super Object> params) throws IOException { 620 Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath())); 621 622 //prepare config for exe 623 //Note: do not need CFBundleDisplayName if we do not support localization 624 Map<String, String> data = new HashMap<>(); 625 data.put("DEPLOY_ICON_FILE", getConfig_Icon(params).getName()); 626 data.put("DEPLOY_BUNDLE_IDENTIFIER", 627 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 628 data.put("DEPLOY_BUNDLE_NAME", 629 getBundleName(params)); 630 data.put("DEPLOY_BUNDLE_COPYRIGHT", 631 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown"); 632 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); 633 if (MAC_RUNTIME.fetchFrom(params) != null) { 634 data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/plugins/Java"); 635 } else { 636 data.put("DEPLOY_JAVA_RUNTIME_NAME", ""); 637 } 638 data.put("DEPLOY_BUNDLE_SHORT_VERSION", 639 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0"); 640 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", 641 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); 642 data.put("DEPLOY_BUNDLE_CATEGORY", 643 //TODO parameters should provide set of values for IDEs 644 MAC_CATEGORY.validatedFetchFrom(params)); 645 646 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next()); 647 648 data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase()); 649 650 StringBuilder sb = new StringBuilder(); 651 List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params); 652 653 String newline = ""; //So we don't add unneccessary extra line after last append 654 for (String o : jvmOptions) { 655 sb.append(newline).append(" <string>").append(o).append("</string>"); 656 newline = "\n"; 657 } 658 659 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params); 660 for (Map.Entry<String, String> entry : jvmProps.entrySet()) { 661 sb.append(newline) 662 .append(" <string>-D") 663 .append(entry.getKey()) 664 .append("=") 665 .append(entry.getValue()) 666 .append("</string>"); 667 newline = "\n"; 668 } 669 670 String preloader = PRELOADER_CLASS.fetchFrom(params); 671 if (preloader != null) { 672 sb.append(newline) 673 .append(" <string>-Djavafx.preloader=") 674 .append(preloader) 675 .append("</string>"); 676 //newline = "\n"; 677 } 678 679 data.put("DEPLOY_JVM_OPTIONS", sb.toString()); 680 681 sb = new StringBuilder(); 682 List<String> args = ARGUMENTS.fetchFrom(params); 683 newline = ""; //So we don't add unneccessary extra line after last append 684 for (String o : args) { 685 sb.append(newline).append(" <string>").append(o).append("</string>"); 686 newline = "\n"; 687 } 688 data.put("DEPLOY_ARGUMENTS", sb.toString()); 689 690 newline = ""; 691 sb = new StringBuilder(); 692 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params); 693 for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) { 694 sb.append(newline) 695 .append(" <key>").append(arg.getKey()).append("</key>\n") 696 .append(" <string>").append(arg.getValue()).append("</string>"); 697 newline = "\n"; 698 } 699 data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString()); 700 701 702 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); 703 704 StringBuilder macroedPath = new StringBuilder(); 705 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { 706 macroedPath.append(s); 707 macroedPath.append(":"); 708 } 709 macroedPath.deleteCharAt(macroedPath.length() - 1); 710 711 data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString()); 712 713 //TODO: Add remainder of the classpath 714 715 StringBuilder bundleDocumentTypes = new StringBuilder(); 716 StringBuilder exportedTypes = new StringBuilder(); 717 for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { 718 719 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 720 721 if (extensions == null) { 722 Log.info(I18N.getString("message.creating-association-with-null-extension")); 723 } 724 725 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); 726 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty()) 727 ? "mime" 728 : extensions.get(0)); 729 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 730 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS 731 732 bundleDocumentTypes.append(" <dict>\n") 733 .append(" <key>LSItemContentTypes</key>\n") 734 .append(" <array>\n") 735 .append(" <string>") 736 .append(itemContentType) 737 .append("</string>\n") 738 .append(" </array>\n") 739 .append("\n") 740 .append(" <key>CFBundleTypeName</key>\n") 741 .append(" <string>") 742 .append(description) 743 .append("</string>\n") 744 .append("\n") 745 .append(" <key>LSHandlerRank</key>\n") 746 .append(" <string>Owner</string>\n") //TODO make a bundler arg 747 .append("\n") 748 .append(" <key>CFBundleTypeRole</key>\n") 749 .append(" <string>Editor</string>\n") // TODO make a bundler arg 750 .append("\n") 751 .append(" <key>LSIsAppleDefaultForType</key>\n") 752 .append(" <true/>\n") // TODO make a bundler arg 753 .append("\n"); 754 755 if (icon != null && icon.exists()) { 756 //? 757 bundleDocumentTypes.append(" <key>CFBundleTypeIconFile</key>\n") 758 .append(" <string>") 759 .append(icon.getName()) 760 .append("</string>\n"); 761 } 762 bundleDocumentTypes.append(" </dict>\n"); 763 764 exportedTypes.append(" <dict>\n") 765 .append(" <key>UTTypeIdentifier</key>\n") 766 .append(" <string>") 767 .append(itemContentType) 768 .append("</string>\n") 769 .append("\n") 770 .append(" <key>UTTypeDescription</key>\n") 771 .append(" <string>") 772 .append(description) 773 .append("</string>\n") 774 .append(" <key>UTTypeConformsTo</key>\n") 775 .append(" <array>\n") 776 .append(" <string>public.data</string>\n") //TODO expose this? 777 .append(" </array>\n") 778 .append("\n"); 779 780 if (icon != null && icon.exists()) { 781 exportedTypes.append(" <key>UTTypeIconFile</key>\n") 782 .append(" <string>") 783 .append(icon.getName()) 784 .append("</string>\n") 785 .append("\n"); 786 } 787 788 exportedTypes.append("\n") 789 .append(" <key>UTTypeTagSpecification</key>\n") 790 .append(" <dict>\n") 791 //TODO expose via param? .append(" <key>com.apple.ostype</key>\n"); 792 //TODO expose via param? .append(" <string>ABCD</string>\n") 793 .append("\n"); 794 795 if (extensions != null && !extensions.isEmpty()) { 796 exportedTypes.append(" <key>public.filename-extension</key>\n") 797 .append(" <array>\n"); 798 799 for (String ext : extensions) { 800 exportedTypes.append(" <string>") 801 .append(ext) 802 .append("</string>\n"); 803 } 804 exportedTypes.append(" </array>\n"); 805 } 806 if (mimeTypes != null && !mimeTypes.isEmpty()) { 807 exportedTypes.append(" <key>public.mime-type</key>\n") 808 .append(" <array>\n"); 809 810 for (String mime : mimeTypes) { 811 exportedTypes.append(" <string>") 812 .append(mime) 813 .append("</string>\n"); 814 } 815 exportedTypes.append(" </array>\n"); 816 } 817 exportedTypes.append(" </dict>\n") 818 .append(" </dict>\n"); 819 } 820 String associationData; 821 if (bundleDocumentTypes.length() > 0) { 822 associationData = "\n <key>CFBundleDocumentTypes</key>\n <array>\n" 823 + bundleDocumentTypes.toString() 824 + " </array>\n\n <key>UTExportedTypeDeclarations</key>\n <array>\n" 825 + exportedTypes.toString() 826 + " </array>\n"; 827 } else { 828 associationData = ""; 829 } 830 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); 831 832 833 Writer w = new BufferedWriter(new FileWriter(file)); 834 w.write(preprocessTextResource( 835 MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(), 836 I18N.getString("resource.bundle-config-file"), TEMPLATE_INFO_PLIST, data, 837 VERBOSE.fetchFrom(params), 838 DROP_IN_RESOURCES_ROOT.fetchFrom(params))); 839 w.close(); 840 841 } 842 843 private void writePkgInfo(File file) throws IOException { 844 845 //hardcoded as it does not seem we need to change it ever 846 String signature = "????"; 847 848 try (Writer out = new BufferedWriter(new FileWriter(file))) { 849 out.write(OS_TYPE_CODE + signature); 850 out.flush(); 851 } 852 } 853 854 public static Rule[] createMacRuntimeRules(Map<String, ? super Object> params) { 855 if (!System.getProperty("os.name").toLowerCase().contains("os x")) { 856 // we will never get a sensible answer unless we are running on OSX, 857 // so quit now and return null indicating 'no sensible value' 858 return null; 859 } 860 861 //Subsetting of JRE is restricted. 862 //JRE README defines what is allowed to strip: 863 // http://www.oracle.com/technetwork/java/javase/jre-8-readme-2095710.html 864 // 865 866 List<Rule> rules = new ArrayList<>(); 867 868 File baseDir; 869 870 if (params.containsKey(MAC_RUNTIME.getID())) { 871 Object o = params.get(MAC_RUNTIME.getID()); 872 if (o instanceof RelativeFileSet) { 873 874 baseDir = ((RelativeFileSet)o).getBaseDirectory(); 875 } else { 876 baseDir = new File(o.toString()); 877 } 878 } else { 879 baseDir = new File(System.getProperty("java.home")); 880 } 881 882 // we accept either pointing at the directories typically installed at: 883 // /Libraries/Java/JavaVirtualMachine/jdk1.8.0_40/ 884 // * . 885 // * Contents/Home 886 // * Contents/Home/jre 887 // /Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/ 888 // * . 889 // * /Contents/Home 890 // version may change, and if we don't detect any Contents/Home or Contents/Home/jre we will 891 // presume we are at a root. 892 893 if (!baseDir.exists()) { 894 throw new RuntimeException(I18N.getString("error.non-existent-runtime"), 895 new ConfigException(I18N.getString("error.non-existent-runtime"), 896 I18N.getString("error.non-existent-runtime.advice"))); 897 } 898 899 boolean isJRE; 900 boolean isJDK; 901 902 try { 903 String path = baseDir.getCanonicalPath(); 904 if (path.endsWith("/Contents/Home/jre")) { 905 baseDir = baseDir.getParentFile().getParentFile().getParentFile(); 906 } else if (path.endsWith("/Contents/Home")) { 907 baseDir = baseDir.getParentFile().getParentFile(); 908 } 909 910 isJRE = new File(baseDir, "Contents/Home/lib/jli/libjli.dylib").exists(); 911 isJDK = new File(baseDir, "Contents/Home/jre/lib/jli/libjli.dylib").exists(); 912 913 } catch (IOException e) { 914 throw new RuntimeException(e); 915 } 916 917 if (!(isJRE || isJDK)) { 918 throw new RuntimeException(I18N.getString("error.cannot-detect-runtime-in-directory"), 919 new ConfigException(I18N.getString("error.cannot-detect-runtime-in-directory"), 920 I18N.getString("error.cannot-detect-runtime-in-directory.advice"))); 921 } 922 923 // we need the Info.plist for signing 924 rules.add(Rule.suffix("/contents/info.plist")); 925 926 // Strip some JRE specific stuff 927 if (isJRE) { 928 rules.add(Rule.suffixNeg("/contents/disabled.plist")); 929 rules.add(Rule.suffixNeg("/contents/enabled.plist")); 930 rules.add(Rule.substrNeg("/contents/frameworks/")); 931 } 932 933 // strip out command line tools 934 rules.add(Rule.suffixNeg("home/bin")); 935 if (isJDK) { 936 rules.add(Rule.suffixNeg("home/jre/bin")); 937 } 938 939 // strip out JRE stuff 940 if (isJRE) { 941 // update helper 942 rules.add(Rule.suffixNeg("resources")); 943 // interfacebuilder files 944 rules.add(Rule.suffixNeg("lib/nibs")); 945 // browser integration 946 rules.add(Rule.suffixNeg("lib/libnpjp2.dylib")); 947 // java webstart 948 rules.add(Rule.suffixNeg("lib/security/javaws.policy")); 949 rules.add(Rule.suffixNeg("lib/shortcuts")); 950 951 // general deploy libraries 952 rules.add(Rule.suffixNeg("lib/deploy")); 953 rules.add(Rule.suffixNeg("lib/deploy.jar")); 954 rules.add(Rule.suffixNeg("lib/javaws.jar")); 955 rules.add(Rule.suffixNeg("lib/libdeploy.dylib")); 956 rules.add(Rule.suffixNeg("lib/plugin.jar")); 957 } 958 959 // strip out man pages 960 rules.add(Rule.suffixNeg("home/man")); 961 962 // this is the build hashes, strip or keep? 963 //rules.add(Rule.suffixNeg("home/release")); 964 965 // strip out JDK stuff like JavaDB, JNI Headers, etc 966 if (isJDK) { 967 rules.add(Rule.suffixNeg("home/db")); 968 rules.add(Rule.suffixNeg("home/demo")); 969 rules.add(Rule.suffixNeg("home/include")); 970 rules.add(Rule.suffixNeg("home/lib")); 971 rules.add(Rule.suffixNeg("home/sample")); 972 rules.add(Rule.suffixNeg("home/src.zip")); 973 rules.add(Rule.suffixNeg("home/javafx-src.zip")); 974 } 975 976 //"home/rt" is not part of the official builds 977 // but we may be creating this symlink to make older NB projects 978 // happy. Make sure to not include it into final artifact 979 rules.add(Rule.suffixNeg("home/rt")); 980 981 //rules.add(Rule.suffixNeg("jre/lib/ext")); //need some of jars there for https to work 982 983 // strip out flight recorder 984 rules.add(Rule.suffixNeg("lib/jfr.jar")); 985 986 return rules.toArray(new Rule[rules.size()]); 987 } 988 989 ////////////////////////////////////////////////////////////////////////////////// 990 // Implement Bundler 991 ////////////////////////////////////////////////////////////////////////////////// 992 993 @Override 994 public String getName() { 995 return I18N.getString("bundler.name"); 996 } 997 998 @Override 999 public String getDescription() { 1000 return I18N.getString("bundler.description"); 1001 } 1002 1003 @Override 1004 public String getID() { 1005 return "mac.app"; 1006 } 1007 1008 @Override 1009 public String getBundleType() { 1010 return "IMAGE"; 1011 } 1012 1013 @Override 1014 public Collection<BundlerParamInfo<?>> getBundleParameters() { 1015 return getAppBundleParameters(); 1016 } 1017 1018 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() { 1019 return Arrays.asList( 1020 APP_NAME, 1021 APP_RESOURCES, 1022 // APP_RESOURCES_LIST, // ?? 1023 ARGUMENTS, 1024 BUNDLE_ID_SIGNING_PREFIX, 1025 CLASSPATH, 1026 DEVELOPER_ID_APP_SIGNING_KEY, 1027 ICON_ICNS, 1028 JVM_OPTIONS, 1029 JVM_PROPERTIES, 1030 MAC_CATEGORY, 1031 MAC_CF_BUNDLE_IDENTIFIER, 1032 MAC_CF_BUNDLE_NAME, 1033 MAC_CF_BUNDLE_VERSION, 1034 MAC_RUNTIME, 1035 MAIN_CLASS, 1036 MAIN_JAR, 1037 PREFERENCES_ID, 1038 PRELOADER_CLASS, 1039 SIGNING_KEYCHAIN, 1040 USER_JVM_OPTIONS, 1041 VERSION 1042 ); 1043 } 1044 1045 1046 @Override 1047 public File execute(Map<String, ? super Object> params, File outputParentDir) { 1048 return doBundle(params, outputParentDir, false); 1049 } 1050 }