modules/fxpackager/src/main/java/com/sun/javafx/tools/packager/bundlers/MacDMGBundler.java

Print this page




  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 package com.sun.javafx.tools.packager.bundlers;
  26 
  27 import com.oracle.bundlers.BundlerParamInfo;
  28 import com.oracle.bundlers.mac.MacBaseInstallerBundler;
  29 import com.sun.javafx.tools.packager.Log;
  30 import com.sun.javafx.tools.resource.mac.MacResources;
  31 import sun.misc.BASE64Encoder;
  32 
  33 import java.io.*;
  34 import java.util.*;
  35 
  36 import static com.oracle.bundlers.StandardBundlerParam.*;
  37 
  38 public class MacDMGBundler extends MacBaseInstallerBundler {
  39 
  40 
  41     static final String DEFAULT_BACKGROUND_IMAGE="background.png";
  42     static final String DEFAULT_DMG_SETUP_SCRIPT="DMGsetup.scpt";
  43     static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  44 
  45     //existing SQE tests look for "license" string in the filenames
  46     // when they look for unathorized license files in the build artifacts
  47     // Use different name to make them happy
  48     static final String DEFAULT_LICENSE_PLIST="lic_template.plist";
  49     private Map<String, ? super Object> params;
  50 
  51     public MacDMGBundler() {
  52         super();
  53         baseResourceLoader = MacResources.class;
  54     }
  55 
  56     //@Override
  57     public File bundle(Map<String, ? super Object> p, File outdir) {
  58         Log.info("Building DMG package for " + NAME.fetchFrom(p));
  59 
  60         params = p;
  61 
  62         File appImageDir = APP_IMAGE_BUILD_ROOT.fetchFrom(params);
  63         try {
  64             appImageDir.mkdirs();
  65 
  66             if (prepareAppBundle(p) && prepareConfigFiles()) {
  67                 File configScript = getConfig_Script();
  68                 if (configScript.exists()) {
  69                     Log.info("Running shell script on application image ["
  70                             + configScript.getAbsolutePath() + "]");
  71                     IOUtils.run("bash", configScript, verbose);
  72                 }
  73 
  74                 return buildDMG(p, outdir);
  75             }
  76             return null;
  77         } catch (ConfigException e) {
  78             Log.info("Bundler " + getName() + " skipped because of a configuration problem: " + e.getMessage() + "\nAdvice to fix: " + e.getAdvice());
  79             return null;
  80         } catch (IOException ex) {
  81             Log.verbose(ex);
  82             return null;
  83         } finally {
  84             try {
  85                 if (appImageDir != null && !Log.isDebug()) {
  86                     IOUtils.deleteRecursive(appImageDir);
  87                 } else if (appImageDir != null) {
  88                     Log.info("[DEBUG] Intermediate application bundle image: "+
  89                             appImageDir.getAbsolutePath());
  90                 }
  91                 if (!verbose) {
  92                     //cleanup
  93                     cleanupConfigFiles();
  94                 } else {
  95                     Log.info("  Config files are saved to "
  96                             + CONFIG_ROOT.fetchFrom(p).getAbsolutePath()
  97                             + ". Use them to customize package.");
  98                 }
  99                 appImageDir = null;
 100             } catch (FileNotFoundException ex) {
 101                 //noinspection ReturnInsideFinallyBlock
 102                 return null;
 103             }
 104         }
 105     }
 106 
 107     //remove
 108     protected void cleanupConfigFiles() {
 109         if (getConfig_VolumeBackground() != null) {
 110             getConfig_VolumeBackground().delete();
 111         }
 112         if (getConfig_VolumeIcon() != null) {
 113             getConfig_VolumeIcon().delete();
 114         }
 115         if (getConfig_VolumeScript() != null) {
 116             getConfig_VolumeScript().delete();
 117         }
 118         if (getConfig_Script() != null) {
 119             getConfig_Script().delete();
 120         }
 121         if (getConfig_LicenseFile() != null) {
 122             getConfig_LicenseFile().delete();
 123         }
 124         APP_BUNDLER.fetchFrom(params).cleanupConfigFiles();
 125     }
 126 
 127     @Override
 128     public String toString() {
 129         return getName();
 130     }
 131 
 132 //    @Override
 133 //    protected void setBuildRoot(File dir) {
 134 //        super.setBuildRoot(dir);
 135 //        configRoot = new File(dir, "macosx");
 136 //        configRoot.mkdirs();
 137 //        APP_BUNDLER.fetchFrom(params).setBuildRoot(dir);
 138 //    }
 139 
 140     private static final String hdiutil = "/usr/bin/hdiutil";
 141 
 142     private void prepareDMGSetupScript(String volumeName, Map<String, ? super Object> p) throws IOException {
 143         File dmgSetup = getConfig_VolumeScript();
 144         Log.verbose("Preparing dmg setup: "+dmgSetup.getAbsolutePath());
 145 
 146         //prepare config for exe
 147         Map<String, String> data = new HashMap<>();
 148         data.put("DEPLOY_ACTUAL_VOLUME_NAME", volumeName);
 149         data.put("DEPLOY_APPLICATION_NAME", NAME.fetchFrom(p));
 150 
 151         //treat default null as "system wide install"
 152         boolean systemWide = SYSTEM_WIDE.fetchFrom(p) == null || SYSTEM_WIDE.fetchFrom(p);
 153 
 154         if (systemWide) {
 155             data.put("DEPLOY_INSTALL_LOCATION", "POSIX file \"/Applications\"");
 156             data.put("DEPLOY_INSTALL_NAME", "Applications");
 157         } else {
 158             data.put("DEPLOY_INSTALL_LOCATION", "(path to desktop folder)");
 159             data.put("DEPLOY_INSTALL_NAME", "Desktop");
 160         }
 161 
 162         Writer w = new BufferedWriter(new FileWriter(dmgSetup));
 163         w.write(preprocessTextResource(
 164                 com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + dmgSetup.getName(),
 165                 "DMG setup script", DEFAULT_DMG_SETUP_SCRIPT, data));
 166         w.close();
 167     }
 168 
 169     private File getConfig_VolumeScript() {
 170         return new File(CONFIG_ROOT.fetchFrom(params), NAME.fetchFrom(params) + "-dmg-setup.scpt");
 171     }
 172 
 173     private File getConfig_VolumeBackground() {
 174         return new File(CONFIG_ROOT.fetchFrom(params), NAME.fetchFrom(params) + "-background.png");
 175     }
 176 
 177     private File getConfig_VolumeIcon() {
 178         return new File(CONFIG_ROOT.fetchFrom(params), NAME.fetchFrom(params) + "-volume.icns");
 179     }
 180 
 181     private File getConfig_LicenseFile() {
 182         return new File(CONFIG_ROOT.fetchFrom(params), NAME.fetchFrom(params) + "-license.plist");
 183     }
 184 
 185     private void prepareLicense() {
 186         try {
 187             if (LICENSE_FILES.fetchFrom(params).isEmpty()) {
 188                 return;
 189             }
 190 
 191             File licFile = new File(APP_RESOURCES.fetchFrom(params).getBaseDirectory(),
 192                     LICENSE_FILES.fetchFrom(params).get(0));
 193 
 194             byte[] licenseContentOriginal = IOUtils.readFully(licFile);
 195             BASE64Encoder encoder = new BASE64Encoder();
 196             String licenseInBase64 = encoder.encode(licenseContentOriginal);
 197 
 198             Map<String, String> data = new HashMap<>();
 199             data.put("APPLICATION_LICENSE_TEXT", licenseInBase64);
 200 
 201             Writer w = new BufferedWriter(new FileWriter(getConfig_LicenseFile()));
 202             w.write(preprocessTextResource(
 203                     com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + getConfig_LicenseFile().getName(),
 204                     "License setup", DEFAULT_LICENSE_PLIST, data));
 205             w.close();
 206 
 207         } catch (IOException ex) {
 208             Log.verbose(ex);
 209         }
 210 
 211     }
 212 
 213     private boolean prepareConfigFiles() throws IOException {
 214         File bgTarget = getConfig_VolumeBackground();
 215         fetchResource(com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + bgTarget.getName(),
 216                 "dmg background",
 217                 DEFAULT_BACKGROUND_IMAGE,
 218                 bgTarget);

 219 
 220         File iconTarget = getConfig_VolumeIcon();
 221         if (ICON.fetchFrom(params) == null || !ICON.fetchFrom(params).exists()) {
 222             fetchResource(com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + iconTarget.getName(),
 223                     "volume icon",
 224                     TEMPLATE_BUNDLE_ICON,
 225                     iconTarget);

 226         } else {
 227             fetchResource(com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + iconTarget.getName(),
 228                     "volume icon",
 229                     ICON.fetchFrom(params),
 230                     iconTarget);

 231         }
 232 
 233 
 234         fetchResource(com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + getConfig_Script().getName(),
 235                 "script to run after application image is populated",
 236                 (String) null,
 237                 getConfig_Script());

 238 
 239         prepareLicense();
 240 
 241         //In theory we need to extract name from results of attach command
 242         //However, this will be a problem for customization as name will
 243         //possibly change every time and developer will not be able to fix it
 244         //As we are using tmp dir chance we get "different" namr are low =>
 245         //Use fixed name we used for bundle
 246         prepareDMGSetupScript(NAME.fetchFrom(params), params);
 247 
 248         return true;
 249     }
 250 
 251     //name of post-image script
 252     private File getConfig_Script() {
 253         return new File(CONFIG_ROOT.fetchFrom(params), NAME.fetchFrom(params) + "-post-image.sh");
 254     }
 255 
 256     //Location of SetFile utility may be different depending on MacOS version
 257     // We look for several known places and if none of them work will
 258     // try ot find it
 259     private String findSetFileUtility() {
 260         String typicalPaths[] = {"/Developer/Tools/SetFile",
 261                 "/usr/bin/SetFile", "/Developer/usr/bin/SetFile"};
 262 
 263         for (String path: typicalPaths) {
 264             File f = new File(path);
 265             if (f.exists() && f.canExecute()) {
 266                 return path;
 267             }
 268         }
 269 
 270         //generic find attempt
 271         try {
 272             ProcessBuilder pb = new ProcessBuilder("xcrun", "-find", "SetFile");
 273             Process p = pb.start();
 274             InputStreamReader isr = new InputStreamReader(p.getInputStream());
 275             BufferedReader br = new BufferedReader(isr);
 276             String lineRead = br.readLine();
 277             if (lineRead != null) {
 278                 File f = new File(lineRead);
 279                 if (f.exists() && f.canExecute()) {
 280                     return f.getAbsolutePath();
 281                 }
 282             }
 283         } catch (IOException ignored) {}
 284 
 285         return null;
 286     }
 287 
 288     private File buildDMG(
 289             Map<String, ? super Object> p, File outdir)
 290             throws IOException, ConfigException {
 291         File protoDMG = new File(IMAGES_ROOT.fetchFrom(p), NAME.fetchFrom(p) +"-tmp.dmg");
 292         File finalDMG = new File(outdir,  NAME.fetchFrom(p) +".dmg");



 293 
 294         File srcFolder = APP_IMAGE_BUILD_ROOT.fetchFrom(p); //new File(imageDir, p.name+".app");
 295         File predefinedImage = getPredefinedImage(p);
 296         if (predefinedImage != null) {
 297             srcFolder = predefinedImage;
 298         }
 299 
 300         Log.verbose(" Creating DMG file: " + finalDMG.getAbsolutePath());
 301 
 302         protoDMG.delete();
 303         if (finalDMG.exists() && !finalDMG.delete()) {
 304             throw new IOException("Dmg file exists (" + finalDMG.getAbsolutePath()
 305                     +" and can not be removed.");
 306         }
 307 
 308         protoDMG.getParentFile().mkdirs();
 309         finalDMG.getParentFile().mkdirs();
 310 
 311         //create temp image
 312         ProcessBuilder pb = new ProcessBuilder(
 313                 hdiutil,
 314                 "create",
 315                 "-quiet",
 316                 "-srcfolder", srcFolder.getAbsolutePath(),
 317                 "-volname", NAME.fetchFrom(p),
 318                 "-ov", protoDMG.getAbsolutePath(),
 319                 "-format", "UDRW");
 320         IOUtils.exec(pb, verbose);
 321 
 322         //mount temp image
 323         pb = new ProcessBuilder(
 324                 hdiutil,
 325                 "attach",
 326                 protoDMG.getAbsolutePath(),
 327                 "-quiet",
 328                 "-mountroot", IMAGES_ROOT.fetchFrom(p).getAbsolutePath());
 329         IOUtils.exec(pb, verbose);
 330 
 331         File mountedRoot = new File(IMAGES_ROOT.fetchFrom(p).getAbsolutePath(), NAME.fetchFrom(p));
 332 
 333         //background image
 334         File bgdir = new File(mountedRoot, ".background");
 335         bgdir.mkdirs();
 336         IOUtils.copyFile(getConfig_VolumeBackground(),
 337                 new File(bgdir, "background.png"));
 338 
 339         //volume icon
 340         File volumeIconFile = new File(mountedRoot, ".VolumeIcon.icns");
 341         IOUtils.copyFile(getConfig_VolumeIcon(),
 342                 volumeIconFile);
 343 
 344         pb = new ProcessBuilder("osascript",
 345                 getConfig_VolumeScript().getAbsolutePath());
 346         IOUtils.exec(pb, verbose);
 347 
 348         //Indicate that we want a custom icon
 349         //NB: attributes of the root directory are ignored when creating the volume
 350         //  Therefore we have to do this after we mount image
 351         String setFileUtility = findSetFileUtility();
 352         if (setFileUtility != null) { //can not find utility => keep going without icon
 353             volumeIconFile.setWritable(true);
 354             //The “creator” attribute on a file is a legacy attribute
 355             // but it seems Finder expects these bytes to be “icnC” for the volume icon
 356             // (http://endrift.com/blog/2010/06/14/dmg-files-volume-icons-cli/)
 357             pb = new ProcessBuilder(
 358                     setFileUtility,
 359                     "-c", "icnC",
 360                     volumeIconFile.getAbsolutePath());
 361             IOUtils.exec(pb, verbose);
 362             volumeIconFile.setReadOnly();
 363 
 364             pb = new ProcessBuilder(
 365                     setFileUtility,
 366                     "-a", "C",
 367                     mountedRoot.getAbsolutePath());
 368             IOUtils.exec(pb, verbose);
 369         } else {
 370             Log.verbose("Skip enabling custom icon as SetFile utility is not found");
 371         }
 372 
 373         // Detach the temporary image
 374         pb = new ProcessBuilder(
 375                 hdiutil,
 376                 "detach",
 377                 "-quiet",
 378                 mountedRoot.getAbsolutePath());
 379         IOUtils.exec(pb, verbose);
 380 
 381         // Compress it to a new image
 382         pb = new ProcessBuilder(
 383                 hdiutil,
 384                 "convert",
 385                 protoDMG.getAbsolutePath(),
 386                 "-quiet",
 387                 "-format", "UDZO",
 388                 "-o", finalDMG.getAbsolutePath());
 389         IOUtils.exec(pb, verbose);
 390 
 391         //add license if needed
 392         if (getConfig_LicenseFile().exists()) {
 393             //hdiutil unflatten your_image_file.dmg
 394             pb = new ProcessBuilder(
 395                     hdiutil,
 396                     "unflatten",
 397                     finalDMG.getAbsolutePath()
 398             );
 399             IOUtils.exec(pb, verbose);
 400 
 401             //add license
 402             pb = new ProcessBuilder(
 403                     hdiutil,
 404                     "udifrez",
 405                     finalDMG.getAbsolutePath(),
 406                     "-xml",
 407                     getConfig_LicenseFile().getAbsolutePath()
 408             );
 409             IOUtils.exec(pb, verbose);
 410 
 411             //hdiutil flatten your_image_file.dmg
 412             pb = new ProcessBuilder(
 413                     hdiutil,
 414                     "flatten",
 415                     finalDMG.getAbsolutePath()
 416             );
 417             IOUtils.exec(pb, verbose);
 418 
 419         }
 420 
 421         //Delete the temporary image
 422         protoDMG.delete();
 423 
 424         Log.info("Result DMG installer for " + NAME.fetchFrom(p) +": "
 425                 + finalDMG.getAbsolutePath());
 426 
 427         return finalDMG;
 428     }
 429 
 430 
 431     //////////////////////////////////////////////////////////////////////////////////
 432     // Implement Bundler
 433     //////////////////////////////////////////////////////////////////////////////////
 434 
 435     @Override
 436     public String getName() {
 437         return "DMG Installer";
 438     }
 439 
 440     @Override
 441     public String getDescription() {
 442         return "Mac DMG Installer Bundle.";
 443     }
 444 
 445     @Override
 446     public String getID() {
 447         return "dmg";
 448     }
 449 
 450     @Override
 451     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 452         //Add DMG Specific parameters as required
 453         return super.getBundleParameters();
 454     }
 455 
 456     @Override
 457     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {

 458         if (params == null) throw new ConfigException("Parameters map is null.", "Pass in a non-null parameters map.");
 459 
 460         // hdiutil is always available so there's no need to test for availability.
 461         //run basic validation to ensure requirements are met
 462 
 463         //run basic validation to ensure requirements are met
 464         //we are not interested in return code, only possible exception
 465         APP_BUNDLER.fetchFrom(params).doValidate(params);







 466         return true;



 467     }
 468 
 469     @Override
 470     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 471         return bundle(params, outputParentDir);
 472     }
 473 }


  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 package com.sun.javafx.tools.packager.bundlers;
  26 
  27 import com.oracle.bundlers.BundlerParamInfo;
  28 import com.oracle.bundlers.mac.MacBaseInstallerBundler;
  29 import com.sun.javafx.tools.packager.Log;
  30 import com.sun.javafx.tools.resource.mac.MacResources;
  31 import sun.misc.BASE64Encoder;
  32 
  33 import java.io.*;
  34 import java.util.*;
  35 
  36 import static com.oracle.bundlers.StandardBundlerParam.*;
  37 
  38 public class MacDMGBundler extends MacBaseInstallerBundler {
  39 

  40     static final String DEFAULT_BACKGROUND_IMAGE="background.png";
  41     static final String DEFAULT_DMG_SETUP_SCRIPT="DMGsetup.scpt";
  42     static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  43 
  44     //existing SQE tests look for "license" string in the filenames
  45     // when they look for unathorized license files in the build artifacts
  46     // Use different name to make them happy
  47     static final String DEFAULT_LICENSE_PLIST="lic_template.plist";

  48 
  49     public MacDMGBundler() {
  50         super();
  51         baseResourceLoader = MacResources.class;
  52     }
  53 
  54     //@Override
  55     public File bundle(Map<String, ? super Object> params, File outdir) {
  56         Log.info("Building DMG package for " + APP_NAME.fetchFrom(params));


  57 
  58         File appImageDir = APP_IMAGE_BUILD_ROOT.fetchFrom(params);
  59         try {
  60             appImageDir.mkdirs();
  61 
  62             if (prepareAppBundle(params) != null && prepareConfigFiles(params)) {
  63                 File configScript = getConfig_Script(params);
  64                 if (configScript.exists()) {
  65                     Log.info("Running shell script on application image ["
  66                             + configScript.getAbsolutePath() + "]");
  67                     IOUtils.run("bash", configScript, VERBOSE.fetchFrom(params));
  68                 }
  69 
  70                 return buildDMG(params, outdir);
  71             }
  72             return null;



  73         } catch (IOException ex) {
  74             Log.verbose(ex);
  75             return null;
  76         } finally {
  77             try {
  78                 if (appImageDir != null && !Log.isDebug()) {
  79                     IOUtils.deleteRecursive(appImageDir);
  80                 } else if (appImageDir != null) {
  81                     Log.info("[DEBUG] Intermediate application bundle image: "+
  82                             appImageDir.getAbsolutePath());
  83                 }
  84                 if (!VERBOSE.fetchFrom(params)) {
  85                     //cleanup
  86                     cleanupConfigFiles(params);
  87                 } else {
  88                     Log.info("  Config files are saved to "
  89                             + CONFIG_ROOT.fetchFrom(params).getAbsolutePath()
  90                             + ". Use them to customize package.");
  91                 }

  92             } catch (FileNotFoundException ex) {
  93                 //noinspection ReturnInsideFinallyBlock
  94                 return null;
  95             }
  96         }
  97     }
  98 
  99     //remove
 100     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
 101         if (getConfig_VolumeBackground(params) != null) {
 102             getConfig_VolumeBackground(params).delete();
 103         }
 104         if (getConfig_VolumeIcon(params) != null) {
 105             getConfig_VolumeIcon(params).delete();
 106         }
 107         if (getConfig_VolumeScript(params) != null) {
 108             getConfig_VolumeScript(params).delete();
 109         }
 110         if (getConfig_Script(params) != null) {
 111             getConfig_Script(params).delete();
 112         }
 113         if (getConfig_LicenseFile(params) != null) {
 114             getConfig_LicenseFile(params).delete();
 115         }
 116         APP_BUNDLER.fetchFrom(params).cleanupConfigFiles(params);
 117     }
 118 
 119     @Override
 120     public String toString() {
 121         return getName();
 122     }
 123 
 124 //    @Override
 125 //    protected void setBuildRoot(File dir) {
 126 //        super.setBuildRoot(dir);
 127 //        configRoot = new File(dir, "macosx");
 128 //        configRoot.mkdirs();
 129 //        APP_BUNDLER.fetchFrom(params).setBuildRoot(dir);
 130 //    }
 131 
 132     private static final String hdiutil = "/usr/bin/hdiutil";
 133 
 134     private void prepareDMGSetupScript(String volumeName, Map<String, ? super Object> p) throws IOException {
 135         File dmgSetup = getConfig_VolumeScript(p);
 136         Log.verbose("Preparing dmg setup: "+dmgSetup.getAbsolutePath());
 137 
 138         //prepare config for exe
 139         Map<String, String> data = new HashMap<>();
 140         data.put("DEPLOY_ACTUAL_VOLUME_NAME", volumeName);
 141         data.put("DEPLOY_APPLICATION_NAME", APP_NAME.fetchFrom(p));
 142 
 143         //treat default null as "system wide install"
 144         boolean systemWide = SYSTEM_WIDE.fetchFrom(p) == null || SYSTEM_WIDE.fetchFrom(p);
 145 
 146         if (systemWide) {
 147             data.put("DEPLOY_INSTALL_LOCATION", "POSIX file \"/Applications\"");
 148             data.put("DEPLOY_INSTALL_NAME", "Applications");
 149         } else {
 150             data.put("DEPLOY_INSTALL_LOCATION", "(path to desktop folder)");
 151             data.put("DEPLOY_INSTALL_NAME", "Desktop");
 152         }
 153 
 154         Writer w = new BufferedWriter(new FileWriter(dmgSetup));
 155         w.write(preprocessTextResource(
 156                 com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + dmgSetup.getName(),
 157                 "DMG setup script", DEFAULT_DMG_SETUP_SCRIPT, data, VERBOSE.fetchFrom(p)));
 158         w.close();
 159     }
 160 
 161     private File getConfig_VolumeScript(Map<String, ? super Object> params) {
 162         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-dmg-setup.scpt");
 163     }
 164 
 165     private File getConfig_VolumeBackground(Map<String, ? super Object> params) {
 166         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-background.png");
 167     }
 168 
 169     private File getConfig_VolumeIcon(Map<String, ? super Object> params) {
 170         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-volume.icns");
 171     }
 172 
 173     private File getConfig_LicenseFile(Map<String, ? super Object> params) {
 174         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-license.plist");
 175     }
 176 
 177     private void prepareLicense(Map<String, ? super Object> params) {
 178         try {
 179             if (LICENSE_FILES.fetchFrom(params).isEmpty()) {
 180                 return;
 181             }
 182 
 183             File licFile = new File(APP_RESOURCES.fetchFrom(params).getBaseDirectory(),
 184                     LICENSE_FILES.fetchFrom(params).get(0));
 185 
 186             byte[] licenseContentOriginal = IOUtils.readFully(licFile);
 187             BASE64Encoder encoder = new BASE64Encoder();
 188             String licenseInBase64 = encoder.encode(licenseContentOriginal);
 189 
 190             Map<String, String> data = new HashMap<>();
 191             data.put("APPLICATION_LICENSE_TEXT", licenseInBase64);
 192 
 193             Writer w = new BufferedWriter(new FileWriter(getConfig_LicenseFile(params)));
 194             w.write(preprocessTextResource(
 195                     com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + getConfig_LicenseFile(params).getName(),
 196                     "License setup", DEFAULT_LICENSE_PLIST, data, VERBOSE.fetchFrom(params)));
 197             w.close();
 198 
 199         } catch (IOException ex) {
 200             Log.verbose(ex);
 201         }
 202 
 203     }
 204 
 205     private boolean prepareConfigFiles(Map<String, ? super Object> params) throws IOException {
 206         File bgTarget = getConfig_VolumeBackground(params);
 207         fetchResource(com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + bgTarget.getName(),
 208                 "dmg background",
 209                 DEFAULT_BACKGROUND_IMAGE,
 210                 bgTarget,
 211                 VERBOSE.fetchFrom(params));
 212 
 213         File iconTarget = getConfig_VolumeIcon(params);
 214         if (ICON.fetchFrom(params) == null || !ICON.fetchFrom(params).exists()) {
 215             fetchResource(com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + iconTarget.getName(),
 216                     "volume icon",
 217                     TEMPLATE_BUNDLE_ICON,
 218                     iconTarget,
 219                     VERBOSE.fetchFrom(params));
 220         } else {
 221             fetchResource(com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + iconTarget.getName(),
 222                     "volume icon",
 223                     ICON.fetchFrom(params),
 224                     iconTarget,
 225                     VERBOSE.fetchFrom(params));
 226         }
 227 
 228 
 229         fetchResource(com.sun.javafx.tools.packager.bundlers.MacAppBundler.MAC_BUNDLER_PREFIX + getConfig_Script(params).getName(),
 230                 "script to run after application image is populated",
 231                 (String) null,
 232                 getConfig_Script(params),
 233                 VERBOSE.fetchFrom(params));
 234 
 235         prepareLicense(params);
 236 
 237         //In theory we need to extract name from results of attach command
 238         //However, this will be a problem for customization as name will
 239         //possibly change every time and developer will not be able to fix it
 240         //As we are using tmp dir chance we get "different" namr are low =>
 241         //Use fixed name we used for bundle
 242         prepareDMGSetupScript(APP_NAME.fetchFrom(params), params);
 243 
 244         return true;
 245     }
 246 
 247     //name of post-image script
 248     private File getConfig_Script(Map<String, ? super Object> params) {
 249         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-post-image.sh");
 250     }
 251 
 252     //Location of SetFile utility may be different depending on MacOS version
 253     // We look for several known places and if none of them work will
 254     // try ot find it
 255     private String findSetFileUtility() {
 256         String typicalPaths[] = {"/Developer/Tools/SetFile",
 257                 "/usr/bin/SetFile", "/Developer/usr/bin/SetFile"};
 258 
 259         for (String path: typicalPaths) {
 260             File f = new File(path);
 261             if (f.exists() && f.canExecute()) {
 262                 return path;
 263             }
 264         }
 265 
 266         //generic find attempt
 267         try {
 268             ProcessBuilder pb = new ProcessBuilder("xcrun", "-find", "SetFile");
 269             Process p = pb.start();
 270             InputStreamReader isr = new InputStreamReader(p.getInputStream());
 271             BufferedReader br = new BufferedReader(isr);
 272             String lineRead = br.readLine();
 273             if (lineRead != null) {
 274                 File f = new File(lineRead);
 275                 if (f.exists() && f.canExecute()) {
 276                     return f.getAbsolutePath();
 277                 }
 278             }
 279         } catch (IOException ignored) {}
 280 
 281         return null;
 282     }
 283 
 284     private File buildDMG(
 285             Map<String, ? super Object> p, File outdir)
 286             throws IOException {
 287         File imagesRoot = IMAGES_ROOT.fetchFrom(p);
 288         if (!imagesRoot.exists()) imagesRoot.mkdirs();
 289 
 290         File protoDMG = new File(imagesRoot, APP_NAME.fetchFrom(p) +"-tmp.dmg");
 291         File finalDMG = new File(outdir,  APP_NAME.fetchFrom(p) +".dmg");
 292 
 293         File srcFolder = APP_IMAGE_BUILD_ROOT.fetchFrom(p); //new File(imageDir, p.name+".app");
 294         File predefinedImage = getPredefinedImage(p);
 295         if (predefinedImage != null) {
 296             srcFolder = predefinedImage;
 297         }
 298 
 299         Log.verbose(" Creating DMG file: " + finalDMG.getAbsolutePath());
 300 
 301         protoDMG.delete();
 302         if (finalDMG.exists() && !finalDMG.delete()) {
 303             throw new IOException("Dmg file exists (" + finalDMG.getAbsolutePath()
 304                     +" and can not be removed.");
 305         }
 306 
 307         protoDMG.getParentFile().mkdirs();
 308         finalDMG.getParentFile().mkdirs();
 309 
 310         //create temp image
 311         ProcessBuilder pb = new ProcessBuilder(
 312                 hdiutil,
 313                 "create",
 314                 "-quiet",
 315                 "-srcfolder", srcFolder.getAbsolutePath(),
 316                 "-volname", APP_NAME.fetchFrom(p),
 317                 "-ov", protoDMG.getAbsolutePath(),
 318                 "-format", "UDRW");
 319         IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 320 
 321         //mount temp image
 322         pb = new ProcessBuilder(
 323                 hdiutil,
 324                 "attach",
 325                 protoDMG.getAbsolutePath(),
 326                 "-quiet",
 327                 "-mountroot", imagesRoot.getAbsolutePath());
 328         IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 329 
 330         File mountedRoot = new File(imagesRoot.getAbsolutePath(), APP_NAME.fetchFrom(p));
 331 
 332         //background image
 333         File bgdir = new File(mountedRoot, ".background");
 334         bgdir.mkdirs();
 335         IOUtils.copyFile(getConfig_VolumeBackground(p),
 336                 new File(bgdir, "background.png"));
 337 
 338         //volume icon
 339         File volumeIconFile = new File(mountedRoot, ".VolumeIcon.icns");
 340         IOUtils.copyFile(getConfig_VolumeIcon(p),
 341                 volumeIconFile);
 342 
 343         pb = new ProcessBuilder("osascript",
 344                 getConfig_VolumeScript(p).getAbsolutePath());
 345         IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 346 
 347         //Indicate that we want a custom icon
 348         //NB: attributes of the root directory are ignored when creating the volume
 349         //  Therefore we have to do this after we mount image
 350         String setFileUtility = findSetFileUtility();
 351         if (setFileUtility != null) { //can not find utility => keep going without icon
 352             volumeIconFile.setWritable(true);
 353             //The “creator” attribute on a file is a legacy attribute
 354             // but it seems Finder expects these bytes to be “icnC” for the volume icon
 355             // (http://endrift.com/blog/2010/06/14/dmg-files-volume-icons-cli/)
 356             pb = new ProcessBuilder(
 357                     setFileUtility,
 358                     "-c", "icnC",
 359                     volumeIconFile.getAbsolutePath());
 360             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 361             volumeIconFile.setReadOnly();
 362 
 363             pb = new ProcessBuilder(
 364                     setFileUtility,
 365                     "-a", "C",
 366                     mountedRoot.getAbsolutePath());
 367             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 368         } else {
 369             Log.verbose("Skip enabling custom icon as SetFile utility is not found");
 370         }
 371 
 372         // Detach the temporary image
 373         pb = new ProcessBuilder(
 374                 hdiutil,
 375                 "detach",
 376                 "-quiet",
 377                 mountedRoot.getAbsolutePath());
 378         IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 379 
 380         // Compress it to a new image
 381         pb = new ProcessBuilder(
 382                 hdiutil,
 383                 "convert",
 384                 protoDMG.getAbsolutePath(),
 385                 "-quiet",
 386                 "-format", "UDZO",
 387                 "-o", finalDMG.getAbsolutePath());
 388         IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 389 
 390         //add license if needed
 391         if (getConfig_LicenseFile(p).exists()) {
 392             //hdiutil unflatten your_image_file.dmg
 393             pb = new ProcessBuilder(
 394                     hdiutil,
 395                     "unflatten",
 396                     finalDMG.getAbsolutePath()
 397             );
 398             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 399 
 400             //add license
 401             pb = new ProcessBuilder(
 402                     hdiutil,
 403                     "udifrez",
 404                     finalDMG.getAbsolutePath(),
 405                     "-xml",
 406                     getConfig_LicenseFile(p).getAbsolutePath()
 407             );
 408             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 409 
 410             //hdiutil flatten your_image_file.dmg
 411             pb = new ProcessBuilder(
 412                     hdiutil,
 413                     "flatten",
 414                     finalDMG.getAbsolutePath()
 415             );
 416             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 417 
 418         }
 419 
 420         //Delete the temporary image
 421         protoDMG.delete();
 422 
 423         Log.info("Result DMG installer for " + APP_NAME.fetchFrom(p) +": "
 424                 + finalDMG.getAbsolutePath());
 425 
 426         return finalDMG;
 427     }
 428 
 429 
 430     //////////////////////////////////////////////////////////////////////////////////
 431     // Implement Bundler
 432     //////////////////////////////////////////////////////////////////////////////////
 433 
 434     @Override
 435     public String getName() {
 436         return "DMG Installer";
 437     }
 438 
 439     @Override
 440     public String getDescription() {
 441         return "Mac DMG Installer Bundle.";
 442     }
 443 
 444     @Override
 445     public String getID() {
 446         return "dmg";
 447     }
 448 
 449     @Override
 450     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 451         //Add DMG Specific parameters as required
 452         return super.getBundleParameters();
 453     }
 454 
 455     @Override
 456     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
 457         try {
 458             if (params == null) throw new ConfigException("Parameters map is null.", "Pass in a non-null parameters map.");
 459 
 460             // hdiutil is always available so there's no need to test for availability.
 461             //run basic validation to ensure requirements are met
 462 
 463             //run basic validation to ensure requirements are met
 464             //we are not interested in return code, only possible exception
 465             validateAppImageAndBundeler(params);
 466 
 467             if (SERVICE_HINT.fetchFrom(params)) {
 468                 throw new ConfigException(
 469                         "DMG bundler doesn't support services.",
 470                         "Make sure that the service hint is set to false.");
 471             }
 472 
 473             return true;
 474         } catch (RuntimeException re) {
 475             throw new ConfigException(re);
 476         }
 477     }
 478 
 479     @Override
 480     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 481         return bundle(params, outputParentDir);
 482     }
 483 }