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