1 /* 2 * Copyright (c) 2015, 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 jdk.packager.builders.mac; 26 27 import com.oracle.tools.packager.BundlerParamInfo; 28 import com.oracle.tools.packager.IOUtils; 29 import com.oracle.tools.packager.Log; 30 import com.oracle.tools.packager.RelativeFileSet; 31 import com.oracle.tools.packager.StandardBundlerParam; 32 import com.oracle.tools.packager.mac.MacResources; 33 import jdk.packager.builders.AbstractAppImageBuilder; 34 import jdk.tools.jlink.plugin.Pool; 35 36 import java.io.BufferedWriter; 37 import java.io.File; 38 import java.io.FileInputStream; 39 import java.io.FileOutputStream; 40 import java.io.FileWriter; 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.io.OutputStream; 44 import java.io.OutputStreamWriter; 45 import java.io.UncheckedIOException; 46 import java.io.Writer; 47 import java.math.BigInteger; 48 import java.nio.file.Files; 49 import java.nio.file.Path; 50 import java.nio.file.attribute.PosixFilePermission; 51 import java.text.MessageFormat; 52 import java.util.ArrayList; 53 import java.util.Arrays; 54 import java.util.EnumSet; 55 import java.util.HashMap; 56 import java.util.List; 57 import java.util.Map; 58 import java.util.Objects; 59 import java.util.Optional; 60 import java.util.ResourceBundle; 61 import java.util.Set; 62 import java.util.concurrent.atomic.AtomicReference; 63 import java.util.function.Consumer; 64 65 import static com.oracle.tools.packager.StandardBundlerParam.*; 66 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.*; 67 import static com.oracle.tools.packager.mac.MacAppBundler.*; 68 /** 69 * 70 */ 71 public class MacAppImageBuilder extends AbstractAppImageBuilder { 72 73 private static final ResourceBundle I18N = 74 ResourceBundle.getBundle(MacAppImageBuilder.class.getName()); 75 76 private static final String EXECUTABLE_NAME = "JavaAppLauncher"; 77 private static final String LIBRARY_NAME = "libpackager.dylib"; 78 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns"; 79 private static final String OS_TYPE_CODE = "APPL"; 80 private static final String TEMPLATE_INFO_PLIST_LITE = "Info-lite.plist.template"; 81 private static final String TEMPLATE_RUNTIME_INFO_PLIST = "Runtime-Info.plist.template"; 82 83 private final Path root; 84 private final Path contentsDir; 85 private final Path javaDir; 86 private final Path resourcesDir; 87 private final Path macOSDir; 88 private final Path runtimeDir; 89 private final Path runtimeRoot; 90 private final Path mdir; 91 92 private final Map<String, ? super Object> params; 93 94 private static Map<String, String> getMacCategories() { 95 Map<String, String> map = new HashMap<>(); 96 map.put("Business", "public.app-category.business"); 97 map.put("Developer Tools", "public.app-category.developer-tools"); 98 map.put("Education", "public.app-category.education"); 99 map.put("Entertainment", "public.app-category.entertainment"); 100 map.put("Finance", "public.app-category.finance"); 101 map.put("Games", "public.app-category.games"); 102 map.put("Graphics & Design", "public.app-category.graphics-design"); 103 map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness"); 104 map.put("Lifestyle", "public.app-category.lifestyle"); 105 map.put("Medical", "public.app-category.medical"); 106 map.put("Music", "public.app-category.music"); 107 map.put("News", "public.app-category.news"); 108 map.put("Photography", "public.app-category.photography"); 109 map.put("Productivity", "public.app-category.productivity"); 110 map.put("Reference", "public.app-category.reference"); 111 map.put("Social Networking", "public.app-category.social-networking"); 112 map.put("Sports", "public.app-category.sports"); 113 map.put("Travel", "public.app-category.travel"); 114 map.put("Utilities", "public.app-category.utilities"); 115 map.put("Video", "public.app-category.video"); 116 map.put("Weather", "public.app-category.weather"); 117 118 map.put("Action Games", "public.app-category.action-games"); 119 map.put("Adventure Games", "public.app-category.adventure-games"); 120 map.put("Arcade Games", "public.app-category.arcade-games"); 121 map.put("Board Games", "public.app-category.board-games"); 122 map.put("Card Games", "public.app-category.card-games"); 123 map.put("Casino Games", "public.app-category.casino-games"); 124 map.put("Dice Games", "public.app-category.dice-games"); 125 map.put("Educational Games", "public.app-category.educational-games"); 126 map.put("Family Games", "public.app-category.family-games"); 127 map.put("Kids Games", "public.app-category.kids-games"); 128 map.put("Music Games", "public.app-category.music-games"); 129 map.put("Puzzle Games", "public.app-category.puzzle-games"); 130 map.put("Racing Games", "public.app-category.racing-games"); 131 map.put("Role Playing Games", "public.app-category.role-playing-games"); 132 map.put("Simulation Games", "public.app-category.simulation-games"); 133 map.put("Sports Games", "public.app-category.sports-games"); 134 map.put("Strategy Games", "public.app-category.strategy-games"); 135 map.put("Trivia Games", "public.app-category.trivia-games"); 136 map.put("Word Games", "public.app-category.word-games"); 137 138 return map; 139 } 140 141 public static final BundlerParamInfo<Boolean> MAC_CONFIGURE_LAUNCHER_IN_PLIST = 142 new StandardBundlerParam<>( 143 I18N.getString("param.configure-launcher-in-plist"), 144 I18N.getString("param.configure-launcher-in-plist.description"), 145 "mac.configure-launcher-in-plist", 146 Boolean.class, 147 params -> Boolean.FALSE, 148 (s, p) -> Boolean.valueOf(s)); 149 150 public static final BundlerParamInfo<String> MAC_CATEGORY = 151 new StandardBundlerParam<>( 152 I18N.getString("param.category-name"), 153 I18N.getString("param.category-name.description"), 154 "mac.category", 155 String.class, 156 params -> "public.app-category.developer-tools", // this category is almost certianly wrong, encouraging the user to set a value 157 (s, p) -> s 158 ); 159 160 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 161 new StandardBundlerParam<>( 162 I18N.getString("param.cfbundle-name.name"), 163 I18N.getString("param.cfbundle-name.description"), 164 "mac.CFBundleName", 165 String.class, 166 params -> null, 167 (s, p) -> s); 168 169 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 170 new StandardBundlerParam<>( 171 I18N.getString("param.cfbundle-identifier.name"), 172 I18N.getString("param.cfbundle-identifier.description"), 173 "mac.CFBundleIdentifier", 174 String.class, 175 params -> "com.example.bad", //FIXME 176 (s, p) -> s); 177 178 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 179 new StandardBundlerParam<>( 180 I18N.getString("param.cfbundle-version.name"), 181 I18N.getString("param.cfbundle-version.description"), 182 "mac.CFBundleVersion", 183 String.class, 184 p -> { 185 String s = VERSION.fetchFrom(p); 186 if (validCFBundleVersion(s)) { 187 return s; 188 } else { 189 return "100"; 190 } 191 }, 192 (s, p) -> s); 193 194 public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>( 195 I18N.getString("param.config-root.name"), 196 I18N.getString("param.config-root.description"), 197 "configRoot", 198 File.class, 199 params -> { 200 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx"); 201 configRoot.mkdirs(); 202 return configRoot; 203 }, 204 (s, p) -> new File(s)); 205 206 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>( 207 I18N.getString("param.default-icon-icns"), 208 I18N.getString("param.default-icon-icns.description"), 209 ".mac.default.icns", 210 String.class, 211 params -> TEMPLATE_BUNDLE_ICON, 212 (s, p) -> s); 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 private static String extractAppName() { 246 return ""; 247 } 248 249 public MacAppImageBuilder(Map<String, Object> config, Path imageOutDir) throws IOException { 250 super(config, imageOutDir.resolve(APP_NAME.fetchFrom(config) + ".app/Contents/PlugIns/Java.runtime/Contents/Home")); 251 252 Objects.requireNonNull(imageOutDir); 253 254 //@SuppressWarnings("unchecked") 255 //String img = (String) config.get("jimage.name"); // FIXME constant 256 257 this.params = config; 258 259 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app"); 260 261 this.contentsDir = root.resolve("Contents"); 262 this.javaDir = contentsDir.resolve("Java"); 263 this.resourcesDir = contentsDir.resolve("Resources"); 264 this.macOSDir = contentsDir.resolve("MacOS"); 265 this.runtimeDir = contentsDir.resolve("PlugIns/Java.runtime"); 266 this.runtimeRoot = runtimeDir.resolve("Contents/Home"); 267 this.mdir = runtimeRoot.resolve("lib"); 268 Files.createDirectories(javaDir); 269 Files.createDirectories(resourcesDir); 270 Files.createDirectories(macOSDir); 271 } 272 273 private void writeEntry(InputStream in, Path dstFile) throws IOException { 274 Files.createDirectories(dstFile.getParent()); 275 Files.copy(in, dstFile); 276 } 277 278 /** 279 * chmod ugo+x file 280 */ 281 private void setExecutable(Path file) { 282 try { 283 Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file); 284 perms.add(PosixFilePermission.OWNER_EXECUTE); 285 perms.add(PosixFilePermission.GROUP_EXECUTE); 286 perms.add(PosixFilePermission.OTHERS_EXECUTE); 287 Files.setPosixFilePermissions(file, perms); 288 } catch (IOException ioe) { 289 throw new UncheckedIOException(ioe); 290 } 291 } 292 293 private static void createUtf8File(File file, String content) throws IOException { 294 try (OutputStream fout = new FileOutputStream(file); 295 Writer output = new OutputStreamWriter(fout, "UTF-8")) { 296 output.write(content); 297 } 298 } 299 300 @Override 301 protected String getCacheLocation(Map<String, ? super Object> params) { 302 return "$CACHEDIR/"; 303 } 304 305 public static boolean validCFBundleVersion(String v) { 306 // CFBundleVersion (String - iOS, OS X) specifies the build version 307 // number of the bundle, which identifies an iteration (released or 308 // unreleased) of the bundle. The build version number should be a 309 // string comprised of three non-negative, period-separated integers 310 // with the first integer being greater than zero. The string should 311 // only contain numeric (0-9) and period (.) characters. Leading zeros 312 // are truncated from each integer and will be ignored (that is, 313 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 314 315 if (v == null) { 316 return false; 317 } 318 319 String p[] = v.split("\\."); 320 if (p.length > 3 || p.length < 1) { 321 Log.verbose(I18N.getString("message.version-string-too-many-components")); 322 return false; 323 } 324 325 try { 326 BigInteger n = new BigInteger(p[0]); 327 if (BigInteger.ONE.compareTo(n) > 0) { 328 Log.verbose(I18N.getString("message.version-string-first-number-not-zero")); 329 return false; 330 } 331 if (p.length > 1) { 332 n = new BigInteger(p[1]); 333 if (BigInteger.ZERO.compareTo(n) > 0) { 334 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 335 return false; 336 } 337 } 338 if (p.length > 2) { 339 n = new BigInteger(p[2]); 340 if (BigInteger.ZERO.compareTo(n) > 0) { 341 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 342 return false; 343 } 344 } 345 } catch (NumberFormatException ne) { 346 Log.verbose(I18N.getString("message.version-string-numbers-only")); 347 Log.verbose(ne); 348 return false; 349 } 350 351 return true; 352 } 353 354 @Override 355 protected InputStream getResourceAsStream(String name) { 356 return MacResources.class.getResourceAsStream(name); 357 } 358 359 360 @Override 361 protected void prepareApplicationFiles(Pool files, Set<String> modules) throws IOException { 362 File f; 363 364 // Generate PkgInfo 365 File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo"); 366 pkgInfoFile.createNewFile(); 367 writePkgInfo(pkgInfoFile); 368 369 370 // Copy executable to MacOS folder 371 Path executable = macOSDir.resolve(getLauncherName(params)); 372 writeEntry(MacResources.class.getResourceAsStream(EXECUTABLE_NAME), executable); 373 executable.toFile().setExecutable(true, false); 374 375 // Copy library to the MacOS folder 376 writeEntry( 377 MacResources.class.getResourceAsStream(LIBRARY_NAME), 378 macOSDir.resolve(LIBRARY_NAME) 379 ); 380 381 // generate launcher config 382 383 writeCfgFile(params, new File(root.toFile(), getLauncherCfgName(params)), "$APPDIR/PlugIns/Java.runtime"); 384 385 // Copy class path entries to Java folder 386 copyClassPathEntries(javaDir); 387 388 //TODO: Need to support adding native libraries. 389 // Copy library path entries to MacOS folder 390 //copyLibraryPathEntries(macOSDirectory); 391 392 /*********** Take care of "config" files *******/ 393 // Copy icon to Resources folder 394 File icon = ICON_ICNS.fetchFrom(params); 395 InputStream in = locateResource("package/macosx/" + APP_NAME.fetchFrom(params) + ".icns", 396 "icon", 397 DEFAULT_ICNS_ICON.fetchFrom(params), 398 icon, 399 VERBOSE.fetchFrom(params), 400 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 401 Files.copy(in, resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns")); 402 403 // copy file association icons 404 for (Map<String, ? super Object> fa : FILE_ASSOCIATIONS.fetchFrom(params)) { 405 f = FA_ICON.fetchFrom(fa); 406 if (f != null && f.exists()) { 407 try (InputStream in2 = new FileInputStream(f)) { 408 Files.copy(in2, resourcesDir.resolve(f.getName())); 409 } 410 411 } 412 } 413 414 // Generate Info.plist 415 writeInfoPlist(contentsDir.resolve("Info.plist").toFile()); 416 417 // generate java runtime info.plist 418 writeRuntimeInfoPlist(runtimeDir.resolve("Contents/Info.plist").toFile()); 419 420 // copy library 421 Path runtimeMacOSDir = Files.createDirectories(runtimeDir.resolve("Contents/MacOS")); 422 Files.copy(runtimeRoot.resolve("lib/jli/libjli.dylib"), runtimeMacOSDir.resolve("libjli.dylib")); 423 424 // maybe sign 425 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { 426 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params); 427 if (signingIdentity != null) { 428 signAppBundle(params, root, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null); 429 } 430 } 431 } 432 433 434 private String getLauncherName(Map<String, ? super Object> params) { 435 if (APP_NAME.fetchFrom(params) != null) { 436 return APP_NAME.fetchFrom(params); 437 } else { 438 return MAIN_CLASS.fetchFrom(params); 439 } 440 } 441 442 public static String getLauncherCfgName(Map<String, ? super Object> p) { 443 return "Contents/Java/" + APP_NAME.fetchFrom(p) + ".cfg"; 444 } 445 446 private void copyClassPathEntries(Path javaDirectory) throws IOException { 447 List<RelativeFileSet> resourcesList = APP_RESOURCES_LIST.fetchFrom(params); 448 if (resourcesList == null) { 449 throw new RuntimeException(I18N.getString("message.null-classpath")); 450 } 451 452 for (RelativeFileSet classPath : resourcesList) { 453 File srcdir = classPath.getBaseDirectory(); 454 for (String fname : classPath.getIncludedFiles()) { 455 // use new File since fname can have file separators 456 Files.copy(new File(srcdir, fname).toPath(), new File(javaDirectory.toFile(), fname).toPath()); 457 } 458 } 459 } 460 461 private String getBundleName(Map<String, ? super Object> params) { 462 //TODO: Check to see what rules/limits are in place for CFBundleName 463 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { 464 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); 465 if (bn.length() > 16) { 466 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn)); 467 } 468 return MAC_CF_BUNDLE_NAME.fetchFrom(params); 469 } else if (APP_NAME.fetchFrom(params) != null) { 470 return APP_NAME.fetchFrom(params); 471 } else { 472 String nm = MAIN_CLASS.fetchFrom(params); 473 if (nm.length() > 16) { 474 nm = nm.substring(0, 16); 475 } 476 return nm; 477 } 478 } 479 480 private void writeRuntimeInfoPlist(File file) throws IOException { 481 //FIXME //TODO these values are bogus. 482 Map<String, String> data = new HashMap<>(); 483 data.put("CF_BUNDLE_INFO", "bundle info"); 484 data.put("CF_BUNDLE_IDENTIFIER", "com.oracle.java.8u60.jdk"); 485 data.put("CF_BUNDLE_NAME", "Java SE 9"); 486 data.put("CF_BUNDLE_VERSION", "1.8.0_60"); 487 488 Writer w = new BufferedWriter(new FileWriter(file)); 489 w.write(preprocessTextResource( 490 "package/macosx/Runtime-Info.plist", 491 I18N.getString("resource.runtime-info-plist"), 492 TEMPLATE_RUNTIME_INFO_PLIST, 493 data, 494 VERBOSE.fetchFrom(params), 495 DROP_IN_RESOURCES_ROOT.fetchFrom(params))); 496 w.close(); 497 } 498 499 private void writeInfoPlist(File file) throws IOException { 500 Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath())); 501 502 //prepare config for exe 503 //Note: do not need CFBundleDisplayName if we do not support localization 504 Map<String, String> data = new HashMap<>(); 505 data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns"); 506 data.put("DEPLOY_BUNDLE_IDENTIFIER", 507 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 508 data.put("DEPLOY_BUNDLE_NAME", 509 getBundleName(params)); 510 data.put("DEPLOY_BUNDLE_COPYRIGHT", 511 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown"); 512 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); 513 data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime"); 514 data.put("DEPLOY_BUNDLE_SHORT_VERSION", 515 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0"); 516 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", 517 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); 518 data.put("DEPLOY_BUNDLE_CATEGORY", 519 //TODO parameters should provide set of values for IDEs 520 MAC_CATEGORY.fetchFrom(params)); 521 522 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next()); 523 524 data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase()); 525 526 StringBuilder sb = new StringBuilder(); 527 List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params); 528 529 String newline = ""; //So we don't add unneccessary extra line after last append 530 for (String o : jvmOptions) { 531 sb.append(newline).append(" <string>").append(o).append("</string>"); 532 newline = "\n"; 533 } 534 535 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params); 536 for (Map.Entry<String, String> entry : jvmProps.entrySet()) { 537 sb.append(newline) 538 .append(" <string>-D") 539 .append(entry.getKey()) 540 .append("=") 541 .append(entry.getValue()) 542 .append("</string>"); 543 newline = "\n"; 544 } 545 546 String preloader = PRELOADER_CLASS.fetchFrom(params); 547 if (preloader != null) { 548 sb.append(newline) 549 .append(" <string>-Djavafx.preloader=") 550 .append(preloader) 551 .append("</string>"); 552 } 553 554 data.put("DEPLOY_JVM_OPTIONS", sb.toString()); 555 556 sb = new StringBuilder(); 557 List<String> args = ARGUMENTS.fetchFrom(params); 558 newline = ""; //So we don't add unneccessary extra line after last append 559 for (String o : args) { 560 sb.append(newline).append(" <string>").append(o).append("</string>"); 561 newline = "\n"; 562 } 563 data.put("DEPLOY_ARGUMENTS", sb.toString()); 564 565 newline = ""; 566 sb = new StringBuilder(); 567 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params); 568 for (Map.Entry<String, String> arg : overridableJVMOptions.entrySet()) { 569 sb.append(newline) 570 .append(" <key>").append(arg.getKey()).append("</key>\n") 571 .append(" <string>").append(arg.getValue()).append("</string>"); 572 newline = "\n"; 573 } 574 data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString()); 575 576 577 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); 578 579 StringBuilder macroedPath = new StringBuilder(); 580 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { 581 macroedPath.append(s); 582 macroedPath.append(":"); 583 } 584 macroedPath.deleteCharAt(macroedPath.length() - 1); 585 586 data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString()); 587 588 //TODO: Add remainder of the classpath 589 590 StringBuilder bundleDocumentTypes = new StringBuilder(); 591 StringBuilder exportedTypes = new StringBuilder(); 592 for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { 593 594 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 595 596 if (extensions == null) { 597 Log.info(I18N.getString("message.creating-association-with-null-extension")); 598 } 599 600 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); 601 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty()) 602 ? "mime" 603 : extensions.get(0)); 604 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 605 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS 606 607 bundleDocumentTypes.append(" <dict>\n") 608 .append(" <key>LSItemContentTypes</key>\n") 609 .append(" <array>\n") 610 .append(" <string>") 611 .append(itemContentType) 612 .append("</string>\n") 613 .append(" </array>\n") 614 .append("\n") 615 .append(" <key>CFBundleTypeName</key>\n") 616 .append(" <string>") 617 .append(description) 618 .append("</string>\n") 619 .append("\n") 620 .append(" <key>LSHandlerRank</key>\n") 621 .append(" <string>Owner</string>\n") //TODO make a bundler arg 622 .append("\n") 623 .append(" <key>CFBundleTypeRole</key>\n") 624 .append(" <string>Editor</string>\n") // TODO make a bundler arg 625 .append("\n") 626 .append(" <key>LSIsAppleDefaultForType</key>\n") 627 .append(" <true/>\n") // TODO make a bundler arg 628 .append("\n"); 629 630 if (icon != null && icon.exists()) { 631 //? 632 bundleDocumentTypes.append(" <key>CFBundleTypeIconFile</key>\n") 633 .append(" <string>") 634 .append(icon.getName()) 635 .append("</string>\n"); 636 } 637 bundleDocumentTypes.append(" </dict>\n"); 638 639 exportedTypes.append(" <dict>\n") 640 .append(" <key>UTTypeIdentifier</key>\n") 641 .append(" <string>") 642 .append(itemContentType) 643 .append("</string>\n") 644 .append("\n") 645 .append(" <key>UTTypeDescription</key>\n") 646 .append(" <string>") 647 .append(description) 648 .append("</string>\n") 649 .append(" <key>UTTypeConformsTo</key>\n") 650 .append(" <array>\n") 651 .append(" <string>public.data</string>\n") //TODO expose this? 652 .append(" </array>\n") 653 .append("\n"); 654 655 if (icon != null && icon.exists()) { 656 exportedTypes.append(" <key>UTTypeIconFile</key>\n") 657 .append(" <string>") 658 .append(icon.getName()) 659 .append("</string>\n") 660 .append("\n"); 661 } 662 663 exportedTypes.append("\n") 664 .append(" <key>UTTypeTagSpecification</key>\n") 665 .append(" <dict>\n") 666 //TODO expose via param? .append(" <key>com.apple.ostype</key>\n"); 667 //TODO expose via param? .append(" <string>ABCD</string>\n") 668 .append("\n"); 669 670 if (extensions != null && !extensions.isEmpty()) { 671 exportedTypes.append(" <key>public.filename-extension</key>\n") 672 .append(" <array>\n"); 673 674 for (String ext : extensions) { 675 exportedTypes.append(" <string>") 676 .append(ext) 677 .append("</string>\n"); 678 } 679 exportedTypes.append(" </array>\n"); 680 } 681 if (mimeTypes != null && !mimeTypes.isEmpty()) { 682 exportedTypes.append(" <key>public.mime-type</key>\n") 683 .append(" <array>\n"); 684 685 for (String mime : mimeTypes) { 686 exportedTypes.append(" <string>") 687 .append(mime) 688 .append("</string>\n"); 689 } 690 exportedTypes.append(" </array>\n"); 691 } 692 exportedTypes.append(" </dict>\n") 693 .append(" </dict>\n"); 694 } 695 String associationData; 696 if (bundleDocumentTypes.length() > 0) { 697 associationData = "\n <key>CFBundleDocumentTypes</key>\n <array>\n" 698 + bundleDocumentTypes.toString() 699 + " </array>\n\n <key>UTExportedTypeDeclarations</key>\n <array>\n" 700 + exportedTypes.toString() 701 + " </array>\n"; 702 } else { 703 associationData = ""; 704 } 705 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); 706 707 708 Writer w = new BufferedWriter(new FileWriter(file)); 709 w.write(preprocessTextResource( 710 //MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(), 711 "package/macosx/Info.plist", 712 I18N.getString("resource.app-info-plist"), 713 TEMPLATE_INFO_PLIST_LITE, 714 data, VERBOSE.fetchFrom(params), 715 DROP_IN_RESOURCES_ROOT.fetchFrom(params))); 716 w.close(); 717 718 } 719 720 private void writePkgInfo(File file) throws IOException { 721 //hardcoded as it does not seem we need to change it ever 722 String signature = "????"; 723 724 try (Writer out = new BufferedWriter(new FileWriter(file))) { 725 out.write(OS_TYPE_CODE + signature); 726 out.flush(); 727 } 728 } 729 730 public static void signAppBundle(Map<String, ? super Object> params, Path appLocation, String signingIdentity, String identifierPrefix, String entitlementsFile, String inheritedEntitlements) throws IOException { 731 AtomicReference<IOException> toThrow = new AtomicReference<>(); 732 String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); 733 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 734 735 // sign all dylibs and jars 736 Files.walk(appLocation) 737 // while we are searching let's fix permissions 738 .peek(path -> { 739 try { 740 Set<PosixFilePermission> pfp = Files.getPosixFilePermissions(path); 741 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { 742 pfp = EnumSet.copyOf(pfp); 743 pfp.add(PosixFilePermission.OWNER_WRITE); 744 Files.setPosixFilePermissions(path, pfp); 745 } 746 } catch (IOException e) { 747 Log.debug(e); 748 } 749 }) 750 .filter(p -> Files.isRegularFile(p) && 751 !(p.toString().contains("/Contents/MacOS/libjli.dylib") 752 || p.toString().contains("/Contents/MacOS/JavaAppletPlugin") 753 || p.toString().endsWith(appExecutable)) 754 ).forEach(p -> { 755 //noinspection ThrowableResultOfMethodCallIgnored 756 if (toThrow.get() != null) return; 757 758 List<String> args = new ArrayList<>(); 759 args.addAll(Arrays.asList("codesign", 760 "-s", signingIdentity, // sign with this key 761 "--prefix", identifierPrefix, // use the identifier as a prefix 762 "-vvvv")); 763 if (entitlementsFile != null && 764 (p.toString().endsWith(".jar") 765 || p.toString().endsWith(".dylib"))) { 766 args.add("--entitlements"); 767 args.add(entitlementsFile); // entitlements 768 } else if (inheritedEntitlements != null && Files.isExecutable(p)) { 769 args.add("--entitlements"); 770 args.add(inheritedEntitlements); // inherited entitlements for executable processes 771 } 772 if (keyChain != null && !keyChain.isEmpty()) { 773 args.add("--keychain"); 774 args.add(keyChain); 775 } 776 args.add(p.toString()); 777 778 try { 779 Set<PosixFilePermission> oldPermissions = Files.getPosixFilePermissions(p); 780 File f = p.toFile(); 781 f.setWritable(true, true); 782 783 ProcessBuilder pb = new ProcessBuilder(args); 784 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 785 786 Files.setPosixFilePermissions(p, oldPermissions); 787 } catch (IOException ioe) { 788 toThrow.set(ioe); 789 } 790 }); 791 792 IOException ioe = toThrow.get(); 793 if (ioe != null) { 794 throw ioe; 795 } 796 797 // sign all plugins and frameworks 798 Consumer<? super Path> signIdentifiedByPList = path -> { 799 //noinspection ThrowableResultOfMethodCallIgnored 800 if (toThrow.get() != null) return; 801 802 try { 803 List<String> args = new ArrayList<>(); 804 args.addAll(Arrays.asList("codesign", 805 "-s", signingIdentity, // sign with this key 806 "--prefix", identifierPrefix, // use the identifier as a prefix 807 "-vvvv")); 808 if (keyChain != null && !keyChain.isEmpty()) { 809 args.add("--keychain"); 810 args.add(keyChain); 811 } 812 args.add(path.toString()); 813 ProcessBuilder pb = new ProcessBuilder(args); 814 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 815 816 args = new ArrayList<>(); 817 args.addAll(Arrays.asList("codesign", 818 "-s", signingIdentity, // sign with this key 819 "--prefix", identifierPrefix, // use the identifier as a prefix 820 "-vvvv")); 821 if (keyChain != null && !keyChain.isEmpty()) { 822 args.add("--keychain"); 823 args.add(keyChain); 824 } 825 args.add(path.toString() + "/Contents/_CodeSignature/CodeResources"); 826 pb = new ProcessBuilder(args); 827 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 828 } catch (IOException e) { 829 toThrow.set(e); 830 } 831 }; 832 833 Path pluginsPath = appLocation.resolve("Contents/PlugIns"); 834 if (Files.isDirectory(pluginsPath)) { 835 Files.list(pluginsPath) 836 .forEach(signIdentifiedByPList); 837 838 ioe = toThrow.get(); 839 if (ioe != null) { 840 throw ioe; 841 } 842 } 843 Path frameworkPath = appLocation.resolve("Contents/Frameworks"); 844 if (Files.isDirectory(frameworkPath)) { 845 Files.list(frameworkPath) 846 .forEach(signIdentifiedByPList); 847 848 ioe = toThrow.get(); 849 if (ioe != null) { 850 throw ioe; 851 } 852 } 853 854 // sign the app itself 855 List<String> args = new ArrayList<>(); 856 args.addAll(Arrays.asList("codesign", 857 "-s", signingIdentity, // sign with this key 858 "-vvvv")); // super verbose output 859 if (entitlementsFile != null) { 860 args.add("--entitlements"); 861 args.add(entitlementsFile); // entitlements 862 } 863 if (keyChain != null && !keyChain.isEmpty()) { 864 args.add("--keychain"); 865 args.add(keyChain); 866 } 867 args.add(appLocation.toString()); 868 869 ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[args.size()])); 870 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 871 } 872 873 }