8334238: Enhance AddLShortcutTest jpackage test

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2025-08-07 02:02:36 +00:00
parent f95af744b0
commit 7e484e2a63
23 changed files with 2337 additions and 736 deletions

View file

@ -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<String> 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<String> printArgs(String[] args) {
@ -45,11 +65,13 @@ public class PrintEnv {
} 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 @@ public class PrintEnv {
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";
}

View file

@ -0,0 +1,87 @@
#!/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/#<ts>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: <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|'
# 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[@]}"
}
for f in "$@"; do
filterFile "$f"
done

View file

@ -22,39 +22,54 @@
*/
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 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);
}
@ -63,12 +78,12 @@ public class AdditionalLauncher {
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);
}
@ -77,51 +92,46 @@ public class AdditionalLauncher {
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<String, String> v) {
return addRawProperties(List.of(v));
}
public final AdditionalLauncher addRawProperties(
Map.Entry<String, String> v, Map.Entry<String, String> v2) {
return addRawProperties(List.of(v, v2));
}
public final AdditionalLauncher addRawProperties(
Collection<Map.Entry<String, String>> 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, menu);
}
return this;
}
public final String getRawPropertyValue(
String key, Supplier<String> getDefault) {
return rawProperties.stream()
.filter(item -> item.getKey().equals(key))
.map(e -> e.getValue()).findAny().orElseGet(getDefault);
}
private String getDesciption(JPackageCommand cmd) {
return getRawPropertyValue("description", () -> cmd.getArgumentValue(
"--description", unused -> cmd.name()));
}
public final AdditionalLauncher setShortcuts(boolean menu, boolean shortcut) {
withMenuShortcut = menu;
withShortcut = shortcut;
public AdditionalLauncher setShortcut(LauncherShortcut shortcut, StartupDirectory value) {
if (value != null) {
setProperty(shortcut.propertyName(), value.asStringValue());
} else {
setProperty(shortcut.propertyName(), false);
}
return this;
}
public final AdditionalLauncher setIcon(Path iconPath) {
if (iconPath == NO_ICON) {
public AdditionalLauncher setShortcut(LauncherShortcut shortcut, boolean value) {
if (value) {
setShortcut(shortcut, StartupDirectory.DEFAULT);
} else {
setShortcut(shortcut, null);
}
return this;
}
public AdditionalLauncher removeShortcut(LauncherShortcut shortcut) {
rawProperties.remove(shortcut.propertyName());
return this;
}
public AdditionalLauncher setIcon(Path iconPath) {
if (iconPath.equals(NO_ICON)) {
throw new IllegalArgumentException();
}
@ -129,13 +139,13 @@ public class AdditionalLauncher {
return this;
}
public final AdditionalLauncher setNoIcon() {
public AdditionalLauncher setNoIcon() {
icon = NO_ICON;
return this;
}
public final AdditionalLauncher setPersistenceHandler(
ThrowingBiConsumer<Path, List<Map.Entry<String, String>>> handler) {
public AdditionalLauncher setPersistenceHandler(
ThrowingBiConsumer<Path, Collection<Map.Entry<String, String>>> handler) {
if (handler != null) {
createFileHandler = ThrowingBiConsumer.toBiConsumer(handler);
} else {
@ -144,21 +154,31 @@ public class AdditionalLauncher {
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<JPackageCommand> createVerifierAsConsumer() {
return cmd -> {
createVerifier().verify(cmd, verifyActions.stream().sorted(Comparator.comparing(Action::ordinal)).toArray(Action[]::new));
};
}
static void forEachAdditionalLauncher(JPackageCommand cmd,
@ -179,11 +199,12 @@ public class AdditionalLauncher {
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 {
@ -191,259 +212,63 @@ public class AdditionalLauncher {
cmd.addArguments("--add-launcher", String.format("%s=%s", name, propsFile));
List<Map.Entry<String, String>> properties = new ArrayList<>();
Map<String, String> 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));
properties.put("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()));
}
}
properties.putAll(rawProperties);
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;
}
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);
}
}
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<String, String> data) {
this.data = new Properties();
this.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) {
public Optional<String> findProperty(String name) {
Objects.requireNonNull(name);
return data.containsKey(name);
return Optional.ofNullable(data.getProperty(name));
}
public Optional<String> getPropertyValue(String name) {
Objects.requireNonNull(name);
return Optional.of(data.get(name));
public Optional<Boolean> findBooleanProperty(String name) {
return findProperty(name).map(Boolean::parseBoolean);
}
public Optional<Boolean> getPropertyBooleanValue(String name) {
Objects.requireNonNull(name);
return Optional.ofNullable(data.get(name)).map(Boolean::parseBoolean);
}
private final Map<String, String> data;
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<String> javaOptions;
private List<String> defaultArguments;
private Path icon;
private final String name;
private final List<Map.Entry<String, String>> rawProperties;
private BiConsumer<Path, List<Map.Entry<String, String>>> createFileHandler;
private Boolean withMenuShortcut;
private Boolean withShortcut;
private final Map<String, String> rawProperties = new HashMap<>();
private BiConsumer<Path, Collection<Map.Entry<String, String>>> createFileHandler;
private final Set<Action> verifyActions = new HashSet<>(Action.VERIFY_DEFAULTS);
private static final Path NO_ICON = Path.of("");
private static final Map.Entry<String, String> LAUNCHER_AS_SERVICE = Map.entry(
"launcher-as-service", "true");
static final Path NO_ICON = Path.of("");
}

View file

@ -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<String, Map<String, String>> launchers) {
public static Path getPathInAppImage(Path appImageDir) {
return ApplicationLayout.platformAppImage()
@ -44,8 +52,23 @@ public record AppImageFile(String mainLauncherName, String mainLauncherClassName
.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<String, Map<String, String>> 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 record AppImageFile(String mainLauncherName, String mainLauncherClassName
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 record AppImageFile(String mainLauncherName, String mainLauncherClassName
"/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<String, String> 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<String, String> copy = new HashMap<>(attrs);
copy.remove("name");
return Map.copyOf(copy);
}));
return new AppImageFile(mainLauncherName, mainLauncherClassName,
version, macSigned, macAppStore);
version, macSigned, macAppStore, launchers);
}).get();
}

View file

@ -35,16 +35,17 @@ public class CommandArguments<T> {
}
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<String> v) {
verifyMutable();
args.addAll(v);
return thiz();
}

View file

@ -220,7 +220,7 @@ final class ConfigFilesStasher {
AdditionalLauncher.forEachAdditionalLauncher(cmd, (launcherName, propertyFilePath) -> {
try {
final var launcherAsService = new AdditionalLauncher.PropertyFile(propertyFilePath)
.getPropertyBooleanValue("launcher-as-service").orElse(false);
.findBooleanProperty("launcher-as-service").orElse(false);
if (launcherAsService) {
withServices[0] = true;
}

View file

@ -72,7 +72,7 @@ public class JPackageCommand extends CommandArguments<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 class JPackageCommand extends CommandArguments<JPackageCommand> {
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);
@ -90,12 +90,15 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
outputValidators = cmd.outputValidators;
executeInDirectory = cmd.executeInDirectory;
winMsiLogFile = cmd.winMsiLogFile;
unpackedPackageDirectory = cmd.unpackedPackageDirectory;
}
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) {
@ -316,13 +319,11 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
JPackageCommand addPrerequisiteAction(ThrowingConsumer<JPackageCommand> action) {
verifyMutable();
prerequisiteActions.add(action);
return this;
}
JPackageCommand addVerifyAction(ThrowingConsumer<JPackageCommand> action) {
verifyMutable();
verifyActions.add(action);
return this;
}
@ -484,7 +485,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
Path unpackedPackageDirectory() {
verifyIsOfType(PackageType.NATIVE);
return getArgumentValue(UNPACKED_PATH_ARGNAME, () -> null, Path::of);
return unpackedPackageDirectory;
}
/**
@ -662,7 +663,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public boolean isPackageUnpacked() {
return hasArgument(UNPACKED_PATH_ARGNAME);
return unpackedPackageDirectory != null;
}
public static void useToolProviderByDefault(ToolProvider jpackageToolProvider) {
@ -791,11 +792,6 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return this;
}
public JPackageCommand executeVerifyActions() {
verifyActions.run();
return this;
}
private Executor createExecutor() {
Executor exec = new Executor()
.saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput)
@ -820,6 +816,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public Executor.Result execute(int expectedExitCode) {
verifyMutable();
executePrerequisiteActions();
if (hasArgument("--dest")) {
@ -859,7 +856,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
ConfigFilesStasher.INSTANCE.accept(this);
}
final var copy = new JPackageCommand(this).adjustArgumentsBeforeExecution();
final var copy = createMutableCopy().adjustArgumentsBeforeExecution();
final var directoriesAssert = new ReadOnlyPathsAssert(copy);
@ -876,7 +873,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
if (result.exitCode() == 0) {
executeVerifyActions();
verifyActions.run();
}
return result;
@ -884,7 +881,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
public Executor.Result executeAndAssertHelloAppImageCreated() {
Executor.Result result = executeAndAssertImageCreated();
HelloApp.executeLauncherAndVerifyOutput(this);
LauncherVerifier.executeMainLauncherAndVerifyOutput(this);
return result;
}
@ -1046,6 +1043,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public JPackageCommand setReadOnlyPathAsserts(ReadOnlyPathAssert... asserts) {
verifyMutable();
readOnlyPathAsserts = Set.of(asserts);
return this;
}
@ -1059,18 +1057,19 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
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 +1096,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
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();
@ -1111,6 +1110,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public JPackageCommand setAppLayoutAsserts(AppLayoutAssert ... asserts) {
verifyMutable();
appLayoutAsserts = Set.of(asserts);
return this;
}
@ -1157,12 +1157,12 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
} else {
assertFileInAppImage(lookupPath);
final Path rootDir = isImagePackageType() ? outputBundle() :
pathToUnpackedPackageFile(appInstallationDirectory());
final AppImageFile aif = AppImageFile.load(rootDir);
if (TKit.isOSX()) {
final Path rootDir = isImagePackageType() ? outputBundle() :
pathToUnpackedPackageFile(appInstallationDirectory());
AppImageFile aif = AppImageFile.load(rootDir);
boolean expectedValue = MacHelper.appImageSigned(this);
boolean actualValue = aif.macSigned();
TKit.assertEquals(expectedValue, actualValue,
@ -1173,6 +1173,11 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
TKit.assertEquals(expectedValue, actualValue,
"Check for unexpected value of <app-store> property in app image file");
}
TKit.assertStringListEquals(
addLauncherNames().stream().sorted().toList(),
aif.addLaunchers().keySet().stream().sorted().toList(),
"Check additional launcher names");
}
}
@ -1254,16 +1259,14 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
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;
}
JPackageCommand winMsiLogFile(Path v) {
verifyMutable();
if (!TKit.isWindows()) {
throw new UnsupportedOperationException();
}
@ -1286,6 +1289,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
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
@ -1469,6 +1473,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
private final Actions verifyActions;
private Path executeInDirectory;
private Path winMsiLogFile;
private Path unpackedPackageDirectory;
private Set<ReadOnlyPathAssert> readOnlyPathAsserts = Set.of(ReadOnlyPathAssert.values());
private Set<AppLayoutAssert> appLayoutAsserts = Set.of(AppLayoutAssert.values());
private List<Consumer<Iterator<String>>> outputValidators = new ArrayList<>();
@ -1496,8 +1501,6 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
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\\] ");

View file

@ -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.regex.Pattern;
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 final class LauncherAsServiceVerifier {
} else {
applyToAdditionalLauncher(pkg);
}
pkg.addInstallVerifier(this::verifyLauncherExecuted);
}
static void verify(JPackageCommand cmd) {
@ -127,7 +133,6 @@ public final class LauncherAsServiceVerifier {
"service-installer.exe");
if (launcherNames.isEmpty()) {
TKit.assertPathExists(serviceInstallerPath, false);
} else {
TKit.assertFileExists(serviceInstallerPath);
}
@ -188,23 +193,11 @@ public final class LauncherAsServiceVerifier {
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 @@ public final class LauncherAsServiceVerifier {
+ 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 @@ public final class LauncherAsServiceVerifier {
}
}
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 @@ public final class LauncherAsServiceVerifier {
private final Path appOutputFileName;
private final Consumer<AdditionalLauncher> additionalLauncherCallback;
static final Set<PackageType> SUPPORTED_PACKAGES = Stream.of(LINUX, WINDOWS,
Set.of(MAC_PKG)).flatMap(x -> x.stream()).collect(Collectors.toSet());
static final Set<PackageType> SUPPORTED_PACKAGES = Stream.of(
LINUX,
WINDOWS,
Set.of(MAC_PKG)
).flatMap(Collection::stream).collect(Collectors.toSet());
}

View file

@ -46,6 +46,11 @@ public final class LauncherIconVerifier {
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 final class LauncherIconVerifier {
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;
}

View file

@ -0,0 +1,167 @@
/*
* 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<StartupDirectory> 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<String, StartupDirectory> 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 appImageFilePropertyName() {
return propertyName.substring(propertyName.indexOf('-') + 1);
}
public String optionName() {
return "--" + propertyName;
}
Optional<StartupDirectory> expectShortcut(JPackageCommand cmd, Optional<AppImageFile> predefinedAppImage, String launcherName) {
Objects.requireNonNull(predefinedAppImage);
final var name = Optional.ofNullable(launcherName).orElseGet(cmd::name);
if (name.equals(cmd.name())) {
return findMainLauncherShortcut(cmd);
} else {
String[] propertyName = new String[1];
return findAddLauncherShortcut(cmd, predefinedAppImage.map(appImage -> {
propertyName[0] = appImageFilePropertyName();
return new PropertyFile(appImage.addLaunchers().get(launcherName));
}).orElseGet(() -> {
propertyName[0] = this.propertyName;
return getAdditionalLauncherProperties(cmd, launcherName);
})::findProperty, propertyName[0]);
}
}
public interface InvokeShortcutSpec {
String launcherName();
LauncherShortcut shortcut();
Optional<Path> expectedWorkDirectory();
List<String> commandLine();
default Executor.Result execute() {
return HelloApp.configureAndExecute(0, Executor.of(commandLine()).dumpOutput());
}
record Stub(
String launcherName,
LauncherShortcut shortcut,
Optional<Path> expectedWorkDirectory,
List<String> commandLine) implements InvokeShortcutSpec {
public Stub {
Objects.requireNonNull(launcherName);
Objects.requireNonNull(shortcut);
Objects.requireNonNull(expectedWorkDirectory);
Objects.requireNonNull(commandLine);
}
}
}
private Optional<StartupDirectory> findMainLauncherShortcut(JPackageCommand cmd) {
if (cmd.hasArgument(optionName())) {
return Optional.of(StartupDirectory.DEFAULT);
} else {
return Optional.empty();
}
}
private Optional<StartupDirectory> findAddLauncherShortcut(JPackageCommand cmd,
Function<String, Optional<String>> addlauncherProperties, String propertyName) {
var explicit = addlauncherProperties.apply(propertyName);
if (explicit.isPresent()) {
return explicit.flatMap(StartupDirectory::parse);
} else {
return findMainLauncherShortcut(cmd);
}
}
private final String propertyName;
}

View file

@ -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<List<String>> javaOptions,
Optional<List<String>> arguments,
Optional<Path> icon,
Map<String, String> 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<LauncherVerifier, JPackageCommand> action) {
this.action = ThrowingBiConsumer.toBiConsumer(action);
}
private void apply(LauncherVerifier verifier, JPackageCommand cmd) {
action.accept(verifier, cmd);
}
private final BiConsumer<LauncherVerifier, JPackageCommand> action;
static final List<Action> VERIFY_APP_IMAGE = List.of(
VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED
);
static final List<Action> 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<Action> actions) {
Objects.requireNonNull(cmd);
for (var a : actions) {
a.apply(this, cmd);
}
}
private boolean isMainLauncher() {
return properties.isEmpty();
}
private Optional<String> 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<String> getArguments(JPackageCommand cmd) {
return getStringArrayProperty(cmd, "--arguments", arguments);
}
private List<String> getJavaOptions(JPackageCommand cmd) {
return getStringArrayProperty(cmd, "--java-options", javaOptions);
}
private List<String> getStringArrayProperty(JPackageCommand cmd, String optionName, Optional<List<String>> 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<Path> 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<List<String>> javaOptions;
private final Optional<List<String>> arguments;
private final Optional<Path> icon;
private final Optional<PropertyFile> properties;
}

View file

@ -23,25 +23,30 @@
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;
import java.util.Map;
import java.util.Objects;
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;
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;
@ -308,8 +313,8 @@ public final class LinuxHelper {
}
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));
@ -330,7 +335,7 @@ public final class LinuxHelper {
checkPrerequisites = packageSize > 5;
}
List<String> prerequisites = LinuxHelper.getPrerequisitePackages(cmd);
List<String> prerequisites = getPrerequisitePackages(cmd);
if (checkPrerequisites) {
final String vitalPackage = "libc";
TKit.assertTrue(prerequisites.stream().filter(
@ -340,13 +345,28 @@ public final class LinuxHelper {
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));
}
}
static void addBundleDesktopIntegrationVerifier(PackageTest test,
boolean integrated) {
public static Collection<? extends InvokeShortcutSpec> 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("gtk-launch", PathUtils.replaceSuffix(systemDesktopFile.getFileName(), "").toString()));
}).toList();
}
static void addBundleDesktopIntegrationVerifier(PackageTest test, boolean integrated) {
final String xdgUtils = "xdg-utils";
Function<List<String>, String> verifier = (lines) -> {
@ -392,52 +412,81 @@ public final class LinuxHelper {
});
test.addInstallVerifier(cmd -> {
// Verify .desktop files.
try (var files = Files.list(cmd.appLayout().desktopIntegrationDirectory())) {
List<Path> 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");
}
for (var desktopFile : desktopFiles) {
verifyDesktopFile(cmd, desktopFile);
}
if (!integrated) {
TKit.assertStringListEquals(
List.of(),
getDesktopFiles(cmd).stream().map(Path::toString).toList(),
"Check there are no .desktop files in the package");
}
});
}
private static void verifyDesktopFile(JPackageCommand cmd, Path desktopFile)
throws IOException {
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, predefinedAppImage, desktopFile);
}
if (!cmd.isPackageUnpacked("Not verifying system .desktop files")) {
for (var desktopFile : desktopFiles) {
Path systemDesktopFile = 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 = getSystemDesktopFilesFolder().resolve(desktopFile.getFileName());
TKit.assertPathExists(systemDesktopFile, false);
}
}
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static Collection<Path> 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 String launcherNameFromDesktopFile(JPackageCommand cmd, Optional<AppImageFile> predefinedAppImage, Path desktopFile) {
Objects.requireNonNull(cmd);
Objects.requireNonNull(predefinedAppImage);
Objects.requireNonNull(desktopFile);
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().orElseThrow(() -> {
TKit.assertUnexpected(String.format("Failed to find launcher corresponding to [%s] file", desktopFile));
// Unreachable
return null;
});
}
private static void verifyDesktopFile(JPackageCommand cmd, Optional<AppImageFile> predefinedAppImage, Path desktopFile) throws IOException {
Objects.requireNonNull(cmd);
Objects.requireNonNull(predefinedAppImage);
Objects.requireNonNull(desktopFile);
TKit.trace(String.format("Check [%s] file BEGIN", desktopFile));
var launcherName = Stream.of(List.of(cmd.name()), cmd.addLauncherNames()).flatMap(List::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");
}
var launcherName = launcherNameFromDesktopFile(cmd, predefinedAppImage, desktopFile);
List<String> lines = Files.readAllLines(desktopFile);
TKit.assertEquals("[Desktop Entry]", lines.get(0), "Check file header");
Map<String, String> 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;
}));
var data = new DesktopFile(desktopFile, true);
final Set<String> mandatoryKeys = new HashSet<>(Set.of("Name", "Comment",
"Exec", "Icon", "Terminal", "Type", "Categories"));
@ -447,34 +496,44 @@ public final class LinuxHelper {
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));
var appLayout = cmd.appLayout();
LauncherShortcut.LINUX_SHORTCUT.expectShortcut(cmd, predefinedAppImage, launcherName).map(shortcutWorkDirType -> {
switch (shortcutWorkDirType) {
case DEFAULT -> {
return (Path)null;
}
default -> {
throw new AssertionError();
}
}
}).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.find("Path").isEmpty(), "Check there is no [Path] key");
});
for (var e : List.<Map.Entry<Map.Entry<String, Optional<String>>, Function<ApplicationLayout, Path>>>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()));
Path expectedDir = cmd.pathToPackageFile(e.getValue().apply(appLayout));
TKit.assertTrue(path.getParent().equals(expectedDir), String.format(
"Check the value of [%s] key references a file in [%s] folder",
e.getKey().getKey(), expectedDir));
@ -761,6 +820,62 @@ public final class LinuxHelper {
}
}
private static final class DesktopFile {
DesktopFile(Path path, boolean verify) {
try {
List<String> lines = Files.readAllLines(path);
if (verify) {
TKit.assertEquals("[Desktop Entry]", lines.getFirst(), "Check file header");
}
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));
});
}
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<String> keySet() {
return data.keySet();
}
Optional<String> find(String property) {
return Optional.ofNullable(data.get(Objects.requireNonNull(property)));
}
Optional<String> 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<String, String> data;
}
static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of(
"lib/server/libjvm.so"));

View file

@ -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<Table> 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<Table> FIND_PROPERTY_REQUIRED_TABLES = Set.of(PROPERTY);
static final Set<Table> LIST_SHORTCUTS_REQUIRED_TABLES = Set.of(COMPONENT, DIRECTORY, FILE, SHORTCUT);
}
private MsiDatabase(Map<Table, MsiTable> tables) {
this.tables = Map.copyOf(tables);
}
Set<Table> tableNames() {
return tables.keySet();
}
MsiDatabase append(MsiDatabase other) {
Map<Table, MsiTable> newTables = new HashMap<>(tables);
newTables.putAll(other.tables);
return new MsiDatabase(newTables);
}
Optional<String> findProperty(String propertyName) {
Objects.requireNonNull(propertyName);
return tables.get(Table.PROPERTY).findRow("Property", propertyName).map(row -> {
return row.apply("Value");
});
}
Collection<Shortcut> 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<String, String> 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<String, List<String>> columns) {
MsiTable {
Objects.requireNonNull(columns);
if (columns.isEmpty()) {
throw new IllegalArgumentException("Table should have columns");
}
}
Optional<Function<String, String>> 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<String, List<String>> 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<String, String> row(int rowIndex) {
return columnName -> {
var column = Objects.requireNonNull(columns.get(Objects.requireNonNull(columnName)));
return column.get(rowIndex);
};
}
}
private record IdtFileHeader(Charset charset, List<String> 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 <a href="https://learn.microsoft.com/en-us/windows/win32/msi/archive-file-format">https://learn.microsoft.com/en-us/windows/win32/msi/archive-file-format</a>
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/msi/ascii-data-in-text-archive-files">https://learn.microsoft.com/en-us/windows/win32/msi/ascii-data-in-text-archive-files</a>
* @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<Table, MsiTable> 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<String> DIRECTORY_PROPERTIES = Set.of(
"DesktopFolder",
"LocalAppDataFolder",
"ProgramFiles64Folder",
"ProgramMenuFolder"
);
}

View file

@ -30,11 +30,13 @@ import static jdk.jpackage.test.PackageType.LINUX;
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;
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;
@ -270,8 +272,7 @@ public final class PackageTest extends RunnablePackageTest {
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, () -> {
@ -296,13 +297,9 @@ public final class PackageTest extends RunnablePackageTest {
Files.deleteIfExists(appOutput);
List<String> 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)) {
@ -360,15 +357,14 @@ public final class PackageTest extends RunnablePackageTest {
public PackageTest configureHelloApp(String javaAppDesc) {
addHelloAppInitializer(javaAppDesc);
addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput);
addInstallVerifier(LauncherVerifier::executeMainLauncherAndVerifyOutput);
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 {
@ -611,11 +607,7 @@ public final class PackageTest extends RunnablePackageTest {
}
}
case VERIFY_INSTALL -> {
if (unpackNotSupported()) {
return ActionAction.SKIP;
}
if (installFailed()) {
if (unpackNotSupported() || installFailed()) {
return ActionAction.SKIP;
}
}
@ -753,6 +745,8 @@ public final class PackageTest extends RunnablePackageTest {
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 +768,11 @@ public final class PackageTest extends RunnablePackageTest {
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);
}
if (isOfType(cmd, LINUX)) {
LinuxHelper.verifyDesktopFiles(cmd, true);
}
}
@ -856,12 +849,11 @@ public final class PackageTest extends RunnablePackageTest {
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);
}
if (isOfType(cmd, LINUX)) {
LinuxHelper.verifyDesktopFiles(cmd, false);
}
}

View file

@ -43,6 +43,8 @@ import java.nio.file.WatchEvent;
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 final class TKit {
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.<Duration>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 @@ public final class TKit {
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 %dms expired", remainderTimeout.toMillis()));
}
for (WatchEvent<?> event : key.pollEvents()) {

View file

@ -0,0 +1,283 @@
/*
* 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 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;
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.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;
public 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(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<Collection<Shortcut>, List<Shortcut>> 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 launcher [%s]", name));
for (int i = 0; i != expectedLauncherShortcuts.size(); i++) {
TKit.trace(String.format("Verify shortcut #%d of launcher [%s]", 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 = cmd.createMutableCopy();
if (copyCmd.hasArgument("--win-per-user-install")) {
copyCmd.removeArgument("--win-per-user-install");
} else {
copyCmd.addArgument("--win-per-user-install");
}
verifyDeployedShortcutsInternal(copyCmd, false);
}
public static Collection<? extends InvokeShortcutSpec> 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();
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<Shortcut> expectLauncherShortcuts(JPackageCommand cmd,
Optional<AppImageFile> predefinedAppImage, String launcherName) {
Objects.requireNonNull(cmd);
Objects.requireNonNull(predefinedAppImage);
final List<Shortcut> shortcuts = new ArrayList<>();
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);
final SpecialFolder installRoot;
if (isUserLocalInstall) {
installRoot = SpecialFolder.LOCAL_APPLICATION_DATA;
} else {
installRoot = SpecialFolder.PROGRAM_FILES;
}
final var installDir = Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd));
final Function<StartupDirectory, Path> 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, winMenu.map(workDir).orElseThrow(), type));
}
if (desktop.isPresent()) {
ShortcutType type;
if (isUserLocalInstall) {
type = ShortcutType.USER_DESKTOP;
} else {
type = ShortcutType.COMMON_DESKTOP;
}
shortcuts.add(createLauncherShortcutSpec(cmd, launcherName, installRoot, desktop.map(workDir).orElseThrow(), type));
}
return shortcuts;
}
private static Map<String, Collection<Shortcut>> expectShortcuts(JPackageCommand cmd) {
Map<String, Collection<Shortcut>> 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;
}
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,
Optional.of(resolvePath(shortcut.workDir(), !isUserLocalInstall)),
List.of("cmd", "/c", "start", "/wait", PathUtils.addSuffix(resolvePath(shortcut.path(), !isUserLocalInstall), ".lnk").toString()));
}
private static final Comparator<Shortcut> SHORTCUT_COMPARATOR = Comparator.comparing(Shortcut::target)
.thenComparing(Comparator.comparing(Shortcut::path))
.thenComparing(Comparator.comparing(Shortcut::workDir));
}

View file

@ -26,19 +26,24 @@ import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked;
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;
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;
@ -63,7 +68,7 @@ public class WindowsHelper {
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,22 +268,22 @@ public class WindowsHelper {
}
}
static void verifyDesktopIntegration(JPackageCommand cmd,
String launcherName) {
new DesktopIntegrationVerifier(cmd, launcherName);
static void verifyDeployedDesktopIntegration(JPackageCommand cmd, boolean installed) {
WinShortcutVerifier.verifyDeployedShortcuts(cmd, installed);
DesktopIntegrationVerifier.verify(cmd, installed);
}
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();
}
public static String getExecutableDesciption(Path pathToExeFile) {
static Collection<MsiDatabase.Shortcut> getMsiShortcuts(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.WIN_MSI);
return MsiDatabaseCache.INSTANCE.listShortcuts(cmd.outputBundle());
}
public static String getExecutableDescription(Path pathToExeFile) {
Executor exec = Executor.of("powershell",
"-NoLogo",
"-NoProfile",
@ -386,7 +391,7 @@ public class WindowsHelper {
}
}
private static boolean isUserLocalInstall(JPackageCommand cmd) {
static boolean isUserLocalInstall(JPackageCommand cmd) {
return cmd.hasArgument("--win-per-user-install");
}
@ -394,141 +399,42 @@ public class WindowsHelper {
return path.toString().length() > WIN_MAX_PATH;
}
private static class DesktopIntegrationVerifier {
DesktopIntegrationVerifier(JPackageCommand cmd, String launcherName) {
static void verify(JPackageCommand cmd, boolean installed) {
cmd.verifyIsOfType(PackageType.WINDOWS);
name = Optional.ofNullable(launcherName).orElseGet(cmd::name);
isUserLocalInstall = isUserLocalInstall(cmd);
appInstalled = cmd.appLauncherPath(launcherName).toFile().exists();
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"));
}
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);
for (var faFile : cmd.getAllArgumentValues("--file-associations")) {
verifyFileAssociationsRegistry(Path.of(faFile), installed);
}
}
private void verifyShortcut(Path path, boolean exists) {
if (exists) {
TKit.assertFileExists(path);
} else {
TKit.assertPathExists(path, false);
}
}
private static void verifyFileAssociationsRegistry(Path faFile, boolean installed) {
private void verifySystemDesktopShortcut(boolean exists) {
Path dir = SpecialFolder.COMMON_DESKTOP.getPath();
verifyShortcut(dir.resolve(desktopShortcutPath), exists);
}
TKit.trace(String.format(
"Get file association properties from [%s] file",
faFile));
private void verifyUserLocalDesktopShortcut(boolean exists) {
Path dir = SpecialFolder.USER_DESKTOP.getPath();
verifyShortcut(dir.resolve(desktopShortcutPath), exists);
}
var faProps = new Properties();
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);
}
private void verifyUserLocalStartMenuShortcut(boolean exists) {
verifyStartMenuShortcut(SpecialFolder.USER_START_MENU_PROGRAMS.getPath(), exists);
}
private void verifyFileAssociationsRegistry(Path faFile) {
try {
TKit.trace(String.format(
"Get file association properties from [%s] file",
faFile));
Map<String, String> 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(),
@ -549,16 +455,9 @@ public class WindowsHelper {
"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()
@ -611,7 +510,12 @@ public class WindowsHelper {
CommonDesktop,
Programs,
CommonPrograms;
CommonPrograms,
ProgramFiles,
LocalApplicationData,
;
Path getPath() {
final var str = Executor.of("powershell", "-NoLogo", "-NoProfile",
@ -636,33 +540,84 @@ public class WindowsHelper {
}
}
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),
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", SpecialFolderDotNet.CommonDesktop),
USER_DESKTOP(USER_SHELL_FOLDERS_REGKEY, "Desktop", SpecialFolderDotNet.Desktop);
COMMON_DESKTOP(
SYSTEM_SHELL_FOLDERS_REGKEY,
"Common Desktop",
"DesktopFolder",
SpecialFolderDotNet.CommonDesktop),
USER_DESKTOP(
USER_SHELL_FOLDERS_REGKEY,
"Desktop",
"DesktopFolder",
SpecialFolderDotNet.Desktop),
SpecialFolder(String keyPath, String valueName) {
reg = new RegValuePath(keyPath, valueName);
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, SpecialFolderDotNet alt) {
reg = new RegValuePath(keyPath, valueName);
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 msiPropertyName, SpecialFolderDotNet alt) {
reg = Optional.empty();
this.alt = Optional.of(alt);
this.msiPropertyName = Objects.requireNonNull(msiPropertyName);
}
static Optional<SpecialFolder> 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<RegValuePath> reg;
private final Optional<SpecialFolderDotNet> 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<SpecialFolder, Path> CACHE = new ConcurrentHashMap<>();
}
@ -693,6 +648,63 @@ public class WindowsHelper {
private static final ShortPathUtils INSTANCE = new ShortPathUtils();
}
private static final class MsiDatabaseCache {
Optional<String> findProperty(Path msiPath, String propertyName) {
return ensureTables(msiPath, MsiDatabase.Table.FIND_PROPERTY_REQUIRED_TABLES).findProperty(propertyName);
}
Collection<MsiDatabase.Shortcut> listShortcuts(Path msiPath) {
return ensureTables(msiPath, MsiDatabase.Table.LIST_SHORTCUTS_REQUIRED_TABLES).listShortcuts();
}
MsiDatabase ensureTables(Path msiPath, Set<MsiDatabase.Table> 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<Path, SoftReference<MsiDatabaseWithTimestamp>> items = new HashMap<>();
static final MsiDatabaseCache INSTANCE = new MsiDatabaseCache();
}
static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of(
"bin\\server\\jvm.dll"));

View file

@ -60,16 +60,16 @@ public class UpgradeTest {
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 @@ public class UpgradeTest {
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));
}

View file

@ -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)
})
})()

View file

@ -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])
})()

View file

@ -21,13 +21,32 @@
* 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.time.Duration;
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.Parameter;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.Test;
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 +63,23 @@ import jdk.jpackage.test.Annotations.Test;
* @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 +140,287 @@ public class AddLShortcutTest {
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<Object[]> testShortcutStartupDirectoryLinux() {
return testShortcutStartupDirectory(LauncherShortcut.LINUX_SHORTCUT);
}
public static Collection<Object[]> testShortcutStartupDirectoryWindows() {
return testShortcutStartupDirectory(LauncherShortcut.WIN_DESKTOP_SHORTCUT, LauncherShortcut.WIN_START_MENU_SHORTCUT);
}
@Test(ifNotOS = OperatingSystem.MACOS)
@Parameter(value = "DEFAULT")
public void testInvokeShortcuts(StartupDirectory startupDirectory) {
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);
test.addInstallVerifier(cmd -> {
if (!cmd.isPackageUnpacked("Not invoking launcher shortcuts")) {
Collection<? extends InvokeShortcutSpec> 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<? extends InvokeShortcutSpec> 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();
invokeShortcutSpec.execute();
// 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.
TKit.waitForFileCreated(expectedOutputFile, Duration.ofSeconds(10), Duration.ofSeconds(3));
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<Object[]> testShortcutStartupDirectory(LauncherShortcut... shortcuts) {
List<List<LauncherShortcutStartupDirectoryConfig>> items = new ArrayList<>();
for (var shortcut : shortcuts) {
List<LauncherShortcutStartupDirectoryConfig> 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<LauncherShortcutStartupDirectoryConfig> 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.addArguments(shortcut.optionName(), value);
}
}
}
void applyToAdditionalLauncher(LauncherShortcut shortcut, AdditionalLauncher addLauncher) {
addLauncher.setProperty(shortcut.propertyName(), value);
}
private final String value;
static final List<StartupDirectoryValueSetter> MAIN_LAUNCHER_VALUES = List.of(
StartupDirectoryValueSetter.DEFAULT
);
static final List<StartupDirectoryValueSetter> ADD_LAUNCHER_VALUES = List.of(
StartupDirectoryValueSetter.TRUE,
StartupDirectoryValueSetter.FALSE
);
}
record LauncherShortcutStartupDirectoryConfig(LauncherShortcut shortcut, Optional<StartupDirectoryValueSetter> 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));
}

View file

@ -89,17 +89,17 @@ public class AddLauncherTest {
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 class AddLauncherTest {
.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 class AddLauncherTest {
.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();

View file

@ -27,13 +27,14 @@ import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
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.AdditionalLauncher;
import jdk.jpackage.test.Annotations.Test;
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 class PerUserCfgTest {
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 class PerUserCfgTest {
}
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() {