mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-15 13:49:42 +02:00
8334238: Enhance AddLShortcutTest jpackage test
Reviewed-by: almatvee
This commit is contained in:
parent
f95af744b0
commit
7e484e2a63
23 changed files with 2337 additions and 736 deletions
|
@ -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";
|
||||
}
|
||||
|
|
87
test/jdk/tools/jpackage/clean_test_output.sh
Normal file
87
test/jdk/tools/jpackage/clean_test_output.sh
Normal 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
|
|
@ -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("");
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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\\] ");
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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"));
|
||||
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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"));
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
81
test/jdk/tools/jpackage/resources/msi-export.js
Normal file
81
test/jdk/tools/jpackage/resources/msi-export.js
Normal 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)
|
||||
})
|
||||
})()
|
|
@ -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])
|
||||
})()
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue