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.Platform; 32 import com.oracle.tools.packager.RelativeFileSet; 33 import com.oracle.tools.packager.StandardBundlerParam; 34 import com.oracle.tools.packager.mac.MacResources; 35 36 import jdk.packager.internal.legacy.builders.AbstractAppImageBuilder; 37 import jdk.packager.internal.legacy.JLinkBundlerHelper; 38 39 import java.io.BufferedWriter; 40 import java.io.File; 41 import java.io.FileInputStream; 42 import java.io.FileOutputStream; 43 import java.io.FileWriter; 44 import java.io.IOException; 45 import java.io.InputStream; 46 import java.io.OutputStream; 47 import java.io.OutputStreamWriter; 48 import java.io.UncheckedIOException; 49 import java.io.Writer; 50 import java.math.BigInteger; 51 import java.nio.file.Files; 52 import java.nio.file.Path; 53 import java.nio.file.attribute.PosixFilePermission; 54 import java.text.MessageFormat; 55 import java.util.ArrayList; 56 import java.util.Arrays; 57 import java.util.EnumSet; 58 import java.util.HashMap; 59 import java.util.List; 60 import java.util.Map; 61 import java.util.Objects; 62 import java.util.Optional; 63 import java.util.ResourceBundle; 64 import java.util.Set; 65 import java.util.concurrent.atomic.AtomicReference; 66 import java.util.function.Consumer; 67 68 import static com.oracle.tools.packager.StandardBundlerParam.*; 69 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.*; 70 import static com.oracle.tools.packager.mac.MacAppBundler.*; 71 72 73 public class MacAppImageBuilder extends AbstractAppImageBuilder { 74 75 private static final ResourceBundle I18N = 76 ResourceBundle.getBundle(MacAppImageBuilder.class.getName()); 77 78 private static final String EXECUTABLE_NAME = "JavaAppLauncher"; 79 private static final String LIBRARY_NAME = "libpackager.dylib"; 80 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns"; 81 private static final String OS_TYPE_CODE = "APPL"; 82 private static final String TEMPLATE_INFO_PLIST_LITE = "Info-lite.plist.template"; 83 private static final String TEMPLATE_RUNTIME_INFO_PLIST = "Runtime-Info.plist.template"; 84 85 private final Path root; 86 private final Path contentsDir; 87 private final Path javaDir; 88 private final Path resourcesDir; 89 private final Path macOSDir; 90 private final Path runtimeDir; 91 private final Path runtimeRoot; 92 private final Path mdir; 93 94 private final Map<String, ? super Object> params; 95 96 private static List<String> keyChains; 97 98 private static Map<String, String> getMacCategories() { 99 Map<String, String> map = new HashMap<>(); 100 map.put("Business", "public.app-category.business"); 101 map.put("Developer Tools", "public.app-category.developer-tools"); 102 map.put("Education", "public.app-category.education"); 103 map.put("Entertainment", "public.app-category.entertainment"); 104 map.put("Finance", "public.app-category.finance"); 105 map.put("Games", "public.app-category.games"); 106 map.put("Graphics & Design", "public.app-category.graphics-design"); 107 map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness"); 108 map.put("Lifestyle", "public.app-category.lifestyle"); 109 map.put("Medical", "public.app-category.medical"); 110 map.put("Music", "public.app-category.music"); 111 map.put("News", "public.app-category.news"); 112 map.put("Photography", "public.app-category.photography"); 113 map.put("Productivity", "public.app-category.productivity"); 114 map.put("Reference", "public.app-category.reference"); 115 map.put("Social Networking", "public.app-category.social-networking"); 116 map.put("Sports", "public.app-category.sports"); 117 map.put("Travel", "public.app-category.travel"); 118 map.put("Utilities", "public.app-category.utilities"); 119 map.put("Video", "public.app-category.video"); 120 map.put("Weather", "public.app-category.weather"); 121 122 map.put("Action Games", "public.app-category.action-games"); 123 map.put("Adventure Games", "public.app-category.adventure-games"); 124 map.put("Arcade Games", "public.app-category.arcade-games"); 125 map.put("Board Games", "public.app-category.board-games"); 126 map.put("Card Games", "public.app-category.card-games"); 127 map.put("Casino Games", "public.app-category.casino-games"); 128 map.put("Dice Games", "public.app-category.dice-games"); 129 map.put("Educational Games", "public.app-category.educational-games"); 130 map.put("Family Games", "public.app-category.family-games"); 131 map.put("Kids Games", "public.app-category.kids-games"); 132 map.put("Music Games", "public.app-category.music-games"); 133 map.put("Puzzle Games", "public.app-category.puzzle-games"); 134 map.put("Racing Games", "public.app-category.racing-games"); 135 map.put("Role Playing Games", "public.app-category.role-playing-games"); 136 map.put("Simulation Games", "public.app-category.simulation-games"); 137 map.put("Sports Games", "public.app-category.sports-games"); 138 map.put("Strategy Games", "public.app-category.strategy-games"); 139 map.put("Trivia Games", "public.app-category.trivia-games"); 140 map.put("Word Games", "public.app-category.word-games"); 141 142 return map; 143 } 144 145 public static final BundlerParamInfo<Boolean> MAC_CONFIGURE_LAUNCHER_IN_PLIST = 146 new StandardBundlerParam<>( 147 I18N.getString("param.configure-launcher-in-plist"), 148 I18N.getString("param.configure-launcher-in-plist.description"), 149 "mac.configure-launcher-in-plist", 150 Boolean.class, 151 params -> Boolean.FALSE, 152 (s, p) -> Boolean.valueOf(s)); 153 154 public static final BundlerParamInfo<String> MAC_CATEGORY = 155 new StandardBundlerParam<>( 156 I18N.getString("param.category-name"), 157 I18N.getString("param.category-name.description"), 158 "mac.category", 159 String.class, 160 CATEGORY::fetchFrom, 161 (s, p) -> s 162 ); 163 164 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 165 new StandardBundlerParam<>( 166 I18N.getString("param.cfbundle-name.name"), 167 I18N.getString("param.cfbundle-name.description"), 168 "mac.CFBundleName", 169 String.class, 170 params -> null, 171 (s, p) -> s); 172 173 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 174 new StandardBundlerParam<>( 175 I18N.getString("param.cfbundle-identifier.name"), 176 I18N.getString("param.cfbundle-identifier.description"), 177 "mac.CFBundleIdentifier", 178 String.class, 179 IDENTIFIER::fetchFrom, 180 (s, p) -> s); 181 182 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 183 new StandardBundlerParam<>( 184 I18N.getString("param.cfbundle-version.name"), 185 I18N.getString("param.cfbundle-version.description"), 186 "mac.CFBundleVersion", 187 String.class, 188 p -> { 189 String s = VERSION.fetchFrom(p); 190 if (validCFBundleVersion(s)) { 191 return s; 192 } else { 193 return "100"; 194 } 195 }, 196 (s, p) -> s); 197 198 public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>( 199 I18N.getString("param.config-root.name"), 200 I18N.getString("param.config-root.description"), 201 "configRoot", 202 File.class, 203 params -> { 204 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx"); 205 configRoot.mkdirs(); 206 return configRoot; 207 }, 208 (s, p) -> new File(s)); 209 210 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>( 211 I18N.getString("param.default-icon-icns"), 212 I18N.getString("param.default-icon-icns.description"), 213 ".mac.default.icns", 214 String.class, 215 params -> TEMPLATE_BUNDLE_ICON, 216 (s, p) -> s); 217 218 // public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>( 219 // I18N.getString("param.signing-key-developer-id-app.name"), 220 // I18N.getString("param.signing-key-developer-id-app.description"), 221 // "mac.signing-key-developer-id-app", 222 // String.class, 223 // params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)), 224 // (s, p) -> s); 225 226 // public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>( 227 // I18N.getString("param.bundle-id-signing-prefix.name"), 228 // I18N.getString("param.bundle-id-signing-prefix.description"), 229 // "mac.bundle-id-signing-prefix", 230 // String.class, 231 // params -> IDENTIFIER.fetchFrom(params) + ".", 232 // (s, p) -> s); 233 // 234 public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>( 235 I18N.getString("param.icon-icns.name"), 236 I18N.getString("param.icon-icns.description"), 237 "icon.icns", 238 File.class, 239 params -> { 240 File f = ICON.fetchFrom(params); 241 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 242 Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f)); 243 return null; 244 } 245 return f; 246 }, 247 (s, p) -> new File(s)); 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 this.params = config; 255 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app"); 256 this.contentsDir = root.resolve("Contents"); 257 this.javaDir = contentsDir.resolve("Java"); 258 this.resourcesDir = contentsDir.resolve("Resources"); 259 this.macOSDir = contentsDir.resolve("MacOS"); 260 this.runtimeDir = contentsDir.resolve("PlugIns/Java.runtime"); 261 this.runtimeRoot = runtimeDir.resolve("Contents/Home"); 262 this.mdir = runtimeRoot.resolve("lib"); 263 Files.createDirectories(javaDir); 264 Files.createDirectories(resourcesDir); 265 Files.createDirectories(macOSDir); 266 Files.createDirectories(runtimeDir); 267 } 268 269 private static String extractAppName() { 270 return ""; 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 public InputStream getResourceAsStream(String name) { 356 return MacResources.class.getResourceAsStream(name); 357 } 358 359 @Override 360 public void prepareApplicationFiles() throws IOException { 361 File f; 362 363 // Generate PkgInfo 364 File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo"); 365 pkgInfoFile.createNewFile(); 366 writePkgInfo(pkgInfoFile); 367 368 369 // Copy executable to MacOS folder 370 Path executable = macOSDir.resolve(getLauncherName(params)); 371 writeEntry(MacResources.class.getResourceAsStream(EXECUTABLE_NAME), executable); 372 executable.toFile().setExecutable(true, false); 373 374 // Copy library to the MacOS folder 375 writeEntry( 376 MacResources.class.getResourceAsStream(LIBRARY_NAME), 377 macOSDir.resolve(LIBRARY_NAME) 378 ); 379 380 // generate launcher config 381 382 writeCfgFile(params, new File(root.toFile(), getLauncherCfgName(params)), "$APPDIR/PlugIns/Java.runtime"); 383 384 // Copy class path entries to Java folder 385 copyClassPathEntries(javaDir); 386 387 /*********** Take care of "config" files *******/ 388 // Copy icon to Resources folder 389 File icon = ICON_ICNS.fetchFrom(params); 390 InputStream in = locateResource("package/macosx/" + APP_NAME.fetchFrom(params) + ".icns", 391 "icon", 392 DEFAULT_ICNS_ICON.fetchFrom(params), 393 icon, 394 VERBOSE.fetchFrom(params), 395 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 396 Files.copy(in, resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns")); 397 398 // copy file association icons 399 for (Map<String, ? super Object> fa : FILE_ASSOCIATIONS.fetchFrom(params)) { 400 f = FA_ICON.fetchFrom(fa); 401 if (f != null && f.exists()) { 402 try (InputStream in2 = new FileInputStream(f)) { 403 Files.copy(in2, resourcesDir.resolve(f.getName())); 404 } 405 406 } 407 } 408 409 // Generate Info.plist 410 writeInfoPlist(contentsDir.resolve("Info.plist").toFile()); 411 412 // generate java runtime info.plist 413 writeRuntimeInfoPlist(runtimeDir.resolve("Contents/Info.plist").toFile()); 414 415 // copy library 416 Path runtimeMacOSDir = Files.createDirectories(runtimeDir.resolve("Contents/MacOS")); 417 Files.copy(runtimeRoot.resolve("lib/jli/libjli.dylib"), runtimeMacOSDir.resolve("libjli.dylib")); 418 419 // maybe sign 420 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { 421 try { 422 addNewKeychain(params); 423 } catch (InterruptedException e) { 424 Log.error(e.getMessage()); 425 } 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 restoreKeychainList(params); 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 copyEntry(javaDirectory, srcdir, fname); 457 } 458 } 459 } 460 461 private String getBundleName(Map<String, ? super Object> params) { 462 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { 463 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); 464 if (bn.length() > 16) { 465 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn)); 466 } 467 return MAC_CF_BUNDLE_NAME.fetchFrom(params); 468 } else if (APP_NAME.fetchFrom(params) != null) { 469 return APP_NAME.fetchFrom(params); 470 } else { 471 String nm = MAIN_CLASS.fetchFrom(params); 472 if (nm.length() > 16) { 473 nm = nm.substring(0, 16); 474 } 475 return nm; 476 } 477 } 478 479 private void writeRuntimeInfoPlist(File file) throws IOException { 480 Map<String, String> data = new HashMap<>(); 481 data.put("CF_BUNDLE_IDENTIFIER", "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 482 data.put("CF_BUNDLE_NAME", "Java Runtime Image"); 483 data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params)); 484 data.put("CF_BUNDLE_SHORT_VERSION_STRING", VERSION.fetchFrom(params)); 485 486 Writer w = new BufferedWriter(new FileWriter(file)); 487 w.write(preprocessTextResource( 488 "package/macosx/Runtime-Info.plist", 489 I18N.getString("resource.runtime-info-plist"), 490 TEMPLATE_RUNTIME_INFO_PLIST, 491 data, 492 VERBOSE.fetchFrom(params), 493 DROP_IN_RESOURCES_ROOT.fetchFrom(params))); 494 w.close(); 495 } 496 497 private void writeInfoPlist(File file) throws IOException { 498 Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath())); 499 500 //prepare config for exe 501 //Note: do not need CFBundleDisplayName if we do not support localization 502 Map<String, String> data = new HashMap<>(); 503 data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns"); 504 data.put("DEPLOY_BUNDLE_IDENTIFIER", 505 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 506 data.put("DEPLOY_BUNDLE_NAME", 507 getBundleName(params)); 508 data.put("DEPLOY_BUNDLE_COPYRIGHT", 509 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown"); 510 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); 511 data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime"); 512 data.put("DEPLOY_BUNDLE_SHORT_VERSION", 513 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0"); 514 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", 515 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); 516 data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params)); 517 518 boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null; 519 boolean hasMainModule = StandardBundlerParam.MODULE.fetchFrom(params) != null; 520 521 if (hasMainJar) { 522 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next()); 523 } 524 else if (hasMainModule) { 525 data.put("DEPLOY_MODULE_NAME", StandardBundlerParam.MODULE.fetchFrom(params)); 526 } 527 528 data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase()); 529 530 StringBuilder sb = new StringBuilder(); 531 List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params); 532 533 String newline = ""; //So we don't add unneccessary extra line after last append 534 for (String o : jvmOptions) { 535 sb.append(newline).append(" <string>").append(o).append("</string>"); 536 newline = "\n"; 537 } 538 539 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params); 540 for (Map.Entry<String, String> entry : jvmProps.entrySet()) { 541 sb.append(newline) 542 .append(" <string>-D") 543 .append(entry.getKey()) 544 .append("=") 545 .append(entry.getValue()) 546 .append("</string>"); 547 newline = "\n"; 548 } 549 550 String preloader = PRELOADER_CLASS.fetchFrom(params); 551 if (preloader != null) { 552 sb.append(newline) 553 .append(" <string>-Djavafx.preloader=") 554 .append(preloader) 555 .append("</string>"); 556 } 557 558 data.put("DEPLOY_JVM_OPTIONS", sb.toString()); 559 560 sb = new StringBuilder(); 561 List<String> args = ARGUMENTS.fetchFrom(params); 562 newline = ""; //So we don't add unneccessary extra line after last append 563 for (String o : args) { 564 sb.append(newline).append(" <string>").append(o).append("</string>"); 565 newline = "\n"; 566 } 567 data.put("DEPLOY_ARGUMENTS", sb.toString()); 568 569 newline = ""; 570 sb = new StringBuilder(); 571 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params); 572 for (Map.Entry<String, String> arg : overridableJVMOptions.entrySet()) { 573 sb.append(newline) 574 .append(" <key>").append(arg.getKey()).append("</key>\n") 575 .append(" <string>").append(arg.getValue()).append("</string>"); 576 newline = "\n"; 577 } 578 data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString()); 579 580 581 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); 582 583 StringBuilder macroedPath = new StringBuilder(); 584 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { 585 macroedPath.append(s); 586 macroedPath.append(":"); 587 } 588 macroedPath.deleteCharAt(macroedPath.length() - 1); 589 590 data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString()); 591 592 StringBuilder bundleDocumentTypes = new StringBuilder(); 593 StringBuilder exportedTypes = new StringBuilder(); 594 for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { 595 596 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 597 598 if (extensions == null) { 599 Log.info(I18N.getString("message.creating-association-with-null-extension")); 600 } 601 602 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); 603 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty()) 604 ? "mime" 605 : extensions.get(0)); 606 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 607 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS 608 609 bundleDocumentTypes.append(" <dict>\n") 610 .append(" <key>LSItemContentTypes</key>\n") 611 .append(" <array>\n") 612 .append(" <string>") 613 .append(itemContentType) 614 .append("</string>\n") 615 .append(" </array>\n") 616 .append("\n") 617 .append(" <key>CFBundleTypeName</key>\n") 618 .append(" <string>") 619 .append(description) 620 .append("</string>\n") 621 .append("\n") 622 .append(" <key>LSHandlerRank</key>\n") 623 .append(" <string>Owner</string>\n") //TODO make a bundler arg 624 .append("\n") 625 .append(" <key>CFBundleTypeRole</key>\n") 626 .append(" <string>Editor</string>\n") // TODO make a bundler arg 627 .append("\n") 628 .append(" <key>LSIsAppleDefaultForType</key>\n") 629 .append(" <true/>\n") // TODO make a bundler arg 630 .append("\n"); 631 632 if (icon != null && icon.exists()) { 633 //? 634 bundleDocumentTypes.append(" <key>CFBundleTypeIconFile</key>\n") 635 .append(" <string>") 636 .append(icon.getName()) 637 .append("</string>\n"); 638 } 639 bundleDocumentTypes.append(" </dict>\n"); 640 641 exportedTypes.append(" <dict>\n") 642 .append(" <key>UTTypeIdentifier</key>\n") 643 .append(" <string>") 644 .append(itemContentType) 645 .append("</string>\n") 646 .append("\n") 647 .append(" <key>UTTypeDescription</key>\n") 648 .append(" <string>") 649 .append(description) 650 .append("</string>\n") 651 .append(" <key>UTTypeConformsTo</key>\n") 652 .append(" <array>\n") 653 .append(" <string>public.data</string>\n") //TODO expose this? 654 .append(" </array>\n") 655 .append("\n"); 656 657 if (icon != null && icon.exists()) { 658 exportedTypes.append(" <key>UTTypeIconFile</key>\n") 659 .append(" <string>") 660 .append(icon.getName()) 661 .append("</string>\n") 662 .append("\n"); 663 } 664 665 exportedTypes.append("\n") 666 .append(" <key>UTTypeTagSpecification</key>\n") 667 .append(" <dict>\n") 668 //TODO expose via param? .append(" <key>com.apple.ostype</key>\n"); 669 //TODO expose via param? .append(" <string>ABCD</string>\n") 670 .append("\n"); 671 672 if (extensions != null && !extensions.isEmpty()) { 673 exportedTypes.append(" <key>public.filename-extension</key>\n") 674 .append(" <array>\n"); 675 676 for (String ext : extensions) { 677 exportedTypes.append(" <string>") 678 .append(ext) 679 .append("</string>\n"); 680 } 681 exportedTypes.append(" </array>\n"); 682 } 683 if (mimeTypes != null && !mimeTypes.isEmpty()) { 684 exportedTypes.append(" <key>public.mime-type</key>\n") 685 .append(" <array>\n"); 686 687 for (String mime : mimeTypes) { 688 exportedTypes.append(" <string>") 689 .append(mime) 690 .append("</string>\n"); 691 } 692 exportedTypes.append(" </array>\n"); 693 } 694 exportedTypes.append(" </dict>\n") 695 .append(" </dict>\n"); 696 } 697 String associationData; 698 if (bundleDocumentTypes.length() > 0) { 699 associationData = "\n <key>CFBundleDocumentTypes</key>\n <array>\n" 700 + bundleDocumentTypes.toString() 701 + " </array>\n\n <key>UTExportedTypeDeclarations</key>\n <array>\n" 702 + exportedTypes.toString() 703 + " </array>\n"; 704 } else { 705 associationData = ""; 706 } 707 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); 708 709 710 Writer w = new BufferedWriter(new FileWriter(file)); 711 w.write(preprocessTextResource( 712 //MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(), 713 "package/macosx/Info.plist", 714 I18N.getString("resource.app-info-plist"), 715 TEMPLATE_INFO_PLIST_LITE, 716 data, VERBOSE.fetchFrom(params), 717 DROP_IN_RESOURCES_ROOT.fetchFrom(params))); 718 w.close(); 719 } 720 721 private void writePkgInfo(File file) throws IOException { 722 //hardcoded as it does not seem we need to change it ever 723 String signature = "????"; 724 725 try (Writer out = new BufferedWriter(new FileWriter(file))) { 726 out.write(OS_TYPE_CODE + signature); 727 out.flush(); 728 } 729 } 730 731 public static void addNewKeychain(Map<String, ? super Object> params) 732 throws IOException, InterruptedException { 733 if (Platform.getMajorVersion() < 10 || 734 (Platform.getMajorVersion() == 10 && Platform.getMinorVersion() < 12)) { 735 // we need this for OS X 10.12+ 736 return; 737 } 738 739 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 740 if (keyChain == null || keyChain.isEmpty()) { 741 return; 742 } 743 744 // get current keychain list 745 String keyChainPath = new File (keyChain).getAbsolutePath().toString(); 746 List<String> keychainList = new ArrayList<>(); 747 int ret = IOUtils.getProcessOutput(keychainList, "security", "list-keychains"); 748 if (ret != 0) { 749 Log.error(I18N.getString("message.keychain.error")); 750 return; 751 } 752 753 boolean contains = keychainList.stream().anyMatch( 754 str -> str.trim().equals("\""+keyChainPath.trim()+"\"")); 755 if (contains) { 756 // keychain is already added in the search list 757 return; 758 } 759 760 keyChains = new ArrayList<>(); 761 // remove " 762 keychainList.forEach((String s) -> { 763 String path = s.trim(); 764 if (path.startsWith("\"") && path.endsWith("\"")) { 765 path = path.substring(1, path.length()-1); 766 } 767 keyChains.add(path); 768 }); 769 770 List<String> args = new ArrayList<>(); 771 args.add("security"); 772 args.add("list-keychains"); 773 args.add("-s"); 774 775 args.addAll(keyChains); 776 args.add(keyChain); 777 778 ProcessBuilder pb = new ProcessBuilder(args); 779 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 780 } 781 782 public static void restoreKeychainList(Map<String, ? super Object> params) throws IOException{ 783 if (Platform.getMajorVersion() < 10 || 784 (Platform.getMajorVersion() == 10 && Platform.getMinorVersion() < 12)) { 785 // we need this for OS X 10.12+ 786 return; 787 } 788 789 if (keyChains == null || keyChains.isEmpty()) { 790 return; 791 } 792 793 List<String> args = new ArrayList<>(); 794 args.add("security"); 795 args.add("list-keychains"); 796 args.add("-s"); 797 798 args.addAll(keyChains); 799 800 ProcessBuilder pb = new ProcessBuilder(args); 801 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 802 } 803 804 public static void signAppBundle(Map<String, ? super Object> params, Path appLocation, String signingIdentity, String identifierPrefix, String entitlementsFile, String inheritedEntitlements) throws IOException { 805 AtomicReference<IOException> toThrow = new AtomicReference<>(); 806 String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); 807 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 808 809 // sign all dylibs and jars 810 Files.walk(appLocation) 811 // fix permissions 812 .peek(path -> { 813 try { 814 Set<PosixFilePermission> pfp = Files.getPosixFilePermissions(path); 815 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { 816 pfp = EnumSet.copyOf(pfp); 817 pfp.add(PosixFilePermission.OWNER_WRITE); 818 Files.setPosixFilePermissions(path, pfp); 819 } 820 } catch (IOException e) { 821 Log.debug(e); 822 } 823 }) 824 .filter(p -> Files.isRegularFile(p) && 825 !(p.toString().contains("/Contents/MacOS/libjli.dylib") 826 || p.toString().contains("/Contents/MacOS/JavaAppletPlugin") 827 || p.toString().endsWith(appExecutable)) 828 ).forEach(p -> { 829 //noinspection ThrowableResultOfMethodCallIgnored 830 if (toThrow.get() != null) return; 831 832 // If p is a symlink then skip the signing process. 833 if (Files.isSymbolicLink(p)) { 834 if (VERBOSE.fetchFrom(params)) { 835 Log.verbose(MessageFormat.format(I18N.getString("message.ignoring.symlink"), p.toString())); 836 } 837 } 838 else { 839 List<String> args = new ArrayList<>(); 840 args.addAll(Arrays.asList("codesign", 841 "-s", signingIdentity, // sign with this key 842 "--prefix", identifierPrefix, // use the identifier as a prefix 843 "-vvvv")); 844 if (entitlementsFile != null && 845 (p.toString().endsWith(".jar") 846 || p.toString().endsWith(".dylib"))) { 847 args.add("--entitlements"); 848 args.add(entitlementsFile); // entitlements 849 } else if (inheritedEntitlements != null && Files.isExecutable(p)) { 850 args.add("--entitlements"); 851 args.add(inheritedEntitlements); // inherited entitlements for executable processes 852 } 853 if (keyChain != null && !keyChain.isEmpty()) { 854 args.add("--keychain"); 855 args.add(keyChain); 856 } 857 args.add(p.toString()); 858 859 try { 860 Set<PosixFilePermission> oldPermissions = Files.getPosixFilePermissions(p); 861 File f = p.toFile(); 862 f.setWritable(true, true); 863 864 ProcessBuilder pb = new ProcessBuilder(args); 865 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 866 867 Files.setPosixFilePermissions(p, oldPermissions); 868 } catch (IOException ioe) { 869 toThrow.set(ioe); 870 } 871 } 872 }); 873 874 IOException ioe = toThrow.get(); 875 if (ioe != null) { 876 throw ioe; 877 } 878 879 // sign all plugins and frameworks 880 Consumer<? super Path> signIdentifiedByPList = path -> { 881 //noinspection ThrowableResultOfMethodCallIgnored 882 if (toThrow.get() != null) return; 883 884 try { 885 List<String> args = new ArrayList<>(); 886 args.addAll(Arrays.asList("codesign", 887 "-s", signingIdentity, // sign with this key 888 "--prefix", identifierPrefix, // use the identifier as a prefix 889 "-vvvv")); 890 if (keyChain != null && !keyChain.isEmpty()) { 891 args.add("--keychain"); 892 args.add(keyChain); 893 } 894 args.add(path.toString()); 895 ProcessBuilder pb = new ProcessBuilder(args); 896 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 897 898 args = new ArrayList<>(); 899 args.addAll(Arrays.asList("codesign", 900 "-s", signingIdentity, // sign with this key 901 "--prefix", identifierPrefix, // use the identifier as a prefix 902 "-vvvv")); 903 if (keyChain != null && !keyChain.isEmpty()) { 904 args.add("--keychain"); 905 args.add(keyChain); 906 } 907 args.add(path.toString() + "/Contents/_CodeSignature/CodeResources"); 908 pb = new ProcessBuilder(args); 909 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 910 } catch (IOException e) { 911 toThrow.set(e); 912 } 913 }; 914 915 Path pluginsPath = appLocation.resolve("Contents/PlugIns"); 916 if (Files.isDirectory(pluginsPath)) { 917 Files.list(pluginsPath) 918 .forEach(signIdentifiedByPList); 919 920 ioe = toThrow.get(); 921 if (ioe != null) { 922 throw ioe; 923 } 924 } 925 Path frameworkPath = appLocation.resolve("Contents/Frameworks"); 926 if (Files.isDirectory(frameworkPath)) { 927 Files.list(frameworkPath) 928 .forEach(signIdentifiedByPList); 929 930 ioe = toThrow.get(); 931 if (ioe != null) { 932 throw ioe; 933 } 934 } 935 936 // sign the app itself 937 List<String> args = new ArrayList<>(); 938 args.addAll(Arrays.asList("codesign", 939 "-s", signingIdentity, // sign with this key 940 "-vvvv")); // super verbose output 941 if (entitlementsFile != null) { 942 args.add("--entitlements"); 943 args.add(entitlementsFile); // entitlements 944 } 945 if (keyChain != null && !keyChain.isEmpty()) { 946 args.add("--keychain"); 947 args.add(keyChain); 948 } 949 args.add(appLocation.toString()); 950 951 ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[args.size()])); 952 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 953 } 954 955 }