8370969: --launcher-as-service option is ignored when used with --app-image option

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2025-11-01 23:29:48 +00:00
parent 13b3d2fca1
commit f7f4f903cf
14 changed files with 601 additions and 163 deletions

View File

@ -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<String, ? super Object> mapLauncherInfo(LauncherInfo launcherInfo) {
private static Map<String, ? super Object> mapLauncherInfo(ExternalApplication appImageFile, LauncherInfo launcherInfo) {
Map<String, ? super Object> 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;
}

View File

@ -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);

View File

@ -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<Map.Entry<String, String>> currentSection = new ArrayList<>();
for (String line : Files.readAllLines(path)) {
for (String line : ThrowingFunction.<Path, List<String>>toFunction(Files::readAllLines).apply(path)) {
Matcher matcher = sectionBeginRegex.matcher(line);
if (matcher.find()) {
if (currentSectionName != null) {

View File

@ -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<String> listAppImage(Path to) {

View File

@ -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<JPackageCommand> {
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<JPackageCommand> {
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<JPackageCommand> {
}
/**
* Returns names of all additional launchers or empty list if none
* configured.
* Returns names of additional launchers or an empty list if none configured.
* <p>
* 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<String> addLauncherNames() {
public List<String> addLauncherNames(boolean lookupInPrederfinedAppImage) {
if (isRuntime()) {
return List.of();
}
List<String> 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.
* <p>
* 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<String> launcherNames(boolean lookupInPrederfinedAppImage) {
if (isRuntime()) {
return List.of();
}
List<String> 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<JPackageCommand> {
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<JPackageCommand> {
// 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");
}

View File

@ -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<AdditionalLauncher> 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<Builder> 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<AdditionalLauncher> additionalLauncherCallback;
}
@ -97,41 +126,50 @@ public final class LauncherAsServiceVerifier {
private LauncherAsServiceVerifier(String launcherName,
String appOutputFileName,
String expectedArgValue,
Consumer<AdditionalLauncher> additionalLauncherCallback) {
this.expectedValue = expectedArgValue;
Optional<Consumer<AdditionalLauncher>> 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<String> getLaunchersAsServices(JPackageCommand cmd) {
List<String> 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<Boolean, List<String>> 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.<Path, List<String>>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<AdditionalLauncher> additionalLauncherCallback;
private final Optional<Consumer<AdditionalLauncher>> additionalLauncherCallback;
static final Set<PackageType> SUPPORTED_PACKAGES = Stream.of(
LINUX,

View File

@ -103,9 +103,7 @@ public enum LauncherShortcut {
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())) {
if (cmd.isMainLauncher(launcherName)) {
return findMainLauncherShortcut(cmd);
} else {
String[] propertyName = new String[1];

View File

@ -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<AppImageFile> 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) {

View File

@ -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<String> 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() {

View File

@ -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.

View File

@ -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<T> {
Optional<String> find(T target);
default Finder<T> defaultValue(String v) {
return target -> {
return Optional.of(find(target).orElse(v));
};
}
default Finder<T> map(UnaryOperator<String> v) {
Objects.requireNonNull(v);
return target -> {
return find(target).map(v);
};
}
default Finder<T> or(Finder<T> other) {
return target -> {
return find(target).or(() -> {
return other.find(target);
});
};
}
}
static <T> Finder<T> nop() {
return target -> {
return Optional.empty();
};
}
static Finder<AppImageFile> 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> appImageFile(Function<AppImageFile, String> propertyGetter) {
Objects.requireNonNull(propertyGetter);
return target -> {
return Optional.of(propertyGetter.apply(target));
};
}
static Finder<AppImageFile> appImageFileOptional(Function<AppImageFile, Optional<String>> propertyGetter) {
Objects.requireNonNull(propertyGetter);
return target -> {
return propertyGetter.apply(target);
};
}
static Finder<PropertyFile> launcherPropertyFile(String propertyName) {
return target -> {
return target.findProperty(propertyName);
};
}
static Finder<JPackageCommand> cmdlineBooleanOption(String optionName) {
return target -> {
return Optional.of(target.hasArgument(optionName)).map(Boolean::valueOf).map(Object::toString);
};
}
static Finder<JPackageCommand> cmdlineOptionWithValue(String optionName) {
return target -> {
return Optional.ofNullable(target.getArgumentValue(optionName));
};
}
static Optional<String> findAppProperty(
JPackageCommand cmd,
Finder<JPackageCommand> cmdlineFinder,
Finder<AppImageFile> 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<String> findLauncherProperty(
JPackageCommand cmd,
String launcherName,
Finder<JPackageCommand> cmdlineFinder,
Finder<PropertyFile> addLauncherPropertyFileFinder,
Finder<AppImageFile> 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<String> 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);
}
}
}

View File

@ -314,8 +314,11 @@ public final class TKit {
public static void createTextFile(Path filename, Stream<String> 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<Map.Entry<String, String>> 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");
}

View File

@ -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<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 -> {
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) {

View File

@ -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<LauncherAsServiceVerifier.Builder> 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;