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