--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackager/macosx/classes/jdk/jpackager/internal/mac/MacDmgBundler.java Mon Nov 05 17:32:00 2018 -0500
@@ -0,0 +1,576 @@
+/*
+ * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.jpackager.internal.mac;
+
+import jdk.jpackager.internal.*;
+import jdk.jpackager.internal.IOUtils;
+import jdk.jpackager.internal.resources.mac.MacResources;
+import jdk.jpackager.internal.Arguments;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.text.MessageFormat;
+import java.util.*;
+
+import static jdk.jpackager.internal.StandardBundlerParam.*;
+
+public class MacDmgBundler extends MacBaseInstallerBundler {
+
+ private static final ResourceBundle I18N =
+ ResourceBundle.getBundle(
+ "jdk.jpackager.internal.resources.mac.MacDmgBundler");
+
+ static final String DEFAULT_BACKGROUND_IMAGE="background_dmg.png";
+ static final String DEFAULT_DMG_SETUP_SCRIPT="DMGsetup.scpt";
+ static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
+
+ static final String DEFAULT_LICENSE_PLIST="lic_template.plist";
+
+ public static final BundlerParamInfo<String> INSTALLER_SUFFIX =
+ new StandardBundlerParam<> (
+ I18N.getString("param.installer-suffix.name"),
+ I18N.getString("param.installer-suffix.description"),
+ "mac.dmg.installerName.suffix",
+ String.class,
+ params -> "",
+ (s, p) -> s);
+
+ public MacDmgBundler() {
+ super();
+ baseResourceLoader = MacResources.class;
+ }
+
+ public File bundle(Map<String, ? super Object> params, File outdir) {
+ Log.info(MessageFormat.format(I18N.getString("message.building-dmg"),
+ APP_NAME.fetchFrom(params)));
+ if (!outdir.isDirectory() && !outdir.mkdirs()) {
+ throw new RuntimeException(MessageFormat.format(
+ I18N.getString("error.cannot-create-output-dir"),
+ outdir.getAbsolutePath()));
+ }
+ if (!outdir.canWrite()) {
+ throw new RuntimeException(MessageFormat.format(
+ I18N.getString("error.cannot-write-to-output-dir"),
+ outdir.getAbsolutePath()));
+ }
+
+ File appImageDir = APP_IMAGE_BUILD_ROOT.fetchFrom(params);
+ try {
+ appImageDir.mkdirs();
+
+ if (prepareAppBundle(params, true) != null &&
+ prepareConfigFiles(params)) {
+ File configScript = getConfig_Script(params);
+ if (configScript.exists()) {
+ Log.info(MessageFormat.format(
+ I18N.getString("message.running-script"),
+ configScript.getAbsolutePath()));
+ IOUtils.run("bash", configScript, false);
+ }
+
+ return buildDMG(params, outdir);
+ }
+ return null;
+ } catch (IOException ex) {
+ Log.verbose(ex);
+ return null;
+ } finally {
+ try {
+ if (appImageDir != null &&
+ PREDEFINED_APP_IMAGE.fetchFrom(params) == null &&
+ (PREDEFINED_RUNTIME_IMAGE.fetchFrom(params) == null ||
+ !Arguments.CREATE_JRE_INSTALLER.fetchFrom(params)) &&
+ !Log.isDebug()) {
+ IOUtils.deleteRecursive(appImageDir);
+ } else if (appImageDir != null) {
+ Log.info(MessageFormat.format(I18N.getString(
+ "message.intermediate-image-location"),
+ appImageDir.getAbsolutePath()));
+ }
+
+ //cleanup
+ cleanupConfigFiles(params);
+ } catch (IOException ex) {
+ Log.debug(ex);
+ //noinspection ReturnInsideFinallyBlock
+ return null;
+ }
+ }
+ }
+
+ //remove
+ protected void cleanupConfigFiles(Map<String, ? super Object> params) {
+ if (getConfig_VolumeBackground(params) != null) {
+ getConfig_VolumeBackground(params).delete();
+ }
+ if (getConfig_VolumeIcon(params) != null) {
+ getConfig_VolumeIcon(params).delete();
+ }
+ if (getConfig_VolumeScript(params) != null) {
+ getConfig_VolumeScript(params).delete();
+ }
+ if (getConfig_Script(params) != null) {
+ getConfig_Script(params).delete();
+ }
+ if (getConfig_LicenseFile(params) != null) {
+ getConfig_LicenseFile(params).delete();
+ }
+ APP_BUNDLER.fetchFrom(params).cleanupConfigFiles(params);
+ }
+
+ private static final String hdiutil = "/usr/bin/hdiutil";
+
+ private void prepareDMGSetupScript(String volumeName,
+ Map<String, ? super Object> p) throws IOException {
+ File dmgSetup = getConfig_VolumeScript(p);
+ Log.verbose(MessageFormat.format(
+ I18N.getString("message.preparing-dmg-setup"),
+ dmgSetup.getAbsolutePath()));
+
+ //prepare config for exe
+ Map<String, String> data = new HashMap<>();
+ data.put("DEPLOY_ACTUAL_VOLUME_NAME", volumeName);
+ data.put("DEPLOY_APPLICATION_NAME", APP_NAME.fetchFrom(p));
+
+ data.put("DEPLOY_INSTALL_LOCATION", "(path to desktop folder)");
+ data.put("DEPLOY_INSTALL_NAME", "Desktop");
+
+ Writer w = new BufferedWriter(new FileWriter(dmgSetup));
+ w.write(preprocessTextResource(
+ MacAppBundler.MAC_BUNDLER_PREFIX + dmgSetup.getName(),
+ I18N.getString("resource.dmg-setup-script"),
+ DEFAULT_DMG_SETUP_SCRIPT, data, VERBOSE.fetchFrom(p),
+ DROP_IN_RESOURCES_ROOT.fetchFrom(p)));
+ w.close();
+ }
+
+ private File getConfig_VolumeScript(Map<String, ? super Object> params) {
+ return new File(CONFIG_ROOT.fetchFrom(params),
+ APP_NAME.fetchFrom(params) + "-dmg-setup.scpt");
+ }
+
+ private File getConfig_VolumeBackground(
+ Map<String, ? super Object> params) {
+ return new File(CONFIG_ROOT.fetchFrom(params),
+ APP_NAME.fetchFrom(params) + "-background.png");
+ }
+
+ private File getConfig_VolumeIcon(Map<String, ? super Object> params) {
+ return new File(CONFIG_ROOT.fetchFrom(params),
+ APP_NAME.fetchFrom(params) + "-volume.icns");
+ }
+
+ private File getConfig_LicenseFile(Map<String, ? super Object> params) {
+ return new File(CONFIG_ROOT.fetchFrom(params),
+ APP_NAME.fetchFrom(params) + "-license.plist");
+ }
+
+ private void prepareLicense(Map<String, ? super Object> params) {
+ try {
+ File licFile = null;
+
+ List<String> licFiles = LICENSE_FILE.fetchFrom(params);
+ if (licFiles.isEmpty()) {
+ return;
+ }
+ String licFileStr = licFiles.get(0);
+
+ for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) {
+ if (rfs.contains(licFileStr)) {
+ licFile = new File(rfs.getBaseDirectory(), licFileStr);
+ break;
+ }
+ }
+
+ if (licFile == null) {
+ // this is NPE protection,
+ // validate should have already caught it's absence
+ Log.error("Licence file is null");
+ return;
+ }
+
+ byte[] licenseContentOriginal = Files.readAllBytes(licFile.toPath());
+ String licenseInBase64 =
+ Base64.getEncoder().encodeToString(licenseContentOriginal);
+
+ Map<String, String> data = new HashMap<>();
+ data.put("APPLICATION_LICENSE_TEXT", licenseInBase64);
+
+ Writer w = new BufferedWriter(
+ new FileWriter(getConfig_LicenseFile(params)));
+ w.write(preprocessTextResource(
+ MacAppBundler.MAC_BUNDLER_PREFIX
+ + getConfig_LicenseFile(params).getName(),
+ I18N.getString("resource.license-setup"),
+ DEFAULT_LICENSE_PLIST, data, VERBOSE.fetchFrom(params),
+ DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
+ w.close();
+
+ } catch (IOException ex) {
+ Log.verbose(ex);
+ }
+ }
+
+ private boolean prepareConfigFiles(Map<String, ? super Object> params)
+ throws IOException {
+ File bgTarget = getConfig_VolumeBackground(params);
+ fetchResource(MacAppBundler.MAC_BUNDLER_PREFIX + bgTarget.getName(),
+ I18N.getString("resource.dmg-background"),
+ DEFAULT_BACKGROUND_IMAGE,
+ bgTarget,
+ VERBOSE.fetchFrom(params),
+ DROP_IN_RESOURCES_ROOT.fetchFrom(params));
+
+ File iconTarget = getConfig_VolumeIcon(params);
+ if (MacAppBundler.ICON_ICNS.fetchFrom(params) == null ||
+ !MacAppBundler.ICON_ICNS.fetchFrom(params).exists()) {
+ fetchResource(
+ MacAppBundler.MAC_BUNDLER_PREFIX + iconTarget.getName(),
+ I18N.getString("resource.volume-icon"),
+ TEMPLATE_BUNDLE_ICON,
+ iconTarget,
+ VERBOSE.fetchFrom(params),
+ DROP_IN_RESOURCES_ROOT.fetchFrom(params));
+ } else {
+ fetchResource(
+ MacAppBundler.MAC_BUNDLER_PREFIX + iconTarget.getName(),
+ I18N.getString("resource.volume-icon"),
+ MacAppBundler.ICON_ICNS.fetchFrom(params),
+ iconTarget,
+ VERBOSE.fetchFrom(params),
+ DROP_IN_RESOURCES_ROOT.fetchFrom(params));
+ }
+
+
+ fetchResource(MacAppBundler.MAC_BUNDLER_PREFIX
+ + getConfig_Script(params).getName(),
+ I18N.getString("resource.post-install-script"),
+ (String) null,
+ getConfig_Script(params),
+ VERBOSE.fetchFrom(params),
+ DROP_IN_RESOURCES_ROOT.fetchFrom(params));
+
+ prepareLicense(params);
+
+ // In theory we need to extract name from results of attach command
+ // However, this will be a problem for customization as name will
+ // possibly change every time and developer will not be able to fix it
+ // As we are using tmp dir chance we get "different" name are low =>
+ // Use fixed name we used for bundle
+ prepareDMGSetupScript(APP_NAME.fetchFrom(params), params);
+
+ return true;
+ }
+
+ // name of post-image script
+ private File getConfig_Script(Map<String, ? super Object> params) {
+ return new File(CONFIG_ROOT.fetchFrom(params),
+ APP_NAME.fetchFrom(params) + "-post-image.sh");
+ }
+
+ // Location of SetFile utility may be different depending on MacOS version
+ // We look for several known places and if none of them work will
+ // try ot find it
+ private String findSetFileUtility() {
+ String typicalPaths[] = {"/Developer/Tools/SetFile",
+ "/usr/bin/SetFile", "/Developer/usr/bin/SetFile"};
+
+ for (String path: typicalPaths) {
+ File f = new File(path);
+ if (f.exists() && f.canExecute()) {
+ return path;
+ }
+ }
+
+ // generic find attempt
+ try {
+ ProcessBuilder pb = new ProcessBuilder("xcrun", "-find", "SetFile");
+ Process p = pb.start();
+ InputStreamReader isr = new InputStreamReader(p.getInputStream());
+ BufferedReader br = new BufferedReader(isr);
+ String lineRead = br.readLine();
+ if (lineRead != null) {
+ File f = new File(lineRead);
+ if (f.exists() && f.canExecute()) {
+ return f.getAbsolutePath();
+ }
+ }
+ } catch (IOException ignored) {}
+
+ return null;
+ }
+
+ private File buildDMG(
+ Map<String, ? super Object> p, File outdir)
+ throws IOException {
+ File imagesRoot = IMAGES_ROOT.fetchFrom(p);
+ if (!imagesRoot.exists()) imagesRoot.mkdirs();
+
+ File protoDMG = new File(imagesRoot, APP_NAME.fetchFrom(p) +"-tmp.dmg");
+ File finalDMG = new File(outdir, INSTALLER_NAME.fetchFrom(p)
+ + INSTALLER_SUFFIX.fetchFrom(p)
+ + ".dmg");
+
+ File srcFolder = APP_IMAGE_BUILD_ROOT.fetchFrom(p);
+ File predefinedImage = StandardBundlerParam.getPredefinedAppImage(p);
+ if (predefinedImage != null) {
+ srcFolder = predefinedImage;
+ }
+
+ Log.verbose(MessageFormat.format(I18N.getString(
+ "message.creating-dmg-file"), finalDMG.getAbsolutePath()));
+
+ protoDMG.delete();
+ if (finalDMG.exists() && !finalDMG.delete()) {
+ throw new IOException(MessageFormat.format(I18N.getString(
+ "message.dmg-cannot-be-overwritten"),
+ finalDMG.getAbsolutePath()));
+ }
+
+ protoDMG.getParentFile().mkdirs();
+ finalDMG.getParentFile().mkdirs();
+
+ String hdiUtilVerbosityFlag = Log.isDebug() ? "-verbose" : "-quiet";
+
+ // create temp image
+ ProcessBuilder pb = new ProcessBuilder(
+ hdiutil,
+ "create",
+ hdiUtilVerbosityFlag,
+ "-srcfolder", srcFolder.getAbsolutePath(),
+ "-volname", APP_NAME.fetchFrom(p),
+ "-ov", protoDMG.getAbsolutePath(),
+ "-fs", "HFS+",
+ "-format", "UDRW");
+ IOUtils.exec(pb, false);
+
+ // mount temp image
+ pb = new ProcessBuilder(
+ hdiutil,
+ "attach",
+ protoDMG.getAbsolutePath(),
+ hdiUtilVerbosityFlag,
+ "-mountroot", imagesRoot.getAbsolutePath());
+ IOUtils.exec(pb, false);
+
+ File mountedRoot =
+ new File(imagesRoot.getAbsolutePath(), APP_NAME.fetchFrom(p));
+
+ // volume icon
+ File volumeIconFile = new File(mountedRoot, ".VolumeIcon.icns");
+ IOUtils.copyFile(getConfig_VolumeIcon(p),
+ volumeIconFile);
+
+ pb = new ProcessBuilder("osascript",
+ getConfig_VolumeScript(p).getAbsolutePath());
+ IOUtils.exec(pb, false);
+
+ // Indicate that we want a custom icon
+ // NB: attributes of the root directory are ignored
+ // when creating the volume
+ // Therefore we have to do this after we mount image
+ String setFileUtility = findSetFileUtility();
+ if (setFileUtility != null) {
+ //can not find utility => keep going without icon
+ try {
+ volumeIconFile.setWritable(true);
+ // The "creator" attribute on a file is a legacy attribute
+ // but it seems Finder excepts these bytes to be
+ // "icnC" for the volume icon
+ // http://endrift.com/blog/2010/06/14/dmg-files-volume-icons-cli
+ // (might not work on Mac 10.13 with old XCode)
+ pb = new ProcessBuilder(
+ setFileUtility,
+ "-c", "icnC",
+ volumeIconFile.getAbsolutePath());
+ IOUtils.exec(pb, false);
+ volumeIconFile.setReadOnly();
+
+ pb = new ProcessBuilder(
+ setFileUtility,
+ "-a", "C",
+ mountedRoot.getAbsolutePath());
+ IOUtils.exec(pb, false);
+ } catch (IOException ex) {
+ Log.info(ex.getMessage());
+ Log.verbose(
+ "Cannot enable custom icon using SetFile utility");
+ }
+ } else {
+ Log.verbose(
+ "Skip enabling custom icon as SetFile utility is not found");
+ }
+
+ // Detach the temporary image
+ pb = new ProcessBuilder(
+ hdiutil,
+ "detach",
+ hdiUtilVerbosityFlag,
+ mountedRoot.getAbsolutePath());
+ IOUtils.exec(pb, false);
+
+ // Compress it to a new image
+ pb = new ProcessBuilder(
+ hdiutil,
+ "convert",
+ protoDMG.getAbsolutePath(),
+ hdiUtilVerbosityFlag,
+ "-format", "UDZO",
+ "-o", finalDMG.getAbsolutePath());
+ IOUtils.exec(pb, false);
+
+ //add license if needed
+ if (getConfig_LicenseFile(p).exists()) {
+ //hdiutil unflatten your_image_file.dmg
+ pb = new ProcessBuilder(
+ hdiutil,
+ "unflatten",
+ finalDMG.getAbsolutePath()
+ );
+ IOUtils.exec(pb, false);
+
+ //add license
+ pb = new ProcessBuilder(
+ hdiutil,
+ "udifrez",
+ finalDMG.getAbsolutePath(),
+ "-xml",
+ getConfig_LicenseFile(p).getAbsolutePath()
+ );
+ IOUtils.exec(pb, false);
+
+ //hdiutil flatten your_image_file.dmg
+ pb = new ProcessBuilder(
+ hdiutil,
+ "flatten",
+ finalDMG.getAbsolutePath()
+ );
+ IOUtils.exec(pb, false);
+
+ }
+
+ //Delete the temporary image
+ protoDMG.delete();
+
+ Log.info(MessageFormat.format(I18N.getString(
+ "message.output-to-location"),
+ APP_NAME.fetchFrom(p), finalDMG.getAbsolutePath()));
+
+ return finalDMG;
+ }
+
+
+ //////////////////////////////////////////////////////////////////////////
+ // Implement Bundler
+ //////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public String getName() {
+ return I18N.getString("bundler.name");
+ }
+
+ @Override
+ public String getDescription() {
+ return I18N.getString("bundler.description");
+ }
+
+ @Override
+ public String getID() {
+ return "dmg";
+ }
+
+ @Override
+ public Collection<BundlerParamInfo<?>> getBundleParameters() {
+ Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
+ results.addAll(MacAppBundler.getAppBundleParameters());
+ results.addAll(getDMGBundleParameters());
+ return results;
+ }
+
+ public Collection<BundlerParamInfo<?>> getDMGBundleParameters() {
+ Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
+
+ results.addAll(MacAppBundler.getAppBundleParameters());
+ results.addAll(Arrays.asList(
+ INSTALLER_SUFFIX,
+ LICENSE_FILE
+ ));
+
+ return results;
+ }
+
+
+ @Override
+ public boolean validate(Map<String, ? super Object> params)
+ throws UnsupportedPlatformException, ConfigException {
+ try {
+ if (params == null) throw new ConfigException(
+ I18N.getString("error.parameters-null"),
+ I18N.getString("error.parameters-null.advice"));
+
+ //run basic validation to ensure requirements are met
+ //we are not interested in return code, only possible exception
+ validateAppImageAndBundeler(params);
+
+ // validate license file, if used, exists in the proper place
+ if (params.containsKey(LICENSE_FILE.getID())) {
+ List<RelativeFileSet> appResourcesList =
+ APP_RESOURCES_LIST.fetchFrom(params);
+ for (String license : LICENSE_FILE.fetchFrom(params)) {
+ boolean found = false;
+ for (RelativeFileSet appResources : appResourcesList) {
+ found = found || appResources.contains(license);
+ }
+ if (!found) {
+ throw new ConfigException(
+ I18N.getString("error.license-missing"),
+ MessageFormat.format(I18N.getString(
+ "error.license-missing.advice"), license));
+ }
+ }
+ }
+
+ return true;
+ } catch (RuntimeException re) {
+ if (re.getCause() instanceof ConfigException) {
+ throw (ConfigException) re.getCause();
+ } else {
+ throw new ConfigException(re);
+ }
+ }
+ }
+
+ @Override
+ public File execute(
+ Map<String, ? super Object> params, File outputParentDir) {
+ return bundle(params, outputParentDir);
+ }
+
+ @Override
+ public boolean supported() {
+ return Platform.getPlatform() == Platform.MAC;
+ }
+}