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