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 com.oracle.tools.packager.mac;
  27 
  28 import com.oracle.tools.packager.BundlerParamInfo;
  29 import com.oracle.tools.packager.StandardBundlerParam;
  30 import com.oracle.tools.packager.Log;
  31 import com.oracle.tools.packager.ConfigException;
  32 import com.oracle.tools.packager.IOUtils;
  33 import com.oracle.tools.packager.Platform;
  34 import com.oracle.tools.packager.RelativeFileSet;
  35 import com.oracle.tools.packager.UnsupportedPlatformException;
  36 
  37 import java.io.BufferedWriter;
  38 import java.io.File;
  39 import java.io.FileNotFoundException;
  40 import java.io.FileWriter;
  41 import java.io.IOException;
  42 import java.io.PrintStream;
  43 import java.io.Writer;
  44 import java.net.URLEncoder;
  45 import java.text.MessageFormat;
  46 import java.util.ArrayList;
  47 import java.util.Arrays;
  48 import java.util.Collection;
  49 import java.util.HashMap;
  50 import java.util.LinkedHashSet;
  51 import java.util.List;
  52 import java.util.Map;
  53 import java.util.Optional;
  54 import java.util.ResourceBundle;
  55 
  56 import static com.oracle.tools.packager.StandardBundlerParam.*;
  57 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN;
  58 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEY_USER;
  59 import jdk.packager.internal.legacy.mac.MacCertificate;
  60 
  61 public class MacPkgBundler extends MacBaseInstallerBundler {
  62 
  63     private static final ResourceBundle I18N =
  64             ResourceBundle.getBundle(MacPkgBundler.class.getName());
  65 
  66     public final static String MAC_BUNDLER_PREFIX =
  67             BUNDLER_PREFIX + "macosx" + File.separator;
  68 
  69     private static final String DEFAULT_BACKGROUND_IMAGE = "background_pkg.png";
  70 
  71     private static final String TEMPLATE_PREINSTALL_SCRIPT = "preinstall.template";
  72     private static final String TEMPLATE_POSTINSTALL_SCRIPT = "postinstall.template";
  73 
  74     private static final BundlerParamInfo<File> PACKAGES_ROOT = new StandardBundlerParam<>(
  75             I18N.getString("param.packages-root.name"),
  76             I18N.getString("param.packages-root.description"),
  77             "mac.pkg.packagesRoot",
  78             File.class,
  79             params -> {
  80                 File packagesRoot = new File(BUILD_ROOT.fetchFrom(params), "packages");
  81                 packagesRoot.mkdirs();
  82                 return packagesRoot;
  83             },
  84             (s, p) -> new File(s));
  85 
  86 
  87     protected final BundlerParamInfo<File> SCRIPTS_DIR = new StandardBundlerParam<>(
  88             I18N.getString("param.scripts-dir.name"),
  89             I18N.getString("param.scripts-dir.description"),
  90             "mac.pkg.scriptsDir",
  91             File.class,
  92             params -> {
  93                 File scriptsDir = new File(CONFIG_ROOT.fetchFrom(params), "scripts");
  94                 scriptsDir.mkdirs();
  95                 return scriptsDir;
  96             },
  97             (s, p) -> new File(s));
  98 
  99     public static final BundlerParamInfo<String> DEVELOPER_ID_INSTALLER_SIGNING_KEY = new StandardBundlerParam<>(
 100             I18N.getString("param.signing-key-developer-id-installer.name"),
 101             I18N.getString("param.signing-key-developer-id-installer.description"),
 102             "mac.signing-key-developer-id-installer",
 103             String.class,
 104             params -> {
 105                     String result = MacBaseInstallerBundler.findKey("Developer ID Installer: " + SIGNING_KEY_USER.fetchFrom(params),
 106                                                                     SIGNING_KEYCHAIN.fetchFrom(params),
 107                                                                     VERBOSE.fetchFrom(params));
 108                     if (result != null) {
 109                         MacCertificate certificate = new MacCertificate(result, VERBOSE.fetchFrom(params));
 110 
 111                         if (!certificate.isValid()) {
 112                             Log.info(MessageFormat.format(I18N.getString("error.certificate.expired"), result));
 113                         }
 114                     }
 115 
 116                     return result;
 117                 },
 118             (s, p) -> s);
 119 
 120     public static final BundlerParamInfo<String> INSTALLER_SUFFIX = new StandardBundlerParam<> (
 121             I18N.getString("param.installer-suffix.name"),
 122             I18N.getString("param.installer-suffix.description"),
 123             "mac.pkg.installerName.suffix",
 124             String.class,
 125             params -> "",
 126             (s, p) -> s);
 127 
 128     public MacPkgBundler() {
 129         super();
 130         baseResourceLoader = MacResources.class;
 131     }
 132 
 133     //@Override
 134     public File bundle(Map<String, ? super Object> params, File outdir) {
 135         Log.info(MessageFormat.format(I18N.getString("message.building-pkg"), APP_NAME.fetchFrom(params)));
 136         if (!outdir.isDirectory() && !outdir.mkdirs()) {
 137             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outdir.getAbsolutePath()));
 138         }
 139         if (!outdir.canWrite()) {
 140             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outdir.getAbsolutePath()));
 141         }
 142 
 143         File appImageDir = null;
 144         try {
 145             appImageDir = prepareAppBundle(params);
 146 
 147             if (appImageDir != null && prepareConfigFiles(params)) {
 148                 if (SERVICE_HINT.fetchFrom(params)) {
 149                     File daemonImageDir = DAEMON_IMAGE_BUILD_ROOT.fetchFrom(params);
 150                     daemonImageDir.mkdirs();
 151                     prepareDaemonBundle(params);
 152                 }
 153 
 154                 File configScript = getConfig_Script(params);
 155                 if (configScript.exists()) {
 156                     Log.info(MessageFormat.format(I18N.getString("message.running-script"), configScript.getAbsolutePath()));
 157                     IOUtils.run("bash", configScript, VERBOSE.fetchFrom(params));
 158                 }
 159 
 160                 return createPKG(params, outdir, appImageDir);
 161             }
 162             return null;
 163         } catch (IOException ex) {
 164             Log.verbose(ex);
 165             return null;
 166         } finally {
 167             try {
 168                 if (appImageDir != null && !Log.isDebug()) {
 169                     IOUtils.deleteRecursive(appImageDir);
 170                 } else if (appImageDir != null) {
 171                     Log.info(MessageFormat.format(I18N.getString("message.intermediate-image-location"), appImageDir.getAbsolutePath()));
 172                 }
 173                 if (!VERBOSE.fetchFrom(params)) {
 174                     //cleanup
 175                     cleanupConfigFiles(params);
 176                 } else {
 177                     Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(params).getAbsolutePath()));
 178                 }
 179             } catch (IOException ex) {
 180                 Log.debug(ex);
 181                 //noinspection ReturnInsideFinallyBlock
 182                 return null;
 183             }
 184         }
 185     }
 186 
 187     private File getPackages_AppPackage(Map<String, ? super Object> params) {
 188         return new File(PACKAGES_ROOT.fetchFrom(params), APP_FS_NAME.fetchFrom(params) + "-app.pkg");
 189     }
 190 
 191     private File getPackages_DaemonPackage(Map<String, ? super Object> params) {
 192         return new File(PACKAGES_ROOT.fetchFrom(params), APP_FS_NAME.fetchFrom(params) + "-daemon.pkg");
 193     }
 194 
 195     private void cleanupPackagesFiles(Map<String, ? super Object> params) {
 196         if (getPackages_AppPackage(params) != null) {
 197             getPackages_AppPackage(params).delete();
 198         }
 199         if (getPackages_DaemonPackage(params) != null) {
 200             getPackages_DaemonPackage(params).delete();
 201         }
 202     }
 203 
 204     private File getConfig_DistributionXMLFile(Map<String, ? super Object> params) {
 205         return new File(CONFIG_ROOT.fetchFrom(params), "distribution.dist");
 206     }
 207 
 208     private File getConfig_BackgroundImage(Map<String, ? super Object> params) {
 209         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-background.png");
 210     }
 211 
 212     private File getScripts_PreinstallFile(Map<String, ? super Object> params) {
 213         return new File(SCRIPTS_DIR.fetchFrom(params), "preinstall");
 214     }
 215 
 216     private File getScripts_PostinstallFile(Map<String, ? super Object> params) {
 217         return new File(SCRIPTS_DIR.fetchFrom(params), "postinstall");
 218     }
 219 
 220     private void cleanupPackageScripts(Map<String, ? super Object> params) {
 221         if (getScripts_PreinstallFile(params) != null) {
 222             getScripts_PreinstallFile(params).delete();
 223         }
 224         if (getScripts_PostinstallFile(params) != null) {
 225             getScripts_PostinstallFile(params).delete();
 226         }
 227     }
 228 
 229     private void cleanupConfigFiles(Map<String, ? super Object> params) {
 230         if (getConfig_DistributionXMLFile(params) != null) {
 231             getConfig_DistributionXMLFile(params).delete();
 232         }
 233         if (getConfig_BackgroundImage(params) != null) {
 234             getConfig_BackgroundImage(params).delete();
 235         }
 236     }
 237 
 238     private String getAppIdentifier(Map<String, ? super Object> params) {
 239         return IDENTIFIER.fetchFrom(params);
 240     }
 241 
 242     private String getDaemonIdentifier(Map<String, ? super Object> params) {
 243         return IDENTIFIER.fetchFrom(params) + ".daemon";
 244     }
 245 
 246     private void preparePackageScripts(Map<String, ? super Object> params) throws IOException
 247     {
 248         Log.verbose(I18N.getString("message.preparing-scripts"));
 249 
 250         Map<String, String> data = new HashMap<>();
 251 
 252         data.put("DEPLOY_DAEMON_IDENTIFIER", getDaemonIdentifier(params));
 253         data.put("DEPLOY_LAUNCHD_PLIST_FILE",
 254                 IDENTIFIER.fetchFrom(params).toLowerCase() + ".launchd.plist");
 255 
 256         Writer w = new BufferedWriter(new FileWriter(getScripts_PreinstallFile(params)));
 257         String content = preprocessTextResource(
 258                 MAC_BUNDLER_PREFIX + getScripts_PreinstallFile(params).getName(),
 259                 I18N.getString("resource.pkg-preinstall-script"),
 260                 TEMPLATE_PREINSTALL_SCRIPT,
 261                 data,
 262                 VERBOSE.fetchFrom(params),
 263                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 264         w.write(content);
 265         w.close();
 266         getScripts_PreinstallFile(params).setExecutable(true, false);
 267 
 268         w = new BufferedWriter(new FileWriter(getScripts_PostinstallFile(params)));
 269         content = preprocessTextResource(
 270                 MAC_BUNDLER_PREFIX + getScripts_PostinstallFile(params).getName(),
 271                 I18N.getString("resource.pkg-postinstall-script"),
 272                 TEMPLATE_POSTINSTALL_SCRIPT,
 273                 data,
 274                 VERBOSE.fetchFrom(params),
 275                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 276         w.write(content);
 277         w.close();
 278         getScripts_PostinstallFile(params).setExecutable(true, false);
 279     }
 280 
 281     private void prepareDistributionXMLFile(Map<String, ? super Object> params)
 282             throws IOException
 283     {
 284         File f = getConfig_DistributionXMLFile(params);
 285 
 286         Log.verbose(MessageFormat.format(I18N.getString("message.preparing-distribution-dist"), f.getAbsolutePath()));
 287 
 288         PrintStream out = new PrintStream(f);
 289 
 290         out.println("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>");
 291         out.println("<installer-gui-script minSpecVersion=\"1\">");
 292 
 293         out.println("<title>" + APP_NAME.fetchFrom(params) + "</title>");
 294         out.println("<background" +
 295                 " file=\"" + getConfig_BackgroundImage(params).getName() + "\"" +
 296                 " mime-type=\"image/png\"" +
 297                 " alignment=\"bottomleft\" " +
 298                 " scaling=\"none\""+
 299                 "/>");
 300 
 301         if (!LICENSE_FILE.fetchFrom(params).isEmpty()) {
 302             File licFile = null;
 303 
 304             List<String> licFiles = LICENSE_FILE.fetchFrom(params);
 305             if (licFiles.isEmpty()) {
 306                 return;
 307             }
 308             String licFileStr = licFiles.get(0);
 309 
 310             for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) {
 311                 if (rfs.contains(licFileStr)) {
 312                     licFile = new File(rfs.getBaseDirectory(), licFileStr);
 313                     break;
 314                 }
 315             }
 316 
 317             // this is NPE protection, validate should have caught it's absence
 318             // so we don't complain or throw an error
 319             if (licFile != null) {
 320                 out.println("<license" +
 321                         " file=\"" + licFile.getAbsolutePath() + "\"" +
 322                         " mime-type=\"text/rtf\"" +
 323                         "/>");
 324             }
 325         }
 326 
 327         /*
 328          * Note that the content of the distribution file
 329          * below is generated by productbuild --synthesize
 330          */
 331 
 332         String appId = getAppIdentifier(params);
 333         String daemonId = getDaemonIdentifier(params);
 334 
 335         out.println("<pkg-ref id=\"" + appId + "\"/>");
 336         if (SERVICE_HINT.fetchFrom(params)) {
 337             out.println("<pkg-ref id=\"" + daemonId + "\"/>");
 338         }
 339 
 340         out.println("<options customize=\"never\" require-scripts=\"false\"/>");
 341         out.println("<choices-outline>");
 342         out.println("    <line choice=\"default\">");
 343         out.println("        <line choice=\"" + appId + "\"/>");
 344         if (SERVICE_HINT.fetchFrom(params)) {
 345             out.println("        <line choice=\"" + daemonId + "\"/>");
 346         }
 347         out.println("    </line>");
 348         out.println("</choices-outline>");
 349         out.println("<choice id=\"default\"/>");
 350         out.println("<choice id=\"" + appId + "\" visible=\"false\">");
 351         out.println("    <pkg-ref id=\"" + appId + "\"/>");
 352         out.println("</choice>");
 353         out.println("<pkg-ref id=\"" + appId + "\" version=\"" + VERSION.fetchFrom(params) +
 354                 "\" onConclusion=\"none\">" +
 355                         URLEncoder.encode(getPackages_AppPackage(params).getName(), "UTF-8") + "</pkg-ref>");
 356 
 357         if (SERVICE_HINT.fetchFrom(params)) {
 358             out.println("<choice id=\"" + daemonId + "\" visible=\"false\">");
 359             out.println("    <pkg-ref id=\"" + daemonId + "\"/>");
 360             out.println("</choice>");
 361             out.println("<pkg-ref id=\"" + daemonId + "\" version=\"" + VERSION.fetchFrom(params) +
 362                     "\" onConclusion=\"none\">" +
 363                     URLEncoder.encode(getPackages_DaemonPackage(params).getName(), "UTF-8") + "</pkg-ref>");
 364         }
 365 
 366         out.println("</installer-gui-script>");
 367 
 368         out.close();
 369     }
 370 
 371     private boolean prepareConfigFiles(Map<String, ? super Object> params) throws IOException {
 372         File imageTarget = getConfig_BackgroundImage(params);
 373         fetchResource(MacAppBundler.MAC_BUNDLER_PREFIX + imageTarget.getName(),
 374                 I18N.getString("resource.pkg-background-image"),
 375                 DEFAULT_BACKGROUND_IMAGE,
 376                 imageTarget,
 377                 VERBOSE.fetchFrom(params),
 378                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 379 
 380         prepareDistributionXMLFile(params);
 381 
 382         fetchResource(MacAppBundler.MAC_BUNDLER_PREFIX + getConfig_Script(params).getName(),
 383                 I18N.getString("resource.post-install-script"),
 384                 (String) null,
 385                 getConfig_Script(params),
 386                 VERBOSE.fetchFrom(params),
 387                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 388 
 389         return true;
 390     }
 391 
 392     //name of post-image script
 393     private File getConfig_Script(Map<String, ? super Object> params) {
 394         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-post-image.sh");
 395     }
 396 
 397     private File createPKG(Map<String, ? super Object> params, File outdir, File appLocation) {
 398         //generic find attempt
 399         try {
 400             if (Platform.getMajorVersion() > 10 ||
 401                 (Platform.getMajorVersion() == 10 && Platform.getMinorVersion() >= 12)) {
 402                 // we need this for OS X 10.12+
 403                 Log.info(I18N.getString("message.signing.pkg"));
 404             }
 405             String daemonLocation = DAEMON_IMAGE_BUILD_ROOT.fetchFrom(params) + "/" + APP_NAME.fetchFrom(params) + ".daemon";
 406 
 407             File appPKG = getPackages_AppPackage(params);
 408             File daemonPKG = getPackages_DaemonPackage(params);
 409 
 410             // build application package
 411             ProcessBuilder pb = new ProcessBuilder("pkgbuild",
 412                     "--component",
 413                     appLocation.toString(),
 414                     "--install-location",
 415                     "/Applications",
 416                     appPKG.getAbsolutePath());
 417             IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 418 
 419             // build daemon package if requested
 420             if (SERVICE_HINT.fetchFrom(params)) {
 421                 preparePackageScripts(params);
 422 
 423                 pb = new ProcessBuilder("pkgbuild",
 424                         "--identifier",
 425                         APP_NAME.fetchFrom(params) + ".daemon",
 426                         "--root",
 427                         daemonLocation,
 428                         "--scripts",
 429                         SCRIPTS_DIR.fetchFrom(params).getAbsolutePath(),
 430                         daemonPKG.getAbsolutePath());
 431                 IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 432             }
 433 
 434             // build final package
 435             File finalPKG = new File(outdir, INSTALLER_NAME.fetchFrom(params)
 436                     + INSTALLER_SUFFIX.fetchFrom(params)
 437                     + ".pkg");
 438             outdir.mkdirs();
 439 
 440             List<String> commandLine = new ArrayList<>();
 441             commandLine.add("productbuild");
 442 
 443             commandLine.add("--resources");
 444             commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
 445 
 446             // maybe sign
 447             if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) {
 448                 String signingIdentity = DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params);
 449                 if (signingIdentity != null) {
 450                     commandLine.add("--sign");
 451                     commandLine.add(signingIdentity);
 452                 }
 453 
 454                 String keychainName = SIGNING_KEYCHAIN.fetchFrom(params);
 455                 if (keychainName != null && !keychainName.isEmpty()) {
 456                     commandLine.add("--keychain");
 457                     commandLine.add(keychainName);
 458                 }
 459             }
 460 
 461             commandLine.add("--distribution");
 462             commandLine.add(getConfig_DistributionXMLFile(params).getAbsolutePath());
 463             commandLine.add("--package-path");
 464             commandLine.add(PACKAGES_ROOT.fetchFrom(params).getAbsolutePath());
 465 
 466             commandLine.add(finalPKG.getAbsolutePath());
 467 
 468             pb = new ProcessBuilder(commandLine);
 469             IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 470 
 471             return finalPKG;
 472         } catch (Exception ignored) {
 473             Log.verbose(ignored);
 474             return null;
 475         } finally {
 476             if (!VERBOSE.fetchFrom(params)) {
 477                 cleanupPackagesFiles(params);
 478                 cleanupConfigFiles(params);
 479 
 480                 if (SERVICE_HINT.fetchFrom(params)) {
 481                     cleanupPackageScripts(params);
 482                 }
 483             }
 484         }
 485     }
 486 
 487     //////////////////////////////////////////////////////////////////////////////////
 488     // Implement Bundler
 489     //////////////////////////////////////////////////////////////////////////////////
 490 
 491     @Override
 492     public String getName() {
 493         return I18N.getString("bundler.name");
 494     }
 495 
 496     @Override
 497     public String getDescription() {
 498         return I18N.getString("bundler.description");
 499     }
 500 
 501     @Override
 502     public String getID() {
 503         return "pkg";
 504     }
 505 
 506     @Override
 507     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 508         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 509         results.addAll(MacAppBundler.getAppBundleParameters());
 510         results.addAll(getPKGBundleParameters());
 511         return results;
 512     }
 513 
 514     public Collection<BundlerParamInfo<?>> getPKGBundleParameters() {
 515         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 516 
 517         results.addAll(MacAppBundler.getAppBundleParameters());
 518         results.addAll(Arrays.asList(
 519                 DEVELOPER_ID_INSTALLER_SIGNING_KEY,
 520                 //IDENTIFIER,
 521                 INSTALLER_SUFFIX,
 522                 LICENSE_FILE,
 523                 //SERVICE_HINT,
 524                 SIGNING_KEYCHAIN));
 525 
 526         return results;
 527     }
 528 
 529     @Override
 530     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
 531         try {
 532             if (params == null) throw new ConfigException(
 533                     I18N.getString("error.parameters-null"),
 534                     I18N.getString("error.parameters-null.advice"));
 535 
 536             //run basic validation to ensure requirements are met
 537             //we are not interested in return code, only possible exception
 538             validateAppImageAndBundeler(params);
 539 
 540             // validate license file, if used, exists in the proper place
 541             if (params.containsKey(LICENSE_FILE.getID())) {
 542                 List<RelativeFileSet> appResourcesList = APP_RESOURCES_LIST.fetchFrom(params);
 543                 for (String license : LICENSE_FILE.fetchFrom(params)) {
 544                     boolean found = false;
 545                     for (RelativeFileSet appResources : appResourcesList) {
 546                         found = found || appResources.contains(license);
 547                     }
 548                     if (!found) {
 549                         throw new ConfigException(
 550                                 I18N.getString("error.license-missing"),
 551                                 MessageFormat.format(I18N.getString("error.license-missing.advice"),
 552                                         license));
 553                     }
 554                 }
 555             }
 556 
 557             // reject explicitly set sign to true and no valid signature key
 558             if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) {
 559                 String signingIdentity = DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params);
 560                 if (signingIdentity == null) {
 561                     throw new ConfigException(
 562                             I18N.getString("error.explicit-sign-no-cert"),
 563                             I18N.getString("error.explicit-sign-no-cert.advice"));
 564                 }
 565             }
 566 
 567             // hdiutil is always available so there's no need to test for availability.
 568 
 569             return true;
 570         } catch (RuntimeException re) {
 571             if (re.getCause() instanceof ConfigException) {
 572                 throw (ConfigException) re.getCause();
 573             } else {
 574                 throw new ConfigException(re);
 575             }
 576         }
 577     }
 578 
 579     @Override
 580     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 581         return bundle(params, outputParentDir);
 582     }
 583 
 584 }