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