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