--- old/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java 2019-11-18 20:43:37.929845100 -0500 +++ /dev/null 2019-11-18 20:43:39.000000000 -0500 @@ -1,579 +0,0 @@ -/* - * Copyright (c) 2014, 2019, 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 java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.PrintStream; -import java.io.PrintWriter; -import java.io.Writer; -import java.net.URLEncoder; -import java.nio.file.Files; -import java.nio.file.Path; -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.jpackage.internal.StandardBundlerParam.*; -import static jdk.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEYCHAIN; -import static jdk.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEY_USER; - -public class MacPkgBundler extends MacBaseInstallerBundler { - - private static final ResourceBundle I18N = ResourceBundle.getBundle( - "jdk.jpackage.internal.resources.MacResources"); - - 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<>( - "mac.pkg.packagesRoot", - File.class, - params -> { - File packagesRoot = - new File(TEMP_ROOT.fetchFrom(params), "packages"); - packagesRoot.mkdirs(); - return packagesRoot; - }, - (s, p) -> new File(s)); - - - protected final BundlerParamInfo SCRIPTS_DIR = - new StandardBundlerParam<>( - "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<>( - "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<>( - "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<> ( - "mac.pkg.installerName.suffix", - String.class, - params -> "", - (s, p) -> s); - - public File bundle(Map params, - File outdir) throws PackagerException { - Log.verbose(MessageFormat.format(I18N.getString("message.building-pkg"), - APP_NAME.fetchFrom(params))); - if (!outdir.isDirectory() && !outdir.mkdirs()) { - throw new PackagerException( - "error.cannot-create-output-dir", - outdir.getAbsolutePath()); - } - if (!outdir.canWrite()) { - throw new PackagerException( - "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); - throw new PackagerException(ex); - } - } - - private File getPackages_AppPackage(Map params) { - return new File(PACKAGES_ROOT.fetchFrom(params), - APP_NAME.fetchFrom(params) + "-app.pkg"); - } - - private File getPackages_DaemonPackage(Map params) { - return new File(PACKAGES_ROOT.fetchFrom(params), - APP_NAME.fetchFrom(params) + "-daemon.pkg"); - } - - 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 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("INSTALL_LOCATION", MAC_INSTALL_DIR.fetchFrom(params)); - - Writer w = new BufferedWriter( - new FileWriter(getScripts_PreinstallFile(params))); - String content = preprocessTextResource( - getScripts_PreinstallFile(params).getName(), - I18N.getString("resource.pkg-preinstall-script"), - TEMPLATE_PREINSTALL_SCRIPT, - data, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - w.write(content); - w.close(); - getScripts_PreinstallFile(params).setExecutable(true, false); - - w = new BufferedWriter( - new FileWriter(getScripts_PostinstallFile(params))); - content = preprocessTextResource( - getScripts_PostinstallFile(params).getName(), - I18N.getString("resource.pkg-postinstall-script"), - TEMPLATE_POSTINSTALL_SCRIPT, - data, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.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(""); - - String licFileStr = LICENSE_FILE.fetchFrom(params); - if (licFileStr != null) { - File licFile = new File(licFileStr); - out.println(""); - } - - /* - * Note that the content of the distribution file - * below is generated by productbuild --synthesize - */ - - String appId = getAppIdentifier(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(imageTarget.getName(), - I18N.getString("resource.pkg-background-image"), - DEFAULT_BACKGROUND_IMAGE, - imageTarget, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - - prepareDistributionXMLFile(params); - - fetchResource(getConfig_Script(params).getName(), - I18N.getString("resource.post-install-script"), - (String) null, - getConfig_Script(params), - VERBOSE.fetchFrom(params), - RESOURCE_DIR.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 void patchCPLFile(File cpl) throws IOException { - String cplData = Files.readString(cpl.toPath()); - String[] lines = cplData.split("\n"); - try (PrintWriter out = new PrintWriter(new BufferedWriter( - new FileWriter(cpl)))) { - boolean skip = false; // Used to skip Java.runtime bundle, since - // pkgbuild with --root will find two bundles app and Java runtime. - // We cannot generate component proprty list when using - // --component argument. - for (int i = 0; i < lines.length; i++) { - if (lines[i].trim().equals("BundleIsRelocatable")) { - out.println(lines[i]); - out.println(""); - i++; - } else if (lines[i].trim().equals("ChildBundles")) { - skip = true; - } else if (skip && lines[i].trim().equals("")) { - skip = false; - } else { - if (!skip) { - out.println(lines[i]); - } - } - } - } - } - - // pkgbuild includes all components from "--root" and subfolders, - // so if we have app image in folder which contains other images, then they - // will be included as well. It does have "--filter" option which use regex - // to exclude files/folder, but it will overwrite default one which excludes - // based on doc "any .svn or CVS directories, and any .DS_Store files". - // So easy aproach will be to copy user provided app-image into temp folder - // if root path contains other files. - private String getRoot(Map params, - File appLocation) throws IOException { - String root = appLocation.getParent() == null ? - "." : appLocation.getParent(); - File rootDir = new File(root); - File[] list = rootDir.listFiles(); - if (list != null) { // Should not happend - // We should only have app image and/or .DS_Store - if (list.length == 1) { - return root; - } else if (list.length == 2) { - // Check case with app image and .DS_Store - if (list[0].toString().toLowerCase().endsWith(".ds_store") || - list[1].toString().toLowerCase().endsWith(".ds_store")) { - return root; // Only app image and .DS_Store - } - } - } - - // Copy to new root - Path newRoot = Files.createTempDirectory( - TEMP_ROOT.fetchFrom(params).toPath(), - "root-"); - - IOUtils.copyRecursive(appLocation.toPath(), - newRoot.resolve(appLocation.getName())); - - return newRoot.toString(); - } - - private File createPKG(Map params, - File outdir, File appLocation) { - // generic find attempt - try { - File appPKG = getPackages_AppPackage(params); - - String root = getRoot(params, appLocation); - - // Generate default CPL file - File cpl = new File(CONFIG_ROOT.fetchFrom(params).getAbsolutePath() - + File.separator + "cpl.plist"); - ProcessBuilder pb = new ProcessBuilder("pkgbuild", - "--root", - root, - "--install-location", - MAC_INSTALL_DIR.fetchFrom(params), - "--analyze", - cpl.getAbsolutePath()); - - IOUtils.exec(pb, false); - - patchCPLFile(cpl); - - preparePackageScripts(params); - - // build application package - pb = new ProcessBuilder("pkgbuild", - "--root", - root, - "--install-location", - MAC_INSTALL_DIR.fetchFrom(params), - "--component-plist", - cpl.getAbsolutePath(), - "--scripts", - SCRIPTS_DIR.fetchFrom(params).getAbsolutePath(), - 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; - } - } - - ////////////////////////////////////////////////////////////////////////// - // Implement Bundler - ////////////////////////////////////////////////////////////////////////// - - @Override - public String getName() { - return I18N.getString("pkg.bundler.name"); - } - - @Override - public String getDescription() { - return I18N.getString("pkg.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); - - // 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) throws PackagerException { - return bundle(params, outputParentDir); - } - - @Override - public boolean supported(boolean runtimeInstaller) { - return Platform.getPlatform() == Platform.MAC; - } - -} --- /dev/null 2019-11-18 20:43:39.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacPkgBundler.java 2019-11-18 20:43:34.608535400 -0500 @@ -0,0 +1,555 @@ +/* + * Copyright (c) 2014, 2019, 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.incubator.jpackage.internal; + +import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.*; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; +import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEYCHAIN; +import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEY_USER; +import static jdk.incubator.jpackage.internal.MacAppImageBuilder.MAC_CF_BUNDLE_IDENTIFIER; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; + +public class MacPkgBundler extends MacBaseInstallerBundler { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MacResources"); + + 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<>( + "mac.pkg.packagesRoot", + File.class, + params -> { + File packagesRoot = + new File(TEMP_ROOT.fetchFrom(params), "packages"); + packagesRoot.mkdirs(); + return packagesRoot; + }, + (s, p) -> new File(s)); + + + protected final BundlerParamInfo SCRIPTS_DIR = + new StandardBundlerParam<>( + "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<>( + "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); + + 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<>( + "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<> ( + "mac.pkg.installerName.suffix", + String.class, + params -> "", + (s, p) -> s); + + public File bundle(Map params, + File outdir) throws PackagerException { + Log.verbose(MessageFormat.format(I18N.getString("message.building-pkg"), + APP_NAME.fetchFrom(params))); + + IOUtils.writableOutputDir(outdir.toPath()); + + try { + File appImageDir = prepareAppBundle(params); + + 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); + } + + return createPKG(params, outdir, appImageDir); + } + return null; + } catch (IOException ex) { + Log.verbose(ex); + throw new PackagerException(ex); + } + } + + private File getPackages_AppPackage(Map params) { + return new File(PACKAGES_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-app.pkg"); + } + + 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 getConfig_BackgroundImageDarkAqua(Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-background-darkAqua.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 String getAppIdentifier(Map params) { + return MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params); + } + + private void preparePackageScripts(Map params) + throws IOException { + Log.verbose(I18N.getString("message.preparing-scripts")); + + Map data = new HashMap<>(); + + Path appLocation = Path.of(MAC_INSTALL_DIR.fetchFrom(params), + APP_NAME.fetchFrom(params) + ".app", "Contents", "app"); + + data.put("INSTALL_LOCATION", MAC_INSTALL_DIR.fetchFrom(params)); + data.put("APP_LOCATION", appLocation.toString()); + + createResource(TEMPLATE_PREINSTALL_SCRIPT, params) + .setCategory(I18N.getString("resource.pkg-preinstall-script")) + .setSubstitutionData(data) + .saveToFile(getScripts_PreinstallFile(params)); + getScripts_PreinstallFile(params).setExecutable(true, false); + + createResource(TEMPLATE_POSTINSTALL_SCRIPT, params) + .setCategory(I18N.getString("resource.pkg-postinstall-script")) + .setSubstitutionData(data) + .saveToFile(getScripts_PostinstallFile(params)); + getScripts_PostinstallFile(params).setExecutable(true, false); + } + + private static String URLEncoding(String pkgName) throws URISyntaxException { + URI uri = new URI(null, null, pkgName, null); + return uri.toASCIIString(); + } + + private void prepareDistributionXMLFile(Map params) + throws IOException { + File f = getConfig_DistributionXMLFile(params); + + Log.verbose(MessageFormat.format(I18N.getString( + "message.preparing-distribution-dist"), f.getAbsolutePath())); + + IOUtils.createXml(f.toPath(), xml -> { + xml.writeStartElement("installer-gui-script"); + xml.writeAttribute("minSpecVersion", "1"); + + xml.writeStartElement("title"); + xml.writeCharacters(APP_NAME.fetchFrom(params)); + xml.writeEndElement(); + + xml.writeStartElement("background"); + xml.writeAttribute("file", getConfig_BackgroundImage(params).getName()); + xml.writeAttribute("mime-type", "image/png"); + xml.writeAttribute("alignment", "bottomleft"); + xml.writeAttribute("scaling", "none"); + xml.writeEndElement(); + + xml.writeStartElement("background-darkAqua"); + xml.writeAttribute("file", getConfig_BackgroundImageDarkAqua(params).getName()); + xml.writeAttribute("mime-type", "image/png"); + xml.writeAttribute("alignment", "bottomleft"); + xml.writeAttribute("scaling", "none"); + xml.writeEndElement(); + + String licFileStr = LICENSE_FILE.fetchFrom(params); + if (licFileStr != null) { + File licFile = new File(licFileStr); + xml.writeStartElement("license"); + xml.writeAttribute("file", licFile.getAbsolutePath()); + xml.writeAttribute("mime-type", "text/rtf"); + xml.writeEndElement(); + } + + /* + * Note that the content of the distribution file + * below is generated by productbuild --synthesize + */ + String appId = getAppIdentifier(params); + + xml.writeStartElement("pkg-ref"); + xml.writeAttribute("id", appId); + xml.writeEndElement(); // + xml.writeStartElement("options"); + xml.writeAttribute("customize", "never"); + xml.writeAttribute("require-scripts", "false"); + xml.writeEndElement(); // + xml.writeStartElement("choices-outline"); + xml.writeStartElement("line"); + xml.writeAttribute("choice", "default"); + xml.writeStartElement("line"); + xml.writeAttribute("choice", appId); + xml.writeEndElement(); // + xml.writeEndElement(); // + xml.writeEndElement(); // + xml.writeStartElement("choice"); + xml.writeAttribute("id", "default"); + xml.writeEndElement(); // + xml.writeStartElement("choice"); + xml.writeAttribute("id", appId); + xml.writeAttribute("visible", "false"); + xml.writeStartElement("pkg-ref"); + xml.writeAttribute("id", appId); + xml.writeEndElement(); // + xml.writeEndElement(); // + xml.writeStartElement("pkg-ref"); + xml.writeAttribute("id", appId); + xml.writeAttribute("version", VERSION.fetchFrom(params)); + xml.writeAttribute("onConclusion", "none"); + try { + xml.writeCharacters(URLEncoding( + getPackages_AppPackage(params).getName())); + } catch (URISyntaxException ex) { + throw new IOException(ex); + } + xml.writeEndElement(); // + + xml.writeEndElement(); // + }); + } + + private boolean prepareConfigFiles(Map params) + throws IOException { + + createResource(DEFAULT_BACKGROUND_IMAGE, params) + .setCategory(I18N.getString("resource.pkg-background-image")) + .saveToFile(getConfig_BackgroundImage(params)); + + createResource(DEFAULT_BACKGROUND_IMAGE, params) + .setCategory(I18N.getString("resource.pkg-background-image")) + .saveToFile(getConfig_BackgroundImageDarkAqua(params)); + + prepareDistributionXMLFile(params); + + createResource(null, params) + .setCategory(I18N.getString("resource.post-install-script")) + .saveToFile(getConfig_Script(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 void patchCPLFile(File cpl) throws IOException { + String cplData = Files.readString(cpl.toPath()); + String[] lines = cplData.split("\n"); + try (PrintWriter out = new PrintWriter(Files.newBufferedWriter( + cpl.toPath()))) { + int skip = 0; + // Used to skip Java.runtime bundle, since + // pkgbuild with --root will find two bundles app and Java runtime. + // We cannot generate component proprty list when using + // --component argument. + for (int i = 0; i < lines.length; i++) { + if (lines[i].trim().equals("BundleIsRelocatable")) { + out.println(lines[i]); + out.println(""); + i++; + } else if (lines[i].trim().equals("ChildBundles")) { + ++skip; + } else if ((skip > 0) && lines[i].trim().equals("")) { + --skip; + } else { + if (skip == 0) { + out.println(lines[i]); + } + } + } + } + } + + // pkgbuild includes all components from "--root" and subfolders, + // so if we have app image in folder which contains other images, then they + // will be included as well. It does have "--filter" option which use regex + // to exclude files/folder, but it will overwrite default one which excludes + // based on doc "any .svn or CVS directories, and any .DS_Store files". + // So easy aproach will be to copy user provided app-image into temp folder + // if root path contains other files. + private String getRoot(Map params, + File appLocation) throws IOException { + String root = appLocation.getParent() == null ? + "." : appLocation.getParent(); + File rootDir = new File(root); + File[] list = rootDir.listFiles(); + if (list != null) { // Should not happend + // We should only have app image and/or .DS_Store + if (list.length == 1) { + return root; + } else if (list.length == 2) { + // Check case with app image and .DS_Store + if (list[0].toString().toLowerCase().endsWith(".ds_store") || + list[1].toString().toLowerCase().endsWith(".ds_store")) { + return root; // Only app image and .DS_Store + } + } + } + + // Copy to new root + Path newRoot = Files.createTempDirectory( + TEMP_ROOT.fetchFrom(params).toPath(), + "root-"); + + IOUtils.copyRecursive(appLocation.toPath(), + newRoot.resolve(appLocation.getName())); + + return newRoot.toString(); + } + + private File createPKG(Map params, + File outdir, File appLocation) { + // generic find attempt + try { + File appPKG = getPackages_AppPackage(params); + + String root = getRoot(params, appLocation); + + // Generate default CPL file + File cpl = new File(CONFIG_ROOT.fetchFrom(params).getAbsolutePath() + + File.separator + "cpl.plist"); + ProcessBuilder pb = new ProcessBuilder("pkgbuild", + "--root", + root, + "--install-location", + MAC_INSTALL_DIR.fetchFrom(params), + "--analyze", + cpl.getAbsolutePath()); + + IOUtils.exec(pb); + + patchCPLFile(cpl); + + preparePackageScripts(params); + + // build application package + pb = new ProcessBuilder("pkgbuild", + "--root", + root, + "--install-location", + MAC_INSTALL_DIR.fetchFrom(params), + "--component-plist", + cpl.getAbsolutePath(), + "--scripts", + SCRIPTS_DIR.fetchFrom(params).getAbsolutePath(), + appPKG.getAbsolutePath()); + IOUtils.exec(pb); + + // 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); + + return finalPKG; + } catch (Exception ignored) { + Log.verbose(ignored); + return null; + } + } + + ////////////////////////////////////////////////////////////////////////// + // Implement Bundler + ////////////////////////////////////////////////////////////////////////// + + @Override + public String getName() { + return I18N.getString("pkg.bundler.name"); + } + + @Override + public String getID() { + return "pkg"; + } + + @Override + public boolean validate(Map params) + throws ConfigException { + try { + Objects.requireNonNull(params); + + // run basic validation to ensure requirements are met + // we are not interested in return code, only possible exception + validateAppImageAndBundeler(params); + + if (MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) == null) { + throw new ConfigException( + I18N.getString("message.app-image-requires-identifier"), + I18N.getString( + "message.app-image-requires-identifier.advice")); + } + + // 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) throws PackagerException { + return bundle(params, outputParentDir); + } + + @Override + public boolean supported(boolean runtimeInstaller) { + return true; + } + + @Override + public boolean isDefault() { + return false; + } + +}