--- /dev/null 2018-11-09 14:35:01.000000000 -0500 +++ new/src/jdk.jpackager/macosx/classes/jdk/jpackager/internal/mac/MacPkgBundler.java 2018-11-09 14:35:00.011149900 -0500 @@ -0,0 +1,603 @@ +/* + * Copyright (c) 2014, 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.BundlerParamInfo; +import jdk.jpackager.internal.StandardBundlerParam; +import jdk.jpackager.internal.Log; +import jdk.jpackager.internal.ConfigException; +import jdk.jpackager.internal.IOUtils; +import jdk.jpackager.internal.Platform; +import jdk.jpackager.internal.RelativeFileSet; +import jdk.jpackager.internal.UnsupportedPlatformException; +import jdk.jpackager.internal.resources.mac.MacResources; +import jdk.jpackager.internal.builders.mac.MacAppImageBuilder; +import jdk.jpackager.internal.Arguments; + +import java.io.BufferedWriter; +import java.io.File; +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 jdk.jpackager.internal.StandardBundlerParam.*; +import static + jdk.jpackager.internal.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN; +import static + jdk.jpackager.internal.mac.MacBaseInstallerBundler.SIGNING_KEY_USER; + +public class MacPkgBundler extends MacBaseInstallerBundler { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.jpackager.internal.resources.mac.MacPkgBundler"); + + 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.error(MessageFormat.format( + I18N.getString("error.certificate.expired"), + result)); + } + } + + return result; + }, + (s, p) -> s); + + public static final BundlerParamInfo MAC_INSTALL_DIR = + new StandardBundlerParam<>( + I18N.getString("param.mac-install-dir.name"), + I18N.getString("param.mac-install-dir.description"), + "mac-install-dir", + String.class, + params -> { + String dir = INSTALL_DIR.fetchFrom(params); + return (dir != null) ? dir : "/Applications"; + }, + (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; + } + + public File bundle(Map params, File outdir) { + Log.verbose(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, false); + + if (appImageDir != 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 createPKG(params, outdir, appImageDir); + } + 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.verbose(MessageFormat.format(I18N.getString( + "message.intermediate-image-location"), + appImageDir.getAbsolutePath())); + } + + // cleanup + cleanupConfigFiles(params); + } 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 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(""); + + out.println(""); + out.println(""); + out.println(" "); + out.println(" "); + out.println(" "); + out.println(""); + out.println(""); + out.println(""); + out.println(" "); + out.println(""); + out.println("" + + URLEncoder.encode(getPackages_AppPackage(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 { + File appPKG = getPackages_AppPackage(params); + + // build application package + ProcessBuilder pb = new ProcessBuilder("pkgbuild", + "--component", + appLocation.toString(), + "--install-location", + MAC_INSTALL_DIR.fetchFrom(params), + appPKG.getAbsolutePath()); + IOUtils.exec(pb, false); + + // 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(MacAppImageBuilder. + SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { + if (Platform.getMajorVersion() > 10 || + (Platform.getMajorVersion() == 10 && + Platform.getMinorVersion() >= 12)) { + // we need this for OS X 10.12+ + Log.verbose(I18N.getString("message.signing.pkg")); + } + + 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, false); + + return finalPKG; + } catch (Exception ignored) { + Log.verbose(ignored); + return null; + } finally { + cleanupPackagesFiles(params); + cleanupConfigFiles(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(MacAppImageBuilder. + 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); + } + + @Override + public boolean supported() { + return Platform.getPlatform() == Platform.MAC; + } + +}