src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java
branchJDK-8200758-branch
changeset 57039 98d3963b0b7b
parent 57038 b0f09e7c4680
child 57059 9bb2a4dc3af7
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java	Fri Nov 23 09:02:17 2018 -0500
@@ -0,0 +1,580 @@
+/*
+ * 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.jpackage.internal;
+
+import jdk.jpackage.internal.*;
+import jdk.jpackage.internal.IOUtils;
+import jdk.jpackage.internal.resources.MacResources;
+import jdk.jpackage.internal.Arguments;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.text.MessageFormat;
+import java.util.*;
+
+import static jdk.jpackage.internal.StandardBundlerParam.*;
+
+public class MacDmgBundler extends MacBaseInstallerBundler {
+
+    private static final ResourceBundle I18N =
+            ResourceBundle.getBundle(
+            "jdk.jpackage.internal.resources.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.verbose(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.verbose(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() &&
+                        !Log.isVerbose()) {
+                    IOUtils.deleteRecursive(appImageDir);
+                } else if (appImageDir != null) {
+                    Log.verbose(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 (Log.isDebug() || Log.isVerbose()) {
+            return;
+        }
+
+        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.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.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.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.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.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.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.error(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.verbose(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;
+    }
+}