# HG changeset patch # User herrick # Date 1571422477 14400 # Node ID 61c44899b4eb92018207791842aa18fb85226ef9 # Parent 64adf683bc7b1fa1922b460eb025d88c66d9a6c4 8223325: Improve wix sources generated by jpackage Submitted-by: asemenyuk Reviewed-by: aherrick, almatvee diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java Fri Oct 18 14:14:37 2019 -0400 @@ -31,7 +31,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.imageio.ImageIO; -import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import static jdk.jpackage.internal.LinuxAppBundler.ICON_PNG; @@ -353,15 +352,9 @@ } private void createFileAssociationsMimeInfoFile() throws IOException { - XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance(); - - try (Writer w = new BufferedWriter(new FileWriter( - mimeInfoFile.srcPath().toFile()))) { - XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w); - - xml.writeStartDocument(); + IOUtils.createXml(mimeInfoFile.srcPath(), xml -> { xml.writeStartElement("mime-info"); - xml.writeNamespace("xmlns", + xml.writeDefaultNamespace( "http://www.freedesktop.org/standards/shared-mime-info"); for (var assoc : associations) { @@ -369,14 +362,7 @@ } xml.writeEndElement(); - xml.writeEndDocument(); - xml.flush(); - xml.close(); - - } catch (XMLStreamException ex) { - Log.verbose(ex); - throw new IOException(ex); - } + }); } private Map createFileAssociationIconFiles() throws diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java Fri Oct 18 14:14:37 2019 -0400 @@ -26,7 +26,6 @@ package jdk.jpackage.internal; import java.io.*; -import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -140,9 +139,7 @@ try { String licenseFile = LICENSE_FILE.fetchFrom(params); if (licenseFile != null) { - return Files.lines(Path.of(licenseFile), - StandardCharsets.UTF_8).collect( - Collectors.joining("\n")); + return Files.readString(Path.of(licenseFile)); } } catch (IOException e) { Log.verbose(e); diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java Fri Oct 18 14:14:37 2019 -0400 @@ -25,12 +25,7 @@ package jdk.jpackage.internal; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.Writer; +import java.io.*; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Files; @@ -42,9 +37,6 @@ import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; import static jdk.jpackage.internal.StandardBundlerParam.*; import static jdk.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEYCHAIN; @@ -155,7 +147,7 @@ return createPKG(params, outdir, appImageDir); } return null; - } catch (IOException|URISyntaxException ex) { + } catch (IOException ex) { Log.verbose(ex); throw new PackagerException(ex); } @@ -219,25 +211,19 @@ getScripts_PostinstallFile(params).setExecutable(true, false); } - private String URLEncoding(String pkgName) throws URISyntaxException { + 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, URISyntaxException { + throws IOException { File f = getConfig_DistributionXMLFile(params); Log.verbose(MessageFormat.format(I18N.getString( "message.preparing-distribution-dist"), f.getAbsolutePath())); - XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance(); - - try (Writer w = new OutputStreamWriter(new FileOutputStream(f), "UTF-8")) { - XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w); - - xml.writeStartDocument("UTF-8", "1.0"); - + IOUtils.createXml(f.toPath(), xml -> { xml.writeStartElement("installer-gui-script"); xml.writeAttribute("minSpecVersion", "1"); @@ -303,24 +289,20 @@ xml.writeAttribute("id", appId); xml.writeAttribute("version", VERSION.fetchFrom(params)); xml.writeAttribute("onConclusion", "none"); - xml.writeCharacters(URLEncoding( - getPackages_AppPackage(params).getName())); + try { + xml.writeCharacters(URLEncoding( + getPackages_AppPackage(params).getName())); + } catch (URISyntaxException ex) { + throw new IOException(ex); + } xml.writeEndElement(); // xml.writeEndElement(); // - - xml.writeEndDocument(); - xml.flush(); - xml.close(); - - } catch (XMLStreamException ex) { - Log.verbose(ex); - throw new IOException(ex); - } + }); } private boolean prepareConfigFiles(Map params) - throws IOException, URISyntaxException { + throws IOException { createResource(DEFAULT_BACKGROUND_IMAGE, params) .setCategory(I18N.getString("resource.pkg-background-image")) diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java Fri Oct 18 14:14:37 2019 -0400 @@ -1,218 +1,233 @@ -/* - * Copyright (c) 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.FileInputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.ArrayList; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; -import org.w3c.dom.Document; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -import static jdk.jpackage.internal.StandardBundlerParam.*; - -class AppImageFile { - - // These values will be loaded from AppImage xml file. - private final String creatorVersion; - private final String creatorPlatform; - private final String launcherName; - private final List addLauncherNames; - - final static String XML_FILENAME = ".jpackage.xml"; - - private final static Map PLATFORM_LABELS = Map.of( - Platform.LINUX, "linux", Platform.WINDOWS, "windows", Platform.MAC, - "macOS"); - - - private AppImageFile() { - this(null, null, null, null); - } - - private AppImageFile(String launcherName, List addLauncherNames, - String creatorVersion, String creatorPlatform) { - this.launcherName = launcherName; - this.addLauncherNames = addLauncherNames; - this.creatorVersion = creatorVersion; - this.creatorPlatform = creatorPlatform; - } - - /** - * Would return null to indicate stored command line is invalid. - */ - List getAddLauncherNames() { - return addLauncherNames; - } - - String getLauncherName() { - return launcherName; - } - - void verifyCompatible() throws ConfigException { - // Just do nohing for now. - } - - static void save(Path appImage, Map params) - throws IOException { - Path xmlFile = appImage.resolve(XML_FILENAME); - XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance(); - - try (Writer w = new BufferedWriter(new FileWriter(xmlFile.toFile()))) { - XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w); - - xml.writeStartDocument(); - xml.writeStartElement("jpackage-state"); - xml.writeAttribute("version", getVersion()); - xml.writeAttribute("platform", getPlatform()); - - xml.writeStartElement("main-launcher"); - xml.writeCharacters(APP_NAME.fetchFrom(params)); - xml.writeEndElement(); - - List> addLaunchers = - ADD_LAUNCHERS.fetchFrom(params); - - for (int i = 0; i < addLaunchers.size(); i++) { - Map sl = addLaunchers.get(i); - xml.writeStartElement("add-launcher"); - xml.writeCharacters(APP_NAME.fetchFrom(sl)); - xml.writeEndElement(); - } - - xml.writeEndElement(); - xml.writeEndDocument(); - xml.flush(); - xml.close(); - - } catch (XMLStreamException ex) { - Log.verbose(ex); - throw new IOException(ex); - } - } - - static AppImageFile load(Path appImageDir) throws IOException { - try { - Path path = appImageDir.resolve(XML_FILENAME); - DocumentBuilderFactory dbf = - DocumentBuilderFactory.newDefaultInstance(); - dbf.setFeature( - "http://apache.org/xml/features/nonvalidating/load-external-dtd", - false); - DocumentBuilder b = dbf.newDocumentBuilder(); - Document doc = b.parse(new FileInputStream(path.toFile())); - - XPath xPath = XPathFactory.newInstance().newXPath(); - - String mainLauncher = xpathQueryNullable(xPath, - "/jpackage-state/main-launcher/text()", doc); - if (mainLauncher == null) { - // No main launcher, this is fatal. - return new AppImageFile(); - } - - List addLaunchers = new ArrayList(); - - String platform = xpathQueryNullable(xPath, - "/jpackage-state/@platform", doc); - - String version = xpathQueryNullable(xPath, - "/jpackage-state/@version", doc); - - NodeList launcherNameNodes = (NodeList) xPath.evaluate( - "/jpackage-state/add-launcher/text()", doc, - XPathConstants.NODESET); - - for (int i = 0; i != launcherNameNodes.getLength(); i++) { - addLaunchers.add(launcherNameNodes.item(i).getNodeValue()); - } - - AppImageFile file = new AppImageFile( - mainLauncher, addLaunchers, version, platform); - if (!file.isValid()) { - file = new AppImageFile(); - } - return file; - } catch (ParserConfigurationException | SAXException ex) { - // Let caller sort this out - throw new IOException(ex); - } catch (XPathExpressionException ex) { - // This should never happen as XPath expressions should be correct - throw new RuntimeException(ex); - } - } - - private static String xpathQueryNullable(XPath xPath, String xpathExpr, - Document xml) throws XPathExpressionException { - NodeList nodes = (NodeList) xPath.evaluate(xpathExpr, xml, - XPathConstants.NODESET); - if (nodes != null && nodes.getLength() > 0) { - return nodes.item(0).getNodeValue(); - } - return null; - } - - private static String getVersion() { - return System.getProperty("java.version"); - } - - private static String getPlatform() { - return PLATFORM_LABELS.get(Platform.getPlatform()); - } - - private boolean isValid() { - if (launcherName == null || launcherName.length() == 0 || - addLauncherNames.indexOf("") != -1) { - // Some launchers have empty names. This is invalid. - return false; - } - - // Add more validation. - - return true; - } - -} +/* + * Copyright (c) 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.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import static jdk.jpackage.internal.StandardBundlerParam.*; + +public class AppImageFile { + + // These values will be loaded from AppImage xml file. + private final String creatorVersion; + private final String creatorPlatform; + private final String launcherName; + private final List addLauncherNames; + + public final static String FILENAME = ".jpackage.xml"; + + private final static Map PLATFORM_LABELS = Map.of( + Platform.LINUX, "linux", Platform.WINDOWS, "windows", Platform.MAC, + "macOS"); + + + private AppImageFile() { + this(null, null, null, null); + } + + private AppImageFile(String launcherName, List addLauncherNames, + String creatorVersion, String creatorPlatform) { + this.launcherName = launcherName; + this.addLauncherNames = addLauncherNames; + this.creatorVersion = creatorVersion; + this.creatorPlatform = creatorPlatform; + } + + /** + * Returns list of additional launchers configured for the application. + * Each item in the list is not null or empty string. + * Returns empty list for application without additional launchers. + */ + List getAddLauncherNames() { + return addLauncherNames; + } + + /** + * Returns main application launcher name. Never returns null or empty value. + */ + String getLauncherName() { + return launcherName; + } + + void verifyCompatible() throws ConfigException { + // Just do nothing for now. + } + + /** + * Saves file with application image info in application image. + * @param appImageDir - path to application image + * @throws IOException + */ + static void save(Path appImage, Map params) + throws IOException { + IOUtils.createXml(appImage.resolve(FILENAME), xml -> { + xml.writeStartElement("jpackage-state"); + xml.writeAttribute("version", getVersion()); + xml.writeAttribute("platform", getPlatform()); + + xml.writeStartElement("main-launcher"); + xml.writeCharacters(APP_NAME.fetchFrom(params)); + xml.writeEndElement(); + + List> addLaunchers = + ADD_LAUNCHERS.fetchFrom(params); + + for (int i = 0; i < addLaunchers.size(); i++) { + Map sl = addLaunchers.get(i); + xml.writeStartElement("add-launcher"); + xml.writeCharacters(APP_NAME.fetchFrom(sl)); + xml.writeEndElement(); + } + }); + } + + /** + * Loads application image info from application image. + * @param appImageDir - path to application image + * @return valid info about application image or null + * @throws IOException + */ + static AppImageFile load(Path appImageDir) throws IOException { + try { + Path path = appImageDir.resolve(FILENAME); + DocumentBuilderFactory dbf = + DocumentBuilderFactory.newDefaultInstance(); + dbf.setFeature( + "http://apache.org/xml/features/nonvalidating/load-external-dtd", + false); + DocumentBuilder b = dbf.newDocumentBuilder(); + Document doc = b.parse(new FileInputStream(path.toFile())); + + XPath xPath = XPathFactory.newInstance().newXPath(); + + String mainLauncher = xpathQueryNullable(xPath, + "/jpackage-state/main-launcher/text()", doc); + if (mainLauncher == null) { + // No main launcher, this is fatal. + return new AppImageFile(); + } + + List addLaunchers = new ArrayList(); + + String platform = xpathQueryNullable(xPath, + "/jpackage-state/@platform", doc); + + String version = xpathQueryNullable(xPath, + "/jpackage-state/@version", doc); + + NodeList launcherNameNodes = (NodeList) xPath.evaluate( + "/jpackage-state/add-launcher/text()", doc, + XPathConstants.NODESET); + + for (int i = 0; i != launcherNameNodes.getLength(); i++) { + addLaunchers.add(launcherNameNodes.item(i).getNodeValue()); + } + + AppImageFile file = new AppImageFile( + mainLauncher, addLaunchers, version, platform); + if (!file.isValid()) { + file = new AppImageFile(); + } + return file; + } catch (ParserConfigurationException | SAXException ex) { + // Let caller sort this out + throw new IOException(ex); + } catch (XPathExpressionException ex) { + // This should never happen as XPath expressions should be correct + throw new RuntimeException(ex); + } + } + + /** + * Returns list of launcher names configured for the application. + * The first item in the returned list is man launcher name. + * Following items in the list are names of additional launchers. + */ + static List getLauncherNames(Path appImageDir, + Map params) { + List launchers = new ArrayList<>(); + try { + AppImageFile appImageInfo = AppImageFile.load(appImageDir); + if (appImageInfo != null) { + launchers.add(appImageInfo.getLauncherName()); + launchers.addAll(appImageInfo.getAddLauncherNames()); + return launchers; + } + } catch (IOException ioe) { + Log.verbose(ioe); + } + + launchers.add(APP_NAME.fetchFrom(params)); + ADD_LAUNCHERS.fetchFrom(params).stream().map(APP_NAME::fetchFrom).forEach( + launchers::add); + return launchers; + } + + private static String xpathQueryNullable(XPath xPath, String xpathExpr, + Document xml) throws XPathExpressionException { + NodeList nodes = (NodeList) xPath.evaluate(xpathExpr, xml, + XPathConstants.NODESET); + if (nodes != null && nodes.getLength() > 0) { + return nodes.item(0).getNodeValue(); + } + return null; + } + + private static String getVersion() { + return System.getProperty("java.version"); + } + + private static String getPlatform() { + return PLATFORM_LABELS.get(Platform.getPlatform()); + } + + private boolean isValid() { + if (launcherName == null || launcherName.length() == 0 || + addLauncherNames.indexOf("") != -1) { + // Some launchers have empty names. This is invalid. + return false; + } + + // Add more validation. + + return true; + } + +} diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/share/classes/jdk/jpackage/internal/DottedVersion.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DottedVersion.java Fri Oct 18 14:14:37 2019 -0400 @@ -0,0 +1,101 @@ +/* + * Copyright (c) 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.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Dotted numeric version string. + * E.g.: 1.0.37, 10, 0.5 + */ +class DottedVersion implements Comparable { + + public DottedVersion(String version) { + components = parseVersionString(version); + value = version; + } + + @Override + public int compareTo(String o) { + int result = 0; + int[] otherComponents = parseVersionString(o); + for (int i = 0; i < Math.min(components.length, otherComponents.length) + && result == 0; ++i) { + result = components[i] - otherComponents[i]; + } + + if (result == 0) { + result = components.length - otherComponents.length; + } + + return result; + } + + private static int[] parseVersionString(String version) { + Objects.requireNonNull(version); + if (version.isEmpty()) { + throw new IllegalArgumentException("Version may not be empty string"); + } + + int lastNotZeroIdx = -1; + List components = new ArrayList<>(); + for (var component : version.split("\\.", -1)) { + if (component.isEmpty()) { + throw new IllegalArgumentException(String.format( + "Version [%s] contains a zero lenght component", version)); + } + + int num = Integer.parseInt(component); + if (num < 0) { + throw new IllegalArgumentException(String.format( + "Version [%s] contains invalid component [%s]", version, + component)); + } + + if (num != 0) { + lastNotZeroIdx = components.size(); + } + components.add(num); + } + + if (lastNotZeroIdx + 1 != components.size()) { + // Strip trailing zeros. + components = components.subList(0, lastNotZeroIdx + 1); + } + + return components.stream().mapToInt(Integer::intValue).toArray(); + } + + @Override + public String toString() { + return value; + } + + final private int[] components; + final private String value; +} diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociation.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociation.java Fri Oct 18 14:14:37 2019 -0400 @@ -0,0 +1,70 @@ +/* + * Copyright (c) 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.File; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; +import static jdk.jpackage.internal.StandardBundlerParam.*; + +final class FileAssociation { + void verify() { + if (extensions.isEmpty()) { + Log.error(I18N.getString( + "message.creating-association-with-null-extension")); + } + } + + static List fetchFrom(Map params) { + String launcherName = APP_NAME.fetchFrom(params); + + return FILE_ASSOCIATIONS.fetchFrom(params).stream().filter( + Objects::nonNull).map(fa -> { + FileAssociation assoc = new FileAssociation(); + + assoc.launcherPath = Path.of(launcherName); + assoc.description = FA_DESCRIPTION.fetchFrom(fa); + assoc.extensions = Optional.ofNullable( + FA_EXTENSIONS.fetchFrom(fa)).orElse(Collections.emptyList()); + assoc.mimeTypes = Optional.ofNullable( + FA_CONTENT_TYPE.fetchFrom(fa)).orElse(Collections.emptyList()); + + File icon = FA_ICON.fetchFrom(fa); + if (icon != null) { + assoc.iconPath = icon.toPath(); + } + + return assoc; + }).collect(Collectors.toList()); + } + + Path launcherPath; + Path iconPath; + List mimeTypes; + List extensions; + String description; +} diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java Fri Oct 18 14:14:37 2019 -0400 @@ -26,14 +26,19 @@ package jdk.jpackage.internal; import java.io.*; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.nio.channels.FileChannel; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; /** * IOUtils @@ -221,4 +226,105 @@ file.getAbsolutePath()); } } + + public static Path replaceSuffix(Path path, String suffix) { + Path parent = path.getParent(); + String filename = path.getFileName().toString().replaceAll("\\.[^.]*$", "") + + Optional.ofNullable(suffix).orElse(""); + return parent != null ? parent.resolve(filename) : Path.of(filename); + } + + public static Path addSuffix(Path path, String suffix) { + Path parent = path.getParent(); + String filename = path.getFileName().toString() + suffix; + return parent != null ? parent.resolve(filename) : Path.of(filename); + } + + @FunctionalInterface + public static interface XmlConsumer { + void accept(XMLStreamWriter xml) throws IOException, XMLStreamException; + } + + public static void createXml(Path dstFile, XmlConsumer xmlConsumer) throws + IOException { + XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance(); + try (Writer w = new BufferedWriter(new FileWriter(dstFile.toFile()))) { + // Wrap with pretty print proxy + XMLStreamWriter xml = (XMLStreamWriter) Proxy.newProxyInstance( + XMLStreamWriter.class.getClassLoader(), new Class[]{ + XMLStreamWriter.class}, new PrettyPrintHandler( + xmlFactory.createXMLStreamWriter(w))); + + xml.writeStartDocument(); + xmlConsumer.accept(xml); + xml.writeEndDocument(); + xml.flush(); + xml.close(); + } catch (XMLStreamException ex) { + throw new IOException(ex); + } catch (IOException ex) { + throw ex; + } + } + + private static class PrettyPrintHandler implements InvocationHandler { + + PrettyPrintHandler(XMLStreamWriter target) { + this.target = target; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws + Throwable { + switch (method.getName()) { + case "writeStartElement": + // update state of parent node + if (depth > 0) { + hasChildElement.put(depth - 1, true); + } + // reset state of current node + hasChildElement.put(depth, false); + // indent for current depth + target.writeCharacters(EOL); + target.writeCharacters(repeat(depth, INDENT)); + depth++; + break; + case "writeEndElement": + depth--; + if (hasChildElement.get(depth) == true) { + target.writeCharacters(EOL); + target.writeCharacters(repeat(depth, INDENT)); + } + break; + case "writeProcessingInstruction": + case "writeEmptyElement": + // update state of parent node + if (depth > 0) { + hasChildElement.put(depth - 1, true); + } + // indent for current depth + target.writeCharacters(EOL); + target.writeCharacters(repeat(depth, INDENT)); + break; + default: + break; + } + method.invoke(target, args); + return null; + } + + private static String repeat(int d, String s) { + StringBuilder sb = new StringBuilder(); + while (d-- > 0) { + sb.append(s); + } + return sb.toString(); + } + + private final XMLStreamWriter target; + private int depth = 0; + private final Map hasChildElement = new HashMap<>(); + private static final String INDENT = " "; + private static final String EOL = "\n"; + } } diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/share/classes/jdk/jpackage/internal/PathGroup.java --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PathGroup.java Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PathGroup.java Fri Oct 18 14:14:37 2019 -0400 @@ -45,13 +45,24 @@ */ final class PathGroup { PathGroup(Map paths) { - entries = Collections.unmodifiableMap(paths); + entries = new HashMap<>(paths); } Path getPath(Object id) { + if (id == null) { + throw new NullPointerException(); + } return entries.get(id); } + void setPath(Object id, Path path) { + if (path != null) { + entries.put(id, path); + } else { + entries.remove(id); + } + } + /** * All configured entries. */ @@ -98,11 +109,15 @@ } void copy(PathGroup dst) throws IOException { - copy(this, dst, false); + copy(this, dst, null, false); } void move(PathGroup dst) throws IOException { - copy(this, dst, true); + copy(this, dst, null, true); + } + + void transform(PathGroup dst, TransformHandler handler) throws IOException { + copy(this, dst, handler, false); } static interface Facade { @@ -129,18 +144,57 @@ default void move(Facade dst) throws IOException { pathGroup().move(dst.pathGroup()); } + + default void transform(Facade dst, TransformHandler handler) throws + IOException { + pathGroup().transform(dst.pathGroup(), handler); + } + } + + static interface TransformHandler { + public void copyFile(Path src, Path dst) throws IOException; + public void createDirectory(Path dir) throws IOException; } - private static void copy(PathGroup src, PathGroup dst, boolean move) throws - IOException { - copy(move, src.entries.keySet().stream().filter( - id -> dst.entries.containsKey(id)).map(id -> Map.entry( - src.entries.get(id), dst.entries.get(id))).collect( - Collectors.toCollection(ArrayList::new))); + private static void copy(PathGroup src, PathGroup dst, + TransformHandler handler, boolean move) throws IOException { + List> copyItems = new ArrayList<>(); + List excludeItems = new ArrayList<>(); + + for (var id: src.entries.keySet()) { + Path srcPath = src.entries.get(id); + if (dst.entries.containsKey(id)) { + copyItems.add(Map.entry(srcPath, dst.entries.get(id))); + } else { + excludeItems.add(srcPath); + } + } + + copy(move, copyItems, excludeItems, handler); } - private static void copy(boolean move, List> entries) - throws IOException { + private static void copy(boolean move, List> entries, + List excludePaths, TransformHandler handler) throws + IOException { + + if (handler == null) { + handler = new TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + Files.createDirectories(dst.getParent()); + if (move) { + Files.move(src, dst); + } else { + Files.copy(src, dst); + } + } + + @Override + public void createDirectory(Path dir) throws IOException { + Files.createDirectories(dir); + } + }; + } // destination -> source file mapping Map actions = new HashMap<>(); @@ -149,12 +203,11 @@ Path dst = action.getValue(); if (src.toFile().isDirectory()) { try (Stream stream = Files.walk(src)) { - stream.forEach(path -> actions.put(dst.resolve( - src.relativize(path)).toAbsolutePath().normalize(), - path)); + stream.sequential().forEach(path -> actions.put(dst.resolve( + src.relativize(path)).normalize(), path)); } } else { - actions.put(dst.toAbsolutePath().normalize(), src); + actions.put(dst.normalize(), src); } } @@ -162,19 +215,18 @@ Path dst = action.getKey(); Path src = action.getValue(); + if (excludePaths.stream().anyMatch(src::startsWith)) { + continue; + } + if (src.equals(dst) || !src.toFile().exists()) { continue; } if (src.toFile().isDirectory()) { - Files.createDirectories(dst); + handler.createDirectory(dst); } else { - Files.createDirectories(dst.getParent()); - if (move) { - Files.move(src, dst); - } else { - Files.copy(src, dst); - } + handler.copyFile(src, dst); } } diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java Fri Oct 18 14:14:37 2019 -0400 @@ -25,23 +25,32 @@ package jdk.jpackage.internal; import java.io.IOException; +import java.nio.file.Path; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Stream; public final class ToolValidator { - ToolValidator(String name) { - this.name = name; + ToolValidator(String tool) { + this(Path.of(tool)); + } + + ToolValidator(Path toolPath) { + this.toolPath = toolPath; args = new ArrayList<>(); if (Platform.getPlatform() == Platform.LINUX) { setCommandLine("--version"); } + + setToolNotFoundErrorHandler(null); + setToolOldVersionErrorHandler(null); } ToolValidator setCommandLine(String... args) { @@ -59,10 +68,23 @@ return this; } + ToolValidator setToolNotFoundErrorHandler( + BiFunction v) { + toolNotFoundErrorHandler = v; + return this; + } + + ToolValidator setToolOldVersionErrorHandler(BiFunction v) { + toolOldVersionErrorHandler = v; + return this; + } + ConfigException validate() { List cmdline = new ArrayList<>(); - cmdline.add(name); + cmdline.add(toolPath.toString()); cmdline.addAll(args); + + String name = toolPath.getFileName().toString(); try { ProcessBuilder pb = new ProcessBuilder(cmdline); AtomicBoolean canUseTool = new AtomicBoolean(); @@ -71,16 +93,20 @@ canUseTool.setPlain(true); } + String[] version = new String[1]; Executor.of(pb).setOutputConsumer(lines -> { if (versionParser != null && minimalVersion != null) { - String version = versionParser.apply(lines); - if (minimalVersion.compareTo(version) < 0) { + version[0] = versionParser.apply(lines); + if (minimalVersion.compareTo(version[0]) < 0) { canUseTool.setPlain(true); } } }).execute(); if (!canUseTool.getPlain()) { + if (toolOldVersionErrorHandler != null) { + return toolOldVersionErrorHandler.apply(name, version[0]); + } return new ConfigException(MessageFormat.format(I18N.getString( "error.tool-old-version"), name, minimalVersion), MessageFormat.format(I18N.getString( @@ -88,6 +114,9 @@ minimalVersion)); } } catch (IOException e) { + if (toolNotFoundErrorHandler != null) { + return toolNotFoundErrorHandler.apply(name, e); + } return new ConfigException(MessageFormat.format(I18N.getString( "error.tool-not-found"), name, e.getMessage()), MessageFormat.format(I18N.getString( @@ -98,8 +127,10 @@ return null; } - private final String name; + private final Path toolPath; private List args; private String minimalVersion; private Function, String> versionParser; + private BiFunction toolNotFoundErrorHandler; + private BiFunction toolOldVersionErrorHandler; } diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/windows/classes/jdk/jpackage/internal/VersionExtractor.java --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/VersionExtractor.java Fri Oct 18 11:00:57 2019 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,91 +0,0 @@ -/* - * Copyright (c) 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.ByteArrayOutputStream; -import java.io.PrintStream; -import java.text.MessageFormat; -import java.util.ResourceBundle; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class VersionExtractor extends PrintStream { - - private static final ResourceBundle I18N = ResourceBundle.getBundle( - "jdk.jpackage.internal.resources.WinResources"); - - private final String pattern; - private String version = null; - - public VersionExtractor(String pattern) { - super(new ByteArrayOutputStream()); - - this.pattern = pattern; - } - - public String getVersion() { - if (version == null) { - String content - = new String(((ByteArrayOutputStream) out).toByteArray()); - Pattern p = Pattern.compile(pattern); - Matcher matcher = p.matcher(content); - if (matcher.find()) { - version = matcher.group(1); - } - } - return version; - } - - public static boolean isLessThan(String version, String compareTo) - throws RuntimeException { - if (version == null || version.isEmpty()) { - throw new RuntimeException(MessageFormat.format( - I18N.getString("error.version-compare"), - version, compareTo)); - } - - if (compareTo == null || compareTo.isEmpty()) { - throw new RuntimeException(MessageFormat.format( - I18N.getString("error.version-compare"), - version, compareTo)); - } - - String [] versionArray = version.trim().split(Pattern.quote(".")); - String [] compareToArray = compareTo.trim().split(Pattern.quote(".")); - - for (int i = 0; i < versionArray.length; i++) { - int v1 = Integer.parseInt(versionArray[i]); - int v2 = Integer.parseInt(compareToArray[i]); - if (v1 < v2) { - return true; - } else if (v1 > v2) { - return false; - } - } - - return false; - } -} diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java Fri Oct 18 14:14:37 2019 -0400 @@ -85,7 +85,7 @@ @Override public boolean supported(boolean platformInstaller) { - return WinMsiBundler.isSupported(); + return msiBundler.supported(platformInstaller); } @Override @@ -96,7 +96,7 @@ @Override public boolean validate(Map params) throws ConfigException { - return new WinMsiBundler().validate(params); + return msiBundler.validate(params); } public File bundle(Map params, File outdir) @@ -107,7 +107,7 @@ File exeImageDir = EXE_IMAGE_DIR.fetchFrom(params); // Write msi to temporary directory. - File msi = new WinMsiBundler().bundle(params, exeImageDir); + File msi = msiBundler.bundle(params, exeImageDir); try { return buildEXE(msi, outdir); @@ -151,5 +151,7 @@ return I18N.getString(key); } + private final WinMsiBundler msiBundler = new WinMsiBundler(); + private static native int embedMSI(String exePath, String msiPath); } diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java Fri Oct 18 14:14:37 2019 -0400 @@ -33,10 +33,12 @@ import java.text.MessageFormat; import java.util.*; import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import static jdk.jpackage.internal.OverridableResource.createResource; +import static jdk.jpackage.internal.StandardBundlerParam.*; import static jdk.jpackage.internal.WindowsBundlerParam.*; @@ -52,10 +54,8 @@ * directory. The following WiX source files are generated: *
    *
  • main.wxs. Main source file with the installer description - *
  • bundle.wxi. Source file with application and Java run-time directory tree - * description. This source file is included from main.wxs - *
  • icons.wxi. Source file with the list of icons used by the application. - * This source file is included from main.wxs + *
  • bundle.wxf. Source file with application and Java run-time directory tree + * description. *
