1 /*
   2  * Copyright (c) 2015, 2019, 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 
  26 package jdk.jpackage.internal;
  27 
  28 import java.io.BufferedWriter;
  29 import java.io.File;
  30 import java.io.FileInputStream;
  31 import java.io.FileOutputStream;
  32 import java.io.FileWriter;
  33 import java.io.IOException;
  34 import java.io.InputStream;
  35 import java.io.OutputStream;
  36 import java.io.OutputStreamWriter;
  37 import java.io.UncheckedIOException;
  38 import java.io.Writer;
  39 import java.math.BigInteger;
  40 import java.nio.file.Files;
  41 import java.nio.file.Path;
  42 import java.nio.file.StandardCopyOption;
  43 import java.nio.file.attribute.PosixFilePermission;
  44 import java.text.MessageFormat;
  45 import java.util.ArrayList;
  46 import java.util.Arrays;
  47 import java.util.EnumSet;
  48 import java.util.HashMap;
  49 import java.util.List;
  50 import java.util.Map;
  51 import java.util.Objects;
  52 import java.util.Optional;
  53 import java.util.ResourceBundle;
  54 import java.util.Set;
  55 import java.util.concurrent.atomic.AtomicReference;
  56 import java.util.function.Consumer;
  57 
  58 import static jdk.jpackage.internal.StandardBundlerParam.*;
  59 import static jdk.jpackage.internal.MacBaseInstallerBundler.*;
  60 import static jdk.jpackage.internal.MacAppBundler.*;
  61 
  62 public class MacAppImageBuilder extends AbstractAppImageBuilder {
  63 
  64     private static final ResourceBundle I18N = ResourceBundle.getBundle(
  65             "jdk.jpackage.internal.resources.MacResources");
  66 
  67     private static final String LIBRARY_NAME = "libapplauncher.dylib";
  68     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  69     private static final String OS_TYPE_CODE = "APPL";
  70     private static final String TEMPLATE_INFO_PLIST_LITE =
  71             "Info-lite.plist.template";
  72     private static final String TEMPLATE_RUNTIME_INFO_PLIST =
  73             "Runtime-Info.plist.template";
  74 
  75     private final Path root;
  76     private final Path contentsDir;
  77     private final Path javaDir;
  78     private final Path javaModsDir;
  79     private final Path resourcesDir;
  80     private final Path macOSDir;
  81     private final Path runtimeDir;
  82     private final Path runtimeRoot;
  83     private final Path mdir;
  84 
  85     private final Map<String, ? super Object> params;
  86 
  87     private static List<String> keyChains;
  88 
  89     public static final BundlerParamInfo<Boolean>
  90             MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>(
  91                     I18N.getString("param.configure-launcher-in-plist"),
  92                     I18N.getString(
  93                             "param.configure-launcher-in-plist.description"),
  94                     "mac.configure-launcher-in-plist",
  95                     Boolean.class,
  96                     params -> Boolean.FALSE,
  97                     (s, p) -> Boolean.valueOf(s));
  98 
  99     public static final BundlerParamInfo<String> MAC_CATEGORY =
 100             new StandardBundlerParam<>(
 101                     I18N.getString("param.category-name"),
 102                     I18N.getString("param.category-name.description"),
 103                     "mac.category",
 104                     String.class,
 105                     CATEGORY::fetchFrom,
 106                     (s, p) -> s
 107             );
 108 
 109     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
 110             new StandardBundlerParam<>(
 111                     I18N.getString("param.cfbundle-name.name"),
 112                     I18N.getString("param.cfbundle-name.description"),
 113                     "mac.CFBundleName",
 114                     String.class,
 115                     params -> null,
 116                     (s, p) -> s);
 117 
 118     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER =
 119             new StandardBundlerParam<>(
 120                     I18N.getString("param.cfbundle-identifier.name"),
 121                     I18N.getString("param.cfbundle-identifier.description"),
 122                     Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(),
 123                     String.class,
 124                     IDENTIFIER::fetchFrom,
 125                     (s, p) -> s);
 126 
 127     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION =
 128             new StandardBundlerParam<>(
 129                     I18N.getString("param.cfbundle-version.name"),
 130                     I18N.getString("param.cfbundle-version.description"),
 131                     "mac.CFBundleVersion",
 132                     String.class,
 133                     p -> {
 134                         String s = VERSION.fetchFrom(p);
 135                         if (validCFBundleVersion(s)) {
 136                             return s;
 137                         } else {
 138                             return "100";
 139                         }
 140                     },
 141                     (s, p) -> s);
 142 
 143     public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON =
 144             new StandardBundlerParam<>(
 145             I18N.getString("param.default-icon-icns"),
 146             I18N.getString("param.default-icon-icns.description"),
 147             ".mac.default.icns",
 148             String.class,
 149             params -> TEMPLATE_BUNDLE_ICON,
 150             (s, p) -> s);
 151 
 152     public static final BundlerParamInfo<File> ICON_ICNS =
 153             new StandardBundlerParam<>(
 154             I18N.getString("param.icon-icns.name"),
 155             I18N.getString("param.icon-icns.description"),
 156             "icon.icns",
 157             File.class,
 158             params -> {
 159                 File f = ICON.fetchFrom(params);
 160                 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
 161                     Log.error(MessageFormat.format(
 162                             I18N.getString("message.icon-not-icns"), f));
 163                     return null;
 164                 }
 165                 return f;
 166             },
 167             (s, p) -> new File(s));
 168 
 169     public static final StandardBundlerParam<Boolean> SIGN_BUNDLE  =
 170             new StandardBundlerParam<>(
 171             I18N.getString("param.sign-bundle.name"),
 172             I18N.getString("param.sign-bundle.description"),
 173             Arguments.CLIOptions.MAC_SIGN.getId(),
 174             Boolean.class,
 175             params -> false,
 176             // valueOf(null) is false, we actually do want null in some cases
 177             (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
 178                     null : Boolean.valueOf(s)
 179         );
 180 
 181     public MacAppImageBuilder(Map<String, Object> config, Path imageOutDir)
 182             throws IOException {
 183         super(config, imageOutDir.resolve(APP_NAME.fetchFrom(config)
 184                 + ".app/Contents/PlugIns/Java.runtime/Contents/Home"));
 185 
 186         Objects.requireNonNull(imageOutDir);
 187 
 188         this.params = config;
 189         this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
 190         this.contentsDir = root.resolve("Contents");
 191         this.javaDir = contentsDir.resolve("Java");
 192         this.javaModsDir = javaDir.resolve("mods");
 193         this.resourcesDir = contentsDir.resolve("Resources");
 194         this.macOSDir = contentsDir.resolve("MacOS");
 195         this.runtimeDir = contentsDir.resolve("PlugIns/Java.runtime");
 196         this.runtimeRoot = runtimeDir.resolve("Contents/Home");
 197         this.mdir = runtimeRoot.resolve("lib");
 198         Files.createDirectories(javaDir);
 199         Files.createDirectories(resourcesDir);
 200         Files.createDirectories(macOSDir);
 201         Files.createDirectories(runtimeDir);
 202     }
 203 
 204     public MacAppImageBuilder(Map<String, Object> config, String jreName,
 205             Path imageOutDir) throws IOException {
 206         super(null, imageOutDir.resolve(jreName + "/Contents/Home"));
 207 
 208         Objects.requireNonNull(imageOutDir);
 209 
 210         this.params = config;
 211         this.root = imageOutDir.resolve(jreName );
 212         this.contentsDir = root.resolve("Contents");
 213         this.javaDir = null;
 214         this.javaModsDir = null;
 215         this.resourcesDir = null;
 216         this.macOSDir = null;
 217         this.runtimeDir = this.root;
 218         this.runtimeRoot = runtimeDir.resolve("Contents/Home");
 219         this.mdir = runtimeRoot.resolve("lib");
 220 
 221         Files.createDirectories(runtimeDir);
 222     }
 223 
 224     private void writeEntry(InputStream in, Path dstFile) throws IOException {
 225         Files.createDirectories(dstFile.getParent());
 226         Files.copy(in, dstFile);
 227     }
 228 
 229     // chmod ugo+x file
 230     private void setExecutable(Path file) {
 231         try {
 232             Set<PosixFilePermission> perms =
 233                     Files.getPosixFilePermissions(file);
 234             perms.add(PosixFilePermission.OWNER_EXECUTE);
 235             perms.add(PosixFilePermission.GROUP_EXECUTE);
 236             perms.add(PosixFilePermission.OTHERS_EXECUTE);
 237             Files.setPosixFilePermissions(file, perms);
 238         } catch (IOException ioe) {
 239             throw new UncheckedIOException(ioe);
 240         }
 241     }
 242 
 243     private static void createUtf8File(File file, String content)
 244         throws IOException {
 245         try (OutputStream fout = new FileOutputStream(file);
 246              Writer output = new OutputStreamWriter(fout, "UTF-8")) {
 247             output.write(content);
 248         }
 249     }
 250 
 251     public static boolean validCFBundleVersion(String v) {
 252         // CFBundleVersion (String - iOS, OS X) specifies the build version
 253         // number of the bundle, which identifies an iteration (released or
 254         // unreleased) of the bundle. The build version number should be a
 255         // string comprised of three non-negative, period-separated integers
 256         // with the first integer being greater than zero. The string should
 257         // only contain numeric (0-9) and period (.) characters. Leading zeros
 258         // are truncated from each integer and will be ignored (that is,
 259         // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
 260 
 261         if (v == null) {
 262             return false;
 263         }
 264 
 265         String p[] = v.split("\\.");
 266         if (p.length > 3 || p.length < 1) {
 267             Log.verbose(I18N.getString(
 268                     "message.version-string-too-many-components"));
 269             return false;
 270         }
 271 
 272         try {
 273             BigInteger n = new BigInteger(p[0]);
 274             if (BigInteger.ONE.compareTo(n) > 0) {
 275                 Log.verbose(I18N.getString(
 276                         "message.version-string-first-number-not-zero"));
 277                 return false;
 278             }
 279             if (p.length > 1) {
 280                 n = new BigInteger(p[1]);
 281                 if (BigInteger.ZERO.compareTo(n) > 0) {
 282                     Log.verbose(I18N.getString(
 283                             "message.version-string-no-negative-numbers"));
 284                     return false;
 285                 }
 286             }
 287             if (p.length > 2) {
 288                 n = new BigInteger(p[2]);
 289                 if (BigInteger.ZERO.compareTo(n) > 0) {
 290                     Log.verbose(I18N.getString(
 291                             "message.version-string-no-negative-numbers"));
 292                     return false;
 293                 }
 294             }
 295         } catch (NumberFormatException ne) {
 296             Log.verbose(I18N.getString("message.version-string-numbers-only"));
 297             Log.verbose(ne);
 298             return false;
 299         }
 300 
 301         return true;
 302     }
 303 
 304     @Override
 305     public Path getAppDir() {
 306         return javaDir;
 307     }
 308 
 309     @Override
 310     public Path getAppModsDir() {
 311         return javaModsDir;
 312     }
 313 
 314     @Override
 315     public void prepareApplicationFiles() throws IOException {
 316         Map<String, ? super Object> originalParams = new HashMap<>(params);
 317         // Generate PkgInfo
 318         File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo");
 319         pkgInfoFile.createNewFile();
 320         writePkgInfo(pkgInfoFile);
 321 
 322         Path executable = macOSDir.resolve(getLauncherName(params));
 323 
 324         // create the main app launcher
 325         try (InputStream is_launcher =
 326                 getResourceAsStream("jpackageapplauncher");
 327             InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) {
 328             // Copy executable and library to MacOS folder
 329             writeEntry(is_launcher, executable);
 330             writeEntry(is_lib, macOSDir.resolve(LIBRARY_NAME));
 331         }
 332         executable.toFile().setExecutable(true, false);
 333         // generate main app launcher config file
 334         File cfg = new File(root.toFile(), getLauncherCfgName(params));
 335         writeCfgFile(params, cfg, "$APPDIR/PlugIns/Java.runtime");
 336 
 337         // create secondary app launcher(s) and config file(s)
 338         List<Map<String, ? super Object>> entryPoints =
 339                 StandardBundlerParam.SECONDARY_LAUNCHERS.fetchFrom(params);
 340         for (Map<String, ? super Object> entryPoint : entryPoints) {
 341             Map<String, ? super Object> tmp = new HashMap<>(originalParams);
 342             tmp.putAll(entryPoint);
 343 
 344             // add executable for secondary launcher
 345             Path secondaryExecutable = macOSDir.resolve(getLauncherName(tmp));
 346             try (InputStream is = getResourceAsStream("jpackageapplauncher");) {
 347                 writeEntry(is, secondaryExecutable);
 348             }
 349             secondaryExecutable.toFile().setExecutable(true, false);
 350 
 351             // add config file for secondary launcher
 352             cfg = new File(root.toFile(), getLauncherCfgName(tmp));
 353             writeCfgFile(tmp, cfg, "$APPDIR/PlugIns/Java.runtime");
 354         }
 355 
 356         // Copy class path entries to Java folder
 357         copyClassPathEntries(javaDir);
 358 
 359         /*********** Take care of "config" files *******/
 360         File icon = ICON_ICNS.fetchFrom(params);
 361 
 362         InputStream in = locateResource(
 363                 APP_NAME.fetchFrom(params) + ".icns",
 364                 "icon",
 365                 DEFAULT_ICNS_ICON.fetchFrom(params),
 366                 icon,
 367                 VERBOSE.fetchFrom(params),
 368                 RESOURCE_DIR.fetchFrom(params));
 369         Files.copy(in,
 370                 resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns"),
 371                 StandardCopyOption.REPLACE_EXISTING);
 372 
 373         // copy file association icons
 374         for (Map<String, ?
 375                 super Object> fa : FILE_ASSOCIATIONS.fetchFrom(params)) {
 376             File f = FA_ICON.fetchFrom(fa);
 377             if (f != null && f.exists()) {
 378                 try (InputStream in2 = new FileInputStream(f)) {
 379                     Files.copy(in2, resourcesDir.resolve(f.getName()));
 380                 }
 381 
 382             }
 383         }
 384 
 385         copyRuntimeFiles();
 386         sign();
 387     }
 388 
 389     @Override
 390     public void prepareJreFiles() throws IOException {
 391         copyRuntimeFiles();
 392         sign();
 393     }
 394 
 395     private void copyRuntimeFiles() throws IOException {
 396         // Generate Info.plist
 397         writeInfoPlist(contentsDir.resolve("Info.plist").toFile());
 398 
 399         // generate java runtime info.plist
 400         writeRuntimeInfoPlist(
 401                 runtimeDir.resolve("Contents/Info.plist").toFile());
 402 
 403         // copy library
 404         Path runtimeMacOSDir = Files.createDirectories(
 405                 runtimeDir.resolve("Contents/MacOS"));
 406 
 407         // JDK 9, 10, and 11 have extra '/jli/' subdir
 408         Path jli = runtimeRoot.resolve("lib/libjli.dylib");
 409         if (!Files.exists(jli)) {
 410             jli = runtimeRoot.resolve("lib/jli/libjli.dylib");
 411         }
 412 
 413         Files.copy(jli, runtimeMacOSDir.resolve("libjli.dylib"));
 414     }
 415 
 416     private void sign() throws IOException {
 417         if (Optional.ofNullable(
 418                 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) {
 419             try {
 420                 addNewKeychain(params);
 421             } catch (InterruptedException e) {
 422                 Log.error(e.getMessage());
 423             }
 424             String signingIdentity =
 425                     DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params);
 426             if (signingIdentity != null) {
 427                 signAppBundle(params, root, signingIdentity,
 428                         BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null);
 429             }
 430             restoreKeychainList(params);
 431         }
 432     }
 433 
 434     private String getLauncherName(Map<String, ? super Object> params) {
 435         if (APP_NAME.fetchFrom(params) != null) {
 436             return APP_NAME.fetchFrom(params);
 437         } else {
 438             return MAIN_CLASS.fetchFrom(params);
 439         }
 440     }
 441 
 442     public static String getLauncherCfgName(Map<String, ? super Object> p) {
 443         return "Contents/Java/" + APP_NAME.fetchFrom(p) + ".cfg";
 444     }
 445 
 446     private void copyClassPathEntries(Path javaDirectory) throws IOException {
 447         List<RelativeFileSet> resourcesList =
 448                 APP_RESOURCES_LIST.fetchFrom(params);
 449         if (resourcesList == null) {
 450             throw new RuntimeException(
 451                     I18N.getString("message.null-classpath"));
 452         }
 453 
 454         for (RelativeFileSet classPath : resourcesList) {
 455             File srcdir = classPath.getBaseDirectory();
 456             for (String fname : classPath.getIncludedFiles()) {
 457                 copyEntry(javaDirectory, srcdir, fname);
 458             }
 459         }
 460     }
 461 
 462     private String getBundleName(Map<String, ? super Object> params) {
 463         if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
 464             String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
 465             if (bn.length() > 16) {
 466                 Log.error(MessageFormat.format(I18N.getString(
 467                         "message.bundle-name-too-long-warning"),
 468                         MAC_CF_BUNDLE_NAME.getID(), bn));
 469             }
 470             return MAC_CF_BUNDLE_NAME.fetchFrom(params);
 471         } else if (APP_NAME.fetchFrom(params) != null) {
 472             return APP_NAME.fetchFrom(params);
 473         } else {
 474             String nm = MAIN_CLASS.fetchFrom(params);
 475             if (nm.length() > 16) {
 476                 nm = nm.substring(0, 16);
 477             }
 478             return nm;
 479         }
 480     }
 481 
 482     private void writeRuntimeInfoPlist(File file) throws IOException {
 483         Map<String, String> data = new HashMap<>();
 484         String identifier = RUNTIME_INSTALLER.fetchFrom(params) ?
 485                 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) :
 486                 "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params);
 487         data.put("CF_BUNDLE_IDENTIFIER", identifier);
 488         String name = RUNTIME_INSTALLER.fetchFrom(params) ?
 489                 getBundleName(params): "Java Runtime Image";
 490         data.put("CF_BUNDLE_NAME", name);
 491         data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params));
 492         data.put("CF_BUNDLE_SHORT_VERSION_STRING", VERSION.fetchFrom(params));
 493 
 494         Writer w = new BufferedWriter(new FileWriter(file));
 495         w.write(preprocessTextResource("Runtime-Info.plist",
 496                 I18N.getString("resource.runtime-info-plist"),
 497                 TEMPLATE_RUNTIME_INFO_PLIST,
 498                 data,
 499                 VERBOSE.fetchFrom(params),
 500                 RESOURCE_DIR.fetchFrom(params)));
 501         w.close();
 502     }
 503 
 504     private void writeInfoPlist(File file) throws IOException {
 505         Log.verbose(MessageFormat.format(I18N.getString(
 506                 "message.preparing-info-plist"), file.getAbsolutePath()));
 507 
 508         //prepare config for exe
 509         //Note: do not need CFBundleDisplayName if we don't support localization
 510         Map<String, String> data = new HashMap<>();
 511         data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns");
 512         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 513                 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
 514         data.put("DEPLOY_BUNDLE_NAME",
 515                 getBundleName(params));
 516         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 517                 COPYRIGHT.fetchFrom(params) != null ?
 518                 COPYRIGHT.fetchFrom(params) : "Unknown");
 519         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
 520         data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime");
 521         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 522                 VERSION.fetchFrom(params) != null ?
 523                 VERSION.fetchFrom(params) : "1.0.0");
 524         data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
 525                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ?
 526                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
 527         data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params));
 528 
 529         boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null;
 530         boolean hasMainModule =
 531                 StandardBundlerParam.MODULE.fetchFrom(params) != null;
 532 
 533         if (hasMainJar) {
 534             data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).
 535                     getIncludedFiles().iterator().next());
 536         }
 537         else if (hasMainModule) {
 538             data.put("DEPLOY_MODULE_NAME",
 539                     StandardBundlerParam.MODULE.fetchFrom(params));
 540         }
 541 
 542         data.put("DEPLOY_PREFERENCES_ID",
 543                 PREFERENCES_ID.fetchFrom(params).toLowerCase());
 544 
 545         StringBuilder sb = new StringBuilder();
 546         List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
 547 
 548         String newline = ""; //So we don't add extra line after last append
 549         for (String o : jvmOptions) {
 550             sb.append(newline).append(
 551                     "    <string>").append(o).append("</string>");
 552             newline = "\n";
 553         }
 554 
 555         data.put("DEPLOY_JVM_OPTIONS", sb.toString());
 556 
 557         sb = new StringBuilder();
 558         List<String> args = ARGUMENTS.fetchFrom(params);
 559         newline = "";
 560         // So we don't add unneccessary extra line after last append
 561 
 562         for (String o : args) {
 563             sb.append(newline).append("    <string>").append(o).append(
 564                     "</string>");
 565             newline = "\n";
 566         }
 567         data.put("DEPLOY_ARGUMENTS", sb.toString());
 568 
 569         newline = "";
 570 
 571         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 572 
 573         StringBuilder macroedPath = new StringBuilder();
 574         for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) {
 575             macroedPath.append(s);
 576             macroedPath.append(":");
 577         }
 578         macroedPath.deleteCharAt(macroedPath.length() - 1);
 579 
 580         data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString());
 581 
 582         StringBuilder bundleDocumentTypes = new StringBuilder();
 583         StringBuilder exportedTypes = new StringBuilder();
 584         for (Map<String, ? super Object>
 585                 fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
 586 
 587             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 588 
 589             if (extensions == null) {
 590                 Log.verbose(I18N.getString(
 591                         "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)
 596                     + "." + ((extensions == null || extensions.isEmpty())
 597                     ? "mime" : 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")
 616                             // TODO make a bundler arg
 617                     .append("\n")
 618                     .append("      <key>CFBundleTypeRole</key>\n")
 619                     .append("      <string>Editor</string>\n")
 620                             // TODO make a bundler arg
 621                     .append("\n")
 622                     .append("      <key>LSIsAppleDefaultForType</key>\n")
 623                     .append("      <true/>\n")
 624                             // TODO make a bundler arg
 625                     .append("\n");
 626 
 627             if (icon != null && icon.exists()) {
 628                 bundleDocumentTypes
 629                         .append("      <key>CFBundleTypeIconFile</key>\n")
 630                         .append("      <string>")
 631                         .append(icon.getName())
 632                         .append("</string>\n");
 633             }
 634             bundleDocumentTypes.append("    </dict>\n");
 635 
 636             exportedTypes.append("    <dict>\n")
 637                     .append("      <key>UTTypeIdentifier</key>\n")
 638                     .append("      <string>")
 639                     .append(itemContentType)
 640                     .append("</string>\n")
 641                     .append("\n")
 642                     .append("      <key>UTTypeDescription</key>\n")
 643                     .append("      <string>")
 644                     .append(description)
 645                     .append("</string>\n")
 646                     .append("      <key>UTTypeConformsTo</key>\n")
 647                     .append("      <array>\n")
 648                     .append("          <string>public.data</string>\n")
 649                             //TODO expose this?
 650                     .append("      </array>\n")
 651                     .append("\n");
 652 
 653             if (icon != null && icon.exists()) {
 654                 exportedTypes.append("      <key>UTTypeIconFile</key>\n")
 655                         .append("      <string>")
 656                         .append(icon.getName())
 657                         .append("</string>\n")
 658                         .append("\n");
 659             }
 660 
 661             exportedTypes.append("\n")
 662                     .append("      <key>UTTypeTagSpecification</key>\n")
 663                     .append("      <dict>\n")
 664                             // TODO expose via param? .append(
 665                             // "        <key>com.apple.ostype</key>\n");
 666                             // TODO expose via param? .append(
 667                             // "        <string>ABCD</string>\n")
 668                     .append("\n");
 669 
 670             if (extensions != null && !extensions.isEmpty()) {
 671                 exportedTypes.append(
 672                         "        <key>public.filename-extension</key>\n")
 673                         .append("        <array>\n");
 674 
 675                 for (String ext : extensions) {
 676                     exportedTypes.append("          <string>")
 677                             .append(ext)
 678                             .append("</string>\n");
 679                 }
 680                 exportedTypes.append("        </array>\n");
 681             }
 682             if (mimeTypes != null && !mimeTypes.isEmpty()) {
 683                 exportedTypes.append("        <key>public.mime-type</key>\n")
 684                         .append("        <array>\n");
 685 
 686                 for (String mime : mimeTypes) {
 687                     exportedTypes.append("          <string>")
 688                             .append(mime)
 689                             .append("</string>\n");
 690                 }
 691                 exportedTypes.append("        </array>\n");
 692             }
 693             exportedTypes.append("      </dict>\n")
 694                     .append("    </dict>\n");
 695         }
 696         String associationData;
 697         if (bundleDocumentTypes.length() > 0) {
 698             associationData =
 699                     "\n  <key>CFBundleDocumentTypes</key>\n  <array>\n"
 700                     + bundleDocumentTypes.toString()
 701                     + "  </array>\n\n"
 702                     + "  <key>UTExportedTypeDeclarations</key>\n  <array>\n"
 703                     + exportedTypes.toString()
 704                     + "  </array>\n";
 705         } else {
 706             associationData = "";
 707         }
 708         data.put("DEPLOY_FILE_ASSOCIATIONS", associationData);
 709 
 710 
 711         Writer w = new BufferedWriter(new FileWriter(file));
 712         w.write(preprocessTextResource(
 713                 // getConfig_InfoPlist(params).getName(),
 714                 "Info.plist",
 715                 I18N.getString("resource.app-info-plist"),
 716                 TEMPLATE_INFO_PLIST_LITE,
 717                 data, VERBOSE.fetchFrom(params),
 718                 RESOURCE_DIR.fetchFrom(params)));
 719         w.close();
 720     }
 721 
 722     private void writePkgInfo(File file) throws IOException {
 723         //hardcoded as it does not seem we need to change it ever
 724         String signature = "????";
 725 
 726         try (Writer out = new BufferedWriter(new FileWriter(file))) {
 727             out.write(OS_TYPE_CODE + signature);
 728             out.flush();
 729         }
 730     }
 731 
 732     public static void addNewKeychain(Map<String, ? super Object> params)
 733                                     throws IOException, InterruptedException {
 734         if (Platform.getMajorVersion() < 10 ||
 735                 (Platform.getMajorVersion() == 10 &&
 736                 Platform.getMinorVersion() < 12)) {
 737             // we need this for OS X 10.12+
 738             return;
 739         }
 740 
 741         String keyChain = SIGNING_KEYCHAIN.fetchFrom(params);
 742         if (keyChain == null || keyChain.isEmpty()) {
 743             return;
 744         }
 745 
 746         // get current keychain list
 747         String keyChainPath = new File (keyChain).getAbsolutePath().toString();
 748         List<String> keychainList = new ArrayList<>();
 749         int ret = IOUtils.getProcessOutput(
 750                 keychainList, "security", "list-keychains");
 751         if (ret != 0) {
 752             Log.error(I18N.getString("message.keychain.error"));
 753             return;
 754         }
 755 
 756         boolean contains = keychainList.stream().anyMatch(
 757                     str -> str.trim().equals("\""+keyChainPath.trim()+"\""));
 758         if (contains) {
 759             // keychain is already added in the search list
 760             return;
 761         }
 762 
 763         keyChains = new ArrayList<>();
 764         // remove "
 765         keychainList.forEach((String s) -> {
 766             String path = s.trim();
 767             if (path.startsWith("\"") && path.endsWith("\"")) {
 768                 path = path.substring(1, path.length()-1);
 769             }
 770             keyChains.add(path);
 771         });
 772 
 773         List<String> args = new ArrayList<>();
 774         args.add("security");
 775         args.add("list-keychains");
 776         args.add("-s");
 777 
 778         args.addAll(keyChains);
 779         args.add(keyChain);
 780 
 781         ProcessBuilder  pb = new ProcessBuilder(args);
 782         IOUtils.exec(pb, false);
 783     }
 784 
 785     public static void restoreKeychainList(Map<String, ? super Object> params)
 786             throws IOException{
 787         if (Platform.getMajorVersion() < 10 ||
 788                 (Platform.getMajorVersion() == 10 &&
 789                 Platform.getMinorVersion() < 12)) {
 790             // we need this for OS X 10.12+
 791             return;
 792         }
 793 
 794         if (keyChains == null || keyChains.isEmpty()) {
 795             return;
 796         }
 797 
 798         List<String> args = new ArrayList<>();
 799         args.add("security");
 800         args.add("list-keychains");
 801         args.add("-s");
 802 
 803         args.addAll(keyChains);
 804 
 805         ProcessBuilder  pb = new ProcessBuilder(args);
 806         IOUtils.exec(pb, false);
 807     }
 808 
 809     public static void signAppBundle(
 810             Map<String, ? super Object> params, Path appLocation,
 811             String signingIdentity, String identifierPrefix,
 812             String entitlementsFile, String inheritedEntitlements)
 813             throws IOException {
 814         AtomicReference<IOException> toThrow = new AtomicReference<>();
 815         String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params);
 816         String keyChain = SIGNING_KEYCHAIN.fetchFrom(params);
 817 
 818         // sign all dylibs and jars
 819         Files.walk(appLocation)
 820                 // fix permissions
 821                 .peek(path -> {
 822                     try {
 823                         Set<PosixFilePermission> pfp =
 824                             Files.getPosixFilePermissions(path);
 825                         if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) {
 826                             pfp = EnumSet.copyOf(pfp);
 827                             pfp.add(PosixFilePermission.OWNER_WRITE);
 828                             Files.setPosixFilePermissions(path, pfp);
 829                         }
 830                     } catch (IOException e) {
 831                         Log.debug(e);
 832                     }
 833                 })
 834                 .filter(p -> Files.isRegularFile(p) &&
 835                         !(p.toString().contains("/Contents/MacOS/libjli.dylib")
 836                         || p.toString().contains(
 837                                 "/Contents/MacOS/JavaAppletPlugin")
 838                         || p.toString().endsWith(appExecutable))
 839                 ).forEach(p -> {
 840             //noinspection ThrowableResultOfMethodCallIgnored
 841             if (toThrow.get() != null) return;
 842 
 843             // If p is a symlink then skip the signing process.
 844             if (Files.isSymbolicLink(p)) {
 845                 if (VERBOSE.fetchFrom(params)) {
 846                     Log.verbose(MessageFormat.format(I18N.getString(
 847                             "message.ignoring.symlink"), p.toString()));
 848                 }
 849             }
 850             else {
 851                 List<String> args = new ArrayList<>();
 852                 args.addAll(Arrays.asList("codesign",
 853                         "-s", signingIdentity, // sign with this key
 854                         "--prefix", identifierPrefix,
 855                                 // use the identifier as a prefix
 856                         "-vvvv"));
 857                 if (entitlementsFile != null &&
 858                         (p.toString().endsWith(".jar")
 859                                 || p.toString().endsWith(".dylib"))) {
 860                     args.add("--entitlements");
 861                     args.add(entitlementsFile); // entitlements
 862                 } else if (inheritedEntitlements != null &&
 863                         Files.isExecutable(p)) {
 864                     args.add("--entitlements");
 865                     args.add(inheritedEntitlements);
 866                             // inherited entitlements for executable processes
 867                 }
 868                 if (keyChain != null && !keyChain.isEmpty()) {
 869                     args.add("--keychain");
 870                     args.add(keyChain);
 871                 }
 872                 args.add(p.toString());
 873 
 874                 try {
 875                     Set<PosixFilePermission> oldPermissions =
 876                             Files.getPosixFilePermissions(p);
 877                     File f = p.toFile();
 878                     f.setWritable(true, true);
 879 
 880                     ProcessBuilder pb = new ProcessBuilder(args);
 881                     IOUtils.exec(pb, false);
 882 
 883                     Files.setPosixFilePermissions(p, oldPermissions);
 884                 } catch (IOException ioe) {
 885                     toThrow.set(ioe);
 886                 }
 887             }
 888         });
 889 
 890         IOException ioe = toThrow.get();
 891         if (ioe != null) {
 892             throw ioe;
 893         }
 894 
 895         // sign all plugins and frameworks
 896         Consumer<? super Path> signIdentifiedByPList = path -> {
 897             //noinspection ThrowableResultOfMethodCallIgnored
 898             if (toThrow.get() != null) return;
 899 
 900             try {
 901                 List<String> args = new ArrayList<>();
 902                 args.addAll(Arrays.asList("codesign",
 903                         "-s", signingIdentity, // sign with this key
 904                         "--prefix", identifierPrefix,
 905                                 // use the identifier as a prefix
 906                         "-vvvv"));
 907                 if (keyChain != null && !keyChain.isEmpty()) {
 908                     args.add("--keychain");
 909                     args.add(keyChain);
 910                 }
 911                 args.add(path.toString());
 912                 ProcessBuilder pb = new ProcessBuilder(args);
 913                 IOUtils.exec(pb, false);
 914 
 915                 args = new ArrayList<>();
 916                 args.addAll(Arrays.asList("codesign",
 917                         "-s", signingIdentity, // sign with this key
 918                         "--prefix", identifierPrefix,
 919                                 // use the identifier as a prefix
 920                         "-vvvv"));
 921                 if (keyChain != null && !keyChain.isEmpty()) {
 922                     args.add("--keychain");
 923                     args.add(keyChain);
 924                 }
 925                 args.add(path.toString()
 926                         + "/Contents/_CodeSignature/CodeResources");
 927                 pb = new ProcessBuilder(args);
 928                 IOUtils.exec(pb, false);
 929             } catch (IOException e) {
 930                 toThrow.set(e);
 931             }
 932         };
 933 
 934         Path pluginsPath = appLocation.resolve("Contents/PlugIns");
 935         if (Files.isDirectory(pluginsPath)) {
 936             Files.list(pluginsPath)
 937                     .forEach(signIdentifiedByPList);
 938 
 939             ioe = toThrow.get();
 940             if (ioe != null) {
 941                 throw ioe;
 942             }
 943         }
 944         Path frameworkPath = appLocation.resolve("Contents/Frameworks");
 945         if (Files.isDirectory(frameworkPath)) {
 946             Files.list(frameworkPath)
 947                     .forEach(signIdentifiedByPList);
 948 
 949             ioe = toThrow.get();
 950             if (ioe != null) {
 951                 throw ioe;
 952             }
 953         }
 954 
 955         // sign the app itself
 956         List<String> args = new ArrayList<>();
 957         args.addAll(Arrays.asList("codesign",
 958                 "-s", signingIdentity, // sign with this key
 959                 "-vvvv")); // super verbose output
 960         if (entitlementsFile != null) {
 961             args.add("--entitlements");
 962             args.add(entitlementsFile); // entitlements
 963         }
 964         if (keyChain != null && !keyChain.isEmpty()) {
 965             args.add("--keychain");
 966             args.add(keyChain);
 967         }
 968         args.add(appLocation.toString());
 969 
 970         ProcessBuilder pb =
 971                 new ProcessBuilder(args.toArray(new String[args.size()]));
 972         IOUtils.exec(pb, false);
 973     }
 974 
 975 }