src/jdk.jpackager/macosx/classes/jdk/jpackager/internal/MacDmgBundler.java
branchJDK-8200758-branch
changeset 57038 b0f09e7c4680
equal deleted inserted replaced
57032:a42d0a8e0916 57038:b0f09e7c4680
       
     1 /*
       
     2  * Copyright (c) 2012, 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;
       
    27 
       
    28 import jdk.jpackager.internal.*;
       
    29 import jdk.jpackager.internal.IOUtils;
       
    30 import jdk.jpackager.internal.resources.MacResources;
       
    31 import jdk.jpackager.internal.Arguments;
       
    32 
       
    33 import java.io.*;
       
    34 import java.nio.file.Files;
       
    35 import java.text.MessageFormat;
       
    36 import java.util.*;
       
    37 
       
    38 import static jdk.jpackager.internal.StandardBundlerParam.*;
       
    39 
       
    40 public class MacDmgBundler extends MacBaseInstallerBundler {
       
    41 
       
    42     private static final ResourceBundle I18N =
       
    43             ResourceBundle.getBundle(
       
    44             "jdk.jpackager.internal.resources.MacDmgBundler");
       
    45 
       
    46     static final String DEFAULT_BACKGROUND_IMAGE="background_dmg.png";
       
    47     static final String DEFAULT_DMG_SETUP_SCRIPT="DMGsetup.scpt";
       
    48     static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
       
    49 
       
    50     static final String DEFAULT_LICENSE_PLIST="lic_template.plist";
       
    51 
       
    52     public static final BundlerParamInfo<String> INSTALLER_SUFFIX =
       
    53             new StandardBundlerParam<> (
       
    54             I18N.getString("param.installer-suffix.name"),
       
    55             I18N.getString("param.installer-suffix.description"),
       
    56             "mac.dmg.installerName.suffix",
       
    57             String.class,
       
    58             params -> "",
       
    59             (s, p) -> s);
       
    60 
       
    61     public MacDmgBundler() {
       
    62         super();
       
    63         baseResourceLoader = MacResources.class;
       
    64     }
       
    65 
       
    66     public File bundle(Map<String, ? super Object> params, File outdir) {
       
    67         Log.verbose(MessageFormat.format(I18N.getString("message.building-dmg"),
       
    68                 APP_NAME.fetchFrom(params)));
       
    69         if (!outdir.isDirectory() && !outdir.mkdirs()) {
       
    70             throw new RuntimeException(MessageFormat.format(
       
    71                     I18N.getString("error.cannot-create-output-dir"),
       
    72                     outdir.getAbsolutePath()));
       
    73         }
       
    74         if (!outdir.canWrite()) {
       
    75             throw new RuntimeException(MessageFormat.format(
       
    76                     I18N.getString("error.cannot-write-to-output-dir"),
       
    77                     outdir.getAbsolutePath()));
       
    78         }
       
    79 
       
    80         File appImageDir = APP_IMAGE_BUILD_ROOT.fetchFrom(params);
       
    81         try {
       
    82             appImageDir.mkdirs();
       
    83 
       
    84             if (prepareAppBundle(params, true) != null &&
       
    85                     prepareConfigFiles(params)) {
       
    86                 File configScript = getConfig_Script(params);
       
    87                 if (configScript.exists()) {
       
    88                     Log.verbose(MessageFormat.format(
       
    89                             I18N.getString("message.running-script"),
       
    90                             configScript.getAbsolutePath()));
       
    91                     IOUtils.run("bash", configScript, false);
       
    92                 }
       
    93 
       
    94                 return buildDMG(params, outdir);
       
    95             }
       
    96             return null;
       
    97         } catch (IOException ex) {
       
    98             Log.verbose(ex);
       
    99             return null;
       
   100         } finally {
       
   101             try {
       
   102                 if (appImageDir != null &&
       
   103                         PREDEFINED_APP_IMAGE.fetchFrom(params) == null &&
       
   104                         (PREDEFINED_RUNTIME_IMAGE.fetchFrom(params) == null ||
       
   105                         !Arguments.CREATE_JRE_INSTALLER.fetchFrom(params)) &&
       
   106                         !Log.isDebug() &&
       
   107                         !Log.isVerbose()) {
       
   108                     IOUtils.deleteRecursive(appImageDir);
       
   109                 } else if (appImageDir != null) {
       
   110                     Log.verbose(MessageFormat.format(I18N.getString(
       
   111                             "message.intermediate-image-location"),
       
   112                             appImageDir.getAbsolutePath()));
       
   113                 }
       
   114 
       
   115                 //cleanup
       
   116                 cleanupConfigFiles(params);
       
   117             } catch (IOException ex) {
       
   118                 Log.debug(ex);
       
   119                 //noinspection ReturnInsideFinallyBlock
       
   120                 return null;
       
   121             }
       
   122         }
       
   123     }
       
   124 
       
   125     //remove
       
   126     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
       
   127         if (Log.isDebug() || Log.isVerbose()) {
       
   128             return;
       
   129         }
       
   130 
       
   131         if (getConfig_VolumeBackground(params) != null) {
       
   132             getConfig_VolumeBackground(params).delete();
       
   133         }
       
   134         if (getConfig_VolumeIcon(params) != null) {
       
   135             getConfig_VolumeIcon(params).delete();
       
   136         }
       
   137         if (getConfig_VolumeScript(params) != null) {
       
   138             getConfig_VolumeScript(params).delete();
       
   139         }
       
   140         if (getConfig_Script(params) != null) {
       
   141             getConfig_Script(params).delete();
       
   142         }
       
   143         if (getConfig_LicenseFile(params) != null) {
       
   144             getConfig_LicenseFile(params).delete();
       
   145         }
       
   146         APP_BUNDLER.fetchFrom(params).cleanupConfigFiles(params);
       
   147     }
       
   148 
       
   149     private static final String hdiutil = "/usr/bin/hdiutil";
       
   150 
       
   151     private void prepareDMGSetupScript(String volumeName,
       
   152             Map<String, ? super Object> p) throws IOException {
       
   153         File dmgSetup = getConfig_VolumeScript(p);
       
   154         Log.verbose(MessageFormat.format(
       
   155                 I18N.getString("message.preparing-dmg-setup"),
       
   156                 dmgSetup.getAbsolutePath()));
       
   157 
       
   158         //prepare config for exe
       
   159         Map<String, String> data = new HashMap<>();
       
   160         data.put("DEPLOY_ACTUAL_VOLUME_NAME", volumeName);
       
   161         data.put("DEPLOY_APPLICATION_NAME", APP_NAME.fetchFrom(p));
       
   162 
       
   163         data.put("DEPLOY_INSTALL_LOCATION", "(path to desktop folder)");
       
   164         data.put("DEPLOY_INSTALL_NAME", "Desktop");
       
   165 
       
   166         Writer w = new BufferedWriter(new FileWriter(dmgSetup));
       
   167         w.write(preprocessTextResource(
       
   168                 MacAppBundler.BUNDLER_PREFIX + dmgSetup.getName(),
       
   169                 I18N.getString("resource.dmg-setup-script"),
       
   170                         DEFAULT_DMG_SETUP_SCRIPT, data, VERBOSE.fetchFrom(p),
       
   171                 DROP_IN_RESOURCES_ROOT.fetchFrom(p)));
       
   172         w.close();
       
   173     }
       
   174 
       
   175     private File getConfig_VolumeScript(Map<String, ? super Object> params) {
       
   176         return new File(CONFIG_ROOT.fetchFrom(params),
       
   177                 APP_NAME.fetchFrom(params) + "-dmg-setup.scpt");
       
   178     }
       
   179 
       
   180     private File getConfig_VolumeBackground(
       
   181             Map<String, ? super Object> params) {
       
   182         return new File(CONFIG_ROOT.fetchFrom(params),
       
   183                 APP_NAME.fetchFrom(params) + "-background.png");
       
   184     }
       
   185 
       
   186     private File getConfig_VolumeIcon(Map<String, ? super Object> params) {
       
   187         return new File(CONFIG_ROOT.fetchFrom(params),
       
   188                 APP_NAME.fetchFrom(params) + "-volume.icns");
       
   189     }
       
   190 
       
   191     private File getConfig_LicenseFile(Map<String, ? super Object> params) {
       
   192         return new File(CONFIG_ROOT.fetchFrom(params),
       
   193                 APP_NAME.fetchFrom(params) + "-license.plist");
       
   194     }
       
   195 
       
   196     private void prepareLicense(Map<String, ? super Object> params) {
       
   197         try {
       
   198             File licFile = null;
       
   199 
       
   200             List<String> licFiles = LICENSE_FILE.fetchFrom(params);
       
   201             if (licFiles.isEmpty()) {
       
   202                 return;
       
   203             }
       
   204             String licFileStr = licFiles.get(0);
       
   205 
       
   206             for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) {
       
   207                 if (rfs.contains(licFileStr)) {
       
   208                     licFile = new File(rfs.getBaseDirectory(), licFileStr);
       
   209                     break;
       
   210                 }
       
   211             }
       
   212 
       
   213             if (licFile == null) {
       
   214                 // this is NPE protection,
       
   215                 // validate should have already caught it's absence
       
   216                 Log.error("Licence file is null");
       
   217                 return;
       
   218             }
       
   219 
       
   220             byte[] licenseContentOriginal = Files.readAllBytes(licFile.toPath());
       
   221             String licenseInBase64 =
       
   222                     Base64.getEncoder().encodeToString(licenseContentOriginal);
       
   223 
       
   224             Map<String, String> data = new HashMap<>();
       
   225             data.put("APPLICATION_LICENSE_TEXT", licenseInBase64);
       
   226 
       
   227             Writer w = new BufferedWriter(
       
   228                     new FileWriter(getConfig_LicenseFile(params)));
       
   229             w.write(preprocessTextResource(
       
   230                     MacAppBundler.BUNDLER_PREFIX
       
   231                     + getConfig_LicenseFile(params).getName(),
       
   232                     I18N.getString("resource.license-setup"),
       
   233                     DEFAULT_LICENSE_PLIST, data, VERBOSE.fetchFrom(params),
       
   234                     DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
       
   235             w.close();
       
   236 
       
   237         } catch (IOException ex) {
       
   238             Log.verbose(ex);
       
   239         }
       
   240     }
       
   241 
       
   242     private boolean prepareConfigFiles(Map<String, ? super Object> params)
       
   243             throws IOException {
       
   244         File bgTarget = getConfig_VolumeBackground(params);
       
   245         fetchResource(MacAppBundler.BUNDLER_PREFIX + bgTarget.getName(),
       
   246                 I18N.getString("resource.dmg-background"),
       
   247                 DEFAULT_BACKGROUND_IMAGE,
       
   248                 bgTarget,
       
   249                 VERBOSE.fetchFrom(params),
       
   250                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
       
   251 
       
   252         File iconTarget = getConfig_VolumeIcon(params);
       
   253         if (MacAppBundler.ICON_ICNS.fetchFrom(params) == null ||
       
   254                 !MacAppBundler.ICON_ICNS.fetchFrom(params).exists()) {
       
   255             fetchResource(
       
   256                     MacAppBundler.BUNDLER_PREFIX + iconTarget.getName(),
       
   257                     I18N.getString("resource.volume-icon"),
       
   258                     TEMPLATE_BUNDLE_ICON,
       
   259                     iconTarget,
       
   260                     VERBOSE.fetchFrom(params),
       
   261                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
       
   262         } else {
       
   263             fetchResource(
       
   264                     MacAppBundler.BUNDLER_PREFIX + iconTarget.getName(),
       
   265                     I18N.getString("resource.volume-icon"),
       
   266                     MacAppBundler.ICON_ICNS.fetchFrom(params),
       
   267                     iconTarget,
       
   268                     VERBOSE.fetchFrom(params),
       
   269                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
       
   270         }
       
   271 
       
   272 
       
   273         fetchResource(MacAppBundler.BUNDLER_PREFIX
       
   274                 + getConfig_Script(params).getName(),
       
   275                 I18N.getString("resource.post-install-script"),
       
   276                 (String) null,
       
   277                 getConfig_Script(params),
       
   278                 VERBOSE.fetchFrom(params),
       
   279                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
       
   280 
       
   281         prepareLicense(params);
       
   282 
       
   283         // In theory we need to extract name from results of attach command
       
   284         // However, this will be a problem for customization as name will
       
   285         // possibly change every time and developer will not be able to fix it
       
   286         // As we are using tmp dir chance we get "different" name are low =>
       
   287         // Use fixed name we used for bundle
       
   288         prepareDMGSetupScript(APP_NAME.fetchFrom(params), params);
       
   289 
       
   290         return true;
       
   291     }
       
   292 
       
   293     // name of post-image script
       
   294     private File getConfig_Script(Map<String, ? super Object> params) {
       
   295         return new File(CONFIG_ROOT.fetchFrom(params),
       
   296                 APP_NAME.fetchFrom(params) + "-post-image.sh");
       
   297     }
       
   298 
       
   299     // Location of SetFile utility may be different depending on MacOS version
       
   300     // We look for several known places and if none of them work will
       
   301     // try ot find it
       
   302     private String findSetFileUtility() {
       
   303         String typicalPaths[] = {"/Developer/Tools/SetFile",
       
   304                 "/usr/bin/SetFile", "/Developer/usr/bin/SetFile"};
       
   305 
       
   306         for (String path: typicalPaths) {
       
   307             File f = new File(path);
       
   308             if (f.exists() && f.canExecute()) {
       
   309                 return path;
       
   310             }
       
   311         }
       
   312 
       
   313         // generic find attempt
       
   314         try {
       
   315             ProcessBuilder pb = new ProcessBuilder("xcrun", "-find", "SetFile");
       
   316             Process p = pb.start();
       
   317             InputStreamReader isr = new InputStreamReader(p.getInputStream());
       
   318             BufferedReader br = new BufferedReader(isr);
       
   319             String lineRead = br.readLine();
       
   320             if (lineRead != null) {
       
   321                 File f = new File(lineRead);
       
   322                 if (f.exists() && f.canExecute()) {
       
   323                     return f.getAbsolutePath();
       
   324                 }
       
   325             }
       
   326         } catch (IOException ignored) {}
       
   327 
       
   328         return null;
       
   329     }
       
   330 
       
   331     private File buildDMG(
       
   332             Map<String, ? super Object> p, File outdir)
       
   333             throws IOException {
       
   334         File imagesRoot = IMAGES_ROOT.fetchFrom(p);
       
   335         if (!imagesRoot.exists()) imagesRoot.mkdirs();
       
   336 
       
   337         File protoDMG = new File(imagesRoot, APP_NAME.fetchFrom(p) +"-tmp.dmg");
       
   338         File finalDMG = new File(outdir, INSTALLER_NAME.fetchFrom(p)
       
   339                 + INSTALLER_SUFFIX.fetchFrom(p)
       
   340                 + ".dmg");
       
   341 
       
   342         File srcFolder = APP_IMAGE_BUILD_ROOT.fetchFrom(p);
       
   343         File predefinedImage = StandardBundlerParam.getPredefinedAppImage(p);
       
   344         if (predefinedImage != null) {
       
   345             srcFolder = predefinedImage;
       
   346         }
       
   347 
       
   348         Log.verbose(MessageFormat.format(I18N.getString(
       
   349                 "message.creating-dmg-file"), finalDMG.getAbsolutePath()));
       
   350 
       
   351         protoDMG.delete();
       
   352         if (finalDMG.exists() && !finalDMG.delete()) {
       
   353             throw new IOException(MessageFormat.format(I18N.getString(
       
   354                     "message.dmg-cannot-be-overwritten"),
       
   355                     finalDMG.getAbsolutePath()));
       
   356         }
       
   357 
       
   358         protoDMG.getParentFile().mkdirs();
       
   359         finalDMG.getParentFile().mkdirs();
       
   360 
       
   361         String hdiUtilVerbosityFlag = Log.isDebug() ? "-verbose" : "-quiet";
       
   362 
       
   363         // create temp image
       
   364         ProcessBuilder pb = new ProcessBuilder(
       
   365                 hdiutil,
       
   366                 "create",
       
   367                 hdiUtilVerbosityFlag,
       
   368                 "-srcfolder", srcFolder.getAbsolutePath(),
       
   369                 "-volname", APP_NAME.fetchFrom(p),
       
   370                 "-ov", protoDMG.getAbsolutePath(),
       
   371                 "-fs", "HFS+",
       
   372                 "-format", "UDRW");
       
   373         IOUtils.exec(pb, false);
       
   374 
       
   375         // mount temp image
       
   376         pb = new ProcessBuilder(
       
   377                 hdiutil,
       
   378                 "attach",
       
   379                 protoDMG.getAbsolutePath(),
       
   380                 hdiUtilVerbosityFlag,
       
   381                 "-mountroot", imagesRoot.getAbsolutePath());
       
   382         IOUtils.exec(pb, false);
       
   383 
       
   384         File mountedRoot =
       
   385                 new File(imagesRoot.getAbsolutePath(), APP_NAME.fetchFrom(p));
       
   386 
       
   387         // volume icon
       
   388         File volumeIconFile = new File(mountedRoot, ".VolumeIcon.icns");
       
   389         IOUtils.copyFile(getConfig_VolumeIcon(p),
       
   390                 volumeIconFile);
       
   391 
       
   392         pb = new ProcessBuilder("osascript",
       
   393                 getConfig_VolumeScript(p).getAbsolutePath());
       
   394         IOUtils.exec(pb, false);
       
   395 
       
   396         // Indicate that we want a custom icon
       
   397         // NB: attributes of the root directory are ignored
       
   398         // when creating the volume
       
   399         // Therefore we have to do this after we mount image
       
   400         String setFileUtility = findSetFileUtility();
       
   401         if (setFileUtility != null) {
       
   402                 //can not find utility => keep going without icon
       
   403             try {
       
   404                 volumeIconFile.setWritable(true);
       
   405                 // The "creator" attribute on a file is a legacy attribute
       
   406                 // but it seems Finder excepts these bytes to be
       
   407                 // "icnC" for the volume icon
       
   408                 // http://endrift.com/blog/2010/06/14/dmg-files-volume-icons-cli
       
   409                 // (might not work on Mac 10.13 with old XCode)
       
   410                 pb = new ProcessBuilder(
       
   411                         setFileUtility,
       
   412                         "-c", "icnC",
       
   413                         volumeIconFile.getAbsolutePath());
       
   414                 IOUtils.exec(pb, false);
       
   415                 volumeIconFile.setReadOnly();
       
   416 
       
   417                 pb = new ProcessBuilder(
       
   418                         setFileUtility,
       
   419                         "-a", "C",
       
   420                         mountedRoot.getAbsolutePath());
       
   421                 IOUtils.exec(pb, false);
       
   422             } catch (IOException ex) {
       
   423                 Log.error(ex.getMessage());
       
   424                 Log.verbose("Cannot enable custom icon using SetFile utility");
       
   425             }
       
   426         } else {
       
   427             Log.verbose(
       
   428                 "Skip enabling custom icon as SetFile utility is not found");
       
   429         }
       
   430 
       
   431         // Detach the temporary image
       
   432         pb = new ProcessBuilder(
       
   433                 hdiutil,
       
   434                 "detach",
       
   435                 hdiUtilVerbosityFlag,
       
   436                 mountedRoot.getAbsolutePath());
       
   437         IOUtils.exec(pb, false);
       
   438 
       
   439         // Compress it to a new image
       
   440         pb = new ProcessBuilder(
       
   441                 hdiutil,
       
   442                 "convert",
       
   443                 protoDMG.getAbsolutePath(),
       
   444                 hdiUtilVerbosityFlag,
       
   445                 "-format", "UDZO",
       
   446                 "-o", finalDMG.getAbsolutePath());
       
   447         IOUtils.exec(pb, false);
       
   448 
       
   449         //add license if needed
       
   450         if (getConfig_LicenseFile(p).exists()) {
       
   451             //hdiutil unflatten your_image_file.dmg
       
   452             pb = new ProcessBuilder(
       
   453                     hdiutil,
       
   454                     "unflatten",
       
   455                     finalDMG.getAbsolutePath()
       
   456             );
       
   457             IOUtils.exec(pb, false);
       
   458 
       
   459             //add license
       
   460             pb = new ProcessBuilder(
       
   461                     hdiutil,
       
   462                     "udifrez",
       
   463                     finalDMG.getAbsolutePath(),
       
   464                     "-xml",
       
   465                     getConfig_LicenseFile(p).getAbsolutePath()
       
   466             );
       
   467             IOUtils.exec(pb, false);
       
   468 
       
   469             //hdiutil flatten your_image_file.dmg
       
   470             pb = new ProcessBuilder(
       
   471                     hdiutil,
       
   472                     "flatten",
       
   473                     finalDMG.getAbsolutePath()
       
   474             );
       
   475             IOUtils.exec(pb, false);
       
   476 
       
   477         }
       
   478 
       
   479         //Delete the temporary image
       
   480         protoDMG.delete();
       
   481 
       
   482         Log.verbose(MessageFormat.format(I18N.getString(
       
   483                 "message.output-to-location"),
       
   484                 APP_NAME.fetchFrom(p), finalDMG.getAbsolutePath()));
       
   485 
       
   486         return finalDMG;
       
   487     }
       
   488 
       
   489 
       
   490     //////////////////////////////////////////////////////////////////////////
       
   491     // Implement Bundler
       
   492     //////////////////////////////////////////////////////////////////////////
       
   493 
       
   494     @Override
       
   495     public String getName() {
       
   496         return I18N.getString("bundler.name");
       
   497     }
       
   498 
       
   499     @Override
       
   500     public String getDescription() {
       
   501         return I18N.getString("bundler.description");
       
   502     }
       
   503 
       
   504     @Override
       
   505     public String getID() {
       
   506         return "dmg";
       
   507     }
       
   508 
       
   509     @Override
       
   510     public Collection<BundlerParamInfo<?>> getBundleParameters() {
       
   511         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
       
   512         results.addAll(MacAppBundler.getAppBundleParameters());
       
   513         results.addAll(getDMGBundleParameters());
       
   514         return results;
       
   515     }
       
   516 
       
   517     public Collection<BundlerParamInfo<?>> getDMGBundleParameters() {
       
   518         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
       
   519 
       
   520         results.addAll(MacAppBundler.getAppBundleParameters());
       
   521         results.addAll(Arrays.asList(
       
   522                 INSTALLER_SUFFIX,
       
   523                 LICENSE_FILE
       
   524         ));
       
   525 
       
   526         return results;
       
   527     }
       
   528 
       
   529 
       
   530     @Override
       
   531     public boolean validate(Map<String, ? super Object> params)
       
   532             throws UnsupportedPlatformException, ConfigException {
       
   533         try {
       
   534             if (params == null) throw new ConfigException(
       
   535                     I18N.getString("error.parameters-null"),
       
   536                     I18N.getString("error.parameters-null.advice"));
       
   537 
       
   538             //run basic validation to ensure requirements are met
       
   539             //we are not interested in return code, only possible exception
       
   540             validateAppImageAndBundeler(params);
       
   541 
       
   542             // validate license file, if used, exists in the proper place
       
   543             if (params.containsKey(LICENSE_FILE.getID())) {
       
   544                 List<RelativeFileSet> appResourcesList =
       
   545                     APP_RESOURCES_LIST.fetchFrom(params);
       
   546                 for (String license : LICENSE_FILE.fetchFrom(params)) {
       
   547                     boolean found = false;
       
   548                     for (RelativeFileSet appResources : appResourcesList) {
       
   549                         found = found || appResources.contains(license);
       
   550                     }
       
   551                     if (!found) {
       
   552                         throw new ConfigException(
       
   553                                 I18N.getString("error.license-missing"),
       
   554                                 MessageFormat.format(I18N.getString(
       
   555                                 "error.license-missing.advice"), license));
       
   556                     }
       
   557                 }
       
   558             }
       
   559 
       
   560             return true;
       
   561         } catch (RuntimeException re) {
       
   562             if (re.getCause() instanceof ConfigException) {
       
   563                 throw (ConfigException) re.getCause();
       
   564             } else {
       
   565                 throw new ConfigException(re);
       
   566             }
       
   567         }
       
   568     }
       
   569 
       
   570     @Override
       
   571     public File execute(
       
   572             Map<String, ? super Object> params, File outputParentDir) {
       
   573         return bundle(params, outputParentDir);
       
   574     }
       
   575 
       
   576     @Override
       
   577     public boolean supported() {
       
   578         return Platform.getPlatform() == Platform.MAC;
       
   579     }
       
   580 }