*

* main.wxs file is a copy of main.wxs resource from @@ -86,15 +86,10 @@ * files. *

  • JpIsSystemWide. Set to "yes" if --win-per-user-install command line * option was not specified. Undefined otherwise - *
  • JpWixVersion36OrNewer. Set to "yes" if WiX Toolkit v3.6 or newer is used. - * Undefined otherwise * */ public class WinMsiBundler extends AbstractBundler { - private static final ResourceBundle I18N = ResourceBundle.getBundle( - "jdk.jpackage.internal.resources.WinResources"); - public static final BundlerParamInfo APP_BUNDLER = new WindowsBundlerParam<>( "win.app.bundler", @@ -102,13 +97,6 @@ params -> new WinAppBundler(), null); - public static final BundlerParamInfo CAN_USE_WIX36 = - new WindowsBundlerParam<>( - "win.msi.canUseWix36", - Boolean.class, - params -> false, - (s, p) -> Boolean.valueOf(s)); - public static final BundlerParamInfo MSI_IMAGE_DIR = new WindowsBundlerParam<>( "win.msi.imageDir", @@ -154,66 +142,6 @@ params -> UUID.randomUUID(), (s, p) -> UUID.fromString(s)); - private static final String TOOL_CANDLE = "candle.exe"; - private static final String TOOL_LIGHT = "light.exe"; - // autodetect just v3.7, v3.8, 3.9, 3.10 and 3.11 - private static final String AUTODETECT_DIRS = - ";C:\\Program Files (x86)\\WiX Toolset v3.11\\bin;" - + "C:\\Program Files\\WiX Toolset v3.11\\bin;" - + "C:\\Program Files (x86)\\WiX Toolset v3.10\\bin;" - + "C:\\Program Files\\WiX Toolset v3.10\\bin;" - + "C:\\Program Files (x86)\\WiX Toolset v3.9\\bin;" - + "C:\\Program Files\\WiX Toolset v3.9\\bin;" - + "C:\\Program Files (x86)\\WiX Toolset v3.8\\bin;" - + "C:\\Program Files\\WiX Toolset v3.8\\bin;" - + "C:\\Program Files (x86)\\WiX Toolset v3.7\\bin;" - + "C:\\Program Files\\WiX Toolset v3.7\\bin"; - - private static String getCandlePath() { - for (String dirString : (System.getenv("PATH") - + AUTODETECT_DIRS).split(";")) { - File f = new File(dirString.replace("\"", ""), TOOL_CANDLE); - if (f.isFile()) { - return f.toString(); - } - } - return null; - } - - private static String getLightPath() { - for (String dirString : (System.getenv("PATH") - + AUTODETECT_DIRS).split(";")) { - File f = new File(dirString.replace("\"", ""), TOOL_LIGHT); - if (f.isFile()) { - return f.toString(); - } - } - return null; - } - - - public static final StandardBundlerParam MENU_HINT = - new WindowsBundlerParam<>( - Arguments.CLIOptions.WIN_MENU_HINT.getId(), - Boolean.class, - params -> false, - // valueOf(null) is false, - // and we actually do want null in some cases - (s, p) -> (s == null || - "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s) - ); - - public static final StandardBundlerParam SHORTCUT_HINT = - new WindowsBundlerParam<>( - Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(), - Boolean.class, - params -> false, - // valueOf(null) is false, - // and we actually do want null in some cases - (s, p) -> (s == null || - "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s) - ); - @Override public String getName() { return I18N.getString("msi.bundler.name"); @@ -237,17 +165,10 @@ @Override public boolean supported(boolean platformInstaller) { - return isSupported(); - } - - @Override - public boolean isDefault() { - return false; - } - - public static boolean isSupported() { try { - validateWixTools(); + if (wixToolset == null) { + wixToolset = WixTool.toolset(); + } return true; } catch (ConfigException ce) { Log.error(ce.getMessage()); @@ -260,71 +181,29 @@ return false; } - private static String findToolVersion(String toolName) { - try { - if (toolName == null || "".equals(toolName)) return null; - - ProcessBuilder pb = new ProcessBuilder( - toolName, - "/?"); - VersionExtractor ve = new VersionExtractor("version (\\d+.\\d+)"); - // not interested in the output - IOUtils.exec(pb, true, ve); - String version = ve.getVersion(); - Log.verbose(MessageFormat.format( - I18N.getString("message.tool-version"), - toolName, version)); - return version; - } catch (Exception e) { - Log.verbose(e); - return null; - } - } - - public static void validateWixTools() throws ConfigException{ - String candleVersion = findToolVersion(getCandlePath()); - String lightVersion = findToolVersion(getLightPath()); - - // WiX 3.0+ is required - String minVersion = "3.0"; - if (candleVersion == null || lightVersion == null) { - throw new ConfigException( - I18N.getString("error.no-wix-tools"), - I18N.getString("error.no-wix-tools.advice")); - } - - if (VersionExtractor.isLessThan(candleVersion, minVersion)) { - throw new ConfigException( - MessageFormat.format( - I18N.getString("message.wrong-tool-version"), - TOOL_CANDLE, candleVersion, minVersion), - I18N.getString("error.no-wix-tools.advice")); - } - if (VersionExtractor.isLessThan(lightVersion, minVersion)) { - throw new ConfigException( - MessageFormat.format( - I18N.getString("message.wrong-tool-version"), - TOOL_LIGHT, lightVersion, minVersion), - I18N.getString("error.no-wix-tools.advice")); - } + @Override + public boolean isDefault() { + return false; } @Override public boolean validate(Map params) throws 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 + if (wixToolset == null) { + wixToolset = WixTool.toolset(); + } - String lightVersion = findToolVersion(getLightPath()); - if (!VersionExtractor.isLessThan(lightVersion, "3.6")) { - Log.verbose(I18N.getString("message.use-wix36-features")); - params.put(CAN_USE_WIX36.getID(), Boolean.TRUE); + for (var toolInfo: wixToolset.values()) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.tool-version"), toolInfo.path.getFileName(), + toolInfo.version)); } + wixSourcesBuilder.setWixVersion(wixToolset.get(WixTool.Light).version); + + wixSourcesBuilder.logWixFeatures(); + /********* validate bundle parameters *************/ String version = PRODUCT_VERSION.fetchFrom(params); @@ -415,7 +294,7 @@ return true; } - private boolean prepareProto(Map params) + private void prepareProto(Map params) throws PackagerException, IOException { File appImage = StandardBundlerParam.getPredefinedAppImage(params); File appDir = null; @@ -445,28 +324,6 @@ destFile.setWritable(true); ensureByMutationFileIsRTF(destFile); } - - // copy file association icons - List> fileAssociations = - FILE_ASSOCIATIONS.fetchFrom(params); - for (Map fa : fileAssociations) { - File icon = FA_ICON.fetchFrom(fa); - if (icon == null) { - continue; - } - - File faIconFile = new File(appDir, icon.getName()); - - if (icon.exists()) { - try { - IOUtils.copyFile(icon, faIconFile); - } catch (IOException e) { - Log.verbose(e); - } - } - } - - return appDir != null; } public File bundle(Map params, File outdir) @@ -474,145 +331,37 @@ IOUtils.writableOutputDir(outdir.toPath()); - // validate we have valid tools before continuing - String light = getLightPath(); - String candle = getCandlePath(); - if (light == null || !new File(light).isFile() || - candle == null || !new File(candle).isFile()) { - Log.verbose(MessageFormat.format( - I18N.getString("message.light-file-string"), light)); - Log.verbose(MessageFormat.format( - I18N.getString("message.candle-file-string"), candle)); - throw new PackagerException("error.no-wix-tools"); - } + Path imageDir = MSI_IMAGE_DIR.fetchFrom(params).toPath(); + try { + Files.createDirectories(imageDir); - Map wixVars = null; + Path postImageScript = imageDir.resolve(APP_NAME.fetchFrom(params) + "-post-image.wsf"); + createResource(null, params) + .setCategory(I18N.getString("resource.post-install-script")) + .saveToFile(postImageScript); - File imageDir = MSI_IMAGE_DIR.fetchFrom(params); - try { - imageDir.mkdirs(); + prepareProto(params); - prepareBasicProjectConfig(params); - if (prepareProto(params)) { - wixVars = prepareWiXConfig(params); + wixSourcesBuilder + .initFromParams(WIN_APP_IMAGE.fetchFrom(params).toPath(), params) + .createMainFragment(CONFIG_ROOT.fetchFrom(params).toPath().resolve( + "bundle.wxf")); - File configScriptSrc = getConfig_Script(params); - if (configScriptSrc.exists()) { - // we need to be running post script in the image folder - - // NOTE: Would it be better to generate it to the image - // folder and save only if "verbose" is requested? + Map wixVars = prepareMainProjectFile(params); - // for now we replicate it - File configScript = - new File(imageDir, configScriptSrc.getName()); - IOUtils.copyFile(configScriptSrc, configScript); - Log.verbose(MessageFormat.format( - I18N.getString("message.running-wsh-script"), - configScript.getAbsolutePath())); - IOUtils.run("wscript", configScript); - } - return buildMSI(params, wixVars, outdir); + if (Files.exists(postImageScript)) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.running-wsh-script"), + postImageScript.toAbsolutePath())); + Executor.of("wscript", postImageScript.toString()).executeExpectSuccess(); } - return null; + return buildMSI(params, wixVars, outdir); } catch (IOException ex) { Log.verbose(ex); throw new PackagerException(ex); } } - // name of post-image script - private File getConfig_Script(Map params) { - return new File(CONFIG_ROOT.fetchFrom(params), - APP_NAME.fetchFrom(params) + "-post-image.wsf"); - } - - private void prepareBasicProjectConfig( - Map params) throws IOException { - - Path scriptPath = getConfig_Script(params).toPath(); - - createResource(null, params) - .setCategory(I18N.getString("resource.post-install-script")) - .saveToFile(scriptPath); - } - - private static String relativePath(File basedir, File file) { - return file.getAbsolutePath().substring( - basedir.getAbsolutePath().length() + 1); - } - - private AppImageFile appImageFile = null; - private String[] getLaunchers( Map params) { - try { - ArrayList launchers = new ArrayList(); - if (appImageFile == null) { - appImageFile = AppImageFile.load( - WIN_APP_IMAGE.fetchFrom(params).toPath()); - } - launchers.add(appImageFile.getLauncherName()); - launchers.addAll(appImageFile.getAddLauncherNames()); - return launchers.toArray(new String[0]); - } catch (IOException ioe) { - Log.verbose(ioe.getMessage()); - } - String [] launcherNames = new String [1]; - launcherNames[0] = APP_NAME.fetchFrom(params); - return launcherNames; - } - - private void prepareIconsFile( - Map params) throws IOException { - - File imageRootDir = WIN_APP_IMAGE.fetchFrom(params); - - List> addLaunchers = - ADD_LAUNCHERS.fetchFrom(params); - - XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance(); - try (Writer w = new BufferedWriter(new FileWriter(new File( - CONFIG_ROOT.fetchFrom(params), "icons.wxi")))) { - XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w); - - xml.writeStartDocument(); - xml.writeStartElement("Include"); - - String[] launcherNames = getLaunchers(params); - - File[] icons = new File[launcherNames.length]; - for (int i=0; i prepareMainProjectFile( Map params) throws IOException { Map data = new HashMap<>(); @@ -636,10 +385,6 @@ data.put("JpAllowDowngrades", "yes"); } - if (CAN_USE_WIX36.fetchFrom(params)) { - data.put("JpWixVersion36OrNewer", "yes"); - } - data.put("JpAppName", APP_NAME.fetchFrom(params)); data.put("JpAppDescription", DESCRIPTION.fetchFrom(params)); data.put("JpAppVendor", VENDOR.fetchFrom(params)); @@ -648,8 +393,6 @@ data.put("JpConfigDir", CONFIG_ROOT.fetchFrom(params).getAbsolutePath()); - File imageRootDir = WIN_APP_IMAGE.fetchFrom(params); - if (MSI_SYSTEM_WIDE.fetchFrom(params)) { data.put("JpIsSystemWide", "yes"); } @@ -689,367 +432,30 @@ return data; } - private int id; - private int compId; - private final static String LAUNCHER_ID = "LauncherId"; - - private void walkFileTree(Map params, - File root, PrintStream out, String prefix) { - List dirs = new ArrayList<>(); - List files = new ArrayList<>(); - - if (!root.isDirectory()) { - throw new RuntimeException( - MessageFormat.format( - I18N.getString("error.cannot-walk-directory"), - root.getAbsolutePath())); - } - - // sort to files and dirs - File[] children = root.listFiles(); - if (children != null) { - for (File f : children) { - if (f.isDirectory()) { - dirs.add(f); - } else { - files.add(f); - } - } - } - - // have files => need to output component - out.println(prefix + " "); - out.println(prefix + " "); - out.println(prefix + " "); - - - File imageRootDir = WIN_APP_IMAGE.fetchFrom(params); - - // Find out if we need to use registry. We need it if - // - we doing user level install as file can not serve as KeyPath - // - if we adding shortcut in this component - boolean menuShortcut = MENU_HINT.fetchFrom(params); - boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params); - boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params) || - menuShortcut || desktopShortcut; - - if (needRegistryKey) { - // has to be under HKCU to make WiX happy - out.println(prefix + " " : " Action=\"createAndRemoveOnUninstall\">")); - out.println(prefix - + " "); - out.println(prefix + " "); - } - - String[] launcherNames = getLaunchers(params); - - File[] launcherFiles = new File[launcherNames.length]; - for (int i=0; i idToFileMap = new TreeMap<>(); - boolean launcherSet = false; - - for (File f : files) { - boolean isMainLauncher = - launcherFiles.length > 0 && f.equals(launcherFiles[0]); - - launcherSet = launcherSet || isMainLauncher; - - String thisFileId = isMainLauncher ? LAUNCHER_ID : ("FileId" + (id++)); - idToFileMap.put(f.getName(), thisFileId); - - out.println(prefix + " "); - if (isMainLauncher && desktopShortcut) { - out.println(prefix - + " "); - } - if (isMainLauncher && menuShortcut) { - out.println(prefix - + " "); - } - - // any additional launchers - for (int index = 1; index < launcherNames.length; index++ ) { - - if (f.equals(launcherFiles[index])) { - if (desktopShortcut) { - out.println(prefix - + " "); - } - if (menuShortcut) { - out.println(prefix - + " "); - } - } - } - out.println(prefix + " "); - } - - if (launcherSet) { - List> fileAssociations = - FILE_ASSOCIATIONS.fetchFrom(params); - Set defaultedMimes = new TreeSet<>(); - for (Map fa : fileAssociations) { - String description = FA_DESCRIPTION.fetchFrom(fa); - List extensions = FA_EXTENSIONS.fetchFrom(fa); - List mimeTypes = FA_CONTENT_TYPE.fetchFrom(fa); - File icon = FA_ICON.fetchFrom(fa); - - String mime = (mimeTypes == null || - mimeTypes.isEmpty()) ? null : mimeTypes.get(0); - - String entryName = APP_REGISTRY_NAME.fetchFrom(params) + "File"; - - if (extensions == null) { - Log.verbose(I18N.getString( - "message.creating-association-with-null-extension")); - - out.print(prefix + " "); - } else { - for (String ext : extensions) { - - entryName = ext.toUpperCase() + "File"; - - out.print(prefix + " "); - - out.print(prefix + " "); - } else { - out.println(" ContentType='" + mime + "'>"); - if (!defaultedMimes.contains(mime)) { - out.println(prefix - + " "); - defaultedMimes.add(mime); - } - } - out.println(prefix - + " "); - out.println(prefix + " "); - out.println(prefix + " "); - } - } - } - } - - out.println(prefix + " "); - - for (File d : dirs) { - out.println(prefix + " "); - walkFileTree(params, d, out, prefix + " "); - out.println(prefix + " "); - } - } - - void prepareContentList(Map params) - throws FileNotFoundException { - File f = new File( - CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE); - - try (PrintStream out = new PrintStream(f)) { - - // opening - out.println(""); - out.println(""); - - out.println(" "); - if (MSI_SYSTEM_WIDE.fetchFrom(params)) { - // install to programfiles - out.println(" "); - } else { - // install to user folder - out.println( - " "); - } - - // reset counters - compId = 0; - id = 0; - - // We should get valid folder or subfolders - String installDir = WINDOWS_INSTALL_DIR.fetchFrom(params); - String [] installDirs = installDir.split(Pattern.quote("\\")); - for (int i = 0; i < (installDirs.length - 1); i++) { - out.println(" "); - if (!MSI_SYSTEM_WIDE.fetchFrom(params)) { - out.println(" "); - out.println(""); - // has to be under HKCU to make WiX happy - out.println(" " : " Action=\"createAndRemoveOnUninstall\">")); - out.println(" "); - out.println(" "); - out.println(" "); - out.println(""); - } - } - - out.println(" "); - - // dynamic part - walkFileTree(params, WIN_APP_IMAGE.fetchFrom(params), out, " "); - - // closing - for (int i = 0; i < installDirs.length; i++) { - out.println(" "); - } - out.println(" "); - - // for shortcuts - if (SHORTCUT_HINT.fetchFrom(params)) { - out.println(" "); - } - if (MENU_HINT.fetchFrom(params)) { - out.println(" "); - out.println(" "); - out.println(" "); - out.println(" "); - // This has to be under HKCU to make WiX happy. - // There are numberous discussions on this amoung WiX users - // (if user A installs and user B uninstalls key is left behind) - // there are suggested workarounds but none are appealing. - // Leave it for now - out.println( - " "); - out.println(" "); - out.println(" "); - out.println(" "); - } - - out.println(" "); - - out.println(" "); - for (int j = 0; j < compId; j++) { - out.println(" "); - } - // component is defined in the main.wsx - out.println( - " "); - out.println(" "); - out.println(""); - - } - } private File getConfig_ProjectFile(Map params) { - return new File(CONFIG_ROOT.fetchFrom(params), - APP_NAME.fetchFrom(params) + ".wxs"); + return new File(CONFIG_ROOT.fetchFrom(params), "main.wxs"); } - private Map prepareWiXConfig( - Map params) throws IOException { - prepareContentList(params); - prepareIconsFile(params); - return prepareMainProjectFile(params); - } - - private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi"; - private File buildMSI(Map params, Map wixVars, File outdir) throws IOException { - File tmpDir = new File(TEMP_ROOT.fetchFrom(params), "tmp"); - File candleOut = new File( - tmpDir, APP_NAME.fetchFrom(params) + ".wixobj"); + File msiOut = new File( outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi"); Log.verbose(MessageFormat.format(I18N.getString( "message.preparing-msi-config"), msiOut.getAbsolutePath())); - msiOut.getParentFile().mkdirs(); - - List commandLine = new ArrayList<>(Arrays.asList( - getCandlePath(), - "-nologo", - getConfig_ProjectFile(params).getAbsolutePath(), - "-ext", "WixUtilExtension", - "-out", candleOut.getAbsolutePath())); - for(Map.Entry wixVar: wixVars.entrySet()) { - String v = "-d" + wixVar.getKey() + "=" + wixVar.getValue(); - commandLine.add(v); - } - ProcessBuilder pb = new ProcessBuilder(commandLine); - pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params)); - IOUtils.exec(pb); + WixPipeline wixPipeline = new WixPipeline() + .setToolset(wixToolset.entrySet().stream().collect( + Collectors.toMap( + entry -> entry.getKey(), + entry -> entry.getValue().path))) + .setWixObjDir(TEMP_ROOT.fetchFrom(params).toPath().resolve("wixobj")) + .setWorkDir(WIN_APP_IMAGE.fetchFrom(params).toPath()) + .addSource(CONFIG_ROOT.fetchFrom(params).toPath().resolve("main.wxs"), wixVars) + .addSource(CONFIG_ROOT.fetchFrom(params).toPath().resolve("bundle.wxf"), null); Log.verbose(MessageFormat.format(I18N.getString( "message.generating-msi"), msiOut.getAbsolutePath())); @@ -1057,44 +463,25 @@ boolean enableLicenseUI = (LICENSE_FILE.fetchFrom(params) != null); boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params); - commandLine = new ArrayList<>(); + List lightArgs = new ArrayList<>(); - commandLine.add(getLightPath()); - - commandLine.add("-nologo"); - commandLine.add("-spdb"); if (!MSI_SYSTEM_WIDE.fetchFrom(params)) { - commandLine.add("-sice:ICE91"); + wixPipeline.addLightOptions("-sice:ICE91"); } - commandLine.add(candleOut.getAbsolutePath()); - commandLine.add("-ext"); - commandLine.add("WixUtilExtension"); if (enableLicenseUI || enableInstalldirUI) { - commandLine.add("-ext"); - commandLine.add("WixUIExtension"); + wixPipeline.addLightOptions("-ext", "WixUIExtension"); } - commandLine.add("-loc"); - commandLine.add(new File(CONFIG_ROOT.fetchFrom(params), I18N.getString( - "resource.wxl-file-name")).getAbsolutePath()); + wixPipeline.addLightOptions("-loc", + CONFIG_ROOT.fetchFrom(params).toPath().resolve(I18N.getString( + "resource.wxl-file-name")).toAbsolutePath().toString()); // Only needed if we using CA dll, so Wix can find it if (enableInstalldirUI) { - commandLine.add("-b"); - commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath()); + wixPipeline.addLightOptions("-b", CONFIG_ROOT.fetchFrom(params).getAbsolutePath()); } - commandLine.add("-out"); - commandLine.add(msiOut.getAbsolutePath()); - - // create .msi - pb = new ProcessBuilder(commandLine); - - pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params)); - IOUtils.exec(pb); - - candleOut.delete(); - IOUtils.deleteRecursive(tmpDir); + wixPipeline.buildMsi(msiOut.toPath().toAbsolutePath()); return msiOut; } @@ -1170,4 +557,7 @@ } + private Map wixToolset; + private WixSourcesBuilder wixSourcesBuilder = new WixSourcesBuilder(); + } diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsBundlerParam.java --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsBundlerParam.java Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsBundlerParam.java Fri Oct 18 14:14:37 2019 -0400 @@ -60,18 +60,6 @@ }, (s, p) -> s); - static final BundlerParamInfo APP_REGISTRY_NAME = - new StandardBundlerParam<> ( - "win.registryName", - String.class, - params -> { - String nm = APP_NAME.fetchFrom(params); - if (nm == null) return null; - - return nm.replaceAll("[^-a-zA-Z\\.0-9]", ""); - }, - (s, p) -> s); - static final StandardBundlerParam MENU_GROUP = new StandardBundlerParam<>( Arguments.CLIOptions.WIN_MENU_GROUP.getId(), diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java Fri Oct 18 14:14:37 2019 -0400 @@ -0,0 +1,145 @@ +/* + * Copyright (c) 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.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +/** + * WiX pipeline. Compiles and links WiX sources. + */ +public class WixPipeline { + WixPipeline() { + sources = new ArrayList<>(); + lightOptions = new ArrayList<>(); + } + + WixPipeline setToolset(Map v) { + toolset = v; + return this; + } + + WixPipeline setWixVariables(Map v) { + wixVariables = v; + return this; + } + + WixPipeline setWixObjDir(Path v) { + wixObjDir = v; + return this; + } + + WixPipeline setWorkDir(Path v) { + workDir = v; + return this; + } + + WixPipeline addSource(Path source, Map wixVariables) { + WixSource entry = new WixSource(); + entry.source = source; + entry.variables = wixVariables; + sources.add(entry); + return this; + } + + WixPipeline addLightOptions(String ... v) { + lightOptions.addAll(List.of(v)); + return this; + } + + void buildMsi(Path msi) throws IOException { + List wixObjs = new ArrayList<>(); + for (var source : sources) { + wixObjs.add(compile(source)); + } + + List lightCmdline = new ArrayList<>(List.of( + toolset.get(WixTool.Light).toString(), + "-nologo", + "-spdb", + "-ext", "WixUtilExtension", + "-out", msi.toString() + )); + + lightCmdline.addAll(lightOptions); + wixObjs.stream().map(Path::toString).forEach(lightCmdline::add); + + Files.createDirectories(msi.getParent()); + execute(lightCmdline); + } + + private Path compile(WixSource wixSource) throws IOException { + UnaryOperator adjustPath = path -> { + return workDir != null ? path.toAbsolutePath() : path; + }; + + Path wixObj = adjustPath.apply(wixObjDir).resolve(IOUtils.replaceSuffix( + wixSource.source.getFileName(), ".wixobj")); + + List cmdline = new ArrayList<>(List.of( + toolset.get(WixTool.Candle).toString(), + "-nologo", + adjustPath.apply(wixSource.source).toString(), + "-ext", "WixUtilExtension", + "-arch", "x64", + "-out", wixObj.toAbsolutePath().toString() + )); + + Map appliedVaribales = new HashMap<>(); + Stream.of(wixVariables, wixSource.variables) + .filter(Objects::nonNull) + .forEachOrdered(appliedVaribales::putAll); + + appliedVaribales.entrySet().stream().map(wixVar -> String.format("-d%s=%s", + wixVar.getKey(), wixVar.getValue())).forEachOrdered( + cmdline::add); + + execute(cmdline); + + return wixObj; + } + + private void execute(List cmdline) throws IOException { + Executor.of(new ProcessBuilder(cmdline).directory( + workDir != null ? workDir.toFile() : null)).executeExpectSuccess(); + } + + private final static class WixSource { + Path source; + Map variables; + } + + private Map toolset; + private Map wixVariables; + private List lightOptions; + private Path wixObjDir; + private Path workDir; + private List sources; +} diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixSourcesBuilder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixSourcesBuilder.java Fri Oct 18 14:14:37 2019 -0400 @@ -0,0 +1,839 @@ +/* + * Copyright (c) 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.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.*; +import java.util.function.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import jdk.jpackage.internal.IOUtils.XmlConsumer; +import static jdk.jpackage.internal.StandardBundlerParam.*; +import static jdk.jpackage.internal.WinMsiBundler.*; +import static jdk.jpackage.internal.WindowsBundlerParam.MENU_GROUP; +import static jdk.jpackage.internal.WindowsBundlerParam.WINDOWS_INSTALL_DIR; + +/** + * Creates application WiX source files. + */ +class WixSourcesBuilder { + + WixSourcesBuilder setWixVersion(DottedVersion v) { + wixVersion = v; + return this; + } + + WixSourcesBuilder initFromParams(Path appImageRoot, + Map params) { + Supplier appImageSupplier = () -> { + if (StandardBundlerParam.isRuntimeInstaller(params)) { + return ApplicationLayout.javaRuntime(); + } else { + return ApplicationLayout.platformAppImage(); + } + }; + + systemWide = MSI_SYSTEM_WIDE.fetchFrom(params); + + registryKeyPath = Path.of("Software", + VENDOR.fetchFrom(params), + APP_NAME.fetchFrom(params), + VERSION.fetchFrom(params)).toString(); + + installDir = (systemWide ? PROGRAM_FILES : LOCAL_PROGRAM_FILES).resolve( + WINDOWS_INSTALL_DIR.fetchFrom(params)); + + do { + ApplicationLayout layout = appImageSupplier.get(); + // Don't want AppImageFile.FILENAME in installed application. + // Register it with app image at a role without a match in installed + // app layout to exclude it from layout transformation. + layout.pathGroup().setPath(new Object(), Path.of(AppImageFile.FILENAME)); + + // Want absolute paths to source files in generated WiX sources. + // This is to handle scenario if sources would be processed from + // differnt current directory. + appImage = layout.resolveAt(appImageRoot.toAbsolutePath().normalize()); + } while (false); + + installedAppImage = appImageSupplier.get().resolveAt(INSTALLDIR); + + shortcutFolders = new HashSet<>(); + if (SHORTCUT_HINT.fetchFrom(params)) { + shortcutFolders.add(ShortcutsFolder.Desktop); + } + if (MENU_HINT.fetchFrom(params)) { + shortcutFolders.add(ShortcutsFolder.ProgramMenu); + } + + if (StandardBundlerParam.isRuntimeInstaller(params)) { + launcherPaths = Collections.emptyList(); + } else { + launcherPaths = AppImageFile.getLauncherNames(appImageRoot, params).stream() + .map(name -> installedAppImage.launchersDirectory().resolve(name)) + .map(WixSourcesBuilder::addExeSuffixToPath) + .collect(Collectors.toList()); + } + + programMenuFolderName = MENU_GROUP.fetchFrom(params); + + initFileAssociations(params); + + return this; + } + + void createMainFragment(Path file) throws IOException { + removeFolderItems = new HashMap<>(); + defaultedMimes = new HashSet<>(); + IOUtils.createXml(file, xml -> { + xml.writeStartElement("Wix"); + xml.writeDefaultNamespace("http://schemas.microsoft.com/wix/2006/wi"); + xml.writeNamespace("util", + "http://schemas.microsoft.com/wix/UtilExtension"); + + xml.writeStartElement("Fragment"); + + addFaComponentGroup(xml); + + addShortcutComponentGroup(xml); + + addFilesComponentGroup(xml); + + xml.writeEndElement(); // + + addIconsFragment(xml); + + xml.writeEndElement(); // + }); + } + + void logWixFeatures() { + if (wixVersion.compareTo("3.6") >= 0) { + Log.verbose(I18N.getString("message.use-wix36-features")); + } + } + + private void normalizeFileAssociation(FileAssociation fa) { + fa.launcherPath = addExeSuffixToPath( + installedAppImage.launchersDirectory().resolve(fa.launcherPath)); + + if (fa.iconPath != null && !fa.iconPath.toFile().exists()) { + fa.iconPath = null; + } + + // Filter out empty extensions. + fa.extensions = fa.extensions.stream().filter(Predicate.not( + String::isEmpty)).collect(Collectors.toList()); + } + + private static Path addExeSuffixToPath(Path path) { + return IOUtils.addSuffix(path, ".exe"); + } + + private Path getInstalledFaIcoPath(FileAssociation fa) { + String fname = String.format("fa_%s.ico", String.join("_", fa.extensions)); + return installedAppImage.destktopIntegrationDirectory().resolve(fname); + } + + private void initFileAssociations(Map params) { + associations = FileAssociation.fetchFrom(params).stream() + .peek(this::normalizeFileAssociation) + // Filter out file associations without extensions. + .filter(fa -> !fa.extensions.isEmpty()) + .collect(Collectors.toList()); + + associations.stream().filter(fa -> fa.iconPath != null).forEach(fa -> { + // Need to add fa icon in the image. + Object key = new Object(); + appImage.pathGroup().setPath(key, fa.iconPath); + installedAppImage.pathGroup().setPath(key, getInstalledFaIcoPath(fa)); + }); + } + + private static UUID createNameUUID(String str) { + return UUID.nameUUIDFromBytes(str.getBytes(StandardCharsets.UTF_8)); + } + + private static UUID createNameUUID(Path path, String role) { + if (path.isAbsolute() || !ROOT_DIRS.contains(path.getName(0))) { + throw throwInvalidPathException(path); + } + // Paths are case insensitive on Windows + String keyPath = path.toString().toLowerCase(); + if (role != null) { + keyPath = role + "@" + keyPath; + } + return createNameUUID(keyPath); + } + + /** + * Value for Id attribute of various WiX elements. + */ + enum Id { + File, + Folder("dir"), + Shortcut, + ProgId, + Icon, + CreateFolder("mkdir"), + RemoveFolder("rm"); + + Id() { + this.prefix = name().toLowerCase(); + } + + Id(String prefix) { + this.prefix = prefix; + } + + String of(Path path) { + if (this == Folder && KNOWN_DIRS.contains(path)) { + return path.getFileName().toString(); + } + + String result = of(path, prefix, name()); + + if (this == Icon) { + // Icon id constructed from UUID value is too long and triggers + // CNDL1000 warning, so use Java hash code instead. + result = String.format("%s%d", prefix, result.hashCode()).replace( + "-", "_"); + } + + return result; + } + + private static String of(Path path, String prefix, String role) { + Objects.requireNonNull(role); + Objects.requireNonNull(prefix); + return String.format("%s%s", prefix, + createNameUUID(path, role).toString().replace("-", "")); + } + + static String of(Path path, String prefix) { + return of(path, prefix, prefix); + } + + private final String prefix; + } + + enum Component { + File(cfg().file()), + Shortcut(cfg().file().withRegistryKeyPath()), + ProgId(cfg().file().withRegistryKeyPath()), + CreateFolder(cfg().withRegistryKeyPath()), + RemoveFolder(cfg().withRegistryKeyPath()); + + Component() { + this.cfg = cfg(); + this.id = Id.valueOf(name()); + } + + Component(Config cfg) { + this.cfg = cfg; + this.id = Id.valueOf(name()); + } + + UUID guidOf(Path path) { + return createNameUUID(path, name()); + } + + String idOf(Path path) { + return id.of(path); + } + + boolean isRegistryKeyPath() { + return cfg.withRegistryKeyPath; + } + + boolean isFile() { + return cfg.isFile; + } + + private static final class Config { + Config withRegistryKeyPath() { + withRegistryKeyPath = true; + return this; + } + + Config file() { + isFile = true; + return this; + } + + private boolean isFile; + private boolean withRegistryKeyPath; + } + + private static Config cfg() { + return new Config(); + } + + private final Config cfg; + private final Id id; + }; + + private static void addComponentGroup(XMLStreamWriter xml, String id, + List componentIds) throws XMLStreamException, IOException { + xml.writeStartElement("ComponentGroup"); + xml.writeAttribute("Id", id); + componentIds = componentIds.stream().filter(Objects::nonNull).collect( + Collectors.toList()); + for (var componentId : componentIds) { + xml.writeStartElement("ComponentRef"); + xml.writeAttribute("Id", componentId); + xml.writeEndElement(); + } + xml.writeEndElement(); + } + + private String addComponent(XMLStreamWriter xml, Path path, + Component role, XmlConsumer xmlConsumer) throws XMLStreamException, + IOException { + + final Path directoryRefPath; + if (role.isFile()) { + directoryRefPath = path.getParent(); + } else { + directoryRefPath = path; + } + + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", Id.Folder.of(directoryRefPath)); + + final String componentId = "c" + role.idOf(path); + xml.writeStartElement("Component"); + xml.writeAttribute("Id", componentId); + xml.writeAttribute("Guid", String.format("{%s}", role.guidOf(path))); + + boolean isRegistryKeyPath = !systemWide || role.isRegistryKeyPath(); + if (isRegistryKeyPath) { + addRegistryKeyPath(xml, directoryRefPath); + if ((role.isFile() || (role == Component.CreateFolder + && !systemWide)) && !SYSTEM_DIRS.contains(directoryRefPath)) { + xml.writeStartElement("RemoveFolder"); + int counter = Optional.ofNullable(removeFolderItems.get( + directoryRefPath)).orElse(Integer.valueOf(0)).intValue() + 1; + removeFolderItems.put(directoryRefPath, counter); + xml.writeAttribute("Id", String.format("%s_%d", Id.RemoveFolder.of( + directoryRefPath), counter)); + xml.writeAttribute("On", "uninstall"); + xml.writeEndElement(); + } + } + + xml.writeStartElement(role.name()); + if (role != Component.CreateFolder) { + xml.writeAttribute("Id", role.idOf(path)); + } + + if (!isRegistryKeyPath) { + xml.writeAttribute("KeyPath", "yes"); + } + + xmlConsumer.accept(xml); + xml.writeEndElement(); + + xml.writeEndElement(); // + xml.writeEndElement(); // + + return componentId; + } + + private void addFaComponentGroup(XMLStreamWriter xml) + throws XMLStreamException, IOException { + + List componentIds = new ArrayList<>(); + for (var fa : associations) { + componentIds.addAll(addFaComponents(xml, fa)); + } + addComponentGroup(xml, "FileAssociations", componentIds); + } + + private void addShortcutComponentGroup(XMLStreamWriter xml) throws + XMLStreamException, IOException { + List componentIds = new ArrayList<>(); + Set defineShortcutFolders = new HashSet<>(); + for (var launcherPath : launcherPaths) { + for (var folder : shortcutFolders) { + String componentId = addShortcutComponent(xml, launcherPath, + folder); + if (componentId != null) { + defineShortcutFolders.add(folder); + componentIds.add(componentId); + } + } + } + + for (var folder : defineShortcutFolders) { + Path path = folder.getPath(this); + componentIds.addAll(addRootBranch(xml, path)); + + if (!KNOWN_DIRS.contains(path)) { + componentIds.add(addDirectoryCleaner(xml, path)); + } + } + + addComponentGroup(xml, "Shortcuts", componentIds); + } + + private String addShortcutComponent(XMLStreamWriter xml, Path launcherPath, + ShortcutsFolder folder) throws XMLStreamException, IOException { + Objects.requireNonNull(folder); + + if (!INSTALLDIR.equals(launcherPath.getName(0))) { + throw throwInvalidPathException(launcherPath); + } + + String launcherBasename = IOUtils.replaceSuffix( + launcherPath.getFileName(), "").toString(); + + Path shortcutPath = folder.getPath(this).resolve(launcherBasename); + return addComponent(xml, shortcutPath, Component.Shortcut, unused -> { + final Path icoFile = IOUtils.addSuffix( + installedAppImage.destktopIntegrationDirectory().resolve( + launcherBasename), ".ico"); + + xml.writeAttribute("Name", launcherBasename); + xml.writeAttribute("WorkingDirectory", INSTALLDIR.toString()); + xml.writeAttribute("Advertise", "no"); + xml.writeAttribute("IconIndex", "0"); + xml.writeAttribute("Target", String.format("[#%s]", + Component.File.idOf(launcherPath))); + xml.writeAttribute("Icon", Id.Icon.of(icoFile)); + }); + } + + private List addFaComponents(XMLStreamWriter xml, + FileAssociation fa) throws XMLStreamException, IOException { + List components = new ArrayList<>(); + for (var extension: fa.extensions) { + Path path = INSTALLDIR.resolve(String.format("%s_%s", extension, + fa.launcherPath.getFileName())); + components.add(addComponent(xml, path, Component.ProgId, unused -> { + xml.writeAttribute("Description", fa.description); + + if (fa.iconPath != null) { + xml.writeAttribute("Icon", Id.File.of(getInstalledFaIcoPath( + fa))); + xml.writeAttribute("IconIndex", "0"); + } + + xml.writeStartElement("Extension"); + xml.writeAttribute("Id", extension); + xml.writeAttribute("Advertise", "no"); + + var mimeIt = fa.mimeTypes.iterator(); + if (mimeIt.hasNext()) { + String mime = mimeIt.next(); + xml.writeAttribute("ContentType", mime); + + if (!defaultedMimes.contains(mime)) { + xml.writeStartElement("MIME"); + xml.writeAttribute("ContentType", mime); + xml.writeAttribute("Default", "yes"); + xml.writeEndElement(); + defaultedMimes.add(mime); + } + } + + xml.writeStartElement("Verb"); + xml.writeAttribute("Id", "open"); + xml.writeAttribute("Command", "Open"); + xml.writeAttribute("Argument", "%1"); + xml.writeAttribute("TargetFile", Id.File.of(fa.launcherPath)); + xml.writeEndElement(); // + + xml.writeEndElement(); // + })); + } + + return components; + } + + private List addRootBranch(XMLStreamWriter xml, Path path) + throws XMLStreamException, IOException { + if (!ROOT_DIRS.contains(path.getName(0))) { + throw throwInvalidPathException(path); + } + + Function createDirectoryName = dir -> null; + + boolean sysDir = true; + int levels = 1; + var dirIt = path.iterator(); + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", dirIt.next().toString()); + + path = path.getName(0); + while (dirIt.hasNext()) { + levels++; + Path name = dirIt.next(); + path = path.resolve(name); + + if (sysDir && !SYSTEM_DIRS.contains(path)) { + sysDir = false; + createDirectoryName = dir -> dir.getFileName().toString(); + } + + final String directoryId; + if (!sysDir && path.equals(installDir)) { + directoryId = INSTALLDIR.toString(); + } else { + directoryId = Id.Folder.of(path); + } + xml.writeStartElement("Directory"); + xml.writeAttribute("Id", directoryId); + + String directoryName = createDirectoryName.apply(path); + if (directoryName != null) { + xml.writeAttribute("Name", directoryName); + } + } + + while (0 != levels--) { + xml.writeEndElement(); + } + + List componentIds = new ArrayList<>(); + while (!SYSTEM_DIRS.contains(path = path.getParent())) { + componentIds.add(addRemoveDirectoryComponent(xml, path)); + } + + return componentIds; + } + + private String addRemoveDirectoryComponent(XMLStreamWriter xml, Path path) + throws XMLStreamException, IOException { + return addComponent(xml, path, Component.RemoveFolder, + unused -> xml.writeAttribute("On", "uninstall")); + } + + private List addDirectoryHierarchy(XMLStreamWriter xml) + throws XMLStreamException, IOException { + + Set allDirs = new HashSet<>(); + Set emptyDirs = new HashSet<>(); + appImage.transform(installedAppImage, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + Path dir = dst.getParent(); + createDirectory(dir); + emptyDirs.remove(dir); + } + + @Override + public void createDirectory(final Path dir) throws IOException { + if (!allDirs.contains(dir)) { + emptyDirs.add(dir); + } + + Path it = dir; + while (it != null && allDirs.add(it)) { + it = it.getParent(); + } + + it = dir; + while ((it = it.getParent()) != null && emptyDirs.remove(it)); + } + }); + + List componentIds = new ArrayList<>(); + for (var dir : emptyDirs) { + componentIds.add(addComponent(xml, dir, Component.CreateFolder, + unused -> {})); + } + + if (!systemWide) { + // Per-user install requires component in every + // directory. + for (var dir : allDirs.stream() + .filter(Predicate.not(emptyDirs::contains)) + .filter(Predicate.not(removeFolderItems::containsKey)) + .collect(Collectors.toList())) { + componentIds.add(addRemoveDirectoryComponent(xml, dir)); + } + } + + allDirs.remove(INSTALLDIR); + for (var dir : allDirs) { + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", Id.Folder.of(dir.getParent())); + xml.writeStartElement("Directory"); + xml.writeAttribute("Id", Id.Folder.of(dir)); + xml.writeAttribute("Name", dir.getFileName().toString()); + xml.writeEndElement(); + xml.writeEndElement(); + } + + componentIds.addAll(addRootBranch(xml, installDir)); + + return componentIds; + } + + private void addFilesComponentGroup(XMLStreamWriter xml) + throws XMLStreamException, IOException { + + List> files = new ArrayList<>(); + appImage.transform(installedAppImage, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + files.add(Map.entry(src, dst)); + } + + @Override + public void createDirectory(final Path dir) throws IOException { + } + }); + + List componentIds = new ArrayList<>(); + for (var file : files) { + Path src = file.getKey(); + Path dst = file.getValue(); + + componentIds.add(addComponent(xml, dst, Component.File, unused -> { + xml.writeAttribute("Source", src.normalize().toString()); + Path name = dst.getFileName(); + if (!name.equals(src.getFileName())) { + xml.writeAttribute("Name", name.toString()); + } + })); + } + + componentIds.addAll(addDirectoryHierarchy(xml)); + + componentIds.add(addDirectoryCleaner(xml, INSTALLDIR)); + + addComponentGroup(xml, "Files", componentIds); + } + + private void addIconsFragment(XMLStreamWriter xml) throws + XMLStreamException, IOException { + + PathGroup srcPathGroup = appImage.pathGroup(); + PathGroup dstPathGroup = installedAppImage.pathGroup(); + + // Build list of copy operations for all .ico files in application image + List> icoFiles = new ArrayList<>(); + srcPathGroup.transform(dstPathGroup, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + if (src.getFileName().toString().endsWith(".ico")) { + icoFiles.add(Map.entry(src, dst)); + } + } + + @Override + public void createDirectory(Path dst) throws IOException { + } + }); + + xml.writeStartElement("Fragment"); + for (var icoFile : icoFiles) { + xml.writeStartElement("Icon"); + xml.writeAttribute("Id", Id.Icon.of(icoFile.getValue())); + xml.writeAttribute("SourceFile", icoFile.getKey().toString()); + xml.writeEndElement(); + } + xml.writeEndElement(); + } + + private void addRegistryKeyPath(XMLStreamWriter xml, Path path) throws + XMLStreamException, IOException { + addRegistryKeyPath(xml, path, () -> "ProductCode", () -> "[ProductCode]"); + } + + private void addRegistryKeyPath(XMLStreamWriter xml, Path path, + Supplier nameAttr, Supplier valueAttr) throws + XMLStreamException, IOException { + + String regRoot = USER_PROFILE_DIRS.stream().anyMatch(path::startsWith) + || !systemWide ? "HKCU" : "HKLM"; + + xml.writeStartElement("RegistryKey"); + xml.writeAttribute("Root", regRoot); + xml.writeAttribute("Key", registryKeyPath); + if (wixVersion.compareTo("3.6") < 0) { + xml.writeAttribute("Action", "createAndRemoveOnUninstall"); + } + xml.writeStartElement("RegistryValue"); + xml.writeAttribute("Type", "string"); + xml.writeAttribute("KeyPath", "yes"); + xml.writeAttribute("Name", nameAttr.get()); + xml.writeAttribute("Value", valueAttr.get()); + xml.writeEndElement(); // + xml.writeEndElement(); // + } + + private String addDirectoryCleaner(XMLStreamWriter xml, Path path) throws + XMLStreamException, IOException { + if (wixVersion.compareTo("3.6") < 0) { + return null; + } + + // rm -rf + final String baseId = Id.of(path, "rm_rf"); + final String propertyId = baseId.toUpperCase(); + final String componentId = ("c" + baseId); + + xml.writeStartElement("Property"); + xml.writeAttribute("Id", propertyId); + xml.writeStartElement("RegistrySearch"); + xml.writeAttribute("Id", Id.of(path, "regsearch")); + xml.writeAttribute("Root", systemWide ? "HKLM" : "HKCU"); + xml.writeAttribute("Key", registryKeyPath); + xml.writeAttribute("Type", "raw"); + xml.writeAttribute("Name", propertyId); + xml.writeEndElement(); // + xml.writeEndElement(); // + + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", INSTALLDIR.toString()); + xml.writeStartElement("Component"); + xml.writeAttribute("Id", componentId); + xml.writeAttribute("Guid", "*"); + + addRegistryKeyPath(xml, INSTALLDIR, () -> propertyId, () -> { + // The following code converts a path to value to be saved in registry. + // E.g.: + // INSTALLDIR -> [INSTALLDIR] + // TERGETDIR/ProgramFiles64Folder/foo/bar -> [ProgramFiles64Folder]foo/bar + final Path rootDir = KNOWN_DIRS.stream() + .sorted(Comparator.comparing(Path::getNameCount).reversed()) + .filter(path::startsWith) + .findFirst().get(); + StringBuilder sb = new StringBuilder(); + sb.append(String.format("[%s]", rootDir.getFileName().toString())); + sb.append(rootDir.relativize(path).toString()); + return sb.toString(); + }); + + xml.writeStartElement( + "http://schemas.microsoft.com/wix/UtilExtension", + "RemoveFolderEx"); + xml.writeAttribute("On", "uninstall"); + xml.writeAttribute("Property", propertyId); + xml.writeEndElement(); // + xml.writeEndElement(); // + xml.writeEndElement(); // + + return componentId; + } + + private static IllegalArgumentException throwInvalidPathException(Path v) { + throw new IllegalArgumentException(String.format("Invalid path [%s]", v)); + } + + enum ShortcutsFolder { + ProgramMenu(PROGRAM_MENU_PATH), + Desktop(DESKTOP_PATH); + + private ShortcutsFolder(Path root) { + this.root = root; + } + + Path getPath(WixSourcesBuilder outer) { + if (this == ProgramMenu) { + return root.resolve(outer.programMenuFolderName); + } + return root; + } + + private final Path root; + } + + private DottedVersion wixVersion; + + private boolean systemWide; + + private String registryKeyPath; + + private Path installDir; + + private String programMenuFolderName; + + private List associations; + + private Set shortcutFolders; + + private List launcherPaths; + + private ApplicationLayout appImage; + private ApplicationLayout installedAppImage; + + private Map removeFolderItems; + private Set defaultedMimes; + + private final static Path TARGETDIR = Path.of("TARGETDIR"); + + private final static Path INSTALLDIR = Path.of("INSTALLDIR"); + + private final static Set ROOT_DIRS = Set.of(INSTALLDIR, TARGETDIR); + + private final static Path PROGRAM_MENU_PATH = TARGETDIR.resolve("ProgramMenuFolder"); + + private final static Path DESKTOP_PATH = TARGETDIR.resolve("DesktopFolder"); + + private final static Path PROGRAM_FILES = TARGETDIR.resolve("ProgramFiles64Folder"); + + private final static Path LOCAL_PROGRAM_FILES = TARGETDIR.resolve("LocalAppDataFolder"); + + private final static Set SYSTEM_DIRS = Set.of(TARGETDIR, + PROGRAM_MENU_PATH, DESKTOP_PATH, PROGRAM_FILES, LOCAL_PROGRAM_FILES); + + private final static Set KNOWN_DIRS = Stream.of(Set.of(INSTALLDIR), + SYSTEM_DIRS).flatMap(Set::stream).collect( + Collectors.toUnmodifiableSet()); + + private final static Set USER_PROFILE_DIRS = Set.of(LOCAL_PROGRAM_FILES, + PROGRAM_MENU_PATH, DESKTOP_PATH); + + private static final StandardBundlerParam MENU_HINT = + new WindowsBundlerParam<>( + Arguments.CLIOptions.WIN_MENU_HINT.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, + // and we actually do want null in some cases + (s, p) -> (s == null || + "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s) + ); + + private static final StandardBundlerParam SHORTCUT_HINT = + new WindowsBundlerParam<>( + Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, + // and we actually do want null in some cases + (s, p) -> (s == null || + "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s) + ); +} diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java Fri Oct 18 14:14:37 2019 -0400 @@ -0,0 +1,146 @@ +/* + * Copyright (c) 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.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.text.MessageFormat; +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * WiX tool. + */ +public enum WixTool { + Candle, Light; + + static final class ToolInfo { + ToolInfo(Path path, String version) { + this.path = path; + this.version = new DottedVersion(version); + } + + final Path path; + final DottedVersion version; + } + + static Map toolset() throws ConfigException { + Map toolset = new HashMap<>(); + for (var tool : values()) { + toolset.put(tool, tool.find()); + } + return toolset; + } + + ToolInfo find() throws ConfigException { + final Path toolFileName = IOUtils.addSuffix( + Path.of(name().toLowerCase()), ".exe"); + + String[] version = new String[1]; + ConfigException reason = createToolValidator(toolFileName, version).get(); + if (version[0] != null) { + if (reason == null) { + // Found in PATH. + return new ToolInfo(toolFileName, version[0]); + } + + // Found in PATH, but something went wrong. + throw reason; + } + + for (var dir : findWixInstallDirs()) { + Path path = dir.resolve(toolFileName); + if (path.toFile().exists()) { + reason = createToolValidator(path, version).get(); + if (reason != null) { + throw reason; + } + return new ToolInfo(path, version[0]); + } + } + + throw reason; + } + + private static Supplier createToolValidator(Path toolPath, + String[] versionCtnr) { + return new ToolValidator(toolPath) + .setCommandLine("/?") + .setMinimalVersion(MINIMAL_VERSION) + .setToolNotFoundErrorHandler( + (name, ex) -> new ConfigException( + I18N.getString("error.no-wix-tools"), + I18N.getString("error.no-wix-tools.advice"))) + .setToolOldVersionErrorHandler( + (name, version) -> new ConfigException( + MessageFormat.format(I18N.getString( + "message.wrong-tool-version"), name, + version, MINIMAL_VERSION), + I18N.getString("error.no-wix-tools.advice"))) + .setVersionParser(output -> { + versionCtnr[0] = ""; + String firstLineOfOutput = output.findFirst().orElse(""); + int separatorIdx = firstLineOfOutput.lastIndexOf(' '); + if (separatorIdx == -1) { + return null; + } + versionCtnr[0] = firstLineOfOutput.substring(separatorIdx + 1); + return versionCtnr[0]; + })::validate; + } + + private final static String MINIMAL_VERSION = "3.0"; + + private final static Path PROGRAM_FILES = Path.of("C:\\Program Files"); + private final static Path PROGRAM_FILES_X86 = Path.of("C:\\Program Files (x86)"); + + private static List findWixInstallDirs() { + PathMatcher wixInstallDirMatcher = FileSystems.getDefault().getPathMatcher( + "glob:WiX Toolset v*"); + + // Returns list of WiX install directories ordered by WiX version number. + // Newer versions go first. + return Stream.of(PROGRAM_FILES, PROGRAM_FILES_X86).map(path -> { + List result; + try (var paths = Files.walk(path, 1)) { + result = paths.collect(Collectors.toList()); + } catch (IOException ex) { + Log.verbose(ex); + result = Collections.emptyList(); + } + return result; + }).flatMap(List::stream) + .filter(path -> wixInstallDirMatcher.matches(path.getFileName())) + .sorted(Comparator.comparing(Path::getFileName).reversed()) + .map(path -> path.resolve("bin")) + .collect(Collectors.toList()); + } +} diff -r 64adf683bc7b -r 61c44899b4eb src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs Fri Oct 18 11:00:57 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs Fri Oct 18 14:14:37 2019 -0400 @@ -1,15 +1,13 @@ - + - - - + - - + + + + + + + - However, RemoveFolderEx is only available in WiX 3.6, - we will comment it out if we running older WiX. + + + + + + + + + + 1 + + + 1 + + + !(loc.message.install.dir.exist) + + - RemoveFolderEx requires that we "remember" the path for uninstall. - Read the path value and set the APPLICATIONFOLDER property with the value. - --> - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - 1 - - - !(loc.message.install.dir.exist) - - - - - - + + + + + 1 + INSTALLDIR_VALID="0" + INSTALLDIR_VALID="1" - 1 - INSTALLDIR_VALID="0" - INSTALLDIR_VALID="1" - - - - 1 - 1 - + + + 1 + 1 + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/helpers/jdk/jpackage/test/.Executor.java.swp Binary file test/jdk/tools/jpackage/helpers/jdk/jpackage/test/.Executor.java.swp has changed diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java Fri Oct 18 14:14:37 2019 -0400 @@ -23,10 +23,11 @@ package jdk.jpackage.test; import java.nio.file.Path; +import java.util.HashMap; import java.util.Map; -public class FileAssociations { +final public class FileAssociations { public FileAssociations(String faSuffixName) { suffixName = faSuffixName; setFilename("fa"); @@ -34,22 +35,36 @@ } public void createFile() { - TKit.createPropertiesFile(file, - Map.entry("extension", suffixName), - Map.entry("mime-type", getMime()), - Map.entry("description", description)); + Map entries = new HashMap<>(Map.of( + "extension", suffixName, + "mime-type", getMime(), + "description", description + )); + if (icon != null) { + if (TKit.isWindows()) { + entries.put("icon", icon.toString().replace("\\", "/")); + } else { + entries.put("icon", icon.toString()); + } + } + TKit.createPropertiesFile(file, entries); } - final public FileAssociations setFilename(String v) { + public FileAssociations setFilename(String v) { file = TKit.workDir().resolve(v + ".properties"); return this; } - final public FileAssociations setDescription(String v) { + public FileAssociations setDescription(String v) { description = v; return this; } + public FileAssociations setIcon(Path v) { + icon = v; + return this; + } + public Path getPropertiesFile() { return file; } @@ -65,4 +80,5 @@ private Path file; final private String suffixName; private String description; + private Path icon; } diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java Fri Oct 18 14:14:37 2019 -0400 @@ -126,7 +126,7 @@ if (moduleName == null && CLASS_NAME.equals(qualifiedClassName)) { // Use Hello.java as is. cmd.addAction((self) -> { - File jarFile = self.inputDir().resolve(jarFileName).toFile(); + Path jarFile = self.inputDir().resolve(jarFileName); createJarBuilder().setOutputJar(jarFile).addSourceFile( HELLO_JAVA).create(); }); @@ -144,8 +144,7 @@ } TKit.withTempDirectory("src", - workDir -> prepareSources(workDir).setOutputJar( - jarFile.toFile()).create()); + workDir -> prepareSources(workDir).setOutputJar(jarFile).create()); }); } diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java Fri Oct 18 14:14:37 2019 -0400 @@ -40,7 +40,7 @@ sourceFiles = new ArrayList<>(); } - public JarBuilder setOutputJar(File v) { + public JarBuilder setOutputJar(Path v) { outputJar = v; return this; } @@ -69,9 +69,16 @@ .addPathArguments(sourceFiles) .execute().assertExitCodeIsZero(); } - Path tmpJar = workDir.resolve("foo.jar"); - Executor jarExe = new Executor(); - jarExe.setToolProvider(JavaTool.JAR).addArguments("-c", "-f", tmpJar.toString()); + + Files.createDirectories(outputJar.getParent()); + if (Files.exists(outputJar)) { + TKit.trace(String.format("Delete [%s] existing jar file", outputJar)); + Files.deleteIfExists(outputJar); + } + + Executor jarExe = new Executor() + .setToolProvider(JavaTool.JAR) + .addArguments("-c", "-f", outputJar.toString()); if (moduleVersion != null) { jarExe.addArguments(String.format("--module-version=%s", moduleVersion)); @@ -81,12 +88,10 @@ } jarExe.addArguments("-C", workDir.toString(), "."); jarExe.execute().assertExitCodeIsZero(); - outputJar.getParentFile().mkdirs(); - Files.copy(tmpJar, outputJar.toPath(), REPLACE_EXISTING); }); } private List sourceFiles; - private File outputJar; + private Path outputJar; private String mainClass; private String moduleVersion; } diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java Fri Oct 18 14:14:37 2019 -0400 @@ -22,7 +22,8 @@ */ package jdk.jpackage.test; -import java.util.Objects; +import java.io.File; +import java.nio.file.Path; public final class JavaAppDesc { @@ -58,6 +59,11 @@ return qualifiedClassName; } + public Path classFilePath() { + return Path.of(qualifiedClassName.replace(".", File.separator) + + ".class"); + } + public String moduleName() { return moduleName; } diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java Fri Oct 18 14:14:37 2019 -0400 @@ -33,6 +33,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.jpackage.internal.AppImageFile; import static jdk.jpackage.test.PackageType.*; /** @@ -83,7 +84,8 @@ return this; } - private PackageTest addInitializer(ThrowingConsumer v, String id) { + private PackageTest addInitializer(ThrowingConsumer v, + String id) { if (id != null) { if (namedInitializers.contains(id)) { return this; @@ -149,8 +151,10 @@ return this; } - static void withTestFileAssociationsFile(FileAssociations fa, ThrowingConsumer consumer) { - final String testFileDefaultName = String.join(".", "test", fa.getSuffix()); + static void withTestFileAssociationsFile(FileAssociations fa, + ThrowingConsumer consumer) { + final String testFileDefaultName = String.join(".", "test", + fa.getSuffix()); TKit.withTempFile(testFileDefaultName, fa.getSuffix(), testFile -> { if (TKit.isLinux()) { LinuxHelper.initFileAssociationsTestFile(testFile); @@ -347,6 +351,9 @@ } } + TKit.assertPathExists(cmd.appInstallationDirectory().resolve( + AppImageFile.FILENAME), false); + installVerifiers.stream().forEach(v -> v.accept(cmd)); } @@ -413,7 +420,8 @@ bundleOutputDir = new File(val).getAbsoluteFile(); if (!bundleOutputDir.isDirectory()) { - throw new IllegalArgumentException(String.format("Invalid value of %s sytem property: [%s]. Should be existing directory", + throw new IllegalArgumentException(String.format( + "Invalid value of %s sytem property: [%s]. Should be existing directory", TKit.getConfigPropertyName(propertyName), bundleOutputDir)); } @@ -425,7 +433,9 @@ String action = Optional.ofNullable(TKit.getConfigProperty(propertyName)).orElse( Action.CREATE.toString()).toLowerCase(); DEFAULT_ACTION = Stream.of(Action.values()).filter( - a -> a.toString().equals(action)).findFirst().orElseThrow(() -> new IllegalArgumentException(String.format("Unrecognized value of %s property: [%s]", + a -> a.toString().equals(action)).findFirst().orElseThrow( + () -> new IllegalArgumentException(String.format( + "Unrecognized value of %s property: [%s]", TKit.getConfigPropertyName(propertyName), action))); } } diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java Fri Oct 18 14:14:37 2019 -0400 @@ -45,6 +45,8 @@ final public class TKit { + private static final String OS = System.getProperty("os.name").toLowerCase(); + public static final Path TEST_SRC_ROOT = Functional.identity(() -> { Path root = Path.of(System.getProperty("test.src")); @@ -58,6 +60,22 @@ throw new RuntimeException("Failed to locate apps directory"); }).get(); + public final static String ICON_SUFFIX = Functional.identity(() -> { + if (isOSX()) { + return ".icns"; + } + + if (isLinux()) { + return ".png"; + } + + if (isWindows()) { + return ".ico"; + } + + throw throwUnknownPlatformError(); + }).get(); + public static void run(String args[], ThrowingRunnable testBody) { if (currentTest != null) { throw new IllegalStateException( @@ -816,6 +834,4 @@ VERBOSE_TEST_SETUP = isNonOf.test(Set.of("init", "i")); } } - - private static final String OS = System.getProperty("os.name").toLowerCase(); } diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java Fri Oct 18 14:14:37 2019 -0400 @@ -153,6 +153,7 @@ this.afterActions = Collections.emptyList(); this.testDesc = TestDesc.createBuilder().get(); this.dryRun = false; + this.workDir = createWorkDirName(testDesc); } TestInstance(MethodCall testBody, List beforeActions, @@ -164,6 +165,7 @@ this.afterActions = afterActions; this.testDesc = testBody.createDescription(); this.dryRun = dryRun; + this.workDir = createWorkDirName(testDesc); } void notifyAssert() { @@ -205,28 +207,7 @@ } Path workDir() { - Path result = Path.of("."); - List components = new ArrayList<>(); - - String testFunctionName = functionName(); - if (testFunctionName != null) { - components.add(testFunctionName); - } - - if (isPrametrized()) { - components.add(String.format("%08x", fullName().hashCode())); - } - - if (!components.isEmpty()) { - result = result.resolve(String.join(".", components)); - } - - return result; - } - - boolean isPrametrized() { - return Stream.of(testDesc.functionArgs, testDesc.instanceArgs).anyMatch( - Objects::nonNull); + return workDir; } @Override @@ -239,7 +220,7 @@ testInstance)); try { if (!dryRun) { - Files.createDirectories(workDir()); + Files.createDirectories(workDir); testBody.accept(testInstance); } } finally { @@ -255,7 +236,7 @@ } if (!KEEP_WORK_DIR.contains(status)) { - TKit.deleteDirectoryRecursive(workDir()); + TKit.deleteDirectoryRecursive(workDir); } TKit.log(String.format("%s %s; checks=%d", status, fullName, @@ -274,6 +255,42 @@ return null; } + private static boolean isCalledByJavatest() { + StackTraceElement st[] = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : st) { + if (ste.getClassName().startsWith("com.sun.javatest.")) { + return true; + } + } + return false; + } + + private static Path createWorkDirName(TestDesc testDesc) { + Path result = Path.of("."); + if (!isCalledByJavatest()) { + result = result.resolve(testDesc.clazz.getSimpleName()); + } + + List components = new ArrayList<>(); + + final String testFunctionName = testDesc.functionName; + if (testFunctionName != null) { + components.add(testFunctionName); + } + + final boolean isPrametrized = Stream.of(testDesc.functionArgs, + testDesc.instanceArgs).anyMatch(Objects::nonNull); + if (isPrametrized) { + components.add(String.format("%08x", testDesc.testFullName().hashCode())); + } + + if (!components.isEmpty()) { + result = result.resolve(String.join(".", components)); + } + + return result; + } + private enum Status { Passed("[ OK ]"), Failed("[ FAILED ]"), @@ -300,6 +317,7 @@ private final List beforeActions; private final List afterActions; private final boolean dryRun; + private final Path workDir; private final static Set KEEP_WORK_DIR = Functional.identity( () -> { diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java --- a/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java Fri Oct 18 14:14:37 2019 -0400 @@ -154,7 +154,7 @@ private AppImageFile createFromXml(String... xmlData) throws IOException { Path directory = tempFolder.getRoot().toPath(); - Path path = directory.resolve(AppImageFile.XML_FILENAME); + Path path = directory.resolve(AppImageFile.FILENAME); path.toFile().mkdirs(); Files.delete(path); diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/junit/jdk/jpackage/internal/CompareDottedVersionTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/CompareDottedVersionTest.java Fri Oct 18 14:14:37 2019 -0400 @@ -0,0 +1,84 @@ +/* + * Copyright (c) 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. + * + * 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.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import junit.framework.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import static org.junit.Assert.*; + +@RunWith(Parameterized.class) +public class CompareDottedVersionTest { + + public CompareDottedVersionTest(String version1, String version2, int result) { + this.version1 = version1; + this.version2 = version2; + this.expectedResult = result; + } + + @Parameters + public static List data() { + return List.of(new Object[][] { + { "00.0.0", "0", 0 }, + { "0.035", "0.0035", 0 }, + { "1", "1", 0 }, + { "2", "2.0", 0 }, + { "2.00", "2.0", 0 }, + { "1.2.3.4", "1.2.3.4.5", -1 }, + { "34", "33", 1 }, + { "34.0.78", "34.1.78", -1 } + }); + } + + @Test + public void testIt() { + int actualResult = compare(version1, version2); + assertEquals(expectedResult, actualResult); + + int actualNegateResult = compare(version2, version1); + assertEquals(actualResult, -1 * actualNegateResult); + } + + private static int compare(String x, String y) { + int result = new DottedVersion(x).compareTo(y); + + if (result < 0) { + return -1; + } + + if (result > 0) { + return 1; + } + + return 0; + } + + private final String version1; + private final String version2; + private final int expectedResult; +} diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/junit/jdk/jpackage/internal/DottedVersionTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/DottedVersionTest.java Fri Oct 18 14:14:37 2019 -0400 @@ -0,0 +1,64 @@ +/* + * Copyright (c) 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. + * + * 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.util.stream.Stream; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import static org.junit.Assert.*; + +public class DottedVersionTest { + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + @Test + public void testValid() { + Stream.of( + "1.0", + "1", + "2.234.045", + "2.234.0", + "0", + "0.1" + ).forEach(value -> { + DottedVersion version = new DottedVersion(value); + assertEquals(version.toString(), value); + }); + } + + @Test + public void testNull() { + exceptionRule.expect(NullPointerException.class); + new DottedVersion(null); + } + + @Test + public void testEmpty() { + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("Version may not be empty string"); + new DottedVersion(""); + } +} diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/junit/jdk/jpackage/internal/InvalidDottedVersionTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/InvalidDottedVersionTest.java Fri Oct 18 14:14:37 2019 -0400 @@ -0,0 +1,68 @@ +/* + * Copyright (c) 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. + * + * 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.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class InvalidDottedVersionTest { + + public InvalidDottedVersionTest(String version) { + this.version = version; + } + + @Parameters + public static List data() { + return Stream.of( + "1.-1", + "5.", + "4.2.", + "3..2", + "2.a", + "0a", + ".", + " ", + " 1", + "1. 2" + ).map(version -> new Object[] { version }).collect(Collectors.toList()); + } + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + @Test + public void testIt() { + exceptionRule.expect(IllegalArgumentException.class); + new DottedVersion(version); + } + + private final String version; +} diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/junit/jdk/jpackage/internal/PathGroupTest.java --- a/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/PathGroupTest.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/PathGroupTest.java Fri Oct 18 14:14:37 2019 -0400 @@ -24,19 +24,27 @@ */ package jdk.jpackage.internal; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.*; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; public class PathGroupTest { - public PathGroupTest() { - } + + @Rule + public final TemporaryFolder tempFolder = new TemporaryFolder(); @Test(expected = NullPointerException.class) public void testNullId() { @@ -125,7 +133,139 @@ assertEquals(aPath, pg2.roots().get(0)); } + @Test + public void testTransform() throws IOException { + for (var transform : TransformType.values()) { + testTransform(false, transform); + } + } + + @Test + public void testTransformWithExcludes() throws IOException { + for (var transform : TransformType.values()) { + testTransform(true, transform); + } + } + + enum TransformType { Copy, Move, Handler }; + + private void testTransform(boolean withExcludes, TransformType transform) + throws IOException { + final PathGroup pg = new PathGroup(Map.of(0, PATH_FOO, 1, PATH_BAR, 2, + PATH_EMPTY, 3, PATH_BAZ)); + + final Path srcDir = tempFolder.newFolder().toPath(); + final Path dstDir = tempFolder.newFolder().toPath(); + + Files.createDirectories(srcDir.resolve(PATH_FOO).resolve("a/b/c/d")); + Files.createFile(srcDir.resolve(PATH_FOO).resolve("a/b/c/file1")); + Files.createFile(srcDir.resolve(PATH_FOO).resolve("a/b/file2")); + Files.createFile(srcDir.resolve(PATH_FOO).resolve("a/b/file3")); + Files.createFile(srcDir.resolve(PATH_BAR)); + Files.createFile(srcDir.resolve(PATH_EMPTY).resolve("file4")); + Files.createDirectories(srcDir.resolve(PATH_BAZ).resolve("1/2/3")); + + var dst = pg.resolveAt(dstDir); + var src = pg.resolveAt(srcDir); + if (withExcludes) { + // Exclude from transformation. + src.setPath(new Object(), srcDir.resolve(PATH_FOO).resolve("a/b/c")); + src.setPath(new Object(), srcDir.resolve(PATH_EMPTY).resolve("file4")); + } + + var srcFilesBeforeTransform = walkFiles(srcDir); + + if (transform == TransformType.Handler) { + List> copyFile = new ArrayList<>(); + List createDirectory = new ArrayList<>(); + src.transform(dst, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + copyFile.add(Map.entry(src, dst)); + } + + @Override + public void createDirectory(Path dir) throws IOException { + createDirectory.add(dir); + } + }); + + Consumer assertFile = path -> { + var entry = Map.entry(srcDir.resolve(path), dstDir.resolve(path)); + assertTrue(copyFile.contains(entry)); + }; + + Consumer assertDir = path -> { + assertTrue(createDirectory.contains(dstDir.resolve(path))); + }; + + assertEquals(withExcludes ? 3 : 5, copyFile.size()); + assertEquals(withExcludes ? 8 : 10, createDirectory.size()); + + assertFile.accept(PATH_FOO.resolve("a/b/file2")); + assertFile.accept(PATH_FOO.resolve("a/b/file3")); + assertFile.accept(PATH_BAR); + assertDir.accept(PATH_FOO.resolve("a/b")); + assertDir.accept(PATH_FOO.resolve("a")); + assertDir.accept(PATH_FOO); + assertDir.accept(PATH_BAZ); + assertDir.accept(PATH_BAZ.resolve("1")); + assertDir.accept(PATH_BAZ.resolve("1/2")); + assertDir.accept(PATH_BAZ.resolve("1/2/3")); + assertDir.accept(PATH_EMPTY); + + if (!withExcludes) { + assertFile.accept(PATH_FOO.resolve("a/b/c/file1")); + assertFile.accept(PATH_EMPTY.resolve("file4")); + assertDir.accept(PATH_FOO.resolve("a/b/c/d")); + assertDir.accept(PATH_FOO.resolve("a/b/c")); + } + + assertArrayEquals(new Path[] { Path.of("") }, walkFiles(dstDir)); + return; + } + + if (transform == TransformType.Copy) { + src.copy(dst); + } else if (transform == TransformType.Move) { + src.move(dst); + } + + final List excludedPaths; + if (withExcludes) { + excludedPaths = List.of( + PATH_EMPTY.resolve("file4"), + PATH_FOO.resolve("a/b/c") + ); + } else { + excludedPaths = Collections.emptyList(); + } + UnaryOperator removeExcludes = paths -> { + return Stream.of(paths) + .filter(path -> !excludedPaths.stream().anyMatch( + path::startsWith)) + .collect(Collectors.toList()).toArray(Path[]::new); + }; + + var dstFiles = walkFiles(dstDir); + assertArrayEquals(removeExcludes.apply(srcFilesBeforeTransform), dstFiles); + + if (transform == TransformType.Copy) { + assertArrayEquals(dstFiles, removeExcludes.apply(walkFiles(srcDir))); + } else if (transform == TransformType.Move) { + assertFalse(Files.exists(srcDir)); + } + } + + private static Path[] walkFiles(Path root) throws IOException { + try (var files = Files.walk(root)) { + return files.map(root::relativize).sorted().collect( + Collectors.toList()).toArray(Path[]::new); + } + } + private final static Path PATH_FOO = Path.of("foo"); private final static Path PATH_BAR = Path.of("bar"); + private final static Path PATH_BAZ = Path.of("baz"); private final static Path PATH_EMPTY = Path.of(""); } diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/share/FileAssociationsTest.java --- a/test/jdk/tools/jpackage/share/FileAssociationsTest.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/share/FileAssociationsTest.java Fri Oct 18 14:14:37 2019 -0400 @@ -21,6 +21,7 @@ * questions. */ +import java.nio.file.Path; import jdk.jpackage.test.TKit; import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.FileAssociations; @@ -42,6 +43,11 @@ * On Linux use "echo > foo.jptest1" and not "touch foo.jptest1" to create test * file as empty files are always interpreted as plain text and will not be * opened with the test app. This is a known bug. + * + * Icon associated with the main launcher should be associated with files with + * ".jptest1" suffix. Different icon should be associated with files with with + * ".jptest2" suffix. Icon for files with ".jptest1" suffix is platform specific + * and is one of 'icon.*' files in test/jdk/tools/jpackage/resources directory. */ /* @@ -60,8 +66,12 @@ PackageTest packageTest = new PackageTest(); applyFileAssociations(packageTest, new FileAssociations("jptest1")); + + Path icon = TKit.TEST_SRC_ROOT.resolve(Path.of("resources", "icon" + + TKit.ICON_SUFFIX)); + applyFileAssociations(packageTest, - new FileAssociations("jptest2").setFilename("fa2")); + new FileAssociations("jptest2").setFilename("fa2").setIcon(icon)); packageTest.run(); } diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/share/IconTest.java --- a/test/jdk/tools/jpackage/share/IconTest.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/share/IconTest.java Fri Oct 18 14:14:37 2019 -0400 @@ -25,6 +25,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import jdk.jpackage.internal.IOUtils; import jdk.jpackage.test.TKit; import jdk.jpackage.test.Functional; import jdk.jpackage.test.Annotations.*; @@ -70,8 +71,8 @@ } private static String appIconFileName(JPackageCommand cmd) { - return cmd.appLauncherPath().getFileName().toString().replaceAll( - "\\.[^.]*$", "") + ICON_SUFFIX; + return IOUtils.replaceSuffix(cmd.appLauncherPath().getFileName(), + TKit.ICON_SUFFIX).toString(); } private static void testIt(JPackageCommand cmd) throws IOException { @@ -87,22 +88,6 @@ iconPath, GOLDEN_ICON)); } - private final static String ICON_SUFFIX = Functional.identity(() -> { - if (TKit.isOSX()) { - return ".icns"; - } - - if (TKit.isLinux()) { - return ".png"; - } - - if (TKit.isWindows()) { - return ".ico"; - } - - throw TKit.throwUnknownPlatformError(); - }).get(); - private final static Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of( - "resources", "icon" + ICON_SUFFIX)); + "resources", "icon" + TKit.ICON_SUFFIX)); } diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/share/RuntimePackageTest.java --- a/test/jdk/tools/jpackage/share/RuntimePackageTest.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/share/RuntimePackageTest.java Fri Oct 18 14:14:37 2019 -0400 @@ -48,7 +48,7 @@ * @comment Temporary disable for Linux and OSX until functionality implemented * @requires (os.family != "mac") * @modules jdk.jpackage/jdk.jpackage.internal - * @run main/othervm/timeout=360 -Xmx512m RuntimePackageTest + * @run main/othervm/timeout=720 -Xmx512m RuntimePackageTest */ public class RuntimePackageTest { diff -r 64adf683bc7b -r 61c44899b4eb test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java --- a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java Fri Oct 18 11:00:57 2019 -0400 +++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java Fri Oct 18 14:14:37 2019 -0400 @@ -53,7 +53,6 @@ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main * --jpt-run=jdk.jpackage.tests.MainClassTest * --jpt-space-subst=_ - * --jpt-exclude=modular=y;_main-class=n;_jar-main-class=b;_jlink=y */ public final class MainClassTest { @@ -64,7 +63,7 @@ } Script modular(boolean v) { - appDesc.setModuleName(v ? null : "com.other"); + appDesc.setModuleName(v ? "com.other" : null); return this; } @@ -138,9 +137,12 @@ script.appDesc.packageName(), "ThereIsNoSuchClass").filter( Objects::nonNull).collect(Collectors.joining(".")); - cmd = JPackageCommand.helloAppImage(script.appDesc); + cmd = JPackageCommand + .helloAppImage(script.appDesc) + .ignoreDefaultRuntime(true); if (!script.withJLink) { - cmd.setFakeRuntime(); + cmd.addArguments("--runtime-image", Path.of(System.getProperty( + "java.home"))); } final String moduleName = script.appDesc.moduleName(); @@ -173,13 +175,6 @@ for (var modular : List.of(true, false)) { for (var mainClass : Script.MainClassType.values()) { for (var jarMainClass : Script.MainClassType.values()) { - if (!withJLink && (jarMainClass == SetWrong || mainClass - == SetWrong)) { - // Without runtime can't run app to verify it will fail, so - // there is no point in such testing. - continue; - } - Script script = new Script() .modular(modular) .withJLink(withJLink) @@ -190,10 +185,10 @@ || withMainClass.contains(mainClass)) { } else if (modular) { script.expectedErrorMessage( - "A main class was not specified nor was one found in the jar"); + "Error: Main application class is missing"); } else { script.expectedErrorMessage( - "Error: Main application class is missing"); + "A main class was not specified nor was one found in the jar"); } scripts.add(new Script[]{script}); @@ -250,13 +245,13 @@ } } - private void initJarWithWrongMainClass() { + private void initJarWithWrongMainClass() throws IOException { // Call JPackageCommand.executePrerequisiteActions() to build app's jar. // executePrerequisiteActions() is called by JPackageCommand instance // only once. cmd.executePrerequisiteActions(); - Path jarFile; + final Path jarFile; if (script.appDesc.moduleName() != null) { jarFile = Path.of(cmd.getArgumentValue("--module-path"), script.appDesc.jarFileName()); @@ -264,37 +259,58 @@ jarFile = cmd.inputDir().resolve(cmd.getArgumentValue("--main-jar")); } - // Create jar file with the main class attribute in manifest set to - // non-existing class. + // Create new jar file filtering out main class from the old jar file. TKit.withTempDirectory("repack-jar", workDir -> { - Path manifestFile = workDir.resolve("META-INF/MANIFEST.MF"); - try (var jar = new JarFile(jarFile.toFile())) { - jar.stream() - .filter(Predicate.not(JarEntry::isDirectory)) - .sequential().forEachOrdered(ThrowingConsumer.toConsumer( - jarEntry -> { - try (var in = jar.getInputStream(jarEntry)) { - Path fileName = workDir.resolve(jarEntry.getName()); - Files.createDirectories(fileName.getParent()); - Files.copy(in, fileName); - } - })); - } + // Extract app's class from the old jar. + explodeJar(jarFile, workDir, + jarEntry -> Path.of(jarEntry.getName()).equals( + script.appDesc.classFilePath())); + + // Create app's jar file with different main class. + var badAppDesc = JavaAppDesc.parse(script.appDesc.toString()).setClassName( + nonExistingMainClass); + JPackageCommand.helloAppImage(badAppDesc).executePrerequisiteActions(); + + // Extract new jar but skip app's class. + explodeJar(jarFile, workDir, + jarEntry -> !Path.of(jarEntry.getName()).equals( + badAppDesc.classFilePath())); + + // At this point we should have: + // 1. Manifest from the new jar referencing non-existing class + // as the main class. + // 2. Module descriptor referencing non-existing class as the main + // class in case of modular app. + // 3. App's class from the old jar. We need it to let jlink find some + // classes in the package declared in module descriptor + // in case of modular app. Files.delete(jarFile); - - // Adjust manifest. - TKit.createTextFile(manifestFile, Files.readAllLines( - manifestFile).stream().map(line -> line.replace( - script.appDesc.className(), nonExistingMainClass))); - new Executor().setToolProvider(JavaTool.JAR) - .addArguments("-c", "-M", "-f", jarFile.toString()) + .addArguments("-v", "-c", "-M", "-f", jarFile.toString()) .addArguments("-C", workDir.toString(), ".") + .dumpOutput() .execute().assertExitCodeIsZero(); }); } + private static void explodeJar(Path jarFile, Path workDir, + Predicate filter) throws IOException { + try (var jar = new JarFile(jarFile.toFile())) { + jar.stream() + .filter(Predicate.not(JarEntry::isDirectory)) + .filter(filter) + .sequential().forEachOrdered(ThrowingConsumer.toConsumer( + jarEntry -> { + try (var in = jar.getInputStream(jarEntry)) { + Path fileName = workDir.resolve(jarEntry.getName()); + Files.createDirectories(fileName.getParent()); + Files.copy(in, fileName); + } + })); + } + } + private final JPackageCommand cmd; private final Script script; private final String nonExistingMainClass;