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