From f7f4f903cfdafecf69ff47d5d37e254adaf63141 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 1 Nov 2025 23:29:48 +0000 Subject: [PATCH] 8370969: --launcher-as-service option is ignored when used with --app-image option Reviewed-by: almatvee --- .../jdk/jpackage/internal/FromParams.java | 11 +- .../jdk/jpackage/test/AdditionalLauncher.java | 5 + .../helpers/jdk/jpackage/test/CfgFile.java | 6 +- .../jdk/jpackage/test/ConfigFilesStasher.java | 20 +- .../jdk/jpackage/test/JPackageCommand.java | 72 +++++- .../test/LauncherAsServiceVerifier.java | 211 ++++++++++++------ .../jdk/jpackage/test/LauncherShortcut.java | 4 +- .../jdk/jpackage/test/LinuxHelper.java | 22 +- .../helpers/jdk/jpackage/test/MacHelper.java | 26 ++- .../jdk/jpackage/test/MacSignVerify.java | 8 +- .../jdk/jpackage/test/PropertyFinder.java | 163 ++++++++++++++ .../helpers/jdk/jpackage/test/TKit.java | 25 ++- .../jpackage/test/WinShortcutVerifier.java | 25 +-- .../jdk/tools/jpackage/share/ServiceTest.java | 166 +++++++++++++- 14 files changed, 601 insertions(+), 163 deletions(-) create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PropertyFinder.java diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java index df9cc528439..cee7492dbc7 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java @@ -67,6 +67,7 @@ import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLaunchers; import jdk.jpackage.internal.model.ApplicationLayout; import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.ExternalApplication; import jdk.jpackage.internal.model.ExternalApplication.LauncherInfo; import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.model.LauncherShortcut; @@ -116,7 +117,7 @@ final class FromParams { if (hasPredefinedAppImage(params)) { final var appImageFile = PREDEFINED_APP_IMAGE_FILE.fetchFrom(params); appBuilder.initFromExternalApplication(appImageFile, launcherInfo -> { - var launcherParams = mapLauncherInfo(launcherInfo); + var launcherParams = mapLauncherInfo(appImageFile, launcherInfo); return launcherMapper.apply(mergeParams(params, launcherParams)); }); } else { @@ -220,10 +221,14 @@ final class FromParams { return new ApplicationLaunchers(mainLauncher, additionalLaunchers); } - private static Map mapLauncherInfo(LauncherInfo launcherInfo) { + private static Map mapLauncherInfo(ExternalApplication appImageFile, LauncherInfo launcherInfo) { Map launcherParams = new HashMap<>(); launcherParams.put(NAME.getID(), launcherInfo.name()); - launcherParams.put(LAUNCHER_AS_SERVICE.getID(), Boolean.toString(launcherInfo.service())); + if (!appImageFile.getLauncherName().equals(launcherInfo.name())) { + // This is not the main launcher, accept the value + // of "launcher-as-service" from the app image file (.jpackage.xml). + launcherParams.put(LAUNCHER_AS_SERVICE.getID(), Boolean.toString(launcherInfo.service())); + } launcherParams.putAll(launcherInfo.extra()); return launcherParams; } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java index 66da89fc3f9..a41e2b5043f 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java @@ -101,6 +101,11 @@ public final class AdditionalLauncher { return this; } + public AdditionalLauncher removeProperty(String name) { + rawProperties.remove(Objects.requireNonNull(name)); + return this; + } + public AdditionalLauncher setShortcuts(boolean menu, boolean desktop) { if (TKit.isLinux()) { setShortcut(LINUX_SHORTCUT, desktop); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java index 52e8ecf819b..e163678bacf 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java @@ -22,7 +22,6 @@ */ package jdk.jpackage.test; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -34,6 +33,7 @@ import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; +import jdk.jpackage.internal.util.function.ThrowingFunction; public final class CfgFile { @@ -116,7 +116,7 @@ public final class CfgFile { return null; } - public static CfgFile load(Path path) throws IOException { + public static CfgFile load(Path path) { TKit.trace(String.format("Read [%s] jpackage cfg file", path)); final Pattern sectionBeginRegex = Pattern.compile( "\\s*\\[([^]]*)\\]\\s*"); @@ -126,7 +126,7 @@ public final class CfgFile { String currentSectionName = null; List> currentSection = new ArrayList<>(); - for (String line : Files.readAllLines(path)) { + for (String line : ThrowingFunction.>toFunction(Files::readAllLines).apply(path)) { Matcher matcher = sectionBeginRegex.matcher(line); if (matcher.find()) { if (currentSectionName != null) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java index 2321e4e852e..11627ab9125 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigFilesStasher.java @@ -29,7 +29,6 @@ import static jdk.jpackage.test.ApplicationLayout.platformAppImage; import java.io.File; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -214,22 +213,9 @@ final class ConfigFilesStasher { } private static boolean isWithServices(JPackageCommand cmd) { - boolean[] withServices = new boolean[1]; - withServices[0] = cmd.hasArgument("--launcher-as-service"); - if (!withServices[0]) { - AdditionalLauncher.forEachAdditionalLauncher(cmd, (launcherName, propertyFilePath) -> { - try { - final var launcherAsService = new AdditionalLauncher.PropertyFile(propertyFilePath) - .findBooleanProperty("launcher-as-service").orElse(false); - if (launcherAsService) { - withServices[0] = true; - } - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - }); - } - return withServices[0]; + return cmd.launcherNames(true).stream().anyMatch(launcherName -> { + return LauncherAsServiceVerifier.launcherAsService(cmd, launcherName); + }); } private static List listAppImage(Path to) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index b3729093ad2..fcd940e725e 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -39,6 +39,7 @@ import java.nio.file.Path; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.ListIterator; @@ -573,6 +574,24 @@ public class JPackageCommand extends CommandArguments { return appLayout().runtimeDirectory(); } + /** + * Returns the name of the main launcher. It will read the name of the main + * launcher from the external app image if such is specified. + * + * @return the name of the main launcher + * + * @throws IllegalArgumentException if the command is configured for packaging + * Java runtime + */ + public String mainLauncherName() { + verifyNotRuntime(); + return name(); + } + + boolean isMainLauncher(String launcherName) { + return launcherName == null || mainLauncherName().equals(launcherName); + } + /** * Returns path for application launcher with the given name. * @@ -589,7 +608,7 @@ public class JPackageCommand extends CommandArguments { public Path appLauncherPath(String launcherName) { verifyNotRuntime(); if (launcherName == null) { - launcherName = name(); + launcherName = mainLauncherName(); } if (TKit.isWindows()) { @@ -607,15 +626,54 @@ public class JPackageCommand extends CommandArguments { } /** - * Returns names of all additional launchers or empty list if none - * configured. + * Returns names of additional launchers or an empty list if none configured. + *

+ * If {@code lookupInPrederfinedAppImage} is {@code true} and the command is + * configured with an external app image, it will read names of the additional + * launchers from the external app image. + * + * @param lookupInPrederfinedAppImage if to read names of additional launchers + * from an external app image + * + * @return the names of additional launchers */ - public List addLauncherNames() { + public List addLauncherNames(boolean lookupInPrederfinedAppImage) { + if (isRuntime()) { + return List.of(); + } + List names = new ArrayList<>(); + if (lookupInPrederfinedAppImage) { + Optional.ofNullable(getArgumentValue("--app-image")) + .map(Path::of) + .map(AppImageFile::load) + .map(AppImageFile::addLaunchers) + .map(Map::keySet) + .ifPresent(names::addAll); + } forEachAdditionalLauncher(this, (launcherName, propFile) -> { names.add(launcherName); }); - return names; + return Collections.unmodifiableList(names); + } + + /** + * Returns names of all launchers. + *

+ * If the list is not empty, the first element is {@code null} referencing the + * main launcher. In the case of runtime packaging, the list is empty. + * + * @return the names of all launchers + */ + public List launcherNames(boolean lookupInPrederfinedAppImage) { + if (isRuntime()) { + return List.of(); + } + + List names = new ArrayList<>(); + names.add(null); + names.addAll(addLauncherNames(lookupInPrederfinedAppImage)); + return Collections.unmodifiableList(names); } private void verifyNotRuntime() { @@ -639,7 +697,7 @@ public class JPackageCommand extends CommandArguments { public Path appLauncherCfgPath(String launcherName) { verifyNotRuntime(); if (launcherName == null) { - launcherName = name(); + launcherName = mainLauncherName(); } return appLayout().appDirectory().resolve(launcherName + ".cfg"); } @@ -1244,7 +1302,7 @@ public class JPackageCommand extends CommandArguments { // a predefined app image. if (!hasArgument("--app-image")) { TKit.assertStringListEquals( - addLauncherNames().stream().sorted().toList(), + addLauncherNames(false).stream().sorted().toList(), aif.addLaunchers().keySet().stream().sorted().toList(), "Check additional launcher names"); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java index e1cd37fe8b4..ce6faf99034 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java @@ -22,20 +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.io.UncheckedIOException; 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.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -44,8 +43,8 @@ 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.ThrowingFunction; import jdk.jpackage.internal.util.function.ThrowingRunnable; -import jdk.jpackage.test.AdditionalLauncher.PropertyFile; import jdk.jpackage.test.LauncherVerifier.Action; public final class LauncherAsServiceVerifier { @@ -67,26 +66,56 @@ public final class LauncherAsServiceVerifier { return this; } + public Builder setAppOutputFileNamePrefix(String v) { + appOutputFileNamePrefix = v; + return this; + } + + public Builder appendAppOutputFileNamePrefix(String v) { + return setAppOutputFileNamePrefix(appOutputFileNamePrefix() + Objects.requireNonNull(v)); + } + + public Builder setAppOutputFileNamePrefixToAppName() { + return setAppOutputFileNamePrefix(TKit.getCurrentDefaultAppName()); + } + public Builder setAdditionalLauncherCallback(Consumer v) { additionalLauncherCallback = v; return this; } - public LauncherAsServiceVerifier create() { - Objects.requireNonNull(expectedValue); - return new LauncherAsServiceVerifier(launcherName, appOutputFileName, - expectedValue, - launcherName != null ? additionalLauncherCallback : null); + public Builder mutate(Consumer mutator) { + mutator.accept(this); + return this; } - public Builder applyTo(PackageTest pkg) { - create().applyTo(pkg); + public LauncherAsServiceVerifier create() { + Objects.requireNonNull(expectedValue); + return new LauncherAsServiceVerifier( + launcherName, + appOutputFileNamePrefix() + + Optional.ofNullable(appOutputFileName).orElse("launcher-as-service.txt"), + expectedValue, + Optional.ofNullable(additionalLauncherCallback)); + } + + public Builder applyTo(PackageTest test) { + return applyTo(new ConfigurationTarget(test)); + } + + public Builder applyTo(ConfigurationTarget target) { + create().applyTo(target); return this; } + private String appOutputFileNamePrefix() { + return Optional.ofNullable(appOutputFileNamePrefix).orElse(""); + } + private String launcherName; private String expectedValue; - private String appOutputFileName = "launcher-as-service.txt"; + private String appOutputFileName; + private String appOutputFileNamePrefix; private Consumer additionalLauncherCallback; } @@ -97,41 +126,50 @@ public final class LauncherAsServiceVerifier { private LauncherAsServiceVerifier(String launcherName, String appOutputFileName, String expectedArgValue, - Consumer additionalLauncherCallback) { - this.expectedValue = expectedArgValue; + Optional> additionalLauncherCallback) { + + if (launcherName == null && additionalLauncherCallback.isPresent()) { + throw new UnsupportedOperationException(); + } + + this.expectedValue = Objects.requireNonNull(expectedArgValue); this.launcherName = launcherName; this.appOutputFileName = Path.of(appOutputFileName); this.additionalLauncherCallback = additionalLauncherCallback; } - public void applyTo(PackageTest pkg) { + public void applyTo(ConfigurationTarget target) { if (launcherName == null) { - pkg.forTypes(WINDOWS, () -> { - pkg.addInitializer(cmd -> { - // Remove parameter added to jpackage command line in HelloApp.addTo() - cmd.removeArgument("--win-console"); - }); + target.addInitializer(cmd -> { + // Remove parameter added to jpackage command line in HelloApp.addTo() + cmd.removeArgument("--win-console"); }); - applyToMainLauncher(pkg); + applyToMainLauncher(target); } else { - applyToAdditionalLauncher(pkg); + applyToAdditionalLauncher(target); } - pkg.addInstallVerifier(this::verifyLauncherExecuted); + target.test().ifPresent(pkg -> { + pkg.addInstallVerifier(this::verifyLauncherExecuted); + }); } static void verify(JPackageCommand cmd) { cmd.verifyIsOfType(SUPPORTED_PACKAGES); - var launcherNames = getLaunchersAsServices(cmd); + var partitionedLauncherNames = partitionLaunchers(cmd); - launcherNames.forEach(toConsumer(launcherName -> { - verify(cmd, launcherName); - })); + var launcherAsServiceNames = partitionedLauncherNames.get(true); + + for (var launcherAsService : List.of(true, false)) { + partitionedLauncherNames.get(launcherAsService).forEach(launcherName -> { + verify(cmd, launcherName, launcherAsService); + }); + } if (WINDOWS.contains(cmd.packageType()) && !cmd.isRuntime()) { Path serviceInstallerPath = cmd.appLayout().launchersDirectory().resolve( "service-installer.exe"); - if (launcherNames.isEmpty()) { + if (launcherAsServiceNames.isEmpty()) { TKit.assertPathExists(serviceInstallerPath, false); } else { TKit.assertFileExists(serviceInstallerPath); @@ -146,16 +184,16 @@ public final class LauncherAsServiceVerifier { if (cmd.isPackageUnpacked()) { servicesSpecificFolders.add(MacHelper.getServicePlistFilePath( - cmd, null).getParent()); + cmd, "foo").getParent()); } } else if (LINUX.contains(cmd.packageType())) { if (cmd.isPackageUnpacked()) { servicesSpecificFolders.add(LinuxHelper.getServiceUnitFilePath( - cmd, null).getParent()); + cmd, "foo").getParent()); } } - if (launcherNames.isEmpty() || cmd.isRuntime()) { + if (launcherAsServiceNames.isEmpty() || cmd.isRuntime()) { servicesSpecificFiles.forEach(path -> TKit.assertPathExists(path, false)); servicesSpecificFolders.forEach(path -> TKit.assertPathExists(path, @@ -187,22 +225,46 @@ public final class LauncherAsServiceVerifier { } static List getLaunchersAsServices(JPackageCommand cmd) { - List launcherNames = new ArrayList<>(); - - if (cmd.hasArgument("--launcher-as-service")) { - launcherNames.add(null); - } - - forEachAdditionalLauncher(cmd, toBiConsumer((launcherName, propFilePath) -> { - if (new PropertyFile(propFilePath).findBooleanProperty("launcher-as-service").orElse(false)) { - launcherNames.add(launcherName); - } - })); - - return launcherNames; + return Objects.requireNonNull(partitionLaunchers(cmd).get(true)); } - private boolean canVerifyInstall(JPackageCommand cmd) throws IOException { + private static Map> partitionLaunchers(JPackageCommand cmd) { + if (cmd.isRuntime()) { + return Map.of(true, List.of(), false, List.of()); + } else { + return cmd.launcherNames(true).stream().collect(Collectors.partitioningBy(launcherName -> { + return launcherAsService(cmd, launcherName); + })); + } + } + + static boolean launcherAsService(JPackageCommand cmd, String launcherName) { + if (cmd.isMainLauncher(launcherName)) { + return PropertyFinder.findLauncherProperty(cmd, null, + PropertyFinder.cmdlineBooleanOption("--launcher-as-service"), + PropertyFinder.nop(), + PropertyFinder.nop() + ).map(Boolean::parseBoolean).orElse(false); + } else { + var mainLauncherValue = PropertyFinder.findLauncherProperty(cmd, null, + PropertyFinder.cmdlineBooleanOption("--launcher-as-service"), + PropertyFinder.nop(), + PropertyFinder.nop() + ).map(Boolean::parseBoolean).orElse(false); + + var value = PropertyFinder.findLauncherProperty(cmd, launcherName, + PropertyFinder.nop(), + PropertyFinder.launcherPropertyFile("launcher-as-service"), + PropertyFinder.appImageFileLauncher(cmd, launcherName, "service").defaultValue(Boolean.FALSE.toString()) + ).map(Boolean::parseBoolean); + + return value.orElse(mainLauncherValue); + } + } + + private boolean canVerifyInstall(JPackageCommand cmd) { + cmd.verifyIsOfType(SUPPORTED_PACKAGES); + String msg = String.format( "Not verifying contents of test output file [%s] for %s launcher", appOutputFilePathInitialize(), @@ -221,8 +283,8 @@ public final class LauncherAsServiceVerifier { return true; } - private void applyToMainLauncher(PackageTest pkg) { - pkg.addInitializer(cmd -> { + private void applyToMainLauncher(ConfigurationTarget target) { + target.addInitializer(cmd -> { cmd.addArgument("--launcher-as-service"); cmd.addArguments("--arguments", JPackageCommand.escapeAndJoin(expectedValue)); @@ -232,7 +294,7 @@ public final class LauncherAsServiceVerifier { }); } - private void applyToAdditionalLauncher(PackageTest pkg) { + private void applyToAdditionalLauncher(ConfigurationTarget target) { var al = new AdditionalLauncher(launcherName) .setProperty("launcher-as-service", true) .addJavaOptions("-Djpackage.test.appOutput=" + appOutputFilePathInitialize().toString()) @@ -240,16 +302,16 @@ public final class LauncherAsServiceVerifier { .addDefaultArguments(expectedValue) .withoutVerifyActions(Action.EXECUTE_LAUNCHER); - Optional.ofNullable(additionalLauncherCallback).ifPresent(v -> v.accept(al)); + additionalLauncherCallback.ifPresent(v -> v.accept(al)); - al.applyTo(pkg); + target.add(al); } - private void verifyLauncherExecuted(JPackageCommand cmd) throws IOException { + public void verifyLauncherExecuted(JPackageCommand cmd) { if (canVerifyInstall(cmd)) { delayInstallVerify(); Path outputFilePath = appOutputFilePathVerify(cmd); - HelloApp.assertApp(cmd.appLauncherPath()) + HelloApp.assertApp(cmd.appLauncherPath(launcherName)) .addParam("jpackage.test.appOutput", outputFilePath.toString()) .addDefaultArguments(expectedValue) .verifyOutput(); @@ -257,31 +319,41 @@ public final class LauncherAsServiceVerifier { } } - private static void deleteOutputFile(Path file) throws IOException { + private static void deleteOutputFile(Path file) { try { TKit.deleteIfExists(file); } catch (FileSystemException ex) { if (TKit.isLinux() || TKit.isOSX()) { // Probably "Operation no permitted" error. Try with "sudo" as the // file is created by a launcher started under root account. - Executor.of("sudo", "rm", "-f").addArgument(file.toString()). - execute(); + Executor.of("sudo", "rm", "-f").addArgument(file.toString()).execute(); } else { - throw ex; + throw new UncheckedIOException(ex); + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static void verify(JPackageCommand cmd, String launcherName, boolean launcherAsService) { + if (LINUX.contains(cmd.packageType())) { + if (launcherAsService) { + verifyLinuxUnitFile(cmd, launcherName); + } else { + var serviceUnitFile = LinuxHelper.getServiceUnitFilePath(cmd, launcherName); + TKit.assertPathExists(serviceUnitFile, false); + } + } else if (MAC_PKG.equals(cmd.packageType())) { + if (launcherAsService) { + verifyMacDaemonPlistFile(cmd, launcherName); + } else { + var servicePlistFile = MacHelper.getServicePlistFilePath(cmd, launcherName); + TKit.assertPathExists(servicePlistFile, false); } } } - 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())) { - verifyMacDaemonPlistFile(cmd, launcherName); - } - } - - private static void verifyLinuxUnitFile(JPackageCommand cmd, - String launcherName) throws IOException { + private static void verifyLinuxUnitFile(JPackageCommand cmd, String launcherName) { var serviceUnitFile = LinuxHelper.getServiceUnitFilePath(cmd, launcherName); @@ -296,11 +368,10 @@ public final class LauncherAsServiceVerifier { TKit.assertTextStream("ExecStart=" + execStartValue) .label("unit file") .predicate(String::equals) - .apply(Files.readAllLines(serviceUnitFile)); + .apply(ThrowingFunction.>toFunction(Files::readAllLines).apply(serviceUnitFile)); } - private static void verifyMacDaemonPlistFile(JPackageCommand cmd, - String launcherName) throws IOException { + private static void verifyMacDaemonPlistFile(JPackageCommand cmd, String launcherName) { var servicePlistFile = MacHelper.getServicePlistFilePath(cmd, launcherName); @@ -348,7 +419,7 @@ public final class LauncherAsServiceVerifier { private final String expectedValue; private final String launcherName; private final Path appOutputFileName; - private final Consumer additionalLauncherCallback; + private final Optional> additionalLauncherCallback; static final Set SUPPORTED_PACKAGES = Stream.of( LINUX, diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java index 1ee3b8d47b0..2cc5fcc95fc 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherShortcut.java @@ -103,9 +103,7 @@ public enum LauncherShortcut { Optional expectShortcut(JPackageCommand cmd, Optional predefinedAppImage, String launcherName) { Objects.requireNonNull(predefinedAppImage); - final var name = Optional.ofNullable(launcherName).orElseGet(cmd::name); - - if (name.equals(cmd.name())) { + if (cmd.isMainLauncher(launcherName)) { return findMainLauncherShortcut(cmd); } else { String[] propertyName = new String[1]; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 1807d3ce256..c8df0f640d0 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -45,7 +45,6 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Matcher; @@ -101,7 +100,7 @@ public final class LinuxHelper { return cmd.pathToUnpackedPackageFile( Path.of("/lib/systemd/system").resolve(getServiceUnitFileName( getPackageName(cmd), - Optional.ofNullable(launcherName).orElseGet(cmd::name)))); + Optional.ofNullable(launcherName).orElseGet(cmd::mainLauncherName)))); } static String getBundleName(JPackageCommand cmd) { @@ -371,12 +370,11 @@ public final class LinuxHelper { 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), + launcherNameFromDesktopFile(cmd, desktopFile), LauncherShortcut.LINUX_SHORTCUT, new DesktopFile(systemDesktopFile, false).findQuotedValue("Path").map(Path::of), List.of("gtk-launch", PathUtils.replaceSuffix(systemDesktopFile.getFileName(), "").toString())); @@ -532,16 +530,11 @@ public final class LinuxHelper { TKit.assertEquals(List.of(), unreferencedIconFiles, "Check there are no unreferenced icon files in the package"); } - private static String launcherNameFromDesktopFile(JPackageCommand cmd, Optional predefinedAppImage, Path desktopFile) { + private static String launcherNameFromDesktopFile(JPackageCommand cmd, 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 Stream.concat(Stream.of(cmd.mainLauncherName()), cmd.addLauncherNames(true).stream()).filter(name-> { return getDesktopFile(cmd, name).equals(desktopFile); }).findAny().orElseThrow(() -> { TKit.assertUnexpected(String.format("Failed to find launcher corresponding to [%s] file", desktopFile)); @@ -557,7 +550,7 @@ public final class LinuxHelper { TKit.trace(String.format("Check [%s] file BEGIN", desktopFile)); - var launcherName = launcherNameFromDesktopFile(cmd, predefinedAppImage, desktopFile); + var launcherName = launcherNameFromDesktopFile(cmd, desktopFile); var data = new DesktopFile(desktopFile, true); @@ -887,8 +880,9 @@ public final class LinuxHelper { return arch; } - private static String getServiceUnitFileName(String packageName, - String launcherName) { + private static String getServiceUnitFileName(String packageName, String launcherName) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(launcherName); try { return getServiceUnitFileName.invoke(null, packageName, launcherName).toString(); } catch (InvocationTargetException | IllegalAccessException ex) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java index 3900851f810..dc753624a32 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -56,6 +56,7 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.UnaryOperator; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -662,16 +663,21 @@ public final class MacHelper { } private static String getPackageId(JPackageCommand cmd) { - return cmd.getArgumentValue("--mac-package-identifier", () -> { - return cmd.getArgumentValue("--main-class", cmd::name, className -> { - var packageName = ClassDesc.of(className).packageName(); - if (packageName.isEmpty()) { - return className; - } else { - return packageName; - } - }); - }); + UnaryOperator getPackageIdFromClassName = className -> { + var packageName = ClassDesc.of(className).packageName(); + if (packageName.isEmpty()) { + return className; + } else { + return packageName; + } + }; + + return PropertyFinder.findAppProperty(cmd, + PropertyFinder.cmdlineOptionWithValue("--mac-package-identifier").or( + PropertyFinder.cmdlineOptionWithValue("--main-class").map(getPackageIdFromClassName) + ), + PropertyFinder.appImageFile(AppImageFile::mainLauncherClassName).map(getPackageIdFromClassName) + ).orElseGet(cmd::name); } public static boolean isXcodeDevToolsInstalled() { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java index 81d31ed7267..123fba56b71 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java @@ -61,11 +61,9 @@ public final class MacSignVerify { assertSigned(bundleRoot, certRequest); - if (!cmd.isRuntime()) { - cmd.addLauncherNames().stream().map(cmd::appLauncherPath).forEach(launcherPath -> { - assertSigned(launcherPath, certRequest); - }); - } + cmd.addLauncherNames(true).stream().map(cmd::appLauncherPath).forEach(launcherPath -> { + assertSigned(launcherPath, certRequest); + }); // Set to "null" if the sign origin is not found, instead of bailing out with an exception. // Let is fail in the following TKit.assertEquals() call with a proper log message. diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PropertyFinder.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PropertyFinder.java new file mode 100644 index 00000000000..e310de855c6 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PropertyFinder.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; +import static jdk.jpackage.test.AdditionalLauncher.getAdditionalLauncherProperties; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import jdk.jpackage.test.AdditionalLauncher.PropertyFile; + +final class PropertyFinder { + + @FunctionalInterface + static interface Finder { + Optional find(T target); + + default Finder defaultValue(String v) { + return target -> { + return Optional.of(find(target).orElse(v)); + }; + } + + default Finder map(UnaryOperator v) { + Objects.requireNonNull(v); + return target -> { + return find(target).map(v); + }; + } + + default Finder or(Finder other) { + return target -> { + return find(target).or(() -> { + return other.find(target); + }); + }; + } + } + + static Finder nop() { + return target -> { + return Optional.empty(); + }; + } + + static Finder appImageFileLauncher(JPackageCommand cmd, String launcherName, String propertyName) { + Objects.requireNonNull(propertyName); + if (cmd.isMainLauncher(launcherName)) { + return target -> { + return Optional.ofNullable(target.launchers().get(target.mainLauncherName()).get(propertyName)); + }; + } else { + return target -> { + return Optional.ofNullable(target.addLaunchers().get(launcherName).get(propertyName)); + }; + } + } + + static Finder appImageFile(Function propertyGetter) { + Objects.requireNonNull(propertyGetter); + return target -> { + return Optional.of(propertyGetter.apply(target)); + }; + } + + static Finder appImageFileOptional(Function> propertyGetter) { + Objects.requireNonNull(propertyGetter); + return target -> { + return propertyGetter.apply(target); + }; + } + + static Finder launcherPropertyFile(String propertyName) { + return target -> { + return target.findProperty(propertyName); + }; + } + + static Finder cmdlineBooleanOption(String optionName) { + return target -> { + return Optional.of(target.hasArgument(optionName)).map(Boolean::valueOf).map(Object::toString); + }; + } + + static Finder cmdlineOptionWithValue(String optionName) { + return target -> { + return Optional.ofNullable(target.getArgumentValue(optionName)); + }; + } + + static Optional findAppProperty( + JPackageCommand cmd, + Finder cmdlineFinder, + Finder appImageFileFinder) { + + Objects.requireNonNull(cmd); + Objects.requireNonNull(cmdlineFinder); + Objects.requireNonNull(appImageFileFinder); + + var reply = cmdlineFinder.find(cmd); + if (reply.isPresent()) { + return reply; + } else { + var appImageFilePath = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of); + return appImageFilePath.map(AppImageFile::load).flatMap(appImageFileFinder::find); + } + } + + static Optional findLauncherProperty( + JPackageCommand cmd, + String launcherName, + Finder cmdlineFinder, + Finder addLauncherPropertyFileFinder, + Finder appImageFileFinder) { + + Objects.requireNonNull(cmd); + Objects.requireNonNull(cmdlineFinder); + Objects.requireNonNull(addLauncherPropertyFileFinder); + Objects.requireNonNull(appImageFileFinder); + + var mainLauncher = cmd.isMainLauncher(launcherName); + + var appImageFilePath = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of); + + Optional reply; + + if (mainLauncher) { + reply = cmdlineFinder.find(cmd); + } else if (appImageFilePath.isEmpty()) { + var props = getAdditionalLauncherProperties(cmd, launcherName); + reply = addLauncherPropertyFileFinder.find(props); + } else { + reply = Optional.empty(); + } + + if (reply.isPresent()) { + return reply; + } else { + return appImageFilePath.map(AppImageFile::load).flatMap(appImageFileFinder::find); + } + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java index baeeda4e569..0afdf31ec68 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -314,8 +314,11 @@ public final class TKit { public static void createTextFile(Path filename, Stream lines) { trace(String.format("Create [%s] text file...", filename.toAbsolutePath().normalize())); - ThrowingRunnable.toRunnable(() -> Files.write(filename, - lines.peek(TKit::trace).collect(Collectors.toList()))).run(); + try { + Files.write(filename, lines.peek(TKit::trace).toList()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } trace("Done"); } @@ -323,16 +326,24 @@ public final class TKit { Collection> props) { trace(String.format("Create [%s] properties file...", propsFilename.toAbsolutePath().normalize())); - ThrowingRunnable.toRunnable(() -> Files.write(propsFilename, - props.stream().map(e -> String.join("=", e.getKey(), - e.getValue())).peek(TKit::trace).collect(Collectors.toList()))).run(); + try { + Files.write(propsFilename, props.stream().map(e -> { + return String.join("=", e.getKey(), e.getValue()); + }).peek(TKit::trace).toList()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } trace("Done"); } - public static void traceFileContents(Path path, String label) throws IOException { + public static void traceFileContents(Path path, String label) { assertFileExists(path); trace(String.format("Dump [%s] %s...", path, label)); - Files.readAllLines(path).forEach(TKit::trace); + try { + Files.readAllLines(path).forEach(TKit::trace); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } trace("Done"); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java index 27e94366e69..0e581c1e7dc 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WinShortcutVerifier.java @@ -23,6 +23,7 @@ package jdk.jpackage.test; import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toUnmodifiableMap; 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; @@ -32,7 +33,6 @@ 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; @@ -51,7 +51,7 @@ 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()) { + if (Stream.of("--win-menu", "--win-shortcut").noneMatch(cmd::hasArgument) && cmd.addLauncherNames(true).isEmpty()) { return; } @@ -170,7 +170,7 @@ public final class WinShortcutVerifier { private static Shortcut createLauncherShortcutSpec(JPackageCommand cmd, String launcherName, SpecialFolder installRoot, Path workDir, ShortcutType type) { - var name = Optional.ofNullable(launcherName).orElseGet(cmd::name); + var name = Optional.ofNullable(launcherName).orElseGet(cmd::mainLauncherName); var appLayout = ApplicationLayout.windowsAppImage().resolveAt( Path.of(installRoot.getMsiPropertyName()).resolve(getInstallationSubDirectory(cmd))); @@ -250,22 +250,19 @@ public final class WinShortcutVerifier { } private static Map> expectShortcuts(JPackageCommand cmd) { - Map> expectedShortcuts = new HashMap<>(); var predefinedAppImage = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of).map(AppImageFile::load); - predefinedAppImage.map(v -> { - return v.launchers().keySet().stream(); - }).orElseGet(() -> { - return Stream.concat(Stream.of(cmd.name()), cmd.addLauncherNames().stream()); - }).forEach(launcherName -> { + return cmd.launcherNames(true).stream().map(launcherName -> { + return Optional.ofNullable(launcherName).orElseGet(cmd::mainLauncherName); + }).map(launcherName -> { var shortcuts = expectLauncherShortcuts(cmd, predefinedAppImage, launcherName); - if (!shortcuts.isEmpty()) { - expectedShortcuts.put(launcherName, shortcuts); + if (shortcuts.isEmpty()) { + return null; + } else { + return Map.entry(launcherName, shortcuts); } - }); - - return expectedShortcuts; + }).filter(Objects::nonNull).collect(toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); } private static InvokeShortcutSpec convert(JPackageCommand cmd, String launcherName, Shortcut shortcut) { diff --git a/test/jdk/tools/jpackage/share/ServiceTest.java b/test/jdk/tools/jpackage/share/ServiceTest.java index f11ca628ed4..56e003f508c 100644 --- a/test/jdk/tools/jpackage/share/ServiceTest.java +++ b/test/jdk/tools/jpackage/share/ServiceTest.java @@ -21,18 +21,26 @@ * questions. */ +import static jdk.jpackage.test.PackageType.MAC_DMG; +import static jdk.jpackage.test.PackageType.WINDOWS; + import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HexFormat; import java.util.Optional; +import java.util.function.Consumer; import java.util.stream.Stream; -import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.AdditionalLauncher; +import jdk.jpackage.test.Annotations.Parameter; import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.ConfigurationTarget; import jdk.jpackage.test.JPackageCommand; import jdk.jpackage.test.JavaTool; import jdk.jpackage.test.LauncherAsServiceVerifier; -import static jdk.jpackage.test.PackageType.MAC_DMG; -import static jdk.jpackage.test.PackageType.WINDOWS; +import jdk.jpackage.test.LauncherVerifier.Action; +import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.RunnablePackageTest; import jdk.jpackage.test.TKit; @@ -47,11 +55,26 @@ import jdk.jpackage.test.TKit; * @library /test/jdk/tools/jpackage/helpers * @build jdk.jpackage.test.* * @key jpackagePlatformPackage + * @requires (jpackage.test.SQETest != null) * @compile -Xlint:all -Werror ServiceTest.java - * @run main/othervm/timeout=360 -Xmx512m + * @run main/othervm/timeout=2880 -Xmx512m + * jdk.jpackage.test.Main + * --jpt-run=ServiceTest.test,ServiceTest.testUpdate + */ + +/* + * @test + * @summary Launcher as service packaging test + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.* + * @key jpackagePlatformPackage + * @requires (jpackage.test.SQETest == null) + * @compile -Xlint:all -Werror ServiceTest.java + * @run main/othervm/timeout=2880 -Xmx512m * jdk.jpackage.test.Main * --jpt-run=ServiceTest */ + public class ServiceTest { public ServiceTest() { @@ -86,10 +109,9 @@ public class ServiceTest { @Test public void test() throws Throwable { - var testInitializer = createTestInitializer(); var pkg = createPackageTest().addHelloAppInitializer("com.foo.ServiceTest"); LauncherAsServiceVerifier.build().setExpectedValue("A1").applyTo(pkg); - testInitializer.applyTo(pkg); + createTestInitializer().applyTo(pkg); pkg.run(); } @@ -132,6 +154,108 @@ public class ServiceTest { new PackageTest.Group(pkg, pkg2).run(); } + @Test + @Parameter("true") + @Parameter("false") + public void testAddL(boolean mainLauncherAsService) { + + final var uniqueOutputFile = uniqueOutputFile(); + + createPackageTest() + .addHelloAppInitializer("com.buz.AddLaunchersServiceTest") + .mutate(test -> { + if (mainLauncherAsService) { + LauncherAsServiceVerifier.build() + .mutate(uniqueOutputFile).appendAppOutputFileNamePrefix("-") + .setExpectedValue("Main").applyTo(test); + } + }) + // Regular launcher. The installer should not automatically execute it. + .mutate(new AdditionalLauncher("notservice") + .withoutVerifyActions(Action.EXECUTE_LAUNCHER) + .setProperty("launcher-as-service", Boolean.FALSE) + .addJavaOptions("-Djpackage.test.noexit=true")::applyTo) + // Additional launcher with explicit "launcher-as-service=true" property in the property file. + .mutate(LauncherAsServiceVerifier.build() + .mutate(uniqueOutputFile).appendAppOutputFileNamePrefix("-A1-") + .setLauncherName("AL1") + .setExpectedValue("AL1")::applyTo) + .mutate(test -> { + if (mainLauncherAsService) { + // Additional launcher without "launcher-as-service" property in the property file. + // Still, should be installed as a service. + LauncherAsServiceVerifier.build() + .mutate(uniqueOutputFile).appendAppOutputFileNamePrefix("-A2-") + .setLauncherName("AL2") + .setExpectedValue("AL2") + .setAdditionalLauncherCallback(al -> { + al.removeProperty("launcher-as-service"); + }) + .applyTo(test); + } + }) + .mutate(createTestInitializer()::applyTo) + .run(); + } + + @Test + @Parameter("true") + @Parameter("false") + public void testAddLFromAppImage(boolean mainLauncherAsService) { + + var uniqueOutputFile = uniqueOutputFile(); + + var appImageCmd = new ConfigurationTarget(JPackageCommand.helloAppImage("com.bar.AddLaunchersFromAppImageServiceTest")); + + if (RunnablePackageTest.hasAction(RunnablePackageTest.Action.INSTALL)) { + // Ensure launchers are executable because the output bundle will be installed + // and we want to verify launchers are automatically started by the installer. + appImageCmd.addInitializer(JPackageCommand::ignoreFakeRuntime); + } + + if (mainLauncherAsService) { + LauncherAsServiceVerifier.build() + .mutate(uniqueOutputFile).appendAppOutputFileNamePrefix("-") + .setExpectedValue("Main") + .applyTo(appImageCmd); + // Can not use "--launcher-as-service" option with app image packaging. + appImageCmd.cmd().orElseThrow().removeArgument("--launcher-as-service"); + } else { + appImageCmd.addInitializer(cmd -> { + // Configure the main launcher to hang at the end of the execution. + // The main launcher should not be executed in this test. + // If it is executed, it indicates it was started as a service, + // which must fail the test. The launcher's hang-up will be the event failing the test. + cmd.addArguments("--java-options", "-Djpackage.test.noexit=true"); + }); + } + + // Additional launcher with explicit "launcher-as-service=true" property in the property file. + LauncherAsServiceVerifier.build() + .mutate(uniqueOutputFile).appendAppOutputFileNamePrefix("-A1-") + .setLauncherName("AL1") + .setExpectedValue("AL1").applyTo(appImageCmd); + + // Regular launcher. The installer should not automatically execute it. + appImageCmd.add(new AdditionalLauncher("notservice") + .withoutVerifyActions(Action.EXECUTE_LAUNCHER) + .addJavaOptions("-Djpackage.test.noexit=true")); + + new PackageTest().excludeTypes(MAC_DMG) + .addRunOnceInitializer(appImageCmd.cmd().orElseThrow()::execute) + .addInitializer(cmd -> { + cmd.removeArgumentWithValue("--input"); + cmd.addArguments("--app-image", appImageCmd.cmd().orElseThrow().outputBundle()); + }) + .addInitializer(cmd -> { + if (mainLauncherAsService) { + cmd.addArgument("--launcher-as-service"); + } + }) + .mutate(createTestInitializer()::applyTo) + .run(); + } + private final class TestInitializer { TestInitializer setUpgradeCode(String v) { @@ -139,11 +263,14 @@ public class ServiceTest { return this; } - void applyTo(PackageTest test) throws IOException { + void applyTo(PackageTest test) { if (winServiceInstaller != null) { var resourceDir = TKit.createTempDirectory("resource-dir"); - Files.copy(winServiceInstaller, resourceDir.resolve( - "service-installer.exe")); + try { + Files.copy(winServiceInstaller, resourceDir.resolve("service-installer.exe")); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } test.forTypes(WINDOWS, () -> test.addInitializer(cmd -> { cmd.addArguments("--resource-dir", resourceDir); @@ -165,9 +292,28 @@ public class ServiceTest { } private static PackageTest createPackageTest() { - return new PackageTest() + var test = new PackageTest() .excludeTypes(MAC_DMG) // DMG not supported .addInitializer(JPackageCommand::setInputToEmptyDirectory); + if (RunnablePackageTest.hasAction(RunnablePackageTest.Action.INSTALL)) { + // Ensure launchers are executable because the output bundle will be installed + // and we want to verify launchers are automatically started by the installer. + test.addInitializer(JPackageCommand::ignoreFakeRuntime); + } + return test; + } + + private static Consumer uniqueOutputFile() { + var prefix = uniquePrefix(); + return builder -> { + builder.setAppOutputFileNamePrefixToAppName() + .appendAppOutputFileNamePrefix("-") + .appendAppOutputFileNamePrefix(prefix); + }; + } + + private static String uniquePrefix() { + return HexFormat.of().toHexDigits(System.currentTimeMillis()); } private final Path winServiceInstaller;