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