1 /*
   2  * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.jpackage.internal;
  27 
  28 import java.io.ByteArrayOutputStream;
  29 import java.io.File;
  30 import java.io.IOException;
  31 import java.io.PrintStream;
  32 import java.nio.file.Files;
  33 import java.text.MessageFormat;
  34 import java.util.ArrayList;
  35 import java.util.Arrays;
  36 import java.util.Collection;
  37 import java.util.LinkedHashSet;
  38 import java.util.List;
  39 import java.util.Map;
  40 import java.util.ResourceBundle;
  41 import java.util.regex.Matcher;
  42 import java.util.regex.Pattern;
  43 
  44 import static jdk.jpackage.internal.StandardBundlerParam.*;
  45 
  46 public abstract class MacBaseInstallerBundler extends AbstractBundler {
  47 
  48     private static final ResourceBundle I18N = ResourceBundle.getBundle(
  49             "jdk.jpackage.internal.resources.MacResources");
  50 
  51     // This could be generalized more to be for any type of Image Bundler
  52     public static final BundlerParamInfo<MacAppBundler> APP_BUNDLER =
  53             new StandardBundlerParam<>(
  54             I18N.getString("param.app-bundler.name"),
  55             I18N.getString("param.app-bundle.description"),
  56             "mac.app.bundler",
  57             MacAppBundler.class,
  58             params -> new MacAppBundler(),
  59             (s, p) -> null);
  60 
  61     public final BundlerParamInfo<File> APP_IMAGE_BUILD_ROOT =
  62             new StandardBundlerParam<>(
  63             I18N.getString("param.app-image-build-root.name"),
  64             I18N.getString("param.app-image-build-root.description"),
  65             "mac.app.imageRoot",
  66             File.class,
  67             params -> {
  68                 File imageDir = IMAGES_ROOT.fetchFrom(params);
  69                 if (!imageDir.exists()) imageDir.mkdirs();
  70                 try {
  71                     return Files.createTempDirectory(
  72                             imageDir.toPath(), "image-").toFile();
  73                 } catch (IOException e) {
  74                     return new File(imageDir, getID()+ ".image");
  75                 }
  76             },
  77             (s, p) -> new File(s));
  78 
  79     public static final BundlerParamInfo<String> SIGNING_KEY_USER =
  80             new StandardBundlerParam<>(
  81             I18N.getString("param.signing-key-name.name"),
  82             I18N.getString("param.signing-key-name.description"),
  83             Arguments.CLIOptions.MAC_SIGNING_KEY_NAME.getId(),
  84             String.class,
  85             params -> "",
  86             null);
  87 
  88     public static final BundlerParamInfo<String> SIGNING_KEYCHAIN =
  89             new StandardBundlerParam<>(
  90             I18N.getString("param.signing-keychain.name"),
  91             I18N.getString("param.signing-keychain.description"),
  92             Arguments.CLIOptions.MAC_SIGNING_KEYCHAIN.getId(),
  93             String.class,
  94             params -> "",
  95             null);
  96 
  97     public static final BundlerParamInfo<String> INSTALLER_NAME =
  98             new StandardBundlerParam<> (
  99             I18N.getString("param.installer-name.name"),
 100             I18N.getString("param.installer-name.description"),
 101             "mac.installerName",
 102             String.class,
 103             params -> {
 104                 String nm = APP_NAME.fetchFrom(params);
 105                 if (nm == null) return null;
 106 
 107                 String version = VERSION.fetchFrom(params);
 108                 if (version == null) {
 109                     return nm;
 110                 } else {
 111                     return nm + "-" + version;
 112                 }
 113             },
 114             (s, p) -> s);
 115 
 116     protected void validateAppImageAndBundeler(
 117             Map<String, ? super Object> params)
 118             throws ConfigException, UnsupportedPlatformException {
 119         if (PREDEFINED_APP_IMAGE.fetchFrom(params) != null) {
 120             File applicationImage = PREDEFINED_APP_IMAGE.fetchFrom(params);
 121             if (!applicationImage.exists()) {
 122                 throw new ConfigException(
 123                         MessageFormat.format(I18N.getString(
 124                                 "message.app-image-dir-does-not-exist"),
 125                                 PREDEFINED_APP_IMAGE.getID(),
 126                                 applicationImage.toString()),
 127                         MessageFormat.format(I18N.getString(
 128                                 "message.app-image-dir-does-not-exist.advice"),
 129                                 PREDEFINED_APP_IMAGE.getID()));
 130             }
 131             if (APP_NAME.fetchFrom(params) == null) {
 132                 throw new ConfigException(
 133                         I18N.getString("message.app-image-requires-app-name"),
 134                         I18N.getString(
 135                             "message.app-image-requires-app-name.advice"));
 136             }
 137             if (IDENTIFIER.fetchFrom(params) == null) {
 138                 throw new ConfigException(
 139                         I18N.getString("message.app-image-requires-identifier"),
 140                         I18N.getString(
 141                             "message.app-image-requires-identifier.advice"));
 142             }
 143         } else {
 144             APP_BUNDLER.fetchFrom(params).validate(params);
 145         }
 146     }
 147 
 148     protected File prepareAppBundle(
 149             Map<String, ? super Object> p, boolean pkg) {
 150         File predefinedImage = StandardBundlerParam.getPredefinedAppImage(p);
 151         if (predefinedImage != null) {
 152             return predefinedImage;
 153         }
 154         File appImageRoot = APP_IMAGE_BUILD_ROOT.fetchFrom(p);
 155         if (pkg) {
 156             // create pkg in dmg
 157             return new MacPkgBundler().bundle(p, appImageRoot);
 158         } else {
 159             return APP_BUNDLER.fetchFrom(p).doBundle(p, appImageRoot, true);
 160         }
 161     }
 162 
 163     @Override
 164     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 165         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 166 
 167         results.addAll(MacAppBundler.getAppBundleParameters());
 168         results.addAll(Arrays.asList(
 169                 APP_BUNDLER,
 170                 CONFIG_ROOT,
 171                 APP_IMAGE_BUILD_ROOT,
 172                 PREDEFINED_APP_IMAGE
 173         ));
 174 
 175         return results;
 176     }
 177 
 178     @Override
 179     public String getBundleType() {
 180         return "INSTALLER";
 181     }
 182 
 183     public static String findKey(String key, String keychainName,
 184             boolean verbose) {
 185         if (Platform.getPlatform() != Platform.MAC) {
 186             return null;
 187         }
 188 
 189         try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
 190                 PrintStream ps = new PrintStream(baos)) {
 191             List<String> searchOptions = new ArrayList<>();
 192             searchOptions.add("security");
 193             searchOptions.add("find-certificate");
 194             searchOptions.add("-c");
 195             searchOptions.add(key);
 196             searchOptions.add("-a");
 197             if (keychainName != null && !keychainName.isEmpty()) {
 198                 searchOptions.add(keychainName);
 199             }
 200 
 201             ProcessBuilder pb = new ProcessBuilder(searchOptions);
 202 
 203             IOUtils.exec(pb, verbose, false, ps);
 204             Pattern p = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"");
 205             Matcher m = p.matcher(baos.toString());
 206             if (!m.find()) {
 207                 Log.error("Did not find a key matching '" + key + "'");
 208                 return null;
 209             }
 210             String matchedKey = m.group(1);
 211             if (m.find()) {
 212                 Log.error("Found more than one key matching '"  + key + "'");
 213                 return null;
 214             }
 215             Log.debug("Using key '" + matchedKey + "'");
 216             return matchedKey;
 217         } catch (IOException ioe) {
 218             Log.verbose(ioe);
 219             return null;
 220         }
 221     }
 222 }