/* * Copyright (c) 2014, 2016, 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 com.oracle.tools.packager.mac; import com.oracle.tools.packager.BundlerParamInfo; import com.oracle.tools.packager.StandardBundlerParam; import com.oracle.tools.packager.Log; import com.oracle.tools.packager.ConfigException; import com.oracle.tools.packager.IOUtils; import com.oracle.tools.packager.RelativeFileSet; import com.oracle.tools.packager.UnsupportedPlatformException; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.io.PrintStream; import java.io.Writer; import java.net.URLEncoder; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; import static com.oracle.tools.packager.StandardBundlerParam.*; import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN; import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEY_USER; import jdk.packager.internal.legacy.mac.MacCertificate; public class MacPkgBundler extends MacBaseInstallerBundler { private static final ResourceBundle I18N = ResourceBundle.getBundle(MacPkgBundler.class.getName()); public final static String MAC_BUNDLER_PREFIX = BUNDLER_PREFIX + "macosx" + File.separator; private static final String DEFAULT_BACKGROUND_IMAGE = "background_pkg.png"; private static final String TEMPLATE_PREINSTALL_SCRIPT = "preinstall.template"; private static final String TEMPLATE_POSTINSTALL_SCRIPT = "postinstall.template"; private static final BundlerParamInfo PACKAGES_ROOT = new StandardBundlerParam<>( I18N.getString("param.packages-root.name"), I18N.getString("param.packages-root.description"), "mac.pkg.packagesRoot", File.class, params -> { File packagesRoot = new File(BUILD_ROOT.fetchFrom(params), "packages"); packagesRoot.mkdirs(); return packagesRoot; }, (s, p) -> new File(s)); protected final BundlerParamInfo SCRIPTS_DIR = new StandardBundlerParam<>( I18N.getString("param.scripts-dir.name"), I18N.getString("param.scripts-dir.description"), "mac.pkg.scriptsDir", File.class, params -> { File scriptsDir = new File(CONFIG_ROOT.fetchFrom(params), "scripts"); scriptsDir.mkdirs(); return scriptsDir; }, (s, p) -> new File(s)); public static final BundlerParamInfo DEVELOPER_ID_INSTALLER_SIGNING_KEY = new StandardBundlerParam<>( I18N.getString("param.signing-key-developer-id-installer.name"), I18N.getString("param.signing-key-developer-id-installer.description"), "mac.signing-key-developer-id-installer", String.class, params -> { String result = MacBaseInstallerBundler.findKey("Developer ID Installer: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)); if (result != null) { MacCertificate certificate = new MacCertificate(result, VERBOSE.fetchFrom(params)); if (!certificate.isValid()) { Log.info(MessageFormat.format(I18N.getString("error.certificate.expired"), result)); } } return result; }, (s, p) -> s); public static final BundlerParamInfo INSTALLER_SUFFIX = new StandardBundlerParam<> ( I18N.getString("param.installer-suffix.name"), I18N.getString("param.installer-suffix.description"), "mac.pkg.installerName.suffix", String.class, params -> "", (s, p) -> s); public MacPkgBundler() { super(); baseResourceLoader = MacResources.class; } //@Override public File bundle(Map params, File outdir) { Log.info(MessageFormat.format(I18N.getString("message.building-pkg"), 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 = null; try { appImageDir = prepareAppBundle(params); if (appImageDir != null && prepareConfigFiles(params)) { if (SERVICE_HINT.fetchFrom(params)) { File daemonImageDir = DAEMON_IMAGE_BUILD_ROOT.fetchFrom(params); daemonImageDir.mkdirs(); prepareDaemonBundle(params); } File configScript = getConfig_Script(params); if (configScript.exists()) { Log.info(MessageFormat.format(I18N.getString("message.running-script"), configScript.getAbsolutePath())); IOUtils.run("bash", configScript, VERBOSE.fetchFrom(params)); } return createPKG(params, outdir, appImageDir); } return null; } catch (IOException ex) { Log.verbose(ex); return null; } finally { try { if (appImageDir != null && !Log.isDebug()) { IOUtils.deleteRecursive(appImageDir); } else if (appImageDir != null) { Log.info(MessageFormat.format(I18N.getString("message.intermediate-image-location"), appImageDir.getAbsolutePath())); } if (!VERBOSE.fetchFrom(params)) { //cleanup cleanupConfigFiles(params); } else { Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(params).getAbsolutePath())); } } catch (IOException ex) { Log.debug(ex); //noinspection ReturnInsideFinallyBlock return null; } } } private File getPackages_AppPackage(Map params) { return new File(PACKAGES_ROOT.fetchFrom(params), APP_FS_NAME.fetchFrom(params) + "-app.pkg"); } private File getPackages_DaemonPackage(Map params) { return new File(PACKAGES_ROOT.fetchFrom(params), APP_FS_NAME.fetchFrom(params) + "-daemon.pkg"); } private void cleanupPackagesFiles(Map params) { if (getPackages_AppPackage(params) != null) { getPackages_AppPackage(params).delete(); } if (getPackages_DaemonPackage(params) != null) { getPackages_DaemonPackage(params).delete(); } } private File getConfig_DistributionXMLFile(Map params) { return new File(CONFIG_ROOT.fetchFrom(params), "distribution.dist"); } private File getConfig_BackgroundImage(Map params) { return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-background.png"); } private File getScripts_PreinstallFile(Map params) { return new File(SCRIPTS_DIR.fetchFrom(params), "preinstall"); } private File getScripts_PostinstallFile(Map params) { return new File(SCRIPTS_DIR.fetchFrom(params), "postinstall"); } private void cleanupPackageScripts(Map params) { if (getScripts_PreinstallFile(params) != null) { getScripts_PreinstallFile(params).delete(); } if (getScripts_PostinstallFile(params) != null) { getScripts_PostinstallFile(params).delete(); } } private void cleanupConfigFiles(Map params) { if (getConfig_DistributionXMLFile(params) != null) { getConfig_DistributionXMLFile(params).delete(); } if (getConfig_BackgroundImage(params) != null) { getConfig_BackgroundImage(params).delete(); } } private String getAppIdentifier(Map params) { return IDENTIFIER.fetchFrom(params); } private String getDaemonIdentifier(Map params) { return IDENTIFIER.fetchFrom(params) + ".daemon"; } private void preparePackageScripts(Map params) throws IOException { Log.verbose(I18N.getString("message.preparing-scripts")); Map data = new HashMap<>(); data.put("DEPLOY_DAEMON_IDENTIFIER", getDaemonIdentifier(params)); data.put("DEPLOY_LAUNCHD_PLIST_FILE", IDENTIFIER.fetchFrom(params).toLowerCase() + ".launchd.plist"); Writer w = new BufferedWriter(new FileWriter(getScripts_PreinstallFile(params))); String content = preprocessTextResource( MAC_BUNDLER_PREFIX + getScripts_PreinstallFile(params).getName(), I18N.getString("resource.pkg-preinstall-script"), TEMPLATE_PREINSTALL_SCRIPT, data, VERBOSE.fetchFrom(params), DROP_IN_RESOURCES_ROOT.fetchFrom(params)); w.write(content); w.close(); getScripts_PreinstallFile(params).setExecutable(true, false); w = new BufferedWriter(new FileWriter(getScripts_PostinstallFile(params))); content = preprocessTextResource( MAC_BUNDLER_PREFIX + getScripts_PostinstallFile(params).getName(), I18N.getString("resource.pkg-postinstall-script"), TEMPLATE_POSTINSTALL_SCRIPT, data, VERBOSE.fetchFrom(params), DROP_IN_RESOURCES_ROOT.fetchFrom(params)); w.write(content); w.close(); getScripts_PostinstallFile(params).setExecutable(true, false); } private void prepareDistributionXMLFile(Map params) throws IOException { File f = getConfig_DistributionXMLFile(params); Log.verbose(MessageFormat.format(I18N.getString("message.preparing-distribution-dist"), f.getAbsolutePath())); PrintStream out = new PrintStream(f); out.println(""); out.println(""); out.println("" + APP_NAME.fetchFrom(params) + ""); out.println(""); if (!LICENSE_FILE.fetchFrom(params).isEmpty()) { File licFile = null; List 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; } } // this is NPE protection, validate should have caught it's absence // so we don't complain or throw an error if (licFile != null) { out.println(""); } } /* * Note that the content of the distribution file * below is generated by productbuild --synthesize */ String appId = getAppIdentifier(params); String daemonId = getDaemonIdentifier(params); out.println(""); if (SERVICE_HINT.fetchFrom(params)) { out.println(""); } out.println(""); out.println(""); out.println(" "); out.println(" "); if (SERVICE_HINT.fetchFrom(params)) { out.println(" "); } out.println(" "); out.println(""); out.println(""); out.println(""); out.println(" "); out.println(""); out.println("" + URLEncoder.encode(getPackages_AppPackage(params).getName(), "UTF-8") + ""); if (SERVICE_HINT.fetchFrom(params)) { out.println(""); out.println(" "); out.println(""); out.println("" + URLEncoder.encode(getPackages_DaemonPackage(params).getName(), "UTF-8") + ""); } out.println(""); out.close(); } private boolean prepareConfigFiles(Map params) throws IOException { File imageTarget = getConfig_BackgroundImage(params); fetchResource(MacAppBundler.MAC_BUNDLER_PREFIX + imageTarget.getName(), I18N.getString("resource.pkg-background-image"), DEFAULT_BACKGROUND_IMAGE, imageTarget, VERBOSE.fetchFrom(params), DROP_IN_RESOURCES_ROOT.fetchFrom(params)); prepareDistributionXMLFile(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)); return true; } //name of post-image script private File getConfig_Script(Map params) { return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-post-image.sh"); } private File createPKG(Map params, File outdir, File appLocation) { //generic find attempt try { String daemonLocation = DAEMON_IMAGE_BUILD_ROOT.fetchFrom(params) + "/" + APP_NAME.fetchFrom(params) + ".daemon"; File appPKG = getPackages_AppPackage(params); File daemonPKG = getPackages_DaemonPackage(params); // build application package ProcessBuilder pb = new ProcessBuilder("pkgbuild", "--component", appLocation.toString(), "--install-location", "/Applications", appPKG.getAbsolutePath()); IOUtils.exec(pb, VERBOSE.fetchFrom(params)); // build daemon package if requested if (SERVICE_HINT.fetchFrom(params)) { preparePackageScripts(params); pb = new ProcessBuilder("pkgbuild", "--identifier", APP_NAME.fetchFrom(params) + ".daemon", "--root", daemonLocation, "--scripts", SCRIPTS_DIR.fetchFrom(params).getAbsolutePath(), daemonPKG.getAbsolutePath()); IOUtils.exec(pb, VERBOSE.fetchFrom(params)); } // build final package File finalPKG = new File(outdir, INSTALLER_NAME.fetchFrom(params) + INSTALLER_SUFFIX.fetchFrom(params) + ".pkg"); outdir.mkdirs(); List commandLine = new ArrayList<>(); commandLine.add("productbuild"); commandLine.add("--resources"); commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath()); // maybe sign if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { String signingIdentity = DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params); if (signingIdentity != null) { commandLine.add("--sign"); commandLine.add(signingIdentity); } String keychainName = SIGNING_KEYCHAIN.fetchFrom(params); if (keychainName != null && !keychainName.isEmpty()) { commandLine.add("--keychain"); commandLine.add(keychainName); } } commandLine.add("--distribution"); commandLine.add(getConfig_DistributionXMLFile(params).getAbsolutePath()); commandLine.add("--package-path"); commandLine.add(PACKAGES_ROOT.fetchFrom(params).getAbsolutePath()); commandLine.add(finalPKG.getAbsolutePath()); pb = new ProcessBuilder(commandLine); IOUtils.exec(pb, VERBOSE.fetchFrom(params)); return finalPKG; } catch (Exception ignored) { Log.verbose(ignored); return null; } finally { if (!VERBOSE.fetchFrom(params)) { cleanupPackagesFiles(params); cleanupConfigFiles(params); if (SERVICE_HINT.fetchFrom(params)) { cleanupPackageScripts(params); } } } } ////////////////////////////////////////////////////////////////////////////////// // 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 "pkg"; } @Override public Collection> getBundleParameters() { Collection> results = new LinkedHashSet<>(); results.addAll(MacAppBundler.getAppBundleParameters()); results.addAll(getPKGBundleParameters()); return results; } public Collection> getPKGBundleParameters() { Collection> results = new LinkedHashSet<>(); results.addAll(MacAppBundler.getAppBundleParameters()); results.addAll(Arrays.asList( DEVELOPER_ID_INSTALLER_SIGNING_KEY, //IDENTIFIER, INSTALLER_SUFFIX, LICENSE_FILE, //SERVICE_HINT, SIGNING_KEYCHAIN)); return results; } @Override public boolean validate(Map 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 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)); } } } // reject explicitly set sign to true and no valid signature key if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) { String signingIdentity = DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params); if (signingIdentity == null) { throw new ConfigException( I18N.getString("error.explicit-sign-no-cert"), I18N.getString("error.explicit-sign-no-cert.advice")); } } // hdiutil is always available so there's no need to test for availability. return true; } catch (RuntimeException re) { if (re.getCause() instanceof ConfigException) { throw (ConfigException) re.getCause(); } else { throw new ConfigException(re); } } } @Override public File execute(Map params, File outputParentDir) { return bundle(params, outputParentDir); } }