1 /*
   2  * Copyright (c) 2014, 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.mac;
  27 
  28 import jdk.jpackager.internal.AbstractBundler;
  29 import jdk.jpackager.internal.BundlerParamInfo;
  30 import jdk.jpackager.internal.StandardBundlerParam;
  31 import jdk.jpackager.internal.Arguments;
  32 import jdk.jpackager.internal.Log;
  33 import jdk.jpackager.internal.ConfigException;
  34 import jdk.jpackager.internal.IOUtils;
  35 import jdk.jpackager.internal.Platform;
  36 import jdk.jpackager.internal.UnsupportedPlatformException;
  37 
  38 import java.io.ByteArrayOutputStream;
  39 import java.io.File;
  40 import java.io.IOException;
  41 import java.io.PrintStream;
  42 import java.nio.file.Files;
  43 import java.text.MessageFormat;
  44 import java.util.ArrayList;
  45 import java.util.Arrays;
  46 import java.util.Collection;
  47 import java.util.LinkedHashSet;
  48 import java.util.List;
  49 import java.util.Map;
  50 import java.util.ResourceBundle;
  51 import java.util.regex.Matcher;
  52 import java.util.regex.Pattern;
  53 
  54 import static jdk.jpackager.internal.StandardBundlerParam.*;
  55 
  56 public abstract class MacBaseInstallerBundler extends AbstractBundler {
  57 
  58     private static final ResourceBundle I18N =
  59             ResourceBundle.getBundle(
  60                 "jdk.jpackager.internal.resources.mac.MacBaseInstallerBundler");
  61 
  62     // This could be generalized more to be for any type of Image Bundler
  63     public static final BundlerParamInfo<MacAppBundler> APP_BUNDLER =
  64             new StandardBundlerParam<>(
  65             I18N.getString("param.app-bundler.name"),
  66             I18N.getString("param.app-bundle.description"),
  67             "mac.app.bundler",
  68             MacAppBundler.class,
  69             params -> new MacAppBundler(),
  70             (s, p) -> null);
  71 
  72     public final BundlerParamInfo<File> APP_IMAGE_BUILD_ROOT =
  73             new StandardBundlerParam<>(
  74             I18N.getString("param.app-image-build-root.name"),
  75             I18N.getString("param.app-image-build-root.description"),
  76             "mac.app.imageRoot",
  77             File.class,
  78             params -> {
  79                 File imageDir = IMAGES_ROOT.fetchFrom(params);
  80                 if (!imageDir.exists()) imageDir.mkdirs();
  81                 try {
  82                     return Files.createTempDirectory(
  83                             imageDir.toPath(), "image-").toFile();
  84                 } catch (IOException e) {
  85                     return new File(imageDir, getID()+ ".image");
  86                 }
  87             },
  88             (s, p) -> new File(s));
  89 
  90     public static final BundlerParamInfo<File> CONFIG_ROOT =
  91             new StandardBundlerParam<>(
  92             I18N.getString("param.config-root.name"),
  93             I18N.getString("param.config-root.description"),
  94             "configRoot",
  95             File.class,
  96             params -> {
  97                 File imagesRoot =
  98                         new File(BUILD_ROOT.fetchFrom(params), "macosx");
  99                 imagesRoot.mkdirs();
 100                 return imagesRoot;
 101             },
 102             (s, p) -> null);
 103 
 104     public static final BundlerParamInfo<String> SIGNING_KEY_USER =
 105             new StandardBundlerParam<>(
 106             I18N.getString("param.signing-key-name.name"),
 107             I18N.getString("param.signing-key-name.description"),
 108             Arguments.CLIOptions.MAC_SIGNING_KEY_NAME.getId(),
 109             String.class,
 110             params -> "",
 111             null);
 112 
 113     public static final BundlerParamInfo<String> SIGNING_KEYCHAIN =
 114             new StandardBundlerParam<>(
 115             I18N.getString("param.signing-keychain.name"),
 116             I18N.getString("param.signing-keychain.description"),
 117             Arguments.CLIOptions.MAC_SIGNING_KEYCHAIN.getId(),
 118             String.class,
 119             params -> "",
 120             null);
 121 
 122     public static final BundlerParamInfo<String> INSTALLER_NAME =
 123             new StandardBundlerParam<> (
 124             I18N.getString("param.installer-name.name"),
 125             I18N.getString("param.installer-name.description"),
 126             "mac.installerName",
 127             String.class,
 128             params -> {
 129                 String nm = APP_NAME.fetchFrom(params);
 130                 if (nm == null) return null;
 131 
 132                 String version = VERSION.fetchFrom(params);
 133                 if (version == null) {
 134                     return nm;
 135                 } else {
 136                     return nm + "-" + version;
 137                 }
 138             },
 139             (s, p) -> s);
 140 
 141     protected void validateAppImageAndBundeler(
 142             Map<String, ? super Object> params)
 143             throws ConfigException, UnsupportedPlatformException {
 144         if (PREDEFINED_APP_IMAGE.fetchFrom(params) != null) {
 145             File applicationImage = PREDEFINED_APP_IMAGE.fetchFrom(params);
 146             if (!applicationImage.exists()) {
 147                 throw new ConfigException(
 148                         MessageFormat.format(I18N.getString(
 149                                 "message.app-image-dir-does-not-exist"),
 150                                 PREDEFINED_APP_IMAGE.getID(),
 151                                 applicationImage.toString()),
 152                         MessageFormat.format(I18N.getString(
 153                                 "message.app-image-dir-does-not-exist.advice"),
 154                                 PREDEFINED_APP_IMAGE.getID()));
 155             }
 156             if (APP_NAME.fetchFrom(params) == null) {
 157                 throw new ConfigException(
 158                         I18N.getString("message.app-image-requires-app-name"),
 159                         I18N.getString(
 160                             "message.app-image-requires-app-name.advice"));
 161             }
 162             if (IDENTIFIER.fetchFrom(params) == null) {
 163                 throw new ConfigException(
 164                         I18N.getString("message.app-image-requires-identifier"),
 165                         I18N.getString(
 166                             "message.app-image-requires-identifier.advice"));
 167             }
 168         } else {
 169             APP_BUNDLER.fetchFrom(params).doValidate(params);
 170         }
 171     }
 172 
 173     protected File prepareAppBundle(
 174             Map<String, ? super Object> p, boolean pkg) {
 175         File predefinedImage = StandardBundlerParam.getPredefinedAppImage(p);
 176         if (predefinedImage != null) {
 177             return predefinedImage;
 178         }
 179         File appImageRoot = APP_IMAGE_BUILD_ROOT.fetchFrom(p);
 180         if (pkg) {
 181             // create pkg in dmg
 182             return new MacPkgBundler().bundle(p, appImageRoot);
 183         } else {
 184             return APP_BUNDLER.fetchFrom(p).doBundle(p, appImageRoot, true);
 185         }
 186     }
 187 
 188     @Override
 189     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 190         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 191 
 192         results.addAll(MacAppBundler.getAppBundleParameters());
 193         results.addAll(Arrays.asList(
 194                 APP_BUNDLER,
 195                 CONFIG_ROOT,
 196                 APP_IMAGE_BUILD_ROOT,
 197                 PREDEFINED_APP_IMAGE
 198         ));
 199 
 200         return results;
 201     }
 202 
 203     @Override
 204     public String getBundleType() {
 205         return "INSTALLER";
 206     }
 207 
 208     public static String findKey(String key, String keychainName,
 209             boolean verbose) {
 210         if (Platform.getPlatform() != Platform.MAC) {
 211             return null;
 212         }
 213 
 214         try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
 215                 PrintStream ps = new PrintStream(baos)) {
 216             List<String> searchOptions = new ArrayList<>();
 217             searchOptions.add("security");
 218             searchOptions.add("find-certificate");
 219             searchOptions.add("-c");
 220             searchOptions.add(key);
 221             searchOptions.add("-a");
 222             if (keychainName != null && !keychainName.isEmpty()) {
 223                 searchOptions.add(keychainName);
 224             }
 225 
 226             ProcessBuilder pb = new ProcessBuilder(searchOptions);
 227 
 228             IOUtils.exec(pb, verbose, false, ps);
 229             Pattern p = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"");
 230             Matcher m = p.matcher(baos.toString());
 231             if (!m.find()) {
 232                 Log.error("Did not find a key matching '" + key + "'");
 233                 return null;
 234             }
 235             String matchedKey = m.group(1);
 236             if (m.find()) {
 237                 Log.error("Found more than one key matching '"  + key + "'");
 238                 return null;
 239             }
 240             Log.debug("Using key '" + matchedKey + "'");
 241             return matchedKey;
 242         } catch (IOException ioe) {
 243             Log.verbose(ioe);
 244             return null;
 245         }
 246     }
 247 }