1 /* 2 * Copyright (c) 2015, 2017, 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.internal.legacy.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.internal.legacy.builders.AbstractAppImageBuilder; 36 import jdk.packager.internal.legacy.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 this.params = config; 252 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app"); 253 this.contentsDir = root.resolve("Contents"); 254 this.javaDir = contentsDir.resolve("Java"); 255 this.resourcesDir = contentsDir.resolve("Resources"); 256 this.macOSDir = contentsDir.resolve("MacOS"); 257 this.runtimeDir = contentsDir.resolve("PlugIns/Java.runtime"); 258 this.runtimeRoot = runtimeDir.resolve("Contents/Home"); 259 this.mdir = runtimeRoot.resolve("lib"); 260 Files.createDirectories(javaDir); 261 Files.createDirectories(resourcesDir); 262 Files.createDirectories(macOSDir); 263 Files.createDirectories(runtimeDir); 264 } 265 266 private static String extractAppName() { 267 return ""; 268 } 269 270 private void writeEntry(InputStream in, Path dstFile) throws IOException { 271 Files.createDirectories(dstFile.getParent()); 272 Files.copy(in, dstFile); 273 } 274 275 /** 276 * chmod ugo+x file 277 */ 278 private void setExecutable(Path file) { 279 try { 280 Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file); 281 perms.add(PosixFilePermission.OWNER_EXECUTE); 282 perms.add(PosixFilePermission.GROUP_EXECUTE); 283 perms.add(PosixFilePermission.OTHERS_EXECUTE); 284 Files.setPosixFilePermissions(file, perms); 285 } catch (IOException ioe) { 286 throw new UncheckedIOException(ioe); 287 } 288 } 289 290 private static void createUtf8File(File file, String content) throws IOException { 291 try (OutputStream fout = new FileOutputStream(file); 292 Writer output = new OutputStreamWriter(fout, "UTF-8")) { 293 output.write(content); 294 } 295 } 296 297 @Override 298 protected String getCacheLocation(Map<String, ? super Object> params) { 299 return "$CACHEDIR/"; 300 } 301 302 public static boolean validCFBundleVersion(String v) { 303 // CFBundleVersion (String - iOS, OS X) specifies the build version 304 // number of the bundle, which identifies an iteration (released or 305 // unreleased) of the bundle. The build version number should be a 306 // string comprised of three non-negative, period-separated integers 307 // with the first integer being greater than zero. The string should 308 // only contain numeric (0-9) and period (.) characters. Leading zeros 309 // are truncated from each integer and will be ignored (that is, 310 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 311 312 if (v == null) { 313 return false; 314 } 315 316 String p[] = v.split("\\."); 317 if (p.length > 3 || p.length < 1) { 318 Log.verbose(I18N.getString("message.version-string-too-many-components")); 319 return false; 320 } 321 322 try { 323 BigInteger n = new BigInteger(p[0]); 324 if (BigInteger.ONE.compareTo(n) > 0) { 325 Log.verbose(I18N.getString("message.version-string-first-number-not-zero")); 326 return false; 327 } 328 if (p.length > 1) { 329 n = new BigInteger(p[1]); 330 if (BigInteger.ZERO.compareTo(n) > 0) { 331 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 332 return false; 333 } 334 } 335 if (p.length > 2) { 336 n = new BigInteger(p[2]); 337 if (BigInteger.ZERO.compareTo(n) > 0) { 338 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 339 return false; 340 } 341 } 342 } catch (NumberFormatException ne) { 343 Log.verbose(I18N.getString("message.version-string-numbers-only")); 344 Log.verbose(ne); 345 return false; 346 } 347 348 return true; 349 } 350 351 @Override 352 public InputStream getResourceAsStream(String name) { 353 return MacResources.class.getResourceAsStream(name); 354 } 355 356 @Override 357 public void prepareApplicationFiles() throws IOException { 358 File f; 359 360 // Generate PkgInfo 361 File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo"); 362 pkgInfoFile.createNewFile(); 363 writePkgInfo(pkgInfoFile); 364 365 366 // Copy executable to MacOS folder 367 Path executable = macOSDir.resolve(getLauncherName(params)); 368 writeEntry(MacResources.class.getResourceAsStream(EXECUTABLE_NAME), executable); 369 executable.toFile().setExecutable(true, false); 370 371 // Copy library to the MacOS folder 372 writeEntry( 373 MacResources.class.getResourceAsStream(LIBRARY_NAME), 374 macOSDir.resolve(LIBRARY_NAME) 375 ); 376 377 // generate launcher config 378 379 writeCfgFile(params, new File(root.toFile(), getLauncherCfgName(params)), "$APPDIR/PlugIns/Java.runtime"); 380 381 // Copy class path entries to Java folder 382 copyClassPathEntries(javaDir); 383 384 /*********** Take care of "config" files *******/ 385 // Copy icon to Resources folder 386 File icon = ICON_ICNS.fetchFrom(params); 387 InputStream in = locateResource("package/macosx/" + APP_NAME.fetchFrom(params) + ".icns", 388 "icon", 389 DEFAULT_ICNS_ICON.fetchFrom(params), 390 icon, 391 VERBOSE.fetchFrom(params), 392 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 393 Files.copy(in, resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns")); 394 395 // copy file association icons 396 for (Map<String, ? super Object> fa : FILE_ASSOCIATIONS.fetchFrom(params)) { 397 f = FA_ICON.fetchFrom(fa); 398 if (f != null && f.exists()) { 399 try (InputStream in2 = new FileInputStream(f)) { 400 Files.copy(in2, resourcesDir.resolve(f.getName())); 401 } 402 403 } 404 } 405 406 // Generate Info.plist 407 writeInfoPlist(contentsDir.resolve("Info.plist").toFile()); 408 409 // generate java runtime info.plist 410 writeRuntimeInfoPlist(runtimeDir.resolve("Contents/Info.plist").toFile()); 411 412 // copy library 413 Path runtimeMacOSDir = Files.createDirectories(runtimeDir.resolve("Contents/MacOS")); 414 Files.copy(runtimeRoot.resolve("lib/jli/libjli.dylib"), runtimeMacOSDir.resolve("libjli.dylib")); 415 416 // maybe sign 417 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { 418 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params); 419 if (signingIdentity != null) { 420 signAppBundle(params, root, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null); 421 } 422 } 423 } 424 425 426 private String getLauncherName(Map<String, ? super Object> params) { 427 if (APP_NAME.fetchFrom(params) != null) { 428 return APP_NAME.fetchFrom(params); 429 } else { 430 return MAIN_CLASS.fetchFrom(params); 431 } 432 } 433 434 public static String getLauncherCfgName(Map<String, ? super Object> p) { 435 return "Contents/Java/" + APP_NAME.fetchFrom(p) + ".cfg"; 436 } 437 438 private void copyClassPathEntries(Path javaDirectory) throws IOException { 439 List<RelativeFileSet> resourcesList = APP_RESOURCES_LIST.fetchFrom(params); 440 if (resourcesList == null) { 441 throw new RuntimeException(I18N.getString("message.null-classpath")); 442 } 443 444 for (RelativeFileSet classPath : resourcesList) { 445 File srcdir = classPath.getBaseDirectory(); 446 for (String fname : classPath.getIncludedFiles()) { 447 // use new File since fname can have file separators 448 Files.copy(new File(srcdir, fname).toPath(), new File(javaDirectory.toFile(), fname).toPath()); 449 } 450 } 451 } 452 453 private String getBundleName(Map<String, ? super Object> params) { 454 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { 455 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); 456 if (bn.length() > 16) { 457 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn)); 458 } 459 return MAC_CF_BUNDLE_NAME.fetchFrom(params); 460 } else if (APP_NAME.fetchFrom(params) != null) { 461 return APP_NAME.fetchFrom(params); 462 } else { 463 String nm = MAIN_CLASS.fetchFrom(params); 464 if (nm.length() > 16) { 465 nm = nm.substring(0, 16); 466 } 467 return nm; 468 } 469 } 470 471 private void writeRuntimeInfoPlist(File file) throws IOException { 472 Map<String, String> data = new HashMap<>(); 473 data.put("CF_BUNDLE_IDENTIFIER", "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 474 data.put("CF_BUNDLE_NAME", "Java Runtime Image"); 475 data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params)); 476 data.put("CF_BUNDLE_SHORT_VERSION_STRING", VERSION.fetchFrom(params)); 477 478 Writer w = new BufferedWriter(new FileWriter(file)); 479 w.write(preprocessTextResource( 480 "package/macosx/Runtime-Info.plist", 481 I18N.getString("resource.runtime-info-plist"), 482 TEMPLATE_RUNTIME_INFO_PLIST, 483 data, 484 VERBOSE.fetchFrom(params), 485 DROP_IN_RESOURCES_ROOT.fetchFrom(params))); 486 w.close(); 487 } 488 489 private void writeInfoPlist(File file) throws IOException { 490 Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath())); 491 492 //prepare config for exe 493 //Note: do not need CFBundleDisplayName if we do not support localization 494 Map<String, String> data = new HashMap<>(); 495 data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns"); 496 data.put("DEPLOY_BUNDLE_IDENTIFIER", 497 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 498 data.put("DEPLOY_BUNDLE_NAME", 499 getBundleName(params)); 500 data.put("DEPLOY_BUNDLE_COPYRIGHT", 501 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown"); 502 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); 503 data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime"); 504 data.put("DEPLOY_BUNDLE_SHORT_VERSION", 505 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0"); 506 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", 507 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); 508 data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params)); 509 510 boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null; 511 boolean hasMainModule = StandardBundlerParam.MODULE.fetchFrom(params) != null; 512 513 if (hasMainJar) { 514 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next()); 515 } 516 else if (hasMainModule) { 517 data.put("DEPLOY_MODULE_NAME", StandardBundlerParam.MODULE.fetchFrom(params)); 518 } 519 520 data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase()); 521 522 StringBuilder sb = new StringBuilder(); 523 List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params); 524 525 String newline = ""; //So we don't add unneccessary extra line after last append 526 for (String o : jvmOptions) { 527 sb.append(newline).append(" <string>").append(o).append("</string>"); 528 newline = "\n"; 529 } 530 531 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params); 532 for (Map.Entry<String, String> entry : jvmProps.entrySet()) { 533 sb.append(newline) 534 .append(" <string>-D") 535 .append(entry.getKey()) 536 .append("=") 537 .append(entry.getValue()) 538 .append("</string>"); 539 newline = "\n"; 540 } 541 542 String preloader = PRELOADER_CLASS.fetchFrom(params); 543 if (preloader != null) { 544 sb.append(newline) 545 .append(" <string>-Djavafx.preloader=") 546 .append(preloader) 547 .append("</string>"); 548 } 549 550 data.put("DEPLOY_JVM_OPTIONS", sb.toString()); 551 552 sb = new StringBuilder(); 553 List<String> args = ARGUMENTS.fetchFrom(params); 554 newline = ""; //So we don't add unneccessary extra line after last append 555 for (String o : args) { 556 sb.append(newline).append(" <string>").append(o).append("</string>"); 557 newline = "\n"; 558 } 559 data.put("DEPLOY_ARGUMENTS", sb.toString()); 560 561 newline = ""; 562 sb = new StringBuilder(); 563 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params); 564 for (Map.Entry<String, String> arg : overridableJVMOptions.entrySet()) { 565 sb.append(newline) 566 .append(" <key>").append(arg.getKey()).append("</key>\n") 567 .append(" <string>").append(arg.getValue()).append("</string>"); 568 newline = "\n"; 569 } 570 data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString()); 571 572 573 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); 574 575 StringBuilder macroedPath = new StringBuilder(); 576 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { 577 macroedPath.append(s); 578 macroedPath.append(":"); 579 } 580 macroedPath.deleteCharAt(macroedPath.length() - 1); 581 582 data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString()); 583 584 StringBuilder bundleDocumentTypes = new StringBuilder(); 585 StringBuilder exportedTypes = new StringBuilder(); 586 for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { 587 588 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 589 590 if (extensions == null) { 591 Log.info(I18N.getString("message.creating-association-with-null-extension")); 592 } 593 594 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); 595 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty()) 596 ? "mime" 597 : extensions.get(0)); 598 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 599 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS 600 601 bundleDocumentTypes.append(" <dict>\n") 602 .append(" <key>LSItemContentTypes</key>\n") 603 .append(" <array>\n") 604 .append(" <string>") 605 .append(itemContentType) 606 .append("</string>\n") 607 .append(" </array>\n") 608 .append("\n") 609 .append(" <key>CFBundleTypeName</key>\n") 610 .append(" <string>") 611 .append(description) 612 .append("</string>\n") 613 .append("\n") 614 .append(" <key>LSHandlerRank</key>\n") 615 .append(" <string>Owner</string>\n") //TODO make a bundler arg 616 .append("\n") 617 .append(" <key>CFBundleTypeRole</key>\n") 618 .append(" <string>Editor</string>\n") // TODO make a bundler arg 619 .append("\n") 620 .append(" <key>LSIsAppleDefaultForType</key>\n") 621 .append(" <true/>\n") // TODO make a bundler arg 622 .append("\n"); 623 624 if (icon != null && icon.exists()) { 625 //? 626 bundleDocumentTypes.append(" <key>CFBundleTypeIconFile</key>\n") 627 .append(" <string>") 628 .append(icon.getName()) 629 .append("</string>\n"); 630 } 631 bundleDocumentTypes.append(" </dict>\n"); 632 633 exportedTypes.append(" <dict>\n") 634 .append(" <key>UTTypeIdentifier</key>\n") 635 .append(" <string>") 636 .append(itemContentType) 637 .append("</string>\n") 638 .append("\n") 639 .append(" <key>UTTypeDescription</key>\n") 640 .append(" <string>") 641 .append(description) 642 .append("</string>\n") 643 .append(" <key>UTTypeConformsTo</key>\n") 644 .append(" <array>\n") 645 .append(" <string>public.data</string>\n") //TODO expose this? 646 .append(" </array>\n") 647 .append("\n"); 648 649 if (icon != null && icon.exists()) { 650 exportedTypes.append(" <key>UTTypeIconFile</key>\n") 651 .append(" <string>") 652 .append(icon.getName()) 653 .append("</string>\n") 654 .append("\n"); 655 } 656 657 exportedTypes.append("\n") 658 .append(" <key>UTTypeTagSpecification</key>\n") 659 .append(" <dict>\n") 660 //TODO expose via param? .append(" <key>com.apple.ostype</key>\n"); 661 //TODO expose via param? .append(" <string>ABCD</string>\n") 662 .append("\n"); 663 664 if (extensions != null && !extensions.isEmpty()) { 665 exportedTypes.append(" <key>public.filename-extension</key>\n") 666 .append(" <array>\n"); 667 668 for (String ext : extensions) { 669 exportedTypes.append(" <string>") 670 .append(ext) 671 .append("</string>\n"); 672 } 673 exportedTypes.append(" </array>\n"); 674 } 675 if (mimeTypes != null && !mimeTypes.isEmpty()) { 676 exportedTypes.append(" <key>public.mime-type</key>\n") 677 .append(" <array>\n"); 678 679 for (String mime : mimeTypes) { 680 exportedTypes.append(" <string>") 681 .append(mime) 682 .append("</string>\n"); 683 } 684 exportedTypes.append(" </array>\n"); 685 } 686 exportedTypes.append(" </dict>\n") 687 .append(" </dict>\n"); 688 } 689 String associationData; 690 if (bundleDocumentTypes.length() > 0) { 691 associationData = "\n <key>CFBundleDocumentTypes</key>\n <array>\n" 692 + bundleDocumentTypes.toString() 693 + " </array>\n\n <key>UTExportedTypeDeclarations</key>\n <array>\n" 694 + exportedTypes.toString() 695 + " </array>\n"; 696 } else { 697 associationData = ""; 698 } 699 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); 700 701 702 Writer w = new BufferedWriter(new FileWriter(file)); 703 w.write(preprocessTextResource( 704 //MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(), 705 "package/macosx/Info.plist", 706 I18N.getString("resource.app-info-plist"), 707 TEMPLATE_INFO_PLIST_LITE, 708 data, VERBOSE.fetchFrom(params), 709 DROP_IN_RESOURCES_ROOT.fetchFrom(params))); 710 w.close(); 711 } 712 713 private void writePkgInfo(File file) throws IOException { 714 //hardcoded as it does not seem we need to change it ever 715 String signature = "????"; 716 717 try (Writer out = new BufferedWriter(new FileWriter(file))) { 718 out.write(OS_TYPE_CODE + signature); 719 out.flush(); 720 } 721 } 722 723 public static void signAppBundle(Map<String, ? super Object> params, Path appLocation, String signingIdentity, String identifierPrefix, String entitlementsFile, String inheritedEntitlements) throws IOException { 724 AtomicReference<IOException> toThrow = new AtomicReference<>(); 725 String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); 726 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 727 728 // sign all dylibs and jars 729 Files.walk(appLocation) 730 // fix permissions 731 .peek(path -> { 732 try { 733 Set<PosixFilePermission> pfp = Files.getPosixFilePermissions(path); 734 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { 735 pfp = EnumSet.copyOf(pfp); 736 pfp.add(PosixFilePermission.OWNER_WRITE); 737 Files.setPosixFilePermissions(path, pfp); 738 } 739 } catch (IOException e) { 740 Log.debug(e); 741 } 742 }) 743 .filter(p -> Files.isRegularFile(p) && 744 !(p.toString().contains("/Contents/MacOS/libjli.dylib") 745 || p.toString().contains("/Contents/MacOS/JavaAppletPlugin") 746 || p.toString().endsWith(appExecutable)) 747 ).forEach(p -> { 748 //noinspection ThrowableResultOfMethodCallIgnored 749 if (toThrow.get() != null) return; 750 751 // If p is a symlink then skip the signing process. 752 if (Files.isSymbolicLink(p)) { 753 if (VERBOSE.fetchFrom(params)) { 754 Log.verbose(MessageFormat.format(I18N.getString("message.ignoring.symlink"), p.toString())); 755 } 756 } 757 else { 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 793 IOException ioe = toThrow.get(); 794 if (ioe != null) { 795 throw ioe; 796 } 797 798 // sign all plugins and frameworks 799 Consumer<? super Path> signIdentifiedByPList = path -> { 800 //noinspection ThrowableResultOfMethodCallIgnored 801 if (toThrow.get() != null) return; 802 803 try { 804 List<String> args = new ArrayList<>(); 805 args.addAll(Arrays.asList("codesign", 806 "-s", signingIdentity, // sign with this key 807 "--prefix", identifierPrefix, // use the identifier as a prefix 808 "-vvvv")); 809 if (keyChain != null && !keyChain.isEmpty()) { 810 args.add("--keychain"); 811 args.add(keyChain); 812 } 813 args.add(path.toString()); 814 ProcessBuilder pb = new ProcessBuilder(args); 815 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 816 817 args = new ArrayList<>(); 818 args.addAll(Arrays.asList("codesign", 819 "-s", signingIdentity, // sign with this key 820 "--prefix", identifierPrefix, // use the identifier as a prefix 821 "-vvvv")); 822 if (keyChain != null && !keyChain.isEmpty()) { 823 args.add("--keychain"); 824 args.add(keyChain); 825 } 826 args.add(path.toString() + "/Contents/_CodeSignature/CodeResources"); 827 pb = new ProcessBuilder(args); 828 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 829 } catch (IOException e) { 830 toThrow.set(e); 831 } 832 }; 833 834 Path pluginsPath = appLocation.resolve("Contents/PlugIns"); 835 if (Files.isDirectory(pluginsPath)) { 836 Files.list(pluginsPath) 837 .forEach(signIdentifiedByPList); 838 839 ioe = toThrow.get(); 840 if (ioe != null) { 841 throw ioe; 842 } 843 } 844 Path frameworkPath = appLocation.resolve("Contents/Frameworks"); 845 if (Files.isDirectory(frameworkPath)) { 846 Files.list(frameworkPath) 847 .forEach(signIdentifiedByPList); 848 849 ioe = toThrow.get(); 850 if (ioe != null) { 851 throw ioe; 852 } 853 } 854 855 // sign the app itself 856 List<String> args = new ArrayList<>(); 857 args.addAll(Arrays.asList("codesign", 858 "-s", signingIdentity, // sign with this key 859 "-vvvv")); // super verbose output 860 if (entitlementsFile != null) { 861 args.add("--entitlements"); 862 args.add(entitlementsFile); // entitlements 863 } 864 if (keyChain != null && !keyChain.isEmpty()) { 865 args.add("--keychain"); 866 args.add(keyChain); 867 } 868 args.add(appLocation.toString()); 869 870 ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[args.size()])); 871 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 872 } 873 874 }