From db88a85596140deb71377eb9506400c48e8af7d1 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 23 Jul 2025 16:50:32 -0400 Subject: [PATCH 01/83] LinuxHelper: fix a typo --- .../tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 1669d1f8233c9..3a1cad55c8731 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -340,7 +340,7 @@ static void verifyPackageBundleEssential(JPackageCommand cmd) { vitalPackage, prerequisites, packageName)); } else { TKit.trace(String.format( - "Not cheking %s required packages of [%s] package", + "Not checking %s required packages of [%s] package", prerequisites, packageName)); } } From 66f527c2d222c118d6f8c48009952341810c5915 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 24 Jul 2025 11:22:17 -0400 Subject: [PATCH 02/83] AdditionalLauncher: add AdditionalLauncher.PropertyFile() ctor; AppImageFile: support additional launchers. --- .../jdk/jpackage/test/AdditionalLauncher.java | 13 ++-- .../jdk/jpackage/test/AppImageFile.java | 69 +++++++++++++++++-- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java index 801df8624c4c1..687c2ef420662 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java @@ -23,7 +23,7 @@ package jdk.jpackage.test; import static java.util.stream.Collectors.toMap; -import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; import java.io.IOException; import java.nio.file.Files; @@ -179,11 +179,12 @@ static PropertyFile getAdditionalLauncherProperties( PropertyFile shell[] = new PropertyFile[1]; forEachAdditionalLauncher(cmd, (name, propertiesFilePath) -> { if (name.equals(launcherName)) { - shell[0] = toFunction(PropertyFile::new).apply( - propertiesFilePath); + shell[0] = toSupplier(() -> { + return new PropertyFile(propertiesFilePath); + }).get(); } }); - return Optional.of(shell[0]).get(); + return Objects.requireNonNull(shell[0]); } private void initialize(JPackageCommand cmd) throws IOException { @@ -390,6 +391,10 @@ protected void verify(JPackageCommand cmd) throws IOException { public static final class PropertyFile { + PropertyFile(Map data) { + this.data = Map.copyOf(data); + } + PropertyFile(Path path) throws IOException { data = Files.readAllLines(path).stream().map(str -> { return str.split("=", 2); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java index 2381aecec2ea1..e676e0d1e878c 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java @@ -22,20 +22,28 @@ */ package jdk.jpackage.test; +import static java.util.stream.Collectors.toMap; +import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathFactory; import jdk.internal.util.OperatingSystem; import jdk.jpackage.internal.util.XmlUtils; -import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; public record AppImageFile(String mainLauncherName, String mainLauncherClassName, - String version, boolean macSigned, boolean macAppStore) { + String version, boolean macSigned, boolean macAppStore, Map> launchers) { public static Path getPathInAppImage(Path appImageDir) { return ApplicationLayout.platformAppImage() @@ -44,8 +52,23 @@ public static Path getPathInAppImage(Path appImageDir) { .resolve(FILENAME); } + public AppImageFile { + Objects.requireNonNull(mainLauncherName); + Objects.requireNonNull(mainLauncherClassName); + Objects.requireNonNull(version); + if (!launchers.containsKey(mainLauncherName)) { + throw new IllegalArgumentException(); + } + } + public AppImageFile(String mainLauncherName, String mainLauncherClassName) { - this(mainLauncherName, mainLauncherClassName, "1.0", false, false); + this(mainLauncherName, mainLauncherClassName, "1.0", false, false, Map.of(mainLauncherName, Map.of())); + } + + public Map> addLaunchers() { + return launchers.entrySet().stream().filter(e -> { + return !e.getKey().equals(mainLauncherName); + }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); } public void save(Path appImageDir) throws IOException { @@ -73,6 +96,18 @@ public void save(Path appImageDir) throws IOException { xml.writeStartElement("app-store"); xml.writeCharacters(Boolean.toString(macAppStore)); xml.writeEndElement(); + + for (var al : addLaunchers().keySet().stream().sorted().toList()) { + xml.writeStartElement("add-launcher"); + xml.writeAttribute("name", al); + var props = launchers.get(al); + for (var prop : props.keySet().stream().sorted().toList()) { + xml.writeStartElement(prop); + xml.writeCharacters(props.get(prop)); + xml.writeEndElement(); + } + xml.writeEndElement(); + } }); } @@ -99,8 +134,34 @@ public static AppImageFile load(Path appImageDir) { "/jpackage-state/app-store/text()", doc)).map( Boolean::parseBoolean).orElse(false); + var addLaunchers = XmlUtils.queryNodes(doc, xPath, "/jpackage-state/add-launcher").map(Element.class::cast).map(toFunction(addLauncher -> { + Map launcherProps = new HashMap<>(); + + // @name and @service attributes. + XmlUtils.toStream(addLauncher.getAttributes()).forEach(attr -> { + launcherProps.put(attr.getNodeName(), attr.getNodeValue()); + }); + + // Extra properties. + XmlUtils.queryNodes(addLauncher, xPath, "*[count(*) = 0]").map(Element.class::cast).forEach(e -> { + launcherProps.put(e.getNodeName(), e.getTextContent()); + }); + + return launcherProps; + })); + + var mainLauncherProperties = Map.of("name", mainLauncherName); + + var launchers = Stream.concat(Stream.of(mainLauncherProperties), addLaunchers).collect(toMap(attrs -> { + return Objects.requireNonNull(attrs.get("name")); + }, attrs -> { + Map copy = new HashMap<>(attrs); + copy.remove("name"); + return Map.copyOf(copy); + })); + return new AppImageFile(mainLauncherName, mainLauncherClassName, - version, macSigned, macAppStore); + version, macSigned, macAppStore, launchers); }).get(); } From 53ea833d4709ac5cbffbe0ce093ea1a21b085bcf Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Tue, 22 Jul 2025 21:53:23 -0400 Subject: [PATCH 03/83] Introduce MsiDatabase; Implement launcher shortcut verification in MSI bundles. With this patch, there is no longer a need to install an MSI package to verify shortcuts. --- .../jdk/jpackage/test/MsiDatabase.java | 377 ++++++++++++++++++ .../jdk/jpackage/test/PackageTest.java | 17 +- .../jpackage/test/WinShortcutVerifier.java | 278 +++++++++++++ .../jdk/jpackage/test/WindowsHelper.java | 177 ++++++-- .../tools/jpackage/resources/msi-export.js | 81 ++++ .../jpackage/resources/query-msi-property.js | 65 --- 6 files changed, 891 insertions(+), 104 deletions(-) create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java create mode 100644 test/jdk/tools/jpackage/resources/msi-export.js delete mode 100644 test/jdk/tools/jpackage/resources/query-msi-property.js diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java new file mode 100644 index 0000000000000..87236575e2ef6 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2025, 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.test; + + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + + +final class MsiDatabase { + + static MsiDatabase load(Path msiFile, Path idtFileOutputDir, Set tableNames) { + try { + Files.createDirectories(idtFileOutputDir); + + var orderedTableNames = tableNames.stream().sorted().toList(); + + Executor.of("cscript.exe", "//Nologo") + .addArgument(TKit.TEST_SRC_ROOT.resolve("resources/msi-export.js")) + .addArgument(msiFile) + .addArgument(idtFileOutputDir) + .addArguments(orderedTableNames.stream().map(Table::tableName).toList()) + .dumpOutput() + .execute(0); + + var tables = orderedTableNames.stream().map(tableName -> { + return Map.entry(tableName, idtFileOutputDir.resolve(tableName + ".idt")); + }).filter(e -> { + return Files.exists(e.getValue()); + }).collect(Collectors.toMap(Map.Entry::getKey, e -> { + return MsiTable.loadFromTextArchiveFile(e.getValue()); + })); + + return new MsiDatabase(tables); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + + enum Table { + COMPONENT("Component"), + DIRECTORY("Directory"), + FILE("File"), + PROPERTY("Property"), + SHORTCUT("Shortcut"), + ; + + Table(String name) { + this.tableName = Objects.requireNonNull(name); + } + + String tableName() { + return tableName; + } + + private final String tableName; + + static final Set
FIND_PROPERTY_REQUIRED_TABLES = Set.of(PROPERTY); + static final Set
LIST_SHORTCUTS_REQUIRED_TABLES = Set.of(COMPONENT, DIRECTORY, FILE, SHORTCUT); + } + + + private MsiDatabase(Map tables) { + this.tables = Map.copyOf(tables); + } + + Set
tableNames() { + return tables.keySet(); + } + + MsiDatabase append(MsiDatabase other) { + Map newTables = new HashMap<>(tables); + newTables.putAll(other.tables); + return new MsiDatabase(newTables); + } + + Optional findProperty(String propertyName) { + Objects.requireNonNull(propertyName); + return tables.get(Table.PROPERTY).findRow("Property", propertyName).map(row -> { + return row.apply("Value"); + }); + } + + Collection listShortcuts() { + var shortcuts = tables.get(Table.SHORTCUT); + if (shortcuts == null) { + return List.of(); + } + return IntStream.range(0, shortcuts.rowCount()).mapToObj(i -> { + var row = shortcuts.row(i); + var shortcutPath = directoryPath(row.apply("Directory_")).resolve(fileNameFromFieldValue(row.apply("Name"))); + var workDir = directoryPath(row.apply("WkDir")); + var shortcutTarget = Path.of(expandFormattedString(row.apply("Target"))); + return new Shortcut(shortcutPath, shortcutTarget, workDir); + }).toList(); + } + + record Shortcut(Path path, Path target, Path workDir) { + + Shortcut { + Objects.requireNonNull(path); + Objects.requireNonNull(target); + Objects.requireNonNull(workDir); + } + + void assertEquals(Shortcut expected) { + TKit.assertEquals(expected.path, path, "Check the shortcut path"); + TKit.assertEquals(expected.target, target, "Check the shortcut target"); + TKit.assertEquals(expected.workDir, workDir, "Check the shortcut work directory"); + } + } + + private Path directoryPath(String directoryId) { + var table = tables.get(Table.DIRECTORY); + Path result = null; + for (var row = table.findRow("Directory", directoryId); + row.isPresent(); + directoryId = row.get().apply("Directory_Parent"), row = table.findRow("Directory", directoryId)) { + + Path pathComponent; + if (DIRECTORY_PROPERTIES.contains(directoryId)) { + pathComponent = Path.of(directoryId); + directoryId = null; + } else { + pathComponent = fileNameFromFieldValue(row.get().apply("DefaultDir")); + } + + if (result != null) { + result = pathComponent.resolve(result); + } else { + result = pathComponent; + } + + if (directoryId == null) { + break; + } + } + + return Objects.requireNonNull(result); + } + + private String expandFormattedString(String str) { + return expandFormattedString(str, token -> { + if (token.charAt(0) == '#') { + var filekey = token.substring(1); + var fileRow = tables.get(Table.FILE).findRow("File", filekey).orElseThrow(); + + var component = fileRow.apply("Component_"); + var componentRow = tables.get(Table.COMPONENT).findRow("Component", component).orElseThrow(); + + var fileName = fileNameFromFieldValue(fileRow.apply("FileName")); + var filePath = directoryPath(componentRow.apply("Directory_")); + + return filePath.resolve(fileName).toString(); + } else { + throw new UnsupportedOperationException(String.format( + "Unrecognized token [%s] in formatted string [%s]", token, str)); + } + }); + } + + private static Path fileNameFromFieldValue(String fieldValue) { + var pipeIdx = fieldValue.indexOf('|'); + if (pipeIdx < 0) { + return Path.of(fieldValue); + } else { + return Path.of(fieldValue.substring(pipeIdx + 1)); + } + } + + private static String expandFormattedString(String str, Function callback) { + // Naive implementation of https://learn.microsoft.com/en-us/windows/win32/msi/formatted + // - No recursive property expansion. + // - No curly brakes ({}) handling. + + Objects.requireNonNull(str); + Objects.requireNonNull(callback); + var sb = new StringBuffer(); + var m = FORMATTED_STRING_TOKEN.matcher(str); + while (m.find()) { + var token = m.group(); + token = token.substring(1, token.length() - 1); + if (token.equals("~")) { + m.appendReplacement(sb, "\0"); + } else { + var replacement = Matcher.quoteReplacement(callback.apply(token)); + m.appendReplacement(sb, replacement); + } + } + m.appendTail(sb); + return sb.toString(); + } + + + private record MsiTable(Map> columns) { + + MsiTable { + Objects.requireNonNull(columns); + if (columns.isEmpty()) { + throw new IllegalArgumentException("Table should have columns"); + } + } + + Optional> findRow(String columnName, String fieldValue) { + Objects.requireNonNull(columnName); + Objects.requireNonNull(fieldValue); + var column = columns.get(columnName); + for (int i = 0; i != column.size(); i++) { + if (fieldValue.equals(column.get(i))) { + return Optional.of(row(i)); + } + } + return Optional.empty(); + } + + /** + * Loads a table from a text archive file. + * @param idtFile path to the input text archive file + * @return the table + */ + static MsiTable loadFromTextArchiveFile(Path idtFile) { + + var header = IdtFileHeader.loadFromTextArchiveFile(idtFile); + + Map> columns = new HashMap<>(); + header.columns.forEach(column -> { + columns.put(column, new ArrayList<>()); + }); + + try { + var lines = Files.readAllLines(idtFile, header.charset()).toArray(String[]::new); + for (int i = 3; i != lines.length; i++) { + var line = lines[i]; + var row = line.split("\t", -1); + if (row.length != header.columns().size()) { + throw new IllegalArgumentException(String.format( + "Expected %d columns. Actual is %d in line %d in [%s] file", + header.columns().size(), row.length, i, idtFile)); + } + for (int j = 0; j != row.length; j++) { + var field = row[j]; + // https://learn.microsoft.com/en-us/windows/win32/msi/archive-file-format + field = field.replace((char)21, (char)0); + field = field.replace((char)27, '\b'); + field = field.replace((char)16, '\t'); + field = field.replace((char)25, '\n'); + field = field.replace((char)24, '\f'); + field = field.replace((char)17, '\r'); + columns.get(header.columns.get(j)).add(field); + } + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + + return new MsiTable(columns); + } + + int columnCount() { + return columns.size(); + } + + int rowCount() { + return columns.values().stream().findAny().orElseThrow().size(); + } + + Function row(int rowIndex) { + return columnName -> { + var column = Objects.requireNonNull(columns.get(Objects.requireNonNull(columnName))); + return column.get(rowIndex); + }; + } + } + + + private record IdtFileHeader(Charset charset, List columns) { + + IdtFileHeader { + Objects.requireNonNull(charset); + columns.forEach(Objects::requireNonNull); + if (columns.isEmpty()) { + throw new IllegalArgumentException("Table should have columns"); + } + } + + /** + * Loads a table header from a text archive (.idt) file. + * @see https://learn.microsoft.com/en-us/windows/win32/msi/archive-file-format + * @see https://learn.microsoft.com/en-us/windows/win32/msi/ascii-data-in-text-archive-files + * @param path path to the input text archive file + * @return the table header + */ + static IdtFileHeader loadFromTextArchiveFile(Path idtFile) { + var charset = StandardCharsets.US_ASCII; + try (var stream = Files.lines(idtFile, charset)) { + var headerLines = stream.limit(3).toList(); + if (headerLines.size() != 3) { + throw new IllegalArgumentException(String.format( + "[%s] file should have at least three text lines", idtFile)); + } + + var columns = headerLines.get(0).split("\t"); + + var header = headerLines.get(2).split("\t", 4); + if (header.length == 3) { + if (Pattern.matches("^[1-9]\\d+$", header[0])) { + charset = Charset.forName(header[0]); + } else { + throw new IllegalArgumentException(String.format( + "Unexpected charset name [%s] in [%s] file", header[0], idtFile)); + } + } else if (header.length != 2) { + throw new IllegalArgumentException(String.format( + "Unexpected number of fields (%d) in the 3rd line of [%s] file", + header.length, idtFile)); + } + + return new IdtFileHeader(charset, List.of(columns)); + + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + } + + + private final Map tables; + + // https://learn.microsoft.com/en-us/windows/win32/msi/formatted + private static final Pattern FORMATTED_STRING_TOKEN = Pattern.compile("\\[[^\\]]+\\]"); + + // https://learn.microsoft.com/en-us/windows/win32/msi/property-reference#system-folder-properties + private final Set DIRECTORY_PROPERTIES = Set.of( + "DesktopFolder", + "LocalAppDataFolder", + "ProgramFiles64Folder", + "ProgramMenuFolder" + ); +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index 61e4ccdb4a263..6afb0eca38db1 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -30,6 +30,7 @@ import static jdk.jpackage.test.PackageType.MAC_PKG; import static jdk.jpackage.test.PackageType.NATIVE; import static jdk.jpackage.test.PackageType.WINDOWS; +import static jdk.jpackage.test.PackageType.WIN_MSI; import java.awt.GraphicsEnvironment; import java.io.IOException; @@ -745,6 +746,8 @@ private void verifyPackageBundle(JPackageCommand cmd, if (expectedJPackageExitCode == 0) { if (isOfType(cmd, LINUX)) { LinuxHelper.verifyPackageBundleEssential(cmd); + } else if (isOfType(cmd, WIN_MSI)) { + WinShortcutVerifier.verifyBundleShortcuts(cmd); } } bundleVerifiers.forEach(v -> v.accept(cmd, result)); @@ -766,12 +769,7 @@ private void verifyPackageInstalled(JPackageCommand cmd) { if (!cmd.isRuntime()) { if (isOfType(cmd, WINDOWS) && !cmd.isPackageUnpacked("Not verifying desktop integration")) { - // Check main launcher - WindowsHelper.verifyDesktopIntegration(cmd, null); - // Check additional launchers - cmd.addLauncherNames().forEach(name -> { - WindowsHelper.verifyDesktopIntegration(cmd, name); - }); + WindowsHelper.verifyDeployedDesktopIntegration(cmd, true); } } @@ -848,12 +846,7 @@ private void verifyPackageUninstalled(JPackageCommand cmd) { TKit.assertPathExists(cmd.appLauncherPath(), false); if (isOfType(cmd, WINDOWS)) { - // Check main launcher - WindowsHelper.verifyDesktopIntegration(cmd, null); - // Check additional launchers - cmd.addLauncherNames().forEach(name -> { - WindowsHelper.verifyDesktopIntegration(cmd, name); - }); + WindowsHelper.verifyDeployedDesktopIntegration(cmd, false); } } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java new file mode 100644 index 0000000000000..10c6209c08925 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2019, 2025, 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.test; + +import static jdk.jpackage.test.WindowsHelper.getInstallationSubDirectory; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.internal.util.PathUtils; +import jdk.jpackage.test.MsiDatabase.Shortcut; +import jdk.jpackage.test.WindowsHelper.SpecialFolder; + + +final class WinShortcutVerifier { + + static void verifyBundleShortcuts(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.WIN_MSI); + + if (Stream.of("--win-menu", "--win-shortcut").noneMatch(cmd::hasArgument) && cmd.addLauncherNames().isEmpty()) { + return; + } + + var actualShortcuts = WindowsHelper.getMsiShortcuts(cmd).stream().collect(Collectors.groupingBy(shortcut -> { + return PathUtils.replaceSuffix(shortcut.target().getFileName(), "").toString(); + })); + + var expectedShortcuts = expectShortcuts(cmd); + + var launcherNames = expectedShortcuts.keySet().stream().sorted().toList(); + + TKit.assertStringListEquals( + launcherNames, + actualShortcuts.keySet().stream().sorted().toList(), + "Check the list of launchers with shortcuts"); + + Function, List> sorter = shortcuts -> { + return shortcuts.stream().sorted(SHORTCUT_COMPARATOR).toList(); + }; + + for (var name : launcherNames) { + var actualLauncherShortcuts = sorter.apply(actualShortcuts.get(name)); + var expectedLauncherShortcuts = sorter.apply(expectedShortcuts.get(name)); + + TKit.assertEquals(expectedLauncherShortcuts.size(), actualLauncherShortcuts.size(), + String.format("Check the number of shortcuts of [%s] launcher", name)); + + for (int i = 0; i != expectedLauncherShortcuts.size(); i++) { + TKit.trace(String.format("Verify shortcut #%d of [%s] launcher", i + 1, name)); + actualLauncherShortcuts.get(i).assertEquals(expectedLauncherShortcuts.get(i)); + TKit.trace("Done"); + } + } + } + + static void verifyDeployedShortcuts(JPackageCommand cmd, boolean installed) { + cmd.verifyIsOfType(PackageType.WINDOWS); + + verifyDeployedShortcutsInternal(cmd, installed); + var copyCmd = new JPackageCommand(cmd); + if (copyCmd.hasArgument("--win-per-user-install")) { + copyCmd.removeArgument("--win-per-user-install"); + } else { + copyCmd.addArgument("--win-per-user-install"); + } + verifyDeployedShortcutsInternal(copyCmd, false); + } + + private static void verifyDeployedShortcutsInternal(JPackageCommand cmd, boolean installed) { + + var expectedShortcuts = expectShortcuts(cmd).values().stream().flatMap(Collection::stream).toList(); + + var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); + + expectedShortcuts.stream().map(Shortcut::path).sorted().map(path -> { + return resolvePath(path, !isUserLocalInstall); + }).map(path -> { + return PathUtils.addSuffix(path, ".lnk"); + }).forEach(path -> { + if (installed) { + TKit.assertFileExists(path); + } else { + TKit.assertPathExists(path, false); + } + }); + + if (!installed) { + expectedShortcuts.stream().map(Shortcut::path).filter(path -> { + return Stream.of(ShortcutType.COMMON_START_MENU, ShortcutType.USER_START_MENU).anyMatch(type -> { + return path.startsWith(Path.of(type.rootFolder().getMsiPropertyName())); + }); + }).map(Path::getParent).distinct().map(unresolvedShortcutDir -> { + return resolvePath(unresolvedShortcutDir, !isUserLocalInstall); + }).forEach(shortcutDir -> { + if (Files.isDirectory(shortcutDir)) { + TKit.assertDirectoryNotEmpty(shortcutDir); + } else { + TKit.assertPathExists(shortcutDir, false); + } + }); + } + } + + private enum ShortcutType { + COMMON_START_MENU(SpecialFolder.COMMON_START_MENU_PROGRAMS), + USER_START_MENU(SpecialFolder.USER_START_MENU_PROGRAMS), + COMMON_DESKTOP(SpecialFolder.COMMON_DESKTOP), + USER_DESKTOP(SpecialFolder.USER_DESKTOP), + ; + + ShortcutType(SpecialFolder rootFolder) { + this.rootFolder = Objects.requireNonNull(rootFolder); + } + + SpecialFolder rootFolder() { + return rootFolder; + } + + private final SpecialFolder rootFolder; + } + + private static Path resolvePath(Path path, boolean allUsers) { + var root = path.getName(0); + var resolvedRoot = SpecialFolder.findMsiProperty(root.toString(), allUsers).orElseThrow().getPath(); + return resolvedRoot.resolve(root.relativize(path)); + } + + private static Shortcut createLauncherShortcutSpec(JPackageCommand cmd, String launcherName, + SpecialFolder installRoot, Path workDir, ShortcutType type) { + + var name = Optional.ofNullable(launcherName).orElseGet(cmd::name); + + var appLayout = ApplicationLayout.windowsAppImage().resolveAt( + Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd))); + + Path path; + switch (type) { + case COMMON_START_MENU, USER_START_MENU -> { + path = Path.of(cmd.getArgumentValue("--win-menu-group", () -> "Unknown"), name); + } + default -> { + path = Path.of(name); + } + } + + return new Shortcut( + Path.of(type.rootFolder().getMsiPropertyName()).resolve(path), + appLayout.launchersDirectory().resolve(name + ".exe"), + workDir); + } + + private static Collection expectLauncherShortcuts(JPackageCommand cmd, + Optional predefinedAppImage, String launcherName) { + Objects.requireNonNull(cmd); + Objects.requireNonNull(predefinedAppImage); + + List shortcuts = new ArrayList<>(); + + var name = Optional.ofNullable(launcherName).orElseGet(cmd::name); + + boolean isWinMenu; + boolean isDesktop; + if (name.equals(cmd.name())) { + isWinMenu = cmd.hasArgument("--win-menu"); + isDesktop = cmd.hasArgument("--win-shortcut"); + } else { + var props = predefinedAppImage.map(v -> { + return v.launchers().get(name); + }).map(appImageFileLauncherProps -> { + Map convProps = new HashMap<>(); + for (var e : Map.of("menu", "win-menu", "shortcut", "win-shortcut").entrySet()) { + Optional.ofNullable(appImageFileLauncherProps.get(e.getKey())).ifPresent(v -> { + convProps.put(e.getValue(), v); + }); + } + return new AdditionalLauncher.PropertyFile(convProps); + }).orElseGet(() -> { + return AdditionalLauncher.getAdditionalLauncherProperties(cmd, launcherName); + }); + isWinMenu = props.getPropertyBooleanValue("win-menu").orElseGet(() -> cmd.hasArgument("--win-menu")); + isDesktop = props.getPropertyBooleanValue("win-shortcut").orElseGet(() -> cmd.hasArgument("--win-shortcut")); + } + + var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); + + SpecialFolder installRoot; + if (isUserLocalInstall) { + installRoot = SpecialFolder.LOCAL_APPLICATION_DATA; + } else { + installRoot = SpecialFolder.PROGRAM_FILES; + } + + var workDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd)); + + if (isWinMenu) { + ShortcutType type; + if (isUserLocalInstall) { + type = ShortcutType.USER_START_MENU; + } else { + type = ShortcutType.COMMON_START_MENU; + } + shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, workDir, type)); + } + + if (isDesktop) { + ShortcutType type; + if (isUserLocalInstall) { + type = ShortcutType.USER_DESKTOP; + } else { + type = ShortcutType.COMMON_DESKTOP; + } + shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, workDir, type)); + } + + return shortcuts; + } + + private static Map> expectShortcuts(JPackageCommand cmd) { + Map> expectedShortcuts = new HashMap<>(); + + var predefinedAppImage = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of).map(AppImageFile::load); + + predefinedAppImage.map(v -> { + return v.launchers().keySet().stream(); + }).orElseGet(() -> { + return Stream.concat(Stream.of(cmd.name()), cmd.addLauncherNames().stream()); + }).forEach(launcherName -> { + var shortcuts = expectLauncherShortcuts(cmd, predefinedAppImage, launcherName); + if (!shortcuts.isEmpty()) { + expectedShortcuts.put(launcherName, shortcuts); + } + }); + + return expectedShortcuts; + } + + addShortcuts.accept(cmd.name()); + predefinedAppImage.map(v -> { + return (Collection)v.addLaunchers().keySet(); + }).orElseGet(cmd::addLauncherNames).forEach(addShortcuts); + + return expectedShortcuts; + } + + private static final Comparator SHORTCUT_COMPARATOR = Comparator.comparing(Shortcut::target) + .thenComparing(Comparator.comparing(Shortcut::path)) + .thenComparing(Comparator.comparing(Shortcut::workDir)); +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 3ac302ce0fed4..8f52a464250d3 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -26,9 +26,14 @@ import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.ref.SoftReference; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; +import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -63,7 +68,7 @@ private static Path getInstallationRootDirectory(JPackageCommand cmd) { return PROGRAM_FILES; } - private static Path getInstallationSubDirectory(JPackageCommand cmd) { + static Path getInstallationSubDirectory(JPackageCommand cmd) { cmd.verifyIsOfType(PackageType.WINDOWS); return Path.of(cmd.getArgumentValue("--install-dir", cmd::name)); } @@ -263,19 +268,24 @@ static Optional toShortPath(Path path) { } } - static void verifyDesktopIntegration(JPackageCommand cmd, - String launcherName) { - new DesktopIntegrationVerifier(cmd, launcherName); + static void verifyDeployedDesktopIntegration(JPackageCommand cmd, boolean installed) { + WinShortcutVerifier.verifyDeployedShortcuts(cmd, installed); + // Check the main launcher + new DesktopIntegrationVerifier(cmd, installed, null); + // Check additional launchers + cmd.addLauncherNames().forEach(name -> { + new DesktopIntegrationVerifier(cmd, installed, name); + }); } public static String getMsiProperty(JPackageCommand cmd, String propertyName) { cmd.verifyIsOfType(PackageType.WIN_MSI); - return Executor.of("cscript.exe", "//Nologo") - .addArgument(TKit.TEST_SRC_ROOT.resolve("resources/query-msi-property.js")) - .addArgument(cmd.outputBundle()) - .addArgument(propertyName) - .dumpOutput() - .executeAndGetOutput().stream().collect(Collectors.joining("\n")); + return MsiDatabaseCache.INSTANCE.findProperty(cmd.outputBundle(), propertyName).orElseThrow(); + } + + static Collection getMsiShortcuts(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.WIN_MSI); + return MsiDatabaseCache.INSTANCE.listShortcuts(cmd.outputBundle()); } public static String getExecutableDesciption(Path pathToExeFile) { @@ -386,7 +396,7 @@ private static long[] findAppLauncherPIDs(JPackageCommand cmd, String launcherNa } } - private static boolean isUserLocalInstall(JPackageCommand cmd) { + static boolean isUserLocalInstall(JPackageCommand cmd) { return cmd.hasArgument("--win-per-user-install"); } @@ -396,14 +406,14 @@ private static boolean isPathTooLong(Path path) { private static class DesktopIntegrationVerifier { - DesktopIntegrationVerifier(JPackageCommand cmd, String launcherName) { + DesktopIntegrationVerifier(JPackageCommand cmd, boolean installed, String launcherName) { cmd.verifyIsOfType(PackageType.WINDOWS); name = Optional.ofNullable(launcherName).orElseGet(cmd::name); isUserLocalInstall = isUserLocalInstall(cmd); - appInstalled = cmd.appLauncherPath(launcherName).toFile().exists(); + this.appInstalled = installed; desktopShortcutPath = Path.of(name + ".lnk"); @@ -611,7 +621,12 @@ private enum SpecialFolderDotNet { CommonDesktop, Programs, - CommonPrograms; + CommonPrograms, + + ProgramFiles, + + LocalApplicationData, + ; Path getPath() { final var str = Executor.of("powershell", "-NoLogo", "-NoProfile", @@ -636,33 +651,84 @@ Optional findValue() { } } - private enum SpecialFolder { - COMMON_START_MENU_PROGRAMS(SYSTEM_SHELL_FOLDERS_REGKEY, "Common Programs", SpecialFolderDotNet.CommonPrograms), - USER_START_MENU_PROGRAMS(USER_SHELL_FOLDERS_REGKEY, "Programs", SpecialFolderDotNet.Programs), - - COMMON_DESKTOP(SYSTEM_SHELL_FOLDERS_REGKEY, "Common Desktop", SpecialFolderDotNet.CommonDesktop), - USER_DESKTOP(USER_SHELL_FOLDERS_REGKEY, "Desktop", SpecialFolderDotNet.Desktop); - - SpecialFolder(String keyPath, String valueName) { - reg = new RegValuePath(keyPath, valueName); + enum SpecialFolder { + COMMON_START_MENU_PROGRAMS( + SYSTEM_SHELL_FOLDERS_REGKEY, + "Common Programs", + "ProgramMenuFolder", + SpecialFolderDotNet.CommonPrograms), + USER_START_MENU_PROGRAMS( + USER_SHELL_FOLDERS_REGKEY, + "Programs", + "ProgramMenuFolder", + SpecialFolderDotNet.Programs), + + COMMON_DESKTOP( + SYSTEM_SHELL_FOLDERS_REGKEY, + "Common Desktop", + "DesktopFolder", + SpecialFolderDotNet.CommonDesktop), + USER_DESKTOP( + USER_SHELL_FOLDERS_REGKEY, + "Desktop", + "DesktopFolder", + SpecialFolderDotNet.Desktop), + + PROGRAM_FILES("ProgramFiles64Folder", SpecialFolderDotNet.ProgramFiles), + + LOCAL_APPLICATION_DATA("LocalAppDataFolder", SpecialFolderDotNet.LocalApplicationData), + ; + + SpecialFolder(String keyPath, String valueName, String msiPropertyName) { + reg = Optional.of(new RegValuePath(keyPath, valueName)); alt = Optional.empty(); + this.msiPropertyName = Objects.requireNonNull(msiPropertyName); + } + + SpecialFolder(String keyPath, String valueName, String msiPropertyName, SpecialFolderDotNet alt) { + reg = Optional.of(new RegValuePath(keyPath, valueName)); + this.alt = Optional.of(alt); + this.msiPropertyName = Objects.requireNonNull(msiPropertyName); } - SpecialFolder(String keyPath, String valueName, SpecialFolderDotNet alt) { - reg = new RegValuePath(keyPath, valueName); + SpecialFolder(String msiPropertyName, SpecialFolderDotNet alt) { + reg = Optional.empty(); this.alt = Optional.of(alt); + this.msiPropertyName = Objects.requireNonNull(msiPropertyName); + } + + static Optional findMsiProperty(String pathComponent, boolean allUsers) { + Objects.requireNonNull(pathComponent); + String regPath; + if (allUsers) { + regPath = SYSTEM_SHELL_FOLDERS_REGKEY; + } else { + regPath = USER_SHELL_FOLDERS_REGKEY; + } + return Stream.of(values()) + .filter(v -> v.msiPropertyName.equals(pathComponent)) + .filter(v -> { + return v.reg.map(r -> r.keyPath().equals(regPath)).orElse(true); + }) + .findFirst(); + } + + String getMsiPropertyName() { + return msiPropertyName; } Path getPath() { - return CACHE.computeIfAbsent(this, k -> reg.findValue().map(Path::of).orElseGet(() -> { + return CACHE.computeIfAbsent(this, k -> reg.flatMap(RegValuePath::findValue).map(Path::of).orElseGet(() -> { return alt.map(SpecialFolderDotNet::getPath).orElseThrow(() -> { return new NoSuchElementException(String.format("Failed to find path to %s folder", name())); }); })); } - private final RegValuePath reg; + private final Optional reg; private final Optional alt; + // One of "System Folder Properties" from https://learn.microsoft.com/en-us/windows/win32/msi/property-reference + private final String msiPropertyName; private static final Map CACHE = new ConcurrentHashMap<>(); } @@ -693,6 +759,63 @@ static Path toShortPath(Path path) { private static final ShortPathUtils INSTANCE = new ShortPathUtils(); } + + private static final class MsiDatabaseCache { + + Optional findProperty(Path msiPath, String propertyName) { + return ensureTables(msiPath, MsiDatabase.Table.FIND_PROPERTY_REQUIRED_TABLES).findProperty(propertyName); + } + + Collection listShortcuts(Path msiPath) { + return ensureTables(msiPath, MsiDatabase.Table.LIST_SHORTCUTS_REQUIRED_TABLES).listShortcuts(); + } + + MsiDatabase ensureTables(Path msiPath, Set tableNames) { + Objects.requireNonNull(msiPath); + try { + synchronized (items) { + var value = Optional.ofNullable(items.get(msiPath)).map(SoftReference::get).orElse(null); + if (value != null) { + var lastModifiedTime = Files.getLastModifiedTime(msiPath).toInstant(); + if (lastModifiedTime.isAfter(value.timestamp())) { + value = null; + } else { + tableNames = Comm.compare(value.db().tableNames(), tableNames).unique2(); + } + } + + if (!tableNames.isEmpty()) { + var idtOutputDir = TKit.createTempDirectory("msi-db"); + var db = MsiDatabase.load(msiPath, idtOutputDir, tableNames); + if (value != null) { + value = new MsiDatabaseWithTimestamp(db.append(value.db()), value.timestamp()); + } else { + value = new MsiDatabaseWithTimestamp(db, Files.getLastModifiedTime(msiPath).toInstant()); + } + items.put(msiPath, new SoftReference<>(value)); + } + + return value.db(); + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private record MsiDatabaseWithTimestamp(MsiDatabase db, Instant timestamp) { + + MsiDatabaseWithTimestamp { + Objects.requireNonNull(db); + Objects.requireNonNull(timestamp); + } + } + + private final Map> items = new HashMap<>(); + + static final MsiDatabaseCache INSTANCE = new MsiDatabaseCache(); + } + + static final Set CRITICAL_RUNTIME_FILES = Set.of(Path.of( "bin\\server\\jvm.dll")); diff --git a/test/jdk/tools/jpackage/resources/msi-export.js b/test/jdk/tools/jpackage/resources/msi-export.js new file mode 100644 index 0000000000000..d639f19ca44a1 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/msi-export.js @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025, 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. + */ + + +function readMsi(msiPath, callback) { + var installer = new ActiveXObject('WindowsInstaller.Installer') + var database = installer.OpenDatabase(msiPath, 0 /* msiOpenDatabaseModeReadOnly */) + + return callback(database) +} + + +function exportTables(db, outputDir, requestedTableNames) { + var tables = {} + + var view = db.OpenView("SELECT `Name` FROM _Tables") + view.Execute() + + try { + while (true) { + var record = view.Fetch() + if (!record) { + break + } + + var name = record.StringData(1) + + if (requestedTableNames.hasOwnProperty(name)) { + tables[name] = name + } + } + } finally { + view.Close() + } + + var fso = new ActiveXObject("Scripting.FileSystemObject") + for (var table in tables) { + var idtFileName = table + ".idt" + var idtFile = outputDir + "/" + idtFileName + if (fso.FileExists(idtFile)) { + WScript.Echo("Delete [" + idtFile + "]") + fso.DeleteFile(idtFile) + } + WScript.Echo("Export table [" + table + "] in [" + idtFile + "] file") + db.Export(table, fso.GetFolder(outputDir).Path, idtFileName) + } +} + + +(function () { + var msi = WScript.arguments(0) + var outputDir = WScript.arguments(1) + var tables = {} + for (var i = 0; i !== WScript.arguments.Count(); i++) { + tables[WScript.arguments(i)] = true + } + + readMsi(msi, function (db) { + exportTables(db, outputDir, tables) + }) +})() diff --git a/test/jdk/tools/jpackage/resources/query-msi-property.js b/test/jdk/tools/jpackage/resources/query-msi-property.js deleted file mode 100644 index d821f5a8a5420..0000000000000 --- a/test/jdk/tools/jpackage/resources/query-msi-property.js +++ /dev/null @@ -1,65 +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. - * - * 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. - */ - - -function readMsi(msiPath, callback) { - var installer = new ActiveXObject('WindowsInstaller.Installer') - var database = installer.OpenDatabase(msiPath, 0 /* msiOpenDatabaseModeReadOnly */) - - return callback(database) -} - - -function queryAllProperties(db) { - var reply = {} - - var view = db.OpenView("SELECT `Property`, `Value` FROM Property") - view.Execute() - - try { - while(true) { - var record = view.Fetch() - if (!record) { - break - } - - var name = record.StringData(1) - var value = record.StringData(2) - - reply[name] = value - } - } finally { - view.Close() - } - - return reply -} - - -(function () { - var msi = WScript.arguments(0) - var propName = WScript.arguments(1) - - var props = readMsi(msi, queryAllProperties) - WScript.Echo(props[propName]) -})() From 54f2d0e1ae946690b8b736614148771281da22e5 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 28 Jul 2025 13:01:55 -0400 Subject: [PATCH 04/83] LauncherIconVerifier: add verifyFileInAppImageOnly() to control if to verify the contents of an icon file or just verify the icon file's availability. --- .../jpackage/test/LauncherIconVerifier.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java index a1971ee083549..6285d9d93a0df 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java @@ -46,6 +46,11 @@ public LauncherIconVerifier setExpectedDefaultIcon() { return this; } + public LauncherIconVerifier verifyFileInAppImageOnly(boolean v) { + verifyFileInAppImageOnly = true; + return this; + } + public void applyTo(JPackageCommand cmd) throws IOException { final String curLauncherName; final String label; @@ -62,22 +67,26 @@ public void applyTo(JPackageCommand cmd) throws IOException { if (TKit.isWindows()) { TKit.assertPathExists(iconPath, false); - WinExecutableIconVerifier.verifyLauncherIcon(cmd, launcherName, - expectedIcon, expectedDefault); + if (!verifyFileInAppImageOnly) { + WinExecutableIconVerifier.verifyLauncherIcon(cmd, launcherName, expectedIcon, expectedDefault); + } } else if (expectedDefault) { TKit.assertPathExists(iconPath, true); } else if (expectedIcon == null) { TKit.assertPathExists(iconPath, false); } else { TKit.assertFileExists(iconPath); - TKit.assertTrue(-1 == Files.mismatch(expectedIcon, iconPath), - String.format( - "Check icon file [%s] of %s launcher is a copy of source icon file [%s]", - iconPath, label, expectedIcon)); + if (!verifyFileInAppImageOnly) { + TKit.assertTrue(-1 == Files.mismatch(expectedIcon, iconPath), + String.format( + "Check icon file [%s] of %s launcher is a copy of source icon file [%s]", + iconPath, label, expectedIcon)); + } } } private String launcherName; private Path expectedIcon; private boolean expectedDefault; + private boolean verifyFileInAppImageOnly; } From 05a79ec9f14ebacbeccc5cfa57088ebe48908043 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 24 Jul 2025 14:04:21 -0400 Subject: [PATCH 05/83] Remove redundant shortcut verification code: it duplicates code in WinShortcutVerifier class --- .../jdk/jpackage/test/WindowsHelper.java | 151 +++--------------- 1 file changed, 20 insertions(+), 131 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 8f52a464250d3..452906573466f 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -39,11 +39,11 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; +import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.internal.util.function.ThrowingRunnable; import jdk.jpackage.test.PackageTest.PackageHandlers; @@ -270,12 +270,7 @@ static Optional toShortPath(Path path) { static void verifyDeployedDesktopIntegration(JPackageCommand cmd, boolean installed) { WinShortcutVerifier.verifyDeployedShortcuts(cmd, installed); - // Check the main launcher - new DesktopIntegrationVerifier(cmd, installed, null); - // Check additional launchers - cmd.addLauncherNames().forEach(name -> { - new DesktopIntegrationVerifier(cmd, installed, name); - }); + DesktopIntegrationVerifier.verify(cmd, installed); } public static String getMsiProperty(JPackageCommand cmd, String propertyName) { @@ -404,141 +399,42 @@ private static boolean isPathTooLong(Path path) { return path.toString().length() > WIN_MAX_PATH; } + private static class DesktopIntegrationVerifier { - DesktopIntegrationVerifier(JPackageCommand cmd, boolean installed, String launcherName) { + static void verify(JPackageCommand cmd, boolean installed) { cmd.verifyIsOfType(PackageType.WINDOWS); - - name = Optional.ofNullable(launcherName).orElseGet(cmd::name); - - isUserLocalInstall = isUserLocalInstall(cmd); - - this.appInstalled = installed; - - desktopShortcutPath = Path.of(name + ".lnk"); - - startMenuShortcutPath = Path.of(cmd.getArgumentValue( - "--win-menu-group", () -> "Unknown"), name + ".lnk"); - - if (name.equals(cmd.name())) { - isWinMenu = cmd.hasArgument("--win-menu"); - isDesktop = cmd.hasArgument("--win-shortcut"); - } else { - var props = AdditionalLauncher.getAdditionalLauncherProperties(cmd, - launcherName); - isWinMenu = props.getPropertyBooleanValue("win-menu").orElseGet( - () -> cmd.hasArgument("--win-menu")); - isDesktop = props.getPropertyBooleanValue("win-shortcut").orElseGet( - () -> cmd.hasArgument("--win-shortcut")); + for (var faFile : cmd.getAllArgumentValues("--file-associations")) { + verifyFileAssociationsRegistry(Path.of(faFile), installed); } - - verifyStartMenuShortcut(); - - verifyDesktopShortcut(); - - Stream.of(cmd.getAllArgumentValues("--file-associations")).map( - Path::of).forEach(this::verifyFileAssociationsRegistry); } - private void verifyDesktopShortcut() { - if (isDesktop) { - if (isUserLocalInstall) { - verifyUserLocalDesktopShortcut(appInstalled); - verifySystemDesktopShortcut(false); - } else { - verifySystemDesktopShortcut(appInstalled); - verifyUserLocalDesktopShortcut(false); - } - } else { - verifySystemDesktopShortcut(false); - verifyUserLocalDesktopShortcut(false); - } - } - - private void verifyShortcut(Path path, boolean exists) { - if (exists) { - TKit.assertFileExists(path); - } else { - TKit.assertPathExists(path, false); - } - } - - private void verifySystemDesktopShortcut(boolean exists) { - Path dir = SpecialFolder.COMMON_DESKTOP.getPath(); - verifyShortcut(dir.resolve(desktopShortcutPath), exists); - } + private static void verifyFileAssociationsRegistry(Path faFile, boolean installed) { - private void verifyUserLocalDesktopShortcut(boolean exists) { - Path dir = SpecialFolder.USER_DESKTOP.getPath(); - verifyShortcut(dir.resolve(desktopShortcutPath), exists); - } - - private void verifyStartMenuShortcut() { - if (isWinMenu) { - if (isUserLocalInstall) { - verifyUserLocalStartMenuShortcut(appInstalled); - verifySystemStartMenuShortcut(false); - } else { - verifySystemStartMenuShortcut(appInstalled); - verifyUserLocalStartMenuShortcut(false); - } - } else { - verifySystemStartMenuShortcut(false); - verifyUserLocalStartMenuShortcut(false); - } - } - - private void verifyStartMenuShortcut(Path shortcutsRoot, boolean exists) { - Path shortcutPath = shortcutsRoot.resolve(startMenuShortcutPath); - verifyShortcut(shortcutPath, exists); - if (!exists) { - final var parentDir = shortcutPath.getParent(); - if (Files.isDirectory(parentDir)) { - TKit.assertDirectoryNotEmpty(parentDir); - } else { - TKit.assertPathExists(parentDir, false); - } - } - } - - private void verifySystemStartMenuShortcut(boolean exists) { - verifyStartMenuShortcut(SpecialFolder.COMMON_START_MENU_PROGRAMS.getPath(), exists); - - } + TKit.trace(String.format( + "Get file association properties from [%s] file", + faFile)); - private void verifyUserLocalStartMenuShortcut(boolean exists) { - verifyStartMenuShortcut(SpecialFolder.USER_START_MENU_PROGRAMS.getPath(), exists); - } + var faProps = new Properties(); - private void verifyFileAssociationsRegistry(Path faFile) { - try { - TKit.trace(String.format( - "Get file association properties from [%s] file", - faFile)); - Map faProps = Files.readAllLines(faFile).stream().filter( - line -> line.trim().startsWith("extension=") || line.trim().startsWith( - "mime-type=")).map( - line -> { - String[] keyValue = line.trim().split("=", 2); - return Map.entry(keyValue[0], keyValue[1]); - }).collect(Collectors.toMap( - entry -> entry.getKey(), - entry -> entry.getValue())); - String suffix = faProps.get("extension"); - String contentType = faProps.get("mime-type"); + try (var reader = Files.newBufferedReader(faFile)) { + faProps.load(reader); + String suffix = faProps.getProperty("extension"); + String contentType = faProps.getProperty("mime-type"); TKit.assertNotNull(suffix, String.format( "Check file association suffix [%s] is found in [%s] property file", suffix, faFile)); TKit.assertNotNull(contentType, String.format( "Check file association content type [%s] is found in [%s] property file", contentType, faFile)); - verifyFileAssociations(appInstalled, "." + suffix, contentType); + verifyFileAssociations(installed, "." + suffix, contentType); + } catch (IOException ex) { - throw new RuntimeException(ex); + throw new UncheckedIOException(ex); } } - private void verifyFileAssociations(boolean exists, String suffix, + private static void verifyFileAssociations(boolean exists, String suffix, String contentType) { String contentTypeFromRegistry = queryRegistryValue(Path.of( "HKLM\\Software\\Classes", suffix).toString(), @@ -559,16 +455,9 @@ private void verifyFileAssociations(boolean exists, String suffix, "Check content type in registry not found"); } } - - private final Path desktopShortcutPath; - private final Path startMenuShortcutPath; - private final boolean isUserLocalInstall; - private final boolean appInstalled; - private final boolean isWinMenu; - private final boolean isDesktop; - private final String name; } + static String queryRegistryValue(String keyPath, String valueName) { var status = Executor.of("reg", "query", keyPath, "/v", valueName) .saveOutput() From d34229fb120ec53c5994ae6d695317421cd23128 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 24 Jul 2025 14:31:25 -0400 Subject: [PATCH 06/83] Simplify AdditionalLauncher.PropertyFile --- .../jdk/jpackage/test/AdditionalLauncher.java | 30 ++++++++----------- .../jdk/jpackage/test/ConfigFilesStasher.java | 2 +- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java index 687c2ef420662..fa847d74058b3 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java @@ -34,6 +34,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Properties; import java.util.function.BiConsumer; import java.util.function.Supplier; import java.util.regex.Matcher; @@ -392,21 +393,15 @@ protected void verify(JPackageCommand cmd) throws IOException { public static final class PropertyFile { PropertyFile(Map data) { - this.data = Map.copyOf(data); + this.data = new Properties(); + data.putAll(data); } PropertyFile(Path path) throws IOException { - data = Files.readAllLines(path).stream().map(str -> { - return str.split("=", 2); - }).collect(toMap(tokens -> tokens[0], tokens -> { - if (tokens.length == 1) { - return ""; - } else { - return tokens[1]; - } - }, (oldValue, newValue) -> { - return newValue; - })); + data = new Properties(); + try (var reader = Files.newBufferedReader(path)) { + data.load(reader); + } } public boolean isPropertySet(String name) { @@ -414,17 +409,16 @@ public boolean isPropertySet(String name) { return data.containsKey(name); } - public Optional getPropertyValue(String name) { + public Optional findPropertyValue(String name) { Objects.requireNonNull(name); - return Optional.of(data.get(name)); + return Optional.ofNullable(data.getProperty(name)); } - public Optional getPropertyBooleanValue(String name) { - Objects.requireNonNull(name); - return Optional.ofNullable(data.get(name)).map(Boolean::parseBoolean); + public Optional findPropertyBooleanValue(String name) { + return findPropertyValue(name).map(Boolean::parseBoolean); } - private final Map data; + private final Properties data; } private static String resolveVariables(JPackageCommand cmd, String str) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java index 98c791310450a..68d50b1f896d8 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java @@ -220,7 +220,7 @@ private static boolean isWithServices(JPackageCommand cmd) { AdditionalLauncher.forEachAdditionalLauncher(cmd, (launcherName, propertyFilePath) -> { try { final var launcherAsService = new AdditionalLauncher.PropertyFile(propertyFilePath) - .getPropertyBooleanValue("launcher-as-service").orElse(false); + .findPropertyBooleanValue("launcher-as-service").orElse(false); if (launcherAsService) { withServices[0] = true; } From 4cc2f3d2c332364319388e96d1e7e3242b128426 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 25 Jul 2025 17:25:07 -0400 Subject: [PATCH 07/83] Add missing CommandArguments.verifyMutable() calls. --- .../helpers/jdk/jpackage/test/CommandArguments.java | 5 +++-- .../helpers/jdk/jpackage/test/JPackageCommand.java | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java index cb7f0574afde5..4a78ad40cd17e 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java @@ -35,16 +35,17 @@ public class CommandArguments { } public final T clearArguments() { + verifyMutable(); args.clear(); return thiz(); } public final T addArgument(String v) { - args.add(v); - return thiz(); + return addArguments(v); } public final T addArguments(List v) { + verifyMutable(); args.addAll(v); return thiz(); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 7f9feb986b482..220aa19c2a698 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -316,13 +316,11 @@ public JPackageCommand setFakeRuntime() { } JPackageCommand addPrerequisiteAction(ThrowingConsumer action) { - verifyMutable(); prerequisiteActions.add(action); return this; } JPackageCommand addVerifyAction(ThrowingConsumer action) { - verifyMutable(); verifyActions.add(action); return this; } @@ -820,6 +818,7 @@ public Executor.Result execute() { } public Executor.Result execute(int expectedExitCode) { + verifyMutable(); executePrerequisiteActions(); if (hasArgument("--dest")) { @@ -1046,6 +1045,7 @@ private static Stream tokenizeValue(String str) { } public JPackageCommand setReadOnlyPathAsserts(ReadOnlyPathAssert... asserts) { + verifyMutable(); readOnlyPathAsserts = Set.of(asserts); return this; } @@ -1115,6 +1115,7 @@ private static JPackageCommand convertFromRuntime(JPackageCommand cmd) { } public JPackageCommand setAppLayoutAsserts(AppLayoutAssert ... asserts) { + verifyMutable(); appLayoutAsserts = Set.of(asserts); return this; } @@ -1258,6 +1259,7 @@ private void assertFileInAppImage(Path filename, Path expectedPath) { } JPackageCommand setUnpackedPackageLocation(Path path) { + verifyMutable(); verifyIsOfType(PackageType.NATIVE); if (path != null) { setArgumentValue(UNPACKED_PATH_ARGNAME, path); @@ -1268,6 +1270,7 @@ JPackageCommand setUnpackedPackageLocation(Path path) { } JPackageCommand winMsiLogFile(Path v) { + verifyMutable(); if (!TKit.isWindows()) { throw new UnsupportedOperationException(); } @@ -1290,6 +1293,7 @@ public Optional> winMsiLogFileContents() { } private JPackageCommand adjustArgumentsBeforeExecution() { + verifyMutable(); if (!isWithToolProvider()) { // if jpackage is launched as a process then set the jlink.debug system property // to allow the jlink process to print exception stacktraces on any failure From 840d464f91593ce692d400fffc90f8d316725ad8 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 25 Jul 2025 20:53:20 -0400 Subject: [PATCH 08/83] WindowsHelper: fix a typo --- .../tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 452906573466f..5e97b0d2dde58 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -283,7 +283,7 @@ static Collection getMsiShortcuts(JPackageCommand cmd) { return MsiDatabaseCache.INSTANCE.listShortcuts(cmd.outputBundle()); } - public static String getExecutableDesciption(Path pathToExeFile) { + public static String getExecutableDescription(Path pathToExeFile) { Executor exec = Executor.of("powershell", "-NoLogo", "-NoProfile", From 769fd7faacd454b31559267fb241aa6bd051130d Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 24 Jul 2025 14:32:01 -0400 Subject: [PATCH 09/83] AddLauncherTest, UpgradeTest, WinShortcutVerifier, ConfigFilesStasher: follow-up for function renames in the AdditionalLauncher. --- .../jdk/jpackage/test/ConfigFilesStasher.java | 2 +- .../jdk/jpackage/test/WinShortcutVerifier.java | 4 ++-- test/jdk/tools/jpackage/linux/UpgradeTest.java | 10 +++++----- test/jdk/tools/jpackage/share/AddLauncherTest.java | 14 +++++++------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java index 68d50b1f896d8..e630659bdb17d 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java @@ -220,7 +220,7 @@ private static boolean isWithServices(JPackageCommand cmd) { AdditionalLauncher.forEachAdditionalLauncher(cmd, (launcherName, propertyFilePath) -> { try { final var launcherAsService = new AdditionalLauncher.PropertyFile(propertyFilePath) - .findPropertyBooleanValue("launcher-as-service").orElse(false); + .findBooleanProperty("launcher-as-service").orElse(false); if (launcherAsService) { withServices[0] = true; } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index 10c6209c08925..a4c9311f041b1 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -207,8 +207,8 @@ private static Collection expectLauncherShortcuts(JPackageCommand cmd, }).orElseGet(() -> { return AdditionalLauncher.getAdditionalLauncherProperties(cmd, launcherName); }); - isWinMenu = props.getPropertyBooleanValue("win-menu").orElseGet(() -> cmd.hasArgument("--win-menu")); - isDesktop = props.getPropertyBooleanValue("win-shortcut").orElseGet(() -> cmd.hasArgument("--win-shortcut")); + isWinMenu = props.findBooleanProperty("win-menu").orElseGet(() -> cmd.hasArgument("--win-menu")); + isDesktop = props.findBooleanProperty("win-shortcut").orElseGet(() -> cmd.hasArgument("--win-shortcut")); } var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); diff --git a/test/jdk/tools/jpackage/linux/UpgradeTest.java b/test/jdk/tools/jpackage/linux/UpgradeTest.java index fb399cec12bdb..bfbdcf3aa0c07 100644 --- a/test/jdk/tools/jpackage/linux/UpgradeTest.java +++ b/test/jdk/tools/jpackage/linux/UpgradeTest.java @@ -60,16 +60,16 @@ public void testDesktopFiles() { var alA = createAdditionalLauncher("launcherA"); alA.applyTo(pkg); - createAdditionalLauncher("launcherB").addRawProperties(Map.entry( - "description", "Foo")).applyTo(pkg); + createAdditionalLauncher("launcherB").setProperty( + "description", "Foo").applyTo(pkg); var pkg2 = createPackageTest().addInitializer(cmd -> { cmd.addArguments("--app-version", "2.0"); }); alA.verifyRemovedInUpgrade(pkg2); - createAdditionalLauncher("launcherB").addRawProperties(Map.entry( - "description", "Bar")).applyTo(pkg2); + createAdditionalLauncher("launcherB").setProperty( + "description", "Bar").applyTo(pkg2); createAdditionalLauncher("launcherC").applyTo(pkg2); new PackageTest.Group(pkg, pkg2).run(); @@ -88,6 +88,6 @@ private static AdditionalLauncher createAdditionalLauncher(String name) { return new AdditionalLauncher(name).setIcon(GOLDEN_ICON); } - private final static Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of( + private static final Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of( "resources", "icon" + TKit.ICON_SUFFIX)); } diff --git a/test/jdk/tools/jpackage/share/AddLauncherTest.java b/test/jdk/tools/jpackage/share/AddLauncherTest.java index 5c21be712581a..8d5f0de28f20d 100644 --- a/test/jdk/tools/jpackage/share/AddLauncherTest.java +++ b/test/jdk/tools/jpackage/share/AddLauncherTest.java @@ -89,17 +89,17 @@ public void test() { new AdditionalLauncher("Baz2") .setDefaultArguments() - .addRawProperties(Map.entry("description", "Baz2 Description")) + .setProperty("description", "Baz2 Description") .applyTo(packageTest); new AdditionalLauncher("foo") .setDefaultArguments("yep!") - .addRawProperties(Map.entry("description", "foo Description")) + .setProperty("description", "foo Description") .applyTo(packageTest); new AdditionalLauncher("Bar") .setDefaultArguments("one", "two", "three") - .addRawProperties(Map.entry("description", "Bar Description")) + .setProperty("description", "Bar Description") .setIcon(GOLDEN_ICON) .applyTo(packageTest); @@ -194,8 +194,8 @@ public void testMainLauncherIsModular(boolean mainLauncherIsModular) { .toString(); new AdditionalLauncher("ModularAppLauncher") - .addRawProperties(Map.entry("module", expectedMod)) - .addRawProperties(Map.entry("main-jar", "")) + .setProperty("module", expectedMod) + .setProperty("main-jar", "") .applyTo(cmd); new AdditionalLauncher("NonModularAppLauncher") @@ -204,8 +204,8 @@ public void testMainLauncherIsModular(boolean mainLauncherIsModular) { .setPersistenceHandler((path, properties) -> TKit.createTextFile(path, properties.stream().map(entry -> String.join(" ", entry.getKey(), entry.getValue())))) - .addRawProperties(Map.entry("main-class", nonModularAppDesc.className())) - .addRawProperties(Map.entry("main-jar", nonModularAppDesc.jarFileName())) + .setProperty("main-class", nonModularAppDesc.className()) + .setProperty("main-jar", nonModularAppDesc.jarFileName()) .applyTo(cmd); cmd.executeAndAssertHelloAppImageCreated(); From 06cf7ae72fb058d6cac20cef30d753e6cc29ae84 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 26 Jul 2025 17:04:34 -0400 Subject: [PATCH 10/83] Use JPackageCommand.createMutableCopy() --- .../jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index a4c9311f041b1..3b8d484497262 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -87,7 +87,7 @@ static void verifyDeployedShortcuts(JPackageCommand cmd, boolean installed) { cmd.verifyIsOfType(PackageType.WINDOWS); verifyDeployedShortcutsInternal(cmd, installed); - var copyCmd = new JPackageCommand(cmd); + var copyCmd = cmd.createMutableCopy(); if (copyCmd.hasArgument("--win-per-user-install")) { copyCmd.removeArgument("--win-per-user-install"); } else { From d5caa5c080ffad75e991676dd0168e9d1fa8c265 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 26 Jul 2025 22:04:30 -0400 Subject: [PATCH 11/83] Decouple additional launcher configuration and verification in the AdditionalLauncher. Introduce LauncherShortcut, LauncherVerifier, LauncherVerifier verifies attributes of any launcher - the main or additional. Add JPackageCommand.createMutableCopy(), hide the copy ctor. --- .../jdk/jpackage/test/AdditionalLauncher.java | 354 +++++------------- .../jdk/jpackage/test/JPackageCommand.java | 38 +- .../jdk/jpackage/test/LauncherShortcut.java | 156 ++++++++ .../jdk/jpackage/test/LauncherVerifier.java | 326 ++++++++++++++++ .../jdk/jpackage/test/PackageTest.java | 18 +- 5 files changed, 596 insertions(+), 296 deletions(-) create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java index fa847d74058b3..c2d2ad0f407a0 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java @@ -22,40 +22,54 @@ */ package jdk.jpackage.test; -import static java.util.stream.Collectors.toMap; import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; +import static jdk.jpackage.test.LauncherShortcut.LINUX_SHORTCUT; +import static jdk.jpackage.test.LauncherShortcut.WIN_DESKTOP_SHORTCUT; +import static jdk.jpackage.test.LauncherShortcut.WIN_START_MENU_SHORTCUT; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Properties; +import java.util.Set; import java.util.function.BiConsumer; -import java.util.function.Supplier; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; import jdk.jpackage.internal.util.function.ThrowingBiConsumer; +import jdk.jpackage.internal.util.function.ThrowingConsumer; +import jdk.jpackage.test.LauncherShortcut.StartupDirectory; +import jdk.jpackage.test.LauncherVerifier.Action; -public class AdditionalLauncher { +public final class AdditionalLauncher { public AdditionalLauncher(String name) { - this.name = name; - this.rawProperties = new ArrayList<>(); + this.name = Objects.requireNonNull(name); setPersistenceHandler(null); } - public final AdditionalLauncher setDefaultArguments(String... v) { + public AdditionalLauncher withVerifyActions(Action... actions) { + verifyActions.addAll(List.of(actions)); + return this; + } + + public AdditionalLauncher withoutVerifyActions(Action... actions) { + verifyActions.removeAll(List.of(actions)); + return this; + } + + public AdditionalLauncher setDefaultArguments(String... v) { defaultArguments = new ArrayList<>(List.of(v)); return this; } - public final AdditionalLauncher addDefaultArguments(String... v) { + public AdditionalLauncher addDefaultArguments(String... v) { if (defaultArguments == null) { return setDefaultArguments(v); } @@ -64,12 +78,12 @@ public final AdditionalLauncher addDefaultArguments(String... v) { return this; } - public final AdditionalLauncher setJavaOptions(String... v) { + public AdditionalLauncher setJavaOptions(String... v) { javaOptions = new ArrayList<>(List.of(v)); return this; } - public final AdditionalLauncher addJavaOptions(String... v) { + public AdditionalLauncher addJavaOptions(String... v) { if (javaOptions == null) { return setJavaOptions(v); } @@ -78,51 +92,46 @@ public final AdditionalLauncher addJavaOptions(String... v) { return this; } - public final AdditionalLauncher setVerifyUninstalled(boolean value) { - verifyUninstalled = value; + public AdditionalLauncher setProperty(String name, Object value) { + rawProperties.put(Objects.requireNonNull(name), Objects.requireNonNull(value.toString())); return this; } - public final AdditionalLauncher setLauncherAsService() { - return addRawProperties(LAUNCHER_AS_SERVICE); - } - - public final AdditionalLauncher addRawProperties( - Map.Entry v) { - return addRawProperties(List.of(v)); - } - - public final AdditionalLauncher addRawProperties( - Map.Entry v, Map.Entry v2) { - return addRawProperties(List.of(v, v2)); - } - - public final AdditionalLauncher addRawProperties( - Collection> v) { - rawProperties.addAll(v); + public AdditionalLauncher setShortcuts(boolean menu, boolean desktop) { + if (TKit.isLinux()) { + setShortcut(LINUX_SHORTCUT, desktop); + } else if (TKit.isWindows()) { + setShortcut(WIN_DESKTOP_SHORTCUT, desktop); + setShortcut(WIN_START_MENU_SHORTCUT, desktop); + } return this; } - public final String getRawPropertyValue( - String key, Supplier getDefault) { - return rawProperties.stream() - .filter(item -> item.getKey().equals(key)) - .map(e -> e.getValue()).findAny().orElseGet(getDefault); + public AdditionalLauncher setShortcut(LauncherShortcut shortcut, StartupDirectory value) { + if (value != null) { + setProperty(shortcut.propertyName(), value.asStringValue()); + } else { + setProperty(shortcut.propertyName(), false); + } + return this; } - private String getDesciption(JPackageCommand cmd) { - return getRawPropertyValue("description", () -> cmd.getArgumentValue( - "--description", unused -> cmd.name())); + public AdditionalLauncher setShortcut(LauncherShortcut shortcut, boolean value) { + if (value) { + setShortcut(shortcut, StartupDirectory.DEFAULT); + } else { + setShortcut(shortcut, null); + } + return this; } - public final AdditionalLauncher setShortcuts(boolean menu, boolean shortcut) { - withMenuShortcut = menu; - withShortcut = shortcut; + public AdditionalLauncher removeShortcut(LauncherShortcut shortcut) { + rawProperties.remove(shortcut.propertyName()); return this; } - public final AdditionalLauncher setIcon(Path iconPath) { - if (iconPath == NO_ICON) { + public AdditionalLauncher setIcon(Path iconPath) { + if (iconPath.equals(NO_ICON)) { throw new IllegalArgumentException(); } @@ -130,13 +139,13 @@ public final AdditionalLauncher setIcon(Path iconPath) { return this; } - public final AdditionalLauncher setNoIcon() { + public AdditionalLauncher setNoIcon() { icon = NO_ICON; return this; } - public final AdditionalLauncher setPersistenceHandler( - ThrowingBiConsumer>> handler) { + public AdditionalLauncher setPersistenceHandler( + ThrowingBiConsumer>> handler) { if (handler != null) { createFileHandler = ThrowingBiConsumer.toBiConsumer(handler); } else { @@ -145,21 +154,31 @@ public final AdditionalLauncher setPersistenceHandler( return this; } - public final void applyTo(JPackageCommand cmd) { + public void applyTo(JPackageCommand cmd) { cmd.addPrerequisiteAction(this::initialize); - cmd.addVerifyAction(this::verify); + cmd.addVerifyAction(createVerifierAsConsumer()); } - public final void applyTo(PackageTest test) { + public void applyTo(PackageTest test) { test.addInitializer(this::initialize); - test.addInstallVerifier(this::verify); - if (verifyUninstalled) { - test.addUninstallVerifier(this::verifyUninstalled); - } + test.addInstallVerifier(createVerifierAsConsumer()); } public final void verifyRemovedInUpgrade(PackageTest test) { - test.addInstallVerifier(this::verifyUninstalled); + test.addInstallVerifier(cmd -> { + createVerifier().verify(cmd, LauncherVerifier.Action.VERIFY_UNINSTALLED); + }); + } + + private LauncherVerifier createVerifier() { + return new LauncherVerifier(name, Optional.ofNullable(javaOptions), + Optional.ofNullable(defaultArguments), Optional.ofNullable(icon), rawProperties); + } + + private ThrowingConsumer createVerifierAsConsumer() { + return cmd -> { + createVerifier().verify(cmd, verifyActions.stream().sorted(Comparator.comparing(Action::ordinal)).toArray(Action[]::new)); + }; } static void forEachAdditionalLauncher(JPackageCommand cmd, @@ -193,208 +212,35 @@ private void initialize(JPackageCommand cmd) throws IOException { cmd.addArguments("--add-launcher", String.format("%s=%s", name, propsFile)); - List> properties = new ArrayList<>(); + Map properties = new HashMap<>(); if (defaultArguments != null) { - properties.add(Map.entry("arguments", - JPackageCommand.escapeAndJoin(defaultArguments))); + properties.put("arguments", JPackageCommand.escapeAndJoin(defaultArguments)); } if (javaOptions != null) { - properties.add(Map.entry("java-options", - JPackageCommand.escapeAndJoin(javaOptions))); + properties.put("java-options", JPackageCommand.escapeAndJoin(javaOptions)); } if (icon != null) { final String iconPath; - if (icon == NO_ICON) { + if (icon.equals(NO_ICON)) { iconPath = ""; } else { iconPath = icon.toAbsolutePath().toString().replace('\\', '/'); } - properties.add(Map.entry("icon", iconPath)); - } - - if (withShortcut != null) { - if (TKit.isLinux()) { - properties.add(Map.entry("linux-shortcut", withShortcut.toString())); - } else if (TKit.isWindows()) { - properties.add(Map.entry("win-shortcut", withShortcut.toString())); - } - } - - if (TKit.isWindows() && withMenuShortcut != null) { - properties.add(Map.entry("win-menu", withMenuShortcut.toString())); - } - - properties.addAll(rawProperties); - - createFileHandler.accept(propsFile, properties); - } - - private static Path iconInResourceDir(JPackageCommand cmd, - String launcherName) { - Path resourceDir = cmd.getArgumentValue("--resource-dir", () -> null, - Path::of); - if (resourceDir != null) { - Path icon = resourceDir.resolve( - Optional.ofNullable(launcherName).orElseGet(() -> cmd.name()) - + TKit.ICON_SUFFIX); - if (Files.exists(icon)) { - return icon; - } - } - return null; - } - - private void verifyIcon(JPackageCommand cmd) throws IOException { - var verifier = new LauncherIconVerifier().setLauncherName(name); - - if (TKit.isOSX()) { - // On Mac should be no icon files for additional launchers. - verifier.applyTo(cmd); - return; - } - - boolean withLinuxDesktopFile = false; - - final Path effectiveIcon = Optional.ofNullable(icon).orElseGet( - () -> iconInResourceDir(cmd, name)); - while (effectiveIcon != NO_ICON) { - if (effectiveIcon != null) { - withLinuxDesktopFile = Boolean.FALSE != withShortcut; - verifier.setExpectedIcon(effectiveIcon); - break; - } - - Path customMainLauncherIcon = cmd.getArgumentValue("--icon", - () -> iconInResourceDir(cmd, null), Path::of); - if (customMainLauncherIcon != null) { - withLinuxDesktopFile = Boolean.FALSE != withShortcut; - verifier.setExpectedIcon(customMainLauncherIcon); - break; - } - - verifier.setExpectedDefaultIcon(); - break; + properties.put("icon", iconPath); } - if (TKit.isLinux() && !cmd.isImagePackageType()) { - if (effectiveIcon != NO_ICON && !withLinuxDesktopFile) { - withLinuxDesktopFile = (Boolean.FALSE != withShortcut) && - Stream.of("--linux-shortcut").anyMatch(cmd::hasArgument); - verifier.setExpectedDefaultIcon(); - } - Path desktopFile = LinuxHelper.getDesktopFile(cmd, name); - if (withLinuxDesktopFile) { - TKit.assertFileExists(desktopFile); - } else { - TKit.assertPathExists(desktopFile, false); - } - } + properties.putAll(rawProperties); - verifier.applyTo(cmd); - } - - private void verifyShortcuts(JPackageCommand cmd) throws IOException { - if (TKit.isLinux() && !cmd.isImagePackageType() - && withShortcut != null) { - Path desktopFile = LinuxHelper.getDesktopFile(cmd, name); - if (withShortcut) { - TKit.assertFileExists(desktopFile); - } else { - TKit.assertPathExists(desktopFile, false); - } - } - } - - private void verifyDescription(JPackageCommand cmd) throws IOException { - if (TKit.isWindows()) { - String expectedDescription = getDesciption(cmd); - Path launcherPath = cmd.appLauncherPath(name); - String actualDescription = - WindowsHelper.getExecutableDesciption(launcherPath); - TKit.assertEquals(expectedDescription, actualDescription, - String.format("Check file description of [%s]", launcherPath)); - } else if (TKit.isLinux() && !cmd.isImagePackageType()) { - String expectedDescription = getDesciption(cmd); - Path desktopFile = LinuxHelper.getDesktopFile(cmd, name); - if (Files.exists(desktopFile)) { - TKit.assertTextStream("Comment=" + expectedDescription) - .label(String.format("[%s] file", desktopFile)) - .predicate(String::equals) - .apply(Files.readAllLines(desktopFile)); - } - } - } - - private void verifyInstalled(JPackageCommand cmd, boolean installed) throws IOException { - if (TKit.isLinux() && !cmd.isImagePackageType() && !cmd. - isPackageUnpacked(String.format( - "Not verifying package and system .desktop files for [%s] launcher", - cmd.appLauncherPath(name)))) { - Path packageDesktopFile = LinuxHelper.getDesktopFile(cmd, name); - Path systemDesktopFile = LinuxHelper.getSystemDesktopFilesFolder(). - resolve(packageDesktopFile.getFileName()); - if (Files.exists(packageDesktopFile) && installed) { - TKit.assertFileExists(systemDesktopFile); - TKit.assertStringListEquals(Files.readAllLines( - packageDesktopFile), - Files.readAllLines(systemDesktopFile), String.format( - "Check [%s] and [%s] files are equal", - packageDesktopFile, - systemDesktopFile)); - } else { - TKit.assertPathExists(packageDesktopFile, false); - TKit.assertPathExists(systemDesktopFile, false); - } - } - } - - protected void verifyUninstalled(JPackageCommand cmd) throws IOException { - verifyInstalled(cmd, false); - Path launcherPath = cmd.appLauncherPath(name); - TKit.assertPathExists(launcherPath, false); - } - - protected void verify(JPackageCommand cmd) throws IOException { - verifyIcon(cmd); - verifyShortcuts(cmd); - verifyDescription(cmd); - verifyInstalled(cmd, true); - - Path launcherPath = cmd.appLauncherPath(name); - - TKit.assertExecutableFileExists(launcherPath); - - if (!cmd.canRunLauncher(String.format( - "Not running %s launcher", launcherPath))) { - return; - } - - var appVerifier = HelloApp.assertApp(launcherPath) - .addDefaultArguments(Optional - .ofNullable(defaultArguments) - .orElseGet(() -> List.of(cmd.getAllArgumentValues("--arguments")))) - .addJavaOptions(Optional - .ofNullable(javaOptions) - .orElseGet(() -> List.of(cmd.getAllArgumentValues( - "--java-options"))).stream().map( - str -> resolveVariables(cmd, str)).toList()); - - if (!rawProperties.contains(LAUNCHER_AS_SERVICE)) { - appVerifier.executeAndVerifyOutput(); - } else if (!cmd.isPackageUnpacked(String.format( - "Not verifying contents of test output file for [%s] launcher", - launcherPath))) { - appVerifier.verifyOutput(); - } + createFileHandler.accept(propsFile, properties.entrySet()); } public static final class PropertyFile { PropertyFile(Map data) { this.data = new Properties(); - data.putAll(data); + this.data.putAll(data); } PropertyFile(Path path) throws IOException { @@ -404,45 +250,25 @@ public static final class PropertyFile { } } - public boolean isPropertySet(String name) { - Objects.requireNonNull(name); - return data.containsKey(name); - } - - public Optional findPropertyValue(String name) { + public Optional findProperty(String name) { Objects.requireNonNull(name); return Optional.ofNullable(data.getProperty(name)); } - public Optional findPropertyBooleanValue(String name) { - return findPropertyValue(name).map(Boolean::parseBoolean); + public Optional findBooleanProperty(String name) { + return findProperty(name).map(Boolean::parseBoolean); } private final Properties data; } - private static String resolveVariables(JPackageCommand cmd, String str) { - var map = Stream.of(JPackageCommand.Macro.values()).collect(toMap(x -> { - return String.format("$%s", x.name()); - }, cmd::macroValue)); - for (var e : map.entrySet()) { - str = str.replaceAll(Pattern.quote(e.getKey()), - Matcher.quoteReplacement(e.getValue().toString())); - } - return str; - } - - private boolean verifyUninstalled; private List javaOptions; private List defaultArguments; private Path icon; private final String name; - private final List> rawProperties; - private BiConsumer>> createFileHandler; - private Boolean withMenuShortcut; - private Boolean withShortcut; - - private static final Path NO_ICON = Path.of(""); - private static final Map.Entry LAUNCHER_AS_SERVICE = Map.entry( - "launcher-as-service", "true"); + private final Map rawProperties = new HashMap<>(); + private BiConsumer>> createFileHandler; + private final Set verifyActions = new HashSet<>(Action.VERIFY_DEFAULTS); + + static final Path NO_ICON = Path.of(""); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 220aa19c2a698..4da226fe338fd 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -72,7 +72,7 @@ public JPackageCommand() { verifyActions = new Actions(); } - public JPackageCommand(JPackageCommand cmd) { + private JPackageCommand(JPackageCommand cmd, boolean immutable) { args.addAll(cmd.args); withToolProvider = cmd.withToolProvider; saveConsoleOutput = cmd.saveConsoleOutput; @@ -81,7 +81,7 @@ public JPackageCommand(JPackageCommand cmd) { suppressOutput = cmd.suppressOutput; ignoreDefaultRuntime = cmd.ignoreDefaultRuntime; ignoreDefaultVerbose = cmd.ignoreDefaultVerbose; - immutable = cmd.immutable; + this.immutable = immutable; dmgInstallDir = cmd.dmgInstallDir; prerequisiteActions = new Actions(cmd.prerequisiteActions); verifyActions = new Actions(cmd.verifyActions); @@ -93,9 +93,11 @@ public JPackageCommand(JPackageCommand cmd) { } JPackageCommand createImmutableCopy() { - JPackageCommand reply = new JPackageCommand(this); - reply.immutable = true; - return reply; + return new JPackageCommand(this, true); + } + + JPackageCommand createMutableCopy() { + return new JPackageCommand(this, false); } public JPackageCommand setArgumentValue(String argName, String newValue) { @@ -789,11 +791,6 @@ public JPackageCommand executePrerequisiteActions() { return this; } - public JPackageCommand executeVerifyActions() { - verifyActions.run(); - return this; - } - private Executor createExecutor() { Executor exec = new Executor() .saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput) @@ -858,7 +855,7 @@ public Executor.Result execute(int expectedExitCode) { ConfigFilesStasher.INSTANCE.accept(this); } - final var copy = new JPackageCommand(this).adjustArgumentsBeforeExecution(); + final var copy = createMutableCopy().adjustArgumentsBeforeExecution(); final var directoriesAssert = new ReadOnlyPathsAssert(copy); @@ -875,7 +872,7 @@ public Executor.Result execute(int expectedExitCode) { } if (result.exitCode() == 0) { - executeVerifyActions(); + verifyActions.run(); } return result; @@ -883,7 +880,7 @@ public Executor.Result execute(int expectedExitCode) { public Executor.Result executeAndAssertHelloAppImageCreated() { Executor.Result result = executeAndAssertImageCreated(); - HelloApp.executeLauncherAndVerifyOutput(this); + LauncherVerifier.executeMainLauncherAndVerifyOutput(this); return result; } @@ -1059,18 +1056,19 @@ public JPackageCommand excludeReadOnlyPathAssert(ReadOnlyPathAssert... asserts) public static enum AppLayoutAssert { APP_IMAGE_FILE(JPackageCommand::assertAppImageFile), PACKAGE_FILE(JPackageCommand::assertPackageFile), - MAIN_LAUNCHER(cmd -> { + NO_MAIN_LAUNCHER_IN_RUNTIME(cmd -> { if (cmd.isRuntime()) { TKit.assertPathExists(convertFromRuntime(cmd).appLauncherPath(), false); - } else { - TKit.assertExecutableFileExists(cmd.appLauncherPath()); } }), - MAIN_LAUNCHER_CFG_FILE(cmd -> { + NO_MAIN_LAUNCHER_CFG_FILE_IN_RUNTIME(cmd -> { if (cmd.isRuntime()) { TKit.assertPathExists(convertFromRuntime(cmd).appLauncherCfgPath(null), false); - } else { - TKit.assertFileExists(cmd.appLauncherCfgPath(null)); + } + }), + MAIN_LAUNCHER_FILES(cmd -> { + if (!cmd.isRuntime()) { + new LauncherVerifier(cmd).verify(cmd, LauncherVerifier.Action.VERIFY_INSTALLED); } }), MAIN_JAR_FILE(cmd -> { @@ -1101,7 +1099,7 @@ public static enum AppLayoutAssert { } private static JPackageCommand convertFromRuntime(JPackageCommand cmd) { - var copy = new JPackageCommand(cmd); + var copy = cmd.createMutableCopy(); copy.immutable = false; copy.removeArgumentWithValue("--runtime-image"); copy.dmgInstallDir = cmd.appInstallationDirectory(); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java new file mode 100644 index 0000000000000..6fb5c6e14a894 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025, 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.test; + +import static java.util.stream.Collectors.toMap; +import static jdk.jpackage.test.AdditionalLauncher.getAdditionalLauncherProperties; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; +import jdk.jpackage.test.AdditionalLauncher.PropertyFile; + +public enum LauncherShortcut { + + LINUX_SHORTCUT("linux-shortcut"), + + WIN_DESKTOP_SHORTCUT("win-shortcut"), + + WIN_START_MENU_SHORTCUT("win-menu"); + + public enum StartupDirectory { + DEFAULT("true"), + ; + + StartupDirectory(String stringValue) { + this.stringValue = Objects.requireNonNull(stringValue); + } + + public String asStringValue() { + return stringValue; + } + + /** + * Returns shortcut startup directory or an empty {@link Optional} instance if + * the value of the {@code str} parameter evaluates to {@code false}. + * + * @param str the value of a shortcut startup directory + * @return shortcut startup directory or an empty {@link Optional} instance + * @throws IllegalArgumentException if the value of the {@code str} parameter is + * unrecognized + */ + static Optional parse(String str) { + Objects.requireNonNull(str); + return Optional.ofNullable(VALUE_MAP.get(str)).or(() -> { + if (Boolean.TRUE.toString().equals(str)) { + return Optional.of(StartupDirectory.DEFAULT); + } else if (Boolean.FALSE.toString().equals(str)) { + return Optional.empty(); + } else { + throw new IllegalArgumentException(String.format( + "Unrecognized launcher shortcut startup directory: [%s]", str)); + } + }); + } + + private final String stringValue; + + private final static Map VALUE_MAP = + Stream.of(values()).collect(toMap(StartupDirectory::asStringValue, x -> x)); + } + + LauncherShortcut(String propertyName) { + this.propertyName = Objects.requireNonNull(propertyName); + } + + public String propertyName() { + return propertyName; + } + + public String optionName() { + return "--" + propertyName; + } + + Optional expectShortcut(JPackageCommand cmd, Optional predefinedAppImage, String launcherName) { + Objects.requireNonNull(predefinedAppImage); + + final var name = Optional.ofNullable(launcherName).orElseGet(cmd::name); + + if (name.equals(cmd.name())) { + return findMainLauncherShortcut(cmd); + } else { + return findAddLauncherShortcut(cmd, predefinedAppImage.map(appImage -> { + return new PropertyFile(appImage.addLaunchers().get(launcherName)); + }).orElseGet(() -> { + return getAdditionalLauncherProperties(cmd, launcherName); + })::findProperty); + } + } + + + public interface InvokeShortcutSpec { + String launcherName(); + LauncherShortcut shortcut(); + Optional expectedWorkDirectory(); + List commandLine(); + + record Stub( + String launcherName, + LauncherShortcut shortcut, + Optional expectedWorkDirectory, + List commandLine) implements InvokeShortcutSpec { + + public Stub { + Objects.requireNonNull(launcherName); + Objects.requireNonNull(shortcut); + Objects.requireNonNull(expectedWorkDirectory); + Objects.requireNonNull(commandLine); + } + } + } + + + private Optional findMainLauncherShortcut(JPackageCommand cmd) { + if (cmd.hasArgument(optionName())) { + return Optional.of(StartupDirectory.DEFAULT); + } else { + return Optional.empty(); + } + } + + private Optional findAddLauncherShortcut(JPackageCommand cmd, + Function> addlauncherProperties) { + var explicit = addlauncherProperties.apply(propertyName()); + if (explicit.isPresent()) { + return explicit.flatMap(StartupDirectory::parse); + } else { + return findMainLauncherShortcut(cmd); + } + } + + private final String propertyName; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java new file mode 100644 index 0000000000000..55f7a1b93145f --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java @@ -0,0 +1,326 @@ +/* + * Copyright (c) 2025, 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.test; + +import static java.util.stream.Collectors.toMap; +import static jdk.jpackage.test.AdditionalLauncher.NO_ICON; +import static jdk.jpackage.test.LauncherShortcut.LINUX_SHORTCUT; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import jdk.jpackage.internal.util.function.ThrowingBiConsumer; +import jdk.jpackage.test.AdditionalLauncher.PropertyFile; +import jdk.jpackage.test.LauncherShortcut.StartupDirectory; + +public final class LauncherVerifier { + + LauncherVerifier(JPackageCommand cmd) { + name = cmd.name(); + javaOptions = Optional.empty(); + arguments = Optional.empty(); + icon = Optional.empty(); + properties = Optional.empty(); + } + + LauncherVerifier(String name, + Optional> javaOptions, + Optional> arguments, + Optional icon, + Map properties) { + this.name = Objects.requireNonNull(name); + this.javaOptions = javaOptions.map(List::copyOf); + this.arguments = arguments.map(List::copyOf); + this.icon = icon; + this.properties = Optional.of(new PropertyFile(properties)); + } + + static void executeMainLauncherAndVerifyOutput(JPackageCommand cmd) { + new LauncherVerifier(cmd).verify(cmd, Action.EXECUTE_LAUNCHER); + } + + + public enum Action { + VERIFY_ICON(LauncherVerifier::verifyIcon), + VERIFY_DESCRIPTION(LauncherVerifier::verifyDescription), + VERIFY_INSTALLED((verifier, cmd) -> { + verifier.verifyInstalled(cmd, true); + }), + VERIFY_UNINSTALLED((verifier, cmd) -> { + verifier.verifyInstalled(cmd, false); + }), + EXECUTE_LAUNCHER(LauncherVerifier::executeLauncher), + ; + + Action(ThrowingBiConsumer action) { + this.action = ThrowingBiConsumer.toBiConsumer(action); + } + + private void apply(LauncherVerifier verifier, JPackageCommand cmd) { + action.accept(verifier, cmd); + } + + private final BiConsumer action; + + static final List VERIFY_APP_IMAGE = List.of( + VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED + ); + + static final List VERIFY_DEFAULTS = Stream.concat( + VERIFY_APP_IMAGE.stream(), Stream.of(EXECUTE_LAUNCHER) + ).toList(); + } + + + void verify(JPackageCommand cmd, Action... actions) { + verify(cmd, List.of(actions)); + } + + void verify(JPackageCommand cmd, Iterable actions) { + Objects.requireNonNull(cmd); + for (var a : actions) { + a.apply(this, cmd); + } + } + + private boolean isMainLauncher() { + return properties.isEmpty(); + } + + private Optional findProperty(String key) { + return properties.flatMap(v -> { + return v.findProperty(key); + }); + } + + private String getDescription(JPackageCommand cmd) { + return findProperty("description").orElseGet(() -> { + return cmd.getArgumentValue("--description", cmd::name); + }); + } + + private List getArguments(JPackageCommand cmd) { + return getStringArrayProperty(cmd, "--arguments", arguments); + } + + private List getJavaOptions(JPackageCommand cmd) { + return getStringArrayProperty(cmd, "--java-options", javaOptions); + } + + private List getStringArrayProperty(JPackageCommand cmd, String optionName, Optional> items) { + Objects.requireNonNull(cmd); + Objects.requireNonNull(optionName); + Objects.requireNonNull(items); + if (isMainLauncher()) { + return List.of(cmd.getAllArgumentValues(optionName)); + } else { + return items.orElseGet(() -> { + return List.of(cmd.getAllArgumentValues(optionName)); + }); + } + } + + private boolean explicitlyNoShortcut(LauncherShortcut shortcut) { + var explicit = findProperty(shortcut.propertyName()); + if (explicit.isPresent()) { + return explicit.flatMap(StartupDirectory::parse).isEmpty(); + } else { + return false; + } + } + + private static boolean explicitShortcutForMainLauncher(JPackageCommand cmd, LauncherShortcut shortcut) { + return cmd.hasArgument(shortcut.optionName()); + } + + private void verifyIcon(JPackageCommand cmd) throws IOException { + initIconVerifier(cmd).applyTo(cmd); + } + + private LauncherIconVerifier initIconVerifier(JPackageCommand cmd) { + var verifier = new LauncherIconVerifier().setLauncherName(name); + + var mainLauncherIcon = Optional.ofNullable(cmd.getArgumentValue("--icon")).map(Path::of).or(() -> { + return iconInResourceDir(cmd, cmd.name()); + }); + + if (TKit.isOSX()) { + // There should be no icon files on Mac for additional launchers, + // and always an icon file for the main launcher. + if (isMainLauncher()) { + mainLauncherIcon.ifPresentOrElse(verifier::setExpectedIcon, verifier::setExpectedDefaultIcon); + } + return verifier; + } + + if (isMainLauncher()) { + mainLauncherIcon.ifPresentOrElse(verifier::setExpectedIcon, verifier::setExpectedDefaultIcon); + } else { + icon.ifPresentOrElse(icon -> { + if (!NO_ICON.equals(icon)) { + verifier.setExpectedIcon(icon); + } + }, () -> { + // No "icon" property in the property file + iconInResourceDir(cmd, name).ifPresentOrElse(verifier::setExpectedIcon, () -> { + // No icon for this additional launcher in the resource directory. + mainLauncherIcon.ifPresentOrElse(verifier::setExpectedIcon, verifier::setExpectedDefaultIcon); + }); + }); + } + + return verifier; + } + + private static boolean withLinuxMainLauncherDesktopFile(JPackageCommand cmd) { + if (!TKit.isLinux() || cmd.isImagePackageType()) { + return false; + } + + return explicitShortcutForMainLauncher(cmd, LINUX_SHORTCUT) + || cmd.hasArgument("--icon") + || cmd.hasArgument("--file-associations") + || iconInResourceDir(cmd, cmd.name()).isPresent(); + } + + private boolean withLinuxDesktopFile(JPackageCommand cmd) { + if (!TKit.isLinux() || cmd.isImagePackageType()) { + return false; + } + + if (isMainLauncher()) { + return withLinuxMainLauncherDesktopFile(cmd); + } else if (explicitlyNoShortcut(LINUX_SHORTCUT) || icon.map(icon -> { + return icon.equals(NO_ICON); + }).orElse(false)) { + return false; + } else if (iconInResourceDir(cmd, name).isPresent() || icon.map(icon -> { + return !icon.equals(NO_ICON); + }).orElse(false)) { + return true; + } else if (findProperty(LINUX_SHORTCUT.propertyName()).flatMap(StartupDirectory::parse).isPresent()) { + return true; + } else { + return withLinuxMainLauncherDesktopFile(cmd.createMutableCopy().removeArgument("--file-associations")); + } + } + + private void verifyDescription(JPackageCommand cmd) throws IOException { + if (TKit.isWindows()) { + String expectedDescription = getDescription(cmd); + Path launcherPath = cmd.appLauncherPath(name); + String actualDescription = + WindowsHelper.getExecutableDescription(launcherPath); + TKit.assertEquals(expectedDescription, actualDescription, + String.format("Check file description of [%s]", launcherPath)); + } else if (TKit.isLinux() && !cmd.isImagePackageType()) { + String expectedDescription = getDescription(cmd); + Path desktopFile = LinuxHelper.getDesktopFile(cmd, name); + if (Files.exists(desktopFile)) { + TKit.assertTextStream("Comment=" + expectedDescription) + .label(String.format("[%s] file", desktopFile)) + .predicate(String::equals) + .apply(Files.readAllLines(desktopFile)); + } + } + } + + private void verifyInstalled(JPackageCommand cmd, boolean installed) throws IOException { + var launcherPath = cmd.appLauncherPath(name); + var launcherCfgFilePath = cmd.appLauncherCfgPath(name); + if (installed) { + TKit.assertExecutableFileExists(launcherPath); + TKit.assertFileExists(launcherCfgFilePath); + } else { + TKit.assertPathExists(launcherPath, false); + TKit.assertPathExists(launcherCfgFilePath, false); + } + + if (TKit.isLinux() && !cmd.isImagePackageType()) { + final var packageDesktopFile = LinuxHelper.getDesktopFile(cmd, name); + final var withLinuxDesktopFile = withLinuxDesktopFile(cmd) && installed; + if (withLinuxDesktopFile) { + TKit.assertFileExists(packageDesktopFile); + } else { + TKit.assertPathExists(packageDesktopFile, false); + } + } + + if (installed) { + initIconVerifier(cmd).verifyFileInAppImageOnly(true).applyTo(cmd); + } + } + + private void executeLauncher(JPackageCommand cmd) throws IOException { + Path launcherPath = cmd.appLauncherPath(name); + + if (!cmd.canRunLauncher(String.format("Not running %s launcher", launcherPath))) { + return; + } + + var appVerifier = HelloApp.assertApp(launcherPath) + .addDefaultArguments(getArguments(cmd)) + .addJavaOptions(getJavaOptions(cmd).stream().map(str -> { + return resolveVariables(cmd, str); + }).toList()); + + appVerifier.executeAndVerifyOutput(); + } + + private static String resolveVariables(JPackageCommand cmd, String str) { + var map = Stream.of(JPackageCommand.Macro.values()).collect(toMap(x -> { + return String.format("$%s", x.name()); + }, cmd::macroValue)); + for (var e : map.entrySet()) { + str = str.replaceAll(Pattern.quote(e.getKey()), + Matcher.quoteReplacement(e.getValue().toString())); + } + return str; + } + + private static Optional iconInResourceDir(JPackageCommand cmd, String launcherName) { + Objects.requireNonNull(launcherName); + return Optional.ofNullable(cmd.getArgumentValue("--resource-dir")).map(Path::of).map(resourceDir -> { + Path icon = resourceDir.resolve(launcherName + TKit.ICON_SUFFIX); + if (Files.exists(icon)) { + return icon; + } else { + return null; + } + }); + } + + private final String name; + private final Optional> javaOptions; + private final Optional> arguments; + private final Optional icon; + private final Optional properties; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index 6afb0eca38db1..a7baf9d92f32b 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -271,8 +271,7 @@ static void withFileAssociationsTestRuns(FileAssociations fa, PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa) { Objects.requireNonNull(fa); - // Setup test app to have valid jpackage command line before - // running check of type of environment. + // Setup test app to have valid jpackage command line before running the check. addHelloAppInitializer(null); forTypes(LINUX, () -> { @@ -353,15 +352,14 @@ public PackageTest configureHelloApp() { public PackageTest configureHelloApp(String javaAppDesc) { addHelloAppInitializer(javaAppDesc); - addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput); + addInstallVerifier(JPackageCommand::executeLaunchers); return this; } public PackageTest addHelloAppInitializer(String javaAppDesc) { - addInitializer( - cmd -> new HelloApp(JavaAppDesc.parse(javaAppDesc)).addTo(cmd), - "HelloApp"); - return this; + return addInitializer(cmd -> { + new HelloApp(JavaAppDesc.parse(javaAppDesc)).addTo(cmd); + }, "HelloApp"); } public static class Group extends RunnablePackageTest { @@ -604,11 +602,7 @@ private ActionAction analizeAction(Action action) { } } case VERIFY_INSTALL -> { - if (unpackNotSupported()) { - return ActionAction.SKIP; - } - - if (installFailed()) { + if (unpackNotSupported() || installFailed()) { return ActionAction.SKIP; } } From 53576884e7cf0fd88ccfedc63eaa97d268ce54e7 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 26 Jul 2025 17:06:03 -0400 Subject: [PATCH 12/83] PerUserCfgTest: follow-up changes in AdditionalLauncher --- test/jdk/tools/jpackage/share/PerUserCfgTest.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/test/jdk/tools/jpackage/share/PerUserCfgTest.java b/test/jdk/tools/jpackage/share/PerUserCfgTest.java index 080df1f959d3e..d2f368cd8243e 100644 --- a/test/jdk/tools/jpackage/share/PerUserCfgTest.java +++ b/test/jdk/tools/jpackage/share/PerUserCfgTest.java @@ -27,13 +27,14 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import jdk.jpackage.internal.util.function.ThrowingConsumer; import jdk.jpackage.test.AdditionalLauncher; -import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.Annotations.Test; -import jdk.jpackage.internal.util.function.ThrowingConsumer; import jdk.jpackage.test.HelloApp; import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.LauncherVerifier.Action; import jdk.jpackage.test.LinuxHelper; +import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.PackageType; import jdk.jpackage.test.TKit; @@ -65,7 +66,7 @@ public static void test() throws IOException { cfgCmd.execute(); - new PackageTest().configureHelloApp().addInstallVerifier(cmd -> { + new PackageTest().addHelloAppInitializer(null).addInstallVerifier(cmd -> { if (cmd.isPackageUnpacked("Not running per-user configuration tests")) { return; } @@ -144,10 +145,7 @@ public static void test() throws IOException { } private static void addLauncher(JPackageCommand cmd, String name) { - new AdditionalLauncher(name) { - @Override - protected void verify(JPackageCommand cmd) {} - }.setDefaultArguments(name).applyTo(cmd); + new AdditionalLauncher(name).setDefaultArguments(name).withoutVerifyActions(Action.EXECUTE_LAUNCHER).applyTo(cmd); } private static Path getUserHomeDir() { From 777830c4f7d6a09ab999ebee44ebbcb67cddd49d Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 26 Jul 2025 22:11:39 -0400 Subject: [PATCH 13/83] LauncherAsServiceVerifier: follow-up for the changes in the AdditionalLauncher --- .../test/LauncherAsServiceVerifier.java | 95 ++++++++----------- 1 file changed, 39 insertions(+), 56 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java index fd8b4011341fb..428218228942b 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java @@ -22,11 +22,19 @@ */ package jdk.jpackage.test; +import static jdk.jpackage.internal.util.function.ThrowingBiConsumer.toBiConsumer; +import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; +import static jdk.jpackage.test.AdditionalLauncher.forEachAdditionalLauncher; +import static jdk.jpackage.test.PackageType.LINUX; +import static jdk.jpackage.test.PackageType.MAC_PKG; +import static jdk.jpackage.test.PackageType.WINDOWS; + import java.io.IOException; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -36,12 +44,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.internal.util.PathUtils; -import jdk.jpackage.internal.util.function.ThrowingBiConsumer; -import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; import jdk.jpackage.internal.util.function.ThrowingRunnable; -import static jdk.jpackage.test.PackageType.LINUX; -import static jdk.jpackage.test.PackageType.MAC_PKG; -import static jdk.jpackage.test.PackageType.WINDOWS; +import jdk.jpackage.test.AdditionalLauncher.PropertyFile; +import jdk.jpackage.test.LauncherVerifier.Action; public final class LauncherAsServiceVerifier { @@ -111,6 +116,7 @@ public void applyTo(PackageTest pkg) { } else { applyToAdditionalLauncher(pkg); } + pkg.addInstallVerifier(this::verifyLauncherExecuted); } static void verify(JPackageCommand cmd) { @@ -127,7 +133,6 @@ static void verify(JPackageCommand cmd) { "service-installer.exe"); if (launcherNames.isEmpty()) { TKit.assertPathExists(serviceInstallerPath, false); - } else { TKit.assertFileExists(serviceInstallerPath); } @@ -188,23 +193,11 @@ static List getLaunchersAsServices(JPackageCommand cmd) { launcherNames.add(null); } - AdditionalLauncher.forEachAdditionalLauncher(cmd, - ThrowingBiConsumer.toBiConsumer( - (launcherName, propFilePath) -> { - if (Files.readAllLines(propFilePath).stream().anyMatch( - line -> { - if (line.startsWith( - "launcher-as-service=")) { - return Boolean.parseBoolean( - line.substring( - "launcher-as-service=".length())); - } else { - return false; - } - })) { - launcherNames.add(launcherName); - } - })); + forEachAdditionalLauncher(cmd, toBiConsumer((launcherName, propFilePath) -> { + if (new PropertyFile(propFilePath).findBooleanProperty("launcher-as-service").orElse(false)) { + launcherNames.add(launcherName); + } + })); return launcherNames; } @@ -237,45 +230,33 @@ private void applyToMainLauncher(PackageTest pkg) { + appOutputFilePathInitialize().toString()); cmd.addArguments("--java-options", "-Djpackage.test.noexit=true"); }); - pkg.addInstallVerifier(cmd -> { - if (canVerifyInstall(cmd)) { - delayInstallVerify(); - Path outputFilePath = appOutputFilePathVerify(cmd); - HelloApp.assertApp(cmd.appLauncherPath()) - .addParam("jpackage.test.appOutput", - outputFilePath.toString()) - .addDefaultArguments(expectedValue) - .verifyOutput(); - deleteOutputFile(outputFilePath); - } - }); - pkg.addInstallVerifier(cmd -> { - verify(cmd, launcherName); - }); } private void applyToAdditionalLauncher(PackageTest pkg) { - AdditionalLauncher al = new AdditionalLauncher(launcherName) { - @Override - protected void verify(JPackageCommand cmd) throws IOException { - if (canVerifyInstall(cmd)) { - delayInstallVerify(); - super.verify(cmd); - deleteOutputFile(appOutputFilePathVerify(cmd)); - } - LauncherAsServiceVerifier.verify(cmd, launcherName); - } - }.setLauncherAsService() - .addJavaOptions("-Djpackage.test.appOutput=" - + appOutputFilePathInitialize().toString()) + var al = new AdditionalLauncher(launcherName) + .setProperty("launcher-as-service", true) + .addJavaOptions("-Djpackage.test.appOutput=" + appOutputFilePathInitialize().toString()) .addJavaOptions("-Djpackage.test.noexit=true") - .addDefaultArguments(expectedValue); + .addDefaultArguments(expectedValue) + .withoutVerifyActions(Action.EXECUTE_LAUNCHER); Optional.ofNullable(additionalLauncherCallback).ifPresent(v -> v.accept(al)); al.applyTo(pkg); } + private void verifyLauncherExecuted(JPackageCommand cmd) throws IOException { + if (canVerifyInstall(cmd)) { + delayInstallVerify(); + Path outputFilePath = appOutputFilePathVerify(cmd); + HelloApp.assertApp(cmd.appLauncherPath()) + .addParam("jpackage.test.appOutput", outputFilePath.toString()) + .addDefaultArguments(expectedValue) + .verifyOutput(); + deleteOutputFile(outputFilePath); + } + } + private static void deleteOutputFile(Path file) throws IOException { try { TKit.deleteIfExists(file); @@ -291,8 +272,7 @@ private static void deleteOutputFile(Path file) throws IOException { } } - private static void verify(JPackageCommand cmd, String launcherName) throws - IOException { + private static void verify(JPackageCommand cmd, String launcherName) throws IOException { if (LINUX.contains(cmd.packageType())) { verifyLinuxUnitFile(cmd, launcherName); } else if (MAC_PKG.equals(cmd.packageType())) { @@ -370,6 +350,9 @@ private Path appOutputFilePathVerify(JPackageCommand cmd) { private final Path appOutputFileName; private final Consumer additionalLauncherCallback; - static final Set SUPPORTED_PACKAGES = Stream.of(LINUX, WINDOWS, - Set.of(MAC_PKG)).flatMap(x -> x.stream()).collect(Collectors.toSet()); + static final Set SUPPORTED_PACKAGES = Stream.of( + LINUX, + WINDOWS, + Set.of(MAC_PKG) + ).flatMap(Collection::stream).collect(Collectors.toSet()); } From 1dc3781fd657d923664a5f96294a9f54fd1f086f Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 28 Jul 2025 12:31:24 -0400 Subject: [PATCH 14/83] Rework .desktop files verification: Always verify .desktop files are installed in the system folder on package install and uninstalled on package uninstall. Always verify contents of .desktop files. --- .../jdk/jpackage/test/LinuxHelper.java | 57 ++++++++++++++----- .../jdk/jpackage/test/PackageTest.java | 10 +++- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 3a1cad55c8731..ffe46288a219b 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -23,12 +23,14 @@ package jdk.jpackage.test; import java.io.IOException; +import java.io.UncheckedIOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -345,8 +347,7 @@ static void verifyPackageBundleEssential(JPackageCommand cmd) { } } - static void addBundleDesktopIntegrationVerifier(PackageTest test, - boolean integrated) { + static void addBundleDesktopIntegrationVerifier(PackageTest test, boolean integrated) { final String xdgUtils = "xdg-utils"; Function, String> verifier = (lines) -> { @@ -392,22 +393,50 @@ static void addBundleDesktopIntegrationVerifier(PackageTest test, }); test.addInstallVerifier(cmd -> { - // Verify .desktop files. - try (var files = Files.list(cmd.appLayout().desktopIntegrationDirectory())) { - List desktopFiles = files - .filter(path -> path.getFileName().toString().endsWith(".desktop")) - .toList(); - if (!integrated) { - TKit.assertStringListEquals(List.of(), - desktopFiles.stream().map(Path::toString).collect( - Collectors.toList()), - "Check there are no .desktop files in the package"); - } + if (!integrated) { + TKit.assertStringListEquals( + List.of(), + getDesktopFiles(cmd).stream().map(Path::toString).toList(), + "Check there are no .desktop files in the package"); + } + }); + } + + static void verifyDesktopFiles(JPackageCommand cmd, boolean installed) { + final var desktopFiles = getDesktopFiles(cmd); + try { + if (installed) { for (var desktopFile : desktopFiles) { verifyDesktopFile(cmd, desktopFile); } + + if (!cmd.isPackageUnpacked("Not verifying system .desktop files")) { + for (var desktopFile : desktopFiles) { + Path systemDesktopFile = LinuxHelper.getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); + TKit.assertFileExists(systemDesktopFile); + TKit.assertStringListEquals( + Files.readAllLines(desktopFile), + Files.readAllLines(systemDesktopFile), + String.format("Check [%s] and [%s] files are equal", desktopFile, systemDesktopFile)); + } + } + } else { + for (var desktopFile : getDesktopFiles(cmd)) { + Path systemDesktopFile = LinuxHelper.getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); + TKit.assertPathExists(systemDesktopFile, false); + } } - }); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static Collection getDesktopFiles(JPackageCommand cmd) { + var unpackedDir = cmd.appLayout().desktopIntegrationDirectory(); + var packageDir = cmd.pathToPackageFile(unpackedDir); + return getPackageFiles(cmd).filter(path -> { + return path.getParent().equals(packageDir) && path.getFileName().toString().endsWith(".desktop"); + }).map(Path::getFileName).map(unpackedDir::resolve).toList(); } private static void verifyDesktopFile(JPackageCommand cmd, Path desktopFile) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index a7baf9d92f32b..affc9ab22bdbd 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -352,7 +352,7 @@ public PackageTest configureHelloApp() { public PackageTest configureHelloApp(String javaAppDesc) { addHelloAppInitializer(javaAppDesc); - addInstallVerifier(JPackageCommand::executeLaunchers); + addInstallVerifier(LauncherVerifier::executeMainLauncherAndVerifyOutput); return this; } @@ -765,6 +765,10 @@ private void verifyPackageInstalled(JPackageCommand cmd) { if (isOfType(cmd, WINDOWS) && !cmd.isPackageUnpacked("Not verifying desktop integration")) { WindowsHelper.verifyDeployedDesktopIntegration(cmd, true); } + + if (isOfType(cmd, LINUX)) { + LinuxHelper.verifyDesktopFiles(cmd, true); + } } if (isOfType(cmd, LauncherAsServiceVerifier.SUPPORTED_PACKAGES)) { @@ -842,6 +846,10 @@ private void verifyPackageUninstalled(JPackageCommand cmd) { if (isOfType(cmd, WINDOWS)) { WindowsHelper.verifyDeployedDesktopIntegration(cmd, false); } + + if (isOfType(cmd, LINUX)) { + LinuxHelper.verifyDesktopFiles(cmd, false); + } } Path appInstallDir = cmd.appInstallationDirectory(); From ff1c5d22d1111a1c4e94e81cd81a1af40269f5c9 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 30 Jul 2025 16:18:54 -0400 Subject: [PATCH 15/83] PrintEnv: support `--print-workdir` CLI option and `jpackage.test.appOutput` Java property to redirect output --- test/jdk/tools/jpackage/apps/PrintEnv.java | 33 ++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/test/jdk/tools/jpackage/apps/PrintEnv.java b/test/jdk/tools/jpackage/apps/PrintEnv.java index bb1cef800f490..64a243a0abcfa 100644 --- a/test/jdk/tools/jpackage/apps/PrintEnv.java +++ b/test/jdk/tools/jpackage/apps/PrintEnv.java @@ -21,18 +21,38 @@ * questions. */ +import java.io.IOException; +import java.io.UncheckedIOException; import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleFinder; import java.lang.module.ModuleReference; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; public class PrintEnv { public static void main(String[] args) { List lines = printArgs(args); - lines.forEach(System.out::println); + Optional.ofNullable(System.getProperty("jpackage.test.appOutput")).map(Path::of).ifPresentOrElse(outputFilePath -> { + Optional.ofNullable(outputFilePath.getParent()).ifPresent(dir -> { + try { + Files.createDirectories(dir); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + try { + Files.write(outputFilePath, lines); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }, () -> { + lines.forEach(System.out::println); + }); } private static List printArgs(String[] args) { @@ -45,11 +65,13 @@ private static List printArgs(String[] args) { } else if (arg.startsWith(PRINT_SYS_PROP)) { String name = arg.substring(PRINT_SYS_PROP.length()); lines.add(name + "=" + System.getProperty(name)); - } else if (arg.startsWith(PRINT_MODULES)) { + } else if (arg.equals(PRINT_MODULES)) { lines.add(ModuleFinder.ofSystem().findAll().stream() .map(ModuleReference::descriptor) .map(ModuleDescriptor::name) .collect(Collectors.joining(","))); + } else if (arg.equals(PRINT_WORK_DIR)) { + lines.add("$CD=" + Path.of("").toAbsolutePath()); } else { throw new IllegalArgumentException(); } @@ -58,7 +80,8 @@ private static List printArgs(String[] args) { return lines; } - private final static String PRINT_ENV_VAR = "--print-env-var="; - private final static String PRINT_SYS_PROP = "--print-sys-prop="; - private final static String PRINT_MODULES = "--print-modules"; + private static final String PRINT_ENV_VAR = "--print-env-var="; + private static final String PRINT_SYS_PROP = "--print-sys-prop="; + private static final String PRINT_MODULES = "--print-modules"; + private static final String PRINT_WORK_DIR = "--print-workdir"; } From 02ac3e778cf68a83814cf1288f9910b6a1bf692d Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 30 Jul 2025 16:28:39 -0400 Subject: [PATCH 16/83] LinuxHelper: add getInvokeShortcutSpecs(); add DesktopFile type to streamline verifyDesktopFile() --- .../jdk/jpackage/test/LinuxHelper.java | 179 +++++++++++++----- 1 file changed, 131 insertions(+), 48 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index ffe46288a219b..32c291173adbe 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -35,6 +35,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -44,6 +45,7 @@ import java.util.stream.Stream; import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.function.ThrowingConsumer; +import jdk.jpackage.test.LauncherShortcut.InvokeShortcutSpec; import jdk.jpackage.test.PackageTest.PackageHandlers; @@ -310,8 +312,8 @@ static String getRpmBundleProperty(Path bundle, String fieldName) { } static void verifyPackageBundleEssential(JPackageCommand cmd) { - String packageName = LinuxHelper.getPackageName(cmd); - long packageSize = LinuxHelper.getInstalledPackageSizeKB(cmd); + String packageName = getPackageName(cmd); + long packageSize = getInstalledPackageSizeKB(cmd); TKit.trace("InstalledPackageSize: " + packageSize); TKit.assertNotEquals(0, packageSize, String.format( "Check installed size of [%s] package in not zero", packageName)); @@ -332,7 +334,7 @@ static void verifyPackageBundleEssential(JPackageCommand cmd) { checkPrerequisites = packageSize > 5; } - List prerequisites = LinuxHelper.getPrerequisitePackages(cmd); + List prerequisites = getPrerequisitePackages(cmd); if (checkPrerequisites) { final String vitalPackage = "libc"; TKit.assertTrue(prerequisites.stream().filter( @@ -347,6 +349,22 @@ static void verifyPackageBundleEssential(JPackageCommand cmd) { } } + public static Collection getInvokeShortcutSpecs(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX); + + final var desktopFiles = getDesktopFiles(cmd); + final var predefinedAppImage = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of).map(AppImageFile::load); + + return desktopFiles.stream().map(desktopFile -> { + var systemDesktopFile = getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); + return new InvokeShortcutSpec.Stub( + launcherNameFromDesktopFile(cmd, predefinedAppImage, desktopFile), + LauncherShortcut.LINUX_SHORTCUT, + new DesktopFile(systemDesktopFile, false).findQuotedValue("Path").map(Path::of), + List.of("xdg-open", systemDesktopFile.toString())); + }).toList(); + } + static void addBundleDesktopIntegrationVerifier(PackageTest test, boolean integrated) { final String xdgUtils = "xdg-utils"; @@ -406,13 +424,14 @@ static void verifyDesktopFiles(JPackageCommand cmd, boolean installed) { final var desktopFiles = getDesktopFiles(cmd); try { if (installed) { + var predefinedAppImage = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of).map(AppImageFile::load); for (var desktopFile : desktopFiles) { - verifyDesktopFile(cmd, desktopFile); + verifyDesktopFile(cmd, predefinedAppImage, desktopFile); } if (!cmd.isPackageUnpacked("Not verifying system .desktop files")) { for (var desktopFile : desktopFiles) { - Path systemDesktopFile = LinuxHelper.getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); + Path systemDesktopFile = getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); TKit.assertFileExists(systemDesktopFile); TKit.assertStringListEquals( Files.readAllLines(desktopFile), @@ -422,7 +441,7 @@ static void verifyDesktopFiles(JPackageCommand cmd, boolean installed) { } } else { for (var desktopFile : getDesktopFiles(cmd)) { - Path systemDesktopFile = LinuxHelper.getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); + Path systemDesktopFile = getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); TKit.assertPathExists(systemDesktopFile, false); } } @@ -439,34 +458,34 @@ private static Collection getDesktopFiles(JPackageCommand cmd) { }).map(Path::getFileName).map(unpackedDir::resolve).toList(); } - private static void verifyDesktopFile(JPackageCommand cmd, Path desktopFile) - throws IOException { - TKit.trace(String.format("Check [%s] file BEGIN", desktopFile)); + private static String launcherNameFromDesktopFile(JPackageCommand cmd, Optional predefinedAppImage, Path desktopFile) { + Objects.requireNonNull(cmd); + Objects.requireNonNull(predefinedAppImage); + Objects.requireNonNull(desktopFile); - var launcherName = Stream.of(List.of(cmd.name()), cmd.addLauncherNames()).flatMap(List::stream).filter(name -> { + return predefinedAppImage.map(v -> { + return v.launchers().keySet().stream(); + }).orElseGet(() -> { + return Stream.concat(Stream.of(cmd.name()), cmd.addLauncherNames().stream()); + }).filter(name-> { return getDesktopFile(cmd, name).equals(desktopFile); - }).findAny(); - if (!cmd.hasArgument("--app-image")) { - TKit.assertTrue(launcherName.isPresent(), - "Check the desktop file corresponds to one of app launchers"); - } + }).findAny().orElseThrow(() -> { + TKit.assertUnexpected(String.format("Failed to find launcher corresponding to [%s] file", desktopFile)); + // Unreachable + return null; + }); + } - List lines = Files.readAllLines(desktopFile); - TKit.assertEquals("[Desktop Entry]", lines.get(0), "Check file header"); + private static void verifyDesktopFile(JPackageCommand cmd, Optional predefinedAppImage, Path desktopFile) throws IOException { + Objects.requireNonNull(cmd); + Objects.requireNonNull(predefinedAppImage); + Objects.requireNonNull(desktopFile); - Map data = lines.stream() - .skip(1) - .peek(str -> TKit.assertTextStream("=").predicate(String::contains).apply(List.of(str))) - .map(str -> { - String components[] = str.split("=(?=.+)"); - if (components.length == 1) { - return Map.entry(str.substring(0, str.length() - 1), ""); - } - return Map.entry(components[0], components[1]); - }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> { - TKit.assertUnexpected("Multiple values of the same key"); - return null; - })); + TKit.trace(String.format("Check [%s] file BEGIN", desktopFile)); + + var launcherName = launcherNameFromDesktopFile(cmd, predefinedAppImage, desktopFile); + + var data = new DesktopFile(desktopFile, true); final Set mandatoryKeys = new HashSet<>(Set.of("Name", "Comment", "Exec", "Icon", "Terminal", "Type", "Categories")); @@ -476,32 +495,40 @@ private static void verifyDesktopFile(JPackageCommand cmd, Path desktopFile) for (var e : Map.of("Type", "Application", "Terminal", "false").entrySet()) { String key = e.getKey(); - TKit.assertEquals(e.getValue(), data.get(key), String.format( + TKit.assertEquals(e.getValue(), data.find(key).orElseThrow(), String.format( "Check value of [%s] key", key)); } - // Verify the value of `Exec` key is escaped if required - String launcherPath = data.get("Exec"); - if (Pattern.compile("\\s").matcher(launcherPath).find()) { - TKit.assertTrue(launcherPath.startsWith("\"") - && launcherPath.endsWith("\""), - "Check path to the launcher is enclosed in double quotes"); - launcherPath = launcherPath.substring(1, launcherPath.length() - 1); - } + String launcherPath = data.findQuotedValue("Exec").orElseThrow(); - if (launcherName.isPresent()) { - TKit.assertEquals(launcherPath, cmd.pathToPackageFile( - cmd.appLauncherPath(launcherName.get())).toString(), - String.format( - "Check the value of [Exec] key references [%s] app launcher", - launcherName.get())); - } + TKit.assertEquals( + launcherPath, + cmd.pathToPackageFile(cmd.appLauncherPath(launcherName)).toString(), + String.format("Check the value of [Exec] key references [%s] app launcher", launcherName)); + + LauncherShortcut.LINUX_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).map(shortcutWorkDirType -> { + switch (shortcutWorkDirType) { + case DEFAULT -> { + return (Path)null; + } + default -> { + throw new AssertionError(); + } + } + }).ifPresentOrElse(shortcutWorkDir -> { + var actualShortcutWorkDir = data.findQuotedValue("Path"); + TKit.assertTrue(actualShortcutWorkDir.isPresent(), "Check [Path] key exists"); + TKit.assertEquals(actualShortcutWorkDir.get(), shortcutWorkDir, "Check the value of [Path] key"); + }, () -> { + TKit.assertTrue(data.findQuotedValue("Path").isEmpty(), "Check there is no [Path] key"); + }); for (var e : List.>, Function>>of( Map.entry(Map.entry("Exec", Optional.of(launcherPath)), ApplicationLayout::launchersDirectory), Map.entry(Map.entry("Icon", Optional.empty()), ApplicationLayout::desktopIntegrationDirectory))) { - var path = e.getKey().getValue().or(() -> Optional.of(data.get( - e.getKey().getKey()))).map(Path::of).get(); + var path = e.getKey().getValue().or(() -> { + return data.findQuotedValue(e.getKey().getKey()); + }).map(Path::of).get(); TKit.assertFileExists(cmd.pathToUnpackedPackageFile(path)); Path expectedDir = cmd.pathToPackageFile(e.getValue().apply(cmd.appLayout())); TKit.assertTrue(path.getParent().equals(expectedDir), String.format( @@ -790,6 +817,62 @@ private static Method initGetServiceUnitFileName() { } } + + private static final class DesktopFile { + DesktopFile(Path path, boolean verify) { + try { + List lines = Files.readAllLines(path); + if (verify) { + TKit.assertEquals("[Desktop Entry]", lines.getFirst(), "Check file header"); + } + + var stream = lines.stream().skip(1); + if (verify) { + stream = stream.peek(str -> { + TKit.assertTextStream("=").predicate(String::contains).apply(List.of(str)); + }); + } + + data = stream.map(str -> { + String components[] = str.split("=(?=.+)"); + if (components.length == 1) { + return Map.entry(str.substring(0, str.length() - 1), ""); + } else { + return Map.entry(components[0], components[1]); + } + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + Set keySet() { + return data.keySet(); + } + + Optional find(String property) { + return Optional.ofNullable(data.get(Objects.requireNonNull(property))); + } + + Optional findQuotedValue(String property) { + return find(property).map(value -> { + if (Pattern.compile("\\s").matcher(value).find()) { + boolean quotesMatched = value.startsWith("\"") && value.endsWith("\""); + if (!quotesMatched) { + TKit.assertTrue(quotesMatched, + String.format("Check the value of key [%s] is enclosed in double quotes", property)); + } + return value.substring(1, value.length() - 1); + } else { + return value; + } + }); + } + + private final Map data; + } + + static final Set CRITICAL_RUNTIME_FILES = Set.of(Path.of( "lib/server/libjvm.so")); From 6d62ef7fafdf48e6a6e8fb3a7e5020257f92c492 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 30 Jul 2025 16:29:38 -0400 Subject: [PATCH 17/83] WinShortcutVerifier: add getInvokeShortcutSpecs(); streamline expectLauncherShortcuts() --- .../jpackage/test/WinShortcutVerifier.java | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index 3b8d484497262..a765413233cd1 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -22,6 +22,9 @@ */ package jdk.jpackage.test; +import static java.util.stream.Collectors.groupingBy; +import static jdk.jpackage.test.LauncherShortcut.WIN_DESKTOP_SHORTCUT; +import static jdk.jpackage.test.LauncherShortcut.WIN_START_MENU_SHORTCUT; import static jdk.jpackage.test.WindowsHelper.getInstallationSubDirectory; import java.nio.file.Files; @@ -35,14 +38,14 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.internal.util.PathUtils; +import jdk.jpackage.test.LauncherShortcut.InvokeShortcutSpec; import jdk.jpackage.test.MsiDatabase.Shortcut; import jdk.jpackage.test.WindowsHelper.SpecialFolder; -final class WinShortcutVerifier { +public final class WinShortcutVerifier { static void verifyBundleShortcuts(JPackageCommand cmd) { cmd.verifyIsOfType(PackageType.WIN_MSI); @@ -51,7 +54,7 @@ static void verifyBundleShortcuts(JPackageCommand cmd) { return; } - var actualShortcuts = WindowsHelper.getMsiShortcuts(cmd).stream().collect(Collectors.groupingBy(shortcut -> { + var actualShortcuts = WindowsHelper.getMsiShortcuts(cmd).stream().collect(groupingBy(shortcut -> { return PathUtils.replaceSuffix(shortcut.target().getFileName(), "").toString(); })); @@ -73,10 +76,10 @@ static void verifyBundleShortcuts(JPackageCommand cmd) { var expectedLauncherShortcuts = sorter.apply(expectedShortcuts.get(name)); TKit.assertEquals(expectedLauncherShortcuts.size(), actualLauncherShortcuts.size(), - String.format("Check the number of shortcuts of [%s] launcher", name)); + String.format("Check the number of shortcuts of launcher [%s]", name)); for (int i = 0; i != expectedLauncherShortcuts.size(); i++) { - TKit.trace(String.format("Verify shortcut #%d of [%s] launcher", i + 1, name)); + TKit.trace(String.format("Verify shortcut #%d of launcher [%s]", i + 1, name)); actualLauncherShortcuts.get(i).assertEquals(expectedLauncherShortcuts.get(i)); TKit.trace("Done"); } @@ -96,6 +99,14 @@ static void verifyDeployedShortcuts(JPackageCommand cmd, boolean installed) { verifyDeployedShortcutsInternal(copyCmd, false); } + public static Collection getInvokeShortcutSpecs(JPackageCommand cmd) { + return expectShortcuts(cmd).entrySet().stream().map(e -> { + return e.getValue().stream().map(shortcut -> { + return convert(cmd, e.getKey(), shortcut); + }); + }).flatMap(x -> x).toList(); + } + private static void verifyDeployedShortcutsInternal(JPackageCommand cmd, boolean installed) { var expectedShortcuts = expectShortcuts(cmd).values().stream().flatMap(Collection::stream).toList(); @@ -184,43 +195,21 @@ private static Collection expectLauncherShortcuts(JPackageCommand cmd, Objects.requireNonNull(cmd); Objects.requireNonNull(predefinedAppImage); - List shortcuts = new ArrayList<>(); + final List shortcuts = new ArrayList<>(); - var name = Optional.ofNullable(launcherName).orElseGet(cmd::name); + final boolean isWinMenu = WIN_START_MENU_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).isPresent(); + final boolean isDesktop = WIN_DESKTOP_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).isPresent(); - boolean isWinMenu; - boolean isDesktop; - if (name.equals(cmd.name())) { - isWinMenu = cmd.hasArgument("--win-menu"); - isDesktop = cmd.hasArgument("--win-shortcut"); - } else { - var props = predefinedAppImage.map(v -> { - return v.launchers().get(name); - }).map(appImageFileLauncherProps -> { - Map convProps = new HashMap<>(); - for (var e : Map.of("menu", "win-menu", "shortcut", "win-shortcut").entrySet()) { - Optional.ofNullable(appImageFileLauncherProps.get(e.getKey())).ifPresent(v -> { - convProps.put(e.getValue(), v); - }); - } - return new AdditionalLauncher.PropertyFile(convProps); - }).orElseGet(() -> { - return AdditionalLauncher.getAdditionalLauncherProperties(cmd, launcherName); - }); - isWinMenu = props.findBooleanProperty("win-menu").orElseGet(() -> cmd.hasArgument("--win-menu")); - isDesktop = props.findBooleanProperty("win-shortcut").orElseGet(() -> cmd.hasArgument("--win-shortcut")); - } + final var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); - var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); - - SpecialFolder installRoot; + final SpecialFolder installRoot; if (isUserLocalInstall) { installRoot = SpecialFolder.LOCAL_APPLICATION_DATA; } else { installRoot = SpecialFolder.PROGRAM_FILES; } - var workDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd)); + final var workDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd)); if (isWinMenu) { ShortcutType type; @@ -272,6 +261,25 @@ private static Map> expectShortcuts(JPackageCommand return expectedShortcuts; } + private static InvokeShortcutSpec convert(JPackageCommand cmd, String launcherName, Shortcut shortcut) { + LauncherShortcut launcherShortcut; + if (Stream.of(ShortcutType.COMMON_START_MENU, ShortcutType.USER_START_MENU).anyMatch(type -> { + return shortcut.path().startsWith(Path.of(type.rootFolder().getMsiPropertyName())); + })) { + launcherShortcut = WIN_START_MENU_SHORTCUT; + } else { + launcherShortcut = WIN_DESKTOP_SHORTCUT; + } + + var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); + return new InvokeShortcutSpec.Stub( + launcherName, + launcherShortcut, + resolvePath(shortcut.workDir(), !isUserLocalInstall), + List.of("cmd", "/c", "start", "/wait", PathUtils.addSuffix(resolvePath(shortcut.path(), !isUserLocalInstall), ".lnk").toString())); + } + + private static final Comparator SHORTCUT_COMPARATOR = Comparator.comparing(Shortcut::target) .thenComparing(Comparator.comparing(Shortcut::path)) .thenComparing(Comparator.comparing(Shortcut::workDir)); From 1829a15da593ff2aa442b077c1ff263d39fda016 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 30 Jul 2025 16:35:03 -0400 Subject: [PATCH 18/83] AddLShortcutTest: add testStartupDirectory() tests to exercise combinations of startup directory in the main launcher and in the additional launcher; add testInvokeShortcuts() to invoke launchers through shortcuts and verify work directory; cover shortcuts in the predefined app image --- .../jpackage/share/AddLShortcutTest.java | 316 +++++++++++++++++- 1 file changed, 312 insertions(+), 4 deletions(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index 6430a55d784af..fbcd64c1cf674 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -21,13 +21,31 @@ * questions. */ -import java.nio.file.Path; +import java.io.IOException; import java.lang.invoke.MethodHandles; -import jdk.jpackage.test.PackageTest; -import jdk.jpackage.test.FileAssociations; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import jdk.internal.util.OperatingSystem; import jdk.jpackage.test.AdditionalLauncher; -import jdk.jpackage.test.TKit; +import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.Executor; +import jdk.jpackage.test.FileAssociations; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.LauncherShortcut; +import jdk.jpackage.test.LauncherShortcut.InvokeShortcutSpec; +import jdk.jpackage.test.LauncherShortcut.StartupDirectory; +import jdk.jpackage.test.LauncherVerifier.Action; +import jdk.jpackage.test.LinuxHelper; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.RunnablePackageTest; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.WinShortcutVerifier; /** * Test --add-launcher parameter with shortcuts (platform permitting). @@ -44,9 +62,23 @@ * @key jpackagePlatformPackage * @library /test/jdk/tools/jpackage/helpers * @build jdk.jpackage.test.* + * @requires (jpackage.test.SQETest != null) * @compile -Xlint:all -Werror AddLShortcutTest.java * @run main/othervm/timeout=540 -Xmx512m * jdk.jpackage.test.Main + * --jpt-run=AddLShortcutTest.test + */ + +/* + * @test + * @summary jpackage with --add-launcher + * @key jpackagePlatformPackage + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.* + * @requires (jpackage.test.SQETest == null) + * @compile -Xlint:all -Werror AddLShortcutTest.java + * @run main/othervm/timeout=1080 -Xmx512m + * jdk.jpackage.test.Main * --jpt-run=AddLShortcutTest */ @@ -107,6 +139,282 @@ public void test() { packageTest.run(); } + @Test(ifNotOS = OperatingSystem.MACOS) + @ParameterSupplier(ifOS = OperatingSystem.LINUX, value = "testShortcutStartupDirectoryLinux") + @ParameterSupplier(ifOS = OperatingSystem.WINDOWS, value = "testShortcutStartupDirectoryWindows") + public void testStartupDirectory(LauncherShortcutStartupDirectoryConfig... cfgs) { + + var test = new PackageTest().addInitializer(cmd -> { + cmd.setArgumentValue("--name", "AddLShortcutDirTest"); + }).addInitializer(JPackageCommand::setFakeRuntime).addHelloAppInitializer(null); + + test.addInitializer(cfgs[0]::applyToMainLauncher); + for (var i = 1; i != cfgs.length; ++i) { + var al = new AdditionalLauncher("launcher-" + i); + cfgs[i].applyToAdditionalLauncher(al); + al.withoutVerifyActions(Action.EXECUTE_LAUNCHER).applyTo(test); + } + + test.run(RunnablePackageTest.Action.CREATE_AND_UNPACK); + } + + @Test(ifNotOS = OperatingSystem.MACOS) + @ParameterSupplier(ifOS = OperatingSystem.LINUX, value = "testShortcutStartupDirectoryLinux") + @ParameterSupplier(ifOS = OperatingSystem.WINDOWS, value = "testShortcutStartupDirectoryWindows") + public void testStartupDirectory2(LauncherShortcutStartupDirectoryConfig... cfgs) { + + // + // Launcher shortcuts in the predefined app image. + // + // Shortcut configuration for the main launcher is not supported when building an app image. + // However, shortcut configuration for additional launchers is supported. + // The test configures shortcuts for additional launchers in the app image building jpackage command + // and applies shortcut configuration to the main launcher in the native packaging jpackage command. + // + + Path[] predefinedAppImage = new Path[1]; + + new PackageTest().addRunOnceInitializer(() -> { + var cmd = JPackageCommand.helloAppImage() + .setArgumentValue("--name", "foo") + .setFakeRuntime(); + + for (var i = 1; i != cfgs.length; ++i) { + var al = new AdditionalLauncher("launcher-" + i); + cfgs[i].applyToAdditionalLauncher(al); + al.withoutVerifyActions(Action.EXECUTE_LAUNCHER).applyTo(cmd); + } + + cmd.execute(); + + predefinedAppImage[0] = cmd.outputBundle(); + }).addInitializer(cmd -> { + cmd.removeArgumentWithValue("--input"); + cmd.setArgumentValue("--name", "AddLShortcutDir2Test"); + cmd.addArguments("--app-image", predefinedAppImage[0]); + cfgs[0].applyToMainLauncher(cmd); + }).run(RunnablePackageTest.Action.CREATE_AND_UNPACK); + } + + public static Collection testShortcutStartupDirectoryLinux() { + return testShortcutStartupDirectory(LauncherShortcut.LINUX_SHORTCUT); + } + + public static Collection testShortcutStartupDirectoryWindows() { + return testShortcutStartupDirectory(LauncherShortcut.WIN_DESKTOP_SHORTCUT, LauncherShortcut.WIN_START_MENU_SHORTCUT); + } + + @Test(ifNotOS = OperatingSystem.MACOS) + public void testInvokeShortcuts() { + + var testApp = TKit.TEST_SRC_ROOT.resolve("apps/PrintEnv.java"); + + var name = "AddLShortcutRunTest"; + + var test = new PackageTest().addInitializer(cmd -> { + cmd.setArgumentValue("--name", name); + }).addInitializer(cmd -> { + cmd.addArguments("--arguments", "--print-workdir"); + }).addInitializer(JPackageCommand::ignoreFakeRuntime).addHelloAppInitializer(testApp + "*Hello"); + + var shortcutStartupDirectoryVerifier = new ShortcutStartupDirectoryVerifier(name, "a"); + + shortcutStartupDirectoryVerifier.applyTo(test, StartupDirectory.DEFAULT); + + test.addInstallVerifier(cmd -> { + if (!cmd.isPackageUnpacked("Not invoking launcher shortcuts")) { + Collection invokeShortcutSpecs; + if (TKit.isLinux()) { + invokeShortcutSpecs = LinuxHelper.getInvokeShortcutSpecs(cmd); + } else if (TKit.isWindows()) { + invokeShortcutSpecs = WinShortcutVerifier.getInvokeShortcutSpecs(cmd); + } else { + throw new UnsupportedOperationException(); + } + shortcutStartupDirectoryVerifier.verify(invokeShortcutSpecs); + } + }); + + test.run(); + } + + + private record ShortcutStartupDirectoryVerifier(String packageName, String launcherName) { + ShortcutStartupDirectoryVerifier { + Objects.requireNonNull(packageName); + Objects.requireNonNull(launcherName); + } + + void applyTo(PackageTest test, StartupDirectory startupDirectory) { + var al = new AdditionalLauncher(launcherName); + al.setShortcut(shortcut(), Objects.requireNonNull(startupDirectory)); + al.addJavaOptions(String.format("-Djpackage.test.appOutput=${%s}/%s", + outputDirVarName(), expectedOutputFilename())); + al.withoutVerifyActions(Action.EXECUTE_LAUNCHER).applyTo(test); + } + + void verify(Collection invokeShortcutSpecs) throws IOException { + + TKit.trace(String.format("Verify shortcut [%s]", launcherName)); + + var expectedOutputFile = Path.of(System.getenv(outputDirVarName())).resolve(expectedOutputFilename()); + + TKit.deleteIfExists(expectedOutputFile); + + var invokeShortcutSpec = invokeShortcutSpecs.stream().filter(v -> { + return launcherName.equals(v.launcherName()); + }).findAny().orElseThrow(); + + Executor.of(invokeShortcutSpec.commandLine()).execute(); + + TKit.assertFileExists(expectedOutputFile); + var actualStr = Files.readAllLines(expectedOutputFile).getFirst(); + + var outputPrefix = "$CD="; + + TKit.assertTrue(actualStr.startsWith(outputPrefix), "Check output starts with '" + outputPrefix+ "' string"); + + invokeShortcutSpec.expectedWorkDirectory().ifPresent(expectedWorkDirectory -> { + TKit.assertEquals( + expectedWorkDirectory, + Path.of(actualStr.substring(outputPrefix.length())), + String.format("Check work directory of %s of launcher [%s]", + invokeShortcutSpec.shortcut().propertyName(), + invokeShortcutSpec.launcherName())); + }); + } + + private String expectedOutputFilename() { + return String.format("%s-%s.out", packageName, launcherName); + } + + private String outputDirVarName() { + if (TKit.isLinux()) { + return "HOME"; + } else if (TKit.isWindows()) { + return "LOCALAPPDATA"; + } else { + throw new UnsupportedOperationException(); + } + } + + private LauncherShortcut shortcut() { + if (TKit.isLinux()) { + return LauncherShortcut.LINUX_SHORTCUT; + } else if (TKit.isWindows()) { + return LauncherShortcut.WIN_DESKTOP_SHORTCUT; + } else { + throw new UnsupportedOperationException(); + } + } + } + + + private static Collection testShortcutStartupDirectory(LauncherShortcut... shortcuts) { + List> items = new ArrayList<>(); + + for (var shortcut : shortcuts) { + List mainLauncherVariants = new ArrayList<>(); + for (var valueSetter : StartupDirectoryValueSetter.MAIN_LAUNCHER_VALUES) { + mainLauncherVariants.add(new LauncherShortcutStartupDirectoryConfig(shortcut, valueSetter)); + } + mainLauncherVariants.stream().map(List::of).forEach(items::add); + mainLauncherVariants.add(new LauncherShortcutStartupDirectoryConfig(shortcut)); + + List addLauncherVariants = new ArrayList<>(); + addLauncherVariants.add(new LauncherShortcutStartupDirectoryConfig(shortcut)); + for (var valueSetter : StartupDirectoryValueSetter.ADD_LAUNCHER_VALUES) { + addLauncherVariants.add(new LauncherShortcutStartupDirectoryConfig(shortcut, valueSetter)); + } + + for (var mainLauncherVariant : mainLauncherVariants) { + for (var addLauncherVariant : addLauncherVariants) { + if (mainLauncherVariant.valueSetter().isPresent() || addLauncherVariant.valueSetter().isPresent()) { + items.add(List.of(mainLauncherVariant, addLauncherVariant)); + } + } + } + } + + return items.stream().map(List::toArray).toList(); + } + + + private enum StartupDirectoryValueSetter { + DEFAULT(""), + TRUE("true"), + FALSE("false"), + ; + + StartupDirectoryValueSetter(String value) { + this.value = Objects.requireNonNull(value); + } + + void applyToMainLauncher(LauncherShortcut shortcut, JPackageCommand cmd) { + switch (this) { + case TRUE, FALSE -> { + throw new UnsupportedOperationException(); + } + case DEFAULT -> { + cmd.addArgument(shortcut.optionName()); + } + default -> { + cmd.addArgument(shortcut.optionName() + "=" + value); + } + } + } + + void applyToAdditionalLauncher(LauncherShortcut shortcut, AdditionalLauncher addLauncher) { + addLauncher.setProperty(shortcut.propertyName(), value); + } + + private final String value; + + static final List MAIN_LAUNCHER_VALUES = List.of( + StartupDirectoryValueSetter.DEFAULT + ); + + static final List ADD_LAUNCHER_VALUES = List.of( + StartupDirectoryValueSetter.TRUE, + StartupDirectoryValueSetter.FALSE + ); + } + + + record LauncherShortcutStartupDirectoryConfig(LauncherShortcut shortcut, Optional valueSetter) { + + LauncherShortcutStartupDirectoryConfig { + Objects.requireNonNull(shortcut); + Objects.requireNonNull(valueSetter); + } + + LauncherShortcutStartupDirectoryConfig(LauncherShortcut shortcut, StartupDirectoryValueSetter valueSetter) { + this(shortcut, Optional.of(valueSetter)); + } + + LauncherShortcutStartupDirectoryConfig(LauncherShortcut shortcut) { + this(shortcut, Optional.empty()); + } + + void applyToMainLauncher(JPackageCommand target) { + valueSetter.ifPresent(valueSetter -> { + valueSetter.applyToMainLauncher(shortcut, target); + }); + } + + void applyToAdditionalLauncher(AdditionalLauncher target) { + valueSetter.ifPresent(valueSetter -> { + valueSetter.applyToAdditionalLauncher(shortcut, target); + }); + } + + @Override + public String toString() { + return shortcut + "=" + valueSetter.map(Object::toString).orElse(""); + } + } + + private static final Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of( "resources", "icon" + TKit.ICON_SUFFIX)); } From 3ba7bf73aeddb595c5cac0426828d5954b720c76 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 30 Jul 2025 23:21:14 -0400 Subject: [PATCH 19/83] WinShortcutVerifier: fix bad merge --- .../helpers/jdk/jpackage/test/WinShortcutVerifier.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index a765413233cd1..acd11a116db86 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -253,14 +253,6 @@ private static Map> expectShortcuts(JPackageCommand return expectedShortcuts; } - addShortcuts.accept(cmd.name()); - predefinedAppImage.map(v -> { - return (Collection)v.addLaunchers().keySet(); - }).orElseGet(cmd::addLauncherNames).forEach(addShortcuts); - - return expectedShortcuts; - } - private static InvokeShortcutSpec convert(JPackageCommand cmd, String launcherName, Shortcut shortcut) { LauncherShortcut launcherShortcut; if (Stream.of(ShortcutType.COMMON_START_MENU, ShortcutType.USER_START_MENU).anyMatch(type -> { @@ -275,7 +267,7 @@ private static InvokeShortcutSpec convert(JPackageCommand cmd, String launcherNa return new InvokeShortcutSpec.Stub( launcherName, launcherShortcut, - resolvePath(shortcut.workDir(), !isUserLocalInstall), + Optional.of(resolvePath(shortcut.workDir(), !isUserLocalInstall)), List.of("cmd", "/c", "start", "/wait", PathUtils.addSuffix(resolvePath(shortcut.path(), !isUserLocalInstall), ".lnk").toString())); } From e7822ea4e18106fc1882e988b1de3cf4c4ba7b3d Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 14:01:46 -0400 Subject: [PATCH 20/83] AddLShortcutTest: make it work on Linux --- test/jdk/tools/jpackage/share/AddLShortcutTest.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index fbcd64c1cf674..51bfdf7af2bed 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import jdk.internal.util.OperatingSystem; @@ -265,7 +266,15 @@ void verify(Collection invokeShortcutSpecs) throws return launcherName.equals(v.launcherName()); }).findAny().orElseThrow(); - Executor.of(invokeShortcutSpec.commandLine()).execute(); + invokeShortcutSpec.execute(); + + // On Linux, "gtk-launch" is used to launch a .desktop file. It is async and there is no + // wait to make it wait for exit of a process it triggers. + Executor.tryRunMultipleTimes(() -> { + if (!Files.exists(expectedOutputFile)) { + throw new NoSuchElementException(String.format("[%s] is not avaialble", expectedOutputFile)); + } + }, 3 /* Number of attempts */, 3 /* Seconds between attempts */); TKit.assertFileExists(expectedOutputFile); var actualStr = Files.readAllLines(expectedOutputFile).getFirst(); From b75a974aed8152d67239b561ff795ac9fa650146 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 14:02:30 -0400 Subject: [PATCH 21/83] LinuxHelper: Use `gtk-launch` command to launch .desktop files --- .../tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 32c291173adbe..054bf89bb0ad6 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -361,7 +361,7 @@ public static Collection getInvokeShortcutSpecs(JP launcherNameFromDesktopFile(cmd, predefinedAppImage, desktopFile), LauncherShortcut.LINUX_SHORTCUT, new DesktopFile(systemDesktopFile, false).findQuotedValue("Path").map(Path::of), - List.of("xdg-open", systemDesktopFile.toString())); + List.of("gtk-launch", PathUtils.replaceSuffix(systemDesktopFile.getFileName(), "").toString())); }).toList(); } From 8d8e7d8f2a69e6cdba42f0ee0502fdfda423e1ed Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 14:03:18 -0400 Subject: [PATCH 22/83] LauncherShortcut: bugfix --- .../jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java index 6fb5c6e14a894..82df7f17616fd 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java @@ -118,6 +118,10 @@ public interface InvokeShortcutSpec { Optional expectedWorkDirectory(); List commandLine(); + default Executor.Result execute() { + return HelloApp.configureAndExecute(0, Executor.of(commandLine()).dumpOutput()); + } + record Stub( String launcherName, LauncherShortcut shortcut, From eb1570a01d4f827fcad9a4ec595636d074486946 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 19:06:37 -0400 Subject: [PATCH 23/83] LauncherShortcut: make it work with the current variant of jpackage without JDK-8308349 mods --- .../helpers/jdk/jpackage/test/LauncherShortcut.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java index 82df7f17616fd..cf917fa3e4706 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java @@ -85,6 +85,7 @@ static Optional parse(String str) { LauncherShortcut(String propertyName) { this.propertyName = Objects.requireNonNull(propertyName); + this.predefinedAppImagePropertyName = propertyName.substring(propertyName.indexOf('-') + 1); } public String propertyName() { @@ -103,11 +104,14 @@ Optional expectShortcut(JPackageCommand cmd, Optional { + propertyName[0] = this.predefinedAppImagePropertyName; return new PropertyFile(appImage.addLaunchers().get(launcherName)); }).orElseGet(() -> { + propertyName[0] = this.propertyName; return getAdditionalLauncherProperties(cmd, launcherName); - })::findProperty); + })::findProperty, propertyName[0]); } } @@ -147,8 +151,8 @@ private Optional findMainLauncherShortcut(JPackageCommand cmd) } private Optional findAddLauncherShortcut(JPackageCommand cmd, - Function> addlauncherProperties) { - var explicit = addlauncherProperties.apply(propertyName()); + Function> addlauncherProperties, String propertyName) { + var explicit = addlauncherProperties.apply(propertyName); if (explicit.isPresent()) { return explicit.flatMap(StartupDirectory::parse); } else { @@ -157,4 +161,5 @@ private Optional findAddLauncherShortcut(JPackageCommand cmd, } private final String propertyName; + private final String predefinedAppImagePropertyName; } From 2e7e746430918c1236df6463ce1989999ef8b2b0 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 19:57:53 -0400 Subject: [PATCH 24/83] AdditionalLauncher: bugfix --- .../jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java index c2d2ad0f407a0..07c8e06856fb4 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java @@ -102,7 +102,7 @@ public AdditionalLauncher setShortcuts(boolean menu, boolean desktop) { setShortcut(LINUX_SHORTCUT, desktop); } else if (TKit.isWindows()) { setShortcut(WIN_DESKTOP_SHORTCUT, desktop); - setShortcut(WIN_START_MENU_SHORTCUT, desktop); + setShortcut(WIN_START_MENU_SHORTCUT, menu); } return this; } From 2d754ee809716d4facab23d39bf0f933b2e89164 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 23:10:13 -0400 Subject: [PATCH 25/83] Consistent log message format --- .../jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java index 55f7a1b93145f..ceda32eb8edad 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java @@ -282,7 +282,7 @@ private void verifyInstalled(JPackageCommand cmd, boolean installed) throws IOEx private void executeLauncher(JPackageCommand cmd) throws IOException { Path launcherPath = cmd.appLauncherPath(name); - if (!cmd.canRunLauncher(String.format("Not running %s launcher", launcherPath))) { + if (!cmd.canRunLauncher(String.format("Not running [%s] launcher", launcherPath))) { return; } From 67bea7d5e7ab8e5ca954591a20257542c040aafd Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 23:12:45 -0400 Subject: [PATCH 26/83] Bash script to clean jpackage test log files to reduce noise in diff-s --- test/jdk/tools/jpackage/clean_test_output.sh | 84 ++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/jdk/tools/jpackage/clean_test_output.sh diff --git a/test/jdk/tools/jpackage/clean_test_output.sh b/test/jdk/tools/jpackage/clean_test_output.sh new file mode 100644 index 0000000000000..ee61de8429258 --- /dev/null +++ b/test/jdk/tools/jpackage/clean_test_output.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# Copyright (c) 2025, 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. + +# +# Filters output produced by running jpackage test(s). +# + +set -eu +set -o pipefail + + +sed_inplace_option=-i +sed_version_string=$(sed --version 2>&1 | head -1 || true) +if [ "${sed_version_string#sed (GNU sed)}" != "$sed_version_string" ]; then + # GNU sed, the default + : +elif [ "${sed_version_string#sed: illegal option}" != "$sed_version_string" ]; then + # Macos sed + sed_inplace_option="-i ''" +else + echo 'WARNING: Unknown sed variant, assume it is GNU compatible' +fi + + +filterFile () { + local expressions=( + # Strip leading log message timestamp `[19:33:44.713] ` + -e 's/^\[[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\.[0-9]\{3\}\] //' + + # Strip log message timestamps `[19:33:44.713]` + -e 's/\[[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\.[0-9]\{3\}\]//g' + + # Convert variable part of R/O directory path timestamp `#2025-07-24T16:38:13.3589878Z` + -e 's/#[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\.[0-9]\{1,\}Z/#Z/' + + # Strip variable part of temporary directory name `jdk.jpackage5060841750457404688` + -e 's|\([\/]\)jdk\.jpackage[0-9]\{1,\}\b|\1jdk.jpackage|g' + + # Convert PID value `[PID: 131561]` + -e 's/\[PID: [0-9]\{1,\}\]/[PID: ]/' + + # Strip a warning message `Windows Defender may prevent jpackage from functioning` + -e '/Windows Defender may prevent jpackage from functioning/d' + + # Convert variable part of test output directory `out-6268` + -e 's|\bout-[0-9]\{1,\}\b|out-N|g' + + # Convert variable part of test summary `[ OK ] IconTest(AppImage, ResourceDirIcon, DefaultIcon).test; checks=39` + -e 's/^\(.*\bchecks=\)[0-9]\{1,\}\(\r\{0,1\}\)$/\1N\2/' + + # Convert variable part of ldd output `libdl.so.2 => /lib64/libdl.so.2 (0x00007fbf63c81000)` + -e 's/(0x[[:xdigit:]]\{1,\})$/(0xHEX)/' + + # Convert variable part of rpmbuild output `Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.CMO6a9` + -e 's|/rpm-tmp\...*$|/rpm-tmp.V|' + ) + + sed $sed_inplace_option "$1" "${expressions[@]}" +} + + +for f in "$@"; do + filterFile "$f" +done From b5da8074c8ca7dda987fc3f31794c7d83b3da0e4 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 23:17:08 -0400 Subject: [PATCH 27/83] Fix a typo --- test/jdk/tools/jpackage/share/AddLShortcutTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index 51bfdf7af2bed..38d505eed6f1e 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -269,7 +269,7 @@ void verify(Collection invokeShortcutSpecs) throws invokeShortcutSpec.execute(); // On Linux, "gtk-launch" is used to launch a .desktop file. It is async and there is no - // wait to make it wait for exit of a process it triggers. + // way to make it wait for exit of a process it triggers. Executor.tryRunMultipleTimes(() -> { if (!Files.exists(expectedOutputFile)) { throw new NoSuchElementException(String.format("[%s] is not avaialble", expectedOutputFile)); From a56e55a739f6a25a19b1bddbf0ba756b0544f0e3 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 11:29:38 -0400 Subject: [PATCH 28/83] LinuxHelper: allow empty lines in .desktop files --- .../tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 054bf89bb0ad6..6d4721d6023d9 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -39,6 +39,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -826,7 +827,7 @@ private static final class DesktopFile { TKit.assertEquals("[Desktop Entry]", lines.getFirst(), "Check file header"); } - var stream = lines.stream().skip(1); + var stream = lines.stream().skip(1).filter(Predicate.not(String::isEmpty)); if (verify) { stream = stream.peek(str -> { TKit.assertTextStream("=").predicate(String::contains).apply(List.of(str)); From 9a4ec0f47b1e5f4b9394acc0d6638d7d81dee7a4 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 11:58:39 -0400 Subject: [PATCH 29/83] WinShortcutVerifier: make it a better fit for JDK-8308349 --- .../jpackage/test/WinShortcutVerifier.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index acd11a116db86..cca904e017e62 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -41,6 +41,7 @@ import java.util.stream.Stream; import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.test.LauncherShortcut.InvokeShortcutSpec; +import jdk.jpackage.test.LauncherShortcut.StartupDirectory; import jdk.jpackage.test.MsiDatabase.Shortcut; import jdk.jpackage.test.WindowsHelper.SpecialFolder; @@ -197,8 +198,8 @@ private static Collection expectLauncherShortcuts(JPackageCommand cmd, final List shortcuts = new ArrayList<>(); - final boolean isWinMenu = WIN_START_MENU_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).isPresent(); - final boolean isDesktop = WIN_DESKTOP_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).isPresent(); + final var winMenu = WIN_START_MENU_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName); + final var desktop = WIN_DESKTOP_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName); final var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); @@ -209,26 +210,30 @@ private static Collection expectLauncherShortcuts(JPackageCommand cmd, installRoot = SpecialFolder.PROGRAM_FILES; } - final var workDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd)); + final var installDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd)); - if (isWinMenu) { + final Function workDir = startupDirectory -> { + return installDir; + }; + + if (winMenu.isPresent()) { ShortcutType type; if (isUserLocalInstall) { type = ShortcutType.USER_START_MENU; } else { type = ShortcutType.COMMON_START_MENU; } - shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, workDir, type)); + shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, winMenu.map(workDir).orElseThrow(), type)); } - if (isDesktop) { + if (desktop.isPresent()) { ShortcutType type; if (isUserLocalInstall) { type = ShortcutType.USER_DESKTOP; } else { type = ShortcutType.COMMON_DESKTOP; } - shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, workDir, type)); + shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, desktop.map(workDir).orElseThrow(), type)); } return shortcuts; From 36410b75d4cd5c9f1aacf7f7d1be266b470cc1a9 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 15:13:07 -0400 Subject: [PATCH 30/83] LinuxHelper: bugfix --- .../helpers/jdk/jpackage/test/LinuxHelper.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 6d4721d6023d9..16b373f41ac85 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -507,21 +507,29 @@ private static void verifyDesktopFile(JPackageCommand cmd, Optional { switch (shortcutWorkDirType) { case DEFAULT -> { return (Path)null; } + case APP_DIR -> { + return cmd.pathToPackageFile(appLayout.appDirectory()); + } + case INSTALL_DIR -> { + return cmd.pathToPackageFile(appLayout.launchersDirectory()); + } default -> { throw new AssertionError(); } } - }).ifPresentOrElse(shortcutWorkDir -> { - var actualShortcutWorkDir = data.findQuotedValue("Path"); + }).map(Path::toString).ifPresentOrElse(shortcutWorkDir -> { + var actualShortcutWorkDir = data.find("Path"); TKit.assertTrue(actualShortcutWorkDir.isPresent(), "Check [Path] key exists"); TKit.assertEquals(actualShortcutWorkDir.get(), shortcutWorkDir, "Check the value of [Path] key"); }, () -> { - TKit.assertTrue(data.findQuotedValue("Path").isEmpty(), "Check there is no [Path] key"); + TKit.assertTrue(data.find("Path").isEmpty(), "Check there is no [Path] key"); }); for (var e : List.>, Function>>of( @@ -531,7 +539,7 @@ private static void verifyDesktopFile(JPackageCommand cmd, Optional Date: Fri, 1 Aug 2025 16:12:25 -0400 Subject: [PATCH 31/83] AddLShortcutTest: modify --- .../jpackage/share/AddLShortcutTest.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index 38d505eed6f1e..5377a1ea869e5 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -34,6 +34,7 @@ import jdk.internal.util.OperatingSystem; import jdk.jpackage.test.AdditionalLauncher; import jdk.jpackage.test.Annotations.ParameterSupplier; +import jdk.jpackage.test.Annotations.Parameter; import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.Executor; import jdk.jpackage.test.FileAssociations; @@ -206,7 +207,11 @@ public static Collection testShortcutStartupDirectoryWindows() { } @Test(ifNotOS = OperatingSystem.MACOS) - public void testInvokeShortcuts() { + @Parameter(value = "DEFAULT") + @Parameter(value = "APP_DIR") + // On Windows, `DEFAULT` and `INSTALL_DIR` are equivalent, run only one of them. + @Parameter(value = "INSTALL_DIR", ifNotOS = OperatingSystem.WINDOWS) + public void testInvokeShortcuts(StartupDirectory startupDirectory) { var testApp = TKit.TEST_SRC_ROOT.resolve("apps/PrintEnv.java"); @@ -220,7 +225,7 @@ public void testInvokeShortcuts() { var shortcutStartupDirectoryVerifier = new ShortcutStartupDirectoryVerifier(name, "a"); - shortcutStartupDirectoryVerifier.applyTo(test, StartupDirectory.DEFAULT); + shortcutStartupDirectoryVerifier.applyTo(test, startupDirectory); test.addInstallVerifier(cmd -> { if (!cmd.isPackageUnpacked("Not invoking launcher shortcuts")) { @@ -353,6 +358,8 @@ private enum StartupDirectoryValueSetter { DEFAULT(""), TRUE("true"), FALSE("false"), + INSTALL_DIR(StartupDirectory.INSTALL_DIR.asStringValue()), + APP_DIR(StartupDirectory.APP_DIR.asStringValue()) ; StartupDirectoryValueSetter(String value) { @@ -368,7 +375,7 @@ void applyToMainLauncher(LauncherShortcut shortcut, JPackageCommand cmd) { cmd.addArgument(shortcut.optionName()); } default -> { - cmd.addArgument(shortcut.optionName() + "=" + value); + cmd.addArguments(shortcut.optionName(), value); } } } @@ -380,12 +387,16 @@ void applyToAdditionalLauncher(LauncherShortcut shortcut, AdditionalLauncher add private final String value; static final List MAIN_LAUNCHER_VALUES = List.of( - StartupDirectoryValueSetter.DEFAULT + StartupDirectoryValueSetter.DEFAULT, + StartupDirectoryValueSetter.INSTALL_DIR, + StartupDirectoryValueSetter.APP_DIR ); static final List ADD_LAUNCHER_VALUES = List.of( StartupDirectoryValueSetter.TRUE, - StartupDirectoryValueSetter.FALSE + StartupDirectoryValueSetter.FALSE, + StartupDirectoryValueSetter.INSTALL_DIR, + StartupDirectoryValueSetter.APP_DIR ); } From 98b916eb6f7067c4a7a1e2e69043391b6c505b88 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 16:34:16 -0400 Subject: [PATCH 32/83] LinuxHelper: bugfix --- .../jpackage/helpers/jdk/jpackage/test/LinuxHelper.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 16b373f41ac85..b99d2bdeceb39 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -514,12 +514,6 @@ private static void verifyDesktopFile(JPackageCommand cmd, Optional { return (Path)null; } - case APP_DIR -> { - return cmd.pathToPackageFile(appLayout.appDirectory()); - } - case INSTALL_DIR -> { - return cmd.pathToPackageFile(appLayout.launchersDirectory()); - } default -> { throw new AssertionError(); } From c8f034f8a17020483dc10cab2d08df44211483ca Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 16:36:13 -0400 Subject: [PATCH 33/83] AddLShortcutTest: make it a better fit for JDK-8308349 --- test/jdk/tools/jpackage/share/AddLShortcutTest.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index 5377a1ea869e5..aef78ea267f61 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -208,9 +208,6 @@ public static Collection testShortcutStartupDirectoryWindows() { @Test(ifNotOS = OperatingSystem.MACOS) @Parameter(value = "DEFAULT") - @Parameter(value = "APP_DIR") - // On Windows, `DEFAULT` and `INSTALL_DIR` are equivalent, run only one of them. - @Parameter(value = "INSTALL_DIR", ifNotOS = OperatingSystem.WINDOWS) public void testInvokeShortcuts(StartupDirectory startupDirectory) { var testApp = TKit.TEST_SRC_ROOT.resolve("apps/PrintEnv.java"); @@ -358,8 +355,6 @@ private enum StartupDirectoryValueSetter { DEFAULT(""), TRUE("true"), FALSE("false"), - INSTALL_DIR(StartupDirectory.INSTALL_DIR.asStringValue()), - APP_DIR(StartupDirectory.APP_DIR.asStringValue()) ; StartupDirectoryValueSetter(String value) { @@ -387,16 +382,12 @@ void applyToAdditionalLauncher(LauncherShortcut shortcut, AdditionalLauncher add private final String value; static final List MAIN_LAUNCHER_VALUES = List.of( - StartupDirectoryValueSetter.DEFAULT, - StartupDirectoryValueSetter.INSTALL_DIR, - StartupDirectoryValueSetter.APP_DIR + StartupDirectoryValueSetter.DEFAULT ); static final List ADD_LAUNCHER_VALUES = List.of( StartupDirectoryValueSetter.TRUE, - StartupDirectoryValueSetter.FALSE, - StartupDirectoryValueSetter.INSTALL_DIR, - StartupDirectoryValueSetter.APP_DIR + StartupDirectoryValueSetter.FALSE ); } From c280a9f23591ce9d207c9d755424419e83d3f874 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 17:25:46 -0400 Subject: [PATCH 34/83] clean_test_output.sh: better --- test/jdk/tools/jpackage/clean_test_output.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/jdk/tools/jpackage/clean_test_output.sh b/test/jdk/tools/jpackage/clean_test_output.sh index ee61de8429258..e472d780dede7 100644 --- a/test/jdk/tools/jpackage/clean_test_output.sh +++ b/test/jdk/tools/jpackage/clean_test_output.sh @@ -73,6 +73,9 @@ filterFile () { # Convert variable part of rpmbuild output `Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.CMO6a9` -e 's|/rpm-tmp\...*$|/rpm-tmp.V|' + + # Convert variable part of stack trace entry `at jdk.jpackage.test.JPackageCommand.execute(JPackageCommand.java:863)` + -e 's/^\(.*\b\.java:\)[0-9]\{1,\}\()\r\{0,1\}\)$/\1N\2/' ) sed $sed_inplace_option "$1" "${expressions[@]}" From 6f428111a1f4791836bfef96715f006af46830a1 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 16:18:07 -0400 Subject: [PATCH 35/83] JPackageCommand: remove path to the unpacked directory from the argument list as it interferes with extracting arguments with optional values. --- .../helpers/jdk/jpackage/test/JPackageCommand.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 4da226fe338fd..77e9b11dd89b6 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -90,6 +90,7 @@ private JPackageCommand(JPackageCommand cmd, boolean immutable) { outputValidators = cmd.outputValidators; executeInDirectory = cmd.executeInDirectory; winMsiLogFile = cmd.winMsiLogFile; + unpackedPackageDirectory = cmd.unpackedPackageDirectory; } JPackageCommand createImmutableCopy() { @@ -484,7 +485,7 @@ public Path pathToPackageFile(Path path) { Path unpackedPackageDirectory() { verifyIsOfType(PackageType.NATIVE); - return getArgumentValue(UNPACKED_PATH_ARGNAME, () -> null, Path::of); + return unpackedPackageDirectory; } /** @@ -662,7 +663,7 @@ public boolean isPackageUnpacked(String msg) { } public boolean isPackageUnpacked() { - return hasArgument(UNPACKED_PATH_ARGNAME); + return unpackedPackageDirectory != null; } public static void useToolProviderByDefault(ToolProvider jpackageToolProvider) { @@ -1259,11 +1260,7 @@ private void assertFileInAppImage(Path filename, Path expectedPath) { JPackageCommand setUnpackedPackageLocation(Path path) { verifyMutable(); verifyIsOfType(PackageType.NATIVE); - if (path != null) { - setArgumentValue(UNPACKED_PATH_ARGNAME, path); - } else { - removeArgumentWithValue(UNPACKED_PATH_ARGNAME); - } + unpackedPackageDirectory = path; return this; } @@ -1475,6 +1472,7 @@ public void run() { private final Actions verifyActions; private Path executeInDirectory; private Path winMsiLogFile; + private Path unpackedPackageDirectory; private Set readOnlyPathAsserts = Set.of(ReadOnlyPathAssert.values()); private Set appLayoutAsserts = Set.of(AppLayoutAssert.values()); private List>> outputValidators = new ArrayList<>(); @@ -1502,8 +1500,6 @@ public void run() { return null; }).get(); - private static final String UNPACKED_PATH_ARGNAME = "jpt-unpacked-folder"; - // [HH:mm:ss.SSS] private static final Pattern TIMESTAMP_REGEXP = Pattern.compile( "^\\[\\d\\d:\\d\\d:\\d\\d.\\d\\d\\d\\] "); From b4fcbdba05d0edfbcffda2153c110977fa585a80 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 21:42:52 -0400 Subject: [PATCH 36/83] JPackageCommand: verify names of additional launcher are precisely recorded in .jpackage.xml file --- .../helpers/jdk/jpackage/test/JPackageCommand.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 77e9b11dd89b6..8b0017d1e3aa6 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -1161,12 +1161,12 @@ private void assertAppImageFile() { } else { assertFileInAppImage(lookupPath); - if (TKit.isOSX()) { - final Path rootDir = isImagePackageType() ? outputBundle() : - pathToUnpackedPackageFile(appInstallationDirectory()); + final Path rootDir = isImagePackageType() ? outputBundle() : + pathToUnpackedPackageFile(appInstallationDirectory()); - AppImageFile aif = AppImageFile.load(rootDir); + final AppImageFile aif = AppImageFile.load(rootDir); + if (TKit.isOSX()) { boolean expectedValue = MacHelper.appImageSigned(this); boolean actualValue = aif.macSigned(); TKit.assertEquals(expectedValue, actualValue, @@ -1177,6 +1177,11 @@ private void assertAppImageFile() { TKit.assertEquals(expectedValue, actualValue, "Check for unexpected value of property in app image file"); } + + TKit.assertStringListEquals( + addLauncherNames().stream().sorted().toList(), + aif.addLaunchers().keySet().stream().sorted().toList(), + "Check additional launcher names"); } } From 975a493f5b14d73dae0a85c0a7fe3938b191028c Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 21:40:42 -0400 Subject: [PATCH 37/83] LauncherShortcut: add appImageFilePropertyName() --- .../helpers/jdk/jpackage/test/LauncherShortcut.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java index cf917fa3e4706..5e86f975870b1 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java @@ -85,13 +85,16 @@ static Optional parse(String str) { LauncherShortcut(String propertyName) { this.propertyName = Objects.requireNonNull(propertyName); - this.predefinedAppImagePropertyName = propertyName.substring(propertyName.indexOf('-') + 1); } public String propertyName() { return propertyName; } + public String appImageFilePropertyName() { + return propertyName.substring(propertyName.indexOf('-') + 1); + } + public String optionName() { return "--" + propertyName; } @@ -106,7 +109,7 @@ Optional expectShortcut(JPackageCommand cmd, Optional { - propertyName[0] = this.predefinedAppImagePropertyName; + propertyName[0] = appImageFilePropertyName(); return new PropertyFile(appImage.addLaunchers().get(launcherName)); }).orElseGet(() -> { propertyName[0] = this.propertyName; @@ -161,5 +164,4 @@ private Optional findAddLauncherShortcut(JPackageCommand cmd, } private final String propertyName; - private final String predefinedAppImagePropertyName; } From 140306691487109d19bdc2fc41aa53c833709bc0 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 4 Aug 2025 00:45:23 -0400 Subject: [PATCH 38/83] Use java.time.Duration and java.time.Instant in TKit.waitForFileCreated(). Make it public. --- .../jdk/jpackage/test/PackageTest.java | 9 ++---- .../helpers/jdk/jpackage/test/TKit.java | 29 ++++++++++++------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index affc9ab22bdbd..17be2b9f196e2 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -36,6 +36,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -296,13 +297,9 @@ PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa) { Files.deleteIfExists(appOutput); List expectedArgs = testRun.openFiles(testFiles); - TKit.waitForFileCreated(appOutput, 7); + TKit.waitForFileCreated(appOutput, Duration.ofSeconds(7), Duration.ofSeconds(3)); - // Wait a little bit after file has been created to - // make sure there are no pending writes into it. - Thread.sleep(3000); - HelloApp.verifyOutputFile(appOutput, expectedArgs, - Collections.emptyMap()); + HelloApp.verifyOutputFile(appOutput, expectedArgs, Map.of()); }); if (isOfType(cmd, WINDOWS)) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java index 2508db00295ce..d55da7d924a43 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -43,6 +43,8 @@ import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -597,8 +599,14 @@ public static Path createRelativePathCopy(final Path file) { return file; } - static void waitForFileCreated(Path fileToWaitFor, - long timeoutSeconds) throws IOException { + public static void waitForFileCreated(Path fileToWaitFor, + Duration timeout, Duration afterCreatedTimeout) throws IOException { + waitForFileCreated(fileToWaitFor, timeout); + // Wait after the file has been created to ensure it is fully written. + ThrowingConsumer.toConsumer(Thread::sleep).accept(afterCreatedTimeout); + } + + private static void waitForFileCreated(Path fileToWaitFor, Duration timeout) throws IOException { trace(String.format("Wait for file [%s] to be available", fileToWaitFor.toAbsolutePath())); @@ -608,22 +616,23 @@ static void waitForFileCreated(Path fileToWaitFor, Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent(); watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY); - long waitUntil = System.currentTimeMillis() + timeoutSeconds * 1000; + var waitUntil = Instant.now().plus(timeout); for (;;) { - long timeout = waitUntil - System.currentTimeMillis(); - assertTrue(timeout > 0, String.format( - "Check timeout value %d is positive", timeout)); + var remainderTimeout = Instant.now().until(waitUntil); + assertTrue(remainderTimeout.isPositive(), String.format( + "Check timeout value %dms is positive", remainderTimeout.toMillis())); - WatchKey key = ThrowingSupplier.toSupplier(() -> ws.poll(timeout, - TimeUnit.MILLISECONDS)).get(); + WatchKey key = ThrowingSupplier.toSupplier(() -> { + return ws.poll(remainderTimeout.toMillis(), TimeUnit.MILLISECONDS); + }).get(); if (key == null) { - if (fileToWaitFor.toFile().exists()) { + if (Files.exists(fileToWaitFor)) { trace(String.format( "File [%s] is available after poll timeout expired", fileToWaitFor)); return; } - assertUnexpected(String.format("Timeout expired", timeout)); + assertUnexpected(String.format("Timeout expired", remainderTimeout)); } for (WatchEvent event : key.pollEvents()) { From a95677fffd3acbdb9873147969a48775aab11441 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 4 Aug 2025 00:46:03 -0400 Subject: [PATCH 39/83] Use TKit.waitForFileCreated() to await for test output file --- test/jdk/tools/jpackage/share/AddLShortcutTest.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index aef78ea267f61..7d7d8b50c1dd1 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -25,18 +25,17 @@ import java.lang.invoke.MethodHandles; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import jdk.internal.util.OperatingSystem; import jdk.jpackage.test.AdditionalLauncher; -import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Parameter; +import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Test; -import jdk.jpackage.test.Executor; import jdk.jpackage.test.FileAssociations; import jdk.jpackage.test.JPackageCommand; import jdk.jpackage.test.LauncherShortcut; @@ -272,11 +271,7 @@ void verify(Collection invokeShortcutSpecs) throws // On Linux, "gtk-launch" is used to launch a .desktop file. It is async and there is no // way to make it wait for exit of a process it triggers. - Executor.tryRunMultipleTimes(() -> { - if (!Files.exists(expectedOutputFile)) { - throw new NoSuchElementException(String.format("[%s] is not avaialble", expectedOutputFile)); - } - }, 3 /* Number of attempts */, 3 /* Seconds between attempts */); + TKit.waitForFileCreated(expectedOutputFile, Duration.ofSeconds(10), Duration.ofSeconds(3)); TKit.assertFileExists(expectedOutputFile); var actualStr = Files.readAllLines(expectedOutputFile).getFirst(); From 7b86ad183060468bfe421f8a06f3fe2912dc4f77 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 23 Jul 2025 16:50:32 -0400 Subject: [PATCH 40/83] LinuxHelper: fix a typo --- .../tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 1669d1f8233c9..3a1cad55c8731 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -340,7 +340,7 @@ static void verifyPackageBundleEssential(JPackageCommand cmd) { vitalPackage, prerequisites, packageName)); } else { TKit.trace(String.format( - "Not cheking %s required packages of [%s] package", + "Not checking %s required packages of [%s] package", prerequisites, packageName)); } } From 39851db091dfce693d836cc14a79a5423d356a0c Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 24 Jul 2025 11:22:17 -0400 Subject: [PATCH 41/83] AdditionalLauncher: add AdditionalLauncher.PropertyFile() ctor; AppImageFile: support additional launchers. --- .../jdk/jpackage/test/AdditionalLauncher.java | 13 ++-- .../jdk/jpackage/test/AppImageFile.java | 69 +++++++++++++++++-- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java index 801df8624c4c1..687c2ef420662 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java @@ -23,7 +23,7 @@ package jdk.jpackage.test; import static java.util.stream.Collectors.toMap; -import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; import java.io.IOException; import java.nio.file.Files; @@ -179,11 +179,12 @@ static PropertyFile getAdditionalLauncherProperties( PropertyFile shell[] = new PropertyFile[1]; forEachAdditionalLauncher(cmd, (name, propertiesFilePath) -> { if (name.equals(launcherName)) { - shell[0] = toFunction(PropertyFile::new).apply( - propertiesFilePath); + shell[0] = toSupplier(() -> { + return new PropertyFile(propertiesFilePath); + }).get(); } }); - return Optional.of(shell[0]).get(); + return Objects.requireNonNull(shell[0]); } private void initialize(JPackageCommand cmd) throws IOException { @@ -390,6 +391,10 @@ protected void verify(JPackageCommand cmd) throws IOException { public static final class PropertyFile { + PropertyFile(Map data) { + this.data = Map.copyOf(data); + } + PropertyFile(Path path) throws IOException { data = Files.readAllLines(path).stream().map(str -> { return str.split("=", 2); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java index 2381aecec2ea1..e676e0d1e878c 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java @@ -22,20 +22,28 @@ */ package jdk.jpackage.test; +import static java.util.stream.Collectors.toMap; +import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathFactory; import jdk.internal.util.OperatingSystem; import jdk.jpackage.internal.util.XmlUtils; -import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; public record AppImageFile(String mainLauncherName, String mainLauncherClassName, - String version, boolean macSigned, boolean macAppStore) { + String version, boolean macSigned, boolean macAppStore, Map> launchers) { public static Path getPathInAppImage(Path appImageDir) { return ApplicationLayout.platformAppImage() @@ -44,8 +52,23 @@ public static Path getPathInAppImage(Path appImageDir) { .resolve(FILENAME); } + public AppImageFile { + Objects.requireNonNull(mainLauncherName); + Objects.requireNonNull(mainLauncherClassName); + Objects.requireNonNull(version); + if (!launchers.containsKey(mainLauncherName)) { + throw new IllegalArgumentException(); + } + } + public AppImageFile(String mainLauncherName, String mainLauncherClassName) { - this(mainLauncherName, mainLauncherClassName, "1.0", false, false); + this(mainLauncherName, mainLauncherClassName, "1.0", false, false, Map.of(mainLauncherName, Map.of())); + } + + public Map> addLaunchers() { + return launchers.entrySet().stream().filter(e -> { + return !e.getKey().equals(mainLauncherName); + }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); } public void save(Path appImageDir) throws IOException { @@ -73,6 +96,18 @@ public void save(Path appImageDir) throws IOException { xml.writeStartElement("app-store"); xml.writeCharacters(Boolean.toString(macAppStore)); xml.writeEndElement(); + + for (var al : addLaunchers().keySet().stream().sorted().toList()) { + xml.writeStartElement("add-launcher"); + xml.writeAttribute("name", al); + var props = launchers.get(al); + for (var prop : props.keySet().stream().sorted().toList()) { + xml.writeStartElement(prop); + xml.writeCharacters(props.get(prop)); + xml.writeEndElement(); + } + xml.writeEndElement(); + } }); } @@ -99,8 +134,34 @@ public static AppImageFile load(Path appImageDir) { "/jpackage-state/app-store/text()", doc)).map( Boolean::parseBoolean).orElse(false); + var addLaunchers = XmlUtils.queryNodes(doc, xPath, "/jpackage-state/add-launcher").map(Element.class::cast).map(toFunction(addLauncher -> { + Map launcherProps = new HashMap<>(); + + // @name and @service attributes. + XmlUtils.toStream(addLauncher.getAttributes()).forEach(attr -> { + launcherProps.put(attr.getNodeName(), attr.getNodeValue()); + }); + + // Extra properties. + XmlUtils.queryNodes(addLauncher, xPath, "*[count(*) = 0]").map(Element.class::cast).forEach(e -> { + launcherProps.put(e.getNodeName(), e.getTextContent()); + }); + + return launcherProps; + })); + + var mainLauncherProperties = Map.of("name", mainLauncherName); + + var launchers = Stream.concat(Stream.of(mainLauncherProperties), addLaunchers).collect(toMap(attrs -> { + return Objects.requireNonNull(attrs.get("name")); + }, attrs -> { + Map copy = new HashMap<>(attrs); + copy.remove("name"); + return Map.copyOf(copy); + })); + return new AppImageFile(mainLauncherName, mainLauncherClassName, - version, macSigned, macAppStore); + version, macSigned, macAppStore, launchers); }).get(); } From 33e85f2fb713fd2b7ce4631d12903588e8550c85 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Tue, 22 Jul 2025 21:53:23 -0400 Subject: [PATCH 42/83] Introduce MsiDatabase; Implement launcher shortcut verification in MSI bundles. With this patch, there is no longer a need to install an MSI package to verify shortcuts. --- .../jdk/jpackage/test/MsiDatabase.java | 377 ++++++++++++++++++ .../jdk/jpackage/test/PackageTest.java | 17 +- .../jpackage/test/WinShortcutVerifier.java | 278 +++++++++++++ .../jdk/jpackage/test/WindowsHelper.java | 177 ++++++-- .../tools/jpackage/resources/msi-export.js | 81 ++++ .../jpackage/resources/query-msi-property.js | 65 --- 6 files changed, 891 insertions(+), 104 deletions(-) create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java create mode 100644 test/jdk/tools/jpackage/resources/msi-export.js delete mode 100644 test/jdk/tools/jpackage/resources/query-msi-property.js diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java new file mode 100644 index 0000000000000..87236575e2ef6 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2025, 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.test; + + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + + +final class MsiDatabase { + + static MsiDatabase load(Path msiFile, Path idtFileOutputDir, Set
tableNames) { + try { + Files.createDirectories(idtFileOutputDir); + + var orderedTableNames = tableNames.stream().sorted().toList(); + + Executor.of("cscript.exe", "//Nologo") + .addArgument(TKit.TEST_SRC_ROOT.resolve("resources/msi-export.js")) + .addArgument(msiFile) + .addArgument(idtFileOutputDir) + .addArguments(orderedTableNames.stream().map(Table::tableName).toList()) + .dumpOutput() + .execute(0); + + var tables = orderedTableNames.stream().map(tableName -> { + return Map.entry(tableName, idtFileOutputDir.resolve(tableName + ".idt")); + }).filter(e -> { + return Files.exists(e.getValue()); + }).collect(Collectors.toMap(Map.Entry::getKey, e -> { + return MsiTable.loadFromTextArchiveFile(e.getValue()); + })); + + return new MsiDatabase(tables); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + + enum Table { + COMPONENT("Component"), + DIRECTORY("Directory"), + FILE("File"), + PROPERTY("Property"), + SHORTCUT("Shortcut"), + ; + + Table(String name) { + this.tableName = Objects.requireNonNull(name); + } + + String tableName() { + return tableName; + } + + private final String tableName; + + static final Set
FIND_PROPERTY_REQUIRED_TABLES = Set.of(PROPERTY); + static final Set
LIST_SHORTCUTS_REQUIRED_TABLES = Set.of(COMPONENT, DIRECTORY, FILE, SHORTCUT); + } + + + private MsiDatabase(Map tables) { + this.tables = Map.copyOf(tables); + } + + Set
tableNames() { + return tables.keySet(); + } + + MsiDatabase append(MsiDatabase other) { + Map newTables = new HashMap<>(tables); + newTables.putAll(other.tables); + return new MsiDatabase(newTables); + } + + Optional findProperty(String propertyName) { + Objects.requireNonNull(propertyName); + return tables.get(Table.PROPERTY).findRow("Property", propertyName).map(row -> { + return row.apply("Value"); + }); + } + + Collection listShortcuts() { + var shortcuts = tables.get(Table.SHORTCUT); + if (shortcuts == null) { + return List.of(); + } + return IntStream.range(0, shortcuts.rowCount()).mapToObj(i -> { + var row = shortcuts.row(i); + var shortcutPath = directoryPath(row.apply("Directory_")).resolve(fileNameFromFieldValue(row.apply("Name"))); + var workDir = directoryPath(row.apply("WkDir")); + var shortcutTarget = Path.of(expandFormattedString(row.apply("Target"))); + return new Shortcut(shortcutPath, shortcutTarget, workDir); + }).toList(); + } + + record Shortcut(Path path, Path target, Path workDir) { + + Shortcut { + Objects.requireNonNull(path); + Objects.requireNonNull(target); + Objects.requireNonNull(workDir); + } + + void assertEquals(Shortcut expected) { + TKit.assertEquals(expected.path, path, "Check the shortcut path"); + TKit.assertEquals(expected.target, target, "Check the shortcut target"); + TKit.assertEquals(expected.workDir, workDir, "Check the shortcut work directory"); + } + } + + private Path directoryPath(String directoryId) { + var table = tables.get(Table.DIRECTORY); + Path result = null; + for (var row = table.findRow("Directory", directoryId); + row.isPresent(); + directoryId = row.get().apply("Directory_Parent"), row = table.findRow("Directory", directoryId)) { + + Path pathComponent; + if (DIRECTORY_PROPERTIES.contains(directoryId)) { + pathComponent = Path.of(directoryId); + directoryId = null; + } else { + pathComponent = fileNameFromFieldValue(row.get().apply("DefaultDir")); + } + + if (result != null) { + result = pathComponent.resolve(result); + } else { + result = pathComponent; + } + + if (directoryId == null) { + break; + } + } + + return Objects.requireNonNull(result); + } + + private String expandFormattedString(String str) { + return expandFormattedString(str, token -> { + if (token.charAt(0) == '#') { + var filekey = token.substring(1); + var fileRow = tables.get(Table.FILE).findRow("File", filekey).orElseThrow(); + + var component = fileRow.apply("Component_"); + var componentRow = tables.get(Table.COMPONENT).findRow("Component", component).orElseThrow(); + + var fileName = fileNameFromFieldValue(fileRow.apply("FileName")); + var filePath = directoryPath(componentRow.apply("Directory_")); + + return filePath.resolve(fileName).toString(); + } else { + throw new UnsupportedOperationException(String.format( + "Unrecognized token [%s] in formatted string [%s]", token, str)); + } + }); + } + + private static Path fileNameFromFieldValue(String fieldValue) { + var pipeIdx = fieldValue.indexOf('|'); + if (pipeIdx < 0) { + return Path.of(fieldValue); + } else { + return Path.of(fieldValue.substring(pipeIdx + 1)); + } + } + + private static String expandFormattedString(String str, Function callback) { + // Naive implementation of https://learn.microsoft.com/en-us/windows/win32/msi/formatted + // - No recursive property expansion. + // - No curly brakes ({}) handling. + + Objects.requireNonNull(str); + Objects.requireNonNull(callback); + var sb = new StringBuffer(); + var m = FORMATTED_STRING_TOKEN.matcher(str); + while (m.find()) { + var token = m.group(); + token = token.substring(1, token.length() - 1); + if (token.equals("~")) { + m.appendReplacement(sb, "\0"); + } else { + var replacement = Matcher.quoteReplacement(callback.apply(token)); + m.appendReplacement(sb, replacement); + } + } + m.appendTail(sb); + return sb.toString(); + } + + + private record MsiTable(Map> columns) { + + MsiTable { + Objects.requireNonNull(columns); + if (columns.isEmpty()) { + throw new IllegalArgumentException("Table should have columns"); + } + } + + Optional> findRow(String columnName, String fieldValue) { + Objects.requireNonNull(columnName); + Objects.requireNonNull(fieldValue); + var column = columns.get(columnName); + for (int i = 0; i != column.size(); i++) { + if (fieldValue.equals(column.get(i))) { + return Optional.of(row(i)); + } + } + return Optional.empty(); + } + + /** + * Loads a table from a text archive file. + * @param idtFile path to the input text archive file + * @return the table + */ + static MsiTable loadFromTextArchiveFile(Path idtFile) { + + var header = IdtFileHeader.loadFromTextArchiveFile(idtFile); + + Map> columns = new HashMap<>(); + header.columns.forEach(column -> { + columns.put(column, new ArrayList<>()); + }); + + try { + var lines = Files.readAllLines(idtFile, header.charset()).toArray(String[]::new); + for (int i = 3; i != lines.length; i++) { + var line = lines[i]; + var row = line.split("\t", -1); + if (row.length != header.columns().size()) { + throw new IllegalArgumentException(String.format( + "Expected %d columns. Actual is %d in line %d in [%s] file", + header.columns().size(), row.length, i, idtFile)); + } + for (int j = 0; j != row.length; j++) { + var field = row[j]; + // https://learn.microsoft.com/en-us/windows/win32/msi/archive-file-format + field = field.replace((char)21, (char)0); + field = field.replace((char)27, '\b'); + field = field.replace((char)16, '\t'); + field = field.replace((char)25, '\n'); + field = field.replace((char)24, '\f'); + field = field.replace((char)17, '\r'); + columns.get(header.columns.get(j)).add(field); + } + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + + return new MsiTable(columns); + } + + int columnCount() { + return columns.size(); + } + + int rowCount() { + return columns.values().stream().findAny().orElseThrow().size(); + } + + Function row(int rowIndex) { + return columnName -> { + var column = Objects.requireNonNull(columns.get(Objects.requireNonNull(columnName))); + return column.get(rowIndex); + }; + } + } + + + private record IdtFileHeader(Charset charset, List columns) { + + IdtFileHeader { + Objects.requireNonNull(charset); + columns.forEach(Objects::requireNonNull); + if (columns.isEmpty()) { + throw new IllegalArgumentException("Table should have columns"); + } + } + + /** + * Loads a table header from a text archive (.idt) file. + * @see https://learn.microsoft.com/en-us/windows/win32/msi/archive-file-format + * @see https://learn.microsoft.com/en-us/windows/win32/msi/ascii-data-in-text-archive-files + * @param path path to the input text archive file + * @return the table header + */ + static IdtFileHeader loadFromTextArchiveFile(Path idtFile) { + var charset = StandardCharsets.US_ASCII; + try (var stream = Files.lines(idtFile, charset)) { + var headerLines = stream.limit(3).toList(); + if (headerLines.size() != 3) { + throw new IllegalArgumentException(String.format( + "[%s] file should have at least three text lines", idtFile)); + } + + var columns = headerLines.get(0).split("\t"); + + var header = headerLines.get(2).split("\t", 4); + if (header.length == 3) { + if (Pattern.matches("^[1-9]\\d+$", header[0])) { + charset = Charset.forName(header[0]); + } else { + throw new IllegalArgumentException(String.format( + "Unexpected charset name [%s] in [%s] file", header[0], idtFile)); + } + } else if (header.length != 2) { + throw new IllegalArgumentException(String.format( + "Unexpected number of fields (%d) in the 3rd line of [%s] file", + header.length, idtFile)); + } + + return new IdtFileHeader(charset, List.of(columns)); + + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + } + + + private final Map tables; + + // https://learn.microsoft.com/en-us/windows/win32/msi/formatted + private static final Pattern FORMATTED_STRING_TOKEN = Pattern.compile("\\[[^\\]]+\\]"); + + // https://learn.microsoft.com/en-us/windows/win32/msi/property-reference#system-folder-properties + private final Set DIRECTORY_PROPERTIES = Set.of( + "DesktopFolder", + "LocalAppDataFolder", + "ProgramFiles64Folder", + "ProgramMenuFolder" + ); +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index 82b1623a42d1a..9642b3d943c4a 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -30,6 +30,7 @@ import static jdk.jpackage.test.PackageType.MAC_PKG; import static jdk.jpackage.test.PackageType.NATIVE; import static jdk.jpackage.test.PackageType.WINDOWS; +import static jdk.jpackage.test.PackageType.WIN_MSI; import java.awt.GraphicsEnvironment; import java.io.IOException; @@ -753,6 +754,8 @@ private void verifyPackageBundle(JPackageCommand cmd, if (expectedJPackageExitCode == 0) { if (isOfType(cmd, LINUX)) { LinuxHelper.verifyPackageBundleEssential(cmd); + } else if (isOfType(cmd, WIN_MSI)) { + WinShortcutVerifier.verifyBundleShortcuts(cmd); } } bundleVerifiers.forEach(v -> v.accept(cmd, result)); @@ -774,12 +777,7 @@ private void verifyPackageInstalled(JPackageCommand cmd) { if (!cmd.isRuntime()) { if (isOfType(cmd, WINDOWS) && !cmd.isPackageUnpacked("Not verifying desktop integration")) { - // Check main launcher - WindowsHelper.verifyDesktopIntegration(cmd, null); - // Check additional launchers - cmd.addLauncherNames().forEach(name -> { - WindowsHelper.verifyDesktopIntegration(cmd, name); - }); + WindowsHelper.verifyDeployedDesktopIntegration(cmd, true); } } @@ -856,12 +854,7 @@ private void verifyPackageUninstalled(JPackageCommand cmd) { TKit.assertPathExists(cmd.appLauncherPath(), false); if (isOfType(cmd, WINDOWS)) { - // Check main launcher - WindowsHelper.verifyDesktopIntegration(cmd, null); - // Check additional launchers - cmd.addLauncherNames().forEach(name -> { - WindowsHelper.verifyDesktopIntegration(cmd, name); - }); + WindowsHelper.verifyDeployedDesktopIntegration(cmd, false); } } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java new file mode 100644 index 0000000000000..10c6209c08925 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2019, 2025, 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.test; + +import static jdk.jpackage.test.WindowsHelper.getInstallationSubDirectory; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.internal.util.PathUtils; +import jdk.jpackage.test.MsiDatabase.Shortcut; +import jdk.jpackage.test.WindowsHelper.SpecialFolder; + + +final class WinShortcutVerifier { + + static void verifyBundleShortcuts(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.WIN_MSI); + + if (Stream.of("--win-menu", "--win-shortcut").noneMatch(cmd::hasArgument) && cmd.addLauncherNames().isEmpty()) { + return; + } + + var actualShortcuts = WindowsHelper.getMsiShortcuts(cmd).stream().collect(Collectors.groupingBy(shortcut -> { + return PathUtils.replaceSuffix(shortcut.target().getFileName(), "").toString(); + })); + + var expectedShortcuts = expectShortcuts(cmd); + + var launcherNames = expectedShortcuts.keySet().stream().sorted().toList(); + + TKit.assertStringListEquals( + launcherNames, + actualShortcuts.keySet().stream().sorted().toList(), + "Check the list of launchers with shortcuts"); + + Function, List> sorter = shortcuts -> { + return shortcuts.stream().sorted(SHORTCUT_COMPARATOR).toList(); + }; + + for (var name : launcherNames) { + var actualLauncherShortcuts = sorter.apply(actualShortcuts.get(name)); + var expectedLauncherShortcuts = sorter.apply(expectedShortcuts.get(name)); + + TKit.assertEquals(expectedLauncherShortcuts.size(), actualLauncherShortcuts.size(), + String.format("Check the number of shortcuts of [%s] launcher", name)); + + for (int i = 0; i != expectedLauncherShortcuts.size(); i++) { + TKit.trace(String.format("Verify shortcut #%d of [%s] launcher", i + 1, name)); + actualLauncherShortcuts.get(i).assertEquals(expectedLauncherShortcuts.get(i)); + TKit.trace("Done"); + } + } + } + + static void verifyDeployedShortcuts(JPackageCommand cmd, boolean installed) { + cmd.verifyIsOfType(PackageType.WINDOWS); + + verifyDeployedShortcutsInternal(cmd, installed); + var copyCmd = new JPackageCommand(cmd); + if (copyCmd.hasArgument("--win-per-user-install")) { + copyCmd.removeArgument("--win-per-user-install"); + } else { + copyCmd.addArgument("--win-per-user-install"); + } + verifyDeployedShortcutsInternal(copyCmd, false); + } + + private static void verifyDeployedShortcutsInternal(JPackageCommand cmd, boolean installed) { + + var expectedShortcuts = expectShortcuts(cmd).values().stream().flatMap(Collection::stream).toList(); + + var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); + + expectedShortcuts.stream().map(Shortcut::path).sorted().map(path -> { + return resolvePath(path, !isUserLocalInstall); + }).map(path -> { + return PathUtils.addSuffix(path, ".lnk"); + }).forEach(path -> { + if (installed) { + TKit.assertFileExists(path); + } else { + TKit.assertPathExists(path, false); + } + }); + + if (!installed) { + expectedShortcuts.stream().map(Shortcut::path).filter(path -> { + return Stream.of(ShortcutType.COMMON_START_MENU, ShortcutType.USER_START_MENU).anyMatch(type -> { + return path.startsWith(Path.of(type.rootFolder().getMsiPropertyName())); + }); + }).map(Path::getParent).distinct().map(unresolvedShortcutDir -> { + return resolvePath(unresolvedShortcutDir, !isUserLocalInstall); + }).forEach(shortcutDir -> { + if (Files.isDirectory(shortcutDir)) { + TKit.assertDirectoryNotEmpty(shortcutDir); + } else { + TKit.assertPathExists(shortcutDir, false); + } + }); + } + } + + private enum ShortcutType { + COMMON_START_MENU(SpecialFolder.COMMON_START_MENU_PROGRAMS), + USER_START_MENU(SpecialFolder.USER_START_MENU_PROGRAMS), + COMMON_DESKTOP(SpecialFolder.COMMON_DESKTOP), + USER_DESKTOP(SpecialFolder.USER_DESKTOP), + ; + + ShortcutType(SpecialFolder rootFolder) { + this.rootFolder = Objects.requireNonNull(rootFolder); + } + + SpecialFolder rootFolder() { + return rootFolder; + } + + private final SpecialFolder rootFolder; + } + + private static Path resolvePath(Path path, boolean allUsers) { + var root = path.getName(0); + var resolvedRoot = SpecialFolder.findMsiProperty(root.toString(), allUsers).orElseThrow().getPath(); + return resolvedRoot.resolve(root.relativize(path)); + } + + private static Shortcut createLauncherShortcutSpec(JPackageCommand cmd, String launcherName, + SpecialFolder installRoot, Path workDir, ShortcutType type) { + + var name = Optional.ofNullable(launcherName).orElseGet(cmd::name); + + var appLayout = ApplicationLayout.windowsAppImage().resolveAt( + Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd))); + + Path path; + switch (type) { + case COMMON_START_MENU, USER_START_MENU -> { + path = Path.of(cmd.getArgumentValue("--win-menu-group", () -> "Unknown"), name); + } + default -> { + path = Path.of(name); + } + } + + return new Shortcut( + Path.of(type.rootFolder().getMsiPropertyName()).resolve(path), + appLayout.launchersDirectory().resolve(name + ".exe"), + workDir); + } + + private static Collection expectLauncherShortcuts(JPackageCommand cmd, + Optional predefinedAppImage, String launcherName) { + Objects.requireNonNull(cmd); + Objects.requireNonNull(predefinedAppImage); + + List shortcuts = new ArrayList<>(); + + var name = Optional.ofNullable(launcherName).orElseGet(cmd::name); + + boolean isWinMenu; + boolean isDesktop; + if (name.equals(cmd.name())) { + isWinMenu = cmd.hasArgument("--win-menu"); + isDesktop = cmd.hasArgument("--win-shortcut"); + } else { + var props = predefinedAppImage.map(v -> { + return v.launchers().get(name); + }).map(appImageFileLauncherProps -> { + Map convProps = new HashMap<>(); + for (var e : Map.of("menu", "win-menu", "shortcut", "win-shortcut").entrySet()) { + Optional.ofNullable(appImageFileLauncherProps.get(e.getKey())).ifPresent(v -> { + convProps.put(e.getValue(), v); + }); + } + return new AdditionalLauncher.PropertyFile(convProps); + }).orElseGet(() -> { + return AdditionalLauncher.getAdditionalLauncherProperties(cmd, launcherName); + }); + isWinMenu = props.getPropertyBooleanValue("win-menu").orElseGet(() -> cmd.hasArgument("--win-menu")); + isDesktop = props.getPropertyBooleanValue("win-shortcut").orElseGet(() -> cmd.hasArgument("--win-shortcut")); + } + + var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); + + SpecialFolder installRoot; + if (isUserLocalInstall) { + installRoot = SpecialFolder.LOCAL_APPLICATION_DATA; + } else { + installRoot = SpecialFolder.PROGRAM_FILES; + } + + var workDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd)); + + if (isWinMenu) { + ShortcutType type; + if (isUserLocalInstall) { + type = ShortcutType.USER_START_MENU; + } else { + type = ShortcutType.COMMON_START_MENU; + } + shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, workDir, type)); + } + + if (isDesktop) { + ShortcutType type; + if (isUserLocalInstall) { + type = ShortcutType.USER_DESKTOP; + } else { + type = ShortcutType.COMMON_DESKTOP; + } + shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, workDir, type)); + } + + return shortcuts; + } + + private static Map> expectShortcuts(JPackageCommand cmd) { + Map> expectedShortcuts = new HashMap<>(); + + var predefinedAppImage = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of).map(AppImageFile::load); + + predefinedAppImage.map(v -> { + return v.launchers().keySet().stream(); + }).orElseGet(() -> { + return Stream.concat(Stream.of(cmd.name()), cmd.addLauncherNames().stream()); + }).forEach(launcherName -> { + var shortcuts = expectLauncherShortcuts(cmd, predefinedAppImage, launcherName); + if (!shortcuts.isEmpty()) { + expectedShortcuts.put(launcherName, shortcuts); + } + }); + + return expectedShortcuts; + } + + addShortcuts.accept(cmd.name()); + predefinedAppImage.map(v -> { + return (Collection)v.addLaunchers().keySet(); + }).orElseGet(cmd::addLauncherNames).forEach(addShortcuts); + + return expectedShortcuts; + } + + private static final Comparator SHORTCUT_COMPARATOR = Comparator.comparing(Shortcut::target) + .thenComparing(Comparator.comparing(Shortcut::path)) + .thenComparing(Comparator.comparing(Shortcut::workDir)); +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 3ac302ce0fed4..8f52a464250d3 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -26,9 +26,14 @@ import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.ref.SoftReference; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; +import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -63,7 +68,7 @@ private static Path getInstallationRootDirectory(JPackageCommand cmd) { return PROGRAM_FILES; } - private static Path getInstallationSubDirectory(JPackageCommand cmd) { + static Path getInstallationSubDirectory(JPackageCommand cmd) { cmd.verifyIsOfType(PackageType.WINDOWS); return Path.of(cmd.getArgumentValue("--install-dir", cmd::name)); } @@ -263,19 +268,24 @@ static Optional toShortPath(Path path) { } } - static void verifyDesktopIntegration(JPackageCommand cmd, - String launcherName) { - new DesktopIntegrationVerifier(cmd, launcherName); + static void verifyDeployedDesktopIntegration(JPackageCommand cmd, boolean installed) { + WinShortcutVerifier.verifyDeployedShortcuts(cmd, installed); + // Check the main launcher + new DesktopIntegrationVerifier(cmd, installed, null); + // Check additional launchers + cmd.addLauncherNames().forEach(name -> { + new DesktopIntegrationVerifier(cmd, installed, name); + }); } public static String getMsiProperty(JPackageCommand cmd, String propertyName) { cmd.verifyIsOfType(PackageType.WIN_MSI); - return Executor.of("cscript.exe", "//Nologo") - .addArgument(TKit.TEST_SRC_ROOT.resolve("resources/query-msi-property.js")) - .addArgument(cmd.outputBundle()) - .addArgument(propertyName) - .dumpOutput() - .executeAndGetOutput().stream().collect(Collectors.joining("\n")); + return MsiDatabaseCache.INSTANCE.findProperty(cmd.outputBundle(), propertyName).orElseThrow(); + } + + static Collection getMsiShortcuts(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.WIN_MSI); + return MsiDatabaseCache.INSTANCE.listShortcuts(cmd.outputBundle()); } public static String getExecutableDesciption(Path pathToExeFile) { @@ -386,7 +396,7 @@ private static long[] findAppLauncherPIDs(JPackageCommand cmd, String launcherNa } } - private static boolean isUserLocalInstall(JPackageCommand cmd) { + static boolean isUserLocalInstall(JPackageCommand cmd) { return cmd.hasArgument("--win-per-user-install"); } @@ -396,14 +406,14 @@ private static boolean isPathTooLong(Path path) { private static class DesktopIntegrationVerifier { - DesktopIntegrationVerifier(JPackageCommand cmd, String launcherName) { + DesktopIntegrationVerifier(JPackageCommand cmd, boolean installed, String launcherName) { cmd.verifyIsOfType(PackageType.WINDOWS); name = Optional.ofNullable(launcherName).orElseGet(cmd::name); isUserLocalInstall = isUserLocalInstall(cmd); - appInstalled = cmd.appLauncherPath(launcherName).toFile().exists(); + this.appInstalled = installed; desktopShortcutPath = Path.of(name + ".lnk"); @@ -611,7 +621,12 @@ private enum SpecialFolderDotNet { CommonDesktop, Programs, - CommonPrograms; + CommonPrograms, + + ProgramFiles, + + LocalApplicationData, + ; Path getPath() { final var str = Executor.of("powershell", "-NoLogo", "-NoProfile", @@ -636,33 +651,84 @@ Optional findValue() { } } - private enum SpecialFolder { - COMMON_START_MENU_PROGRAMS(SYSTEM_SHELL_FOLDERS_REGKEY, "Common Programs", SpecialFolderDotNet.CommonPrograms), - USER_START_MENU_PROGRAMS(USER_SHELL_FOLDERS_REGKEY, "Programs", SpecialFolderDotNet.Programs), - - COMMON_DESKTOP(SYSTEM_SHELL_FOLDERS_REGKEY, "Common Desktop", SpecialFolderDotNet.CommonDesktop), - USER_DESKTOP(USER_SHELL_FOLDERS_REGKEY, "Desktop", SpecialFolderDotNet.Desktop); - - SpecialFolder(String keyPath, String valueName) { - reg = new RegValuePath(keyPath, valueName); + enum SpecialFolder { + COMMON_START_MENU_PROGRAMS( + SYSTEM_SHELL_FOLDERS_REGKEY, + "Common Programs", + "ProgramMenuFolder", + SpecialFolderDotNet.CommonPrograms), + USER_START_MENU_PROGRAMS( + USER_SHELL_FOLDERS_REGKEY, + "Programs", + "ProgramMenuFolder", + SpecialFolderDotNet.Programs), + + COMMON_DESKTOP( + SYSTEM_SHELL_FOLDERS_REGKEY, + "Common Desktop", + "DesktopFolder", + SpecialFolderDotNet.CommonDesktop), + USER_DESKTOP( + USER_SHELL_FOLDERS_REGKEY, + "Desktop", + "DesktopFolder", + SpecialFolderDotNet.Desktop), + + PROGRAM_FILES("ProgramFiles64Folder", SpecialFolderDotNet.ProgramFiles), + + LOCAL_APPLICATION_DATA("LocalAppDataFolder", SpecialFolderDotNet.LocalApplicationData), + ; + + SpecialFolder(String keyPath, String valueName, String msiPropertyName) { + reg = Optional.of(new RegValuePath(keyPath, valueName)); alt = Optional.empty(); + this.msiPropertyName = Objects.requireNonNull(msiPropertyName); + } + + SpecialFolder(String keyPath, String valueName, String msiPropertyName, SpecialFolderDotNet alt) { + reg = Optional.of(new RegValuePath(keyPath, valueName)); + this.alt = Optional.of(alt); + this.msiPropertyName = Objects.requireNonNull(msiPropertyName); } - SpecialFolder(String keyPath, String valueName, SpecialFolderDotNet alt) { - reg = new RegValuePath(keyPath, valueName); + SpecialFolder(String msiPropertyName, SpecialFolderDotNet alt) { + reg = Optional.empty(); this.alt = Optional.of(alt); + this.msiPropertyName = Objects.requireNonNull(msiPropertyName); + } + + static Optional findMsiProperty(String pathComponent, boolean allUsers) { + Objects.requireNonNull(pathComponent); + String regPath; + if (allUsers) { + regPath = SYSTEM_SHELL_FOLDERS_REGKEY; + } else { + regPath = USER_SHELL_FOLDERS_REGKEY; + } + return Stream.of(values()) + .filter(v -> v.msiPropertyName.equals(pathComponent)) + .filter(v -> { + return v.reg.map(r -> r.keyPath().equals(regPath)).orElse(true); + }) + .findFirst(); + } + + String getMsiPropertyName() { + return msiPropertyName; } Path getPath() { - return CACHE.computeIfAbsent(this, k -> reg.findValue().map(Path::of).orElseGet(() -> { + return CACHE.computeIfAbsent(this, k -> reg.flatMap(RegValuePath::findValue).map(Path::of).orElseGet(() -> { return alt.map(SpecialFolderDotNet::getPath).orElseThrow(() -> { return new NoSuchElementException(String.format("Failed to find path to %s folder", name())); }); })); } - private final RegValuePath reg; + private final Optional reg; private final Optional alt; + // One of "System Folder Properties" from https://learn.microsoft.com/en-us/windows/win32/msi/property-reference + private final String msiPropertyName; private static final Map CACHE = new ConcurrentHashMap<>(); } @@ -693,6 +759,63 @@ static Path toShortPath(Path path) { private static final ShortPathUtils INSTANCE = new ShortPathUtils(); } + + private static final class MsiDatabaseCache { + + Optional findProperty(Path msiPath, String propertyName) { + return ensureTables(msiPath, MsiDatabase.Table.FIND_PROPERTY_REQUIRED_TABLES).findProperty(propertyName); + } + + Collection listShortcuts(Path msiPath) { + return ensureTables(msiPath, MsiDatabase.Table.LIST_SHORTCUTS_REQUIRED_TABLES).listShortcuts(); + } + + MsiDatabase ensureTables(Path msiPath, Set tableNames) { + Objects.requireNonNull(msiPath); + try { + synchronized (items) { + var value = Optional.ofNullable(items.get(msiPath)).map(SoftReference::get).orElse(null); + if (value != null) { + var lastModifiedTime = Files.getLastModifiedTime(msiPath).toInstant(); + if (lastModifiedTime.isAfter(value.timestamp())) { + value = null; + } else { + tableNames = Comm.compare(value.db().tableNames(), tableNames).unique2(); + } + } + + if (!tableNames.isEmpty()) { + var idtOutputDir = TKit.createTempDirectory("msi-db"); + var db = MsiDatabase.load(msiPath, idtOutputDir, tableNames); + if (value != null) { + value = new MsiDatabaseWithTimestamp(db.append(value.db()), value.timestamp()); + } else { + value = new MsiDatabaseWithTimestamp(db, Files.getLastModifiedTime(msiPath).toInstant()); + } + items.put(msiPath, new SoftReference<>(value)); + } + + return value.db(); + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private record MsiDatabaseWithTimestamp(MsiDatabase db, Instant timestamp) { + + MsiDatabaseWithTimestamp { + Objects.requireNonNull(db); + Objects.requireNonNull(timestamp); + } + } + + private final Map> items = new HashMap<>(); + + static final MsiDatabaseCache INSTANCE = new MsiDatabaseCache(); + } + + static final Set CRITICAL_RUNTIME_FILES = Set.of(Path.of( "bin\\server\\jvm.dll")); diff --git a/test/jdk/tools/jpackage/resources/msi-export.js b/test/jdk/tools/jpackage/resources/msi-export.js new file mode 100644 index 0000000000000..d639f19ca44a1 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/msi-export.js @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025, 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. + */ + + +function readMsi(msiPath, callback) { + var installer = new ActiveXObject('WindowsInstaller.Installer') + var database = installer.OpenDatabase(msiPath, 0 /* msiOpenDatabaseModeReadOnly */) + + return callback(database) +} + + +function exportTables(db, outputDir, requestedTableNames) { + var tables = {} + + var view = db.OpenView("SELECT `Name` FROM _Tables") + view.Execute() + + try { + while (true) { + var record = view.Fetch() + if (!record) { + break + } + + var name = record.StringData(1) + + if (requestedTableNames.hasOwnProperty(name)) { + tables[name] = name + } + } + } finally { + view.Close() + } + + var fso = new ActiveXObject("Scripting.FileSystemObject") + for (var table in tables) { + var idtFileName = table + ".idt" + var idtFile = outputDir + "/" + idtFileName + if (fso.FileExists(idtFile)) { + WScript.Echo("Delete [" + idtFile + "]") + fso.DeleteFile(idtFile) + } + WScript.Echo("Export table [" + table + "] in [" + idtFile + "] file") + db.Export(table, fso.GetFolder(outputDir).Path, idtFileName) + } +} + + +(function () { + var msi = WScript.arguments(0) + var outputDir = WScript.arguments(1) + var tables = {} + for (var i = 0; i !== WScript.arguments.Count(); i++) { + tables[WScript.arguments(i)] = true + } + + readMsi(msi, function (db) { + exportTables(db, outputDir, tables) + }) +})() diff --git a/test/jdk/tools/jpackage/resources/query-msi-property.js b/test/jdk/tools/jpackage/resources/query-msi-property.js deleted file mode 100644 index d821f5a8a5420..0000000000000 --- a/test/jdk/tools/jpackage/resources/query-msi-property.js +++ /dev/null @@ -1,65 +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. - * - * 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. - */ - - -function readMsi(msiPath, callback) { - var installer = new ActiveXObject('WindowsInstaller.Installer') - var database = installer.OpenDatabase(msiPath, 0 /* msiOpenDatabaseModeReadOnly */) - - return callback(database) -} - - -function queryAllProperties(db) { - var reply = {} - - var view = db.OpenView("SELECT `Property`, `Value` FROM Property") - view.Execute() - - try { - while(true) { - var record = view.Fetch() - if (!record) { - break - } - - var name = record.StringData(1) - var value = record.StringData(2) - - reply[name] = value - } - } finally { - view.Close() - } - - return reply -} - - -(function () { - var msi = WScript.arguments(0) - var propName = WScript.arguments(1) - - var props = readMsi(msi, queryAllProperties) - WScript.Echo(props[propName]) -})() From 476bbf5133d78dc176003cc236a0a35bb59d88f1 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 28 Jul 2025 13:01:55 -0400 Subject: [PATCH 43/83] LauncherIconVerifier: add verifyFileInAppImageOnly() to control if to verify the contents of an icon file or just verify the icon file's availability. --- .../jpackage/test/LauncherIconVerifier.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java index a1971ee083549..6285d9d93a0df 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java @@ -46,6 +46,11 @@ public LauncherIconVerifier setExpectedDefaultIcon() { return this; } + public LauncherIconVerifier verifyFileInAppImageOnly(boolean v) { + verifyFileInAppImageOnly = true; + return this; + } + public void applyTo(JPackageCommand cmd) throws IOException { final String curLauncherName; final String label; @@ -62,22 +67,26 @@ public void applyTo(JPackageCommand cmd) throws IOException { if (TKit.isWindows()) { TKit.assertPathExists(iconPath, false); - WinExecutableIconVerifier.verifyLauncherIcon(cmd, launcherName, - expectedIcon, expectedDefault); + if (!verifyFileInAppImageOnly) { + WinExecutableIconVerifier.verifyLauncherIcon(cmd, launcherName, expectedIcon, expectedDefault); + } } else if (expectedDefault) { TKit.assertPathExists(iconPath, true); } else if (expectedIcon == null) { TKit.assertPathExists(iconPath, false); } else { TKit.assertFileExists(iconPath); - TKit.assertTrue(-1 == Files.mismatch(expectedIcon, iconPath), - String.format( - "Check icon file [%s] of %s launcher is a copy of source icon file [%s]", - iconPath, label, expectedIcon)); + if (!verifyFileInAppImageOnly) { + TKit.assertTrue(-1 == Files.mismatch(expectedIcon, iconPath), + String.format( + "Check icon file [%s] of %s launcher is a copy of source icon file [%s]", + iconPath, label, expectedIcon)); + } } } private String launcherName; private Path expectedIcon; private boolean expectedDefault; + private boolean verifyFileInAppImageOnly; } From 184ce26e4b1c18baad26339a21f5754b06c49444 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 24 Jul 2025 14:04:21 -0400 Subject: [PATCH 44/83] Remove redundant shortcut verification code: it duplicates code in WinShortcutVerifier class --- .../jdk/jpackage/test/WindowsHelper.java | 151 +++--------------- 1 file changed, 20 insertions(+), 131 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 8f52a464250d3..452906573466f 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -39,11 +39,11 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; +import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.internal.util.function.ThrowingRunnable; import jdk.jpackage.test.PackageTest.PackageHandlers; @@ -270,12 +270,7 @@ static Optional toShortPath(Path path) { static void verifyDeployedDesktopIntegration(JPackageCommand cmd, boolean installed) { WinShortcutVerifier.verifyDeployedShortcuts(cmd, installed); - // Check the main launcher - new DesktopIntegrationVerifier(cmd, installed, null); - // Check additional launchers - cmd.addLauncherNames().forEach(name -> { - new DesktopIntegrationVerifier(cmd, installed, name); - }); + DesktopIntegrationVerifier.verify(cmd, installed); } public static String getMsiProperty(JPackageCommand cmd, String propertyName) { @@ -404,141 +399,42 @@ private static boolean isPathTooLong(Path path) { return path.toString().length() > WIN_MAX_PATH; } + private static class DesktopIntegrationVerifier { - DesktopIntegrationVerifier(JPackageCommand cmd, boolean installed, String launcherName) { + static void verify(JPackageCommand cmd, boolean installed) { cmd.verifyIsOfType(PackageType.WINDOWS); - - name = Optional.ofNullable(launcherName).orElseGet(cmd::name); - - isUserLocalInstall = isUserLocalInstall(cmd); - - this.appInstalled = installed; - - desktopShortcutPath = Path.of(name + ".lnk"); - - startMenuShortcutPath = Path.of(cmd.getArgumentValue( - "--win-menu-group", () -> "Unknown"), name + ".lnk"); - - if (name.equals(cmd.name())) { - isWinMenu = cmd.hasArgument("--win-menu"); - isDesktop = cmd.hasArgument("--win-shortcut"); - } else { - var props = AdditionalLauncher.getAdditionalLauncherProperties(cmd, - launcherName); - isWinMenu = props.getPropertyBooleanValue("win-menu").orElseGet( - () -> cmd.hasArgument("--win-menu")); - isDesktop = props.getPropertyBooleanValue("win-shortcut").orElseGet( - () -> cmd.hasArgument("--win-shortcut")); + for (var faFile : cmd.getAllArgumentValues("--file-associations")) { + verifyFileAssociationsRegistry(Path.of(faFile), installed); } - - verifyStartMenuShortcut(); - - verifyDesktopShortcut(); - - Stream.of(cmd.getAllArgumentValues("--file-associations")).map( - Path::of).forEach(this::verifyFileAssociationsRegistry); } - private void verifyDesktopShortcut() { - if (isDesktop) { - if (isUserLocalInstall) { - verifyUserLocalDesktopShortcut(appInstalled); - verifySystemDesktopShortcut(false); - } else { - verifySystemDesktopShortcut(appInstalled); - verifyUserLocalDesktopShortcut(false); - } - } else { - verifySystemDesktopShortcut(false); - verifyUserLocalDesktopShortcut(false); - } - } - - private void verifyShortcut(Path path, boolean exists) { - if (exists) { - TKit.assertFileExists(path); - } else { - TKit.assertPathExists(path, false); - } - } - - private void verifySystemDesktopShortcut(boolean exists) { - Path dir = SpecialFolder.COMMON_DESKTOP.getPath(); - verifyShortcut(dir.resolve(desktopShortcutPath), exists); - } + private static void verifyFileAssociationsRegistry(Path faFile, boolean installed) { - private void verifyUserLocalDesktopShortcut(boolean exists) { - Path dir = SpecialFolder.USER_DESKTOP.getPath(); - verifyShortcut(dir.resolve(desktopShortcutPath), exists); - } - - private void verifyStartMenuShortcut() { - if (isWinMenu) { - if (isUserLocalInstall) { - verifyUserLocalStartMenuShortcut(appInstalled); - verifySystemStartMenuShortcut(false); - } else { - verifySystemStartMenuShortcut(appInstalled); - verifyUserLocalStartMenuShortcut(false); - } - } else { - verifySystemStartMenuShortcut(false); - verifyUserLocalStartMenuShortcut(false); - } - } - - private void verifyStartMenuShortcut(Path shortcutsRoot, boolean exists) { - Path shortcutPath = shortcutsRoot.resolve(startMenuShortcutPath); - verifyShortcut(shortcutPath, exists); - if (!exists) { - final var parentDir = shortcutPath.getParent(); - if (Files.isDirectory(parentDir)) { - TKit.assertDirectoryNotEmpty(parentDir); - } else { - TKit.assertPathExists(parentDir, false); - } - } - } - - private void verifySystemStartMenuShortcut(boolean exists) { - verifyStartMenuShortcut(SpecialFolder.COMMON_START_MENU_PROGRAMS.getPath(), exists); - - } + TKit.trace(String.format( + "Get file association properties from [%s] file", + faFile)); - private void verifyUserLocalStartMenuShortcut(boolean exists) { - verifyStartMenuShortcut(SpecialFolder.USER_START_MENU_PROGRAMS.getPath(), exists); - } + var faProps = new Properties(); - private void verifyFileAssociationsRegistry(Path faFile) { - try { - TKit.trace(String.format( - "Get file association properties from [%s] file", - faFile)); - Map faProps = Files.readAllLines(faFile).stream().filter( - line -> line.trim().startsWith("extension=") || line.trim().startsWith( - "mime-type=")).map( - line -> { - String[] keyValue = line.trim().split("=", 2); - return Map.entry(keyValue[0], keyValue[1]); - }).collect(Collectors.toMap( - entry -> entry.getKey(), - entry -> entry.getValue())); - String suffix = faProps.get("extension"); - String contentType = faProps.get("mime-type"); + try (var reader = Files.newBufferedReader(faFile)) { + faProps.load(reader); + String suffix = faProps.getProperty("extension"); + String contentType = faProps.getProperty("mime-type"); TKit.assertNotNull(suffix, String.format( "Check file association suffix [%s] is found in [%s] property file", suffix, faFile)); TKit.assertNotNull(contentType, String.format( "Check file association content type [%s] is found in [%s] property file", contentType, faFile)); - verifyFileAssociations(appInstalled, "." + suffix, contentType); + verifyFileAssociations(installed, "." + suffix, contentType); + } catch (IOException ex) { - throw new RuntimeException(ex); + throw new UncheckedIOException(ex); } } - private void verifyFileAssociations(boolean exists, String suffix, + private static void verifyFileAssociations(boolean exists, String suffix, String contentType) { String contentTypeFromRegistry = queryRegistryValue(Path.of( "HKLM\\Software\\Classes", suffix).toString(), @@ -559,16 +455,9 @@ private void verifyFileAssociations(boolean exists, String suffix, "Check content type in registry not found"); } } - - private final Path desktopShortcutPath; - private final Path startMenuShortcutPath; - private final boolean isUserLocalInstall; - private final boolean appInstalled; - private final boolean isWinMenu; - private final boolean isDesktop; - private final String name; } + static String queryRegistryValue(String keyPath, String valueName) { var status = Executor.of("reg", "query", keyPath, "/v", valueName) .saveOutput() From 095f8dcae4b388bc61ab3c240df1c4aa92454948 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 24 Jul 2025 14:31:25 -0400 Subject: [PATCH 45/83] Simplify AdditionalLauncher.PropertyFile --- .../jdk/jpackage/test/AdditionalLauncher.java | 30 ++++++++----------- .../jdk/jpackage/test/ConfigFilesStasher.java | 2 +- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java index 687c2ef420662..fa847d74058b3 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java @@ -34,6 +34,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Properties; import java.util.function.BiConsumer; import java.util.function.Supplier; import java.util.regex.Matcher; @@ -392,21 +393,15 @@ protected void verify(JPackageCommand cmd) throws IOException { public static final class PropertyFile { PropertyFile(Map data) { - this.data = Map.copyOf(data); + this.data = new Properties(); + data.putAll(data); } PropertyFile(Path path) throws IOException { - data = Files.readAllLines(path).stream().map(str -> { - return str.split("=", 2); - }).collect(toMap(tokens -> tokens[0], tokens -> { - if (tokens.length == 1) { - return ""; - } else { - return tokens[1]; - } - }, (oldValue, newValue) -> { - return newValue; - })); + data = new Properties(); + try (var reader = Files.newBufferedReader(path)) { + data.load(reader); + } } public boolean isPropertySet(String name) { @@ -414,17 +409,16 @@ public boolean isPropertySet(String name) { return data.containsKey(name); } - public Optional getPropertyValue(String name) { + public Optional findPropertyValue(String name) { Objects.requireNonNull(name); - return Optional.of(data.get(name)); + return Optional.ofNullable(data.getProperty(name)); } - public Optional getPropertyBooleanValue(String name) { - Objects.requireNonNull(name); - return Optional.ofNullable(data.get(name)).map(Boolean::parseBoolean); + public Optional findPropertyBooleanValue(String name) { + return findPropertyValue(name).map(Boolean::parseBoolean); } - private final Map data; + private final Properties data; } private static String resolveVariables(JPackageCommand cmd, String str) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java index 98c791310450a..68d50b1f896d8 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java @@ -220,7 +220,7 @@ private static boolean isWithServices(JPackageCommand cmd) { AdditionalLauncher.forEachAdditionalLauncher(cmd, (launcherName, propertyFilePath) -> { try { final var launcherAsService = new AdditionalLauncher.PropertyFile(propertyFilePath) - .getPropertyBooleanValue("launcher-as-service").orElse(false); + .findPropertyBooleanValue("launcher-as-service").orElse(false); if (launcherAsService) { withServices[0] = true; } From ce9114f45a66e054bef24e60d52c93f9add70a0b Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 25 Jul 2025 17:25:07 -0400 Subject: [PATCH 46/83] Add missing CommandArguments.verifyMutable() calls. --- .../helpers/jdk/jpackage/test/CommandArguments.java | 5 +++-- .../helpers/jdk/jpackage/test/JPackageCommand.java | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java index cb7f0574afde5..4a78ad40cd17e 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java @@ -35,16 +35,17 @@ public class CommandArguments { } public final T clearArguments() { + verifyMutable(); args.clear(); return thiz(); } public final T addArgument(String v) { - args.add(v); - return thiz(); + return addArguments(v); } public final T addArguments(List v) { + verifyMutable(); args.addAll(v); return thiz(); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 169457d6f5873..559a86f0ba9e9 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -316,13 +316,11 @@ public JPackageCommand setFakeRuntime() { } JPackageCommand addPrerequisiteAction(ThrowingConsumer action) { - verifyMutable(); prerequisiteActions.add(action); return this; } JPackageCommand addVerifyAction(ThrowingConsumer action) { - verifyMutable(); verifyActions.add(action); return this; } @@ -820,6 +818,7 @@ public Executor.Result execute() { } public Executor.Result execute(int expectedExitCode) { + verifyMutable(); executePrerequisiteActions(); if (hasArgument("--dest")) { @@ -1046,6 +1045,7 @@ private static Stream tokenizeValue(String str) { } public JPackageCommand setReadOnlyPathAsserts(ReadOnlyPathAssert... asserts) { + verifyMutable(); readOnlyPathAsserts = Set.of(asserts); return this; } @@ -1111,6 +1111,7 @@ private static JPackageCommand convertFromRuntime(JPackageCommand cmd) { } public JPackageCommand setAppLayoutAsserts(AppLayoutAssert ... asserts) { + verifyMutable(); appLayoutAsserts = Set.of(asserts); return this; } @@ -1254,6 +1255,7 @@ private void assertFileInAppImage(Path filename, Path expectedPath) { } JPackageCommand setUnpackedPackageLocation(Path path) { + verifyMutable(); verifyIsOfType(PackageType.NATIVE); if (path != null) { setArgumentValue(UNPACKED_PATH_ARGNAME, path); @@ -1264,6 +1266,7 @@ JPackageCommand setUnpackedPackageLocation(Path path) { } JPackageCommand winMsiLogFile(Path v) { + verifyMutable(); if (!TKit.isWindows()) { throw new UnsupportedOperationException(); } @@ -1286,6 +1289,7 @@ public Optional> winMsiLogFileContents() { } private JPackageCommand adjustArgumentsBeforeExecution() { + verifyMutable(); if (!isWithToolProvider()) { // if jpackage is launched as a process then set the jlink.debug system property // to allow the jlink process to print exception stacktraces on any failure From 0775aa6393316e995f01c0dfb31729bdc547b1ef Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 25 Jul 2025 20:53:20 -0400 Subject: [PATCH 47/83] WindowsHelper: fix a typo --- .../tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 452906573466f..5e97b0d2dde58 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -283,7 +283,7 @@ static Collection getMsiShortcuts(JPackageCommand cmd) { return MsiDatabaseCache.INSTANCE.listShortcuts(cmd.outputBundle()); } - public static String getExecutableDesciption(Path pathToExeFile) { + public static String getExecutableDescription(Path pathToExeFile) { Executor exec = Executor.of("powershell", "-NoLogo", "-NoProfile", From fcdb5b0b074dc912b499ac996d1ce8e758c51b86 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 24 Jul 2025 14:32:01 -0400 Subject: [PATCH 48/83] AddLauncherTest, UpgradeTest, WinShortcutVerifier, ConfigFilesStasher: follow-up for function renames in the AdditionalLauncher. --- .../jdk/jpackage/test/ConfigFilesStasher.java | 2 +- .../jdk/jpackage/test/WinShortcutVerifier.java | 4 ++-- test/jdk/tools/jpackage/linux/UpgradeTest.java | 10 +++++----- test/jdk/tools/jpackage/share/AddLauncherTest.java | 14 +++++++------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java index 68d50b1f896d8..e630659bdb17d 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java @@ -220,7 +220,7 @@ private static boolean isWithServices(JPackageCommand cmd) { AdditionalLauncher.forEachAdditionalLauncher(cmd, (launcherName, propertyFilePath) -> { try { final var launcherAsService = new AdditionalLauncher.PropertyFile(propertyFilePath) - .findPropertyBooleanValue("launcher-as-service").orElse(false); + .findBooleanProperty("launcher-as-service").orElse(false); if (launcherAsService) { withServices[0] = true; } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index 10c6209c08925..a4c9311f041b1 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -207,8 +207,8 @@ private static Collection expectLauncherShortcuts(JPackageCommand cmd, }).orElseGet(() -> { return AdditionalLauncher.getAdditionalLauncherProperties(cmd, launcherName); }); - isWinMenu = props.getPropertyBooleanValue("win-menu").orElseGet(() -> cmd.hasArgument("--win-menu")); - isDesktop = props.getPropertyBooleanValue("win-shortcut").orElseGet(() -> cmd.hasArgument("--win-shortcut")); + isWinMenu = props.findBooleanProperty("win-menu").orElseGet(() -> cmd.hasArgument("--win-menu")); + isDesktop = props.findBooleanProperty("win-shortcut").orElseGet(() -> cmd.hasArgument("--win-shortcut")); } var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); diff --git a/test/jdk/tools/jpackage/linux/UpgradeTest.java b/test/jdk/tools/jpackage/linux/UpgradeTest.java index fb399cec12bdb..bfbdcf3aa0c07 100644 --- a/test/jdk/tools/jpackage/linux/UpgradeTest.java +++ b/test/jdk/tools/jpackage/linux/UpgradeTest.java @@ -60,16 +60,16 @@ public void testDesktopFiles() { var alA = createAdditionalLauncher("launcherA"); alA.applyTo(pkg); - createAdditionalLauncher("launcherB").addRawProperties(Map.entry( - "description", "Foo")).applyTo(pkg); + createAdditionalLauncher("launcherB").setProperty( + "description", "Foo").applyTo(pkg); var pkg2 = createPackageTest().addInitializer(cmd -> { cmd.addArguments("--app-version", "2.0"); }); alA.verifyRemovedInUpgrade(pkg2); - createAdditionalLauncher("launcherB").addRawProperties(Map.entry( - "description", "Bar")).applyTo(pkg2); + createAdditionalLauncher("launcherB").setProperty( + "description", "Bar").applyTo(pkg2); createAdditionalLauncher("launcherC").applyTo(pkg2); new PackageTest.Group(pkg, pkg2).run(); @@ -88,6 +88,6 @@ private static AdditionalLauncher createAdditionalLauncher(String name) { return new AdditionalLauncher(name).setIcon(GOLDEN_ICON); } - private final static Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of( + private static final Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of( "resources", "icon" + TKit.ICON_SUFFIX)); } diff --git a/test/jdk/tools/jpackage/share/AddLauncherTest.java b/test/jdk/tools/jpackage/share/AddLauncherTest.java index 5c21be712581a..8d5f0de28f20d 100644 --- a/test/jdk/tools/jpackage/share/AddLauncherTest.java +++ b/test/jdk/tools/jpackage/share/AddLauncherTest.java @@ -89,17 +89,17 @@ public void test() { new AdditionalLauncher("Baz2") .setDefaultArguments() - .addRawProperties(Map.entry("description", "Baz2 Description")) + .setProperty("description", "Baz2 Description") .applyTo(packageTest); new AdditionalLauncher("foo") .setDefaultArguments("yep!") - .addRawProperties(Map.entry("description", "foo Description")) + .setProperty("description", "foo Description") .applyTo(packageTest); new AdditionalLauncher("Bar") .setDefaultArguments("one", "two", "three") - .addRawProperties(Map.entry("description", "Bar Description")) + .setProperty("description", "Bar Description") .setIcon(GOLDEN_ICON) .applyTo(packageTest); @@ -194,8 +194,8 @@ public void testMainLauncherIsModular(boolean mainLauncherIsModular) { .toString(); new AdditionalLauncher("ModularAppLauncher") - .addRawProperties(Map.entry("module", expectedMod)) - .addRawProperties(Map.entry("main-jar", "")) + .setProperty("module", expectedMod) + .setProperty("main-jar", "") .applyTo(cmd); new AdditionalLauncher("NonModularAppLauncher") @@ -204,8 +204,8 @@ public void testMainLauncherIsModular(boolean mainLauncherIsModular) { .setPersistenceHandler((path, properties) -> TKit.createTextFile(path, properties.stream().map(entry -> String.join(" ", entry.getKey(), entry.getValue())))) - .addRawProperties(Map.entry("main-class", nonModularAppDesc.className())) - .addRawProperties(Map.entry("main-jar", nonModularAppDesc.jarFileName())) + .setProperty("main-class", nonModularAppDesc.className()) + .setProperty("main-jar", nonModularAppDesc.jarFileName()) .applyTo(cmd); cmd.executeAndAssertHelloAppImageCreated(); From 7131578b73aacd8659c25436df779d217d4ac3d7 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 26 Jul 2025 17:04:34 -0400 Subject: [PATCH 49/83] Use JPackageCommand.createMutableCopy() --- .../jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index a4c9311f041b1..3b8d484497262 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -87,7 +87,7 @@ static void verifyDeployedShortcuts(JPackageCommand cmd, boolean installed) { cmd.verifyIsOfType(PackageType.WINDOWS); verifyDeployedShortcutsInternal(cmd, installed); - var copyCmd = new JPackageCommand(cmd); + var copyCmd = cmd.createMutableCopy(); if (copyCmd.hasArgument("--win-per-user-install")) { copyCmd.removeArgument("--win-per-user-install"); } else { From adc8e146ec15eee6359ced53ece4c98c6fe48413 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 26 Jul 2025 22:04:30 -0400 Subject: [PATCH 50/83] Decouple additional launcher configuration and verification in the AdditionalLauncher. Introduce LauncherShortcut, LauncherVerifier, LauncherVerifier verifies attributes of any launcher - the main or additional. Add JPackageCommand.createMutableCopy(), hide the copy ctor. --- .../jdk/jpackage/test/AdditionalLauncher.java | 354 +++++------------- .../jdk/jpackage/test/JPackageCommand.java | 38 +- .../jdk/jpackage/test/LauncherShortcut.java | 156 ++++++++ .../jdk/jpackage/test/LauncherVerifier.java | 326 ++++++++++++++++ .../jdk/jpackage/test/PackageTest.java | 18 +- 5 files changed, 596 insertions(+), 296 deletions(-) create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java index fa847d74058b3..c2d2ad0f407a0 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java @@ -22,40 +22,54 @@ */ package jdk.jpackage.test; -import static java.util.stream.Collectors.toMap; import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; +import static jdk.jpackage.test.LauncherShortcut.LINUX_SHORTCUT; +import static jdk.jpackage.test.LauncherShortcut.WIN_DESKTOP_SHORTCUT; +import static jdk.jpackage.test.LauncherShortcut.WIN_START_MENU_SHORTCUT; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Properties; +import java.util.Set; import java.util.function.BiConsumer; -import java.util.function.Supplier; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; import jdk.jpackage.internal.util.function.ThrowingBiConsumer; +import jdk.jpackage.internal.util.function.ThrowingConsumer; +import jdk.jpackage.test.LauncherShortcut.StartupDirectory; +import jdk.jpackage.test.LauncherVerifier.Action; -public class AdditionalLauncher { +public final class AdditionalLauncher { public AdditionalLauncher(String name) { - this.name = name; - this.rawProperties = new ArrayList<>(); + this.name = Objects.requireNonNull(name); setPersistenceHandler(null); } - public final AdditionalLauncher setDefaultArguments(String... v) { + public AdditionalLauncher withVerifyActions(Action... actions) { + verifyActions.addAll(List.of(actions)); + return this; + } + + public AdditionalLauncher withoutVerifyActions(Action... actions) { + verifyActions.removeAll(List.of(actions)); + return this; + } + + public AdditionalLauncher setDefaultArguments(String... v) { defaultArguments = new ArrayList<>(List.of(v)); return this; } - public final AdditionalLauncher addDefaultArguments(String... v) { + public AdditionalLauncher addDefaultArguments(String... v) { if (defaultArguments == null) { return setDefaultArguments(v); } @@ -64,12 +78,12 @@ public final AdditionalLauncher addDefaultArguments(String... v) { return this; } - public final AdditionalLauncher setJavaOptions(String... v) { + public AdditionalLauncher setJavaOptions(String... v) { javaOptions = new ArrayList<>(List.of(v)); return this; } - public final AdditionalLauncher addJavaOptions(String... v) { + public AdditionalLauncher addJavaOptions(String... v) { if (javaOptions == null) { return setJavaOptions(v); } @@ -78,51 +92,46 @@ public final AdditionalLauncher addJavaOptions(String... v) { return this; } - public final AdditionalLauncher setVerifyUninstalled(boolean value) { - verifyUninstalled = value; + public AdditionalLauncher setProperty(String name, Object value) { + rawProperties.put(Objects.requireNonNull(name), Objects.requireNonNull(value.toString())); return this; } - public final AdditionalLauncher setLauncherAsService() { - return addRawProperties(LAUNCHER_AS_SERVICE); - } - - public final AdditionalLauncher addRawProperties( - Map.Entry v) { - return addRawProperties(List.of(v)); - } - - public final AdditionalLauncher addRawProperties( - Map.Entry v, Map.Entry v2) { - return addRawProperties(List.of(v, v2)); - } - - public final AdditionalLauncher addRawProperties( - Collection> v) { - rawProperties.addAll(v); + public AdditionalLauncher setShortcuts(boolean menu, boolean desktop) { + if (TKit.isLinux()) { + setShortcut(LINUX_SHORTCUT, desktop); + } else if (TKit.isWindows()) { + setShortcut(WIN_DESKTOP_SHORTCUT, desktop); + setShortcut(WIN_START_MENU_SHORTCUT, desktop); + } return this; } - public final String getRawPropertyValue( - String key, Supplier getDefault) { - return rawProperties.stream() - .filter(item -> item.getKey().equals(key)) - .map(e -> e.getValue()).findAny().orElseGet(getDefault); + public AdditionalLauncher setShortcut(LauncherShortcut shortcut, StartupDirectory value) { + if (value != null) { + setProperty(shortcut.propertyName(), value.asStringValue()); + } else { + setProperty(shortcut.propertyName(), false); + } + return this; } - private String getDesciption(JPackageCommand cmd) { - return getRawPropertyValue("description", () -> cmd.getArgumentValue( - "--description", unused -> cmd.name())); + public AdditionalLauncher setShortcut(LauncherShortcut shortcut, boolean value) { + if (value) { + setShortcut(shortcut, StartupDirectory.DEFAULT); + } else { + setShortcut(shortcut, null); + } + return this; } - public final AdditionalLauncher setShortcuts(boolean menu, boolean shortcut) { - withMenuShortcut = menu; - withShortcut = shortcut; + public AdditionalLauncher removeShortcut(LauncherShortcut shortcut) { + rawProperties.remove(shortcut.propertyName()); return this; } - public final AdditionalLauncher setIcon(Path iconPath) { - if (iconPath == NO_ICON) { + public AdditionalLauncher setIcon(Path iconPath) { + if (iconPath.equals(NO_ICON)) { throw new IllegalArgumentException(); } @@ -130,13 +139,13 @@ public final AdditionalLauncher setIcon(Path iconPath) { return this; } - public final AdditionalLauncher setNoIcon() { + public AdditionalLauncher setNoIcon() { icon = NO_ICON; return this; } - public final AdditionalLauncher setPersistenceHandler( - ThrowingBiConsumer>> handler) { + public AdditionalLauncher setPersistenceHandler( + ThrowingBiConsumer>> handler) { if (handler != null) { createFileHandler = ThrowingBiConsumer.toBiConsumer(handler); } else { @@ -145,21 +154,31 @@ public final AdditionalLauncher setPersistenceHandler( return this; } - public final void applyTo(JPackageCommand cmd) { + public void applyTo(JPackageCommand cmd) { cmd.addPrerequisiteAction(this::initialize); - cmd.addVerifyAction(this::verify); + cmd.addVerifyAction(createVerifierAsConsumer()); } - public final void applyTo(PackageTest test) { + public void applyTo(PackageTest test) { test.addInitializer(this::initialize); - test.addInstallVerifier(this::verify); - if (verifyUninstalled) { - test.addUninstallVerifier(this::verifyUninstalled); - } + test.addInstallVerifier(createVerifierAsConsumer()); } public final void verifyRemovedInUpgrade(PackageTest test) { - test.addInstallVerifier(this::verifyUninstalled); + test.addInstallVerifier(cmd -> { + createVerifier().verify(cmd, LauncherVerifier.Action.VERIFY_UNINSTALLED); + }); + } + + private LauncherVerifier createVerifier() { + return new LauncherVerifier(name, Optional.ofNullable(javaOptions), + Optional.ofNullable(defaultArguments), Optional.ofNullable(icon), rawProperties); + } + + private ThrowingConsumer createVerifierAsConsumer() { + return cmd -> { + createVerifier().verify(cmd, verifyActions.stream().sorted(Comparator.comparing(Action::ordinal)).toArray(Action[]::new)); + }; } static void forEachAdditionalLauncher(JPackageCommand cmd, @@ -193,208 +212,35 @@ private void initialize(JPackageCommand cmd) throws IOException { cmd.addArguments("--add-launcher", String.format("%s=%s", name, propsFile)); - List> properties = new ArrayList<>(); + Map properties = new HashMap<>(); if (defaultArguments != null) { - properties.add(Map.entry("arguments", - JPackageCommand.escapeAndJoin(defaultArguments))); + properties.put("arguments", JPackageCommand.escapeAndJoin(defaultArguments)); } if (javaOptions != null) { - properties.add(Map.entry("java-options", - JPackageCommand.escapeAndJoin(javaOptions))); + properties.put("java-options", JPackageCommand.escapeAndJoin(javaOptions)); } if (icon != null) { final String iconPath; - if (icon == NO_ICON) { + if (icon.equals(NO_ICON)) { iconPath = ""; } else { iconPath = icon.toAbsolutePath().toString().replace('\\', '/'); } - properties.add(Map.entry("icon", iconPath)); - } - - if (withShortcut != null) { - if (TKit.isLinux()) { - properties.add(Map.entry("linux-shortcut", withShortcut.toString())); - } else if (TKit.isWindows()) { - properties.add(Map.entry("win-shortcut", withShortcut.toString())); - } - } - - if (TKit.isWindows() && withMenuShortcut != null) { - properties.add(Map.entry("win-menu", withMenuShortcut.toString())); - } - - properties.addAll(rawProperties); - - createFileHandler.accept(propsFile, properties); - } - - private static Path iconInResourceDir(JPackageCommand cmd, - String launcherName) { - Path resourceDir = cmd.getArgumentValue("--resource-dir", () -> null, - Path::of); - if (resourceDir != null) { - Path icon = resourceDir.resolve( - Optional.ofNullable(launcherName).orElseGet(() -> cmd.name()) - + TKit.ICON_SUFFIX); - if (Files.exists(icon)) { - return icon; - } - } - return null; - } - - private void verifyIcon(JPackageCommand cmd) throws IOException { - var verifier = new LauncherIconVerifier().setLauncherName(name); - - if (TKit.isOSX()) { - // On Mac should be no icon files for additional launchers. - verifier.applyTo(cmd); - return; - } - - boolean withLinuxDesktopFile = false; - - final Path effectiveIcon = Optional.ofNullable(icon).orElseGet( - () -> iconInResourceDir(cmd, name)); - while (effectiveIcon != NO_ICON) { - if (effectiveIcon != null) { - withLinuxDesktopFile = Boolean.FALSE != withShortcut; - verifier.setExpectedIcon(effectiveIcon); - break; - } - - Path customMainLauncherIcon = cmd.getArgumentValue("--icon", - () -> iconInResourceDir(cmd, null), Path::of); - if (customMainLauncherIcon != null) { - withLinuxDesktopFile = Boolean.FALSE != withShortcut; - verifier.setExpectedIcon(customMainLauncherIcon); - break; - } - - verifier.setExpectedDefaultIcon(); - break; + properties.put("icon", iconPath); } - if (TKit.isLinux() && !cmd.isImagePackageType()) { - if (effectiveIcon != NO_ICON && !withLinuxDesktopFile) { - withLinuxDesktopFile = (Boolean.FALSE != withShortcut) && - Stream.of("--linux-shortcut").anyMatch(cmd::hasArgument); - verifier.setExpectedDefaultIcon(); - } - Path desktopFile = LinuxHelper.getDesktopFile(cmd, name); - if (withLinuxDesktopFile) { - TKit.assertFileExists(desktopFile); - } else { - TKit.assertPathExists(desktopFile, false); - } - } + properties.putAll(rawProperties); - verifier.applyTo(cmd); - } - - private void verifyShortcuts(JPackageCommand cmd) throws IOException { - if (TKit.isLinux() && !cmd.isImagePackageType() - && withShortcut != null) { - Path desktopFile = LinuxHelper.getDesktopFile(cmd, name); - if (withShortcut) { - TKit.assertFileExists(desktopFile); - } else { - TKit.assertPathExists(desktopFile, false); - } - } - } - - private void verifyDescription(JPackageCommand cmd) throws IOException { - if (TKit.isWindows()) { - String expectedDescription = getDesciption(cmd); - Path launcherPath = cmd.appLauncherPath(name); - String actualDescription = - WindowsHelper.getExecutableDesciption(launcherPath); - TKit.assertEquals(expectedDescription, actualDescription, - String.format("Check file description of [%s]", launcherPath)); - } else if (TKit.isLinux() && !cmd.isImagePackageType()) { - String expectedDescription = getDesciption(cmd); - Path desktopFile = LinuxHelper.getDesktopFile(cmd, name); - if (Files.exists(desktopFile)) { - TKit.assertTextStream("Comment=" + expectedDescription) - .label(String.format("[%s] file", desktopFile)) - .predicate(String::equals) - .apply(Files.readAllLines(desktopFile)); - } - } - } - - private void verifyInstalled(JPackageCommand cmd, boolean installed) throws IOException { - if (TKit.isLinux() && !cmd.isImagePackageType() && !cmd. - isPackageUnpacked(String.format( - "Not verifying package and system .desktop files for [%s] launcher", - cmd.appLauncherPath(name)))) { - Path packageDesktopFile = LinuxHelper.getDesktopFile(cmd, name); - Path systemDesktopFile = LinuxHelper.getSystemDesktopFilesFolder(). - resolve(packageDesktopFile.getFileName()); - if (Files.exists(packageDesktopFile) && installed) { - TKit.assertFileExists(systemDesktopFile); - TKit.assertStringListEquals(Files.readAllLines( - packageDesktopFile), - Files.readAllLines(systemDesktopFile), String.format( - "Check [%s] and [%s] files are equal", - packageDesktopFile, - systemDesktopFile)); - } else { - TKit.assertPathExists(packageDesktopFile, false); - TKit.assertPathExists(systemDesktopFile, false); - } - } - } - - protected void verifyUninstalled(JPackageCommand cmd) throws IOException { - verifyInstalled(cmd, false); - Path launcherPath = cmd.appLauncherPath(name); - TKit.assertPathExists(launcherPath, false); - } - - protected void verify(JPackageCommand cmd) throws IOException { - verifyIcon(cmd); - verifyShortcuts(cmd); - verifyDescription(cmd); - verifyInstalled(cmd, true); - - Path launcherPath = cmd.appLauncherPath(name); - - TKit.assertExecutableFileExists(launcherPath); - - if (!cmd.canRunLauncher(String.format( - "Not running %s launcher", launcherPath))) { - return; - } - - var appVerifier = HelloApp.assertApp(launcherPath) - .addDefaultArguments(Optional - .ofNullable(defaultArguments) - .orElseGet(() -> List.of(cmd.getAllArgumentValues("--arguments")))) - .addJavaOptions(Optional - .ofNullable(javaOptions) - .orElseGet(() -> List.of(cmd.getAllArgumentValues( - "--java-options"))).stream().map( - str -> resolveVariables(cmd, str)).toList()); - - if (!rawProperties.contains(LAUNCHER_AS_SERVICE)) { - appVerifier.executeAndVerifyOutput(); - } else if (!cmd.isPackageUnpacked(String.format( - "Not verifying contents of test output file for [%s] launcher", - launcherPath))) { - appVerifier.verifyOutput(); - } + createFileHandler.accept(propsFile, properties.entrySet()); } public static final class PropertyFile { PropertyFile(Map data) { this.data = new Properties(); - data.putAll(data); + this.data.putAll(data); } PropertyFile(Path path) throws IOException { @@ -404,45 +250,25 @@ public static final class PropertyFile { } } - public boolean isPropertySet(String name) { - Objects.requireNonNull(name); - return data.containsKey(name); - } - - public Optional findPropertyValue(String name) { + public Optional findProperty(String name) { Objects.requireNonNull(name); return Optional.ofNullable(data.getProperty(name)); } - public Optional findPropertyBooleanValue(String name) { - return findPropertyValue(name).map(Boolean::parseBoolean); + public Optional findBooleanProperty(String name) { + return findProperty(name).map(Boolean::parseBoolean); } private final Properties data; } - private static String resolveVariables(JPackageCommand cmd, String str) { - var map = Stream.of(JPackageCommand.Macro.values()).collect(toMap(x -> { - return String.format("$%s", x.name()); - }, cmd::macroValue)); - for (var e : map.entrySet()) { - str = str.replaceAll(Pattern.quote(e.getKey()), - Matcher.quoteReplacement(e.getValue().toString())); - } - return str; - } - - private boolean verifyUninstalled; private List javaOptions; private List defaultArguments; private Path icon; private final String name; - private final List> rawProperties; - private BiConsumer>> createFileHandler; - private Boolean withMenuShortcut; - private Boolean withShortcut; - - private static final Path NO_ICON = Path.of(""); - private static final Map.Entry LAUNCHER_AS_SERVICE = Map.entry( - "launcher-as-service", "true"); + private final Map rawProperties = new HashMap<>(); + private BiConsumer>> createFileHandler; + private final Set verifyActions = new HashSet<>(Action.VERIFY_DEFAULTS); + + static final Path NO_ICON = Path.of(""); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 559a86f0ba9e9..674e05f4ba129 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -72,7 +72,7 @@ public JPackageCommand() { verifyActions = new Actions(); } - public JPackageCommand(JPackageCommand cmd) { + private JPackageCommand(JPackageCommand cmd, boolean immutable) { args.addAll(cmd.args); withToolProvider = cmd.withToolProvider; saveConsoleOutput = cmd.saveConsoleOutput; @@ -81,7 +81,7 @@ public JPackageCommand(JPackageCommand cmd) { suppressOutput = cmd.suppressOutput; ignoreDefaultRuntime = cmd.ignoreDefaultRuntime; ignoreDefaultVerbose = cmd.ignoreDefaultVerbose; - immutable = cmd.immutable; + this.immutable = immutable; dmgInstallDir = cmd.dmgInstallDir; prerequisiteActions = new Actions(cmd.prerequisiteActions); verifyActions = new Actions(cmd.verifyActions); @@ -93,9 +93,11 @@ public JPackageCommand(JPackageCommand cmd) { } JPackageCommand createImmutableCopy() { - JPackageCommand reply = new JPackageCommand(this); - reply.immutable = true; - return reply; + return new JPackageCommand(this, true); + } + + JPackageCommand createMutableCopy() { + return new JPackageCommand(this, false); } public JPackageCommand setArgumentValue(String argName, String newValue) { @@ -789,11 +791,6 @@ public JPackageCommand executePrerequisiteActions() { return this; } - public JPackageCommand executeVerifyActions() { - verifyActions.run(); - return this; - } - private Executor createExecutor() { Executor exec = new Executor() .saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput) @@ -858,7 +855,7 @@ public Executor.Result execute(int expectedExitCode) { ConfigFilesStasher.INSTANCE.accept(this); } - final var copy = new JPackageCommand(this).adjustArgumentsBeforeExecution(); + final var copy = createMutableCopy().adjustArgumentsBeforeExecution(); final var directoriesAssert = new ReadOnlyPathsAssert(copy); @@ -875,7 +872,7 @@ public Executor.Result execute(int expectedExitCode) { } if (result.exitCode() == 0) { - executeVerifyActions(); + verifyActions.run(); } return result; @@ -883,7 +880,7 @@ public Executor.Result execute(int expectedExitCode) { public Executor.Result executeAndAssertHelloAppImageCreated() { Executor.Result result = executeAndAssertImageCreated(); - HelloApp.executeLauncherAndVerifyOutput(this); + LauncherVerifier.executeMainLauncherAndVerifyOutput(this); return result; } @@ -1059,18 +1056,19 @@ public JPackageCommand excludeReadOnlyPathAssert(ReadOnlyPathAssert... asserts) public static enum AppLayoutAssert { APP_IMAGE_FILE(JPackageCommand::assertAppImageFile), PACKAGE_FILE(JPackageCommand::assertPackageFile), - MAIN_LAUNCHER(cmd -> { + NO_MAIN_LAUNCHER_IN_RUNTIME(cmd -> { if (cmd.isRuntime()) { TKit.assertPathExists(convertFromRuntime(cmd).appLauncherPath(), false); - } else { - TKit.assertExecutableFileExists(cmd.appLauncherPath()); } }), - MAIN_LAUNCHER_CFG_FILE(cmd -> { + NO_MAIN_LAUNCHER_CFG_FILE_IN_RUNTIME(cmd -> { if (cmd.isRuntime()) { TKit.assertPathExists(convertFromRuntime(cmd).appLauncherCfgPath(null), false); - } else { - TKit.assertFileExists(cmd.appLauncherCfgPath(null)); + } + }), + MAIN_LAUNCHER_FILES(cmd -> { + if (!cmd.isRuntime()) { + new LauncherVerifier(cmd).verify(cmd, LauncherVerifier.Action.VERIFY_INSTALLED); } }), MAIN_JAR_FILE(cmd -> { @@ -1097,7 +1095,7 @@ public static enum AppLayoutAssert { } private static JPackageCommand convertFromRuntime(JPackageCommand cmd) { - var copy = new JPackageCommand(cmd); + var copy = cmd.createMutableCopy(); copy.immutable = false; copy.removeArgumentWithValue("--runtime-image"); copy.dmgInstallDir = cmd.appInstallationDirectory(); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java new file mode 100644 index 0000000000000..6fb5c6e14a894 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025, 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.test; + +import static java.util.stream.Collectors.toMap; +import static jdk.jpackage.test.AdditionalLauncher.getAdditionalLauncherProperties; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; +import jdk.jpackage.test.AdditionalLauncher.PropertyFile; + +public enum LauncherShortcut { + + LINUX_SHORTCUT("linux-shortcut"), + + WIN_DESKTOP_SHORTCUT("win-shortcut"), + + WIN_START_MENU_SHORTCUT("win-menu"); + + public enum StartupDirectory { + DEFAULT("true"), + ; + + StartupDirectory(String stringValue) { + this.stringValue = Objects.requireNonNull(stringValue); + } + + public String asStringValue() { + return stringValue; + } + + /** + * Returns shortcut startup directory or an empty {@link Optional} instance if + * the value of the {@code str} parameter evaluates to {@code false}. + * + * @param str the value of a shortcut startup directory + * @return shortcut startup directory or an empty {@link Optional} instance + * @throws IllegalArgumentException if the value of the {@code str} parameter is + * unrecognized + */ + static Optional parse(String str) { + Objects.requireNonNull(str); + return Optional.ofNullable(VALUE_MAP.get(str)).or(() -> { + if (Boolean.TRUE.toString().equals(str)) { + return Optional.of(StartupDirectory.DEFAULT); + } else if (Boolean.FALSE.toString().equals(str)) { + return Optional.empty(); + } else { + throw new IllegalArgumentException(String.format( + "Unrecognized launcher shortcut startup directory: [%s]", str)); + } + }); + } + + private final String stringValue; + + private final static Map VALUE_MAP = + Stream.of(values()).collect(toMap(StartupDirectory::asStringValue, x -> x)); + } + + LauncherShortcut(String propertyName) { + this.propertyName = Objects.requireNonNull(propertyName); + } + + public String propertyName() { + return propertyName; + } + + public String optionName() { + return "--" + propertyName; + } + + Optional expectShortcut(JPackageCommand cmd, Optional predefinedAppImage, String launcherName) { + Objects.requireNonNull(predefinedAppImage); + + final var name = Optional.ofNullable(launcherName).orElseGet(cmd::name); + + if (name.equals(cmd.name())) { + return findMainLauncherShortcut(cmd); + } else { + return findAddLauncherShortcut(cmd, predefinedAppImage.map(appImage -> { + return new PropertyFile(appImage.addLaunchers().get(launcherName)); + }).orElseGet(() -> { + return getAdditionalLauncherProperties(cmd, launcherName); + })::findProperty); + } + } + + + public interface InvokeShortcutSpec { + String launcherName(); + LauncherShortcut shortcut(); + Optional expectedWorkDirectory(); + List commandLine(); + + record Stub( + String launcherName, + LauncherShortcut shortcut, + Optional expectedWorkDirectory, + List commandLine) implements InvokeShortcutSpec { + + public Stub { + Objects.requireNonNull(launcherName); + Objects.requireNonNull(shortcut); + Objects.requireNonNull(expectedWorkDirectory); + Objects.requireNonNull(commandLine); + } + } + } + + + private Optional findMainLauncherShortcut(JPackageCommand cmd) { + if (cmd.hasArgument(optionName())) { + return Optional.of(StartupDirectory.DEFAULT); + } else { + return Optional.empty(); + } + } + + private Optional findAddLauncherShortcut(JPackageCommand cmd, + Function> addlauncherProperties) { + var explicit = addlauncherProperties.apply(propertyName()); + if (explicit.isPresent()) { + return explicit.flatMap(StartupDirectory::parse); + } else { + return findMainLauncherShortcut(cmd); + } + } + + private final String propertyName; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java new file mode 100644 index 0000000000000..55f7a1b93145f --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java @@ -0,0 +1,326 @@ +/* + * Copyright (c) 2025, 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.test; + +import static java.util.stream.Collectors.toMap; +import static jdk.jpackage.test.AdditionalLauncher.NO_ICON; +import static jdk.jpackage.test.LauncherShortcut.LINUX_SHORTCUT; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import jdk.jpackage.internal.util.function.ThrowingBiConsumer; +import jdk.jpackage.test.AdditionalLauncher.PropertyFile; +import jdk.jpackage.test.LauncherShortcut.StartupDirectory; + +public final class LauncherVerifier { + + LauncherVerifier(JPackageCommand cmd) { + name = cmd.name(); + javaOptions = Optional.empty(); + arguments = Optional.empty(); + icon = Optional.empty(); + properties = Optional.empty(); + } + + LauncherVerifier(String name, + Optional> javaOptions, + Optional> arguments, + Optional icon, + Map properties) { + this.name = Objects.requireNonNull(name); + this.javaOptions = javaOptions.map(List::copyOf); + this.arguments = arguments.map(List::copyOf); + this.icon = icon; + this.properties = Optional.of(new PropertyFile(properties)); + } + + static void executeMainLauncherAndVerifyOutput(JPackageCommand cmd) { + new LauncherVerifier(cmd).verify(cmd, Action.EXECUTE_LAUNCHER); + } + + + public enum Action { + VERIFY_ICON(LauncherVerifier::verifyIcon), + VERIFY_DESCRIPTION(LauncherVerifier::verifyDescription), + VERIFY_INSTALLED((verifier, cmd) -> { + verifier.verifyInstalled(cmd, true); + }), + VERIFY_UNINSTALLED((verifier, cmd) -> { + verifier.verifyInstalled(cmd, false); + }), + EXECUTE_LAUNCHER(LauncherVerifier::executeLauncher), + ; + + Action(ThrowingBiConsumer action) { + this.action = ThrowingBiConsumer.toBiConsumer(action); + } + + private void apply(LauncherVerifier verifier, JPackageCommand cmd) { + action.accept(verifier, cmd); + } + + private final BiConsumer action; + + static final List VERIFY_APP_IMAGE = List.of( + VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED + ); + + static final List VERIFY_DEFAULTS = Stream.concat( + VERIFY_APP_IMAGE.stream(), Stream.of(EXECUTE_LAUNCHER) + ).toList(); + } + + + void verify(JPackageCommand cmd, Action... actions) { + verify(cmd, List.of(actions)); + } + + void verify(JPackageCommand cmd, Iterable actions) { + Objects.requireNonNull(cmd); + for (var a : actions) { + a.apply(this, cmd); + } + } + + private boolean isMainLauncher() { + return properties.isEmpty(); + } + + private Optional findProperty(String key) { + return properties.flatMap(v -> { + return v.findProperty(key); + }); + } + + private String getDescription(JPackageCommand cmd) { + return findProperty("description").orElseGet(() -> { + return cmd.getArgumentValue("--description", cmd::name); + }); + } + + private List getArguments(JPackageCommand cmd) { + return getStringArrayProperty(cmd, "--arguments", arguments); + } + + private List getJavaOptions(JPackageCommand cmd) { + return getStringArrayProperty(cmd, "--java-options", javaOptions); + } + + private List getStringArrayProperty(JPackageCommand cmd, String optionName, Optional> items) { + Objects.requireNonNull(cmd); + Objects.requireNonNull(optionName); + Objects.requireNonNull(items); + if (isMainLauncher()) { + return List.of(cmd.getAllArgumentValues(optionName)); + } else { + return items.orElseGet(() -> { + return List.of(cmd.getAllArgumentValues(optionName)); + }); + } + } + + private boolean explicitlyNoShortcut(LauncherShortcut shortcut) { + var explicit = findProperty(shortcut.propertyName()); + if (explicit.isPresent()) { + return explicit.flatMap(StartupDirectory::parse).isEmpty(); + } else { + return false; + } + } + + private static boolean explicitShortcutForMainLauncher(JPackageCommand cmd, LauncherShortcut shortcut) { + return cmd.hasArgument(shortcut.optionName()); + } + + private void verifyIcon(JPackageCommand cmd) throws IOException { + initIconVerifier(cmd).applyTo(cmd); + } + + private LauncherIconVerifier initIconVerifier(JPackageCommand cmd) { + var verifier = new LauncherIconVerifier().setLauncherName(name); + + var mainLauncherIcon = Optional.ofNullable(cmd.getArgumentValue("--icon")).map(Path::of).or(() -> { + return iconInResourceDir(cmd, cmd.name()); + }); + + if (TKit.isOSX()) { + // There should be no icon files on Mac for additional launchers, + // and always an icon file for the main launcher. + if (isMainLauncher()) { + mainLauncherIcon.ifPresentOrElse(verifier::setExpectedIcon, verifier::setExpectedDefaultIcon); + } + return verifier; + } + + if (isMainLauncher()) { + mainLauncherIcon.ifPresentOrElse(verifier::setExpectedIcon, verifier::setExpectedDefaultIcon); + } else { + icon.ifPresentOrElse(icon -> { + if (!NO_ICON.equals(icon)) { + verifier.setExpectedIcon(icon); + } + }, () -> { + // No "icon" property in the property file + iconInResourceDir(cmd, name).ifPresentOrElse(verifier::setExpectedIcon, () -> { + // No icon for this additional launcher in the resource directory. + mainLauncherIcon.ifPresentOrElse(verifier::setExpectedIcon, verifier::setExpectedDefaultIcon); + }); + }); + } + + return verifier; + } + + private static boolean withLinuxMainLauncherDesktopFile(JPackageCommand cmd) { + if (!TKit.isLinux() || cmd.isImagePackageType()) { + return false; + } + + return explicitShortcutForMainLauncher(cmd, LINUX_SHORTCUT) + || cmd.hasArgument("--icon") + || cmd.hasArgument("--file-associations") + || iconInResourceDir(cmd, cmd.name()).isPresent(); + } + + private boolean withLinuxDesktopFile(JPackageCommand cmd) { + if (!TKit.isLinux() || cmd.isImagePackageType()) { + return false; + } + + if (isMainLauncher()) { + return withLinuxMainLauncherDesktopFile(cmd); + } else if (explicitlyNoShortcut(LINUX_SHORTCUT) || icon.map(icon -> { + return icon.equals(NO_ICON); + }).orElse(false)) { + return false; + } else if (iconInResourceDir(cmd, name).isPresent() || icon.map(icon -> { + return !icon.equals(NO_ICON); + }).orElse(false)) { + return true; + } else if (findProperty(LINUX_SHORTCUT.propertyName()).flatMap(StartupDirectory::parse).isPresent()) { + return true; + } else { + return withLinuxMainLauncherDesktopFile(cmd.createMutableCopy().removeArgument("--file-associations")); + } + } + + private void verifyDescription(JPackageCommand cmd) throws IOException { + if (TKit.isWindows()) { + String expectedDescription = getDescription(cmd); + Path launcherPath = cmd.appLauncherPath(name); + String actualDescription = + WindowsHelper.getExecutableDescription(launcherPath); + TKit.assertEquals(expectedDescription, actualDescription, + String.format("Check file description of [%s]", launcherPath)); + } else if (TKit.isLinux() && !cmd.isImagePackageType()) { + String expectedDescription = getDescription(cmd); + Path desktopFile = LinuxHelper.getDesktopFile(cmd, name); + if (Files.exists(desktopFile)) { + TKit.assertTextStream("Comment=" + expectedDescription) + .label(String.format("[%s] file", desktopFile)) + .predicate(String::equals) + .apply(Files.readAllLines(desktopFile)); + } + } + } + + private void verifyInstalled(JPackageCommand cmd, boolean installed) throws IOException { + var launcherPath = cmd.appLauncherPath(name); + var launcherCfgFilePath = cmd.appLauncherCfgPath(name); + if (installed) { + TKit.assertExecutableFileExists(launcherPath); + TKit.assertFileExists(launcherCfgFilePath); + } else { + TKit.assertPathExists(launcherPath, false); + TKit.assertPathExists(launcherCfgFilePath, false); + } + + if (TKit.isLinux() && !cmd.isImagePackageType()) { + final var packageDesktopFile = LinuxHelper.getDesktopFile(cmd, name); + final var withLinuxDesktopFile = withLinuxDesktopFile(cmd) && installed; + if (withLinuxDesktopFile) { + TKit.assertFileExists(packageDesktopFile); + } else { + TKit.assertPathExists(packageDesktopFile, false); + } + } + + if (installed) { + initIconVerifier(cmd).verifyFileInAppImageOnly(true).applyTo(cmd); + } + } + + private void executeLauncher(JPackageCommand cmd) throws IOException { + Path launcherPath = cmd.appLauncherPath(name); + + if (!cmd.canRunLauncher(String.format("Not running %s launcher", launcherPath))) { + return; + } + + var appVerifier = HelloApp.assertApp(launcherPath) + .addDefaultArguments(getArguments(cmd)) + .addJavaOptions(getJavaOptions(cmd).stream().map(str -> { + return resolveVariables(cmd, str); + }).toList()); + + appVerifier.executeAndVerifyOutput(); + } + + private static String resolveVariables(JPackageCommand cmd, String str) { + var map = Stream.of(JPackageCommand.Macro.values()).collect(toMap(x -> { + return String.format("$%s", x.name()); + }, cmd::macroValue)); + for (var e : map.entrySet()) { + str = str.replaceAll(Pattern.quote(e.getKey()), + Matcher.quoteReplacement(e.getValue().toString())); + } + return str; + } + + private static Optional iconInResourceDir(JPackageCommand cmd, String launcherName) { + Objects.requireNonNull(launcherName); + return Optional.ofNullable(cmd.getArgumentValue("--resource-dir")).map(Path::of).map(resourceDir -> { + Path icon = resourceDir.resolve(launcherName + TKit.ICON_SUFFIX); + if (Files.exists(icon)) { + return icon; + } else { + return null; + } + }); + } + + private final String name; + private final Optional> javaOptions; + private final Optional> arguments; + private final Optional icon; + private final Optional properties; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index 9642b3d943c4a..2ee5e79f07cf8 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -271,8 +271,7 @@ static void withFileAssociationsTestRuns(FileAssociations fa, PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa) { Objects.requireNonNull(fa); - // Setup test app to have valid jpackage command line before - // running check of type of environment. + // Setup test app to have valid jpackage command line before running the check. addHelloAppInitializer(null); forTypes(LINUX, () -> { @@ -361,15 +360,14 @@ public PackageTest configureHelloApp() { public PackageTest configureHelloApp(String javaAppDesc) { addHelloAppInitializer(javaAppDesc); - addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput); + addInstallVerifier(JPackageCommand::executeLaunchers); return this; } public PackageTest addHelloAppInitializer(String javaAppDesc) { - addInitializer( - cmd -> new HelloApp(JavaAppDesc.parse(javaAppDesc)).addTo(cmd), - "HelloApp"); - return this; + return addInitializer(cmd -> { + new HelloApp(JavaAppDesc.parse(javaAppDesc)).addTo(cmd); + }, "HelloApp"); } public static class Group extends RunnablePackageTest { @@ -612,11 +610,7 @@ private ActionAction analizeAction(Action action) { } } case VERIFY_INSTALL -> { - if (unpackNotSupported()) { - return ActionAction.SKIP; - } - - if (installFailed()) { + if (unpackNotSupported() || installFailed()) { return ActionAction.SKIP; } } From 512f9f99d0a5fd7273ce8a817356441214a4a44c Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 26 Jul 2025 17:06:03 -0400 Subject: [PATCH 51/83] PerUserCfgTest: follow-up changes in AdditionalLauncher --- test/jdk/tools/jpackage/share/PerUserCfgTest.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/test/jdk/tools/jpackage/share/PerUserCfgTest.java b/test/jdk/tools/jpackage/share/PerUserCfgTest.java index 080df1f959d3e..d2f368cd8243e 100644 --- a/test/jdk/tools/jpackage/share/PerUserCfgTest.java +++ b/test/jdk/tools/jpackage/share/PerUserCfgTest.java @@ -27,13 +27,14 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import jdk.jpackage.internal.util.function.ThrowingConsumer; import jdk.jpackage.test.AdditionalLauncher; -import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.Annotations.Test; -import jdk.jpackage.internal.util.function.ThrowingConsumer; import jdk.jpackage.test.HelloApp; import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.LauncherVerifier.Action; import jdk.jpackage.test.LinuxHelper; +import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.PackageType; import jdk.jpackage.test.TKit; @@ -65,7 +66,7 @@ public static void test() throws IOException { cfgCmd.execute(); - new PackageTest().configureHelloApp().addInstallVerifier(cmd -> { + new PackageTest().addHelloAppInitializer(null).addInstallVerifier(cmd -> { if (cmd.isPackageUnpacked("Not running per-user configuration tests")) { return; } @@ -144,10 +145,7 @@ public static void test() throws IOException { } private static void addLauncher(JPackageCommand cmd, String name) { - new AdditionalLauncher(name) { - @Override - protected void verify(JPackageCommand cmd) {} - }.setDefaultArguments(name).applyTo(cmd); + new AdditionalLauncher(name).setDefaultArguments(name).withoutVerifyActions(Action.EXECUTE_LAUNCHER).applyTo(cmd); } private static Path getUserHomeDir() { From 1d54092a753c67f782047e0240c5d05c762d97da Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 26 Jul 2025 22:11:39 -0400 Subject: [PATCH 52/83] LauncherAsServiceVerifier: follow-up for the changes in the AdditionalLauncher --- .../test/LauncherAsServiceVerifier.java | 95 ++++++++----------- 1 file changed, 39 insertions(+), 56 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java index fd8b4011341fb..428218228942b 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java @@ -22,11 +22,19 @@ */ package jdk.jpackage.test; +import static jdk.jpackage.internal.util.function.ThrowingBiConsumer.toBiConsumer; +import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; +import static jdk.jpackage.test.AdditionalLauncher.forEachAdditionalLauncher; +import static jdk.jpackage.test.PackageType.LINUX; +import static jdk.jpackage.test.PackageType.MAC_PKG; +import static jdk.jpackage.test.PackageType.WINDOWS; + import java.io.IOException; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -36,12 +44,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.internal.util.PathUtils; -import jdk.jpackage.internal.util.function.ThrowingBiConsumer; -import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; import jdk.jpackage.internal.util.function.ThrowingRunnable; -import static jdk.jpackage.test.PackageType.LINUX; -import static jdk.jpackage.test.PackageType.MAC_PKG; -import static jdk.jpackage.test.PackageType.WINDOWS; +import jdk.jpackage.test.AdditionalLauncher.PropertyFile; +import jdk.jpackage.test.LauncherVerifier.Action; public final class LauncherAsServiceVerifier { @@ -111,6 +116,7 @@ public void applyTo(PackageTest pkg) { } else { applyToAdditionalLauncher(pkg); } + pkg.addInstallVerifier(this::verifyLauncherExecuted); } static void verify(JPackageCommand cmd) { @@ -127,7 +133,6 @@ static void verify(JPackageCommand cmd) { "service-installer.exe"); if (launcherNames.isEmpty()) { TKit.assertPathExists(serviceInstallerPath, false); - } else { TKit.assertFileExists(serviceInstallerPath); } @@ -188,23 +193,11 @@ static List getLaunchersAsServices(JPackageCommand cmd) { launcherNames.add(null); } - AdditionalLauncher.forEachAdditionalLauncher(cmd, - ThrowingBiConsumer.toBiConsumer( - (launcherName, propFilePath) -> { - if (Files.readAllLines(propFilePath).stream().anyMatch( - line -> { - if (line.startsWith( - "launcher-as-service=")) { - return Boolean.parseBoolean( - line.substring( - "launcher-as-service=".length())); - } else { - return false; - } - })) { - launcherNames.add(launcherName); - } - })); + forEachAdditionalLauncher(cmd, toBiConsumer((launcherName, propFilePath) -> { + if (new PropertyFile(propFilePath).findBooleanProperty("launcher-as-service").orElse(false)) { + launcherNames.add(launcherName); + } + })); return launcherNames; } @@ -237,45 +230,33 @@ private void applyToMainLauncher(PackageTest pkg) { + appOutputFilePathInitialize().toString()); cmd.addArguments("--java-options", "-Djpackage.test.noexit=true"); }); - pkg.addInstallVerifier(cmd -> { - if (canVerifyInstall(cmd)) { - delayInstallVerify(); - Path outputFilePath = appOutputFilePathVerify(cmd); - HelloApp.assertApp(cmd.appLauncherPath()) - .addParam("jpackage.test.appOutput", - outputFilePath.toString()) - .addDefaultArguments(expectedValue) - .verifyOutput(); - deleteOutputFile(outputFilePath); - } - }); - pkg.addInstallVerifier(cmd -> { - verify(cmd, launcherName); - }); } private void applyToAdditionalLauncher(PackageTest pkg) { - AdditionalLauncher al = new AdditionalLauncher(launcherName) { - @Override - protected void verify(JPackageCommand cmd) throws IOException { - if (canVerifyInstall(cmd)) { - delayInstallVerify(); - super.verify(cmd); - deleteOutputFile(appOutputFilePathVerify(cmd)); - } - LauncherAsServiceVerifier.verify(cmd, launcherName); - } - }.setLauncherAsService() - .addJavaOptions("-Djpackage.test.appOutput=" - + appOutputFilePathInitialize().toString()) + var al = new AdditionalLauncher(launcherName) + .setProperty("launcher-as-service", true) + .addJavaOptions("-Djpackage.test.appOutput=" + appOutputFilePathInitialize().toString()) .addJavaOptions("-Djpackage.test.noexit=true") - .addDefaultArguments(expectedValue); + .addDefaultArguments(expectedValue) + .withoutVerifyActions(Action.EXECUTE_LAUNCHER); Optional.ofNullable(additionalLauncherCallback).ifPresent(v -> v.accept(al)); al.applyTo(pkg); } + private void verifyLauncherExecuted(JPackageCommand cmd) throws IOException { + if (canVerifyInstall(cmd)) { + delayInstallVerify(); + Path outputFilePath = appOutputFilePathVerify(cmd); + HelloApp.assertApp(cmd.appLauncherPath()) + .addParam("jpackage.test.appOutput", outputFilePath.toString()) + .addDefaultArguments(expectedValue) + .verifyOutput(); + deleteOutputFile(outputFilePath); + } + } + private static void deleteOutputFile(Path file) throws IOException { try { TKit.deleteIfExists(file); @@ -291,8 +272,7 @@ private static void deleteOutputFile(Path file) throws IOException { } } - private static void verify(JPackageCommand cmd, String launcherName) throws - IOException { + private static void verify(JPackageCommand cmd, String launcherName) throws IOException { if (LINUX.contains(cmd.packageType())) { verifyLinuxUnitFile(cmd, launcherName); } else if (MAC_PKG.equals(cmd.packageType())) { @@ -370,6 +350,9 @@ private Path appOutputFilePathVerify(JPackageCommand cmd) { private final Path appOutputFileName; private final Consumer additionalLauncherCallback; - static final Set SUPPORTED_PACKAGES = Stream.of(LINUX, WINDOWS, - Set.of(MAC_PKG)).flatMap(x -> x.stream()).collect(Collectors.toSet()); + static final Set SUPPORTED_PACKAGES = Stream.of( + LINUX, + WINDOWS, + Set.of(MAC_PKG) + ).flatMap(Collection::stream).collect(Collectors.toSet()); } From fd50c919db8257e4d4d640d7cd42905ce2ef3c84 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 28 Jul 2025 12:31:24 -0400 Subject: [PATCH 53/83] Rework .desktop files verification: Always verify .desktop files are installed in the system folder on package install and uninstalled on package uninstall. Always verify contents of .desktop files. --- .../jdk/jpackage/test/LinuxHelper.java | 57 ++++++++++++++----- .../jdk/jpackage/test/PackageTest.java | 10 +++- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 3a1cad55c8731..ffe46288a219b 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -23,12 +23,14 @@ package jdk.jpackage.test; import java.io.IOException; +import java.io.UncheckedIOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -345,8 +347,7 @@ static void verifyPackageBundleEssential(JPackageCommand cmd) { } } - static void addBundleDesktopIntegrationVerifier(PackageTest test, - boolean integrated) { + static void addBundleDesktopIntegrationVerifier(PackageTest test, boolean integrated) { final String xdgUtils = "xdg-utils"; Function, String> verifier = (lines) -> { @@ -392,22 +393,50 @@ static void addBundleDesktopIntegrationVerifier(PackageTest test, }); test.addInstallVerifier(cmd -> { - // Verify .desktop files. - try (var files = Files.list(cmd.appLayout().desktopIntegrationDirectory())) { - List desktopFiles = files - .filter(path -> path.getFileName().toString().endsWith(".desktop")) - .toList(); - if (!integrated) { - TKit.assertStringListEquals(List.of(), - desktopFiles.stream().map(Path::toString).collect( - Collectors.toList()), - "Check there are no .desktop files in the package"); - } + if (!integrated) { + TKit.assertStringListEquals( + List.of(), + getDesktopFiles(cmd).stream().map(Path::toString).toList(), + "Check there are no .desktop files in the package"); + } + }); + } + + static void verifyDesktopFiles(JPackageCommand cmd, boolean installed) { + final var desktopFiles = getDesktopFiles(cmd); + try { + if (installed) { for (var desktopFile : desktopFiles) { verifyDesktopFile(cmd, desktopFile); } + + if (!cmd.isPackageUnpacked("Not verifying system .desktop files")) { + for (var desktopFile : desktopFiles) { + Path systemDesktopFile = LinuxHelper.getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); + TKit.assertFileExists(systemDesktopFile); + TKit.assertStringListEquals( + Files.readAllLines(desktopFile), + Files.readAllLines(systemDesktopFile), + String.format("Check [%s] and [%s] files are equal", desktopFile, systemDesktopFile)); + } + } + } else { + for (var desktopFile : getDesktopFiles(cmd)) { + Path systemDesktopFile = LinuxHelper.getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); + TKit.assertPathExists(systemDesktopFile, false); + } } - }); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static Collection getDesktopFiles(JPackageCommand cmd) { + var unpackedDir = cmd.appLayout().desktopIntegrationDirectory(); + var packageDir = cmd.pathToPackageFile(unpackedDir); + return getPackageFiles(cmd).filter(path -> { + return path.getParent().equals(packageDir) && path.getFileName().toString().endsWith(".desktop"); + }).map(Path::getFileName).map(unpackedDir::resolve).toList(); } private static void verifyDesktopFile(JPackageCommand cmd, Path desktopFile) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index 2ee5e79f07cf8..4b4d7c23f5b1c 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -360,7 +360,7 @@ public PackageTest configureHelloApp() { public PackageTest configureHelloApp(String javaAppDesc) { addHelloAppInitializer(javaAppDesc); - addInstallVerifier(JPackageCommand::executeLaunchers); + addInstallVerifier(LauncherVerifier::executeMainLauncherAndVerifyOutput); return this; } @@ -773,6 +773,10 @@ private void verifyPackageInstalled(JPackageCommand cmd) { if (isOfType(cmd, WINDOWS) && !cmd.isPackageUnpacked("Not verifying desktop integration")) { WindowsHelper.verifyDeployedDesktopIntegration(cmd, true); } + + if (isOfType(cmd, LINUX)) { + LinuxHelper.verifyDesktopFiles(cmd, true); + } } if (isOfType(cmd, LauncherAsServiceVerifier.SUPPORTED_PACKAGES)) { @@ -850,6 +854,10 @@ private void verifyPackageUninstalled(JPackageCommand cmd) { if (isOfType(cmd, WINDOWS)) { WindowsHelper.verifyDeployedDesktopIntegration(cmd, false); } + + if (isOfType(cmd, LINUX)) { + LinuxHelper.verifyDesktopFiles(cmd, false); + } } Path appInstallDir = cmd.appInstallationDirectory(); From 251033b8dbcaf1f74b9dcdb496419710ddab71fd Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 30 Jul 2025 16:18:54 -0400 Subject: [PATCH 54/83] PrintEnv: support `--print-workdir` CLI option and `jpackage.test.appOutput` Java property to redirect output --- test/jdk/tools/jpackage/apps/PrintEnv.java | 33 ++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/test/jdk/tools/jpackage/apps/PrintEnv.java b/test/jdk/tools/jpackage/apps/PrintEnv.java index bb1cef800f490..64a243a0abcfa 100644 --- a/test/jdk/tools/jpackage/apps/PrintEnv.java +++ b/test/jdk/tools/jpackage/apps/PrintEnv.java @@ -21,18 +21,38 @@ * questions. */ +import java.io.IOException; +import java.io.UncheckedIOException; import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleFinder; import java.lang.module.ModuleReference; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; public class PrintEnv { public static void main(String[] args) { List lines = printArgs(args); - lines.forEach(System.out::println); + Optional.ofNullable(System.getProperty("jpackage.test.appOutput")).map(Path::of).ifPresentOrElse(outputFilePath -> { + Optional.ofNullable(outputFilePath.getParent()).ifPresent(dir -> { + try { + Files.createDirectories(dir); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + try { + Files.write(outputFilePath, lines); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }, () -> { + lines.forEach(System.out::println); + }); } private static List printArgs(String[] args) { @@ -45,11 +65,13 @@ private static List printArgs(String[] args) { } else if (arg.startsWith(PRINT_SYS_PROP)) { String name = arg.substring(PRINT_SYS_PROP.length()); lines.add(name + "=" + System.getProperty(name)); - } else if (arg.startsWith(PRINT_MODULES)) { + } else if (arg.equals(PRINT_MODULES)) { lines.add(ModuleFinder.ofSystem().findAll().stream() .map(ModuleReference::descriptor) .map(ModuleDescriptor::name) .collect(Collectors.joining(","))); + } else if (arg.equals(PRINT_WORK_DIR)) { + lines.add("$CD=" + Path.of("").toAbsolutePath()); } else { throw new IllegalArgumentException(); } @@ -58,7 +80,8 @@ private static List printArgs(String[] args) { return lines; } - private final static String PRINT_ENV_VAR = "--print-env-var="; - private final static String PRINT_SYS_PROP = "--print-sys-prop="; - private final static String PRINT_MODULES = "--print-modules"; + private static final String PRINT_ENV_VAR = "--print-env-var="; + private static final String PRINT_SYS_PROP = "--print-sys-prop="; + private static final String PRINT_MODULES = "--print-modules"; + private static final String PRINT_WORK_DIR = "--print-workdir"; } From 6156136f785ff25b935c3a9b29bbde585f4380b0 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 30 Jul 2025 16:28:39 -0400 Subject: [PATCH 55/83] LinuxHelper: add getInvokeShortcutSpecs(); add DesktopFile type to streamline verifyDesktopFile() --- .../jdk/jpackage/test/LinuxHelper.java | 179 +++++++++++++----- 1 file changed, 131 insertions(+), 48 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index ffe46288a219b..32c291173adbe 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -35,6 +35,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -44,6 +45,7 @@ import java.util.stream.Stream; import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.function.ThrowingConsumer; +import jdk.jpackage.test.LauncherShortcut.InvokeShortcutSpec; import jdk.jpackage.test.PackageTest.PackageHandlers; @@ -310,8 +312,8 @@ static String getRpmBundleProperty(Path bundle, String fieldName) { } static void verifyPackageBundleEssential(JPackageCommand cmd) { - String packageName = LinuxHelper.getPackageName(cmd); - long packageSize = LinuxHelper.getInstalledPackageSizeKB(cmd); + String packageName = getPackageName(cmd); + long packageSize = getInstalledPackageSizeKB(cmd); TKit.trace("InstalledPackageSize: " + packageSize); TKit.assertNotEquals(0, packageSize, String.format( "Check installed size of [%s] package in not zero", packageName)); @@ -332,7 +334,7 @@ static void verifyPackageBundleEssential(JPackageCommand cmd) { checkPrerequisites = packageSize > 5; } - List prerequisites = LinuxHelper.getPrerequisitePackages(cmd); + List prerequisites = getPrerequisitePackages(cmd); if (checkPrerequisites) { final String vitalPackage = "libc"; TKit.assertTrue(prerequisites.stream().filter( @@ -347,6 +349,22 @@ static void verifyPackageBundleEssential(JPackageCommand cmd) { } } + public static Collection getInvokeShortcutSpecs(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX); + + final var desktopFiles = getDesktopFiles(cmd); + final var predefinedAppImage = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of).map(AppImageFile::load); + + return desktopFiles.stream().map(desktopFile -> { + var systemDesktopFile = getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); + return new InvokeShortcutSpec.Stub( + launcherNameFromDesktopFile(cmd, predefinedAppImage, desktopFile), + LauncherShortcut.LINUX_SHORTCUT, + new DesktopFile(systemDesktopFile, false).findQuotedValue("Path").map(Path::of), + List.of("xdg-open", systemDesktopFile.toString())); + }).toList(); + } + static void addBundleDesktopIntegrationVerifier(PackageTest test, boolean integrated) { final String xdgUtils = "xdg-utils"; @@ -406,13 +424,14 @@ static void verifyDesktopFiles(JPackageCommand cmd, boolean installed) { final var desktopFiles = getDesktopFiles(cmd); try { if (installed) { + var predefinedAppImage = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of).map(AppImageFile::load); for (var desktopFile : desktopFiles) { - verifyDesktopFile(cmd, desktopFile); + verifyDesktopFile(cmd, predefinedAppImage, desktopFile); } if (!cmd.isPackageUnpacked("Not verifying system .desktop files")) { for (var desktopFile : desktopFiles) { - Path systemDesktopFile = LinuxHelper.getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); + Path systemDesktopFile = getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); TKit.assertFileExists(systemDesktopFile); TKit.assertStringListEquals( Files.readAllLines(desktopFile), @@ -422,7 +441,7 @@ static void verifyDesktopFiles(JPackageCommand cmd, boolean installed) { } } else { for (var desktopFile : getDesktopFiles(cmd)) { - Path systemDesktopFile = LinuxHelper.getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); + Path systemDesktopFile = getSystemDesktopFilesFolder().resolve(desktopFile.getFileName()); TKit.assertPathExists(systemDesktopFile, false); } } @@ -439,34 +458,34 @@ private static Collection getDesktopFiles(JPackageCommand cmd) { }).map(Path::getFileName).map(unpackedDir::resolve).toList(); } - private static void verifyDesktopFile(JPackageCommand cmd, Path desktopFile) - throws IOException { - TKit.trace(String.format("Check [%s] file BEGIN", desktopFile)); + private static String launcherNameFromDesktopFile(JPackageCommand cmd, Optional predefinedAppImage, Path desktopFile) { + Objects.requireNonNull(cmd); + Objects.requireNonNull(predefinedAppImage); + Objects.requireNonNull(desktopFile); - var launcherName = Stream.of(List.of(cmd.name()), cmd.addLauncherNames()).flatMap(List::stream).filter(name -> { + return predefinedAppImage.map(v -> { + return v.launchers().keySet().stream(); + }).orElseGet(() -> { + return Stream.concat(Stream.of(cmd.name()), cmd.addLauncherNames().stream()); + }).filter(name-> { return getDesktopFile(cmd, name).equals(desktopFile); - }).findAny(); - if (!cmd.hasArgument("--app-image")) { - TKit.assertTrue(launcherName.isPresent(), - "Check the desktop file corresponds to one of app launchers"); - } + }).findAny().orElseThrow(() -> { + TKit.assertUnexpected(String.format("Failed to find launcher corresponding to [%s] file", desktopFile)); + // Unreachable + return null; + }); + } - List lines = Files.readAllLines(desktopFile); - TKit.assertEquals("[Desktop Entry]", lines.get(0), "Check file header"); + private static void verifyDesktopFile(JPackageCommand cmd, Optional predefinedAppImage, Path desktopFile) throws IOException { + Objects.requireNonNull(cmd); + Objects.requireNonNull(predefinedAppImage); + Objects.requireNonNull(desktopFile); - Map data = lines.stream() - .skip(1) - .peek(str -> TKit.assertTextStream("=").predicate(String::contains).apply(List.of(str))) - .map(str -> { - String components[] = str.split("=(?=.+)"); - if (components.length == 1) { - return Map.entry(str.substring(0, str.length() - 1), ""); - } - return Map.entry(components[0], components[1]); - }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> { - TKit.assertUnexpected("Multiple values of the same key"); - return null; - })); + TKit.trace(String.format("Check [%s] file BEGIN", desktopFile)); + + var launcherName = launcherNameFromDesktopFile(cmd, predefinedAppImage, desktopFile); + + var data = new DesktopFile(desktopFile, true); final Set mandatoryKeys = new HashSet<>(Set.of("Name", "Comment", "Exec", "Icon", "Terminal", "Type", "Categories")); @@ -476,32 +495,40 @@ private static void verifyDesktopFile(JPackageCommand cmd, Path desktopFile) for (var e : Map.of("Type", "Application", "Terminal", "false").entrySet()) { String key = e.getKey(); - TKit.assertEquals(e.getValue(), data.get(key), String.format( + TKit.assertEquals(e.getValue(), data.find(key).orElseThrow(), String.format( "Check value of [%s] key", key)); } - // Verify the value of `Exec` key is escaped if required - String launcherPath = data.get("Exec"); - if (Pattern.compile("\\s").matcher(launcherPath).find()) { - TKit.assertTrue(launcherPath.startsWith("\"") - && launcherPath.endsWith("\""), - "Check path to the launcher is enclosed in double quotes"); - launcherPath = launcherPath.substring(1, launcherPath.length() - 1); - } + String launcherPath = data.findQuotedValue("Exec").orElseThrow(); - if (launcherName.isPresent()) { - TKit.assertEquals(launcherPath, cmd.pathToPackageFile( - cmd.appLauncherPath(launcherName.get())).toString(), - String.format( - "Check the value of [Exec] key references [%s] app launcher", - launcherName.get())); - } + TKit.assertEquals( + launcherPath, + cmd.pathToPackageFile(cmd.appLauncherPath(launcherName)).toString(), + String.format("Check the value of [Exec] key references [%s] app launcher", launcherName)); + + LauncherShortcut.LINUX_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).map(shortcutWorkDirType -> { + switch (shortcutWorkDirType) { + case DEFAULT -> { + return (Path)null; + } + default -> { + throw new AssertionError(); + } + } + }).ifPresentOrElse(shortcutWorkDir -> { + var actualShortcutWorkDir = data.findQuotedValue("Path"); + TKit.assertTrue(actualShortcutWorkDir.isPresent(), "Check [Path] key exists"); + TKit.assertEquals(actualShortcutWorkDir.get(), shortcutWorkDir, "Check the value of [Path] key"); + }, () -> { + TKit.assertTrue(data.findQuotedValue("Path").isEmpty(), "Check there is no [Path] key"); + }); for (var e : List.>, Function>>of( Map.entry(Map.entry("Exec", Optional.of(launcherPath)), ApplicationLayout::launchersDirectory), Map.entry(Map.entry("Icon", Optional.empty()), ApplicationLayout::desktopIntegrationDirectory))) { - var path = e.getKey().getValue().or(() -> Optional.of(data.get( - e.getKey().getKey()))).map(Path::of).get(); + var path = e.getKey().getValue().or(() -> { + return data.findQuotedValue(e.getKey().getKey()); + }).map(Path::of).get(); TKit.assertFileExists(cmd.pathToUnpackedPackageFile(path)); Path expectedDir = cmd.pathToPackageFile(e.getValue().apply(cmd.appLayout())); TKit.assertTrue(path.getParent().equals(expectedDir), String.format( @@ -790,6 +817,62 @@ private static Method initGetServiceUnitFileName() { } } + + private static final class DesktopFile { + DesktopFile(Path path, boolean verify) { + try { + List lines = Files.readAllLines(path); + if (verify) { + TKit.assertEquals("[Desktop Entry]", lines.getFirst(), "Check file header"); + } + + var stream = lines.stream().skip(1); + if (verify) { + stream = stream.peek(str -> { + TKit.assertTextStream("=").predicate(String::contains).apply(List.of(str)); + }); + } + + data = stream.map(str -> { + String components[] = str.split("=(?=.+)"); + if (components.length == 1) { + return Map.entry(str.substring(0, str.length() - 1), ""); + } else { + return Map.entry(components[0], components[1]); + } + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + Set keySet() { + return data.keySet(); + } + + Optional find(String property) { + return Optional.ofNullable(data.get(Objects.requireNonNull(property))); + } + + Optional findQuotedValue(String property) { + return find(property).map(value -> { + if (Pattern.compile("\\s").matcher(value).find()) { + boolean quotesMatched = value.startsWith("\"") && value.endsWith("\""); + if (!quotesMatched) { + TKit.assertTrue(quotesMatched, + String.format("Check the value of key [%s] is enclosed in double quotes", property)); + } + return value.substring(1, value.length() - 1); + } else { + return value; + } + }); + } + + private final Map data; + } + + static final Set CRITICAL_RUNTIME_FILES = Set.of(Path.of( "lib/server/libjvm.so")); From f8770493d8f1ee32fbd406d00b6842f9305deafa Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 30 Jul 2025 16:29:38 -0400 Subject: [PATCH 56/83] WinShortcutVerifier: add getInvokeShortcutSpecs(); streamline expectLauncherShortcuts() --- .../jpackage/test/WinShortcutVerifier.java | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index 3b8d484497262..a765413233cd1 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -22,6 +22,9 @@ */ package jdk.jpackage.test; +import static java.util.stream.Collectors.groupingBy; +import static jdk.jpackage.test.LauncherShortcut.WIN_DESKTOP_SHORTCUT; +import static jdk.jpackage.test.LauncherShortcut.WIN_START_MENU_SHORTCUT; import static jdk.jpackage.test.WindowsHelper.getInstallationSubDirectory; import java.nio.file.Files; @@ -35,14 +38,14 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.internal.util.PathUtils; +import jdk.jpackage.test.LauncherShortcut.InvokeShortcutSpec; import jdk.jpackage.test.MsiDatabase.Shortcut; import jdk.jpackage.test.WindowsHelper.SpecialFolder; -final class WinShortcutVerifier { +public final class WinShortcutVerifier { static void verifyBundleShortcuts(JPackageCommand cmd) { cmd.verifyIsOfType(PackageType.WIN_MSI); @@ -51,7 +54,7 @@ static void verifyBundleShortcuts(JPackageCommand cmd) { return; } - var actualShortcuts = WindowsHelper.getMsiShortcuts(cmd).stream().collect(Collectors.groupingBy(shortcut -> { + var actualShortcuts = WindowsHelper.getMsiShortcuts(cmd).stream().collect(groupingBy(shortcut -> { return PathUtils.replaceSuffix(shortcut.target().getFileName(), "").toString(); })); @@ -73,10 +76,10 @@ static void verifyBundleShortcuts(JPackageCommand cmd) { var expectedLauncherShortcuts = sorter.apply(expectedShortcuts.get(name)); TKit.assertEquals(expectedLauncherShortcuts.size(), actualLauncherShortcuts.size(), - String.format("Check the number of shortcuts of [%s] launcher", name)); + String.format("Check the number of shortcuts of launcher [%s]", name)); for (int i = 0; i != expectedLauncherShortcuts.size(); i++) { - TKit.trace(String.format("Verify shortcut #%d of [%s] launcher", i + 1, name)); + TKit.trace(String.format("Verify shortcut #%d of launcher [%s]", i + 1, name)); actualLauncherShortcuts.get(i).assertEquals(expectedLauncherShortcuts.get(i)); TKit.trace("Done"); } @@ -96,6 +99,14 @@ static void verifyDeployedShortcuts(JPackageCommand cmd, boolean installed) { verifyDeployedShortcutsInternal(copyCmd, false); } + public static Collection getInvokeShortcutSpecs(JPackageCommand cmd) { + return expectShortcuts(cmd).entrySet().stream().map(e -> { + return e.getValue().stream().map(shortcut -> { + return convert(cmd, e.getKey(), shortcut); + }); + }).flatMap(x -> x).toList(); + } + private static void verifyDeployedShortcutsInternal(JPackageCommand cmd, boolean installed) { var expectedShortcuts = expectShortcuts(cmd).values().stream().flatMap(Collection::stream).toList(); @@ -184,43 +195,21 @@ private static Collection expectLauncherShortcuts(JPackageCommand cmd, Objects.requireNonNull(cmd); Objects.requireNonNull(predefinedAppImage); - List shortcuts = new ArrayList<>(); + final List shortcuts = new ArrayList<>(); - var name = Optional.ofNullable(launcherName).orElseGet(cmd::name); + final boolean isWinMenu = WIN_START_MENU_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).isPresent(); + final boolean isDesktop = WIN_DESKTOP_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).isPresent(); - boolean isWinMenu; - boolean isDesktop; - if (name.equals(cmd.name())) { - isWinMenu = cmd.hasArgument("--win-menu"); - isDesktop = cmd.hasArgument("--win-shortcut"); - } else { - var props = predefinedAppImage.map(v -> { - return v.launchers().get(name); - }).map(appImageFileLauncherProps -> { - Map convProps = new HashMap<>(); - for (var e : Map.of("menu", "win-menu", "shortcut", "win-shortcut").entrySet()) { - Optional.ofNullable(appImageFileLauncherProps.get(e.getKey())).ifPresent(v -> { - convProps.put(e.getValue(), v); - }); - } - return new AdditionalLauncher.PropertyFile(convProps); - }).orElseGet(() -> { - return AdditionalLauncher.getAdditionalLauncherProperties(cmd, launcherName); - }); - isWinMenu = props.findBooleanProperty("win-menu").orElseGet(() -> cmd.hasArgument("--win-menu")); - isDesktop = props.findBooleanProperty("win-shortcut").orElseGet(() -> cmd.hasArgument("--win-shortcut")); - } + final var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); - var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); - - SpecialFolder installRoot; + final SpecialFolder installRoot; if (isUserLocalInstall) { installRoot = SpecialFolder.LOCAL_APPLICATION_DATA; } else { installRoot = SpecialFolder.PROGRAM_FILES; } - var workDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd)); + final var workDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd)); if (isWinMenu) { ShortcutType type; @@ -272,6 +261,25 @@ private static Map> expectShortcuts(JPackageCommand return expectedShortcuts; } + private static InvokeShortcutSpec convert(JPackageCommand cmd, String launcherName, Shortcut shortcut) { + LauncherShortcut launcherShortcut; + if (Stream.of(ShortcutType.COMMON_START_MENU, ShortcutType.USER_START_MENU).anyMatch(type -> { + return shortcut.path().startsWith(Path.of(type.rootFolder().getMsiPropertyName())); + })) { + launcherShortcut = WIN_START_MENU_SHORTCUT; + } else { + launcherShortcut = WIN_DESKTOP_SHORTCUT; + } + + var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); + return new InvokeShortcutSpec.Stub( + launcherName, + launcherShortcut, + resolvePath(shortcut.workDir(), !isUserLocalInstall), + List.of("cmd", "/c", "start", "/wait", PathUtils.addSuffix(resolvePath(shortcut.path(), !isUserLocalInstall), ".lnk").toString())); + } + + private static final Comparator SHORTCUT_COMPARATOR = Comparator.comparing(Shortcut::target) .thenComparing(Comparator.comparing(Shortcut::path)) .thenComparing(Comparator.comparing(Shortcut::workDir)); From 27ed69b3689b65c95d1ef5f1a3d62f311d47930d Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 30 Jul 2025 16:35:03 -0400 Subject: [PATCH 57/83] AddLShortcutTest: add testStartupDirectory() tests to exercise combinations of startup directory in the main launcher and in the additional launcher; add testInvokeShortcuts() to invoke launchers through shortcuts and verify work directory; cover shortcuts in the predefined app image --- .../jpackage/share/AddLShortcutTest.java | 316 +++++++++++++++++- 1 file changed, 312 insertions(+), 4 deletions(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index 6430a55d784af..fbcd64c1cf674 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -21,13 +21,31 @@ * questions. */ -import java.nio.file.Path; +import java.io.IOException; import java.lang.invoke.MethodHandles; -import jdk.jpackage.test.PackageTest; -import jdk.jpackage.test.FileAssociations; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import jdk.internal.util.OperatingSystem; import jdk.jpackage.test.AdditionalLauncher; -import jdk.jpackage.test.TKit; +import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.Executor; +import jdk.jpackage.test.FileAssociations; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.LauncherShortcut; +import jdk.jpackage.test.LauncherShortcut.InvokeShortcutSpec; +import jdk.jpackage.test.LauncherShortcut.StartupDirectory; +import jdk.jpackage.test.LauncherVerifier.Action; +import jdk.jpackage.test.LinuxHelper; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.RunnablePackageTest; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.WinShortcutVerifier; /** * Test --add-launcher parameter with shortcuts (platform permitting). @@ -44,9 +62,23 @@ * @key jpackagePlatformPackage * @library /test/jdk/tools/jpackage/helpers * @build jdk.jpackage.test.* + * @requires (jpackage.test.SQETest != null) * @compile -Xlint:all -Werror AddLShortcutTest.java * @run main/othervm/timeout=540 -Xmx512m * jdk.jpackage.test.Main + * --jpt-run=AddLShortcutTest.test + */ + +/* + * @test + * @summary jpackage with --add-launcher + * @key jpackagePlatformPackage + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.* + * @requires (jpackage.test.SQETest == null) + * @compile -Xlint:all -Werror AddLShortcutTest.java + * @run main/othervm/timeout=1080 -Xmx512m + * jdk.jpackage.test.Main * --jpt-run=AddLShortcutTest */ @@ -107,6 +139,282 @@ public void test() { packageTest.run(); } + @Test(ifNotOS = OperatingSystem.MACOS) + @ParameterSupplier(ifOS = OperatingSystem.LINUX, value = "testShortcutStartupDirectoryLinux") + @ParameterSupplier(ifOS = OperatingSystem.WINDOWS, value = "testShortcutStartupDirectoryWindows") + public void testStartupDirectory(LauncherShortcutStartupDirectoryConfig... cfgs) { + + var test = new PackageTest().addInitializer(cmd -> { + cmd.setArgumentValue("--name", "AddLShortcutDirTest"); + }).addInitializer(JPackageCommand::setFakeRuntime).addHelloAppInitializer(null); + + test.addInitializer(cfgs[0]::applyToMainLauncher); + for (var i = 1; i != cfgs.length; ++i) { + var al = new AdditionalLauncher("launcher-" + i); + cfgs[i].applyToAdditionalLauncher(al); + al.withoutVerifyActions(Action.EXECUTE_LAUNCHER).applyTo(test); + } + + test.run(RunnablePackageTest.Action.CREATE_AND_UNPACK); + } + + @Test(ifNotOS = OperatingSystem.MACOS) + @ParameterSupplier(ifOS = OperatingSystem.LINUX, value = "testShortcutStartupDirectoryLinux") + @ParameterSupplier(ifOS = OperatingSystem.WINDOWS, value = "testShortcutStartupDirectoryWindows") + public void testStartupDirectory2(LauncherShortcutStartupDirectoryConfig... cfgs) { + + // + // Launcher shortcuts in the predefined app image. + // + // Shortcut configuration for the main launcher is not supported when building an app image. + // However, shortcut configuration for additional launchers is supported. + // The test configures shortcuts for additional launchers in the app image building jpackage command + // and applies shortcut configuration to the main launcher in the native packaging jpackage command. + // + + Path[] predefinedAppImage = new Path[1]; + + new PackageTest().addRunOnceInitializer(() -> { + var cmd = JPackageCommand.helloAppImage() + .setArgumentValue("--name", "foo") + .setFakeRuntime(); + + for (var i = 1; i != cfgs.length; ++i) { + var al = new AdditionalLauncher("launcher-" + i); + cfgs[i].applyToAdditionalLauncher(al); + al.withoutVerifyActions(Action.EXECUTE_LAUNCHER).applyTo(cmd); + } + + cmd.execute(); + + predefinedAppImage[0] = cmd.outputBundle(); + }).addInitializer(cmd -> { + cmd.removeArgumentWithValue("--input"); + cmd.setArgumentValue("--name", "AddLShortcutDir2Test"); + cmd.addArguments("--app-image", predefinedAppImage[0]); + cfgs[0].applyToMainLauncher(cmd); + }).run(RunnablePackageTest.Action.CREATE_AND_UNPACK); + } + + public static Collection testShortcutStartupDirectoryLinux() { + return testShortcutStartupDirectory(LauncherShortcut.LINUX_SHORTCUT); + } + + public static Collection testShortcutStartupDirectoryWindows() { + return testShortcutStartupDirectory(LauncherShortcut.WIN_DESKTOP_SHORTCUT, LauncherShortcut.WIN_START_MENU_SHORTCUT); + } + + @Test(ifNotOS = OperatingSystem.MACOS) + public void testInvokeShortcuts() { + + var testApp = TKit.TEST_SRC_ROOT.resolve("apps/PrintEnv.java"); + + var name = "AddLShortcutRunTest"; + + var test = new PackageTest().addInitializer(cmd -> { + cmd.setArgumentValue("--name", name); + }).addInitializer(cmd -> { + cmd.addArguments("--arguments", "--print-workdir"); + }).addInitializer(JPackageCommand::ignoreFakeRuntime).addHelloAppInitializer(testApp + "*Hello"); + + var shortcutStartupDirectoryVerifier = new ShortcutStartupDirectoryVerifier(name, "a"); + + shortcutStartupDirectoryVerifier.applyTo(test, StartupDirectory.DEFAULT); + + test.addInstallVerifier(cmd -> { + if (!cmd.isPackageUnpacked("Not invoking launcher shortcuts")) { + Collection invokeShortcutSpecs; + if (TKit.isLinux()) { + invokeShortcutSpecs = LinuxHelper.getInvokeShortcutSpecs(cmd); + } else if (TKit.isWindows()) { + invokeShortcutSpecs = WinShortcutVerifier.getInvokeShortcutSpecs(cmd); + } else { + throw new UnsupportedOperationException(); + } + shortcutStartupDirectoryVerifier.verify(invokeShortcutSpecs); + } + }); + + test.run(); + } + + + private record ShortcutStartupDirectoryVerifier(String packageName, String launcherName) { + ShortcutStartupDirectoryVerifier { + Objects.requireNonNull(packageName); + Objects.requireNonNull(launcherName); + } + + void applyTo(PackageTest test, StartupDirectory startupDirectory) { + var al = new AdditionalLauncher(launcherName); + al.setShortcut(shortcut(), Objects.requireNonNull(startupDirectory)); + al.addJavaOptions(String.format("-Djpackage.test.appOutput=${%s}/%s", + outputDirVarName(), expectedOutputFilename())); + al.withoutVerifyActions(Action.EXECUTE_LAUNCHER).applyTo(test); + } + + void verify(Collection invokeShortcutSpecs) throws IOException { + + TKit.trace(String.format("Verify shortcut [%s]", launcherName)); + + var expectedOutputFile = Path.of(System.getenv(outputDirVarName())).resolve(expectedOutputFilename()); + + TKit.deleteIfExists(expectedOutputFile); + + var invokeShortcutSpec = invokeShortcutSpecs.stream().filter(v -> { + return launcherName.equals(v.launcherName()); + }).findAny().orElseThrow(); + + Executor.of(invokeShortcutSpec.commandLine()).execute(); + + TKit.assertFileExists(expectedOutputFile); + var actualStr = Files.readAllLines(expectedOutputFile).getFirst(); + + var outputPrefix = "$CD="; + + TKit.assertTrue(actualStr.startsWith(outputPrefix), "Check output starts with '" + outputPrefix+ "' string"); + + invokeShortcutSpec.expectedWorkDirectory().ifPresent(expectedWorkDirectory -> { + TKit.assertEquals( + expectedWorkDirectory, + Path.of(actualStr.substring(outputPrefix.length())), + String.format("Check work directory of %s of launcher [%s]", + invokeShortcutSpec.shortcut().propertyName(), + invokeShortcutSpec.launcherName())); + }); + } + + private String expectedOutputFilename() { + return String.format("%s-%s.out", packageName, launcherName); + } + + private String outputDirVarName() { + if (TKit.isLinux()) { + return "HOME"; + } else if (TKit.isWindows()) { + return "LOCALAPPDATA"; + } else { + throw new UnsupportedOperationException(); + } + } + + private LauncherShortcut shortcut() { + if (TKit.isLinux()) { + return LauncherShortcut.LINUX_SHORTCUT; + } else if (TKit.isWindows()) { + return LauncherShortcut.WIN_DESKTOP_SHORTCUT; + } else { + throw new UnsupportedOperationException(); + } + } + } + + + private static Collection testShortcutStartupDirectory(LauncherShortcut... shortcuts) { + List> items = new ArrayList<>(); + + for (var shortcut : shortcuts) { + List mainLauncherVariants = new ArrayList<>(); + for (var valueSetter : StartupDirectoryValueSetter.MAIN_LAUNCHER_VALUES) { + mainLauncherVariants.add(new LauncherShortcutStartupDirectoryConfig(shortcut, valueSetter)); + } + mainLauncherVariants.stream().map(List::of).forEach(items::add); + mainLauncherVariants.add(new LauncherShortcutStartupDirectoryConfig(shortcut)); + + List addLauncherVariants = new ArrayList<>(); + addLauncherVariants.add(new LauncherShortcutStartupDirectoryConfig(shortcut)); + for (var valueSetter : StartupDirectoryValueSetter.ADD_LAUNCHER_VALUES) { + addLauncherVariants.add(new LauncherShortcutStartupDirectoryConfig(shortcut, valueSetter)); + } + + for (var mainLauncherVariant : mainLauncherVariants) { + for (var addLauncherVariant : addLauncherVariants) { + if (mainLauncherVariant.valueSetter().isPresent() || addLauncherVariant.valueSetter().isPresent()) { + items.add(List.of(mainLauncherVariant, addLauncherVariant)); + } + } + } + } + + return items.stream().map(List::toArray).toList(); + } + + + private enum StartupDirectoryValueSetter { + DEFAULT(""), + TRUE("true"), + FALSE("false"), + ; + + StartupDirectoryValueSetter(String value) { + this.value = Objects.requireNonNull(value); + } + + void applyToMainLauncher(LauncherShortcut shortcut, JPackageCommand cmd) { + switch (this) { + case TRUE, FALSE -> { + throw new UnsupportedOperationException(); + } + case DEFAULT -> { + cmd.addArgument(shortcut.optionName()); + } + default -> { + cmd.addArgument(shortcut.optionName() + "=" + value); + } + } + } + + void applyToAdditionalLauncher(LauncherShortcut shortcut, AdditionalLauncher addLauncher) { + addLauncher.setProperty(shortcut.propertyName(), value); + } + + private final String value; + + static final List MAIN_LAUNCHER_VALUES = List.of( + StartupDirectoryValueSetter.DEFAULT + ); + + static final List ADD_LAUNCHER_VALUES = List.of( + StartupDirectoryValueSetter.TRUE, + StartupDirectoryValueSetter.FALSE + ); + } + + + record LauncherShortcutStartupDirectoryConfig(LauncherShortcut shortcut, Optional valueSetter) { + + LauncherShortcutStartupDirectoryConfig { + Objects.requireNonNull(shortcut); + Objects.requireNonNull(valueSetter); + } + + LauncherShortcutStartupDirectoryConfig(LauncherShortcut shortcut, StartupDirectoryValueSetter valueSetter) { + this(shortcut, Optional.of(valueSetter)); + } + + LauncherShortcutStartupDirectoryConfig(LauncherShortcut shortcut) { + this(shortcut, Optional.empty()); + } + + void applyToMainLauncher(JPackageCommand target) { + valueSetter.ifPresent(valueSetter -> { + valueSetter.applyToMainLauncher(shortcut, target); + }); + } + + void applyToAdditionalLauncher(AdditionalLauncher target) { + valueSetter.ifPresent(valueSetter -> { + valueSetter.applyToAdditionalLauncher(shortcut, target); + }); + } + + @Override + public String toString() { + return shortcut + "=" + valueSetter.map(Object::toString).orElse(""); + } + } + + private static final Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of( "resources", "icon" + TKit.ICON_SUFFIX)); } From c76aacd9805f22c0b4be90e21af26ad6235b26b2 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 30 Jul 2025 23:21:14 -0400 Subject: [PATCH 58/83] WinShortcutVerifier: fix bad merge --- .../helpers/jdk/jpackage/test/WinShortcutVerifier.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index a765413233cd1..acd11a116db86 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -253,14 +253,6 @@ private static Map> expectShortcuts(JPackageCommand return expectedShortcuts; } - addShortcuts.accept(cmd.name()); - predefinedAppImage.map(v -> { - return (Collection)v.addLaunchers().keySet(); - }).orElseGet(cmd::addLauncherNames).forEach(addShortcuts); - - return expectedShortcuts; - } - private static InvokeShortcutSpec convert(JPackageCommand cmd, String launcherName, Shortcut shortcut) { LauncherShortcut launcherShortcut; if (Stream.of(ShortcutType.COMMON_START_MENU, ShortcutType.USER_START_MENU).anyMatch(type -> { @@ -275,7 +267,7 @@ private static InvokeShortcutSpec convert(JPackageCommand cmd, String launcherNa return new InvokeShortcutSpec.Stub( launcherName, launcherShortcut, - resolvePath(shortcut.workDir(), !isUserLocalInstall), + Optional.of(resolvePath(shortcut.workDir(), !isUserLocalInstall)), List.of("cmd", "/c", "start", "/wait", PathUtils.addSuffix(resolvePath(shortcut.path(), !isUserLocalInstall), ".lnk").toString())); } From 869a0f6f4e583181e0a08c2083a443f2718cbad6 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 14:01:46 -0400 Subject: [PATCH 59/83] AddLShortcutTest: make it work on Linux --- test/jdk/tools/jpackage/share/AddLShortcutTest.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index fbcd64c1cf674..51bfdf7af2bed 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import jdk.internal.util.OperatingSystem; @@ -265,7 +266,15 @@ void verify(Collection invokeShortcutSpecs) throws return launcherName.equals(v.launcherName()); }).findAny().orElseThrow(); - Executor.of(invokeShortcutSpec.commandLine()).execute(); + invokeShortcutSpec.execute(); + + // On Linux, "gtk-launch" is used to launch a .desktop file. It is async and there is no + // wait to make it wait for exit of a process it triggers. + Executor.tryRunMultipleTimes(() -> { + if (!Files.exists(expectedOutputFile)) { + throw new NoSuchElementException(String.format("[%s] is not avaialble", expectedOutputFile)); + } + }, 3 /* Number of attempts */, 3 /* Seconds between attempts */); TKit.assertFileExists(expectedOutputFile); var actualStr = Files.readAllLines(expectedOutputFile).getFirst(); From 527b010fca63bfaf92ed3b1eda17d399148ed478 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 14:02:30 -0400 Subject: [PATCH 60/83] LinuxHelper: Use `gtk-launch` command to launch .desktop files --- .../tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 32c291173adbe..054bf89bb0ad6 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -361,7 +361,7 @@ public static Collection getInvokeShortcutSpecs(JP launcherNameFromDesktopFile(cmd, predefinedAppImage, desktopFile), LauncherShortcut.LINUX_SHORTCUT, new DesktopFile(systemDesktopFile, false).findQuotedValue("Path").map(Path::of), - List.of("xdg-open", systemDesktopFile.toString())); + List.of("gtk-launch", PathUtils.replaceSuffix(systemDesktopFile.getFileName(), "").toString())); }).toList(); } From d24a167568f30e7ea496086909e9a297071edc96 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 14:03:18 -0400 Subject: [PATCH 61/83] LauncherShortcut: bugfix --- .../jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java index 6fb5c6e14a894..82df7f17616fd 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java @@ -118,6 +118,10 @@ public interface InvokeShortcutSpec { Optional expectedWorkDirectory(); List commandLine(); + default Executor.Result execute() { + return HelloApp.configureAndExecute(0, Executor.of(commandLine()).dumpOutput()); + } + record Stub( String launcherName, LauncherShortcut shortcut, From 4e08a420e03b6478faee3013e7ac3b101804b4bc Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 19:06:37 -0400 Subject: [PATCH 62/83] LauncherShortcut: make it work with the current variant of jpackage without JDK-8308349 mods --- .../helpers/jdk/jpackage/test/LauncherShortcut.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java index 82df7f17616fd..cf917fa3e4706 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java @@ -85,6 +85,7 @@ static Optional parse(String str) { LauncherShortcut(String propertyName) { this.propertyName = Objects.requireNonNull(propertyName); + this.predefinedAppImagePropertyName = propertyName.substring(propertyName.indexOf('-') + 1); } public String propertyName() { @@ -103,11 +104,14 @@ Optional expectShortcut(JPackageCommand cmd, Optional { + propertyName[0] = this.predefinedAppImagePropertyName; return new PropertyFile(appImage.addLaunchers().get(launcherName)); }).orElseGet(() -> { + propertyName[0] = this.propertyName; return getAdditionalLauncherProperties(cmd, launcherName); - })::findProperty); + })::findProperty, propertyName[0]); } } @@ -147,8 +151,8 @@ private Optional findMainLauncherShortcut(JPackageCommand cmd) } private Optional findAddLauncherShortcut(JPackageCommand cmd, - Function> addlauncherProperties) { - var explicit = addlauncherProperties.apply(propertyName()); + Function> addlauncherProperties, String propertyName) { + var explicit = addlauncherProperties.apply(propertyName); if (explicit.isPresent()) { return explicit.flatMap(StartupDirectory::parse); } else { @@ -157,4 +161,5 @@ private Optional findAddLauncherShortcut(JPackageCommand cmd, } private final String propertyName; + private final String predefinedAppImagePropertyName; } From 442fc740bec5654c396bde0a128ae1dce7bdeffa Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 19:57:53 -0400 Subject: [PATCH 63/83] AdditionalLauncher: bugfix --- .../jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java index c2d2ad0f407a0..07c8e06856fb4 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java @@ -102,7 +102,7 @@ public AdditionalLauncher setShortcuts(boolean menu, boolean desktop) { setShortcut(LINUX_SHORTCUT, desktop); } else if (TKit.isWindows()) { setShortcut(WIN_DESKTOP_SHORTCUT, desktop); - setShortcut(WIN_START_MENU_SHORTCUT, desktop); + setShortcut(WIN_START_MENU_SHORTCUT, menu); } return this; } From 2dcae6480e4af19beebf750a0f5f4370579cf802 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 23:10:13 -0400 Subject: [PATCH 64/83] Consistent log message format --- .../jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java index 55f7a1b93145f..ceda32eb8edad 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java @@ -282,7 +282,7 @@ private void verifyInstalled(JPackageCommand cmd, boolean installed) throws IOEx private void executeLauncher(JPackageCommand cmd) throws IOException { Path launcherPath = cmd.appLauncherPath(name); - if (!cmd.canRunLauncher(String.format("Not running %s launcher", launcherPath))) { + if (!cmd.canRunLauncher(String.format("Not running [%s] launcher", launcherPath))) { return; } From 7be8ad0779dcba81713194f4881ecf5de24fbe5f Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 23:12:45 -0400 Subject: [PATCH 65/83] Bash script to clean jpackage test log files to reduce noise in diff-s --- test/jdk/tools/jpackage/clean_test_output.sh | 84 ++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/jdk/tools/jpackage/clean_test_output.sh diff --git a/test/jdk/tools/jpackage/clean_test_output.sh b/test/jdk/tools/jpackage/clean_test_output.sh new file mode 100644 index 0000000000000..ee61de8429258 --- /dev/null +++ b/test/jdk/tools/jpackage/clean_test_output.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# Copyright (c) 2025, 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. + +# +# Filters output produced by running jpackage test(s). +# + +set -eu +set -o pipefail + + +sed_inplace_option=-i +sed_version_string=$(sed --version 2>&1 | head -1 || true) +if [ "${sed_version_string#sed (GNU sed)}" != "$sed_version_string" ]; then + # GNU sed, the default + : +elif [ "${sed_version_string#sed: illegal option}" != "$sed_version_string" ]; then + # Macos sed + sed_inplace_option="-i ''" +else + echo 'WARNING: Unknown sed variant, assume it is GNU compatible' +fi + + +filterFile () { + local expressions=( + # Strip leading log message timestamp `[19:33:44.713] ` + -e 's/^\[[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\.[0-9]\{3\}\] //' + + # Strip log message timestamps `[19:33:44.713]` + -e 's/\[[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\.[0-9]\{3\}\]//g' + + # Convert variable part of R/O directory path timestamp `#2025-07-24T16:38:13.3589878Z` + -e 's/#[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\.[0-9]\{1,\}Z/#Z/' + + # Strip variable part of temporary directory name `jdk.jpackage5060841750457404688` + -e 's|\([\/]\)jdk\.jpackage[0-9]\{1,\}\b|\1jdk.jpackage|g' + + # Convert PID value `[PID: 131561]` + -e 's/\[PID: [0-9]\{1,\}\]/[PID: ]/' + + # Strip a warning message `Windows Defender may prevent jpackage from functioning` + -e '/Windows Defender may prevent jpackage from functioning/d' + + # Convert variable part of test output directory `out-6268` + -e 's|\bout-[0-9]\{1,\}\b|out-N|g' + + # Convert variable part of test summary `[ OK ] IconTest(AppImage, ResourceDirIcon, DefaultIcon).test; checks=39` + -e 's/^\(.*\bchecks=\)[0-9]\{1,\}\(\r\{0,1\}\)$/\1N\2/' + + # Convert variable part of ldd output `libdl.so.2 => /lib64/libdl.so.2 (0x00007fbf63c81000)` + -e 's/(0x[[:xdigit:]]\{1,\})$/(0xHEX)/' + + # Convert variable part of rpmbuild output `Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.CMO6a9` + -e 's|/rpm-tmp\...*$|/rpm-tmp.V|' + ) + + sed $sed_inplace_option "$1" "${expressions[@]}" +} + + +for f in "$@"; do + filterFile "$f" +done From 9d55c1cee25db48d6cfbf2af06b961e54911dc58 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 31 Jul 2025 23:17:08 -0400 Subject: [PATCH 66/83] Fix a typo --- test/jdk/tools/jpackage/share/AddLShortcutTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index 51bfdf7af2bed..38d505eed6f1e 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -269,7 +269,7 @@ void verify(Collection invokeShortcutSpecs) throws invokeShortcutSpec.execute(); // On Linux, "gtk-launch" is used to launch a .desktop file. It is async and there is no - // wait to make it wait for exit of a process it triggers. + // way to make it wait for exit of a process it triggers. Executor.tryRunMultipleTimes(() -> { if (!Files.exists(expectedOutputFile)) { throw new NoSuchElementException(String.format("[%s] is not avaialble", expectedOutputFile)); From 1c7e0114b6fdc443a0e31189399d4fcd04bcd7a2 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 11:29:38 -0400 Subject: [PATCH 67/83] LinuxHelper: allow empty lines in .desktop files --- .../tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 054bf89bb0ad6..6d4721d6023d9 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -39,6 +39,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -826,7 +827,7 @@ private static final class DesktopFile { TKit.assertEquals("[Desktop Entry]", lines.getFirst(), "Check file header"); } - var stream = lines.stream().skip(1); + var stream = lines.stream().skip(1).filter(Predicate.not(String::isEmpty)); if (verify) { stream = stream.peek(str -> { TKit.assertTextStream("=").predicate(String::contains).apply(List.of(str)); From d7a1dc2e89f1d4607a973bb20a75f1fca9b5df8b Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 11:58:39 -0400 Subject: [PATCH 68/83] WinShortcutVerifier: make it a better fit for JDK-8308349 --- .../jpackage/test/WinShortcutVerifier.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index acd11a116db86..cca904e017e62 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -41,6 +41,7 @@ import java.util.stream.Stream; import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.test.LauncherShortcut.InvokeShortcutSpec; +import jdk.jpackage.test.LauncherShortcut.StartupDirectory; import jdk.jpackage.test.MsiDatabase.Shortcut; import jdk.jpackage.test.WindowsHelper.SpecialFolder; @@ -197,8 +198,8 @@ private static Collection expectLauncherShortcuts(JPackageCommand cmd, final List shortcuts = new ArrayList<>(); - final boolean isWinMenu = WIN_START_MENU_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).isPresent(); - final boolean isDesktop = WIN_DESKTOP_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).isPresent(); + final var winMenu = WIN_START_MENU_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName); + final var desktop = WIN_DESKTOP_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName); final var isUserLocalInstall = WindowsHelper.isUserLocalInstall(cmd); @@ -209,26 +210,30 @@ private static Collection expectLauncherShortcuts(JPackageCommand cmd, installRoot = SpecialFolder.PROGRAM_FILES; } - final var workDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd)); + final var installDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd)); - if (isWinMenu) { + final Function workDir = startupDirectory -> { + return installDir; + }; + + if (winMenu.isPresent()) { ShortcutType type; if (isUserLocalInstall) { type = ShortcutType.USER_START_MENU; } else { type = ShortcutType.COMMON_START_MENU; } - shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, workDir, type)); + shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, winMenu.map(workDir).orElseThrow(), type)); } - if (isDesktop) { + if (desktop.isPresent()) { ShortcutType type; if (isUserLocalInstall) { type = ShortcutType.USER_DESKTOP; } else { type = ShortcutType.COMMON_DESKTOP; } - shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, workDir, type)); + shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, desktop.map(workDir).orElseThrow(), type)); } return shortcuts; From 63a208820560302ee6866a908c27c067b9b0cf2f Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 15:13:07 -0400 Subject: [PATCH 69/83] LinuxHelper: bugfix --- .../helpers/jdk/jpackage/test/LinuxHelper.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 6d4721d6023d9..16b373f41ac85 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -507,21 +507,29 @@ private static void verifyDesktopFile(JPackageCommand cmd, Optional { switch (shortcutWorkDirType) { case DEFAULT -> { return (Path)null; } + case APP_DIR -> { + return cmd.pathToPackageFile(appLayout.appDirectory()); + } + case INSTALL_DIR -> { + return cmd.pathToPackageFile(appLayout.launchersDirectory()); + } default -> { throw new AssertionError(); } } - }).ifPresentOrElse(shortcutWorkDir -> { - var actualShortcutWorkDir = data.findQuotedValue("Path"); + }).map(Path::toString).ifPresentOrElse(shortcutWorkDir -> { + var actualShortcutWorkDir = data.find("Path"); TKit.assertTrue(actualShortcutWorkDir.isPresent(), "Check [Path] key exists"); TKit.assertEquals(actualShortcutWorkDir.get(), shortcutWorkDir, "Check the value of [Path] key"); }, () -> { - TKit.assertTrue(data.findQuotedValue("Path").isEmpty(), "Check there is no [Path] key"); + TKit.assertTrue(data.find("Path").isEmpty(), "Check there is no [Path] key"); }); for (var e : List.>, Function>>of( @@ -531,7 +539,7 @@ private static void verifyDesktopFile(JPackageCommand cmd, Optional Date: Fri, 1 Aug 2025 16:12:25 -0400 Subject: [PATCH 70/83] AddLShortcutTest: modify --- .../jpackage/share/AddLShortcutTest.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index 38d505eed6f1e..5377a1ea869e5 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -34,6 +34,7 @@ import jdk.internal.util.OperatingSystem; import jdk.jpackage.test.AdditionalLauncher; import jdk.jpackage.test.Annotations.ParameterSupplier; +import jdk.jpackage.test.Annotations.Parameter; import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.Executor; import jdk.jpackage.test.FileAssociations; @@ -206,7 +207,11 @@ public static Collection testShortcutStartupDirectoryWindows() { } @Test(ifNotOS = OperatingSystem.MACOS) - public void testInvokeShortcuts() { + @Parameter(value = "DEFAULT") + @Parameter(value = "APP_DIR") + // On Windows, `DEFAULT` and `INSTALL_DIR` are equivalent, run only one of them. + @Parameter(value = "INSTALL_DIR", ifNotOS = OperatingSystem.WINDOWS) + public void testInvokeShortcuts(StartupDirectory startupDirectory) { var testApp = TKit.TEST_SRC_ROOT.resolve("apps/PrintEnv.java"); @@ -220,7 +225,7 @@ public void testInvokeShortcuts() { var shortcutStartupDirectoryVerifier = new ShortcutStartupDirectoryVerifier(name, "a"); - shortcutStartupDirectoryVerifier.applyTo(test, StartupDirectory.DEFAULT); + shortcutStartupDirectoryVerifier.applyTo(test, startupDirectory); test.addInstallVerifier(cmd -> { if (!cmd.isPackageUnpacked("Not invoking launcher shortcuts")) { @@ -353,6 +358,8 @@ private enum StartupDirectoryValueSetter { DEFAULT(""), TRUE("true"), FALSE("false"), + INSTALL_DIR(StartupDirectory.INSTALL_DIR.asStringValue()), + APP_DIR(StartupDirectory.APP_DIR.asStringValue()) ; StartupDirectoryValueSetter(String value) { @@ -368,7 +375,7 @@ void applyToMainLauncher(LauncherShortcut shortcut, JPackageCommand cmd) { cmd.addArgument(shortcut.optionName()); } default -> { - cmd.addArgument(shortcut.optionName() + "=" + value); + cmd.addArguments(shortcut.optionName(), value); } } } @@ -380,12 +387,16 @@ void applyToAdditionalLauncher(LauncherShortcut shortcut, AdditionalLauncher add private final String value; static final List MAIN_LAUNCHER_VALUES = List.of( - StartupDirectoryValueSetter.DEFAULT + StartupDirectoryValueSetter.DEFAULT, + StartupDirectoryValueSetter.INSTALL_DIR, + StartupDirectoryValueSetter.APP_DIR ); static final List ADD_LAUNCHER_VALUES = List.of( StartupDirectoryValueSetter.TRUE, - StartupDirectoryValueSetter.FALSE + StartupDirectoryValueSetter.FALSE, + StartupDirectoryValueSetter.INSTALL_DIR, + StartupDirectoryValueSetter.APP_DIR ); } From 317354fab8f6fc8f479d6a140191ab62b4d9a82e Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 16:34:16 -0400 Subject: [PATCH 71/83] LinuxHelper: bugfix --- .../jpackage/helpers/jdk/jpackage/test/LinuxHelper.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 16b373f41ac85..b99d2bdeceb39 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -514,12 +514,6 @@ private static void verifyDesktopFile(JPackageCommand cmd, Optional { return (Path)null; } - case APP_DIR -> { - return cmd.pathToPackageFile(appLayout.appDirectory()); - } - case INSTALL_DIR -> { - return cmd.pathToPackageFile(appLayout.launchersDirectory()); - } default -> { throw new AssertionError(); } From 9617f830513742080131d6595baab2011da75499 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 16:36:13 -0400 Subject: [PATCH 72/83] AddLShortcutTest: make it a better fit for JDK-8308349 --- test/jdk/tools/jpackage/share/AddLShortcutTest.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index 5377a1ea869e5..aef78ea267f61 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -208,9 +208,6 @@ public static Collection testShortcutStartupDirectoryWindows() { @Test(ifNotOS = OperatingSystem.MACOS) @Parameter(value = "DEFAULT") - @Parameter(value = "APP_DIR") - // On Windows, `DEFAULT` and `INSTALL_DIR` are equivalent, run only one of them. - @Parameter(value = "INSTALL_DIR", ifNotOS = OperatingSystem.WINDOWS) public void testInvokeShortcuts(StartupDirectory startupDirectory) { var testApp = TKit.TEST_SRC_ROOT.resolve("apps/PrintEnv.java"); @@ -358,8 +355,6 @@ private enum StartupDirectoryValueSetter { DEFAULT(""), TRUE("true"), FALSE("false"), - INSTALL_DIR(StartupDirectory.INSTALL_DIR.asStringValue()), - APP_DIR(StartupDirectory.APP_DIR.asStringValue()) ; StartupDirectoryValueSetter(String value) { @@ -387,16 +382,12 @@ void applyToAdditionalLauncher(LauncherShortcut shortcut, AdditionalLauncher add private final String value; static final List MAIN_LAUNCHER_VALUES = List.of( - StartupDirectoryValueSetter.DEFAULT, - StartupDirectoryValueSetter.INSTALL_DIR, - StartupDirectoryValueSetter.APP_DIR + StartupDirectoryValueSetter.DEFAULT ); static final List ADD_LAUNCHER_VALUES = List.of( StartupDirectoryValueSetter.TRUE, - StartupDirectoryValueSetter.FALSE, - StartupDirectoryValueSetter.INSTALL_DIR, - StartupDirectoryValueSetter.APP_DIR + StartupDirectoryValueSetter.FALSE ); } From 0139d1406751483f3fc4d9924c87d80470803929 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 17:25:46 -0400 Subject: [PATCH 73/83] clean_test_output.sh: better --- test/jdk/tools/jpackage/clean_test_output.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/jdk/tools/jpackage/clean_test_output.sh b/test/jdk/tools/jpackage/clean_test_output.sh index ee61de8429258..e472d780dede7 100644 --- a/test/jdk/tools/jpackage/clean_test_output.sh +++ b/test/jdk/tools/jpackage/clean_test_output.sh @@ -73,6 +73,9 @@ filterFile () { # Convert variable part of rpmbuild output `Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.CMO6a9` -e 's|/rpm-tmp\...*$|/rpm-tmp.V|' + + # Convert variable part of stack trace entry `at jdk.jpackage.test.JPackageCommand.execute(JPackageCommand.java:863)` + -e 's/^\(.*\b\.java:\)[0-9]\{1,\}\()\r\{0,1\}\)$/\1N\2/' ) sed $sed_inplace_option "$1" "${expressions[@]}" From 73836c013b88fae548840c53ee0a03d567a48291 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 16:18:07 -0400 Subject: [PATCH 74/83] JPackageCommand: remove path to the unpacked directory from the argument list as it interferes with extracting arguments with optional values. --- .../helpers/jdk/jpackage/test/JPackageCommand.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 674e05f4ba129..311b94ef642ac 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -90,6 +90,7 @@ private JPackageCommand(JPackageCommand cmd, boolean immutable) { outputValidators = cmd.outputValidators; executeInDirectory = cmd.executeInDirectory; winMsiLogFile = cmd.winMsiLogFile; + unpackedPackageDirectory = cmd.unpackedPackageDirectory; } JPackageCommand createImmutableCopy() { @@ -484,7 +485,7 @@ public Path pathToPackageFile(Path path) { Path unpackedPackageDirectory() { verifyIsOfType(PackageType.NATIVE); - return getArgumentValue(UNPACKED_PATH_ARGNAME, () -> null, Path::of); + return unpackedPackageDirectory; } /** @@ -662,7 +663,7 @@ public boolean isPackageUnpacked(String msg) { } public boolean isPackageUnpacked() { - return hasArgument(UNPACKED_PATH_ARGNAME); + return unpackedPackageDirectory != null; } public static void useToolProviderByDefault(ToolProvider jpackageToolProvider) { @@ -1255,11 +1256,7 @@ private void assertFileInAppImage(Path filename, Path expectedPath) { JPackageCommand setUnpackedPackageLocation(Path path) { verifyMutable(); verifyIsOfType(PackageType.NATIVE); - if (path != null) { - setArgumentValue(UNPACKED_PATH_ARGNAME, path); - } else { - removeArgumentWithValue(UNPACKED_PATH_ARGNAME); - } + unpackedPackageDirectory = path; return this; } @@ -1471,6 +1468,7 @@ public void run() { private final Actions verifyActions; private Path executeInDirectory; private Path winMsiLogFile; + private Path unpackedPackageDirectory; private Set readOnlyPathAsserts = Set.of(ReadOnlyPathAssert.values()); private Set appLayoutAsserts = Set.of(AppLayoutAssert.values()); private List>> outputValidators = new ArrayList<>(); @@ -1498,8 +1496,6 @@ public void run() { return null; }).get(); - private static final String UNPACKED_PATH_ARGNAME = "jpt-unpacked-folder"; - // [HH:mm:ss.SSS] private static final Pattern TIMESTAMP_REGEXP = Pattern.compile( "^\\[\\d\\d:\\d\\d:\\d\\d.\\d\\d\\d\\] "); From e80aba5374b457fa9e98f27862105345fe91c7db Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 21:42:52 -0400 Subject: [PATCH 75/83] JPackageCommand: verify names of additional launcher are precisely recorded in .jpackage.xml file --- .../helpers/jdk/jpackage/test/JPackageCommand.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 311b94ef642ac..3a89ba28d2696 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -1157,12 +1157,12 @@ private void assertAppImageFile() { } else { assertFileInAppImage(lookupPath); - if (TKit.isOSX()) { - final Path rootDir = isImagePackageType() ? outputBundle() : - pathToUnpackedPackageFile(appInstallationDirectory()); + final Path rootDir = isImagePackageType() ? outputBundle() : + pathToUnpackedPackageFile(appInstallationDirectory()); - AppImageFile aif = AppImageFile.load(rootDir); + final AppImageFile aif = AppImageFile.load(rootDir); + if (TKit.isOSX()) { boolean expectedValue = MacHelper.appImageSigned(this); boolean actualValue = aif.macSigned(); TKit.assertEquals(expectedValue, actualValue, @@ -1173,6 +1173,11 @@ private void assertAppImageFile() { TKit.assertEquals(expectedValue, actualValue, "Check for unexpected value of property in app image file"); } + + TKit.assertStringListEquals( + addLauncherNames().stream().sorted().toList(), + aif.addLaunchers().keySet().stream().sorted().toList(), + "Check additional launcher names"); } } From 3e63c202b085b897fdee394045c273cedb8e25f0 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 21:40:42 -0400 Subject: [PATCH 76/83] LauncherShortcut: add appImageFilePropertyName() --- .../helpers/jdk/jpackage/test/LauncherShortcut.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java index cf917fa3e4706..5e86f975870b1 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java @@ -85,13 +85,16 @@ static Optional parse(String str) { LauncherShortcut(String propertyName) { this.propertyName = Objects.requireNonNull(propertyName); - this.predefinedAppImagePropertyName = propertyName.substring(propertyName.indexOf('-') + 1); } public String propertyName() { return propertyName; } + public String appImageFilePropertyName() { + return propertyName.substring(propertyName.indexOf('-') + 1); + } + public String optionName() { return "--" + propertyName; } @@ -106,7 +109,7 @@ Optional expectShortcut(JPackageCommand cmd, Optional { - propertyName[0] = this.predefinedAppImagePropertyName; + propertyName[0] = appImageFilePropertyName(); return new PropertyFile(appImage.addLaunchers().get(launcherName)); }).orElseGet(() -> { propertyName[0] = this.propertyName; @@ -161,5 +164,4 @@ private Optional findAddLauncherShortcut(JPackageCommand cmd, } private final String propertyName; - private final String predefinedAppImagePropertyName; } From ff64379899c930bc229feee33667f27b9ef84f5e Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 4 Aug 2025 00:45:23 -0400 Subject: [PATCH 77/83] Use java.time.Duration and java.time.Instant in TKit.waitForFileCreated(). Make it public. --- .../jdk/jpackage/test/PackageTest.java | 9 ++---- .../helpers/jdk/jpackage/test/TKit.java | 29 ++++++++++++------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index 4b4d7c23f5b1c..84453038cd2c8 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -36,6 +36,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -296,13 +297,9 @@ PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa) { Files.deleteIfExists(appOutput); List expectedArgs = testRun.openFiles(testFiles); - TKit.waitForFileCreated(appOutput, 7); + TKit.waitForFileCreated(appOutput, Duration.ofSeconds(7), Duration.ofSeconds(3)); - // Wait a little bit after file has been created to - // make sure there are no pending writes into it. - Thread.sleep(3000); - HelloApp.verifyOutputFile(appOutput, expectedArgs, - Collections.emptyMap()); + HelloApp.verifyOutputFile(appOutput, expectedArgs, Map.of()); }); if (isOfType(cmd, WINDOWS)) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java index 2508db00295ce..d55da7d924a43 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -43,6 +43,8 @@ import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -597,8 +599,14 @@ public static Path createRelativePathCopy(final Path file) { return file; } - static void waitForFileCreated(Path fileToWaitFor, - long timeoutSeconds) throws IOException { + public static void waitForFileCreated(Path fileToWaitFor, + Duration timeout, Duration afterCreatedTimeout) throws IOException { + waitForFileCreated(fileToWaitFor, timeout); + // Wait after the file has been created to ensure it is fully written. + ThrowingConsumer.toConsumer(Thread::sleep).accept(afterCreatedTimeout); + } + + private static void waitForFileCreated(Path fileToWaitFor, Duration timeout) throws IOException { trace(String.format("Wait for file [%s] to be available", fileToWaitFor.toAbsolutePath())); @@ -608,22 +616,23 @@ static void waitForFileCreated(Path fileToWaitFor, Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent(); watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY); - long waitUntil = System.currentTimeMillis() + timeoutSeconds * 1000; + var waitUntil = Instant.now().plus(timeout); for (;;) { - long timeout = waitUntil - System.currentTimeMillis(); - assertTrue(timeout > 0, String.format( - "Check timeout value %d is positive", timeout)); + var remainderTimeout = Instant.now().until(waitUntil); + assertTrue(remainderTimeout.isPositive(), String.format( + "Check timeout value %dms is positive", remainderTimeout.toMillis())); - WatchKey key = ThrowingSupplier.toSupplier(() -> ws.poll(timeout, - TimeUnit.MILLISECONDS)).get(); + WatchKey key = ThrowingSupplier.toSupplier(() -> { + return ws.poll(remainderTimeout.toMillis(), TimeUnit.MILLISECONDS); + }).get(); if (key == null) { - if (fileToWaitFor.toFile().exists()) { + if (Files.exists(fileToWaitFor)) { trace(String.format( "File [%s] is available after poll timeout expired", fileToWaitFor)); return; } - assertUnexpected(String.format("Timeout expired", timeout)); + assertUnexpected(String.format("Timeout expired", remainderTimeout)); } for (WatchEvent event : key.pollEvents()) { From e4cdb22925bede837855decbc3772fa7cc38a709 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 4 Aug 2025 00:46:03 -0400 Subject: [PATCH 78/83] Use TKit.waitForFileCreated() to await for test output file --- test/jdk/tools/jpackage/share/AddLShortcutTest.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index aef78ea267f61..7d7d8b50c1dd1 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -25,18 +25,17 @@ import java.lang.invoke.MethodHandles; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import jdk.internal.util.OperatingSystem; import jdk.jpackage.test.AdditionalLauncher; -import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Parameter; +import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Test; -import jdk.jpackage.test.Executor; import jdk.jpackage.test.FileAssociations; import jdk.jpackage.test.JPackageCommand; import jdk.jpackage.test.LauncherShortcut; @@ -272,11 +271,7 @@ void verify(Collection invokeShortcutSpecs) throws // On Linux, "gtk-launch" is used to launch a .desktop file. It is async and there is no // way to make it wait for exit of a process it triggers. - Executor.tryRunMultipleTimes(() -> { - if (!Files.exists(expectedOutputFile)) { - throw new NoSuchElementException(String.format("[%s] is not avaialble", expectedOutputFile)); - } - }, 3 /* Number of attempts */, 3 /* Seconds between attempts */); + TKit.waitForFileCreated(expectedOutputFile, Duration.ofSeconds(10), Duration.ofSeconds(3)); TKit.assertFileExists(expectedOutputFile); var actualStr = Files.readAllLines(expectedOutputFile).getFirst(); From 2d31e6a57f5bdb8d0e7936669a8cec6a42f31828 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 6 Aug 2025 17:54:28 -0400 Subject: [PATCH 79/83] TKit: bugfix --- test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java index d55da7d924a43..a94dfa135c1ca 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -632,7 +632,7 @@ private static void waitForFileCreated(Path fileToWaitFor, Duration timeout) thr fileToWaitFor)); return; } - assertUnexpected(String.format("Timeout expired", remainderTimeout)); + assertUnexpected(String.format("Timeout %dms expired", remainderTimeout.toMillis())); } for (WatchEvent event : key.pollEvents()) { From 941340365d833966e1fbce7c5a4fe13b5c9a4c62 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 21:43:44 -0400 Subject: [PATCH 80/83] LauncherVerifier: add verifyInAppImageFile() --- .../jdk/jpackage/test/LauncherVerifier.java | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java index ceda32eb8edad..b3ed030c69d8a 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java @@ -77,6 +77,11 @@ public enum Action { VERIFY_UNINSTALLED((verifier, cmd) -> { verifier.verifyInstalled(cmd, false); }), + VERIFY_APP_IMAGE_FILE((verifier, cmd) -> { + if (cmd.isImagePackageType()) { + verifier.verifyInAppImageFile(cmd); + } + }), EXECUTE_LAUNCHER(LauncherVerifier::executeLauncher), ; @@ -91,7 +96,7 @@ private void apply(LauncherVerifier verifier, JPackageCommand cmd) { private final BiConsumer action; static final List VERIFY_APP_IMAGE = List.of( - VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED + VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED, VERIFY_APP_IMAGE_FILE ); static final List VERIFY_DEFAULTS = Stream.concat( @@ -279,6 +284,45 @@ private void verifyInstalled(JPackageCommand cmd, boolean installed) throws IOEx } } + private void verifyInAppImageFile(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.IMAGE); + if (!isMainLauncher()) { + Stream shortcuts; + if (TKit.isWindows()) { + shortcuts = Stream.of(LauncherShortcut.WIN_DESKTOP_SHORTCUT, LauncherShortcut.WIN_START_MENU_SHORTCUT); + } else if (TKit.isLinux()) { + shortcuts = Stream.of(LauncherShortcut.LINUX_SHORTCUT); + } else { + shortcuts = Stream.of(); + } + + var aif = AppImageFile.load(cmd.outputBundle()); + var aifFileName = AppImageFile.getPathInAppImage(Path.of("")).getFileName(); + + var aifProps = Objects.requireNonNull(aif.addLaunchers().get(name)); + + shortcuts.forEach(shortcut -> { + var recordedShortcut = aifProps.get(shortcut.appImageFilePropertyName()); + properties.flatMap(props -> { + return props.findProperty(shortcut.propertyName()); + }).ifPresentOrElse(expectedShortcut -> { + TKit.assertNotNull(recordedShortcut, String.format( + "Check shortcut [%s] of launcher [%s] is recorded in %s file", + shortcut, name, aifFileName)); + TKit.assertEquals( + StartupDirectory.parse(expectedShortcut), + StartupDirectory.parse(recordedShortcut), + String.format("Check the value of shortcut [%s] of launcher [%s] recorded in %s file", + shortcut, name, aifFileName)); + }, () -> { + TKit.assertNull(recordedShortcut, String.format( + "Check shortcut [%s] of launcher [%s] is NOT recorded in %s file", + shortcut, name, aifFileName)); + }); + }); + } + } + private void executeLauncher(JPackageCommand cmd) throws IOException { Path launcherPath = cmd.appLauncherPath(name); From 0c0300eb52adbe6a365f122ecceae9399602c0de Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 27 Jun 2025 10:13:36 -0400 Subject: [PATCH 81/83] 8308349: missing working directory option for launcher when invoked from shortcuts --- .../jpackage/internal/DesktopIntegration.java | 46 +++++++++--- .../jpackage/internal/LinuxFromParams.java | 17 ++--- .../internal/model/LinuxLauncher.java | 11 ++- .../internal/model/LinuxLauncherMixin.java | 20 ++--- .../internal/resources/template.desktop | 1 + .../internal/AddLauncherArguments.java | 8 +- .../jdk/jpackage/internal/Arguments.java | 36 ++++++--- .../jdk/jpackage/internal/FromParams.java | 39 +++++++++- ...arsedLauncherShortcutStartupDirectory.java | 70 ++++++++++++++++++ .../internal/StandardBundlerParam.java | 18 ----- .../internal/model/LauncherShortcut.java | 74 +++++++++++++++++++ .../LauncherShortcutStartupDirectory.java | 67 +++++++++++++++++ .../resources/MainResources.properties | 2 + .../jdk/jpackage/internal/WinFromParams.java | 24 ++---- .../internal/WixAppImageFragmentBuilder.java | 67 +++++++++++------ .../jpackage/internal/model/WinLauncher.java | 15 +++- .../internal/model/WinLauncherMixin.java | 47 +++++++----- 17 files changed, 424 insertions(+), 138 deletions(-) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/ParsedLauncherShortcutStartupDirectory.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcut.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcutStartupDirectory.java diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java index 8de462abac793..38beda708e22c 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java @@ -24,10 +24,10 @@ */ package jdk.jpackage.internal; -import jdk.jpackage.internal.model.LinuxPackage; -import jdk.jpackage.internal.model.LinuxLauncher; -import jdk.jpackage.internal.model.Package; -import jdk.jpackage.internal.model.Launcher; +import static jdk.jpackage.internal.ApplicationImageUtils.createLauncherIconResource; +import static jdk.jpackage.internal.model.LauncherShortcut.toRequest; +import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; + import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -45,12 +45,14 @@ import javax.imageio.ImageIO; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; -import static jdk.jpackage.internal.ApplicationImageUtils.createLauncherIconResource; import jdk.jpackage.internal.model.FileAssociation; +import jdk.jpackage.internal.model.LauncherShortcut; +import jdk.jpackage.internal.model.LinuxLauncher; +import jdk.jpackage.internal.model.LinuxPackage; +import jdk.jpackage.internal.model.Package; import jdk.jpackage.internal.util.CompositeProxy; import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.XmlUtils; -import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; /** * Helper to create files for desktop integration. @@ -77,7 +79,7 @@ private DesktopIntegration(BuildEnv env, LinuxPackage pkg, LinuxLauncher launche // Need desktop and icon files if one of conditions is met: // - there are file associations configured // - user explicitly requested to create a shortcut - boolean withDesktopFile = !associations.isEmpty() || launcher.shortcut().orElse(false); + boolean withDesktopFile = !associations.isEmpty() || toRequest(launcher.shortcut()).orElse(false); var curIconResource = createLauncherIconResource(pkg.app(), launcher, env::createResource); @@ -132,7 +134,7 @@ private DesktopIntegration(BuildEnv env, LinuxPackage pkg, LinuxLauncher launche nestedIntegrations = pkg.app().additionalLaunchers().stream().map(v -> { return (LinuxLauncher)v; }).filter(l -> { - return l.shortcut().orElse(true); + return toRequest(l.shortcut()).orElse(true); }).map(toFunction(l -> { return new DesktopIntegration(env, pkg, l); })).toList(); @@ -225,6 +227,9 @@ private List requiredPackagesSelf() { } private Map createDataForDesktopFile() { + + var installedLayout = pkg.asInstalledPackageApplicationLayout().orElseThrow(); + Map data = new HashMap<>(); data.put("APPLICATION_NAME", launcher.name()); data.put("APPLICATION_DESCRIPTION", launcher.description()); @@ -232,8 +237,27 @@ private Map createDataForDesktopFile() { f -> f.installPath().toString()).orElse(null)); data.put("DEPLOY_BUNDLE_CATEGORY", pkg.menuGroupName()); data.put("APPLICATION_LAUNCHER", Enquoter.forPropertyValues().applyTo( - pkg.asInstalledPackageApplicationLayout().orElseThrow().launchersDirectory().resolve( - launcher.executableNameWithSuffix()).toString())); + installedLayout.launchersDirectory().resolve(launcher.executableNameWithSuffix()).toString())); + data.put("STARTUP_DIRECTORY", launcher.shortcut() + .flatMap(LauncherShortcut::startupDirectory) + .map(startupDirectory -> { + switch (startupDirectory) { + case DEFAULT -> { + return (Path)null; + } + case APP_DIR -> { + return installedLayout.appDirectory(); + } + case INSTALL_DIR -> { + return installedLayout.launchersDirectory(); + } + default -> { + throw new AssertionError(); + } + } + }).map(str -> { + return "Path=" + str; + }).orElse(null)); return data; } @@ -481,7 +505,7 @@ private static int getIconSize(FileAssociation fa) { private final BuildEnv env; private final LinuxPackage pkg; - private final Launcher launcher; + private final LinuxLauncher launcher; private final List associations; diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java index 7dff3cd73ae68..ced77b1aa68d1 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java @@ -29,16 +29,14 @@ import static jdk.jpackage.internal.FromParams.createApplicationBundlerParam; import static jdk.jpackage.internal.FromParams.createPackageBuilder; import static jdk.jpackage.internal.FromParams.createPackageBundlerParam; +import static jdk.jpackage.internal.FromParams.findLauncherShortcut; import static jdk.jpackage.internal.LinuxPackagingPipeline.APPLICATION_LAYOUT; -import static jdk.jpackage.internal.StandardBundlerParam.SHORTCUT_HINT; import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB; import static jdk.jpackage.internal.model.StandardPackageType.LINUX_RPM; import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; import java.io.IOException; import java.util.Map; -import java.util.Optional; -import java.util.stream.Stream; import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.LinuxApplication; import jdk.jpackage.internal.model.LinuxLauncher; @@ -51,11 +49,10 @@ final class LinuxFromParams { private static LinuxApplication createLinuxApplication( Map params) throws ConfigException, IOException { final var launcherFromParams = new LauncherFromParams(); + final var app = createApplicationBuilder(params, toFunction(launcherParams -> { final var launcher = launcherFromParams.create(launcherParams); - final var shortcut = Stream.of(SHORTCUT_HINT, LINUX_SHORTCUT_HINT).map(param -> { - return param.findIn(launcherParams); - }).filter(Optional::isPresent).map(Optional::get).findFirst(); + final var shortcut = findLauncherShortcut(LINUX_SHORTCUT_HINT, params, launcherParams); return LinuxLauncher.create(launcher, new LinuxLauncherMixin.Stub(shortcut)); }), APPLICATION_LAYOUT).create(); return LinuxApplication.create(app); @@ -112,12 +109,8 @@ private static LinuxPackage createLinuxDebPackage( static final BundlerParamInfo DEB_PACKAGE = createPackageBundlerParam( LinuxFromParams::createLinuxDebPackage); - private static final BundlerParamInfo LINUX_SHORTCUT_HINT = new BundlerParamInfo<>( - Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(), - Boolean.class, - params -> false, - (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? false : Boolean.valueOf(s) - ); + private static final BundlerParamInfo LINUX_SHORTCUT_HINT = createStringBundlerParam( + Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId()); private static final BundlerParamInfo LINUX_CATEGORY = createStringBundlerParam( Arguments.CLIOptions.LINUX_CATEGORY.getId()); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncher.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncher.java index 8970f2198c286..c84b5e3bbf59b 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncher.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncher.java @@ -24,6 +24,7 @@ */ package jdk.jpackage.internal.model; +import java.util.HashMap; import java.util.Map; import jdk.jpackage.internal.util.CompositeProxy; @@ -36,9 +37,11 @@ public interface LinuxLauncher extends Launcher, LinuxLauncherMixin { @Override default Map extraAppImageFileData() { - return shortcut().map(v -> { - return Map.of("shortcut", Boolean.toString(v)); - }).orElseGet(Map::of); + Map map = new HashMap<>(); + shortcut().ifPresent(shortcut -> { + shortcut.store(SHORTCUT_ID, map::put); + }); + return map; } /** @@ -52,4 +55,6 @@ default Map extraAppImageFileData() { public static LinuxLauncher create(Launcher launcher, LinuxLauncherMixin mixin) { return CompositeProxy.create(LinuxLauncher.class, launcher, mixin); } + + public static final String SHORTCUT_ID = "linux-shortcut"; } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncherMixin.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncherMixin.java index d5e15101c7e5a..e8ff61ca239c3 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncherMixin.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/model/LinuxLauncherMixin.java @@ -32,24 +32,20 @@ public interface LinuxLauncherMixin { /** - * Gets the start menu shortcut setting of this application launcher. + * Gets the start menu shortcut of this application launcher. *

- * Returns true if this application launcher was requested to have - * the start menu shortcut. - *

- * Returns false if this application launcher was requested not to - * have the start menu shortcut. - *

- * Returns an empty {@link Optional} instance if there was no request about the - * start menu shortcut for this application launcher. + * Returns a non-empty {@link Optional} instance if a request about the start + * menu shortcut for this application launcher was made and an empty + * {@link Optional} instance if there was no request about the start menu + * shortcut for this application launcher. * - * @return the start menu shortcut setting of this application launcher + * @return the start menu shortcut of this application launcher */ - Optional shortcut(); + Optional shortcut(); /** * Default implementation of {@link LinuxLauncherMixin} interface. */ - record Stub(Optional shortcut) implements LinuxLauncherMixin { + record Stub(Optional shortcut) implements LinuxLauncherMixin { } } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop index bd645b77669d5..de7df56845488 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop @@ -2,6 +2,7 @@ Name=APPLICATION_NAME Comment=APPLICATION_DESCRIPTION Exec=APPLICATION_LAUNCHER +STARTUP_DIRECTORY Icon=APPLICATION_ICON Terminal=false Type=Application diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AddLauncherArguments.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AddLauncherArguments.java index d9946075c4f84..93d037c6a4535 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AddLauncherArguments.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AddLauncherArguments.java @@ -36,8 +36,6 @@ import jdk.jpackage.internal.Arguments.CLIOptions; import static jdk.jpackage.internal.StandardBundlerParam.LAUNCHER_DATA; import static jdk.jpackage.internal.StandardBundlerParam.APP_NAME; -import static jdk.jpackage.internal.StandardBundlerParam.MENU_HINT; -import static jdk.jpackage.internal.StandardBundlerParam.SHORTCUT_HINT; /* * AddLauncherArguments @@ -135,16 +133,16 @@ private void initLauncherMap() { Arguments.putUnlessNull(bundleParams, CLIOptions.WIN_CONSOLE_HINT.getId(), getOptionValue(CLIOptions.WIN_CONSOLE_HINT)); - Arguments.putUnlessNull(bundleParams, SHORTCUT_HINT.getID(), + Arguments.putUnlessNull(bundleParams, CLIOptions.WIN_SHORTCUT_HINT.getId(), getOptionValue(CLIOptions.WIN_SHORTCUT_HINT)); - Arguments.putUnlessNull(bundleParams, MENU_HINT.getID(), + Arguments.putUnlessNull(bundleParams, CLIOptions.WIN_MENU_HINT.getId(), getOptionValue(CLIOptions.WIN_MENU_HINT)); } if (OperatingSystem.isLinux()) { Arguments.putUnlessNull(bundleParams, CLIOptions.LINUX_CATEGORY.getId(), getOptionValue(CLIOptions.LINUX_CATEGORY)); - Arguments.putUnlessNull(bundleParams, SHORTCUT_HINT.getID(), + Arguments.putUnlessNull(bundleParams, CLIOptions.LINUX_SHORTCUT_HINT.getId(), getOptionValue(CLIOptions.LINUX_SHORTCUT_HINT)); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java index 4700231a16248..ffcc45d88418c 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java @@ -37,6 +37,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.ResourceBundle; @@ -348,16 +349,13 @@ public enum CLIOptions { WIN_UPDATE_URL ("win-update-url", OptionCategories.PLATFORM_WIN), - WIN_MENU_HINT ("win-menu", OptionCategories.PLATFORM_WIN, () -> { - setOptionValue("win-menu", true); - }), + WIN_MENU_HINT ("win-menu", OptionCategories.PLATFORM_WIN, + createArgumentWithOptionalValueAction("win-menu")), WIN_MENU_GROUP ("win-menu-group", OptionCategories.PLATFORM_WIN), - WIN_SHORTCUT_HINT ("win-shortcut", - OptionCategories.PLATFORM_WIN, () -> { - setOptionValue("win-shortcut", true); - }), + WIN_SHORTCUT_HINT ("win-shortcut", OptionCategories.PLATFORM_WIN, + createArgumentWithOptionalValueAction("win-shortcut")), WIN_SHORTCUT_PROMPT ("win-shortcut-prompt", OptionCategories.PLATFORM_WIN, () -> { @@ -396,10 +394,8 @@ public enum CLIOptions { LINUX_PACKAGE_DEPENDENCIES ("linux-package-deps", OptionCategories.PLATFORM_LINUX), - LINUX_SHORTCUT_HINT ("linux-shortcut", - OptionCategories.PLATFORM_LINUX, () -> { - setOptionValue("linux-shortcut", true); - }), + LINUX_SHORTCUT_HINT ("linux-shortcut", OptionCategories.PLATFORM_LINUX, + createArgumentWithOptionalValueAction("linux-shortcut")), LINUX_MENU_GROUP ("linux-menu-group", OptionCategories.PLATFORM_LINUX); @@ -478,9 +474,27 @@ private static void nextArg() { context().pos++; } + private static void prevArg() { + Objects.checkIndex(context().pos, context().argList.size()); + context().pos--; + } + private static boolean hasNextArg() { return context().pos < context().argList.size(); } + + private static Runnable createArgumentWithOptionalValueAction(String option) { + Objects.requireNonNull(option); + return () -> { + var value = popArg(); + if (value.startsWith("-")) { + prevArg(); + setOptionValue(option, true); + } else { + setOptionValue(option, value); + } + }; + } } enum OptionCategories { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java index 5e940aba18b95..ebcd8ff3b6ce4 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java @@ -24,6 +24,9 @@ */ package jdk.jpackage.internal; +import static jdk.jpackage.internal.Arguments.CLIOptions.LINUX_SHORTCUT_HINT; +import static jdk.jpackage.internal.Arguments.CLIOptions.WIN_MENU_HINT; +import static jdk.jpackage.internal.Arguments.CLIOptions.WIN_SHORTCUT_HINT; import static jdk.jpackage.internal.StandardBundlerParam.ABOUT_URL; import static jdk.jpackage.internal.StandardBundlerParam.ADD_LAUNCHERS; import static jdk.jpackage.internal.StandardBundlerParam.ADD_MODULES; @@ -49,6 +52,7 @@ import static jdk.jpackage.internal.StandardBundlerParam.VERSION; import static jdk.jpackage.internal.StandardBundlerParam.hasPredefinedAppImage; import static jdk.jpackage.internal.StandardBundlerParam.isRuntimeInstaller; +import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; import java.io.IOException; import java.nio.file.Path; @@ -63,6 +67,8 @@ import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.ExternalApplication.LauncherInfo; import jdk.jpackage.internal.model.Launcher; +import jdk.jpackage.internal.model.LauncherShortcut; +import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory; import jdk.jpackage.internal.model.PackageType; import jdk.jpackage.internal.model.RuntimeLayout; import jdk.jpackage.internal.util.function.ThrowingFunction; @@ -165,6 +171,34 @@ static Optional getCurrentPackage(Map findLauncherShortcut( + BundlerParamInfo shortcutParam, + Map mainParams, + Map launcherParams) { + + Optional launcherValue; + if (launcherParams == mainParams) { + // The main launcher + launcherValue = Optional.empty(); + } else { + launcherValue = shortcutParam.findIn(launcherParams); + } + + return launcherValue.map(ParsedLauncherShortcutStartupDirectory::parseForAddLauncher).or(() -> { + return Optional.ofNullable(mainParams.get(shortcutParam.getID())).map(toFunction(value -> { + if (value instanceof Boolean) { + return new ParsedLauncherShortcutStartupDirectory(LauncherShortcutStartupDirectory.DEFAULT); + } else { + try { + return ParsedLauncherShortcutStartupDirectory.parseForMainLauncher((String)value); + } catch (IllegalArgumentException ex) { + throw I18N.buildConfigException("error.invalid-option-value", value, "--" + shortcutParam.getID()).create(); + } + } + })); + }).map(ParsedLauncherShortcutStartupDirectory::value).map(LauncherShortcut::new); + } + private static ApplicationLaunchers createLaunchers( Map params, Function, Launcher> launcherMapper) { @@ -195,8 +229,9 @@ private static ApplicationLaunchers createLaunchers( // mainParams), APP_NAME.fetchFrom(launcherParams))); launcherParams.put(DESCRIPTION.getID(), DESCRIPTION.fetchFrom(mainParams)); } - return AddLauncherArguments.merge(mainParams, launcherParams, ICON.getID(), ADD_LAUNCHERS - .getID(), FILE_ASSOCIATIONS.getID()); + return AddLauncherArguments.merge(mainParams, launcherParams, ICON.getID(), + ADD_LAUNCHERS.getID(), FILE_ASSOCIATIONS.getID(), WIN_MENU_HINT.getId(), + WIN_SHORTCUT_HINT.getId(), LINUX_SHORTCUT_HINT.getId()); } static final BundlerParamInfo APPLICATION = createApplicationBundlerParam(null); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ParsedLauncherShortcutStartupDirectory.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ParsedLauncherShortcutStartupDirectory.java new file mode 100644 index 0000000000000..6e01f174fb4fb --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ParsedLauncherShortcutStartupDirectory.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025, 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.Objects; +import java.util.Optional; +import java.util.stream.Stream; +import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory; + +record ParsedLauncherShortcutStartupDirectory(Optional value) { + + ParsedLauncherShortcutStartupDirectory { + Objects.requireNonNull(value); + } + + ParsedLauncherShortcutStartupDirectory() { + this(Optional.empty()); + } + + ParsedLauncherShortcutStartupDirectory(LauncherShortcutStartupDirectory value) { + this(Optional.of(value)); + } + + static ParsedLauncherShortcutStartupDirectory parseForMainLauncher(String str) { + return parse(str, + LauncherShortcutStartupDirectory.APP_DIR, + LauncherShortcutStartupDirectory.INSTALL_DIR + ).map(ParsedLauncherShortcutStartupDirectory::new).orElseThrow(IllegalArgumentException::new); + } + + static ParsedLauncherShortcutStartupDirectory parseForAddLauncher(String str) { + return parse(str, LauncherShortcutStartupDirectory.values()).map(ParsedLauncherShortcutStartupDirectory::new).orElseGet(() -> { + if (Boolean.valueOf(str)) { + return new ParsedLauncherShortcutStartupDirectory(LauncherShortcutStartupDirectory.DEFAULT); + } else { + return new ParsedLauncherShortcutStartupDirectory(); + } + }); + } + + private static Optional parse(String str, LauncherShortcutStartupDirectory... recognizedValues) { + Objects.requireNonNull(str); + return Stream.of(recognizedValues).filter(v -> { + return str.equals(v.asStringValue()); + }).findFirst(); + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java index 076d6bfc8950e..6b89bb3ee65f5 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java @@ -307,24 +307,6 @@ final class StandardBundlerParam { true : Boolean.valueOf(s) ); - static final BundlerParamInfo SHORTCUT_HINT = - new BundlerParamInfo<>( - "shortcut-hint", // not directly related to a CLI option - Boolean.class, - params -> true, // defaults to true - (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? - true : Boolean.valueOf(s) - ); - - static final BundlerParamInfo MENU_HINT = - new BundlerParamInfo<>( - "menu-hint", // not directly related to a CLI option - Boolean.class, - params -> true, // defaults to true - (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? - true : Boolean.valueOf(s) - ); - static final BundlerParamInfo RESOURCE_DIR = new BundlerParamInfo<>( Arguments.CLIOptions.RESOURCE_DIR.getId(), diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcut.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcut.java new file mode 100644 index 0000000000000..4188bccb07387 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcut.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025, 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.model; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; + +/** + * A shortcut to launch an application launcher. + */ +public record LauncherShortcut(Optional startupDirectory) { + + public LauncherShortcut { + Objects.requireNonNull(startupDirectory); + } + + public LauncherShortcut(LauncherShortcutStartupDirectory startupDirectory) { + this(Optional.of(startupDirectory)); + } + + public LauncherShortcut() { + this(Optional.empty()); + } + + void store(String propertyName, BiConsumer sink) { + Objects.requireNonNull(propertyName); + Objects.requireNonNull(sink); + if (startupDirectory.isEmpty()) { + sink.accept(propertyName, Boolean.FALSE.toString()); + } else { + startupDirectory.ifPresent(v -> { + sink.accept(propertyName, v.asStringValue()); + }); + } + } + + /** + * Converts the given shortcut into a shortcut request. + *

+ * Returns true if shortcut was explicitly requested. + *

+ * Returns false if no shortcut was explicitly requested. + *

+ * Returns an empty {@link Optional} instance if there was no shortcut request. + * + * @return shortcut request + */ + public static Optional toRequest(Optional shortcut) { + return shortcut.map(v -> v.startupDirectory().isPresent()); + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcutStartupDirectory.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcutStartupDirectory.java new file mode 100644 index 0000000000000..5fbf1db78506f --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcutStartupDirectory.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025, 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.model; + +import java.util.Objects; + +/** + * The directory in which to run an application launcher when it is started from + * a shortcut. + */ +public enum LauncherShortcutStartupDirectory { + + /** + * Platform-specific default value. + *

+ * On Windows, it indicates that the startup directory should be the package's + * installation directory. I.e. same as {@link #INSTALL_DIR}. + *

+ * On Linux, it indicates that a shortcut doesn't have the startup directory + * configured explicitly. + */ + DEFAULT("true"), + + /** + * The 'app' directory in the installed application app image. This is the + * directory that is referenced with {@link ApplicationLayout#appDirectory()} + * method. + */ + APP_DIR("app-dir"), + + /** + * The installation directory of the package. + */ + INSTALL_DIR("install-dir"); + + LauncherShortcutStartupDirectory(String stringValue) { + this.stringValue = Objects.requireNonNull(stringValue); + } + + public String asStringValue() { + return stringValue; + } + + private final String stringValue; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties index ae225e15ea2f1..684a97bc1bdea 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties @@ -82,6 +82,8 @@ error.invalid-app-image=Error: app-image dir "{0}" generated by another jpackage error.invalid-install-dir=Invalid installation directory "{0}" +error.invalid-option-value=Invalid value "{0}" of option {1} + MSG_BundlerFailed=Error: Bundler "{1}" ({0}) failed to produce a package MSG_BundlerConfigException=Bundler {0} skipped because of a configuration problem: {1} \n\ Advice to fix: {2} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java index 95f16d09575dd..e2259535058ff 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java @@ -24,24 +24,19 @@ */ package jdk.jpackage.internal; -import static java.util.stream.Collectors.toSet; import static jdk.jpackage.internal.BundlerParamInfo.createBooleanBundlerParam; import static jdk.jpackage.internal.BundlerParamInfo.createStringBundlerParam; import static jdk.jpackage.internal.FromParams.createApplicationBuilder; import static jdk.jpackage.internal.FromParams.createApplicationBundlerParam; import static jdk.jpackage.internal.FromParams.createPackageBuilder; import static jdk.jpackage.internal.FromParams.createPackageBundlerParam; -import static jdk.jpackage.internal.StandardBundlerParam.MENU_HINT; +import static jdk.jpackage.internal.FromParams.findLauncherShortcut; import static jdk.jpackage.internal.StandardBundlerParam.RESOURCE_DIR; -import static jdk.jpackage.internal.StandardBundlerParam.SHORTCUT_HINT; import static jdk.jpackage.internal.WinPackagingPipeline.APPLICATION_LAYOUT; import static jdk.jpackage.internal.model.StandardPackageType.WIN_MSI; -import static jdk.jpackage.internal.model.WinLauncherMixin.WinShortcut.WIN_SHORTCUT_DESKTOP; -import static jdk.jpackage.internal.model.WinLauncherMixin.WinShortcut.WIN_SHORTCUT_START_MENU; import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; import java.io.IOException; -import java.util.List; import java.util.Map; import java.util.UUID; import jdk.jpackage.internal.model.ConfigException; @@ -63,18 +58,11 @@ private static WinApplication createWinApplication( final boolean isConsole = CONSOLE_HINT.findIn(launcherParams).orElse(false); - final var shortcuts = Map.of(WIN_SHORTCUT_DESKTOP, List.of(SHORTCUT_HINT, - WIN_SHORTCUT_HINT), WIN_SHORTCUT_START_MENU, List.of(MENU_HINT, - WIN_MENU_HINT)).entrySet().stream().filter(e -> { + final var startMenuShortcut = findLauncherShortcut(WIN_MENU_HINT, params, launcherParams); - final var shortcutParams = e.getValue(); + final var desktopShortcut = findLauncherShortcut(WIN_SHORTCUT_HINT, params, launcherParams); - return shortcutParams.get(0).findIn(launcherParams).orElseGet(() -> { - return shortcutParams.get(1).findIn(launcherParams).orElse(false); - }); - }).map(Map.Entry::getKey).collect(toSet()); - - return WinLauncher.create(launcher, new WinLauncherMixin.Stub(isConsole, shortcuts)); + return WinLauncher.create(launcher, new WinLauncherMixin.Stub(isConsole, startMenuShortcut, desktopShortcut)); }), APPLICATION_LAYOUT).create(); @@ -117,10 +105,10 @@ private static WinMsiPackage createWinMsiPackage(Map par static final BundlerParamInfo MSI_PACKAGE = createPackageBundlerParam( WinFromParams::createWinMsiPackage); - private static final BundlerParamInfo WIN_MENU_HINT = createBooleanBundlerParam( + private static final BundlerParamInfo WIN_MENU_HINT = createStringBundlerParam( Arguments.CLIOptions.WIN_MENU_HINT.getId()); - private static final BundlerParamInfo WIN_SHORTCUT_HINT = createBooleanBundlerParam( + private static final BundlerParamInfo WIN_SHORTCUT_HINT = createStringBundlerParam( Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId()); public static final BundlerParamInfo CONSOLE_HINT = createBooleanBundlerParam( diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java index 7e400c5be29c5..e0a5246d1ffe3 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java @@ -25,12 +25,9 @@ package jdk.jpackage.internal; -import jdk.jpackage.internal.model.WinLauncher; -import jdk.jpackage.internal.model.WinMsiPackage; -import jdk.jpackage.internal.model.Launcher; -import jdk.jpackage.internal.model.DottedVersion; -import jdk.jpackage.internal.model.ApplicationLayout; -import jdk.jpackage.internal.util.PathGroup; +import static java.util.stream.Collectors.toMap; +import static jdk.jpackage.internal.util.CollectionUtils.toCollection; + import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -50,7 +47,6 @@ import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; -import static java.util.stream.Collectors.toMap; import java.util.stream.Stream; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; @@ -60,15 +56,19 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; -import static jdk.jpackage.internal.util.CollectionUtils.toCollection; -import jdk.jpackage.internal.model.WinLauncherMixin.WinShortcut; import jdk.jpackage.internal.WixToolset.WixToolsetType; import jdk.jpackage.internal.model.AppImageLayout; +import jdk.jpackage.internal.model.ApplicationLayout; +import jdk.jpackage.internal.model.DottedVersion; import jdk.jpackage.internal.model.FileAssociation; +import jdk.jpackage.internal.model.Launcher; +import jdk.jpackage.internal.model.LauncherShortcut; +import jdk.jpackage.internal.model.WinLauncher; +import jdk.jpackage.internal.model.WinMsiPackage; +import jdk.jpackage.internal.util.PathGroup; import jdk.jpackage.internal.util.PathUtils; -import jdk.jpackage.internal.util.XmlUtils; import jdk.jpackage.internal.util.XmlConsumer; -import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; +import jdk.jpackage.internal.util.XmlUtils; import org.w3c.dom.NodeList; /** @@ -352,7 +352,7 @@ private static Config cfg() { private final Config cfg; private final Id id; - }; + } private static void addComponentGroup(XMLStreamWriter xml, String id, List componentIds) throws XMLStreamException, IOException { @@ -469,7 +469,21 @@ private void addShortcutComponentGroup(XMLStreamWriter xml) throws launcher.executableNameWithSuffix()); if (folder.isRequestedFor(launcher)) { - String componentId = addShortcutComponent(xml, launcherPath, folder); + var workDirectory = folder.shortcut(launcher).startupDirectory().map(v -> { + switch (v) { + case DEFAULT, INSTALL_DIR -> { + return INSTALLDIR; + } + case APP_DIR -> { + return installedAppImage.appDirectory(); + } + default -> { + throw new AssertionError(); + } + } + }).orElseThrow(); + + String componentId = addShortcutComponent(xml, launcherPath, folder, workDirectory); if (componentId != null) { Path folderPath = folder.getPath(this); @@ -499,23 +513,26 @@ private void addShortcutComponentGroup(XMLStreamWriter xml) throws } private String addShortcutComponent(XMLStreamWriter xml, Path launcherPath, - ShortcutsFolder folder) throws XMLStreamException, IOException { + ShortcutsFolder folder, Path shortcutWorkDir) throws XMLStreamException, IOException { Objects.requireNonNull(folder); if (!INSTALLDIR.equals(launcherPath.getName(0))) { throw throwInvalidPathException(launcherPath); } + if (!INSTALLDIR.equals(shortcutWorkDir.getName(0))) { + throw throwInvalidPathException(shortcutWorkDir); + } + String launcherBasename = PathUtils.replaceSuffix( IOUtils.getFileName(launcherPath), "").toString(); Path shortcutPath = folder.getPath(this).resolve(launcherBasename); return addComponent(xml, shortcutPath, Component.Shortcut, unused -> { xml.writeAttribute("Name", launcherBasename); - xml.writeAttribute("WorkingDirectory", INSTALLDIR.toString()); + xml.writeAttribute("WorkingDirectory", Id.Folder.of(shortcutWorkDir)); xml.writeAttribute("Advertise", "no"); - xml.writeAttribute("Target", String.format("[#%s]", - Component.File.idOf(launcherPath))); + xml.writeAttribute("Target", String.format("[#%s]", Id.File.of(launcherPath))); }); } @@ -906,15 +923,15 @@ private static IllegalArgumentException throwInvalidPathException(Path v) { } enum ShortcutsFolder { - ProgramMenu(PROGRAM_MENU_PATH, WinShortcut.WIN_SHORTCUT_START_MENU, + ProgramMenu(PROGRAM_MENU_PATH, WinLauncher::startMenuShortcut, "JP_INSTALL_STARTMENU_SHORTCUT", "JpStartMenuShortcutPrompt"), - Desktop(DESKTOP_PATH, WinShortcut.WIN_SHORTCUT_DESKTOP, + Desktop(DESKTOP_PATH, WinLauncher::desktopShortcut, "JP_INSTALL_DESKTOP_SHORTCUT", "JpDesktopShortcutPrompt"); - private ShortcutsFolder(Path root, WinShortcut shortcutId, + private ShortcutsFolder(Path root, Function> shortcut, String property, String wixVariableName) { this.root = root; - this.shortcutId = shortcutId; + this.shortcut = shortcut; this.wixVariableName = wixVariableName; this.property = property; } @@ -927,7 +944,11 @@ Path getPath(WixAppImageFragmentBuilder outer) { } boolean isRequestedFor(WinLauncher launcher) { - return launcher.shortcuts().contains(shortcutId); + return LauncherShortcut.toRequest(shortcut.apply(launcher)).orElse(false); + } + + LauncherShortcut shortcut(WinLauncher launcher) { + return shortcut.apply(launcher).orElseThrow(); } String getWixVariableName() { @@ -947,7 +968,7 @@ static Set getForPackage(WinMsiPackage pkg) { private final Path root; private final String property; private final String wixVariableName; - private final WinShortcut shortcutId; + private final Function> shortcut; } private boolean systemWide; diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinLauncher.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinLauncher.java index b10ca99d483ec..3052029a33c05 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinLauncher.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinLauncher.java @@ -24,9 +24,8 @@ */ package jdk.jpackage.internal.model; -import static java.util.stream.Collectors.toMap; - import java.io.InputStream; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import jdk.jpackage.internal.resources.ResourceLocator; @@ -47,10 +46,20 @@ default InputStream executableResource() { @Override default Map extraAppImageFileData() { - return shortcuts().stream().collect(toMap(WinShortcut::name, v -> Boolean.toString(true))); + Map map = new HashMap<>(); + desktopShortcut().ifPresent(shortcut -> { + shortcut.store(SHORTCUT_DESKTOP_ID, map::put); + }); + startMenuShortcut().ifPresent(shortcut -> { + shortcut.store(SHORTCUT_START_MENU_ID, map::put); + }); + return map; } public static WinLauncher create(Launcher launcher, WinLauncherMixin mixin) { return CompositeProxy.create(WinLauncher.class, launcher, mixin); } + + public static final String SHORTCUT_START_MENU_ID = "win-menu"; + public static final String SHORTCUT_DESKTOP_ID = "win-shortcut"; } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinLauncherMixin.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinLauncherMixin.java index 65b6a1bab46f2..1762678434b59 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinLauncherMixin.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinLauncherMixin.java @@ -24,30 +24,37 @@ */ package jdk.jpackage.internal.model; -import java.util.Set; +import java.util.Optional; public interface WinLauncherMixin { boolean isConsole(); - enum WinShortcut { - WIN_SHORTCUT_DESKTOP("shortcut"), - WIN_SHORTCUT_START_MENU("menu"), - ; - - WinShortcut(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - private final String name; - } - - Set shortcuts(); - - record Stub(boolean isConsole, Set shortcuts) implements WinLauncherMixin { + /** + * Gets the start menu shortcut of this application launcher. + *

+ * Returns a non-empty {@link Optional} instance if a request about the start + * menu shortcut for this application launcher was made and an empty + * {@link Optional} instance if there was no request about the start menu + * shortcut for this application launcher. + * + * @return the start menu shortcut of this application launcher + */ + Optional startMenuShortcut(); + + /** + * Gets the desktop shortcut of this application launcher. + *

+ * Returns a non-empty {@link Optional} instance if a request about the desktop + * shortcut for this application launcher was made and an empty {@link Optional} + * instance if there was no request about the desktop shortcut for this + * application launcher. + * + * @return the start menu shortcut of this application launcher + */ + Optional desktopShortcut(); + + record Stub(boolean isConsole, Optional startMenuShortcut, + Optional desktopShortcut) implements WinLauncherMixin { } } From 397abf83bb1fdf7b614d38e117f9c0952c29d102 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 23:44:33 -0400 Subject: [PATCH 82/83] Revert excessive changes of JDK-8308349 patch irrelevant to JDK-8364564 --- .../jpackage/internal/DesktopIntegration.java | 21 ------ .../jpackage/internal/LinuxFromParams.java | 8 ++- .../internal/resources/template.desktop | 1 - .../jdk/jpackage/internal/Arguments.java | 36 +++------- .../jdk/jpackage/internal/FromParams.java | 29 ++++---- ...arsedLauncherShortcutStartupDirectory.java | 70 ------------------- .../LauncherShortcutStartupDirectory.java | 16 +---- .../resources/MainResources.properties | 2 - .../jdk/jpackage/internal/WinFromParams.java | 4 +- .../internal/WixAppImageFragmentBuilder.java | 5 +- 10 files changed, 35 insertions(+), 157 deletions(-) delete mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/ParsedLauncherShortcutStartupDirectory.java diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java index 38beda708e22c..476ca3201cef0 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java @@ -46,7 +46,6 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import jdk.jpackage.internal.model.FileAssociation; -import jdk.jpackage.internal.model.LauncherShortcut; import jdk.jpackage.internal.model.LinuxLauncher; import jdk.jpackage.internal.model.LinuxPackage; import jdk.jpackage.internal.model.Package; @@ -238,26 +237,6 @@ private Map createDataForDesktopFile() { data.put("DEPLOY_BUNDLE_CATEGORY", pkg.menuGroupName()); data.put("APPLICATION_LAUNCHER", Enquoter.forPropertyValues().applyTo( installedLayout.launchersDirectory().resolve(launcher.executableNameWithSuffix()).toString())); - data.put("STARTUP_DIRECTORY", launcher.shortcut() - .flatMap(LauncherShortcut::startupDirectory) - .map(startupDirectory -> { - switch (startupDirectory) { - case DEFAULT -> { - return (Path)null; - } - case APP_DIR -> { - return installedLayout.appDirectory(); - } - case INSTALL_DIR -> { - return installedLayout.launchersDirectory(); - } - default -> { - throw new AssertionError(); - } - } - }).map(str -> { - return "Path=" + str; - }).orElse(null)); return data; } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java index ced77b1aa68d1..6967dea111ee8 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java @@ -109,8 +109,12 @@ private static LinuxPackage createLinuxDebPackage( static final BundlerParamInfo DEB_PACKAGE = createPackageBundlerParam( LinuxFromParams::createLinuxDebPackage); - private static final BundlerParamInfo LINUX_SHORTCUT_HINT = createStringBundlerParam( - Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId()); + private static final BundlerParamInfo LINUX_SHORTCUT_HINT = new BundlerParamInfo<>( + Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(), + Boolean.class, + params -> false, + (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? false : Boolean.valueOf(s) + ); private static final BundlerParamInfo LINUX_CATEGORY = createStringBundlerParam( Arguments.CLIOptions.LINUX_CATEGORY.getId()); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop index de7df56845488..bd645b77669d5 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop @@ -2,7 +2,6 @@ Name=APPLICATION_NAME Comment=APPLICATION_DESCRIPTION Exec=APPLICATION_LAUNCHER -STARTUP_DIRECTORY Icon=APPLICATION_ICON Terminal=false Type=Application diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java index ffcc45d88418c..4700231a16248 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java @@ -37,7 +37,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.ResourceBundle; @@ -349,13 +348,16 @@ public enum CLIOptions { WIN_UPDATE_URL ("win-update-url", OptionCategories.PLATFORM_WIN), - WIN_MENU_HINT ("win-menu", OptionCategories.PLATFORM_WIN, - createArgumentWithOptionalValueAction("win-menu")), + WIN_MENU_HINT ("win-menu", OptionCategories.PLATFORM_WIN, () -> { + setOptionValue("win-menu", true); + }), WIN_MENU_GROUP ("win-menu-group", OptionCategories.PLATFORM_WIN), - WIN_SHORTCUT_HINT ("win-shortcut", OptionCategories.PLATFORM_WIN, - createArgumentWithOptionalValueAction("win-shortcut")), + WIN_SHORTCUT_HINT ("win-shortcut", + OptionCategories.PLATFORM_WIN, () -> { + setOptionValue("win-shortcut", true); + }), WIN_SHORTCUT_PROMPT ("win-shortcut-prompt", OptionCategories.PLATFORM_WIN, () -> { @@ -394,8 +396,10 @@ public enum CLIOptions { LINUX_PACKAGE_DEPENDENCIES ("linux-package-deps", OptionCategories.PLATFORM_LINUX), - LINUX_SHORTCUT_HINT ("linux-shortcut", OptionCategories.PLATFORM_LINUX, - createArgumentWithOptionalValueAction("linux-shortcut")), + LINUX_SHORTCUT_HINT ("linux-shortcut", + OptionCategories.PLATFORM_LINUX, () -> { + setOptionValue("linux-shortcut", true); + }), LINUX_MENU_GROUP ("linux-menu-group", OptionCategories.PLATFORM_LINUX); @@ -474,27 +478,9 @@ private static void nextArg() { context().pos++; } - private static void prevArg() { - Objects.checkIndex(context().pos, context().argList.size()); - context().pos--; - } - private static boolean hasNextArg() { return context().pos < context().argList.size(); } - - private static Runnable createArgumentWithOptionalValueAction(String option) { - Objects.requireNonNull(option); - return () -> { - var value = popArg(); - if (value.startsWith("-")) { - prevArg(); - setOptionValue(option, true); - } else { - setOptionValue(option, value); - } - }; - } } enum OptionCategories { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java index ebcd8ff3b6ce4..34818fafc94b5 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java @@ -52,7 +52,6 @@ import static jdk.jpackage.internal.StandardBundlerParam.VERSION; import static jdk.jpackage.internal.StandardBundlerParam.hasPredefinedAppImage; import static jdk.jpackage.internal.StandardBundlerParam.isRuntimeInstaller; -import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; import java.io.IOException; import java.nio.file.Path; @@ -172,11 +171,11 @@ static Optional getCurrentPackage(Map findLauncherShortcut( - BundlerParamInfo shortcutParam, + BundlerParamInfo shortcutParam, Map mainParams, Map launcherParams) { - Optional launcherValue; + Optional launcherValue; if (launcherParams == mainParams) { // The main launcher launcherValue = Optional.empty(); @@ -184,19 +183,17 @@ static Optional findLauncherShortcut( launcherValue = shortcutParam.findIn(launcherParams); } - return launcherValue.map(ParsedLauncherShortcutStartupDirectory::parseForAddLauncher).or(() -> { - return Optional.ofNullable(mainParams.get(shortcutParam.getID())).map(toFunction(value -> { - if (value instanceof Boolean) { - return new ParsedLauncherShortcutStartupDirectory(LauncherShortcutStartupDirectory.DEFAULT); - } else { - try { - return ParsedLauncherShortcutStartupDirectory.parseForMainLauncher((String)value); - } catch (IllegalArgumentException ex) { - throw I18N.buildConfigException("error.invalid-option-value", value, "--" + shortcutParam.getID()).create(); - } - } - })); - }).map(ParsedLauncherShortcutStartupDirectory::value).map(LauncherShortcut::new); + return launcherValue.map(withShortcut -> { + if (withShortcut) { + return Optional.of(LauncherShortcutStartupDirectory.DEFAULT); + } else { + return Optional.empty(); + } + }).or(() -> { + return shortcutParam.findIn(mainParams).map(_ -> { + return Optional.of(LauncherShortcutStartupDirectory.DEFAULT); + }); + }).map(LauncherShortcut::new); } private static ApplicationLaunchers createLaunchers( diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ParsedLauncherShortcutStartupDirectory.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ParsedLauncherShortcutStartupDirectory.java deleted file mode 100644 index 6e01f174fb4fb..0000000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ParsedLauncherShortcutStartupDirectory.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2025, 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.Objects; -import java.util.Optional; -import java.util.stream.Stream; -import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory; - -record ParsedLauncherShortcutStartupDirectory(Optional value) { - - ParsedLauncherShortcutStartupDirectory { - Objects.requireNonNull(value); - } - - ParsedLauncherShortcutStartupDirectory() { - this(Optional.empty()); - } - - ParsedLauncherShortcutStartupDirectory(LauncherShortcutStartupDirectory value) { - this(Optional.of(value)); - } - - static ParsedLauncherShortcutStartupDirectory parseForMainLauncher(String str) { - return parse(str, - LauncherShortcutStartupDirectory.APP_DIR, - LauncherShortcutStartupDirectory.INSTALL_DIR - ).map(ParsedLauncherShortcutStartupDirectory::new).orElseThrow(IllegalArgumentException::new); - } - - static ParsedLauncherShortcutStartupDirectory parseForAddLauncher(String str) { - return parse(str, LauncherShortcutStartupDirectory.values()).map(ParsedLauncherShortcutStartupDirectory::new).orElseGet(() -> { - if (Boolean.valueOf(str)) { - return new ParsedLauncherShortcutStartupDirectory(LauncherShortcutStartupDirectory.DEFAULT); - } else { - return new ParsedLauncherShortcutStartupDirectory(); - } - }); - } - - private static Optional parse(String str, LauncherShortcutStartupDirectory... recognizedValues) { - Objects.requireNonNull(str); - return Stream.of(recognizedValues).filter(v -> { - return str.equals(v.asStringValue()); - }).findFirst(); - } -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcutStartupDirectory.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcutStartupDirectory.java index 5fbf1db78506f..c604b00c3e268 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcutStartupDirectory.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherShortcutStartupDirectory.java @@ -36,24 +36,12 @@ public enum LauncherShortcutStartupDirectory { * Platform-specific default value. *

* On Windows, it indicates that the startup directory should be the package's - * installation directory. I.e. same as {@link #INSTALL_DIR}. + * installation directory. *

* On Linux, it indicates that a shortcut doesn't have the startup directory * configured explicitly. */ - DEFAULT("true"), - - /** - * The 'app' directory in the installed application app image. This is the - * directory that is referenced with {@link ApplicationLayout#appDirectory()} - * method. - */ - APP_DIR("app-dir"), - - /** - * The installation directory of the package. - */ - INSTALL_DIR("install-dir"); + DEFAULT("true"); LauncherShortcutStartupDirectory(String stringValue) { this.stringValue = Objects.requireNonNull(stringValue); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties index 684a97bc1bdea..ae225e15ea2f1 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties @@ -82,8 +82,6 @@ error.invalid-app-image=Error: app-image dir "{0}" generated by another jpackage error.invalid-install-dir=Invalid installation directory "{0}" -error.invalid-option-value=Invalid value "{0}" of option {1} - MSG_BundlerFailed=Error: Bundler "{1}" ({0}) failed to produce a package MSG_BundlerConfigException=Bundler {0} skipped because of a configuration problem: {1} \n\ Advice to fix: {2} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java index e2259535058ff..15d8d2f83b0c2 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java @@ -105,10 +105,10 @@ private static WinMsiPackage createWinMsiPackage(Map par static final BundlerParamInfo MSI_PACKAGE = createPackageBundlerParam( WinFromParams::createWinMsiPackage); - private static final BundlerParamInfo WIN_MENU_HINT = createStringBundlerParam( + private static final BundlerParamInfo WIN_MENU_HINT = createBooleanBundlerParam( Arguments.CLIOptions.WIN_MENU_HINT.getId()); - private static final BundlerParamInfo WIN_SHORTCUT_HINT = createStringBundlerParam( + private static final BundlerParamInfo WIN_SHORTCUT_HINT = createBooleanBundlerParam( Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId()); public static final BundlerParamInfo CONSOLE_HINT = createBooleanBundlerParam( diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java index e0a5246d1ffe3..ea4d9eee19a28 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java @@ -471,12 +471,9 @@ private void addShortcutComponentGroup(XMLStreamWriter xml) throws if (folder.isRequestedFor(launcher)) { var workDirectory = folder.shortcut(launcher).startupDirectory().map(v -> { switch (v) { - case DEFAULT, INSTALL_DIR -> { + case DEFAULT -> { return INSTALLDIR; } - case APP_DIR -> { - return installedAppImage.appDirectory(); - } default -> { throw new AssertionError(); } From 7ca2e94257f70dce221db82a4b4547269f60e635 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 1 Aug 2025 23:00:34 -0400 Subject: [PATCH 83/83] LauncherShortcut: reflects changes in the implementation --- .../jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java index 5e86f975870b1..15bb3ea033317 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java @@ -92,7 +92,7 @@ public String propertyName() { } public String appImageFilePropertyName() { - return propertyName.substring(propertyName.indexOf('-') + 1); + return propertyName; } public String optionName() {