# HG changeset patch # User herrick # Date 1569870794 14400 # Node ID a5f66aa04f68b1f5d171196764d7d7a8809018ee # Parent fd45b7e2c027563f4567f374425529ce022b61d7 8230920 : jpackage problems when -input dir contains any files with "cfg" extension. Reviewed-by: asemenyuk, almatvee diff -r fd45b7e2c027 -r a5f66aa04f68 src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java Mon Sep 30 15:13:14 2019 -0400 @@ -0,0 +1,218 @@ +/* + * 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; + } + +} diff -r fd45b7e2c027 -r a5f66aa04f68 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinAppBundler.java --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinAppBundler.java Thu Sep 26 10:37:37 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinAppBundler.java Mon Sep 30 15:13:14 2019 -0400 @@ -98,61 +98,6 @@ return true; } - private static boolean usePredefineAppName(Map p) { - return (PREDEFINED_APP_IMAGE.fetchFrom(p) != null); - } - - private static String appName; - synchronized static String getAppName( - Map p) { - // If we building from predefined app image, then we should use names - // from image and not from CLI. - if (usePredefineAppName(p)) { - if (appName == null) { - // Use WIN_APP_IMAGE here, since we already copy pre-defined - // image to WIN_APP_IMAGE - File appImageDir = WIN_APP_IMAGE.fetchFrom(p); - - File appDir = new File(appImageDir.toString() + "\\app"); - File [] files = appDir.listFiles( - (File dir, String name) -> name.endsWith(".cfg")); - if (files == null || files.length == 0) { - String name = APP_NAME.fetchFrom(p); - Path exePath = appImageDir.toPath().resolve(name + ".exe"); - Path icoPath = appImageDir.toPath().resolve(name + ".ico"); - if (exePath.toFile().exists() && - icoPath.toFile().exists()) { - return name; - } else { - throw new RuntimeException(MessageFormat.format( - I18N.getString("error.cannot-find-launcher"), - appImageDir)); - } - } else { - appName = files[0].getName(); - int index = appName.indexOf("."); - if (index != -1) { - appName = appName.substring(0, index); - } - if (files.length > 1) { - Log.error(MessageFormat.format(I18N.getString( - "message.multiple-launchers"), appName)); - } - } - return appName; - } else { - return appName; - } - } - - return APP_NAME.fetchFrom(p); - } - - public static String getLauncherRelativePath( - Map p) { - return getAppName(p) + ".exe"; - } - public boolean bundle(Map p, File outputDirectory) throws PackagerException { return doBundle(p, outputDirectory, false) != null; diff -r fd45b7e2c027 -r a5f66aa04f68 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java Thu Sep 26 10:37:37 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java Mon Sep 30 15:13:14 2019 -0400 @@ -542,6 +542,25 @@ 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 { @@ -558,39 +577,29 @@ xml.writeStartDocument(); xml.writeStartElement("Include"); - File launcher = new File(imageRootDir, - WinAppBundler.getLauncherRelativePath(params)); - if (launcher.exists()) { - String iconPath = launcher.getAbsolutePath().replace( - ".exe", ".ico"); - if (MENU_HINT.fetchFrom(params)) { - xml.writeStartElement("Icon"); - xml.writeAttribute("Id", "StartMenuIcon.exe"); - xml.writeAttribute("SourceFile", iconPath); - xml.writeEndElement(); - } - if (SHORTCUT_HINT.fetchFrom(params)) { - xml.writeStartElement("Icon"); - xml.writeAttribute("Id", "DesktopIcon.exe"); - xml.writeAttribute("SourceFile", iconPath); - xml.writeEndElement(); - } + String[] launcherNames = getLaunchers(params); + + File[] icons = new File[launcherNames.length]; + for (int i=0; i sl = addLaunchers.get(i); - if (SHORTCUT_HINT.fetchFrom(sl) || MENU_HINT.fetchFrom(sl)) { - File addLauncher = new File(imageRootDir, - WinAppBundler.getLauncherRelativePath(sl)); - String addLauncherPath - = relativePath(imageRootDir, addLauncher); - String addLauncherIconPath - = addLauncherPath.replace(".exe", ".ico"); + for (int i = 0; i < icons.length; i++) { + if (icons[i].exists()) { + String iconPath = icons[i].getAbsolutePath(); - xml.writeStartElement("Icon"); - xml.writeAttribute("Id", "Launcher" + i + ".exe"); - xml.writeAttribute("SourceFile", addLauncherIconPath); - xml.writeEndElement(); + if (MENU_HINT.fetchFrom(params)) { + xml.writeStartElement("Icon"); + xml.writeAttribute("Id", "StartMenuIcon.exe" + i); + xml.writeAttribute("SourceFile", iconPath); + xml.writeEndElement(); + } + if (SHORTCUT_HINT.fetchFrom(params)) { + xml.writeStartElement("Icon"); + xml.writeAttribute("Id", "DesktopIcon.exe" + i); + xml.writeAttribute("SourceFile", iconPath); + xml.writeEndElement(); + } } } @@ -718,21 +727,16 @@ out.println(prefix + " "); - boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params); + File imageRootDir = WIN_APP_IMAGE.fetchFrom(params); - File launcherFile = new File(imageRootDir, - WinAppBundler.getLauncherRelativePath(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 - - for (File f: files) { - boolean isLauncher = f.equals(launcherFile); - if (isLauncher) { - needRegistryKey = true; - } - } + 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 @@ -748,21 +752,23 @@ out.println(prefix + " "); } - boolean menuShortcut = MENU_HINT.fetchFrom(params); - boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params); + 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 isLauncher = f.equals(launcherFile); - - launcherSet = launcherSet || isLauncher; + boolean isMainLauncher = + launcherFiles.length > 0 && f.equals(launcherFiles[0]); - boolean doShortcuts = - isLauncher && (menuShortcut || desktopShortcut); + launcherSet = launcherSet || isMainLauncher; - String thisFileId = isLauncher ? LAUNCHER_ID : ("FileId" + (id++)); + String thisFileId = isMainLauncher ? LAUNCHER_ID : ("FileId" + (id++)); idToFileMap.put(f.getName(), thisFileId); out.println(prefix + " "); - if (doShortcuts && desktopShortcut) { + if (isMainLauncher && desktopShortcut) { out.println(prefix + " "); } - if (doShortcuts && menuShortcut) { + if (isMainLauncher && menuShortcut) { out.println(prefix + " "); } - List> addLaunchers = - ADD_LAUNCHERS.fetchFrom(params); - for (int i = 0; i < addLaunchers.size(); i++) { - Map sl = addLaunchers.get(i); - File addLauncherFile = new File(imageRootDir, - WinAppBundler.getLauncherRelativePath(sl)); - if (f.equals(addLauncherFile)) { - if (SHORTCUT_HINT.fetchFrom(sl)) { + // any additional launchers + for (int index = 1; index < launcherNames.length; index++ ) { + + if (f.equals(launcherFiles[index])) { + if (desktopShortcut) { out.println(prefix + " "); + + " Advertise=\"no\" Icon=\"DesktopIcon.exe" + + index + "\"" + + " IconIndex=\"0\" />"); } - if (MENU_HINT.fetchFrom(sl)) { + if (menuShortcut) { out.println(prefix - + " "); - // Should we allow different menu groups? Not for now. + + " "); } } } diff -r fd45b7e2c027 -r a5f66aa04f68 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java Thu Sep 26 10:37:37 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java Mon Sep 30 15:13:14 2019 -0400 @@ -187,6 +187,7 @@ } catch (PackagerException pe) { throw new RuntimeException(pe); } + AppImageFile.save(root, params); // create the .exe launchers createLauncherForEntryPoint(params); diff -r fd45b7e2c027 -r a5f66aa04f68 test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java Mon Sep 30 15:13:14 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.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.LinkedHashMap; +import org.junit.Assert; +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +public class AppImageFileTest { + + @Rule + public final TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testIdentity() throws IOException { + Map params = new LinkedHashMap<>(); + params.put("name", "Foo"); + params.put("app-version", "2.3"); + params.put("description", "Duck is the King"); + AppImageFile aif = create(params); + + Assert.assertEquals("Foo", aif.getLauncherName()); + } + + @Test + public void testInvalidCommandLine() throws IOException { + // Just make sure AppImageFile will tolerate jpackage params that would + // never create app image at both load/save phases. + // People would edit this file just because they can. + // We should be ready to handle curious minds. + Map params = new LinkedHashMap<>(); + params.put("invalidParamName", "randomStringValue"); + create(params); + + params = new LinkedHashMap<>(); + params.put("name", "foo"); + params.put("app-version", ""); + create(params); + } + + @Test + public void testInavlidXml() throws IOException { + assertInvalid(createFromXml("")); + assertInvalid(createFromXml("")); + assertInvalid(createFromXml( + "", + "", + "")); + } + + @Test + public void testValidXml() throws IOException { + Assert.assertEquals("Foo", (createFromXml( + "", + "Foo", + "")).getLauncherName()); + + Assert.assertEquals("Foo", (createFromXml( + "", + "Foo", + "Bar", + "")).getLauncherName()); + + var file = createFromXml( + "", + "Foo", + "", + ""); + Assert.assertEquals("Foo", file.getLauncherName()); + Assert.assertArrayEquals(new String[0], + file.getAddLauncherNames().toArray(String[]::new)); + } + + @Test + public void testMainLauncherName() throws IOException { + Map params = new LinkedHashMap<>(); + params.put("name", "Foo"); + params.put("description", "Duck App Description"); + AppImageFile aif = create(params); + + Assert.assertEquals("Foo", aif.getLauncherName()); + } + + @Test + public void testAddLauncherNames() throws IOException { + } + + private AppImageFile create(Map params) throws IOException { + AppImageFile.save(tempFolder.getRoot().toPath(), params); + return AppImageFile.load(tempFolder.getRoot().toPath()); + } + + private void assertInvalid(AppImageFile file) { + Assert.assertNull(file.getLauncherName()); + Assert.assertNull(file.getAddLauncherNames()); + } + + private AppImageFile createFromXml(String... xmlData) throws IOException { + Path directory = tempFolder.getRoot().toPath(); + Path path = directory.resolve(AppImageFile.XML_FILENAME); + path.toFile().mkdirs(); + Files.delete(path); + + ArrayList data = new ArrayList(); + data.add(""); + data.addAll(List.of(xmlData)); + + Files.write(path, data, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + + AppImageFile image = AppImageFile.load(directory); + return image; + } + +} diff -r fd45b7e2c027 -r a5f66aa04f68 test/jdk/tools/jpackage/junit/junit.java --- a/test/jdk/tools/jpackage/junit/junit.java Thu Sep 26 10:37:37 2019 -0400 +++ b/test/jdk/tools/jpackage/junit/junit.java Mon Sep 30 15:13:14 2019 -0400 @@ -1,3 +1,28 @@ +/* + * 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. + */ + /* * @test * @summary jpackage unit tests @@ -6,4 +31,5 @@ * jdk.jpackage.internal.PathGroupTest * jdk.jpackage.internal.DeployParamsTest * jdk.jpackage.internal.ApplicationLayoutTest + * jdk.jpackage.internal.AppImageFileTest */