8364564: Shortcut configuration is not recorded in .jpackage.xml file

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2025-08-08 22:11:52 +00:00
parent c1c0155604
commit 8ad1fcc48a
15 changed files with 327 additions and 119 deletions

View File

@ -24,10 +24,10 @@
*/
package jdk.jpackage.internal;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.LinuxLauncher;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.model.Launcher;
import static jdk.jpackage.internal.ApplicationImageUtils.createLauncherIconResource;
import static jdk.jpackage.internal.model.LauncherShortcut.toRequest;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@ -45,12 +45,13 @@ import java.util.stream.Stream;
import javax.imageio.ImageIO;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import static jdk.jpackage.internal.ApplicationImageUtils.createLauncherIconResource;
import jdk.jpackage.internal.model.FileAssociation;
import jdk.jpackage.internal.model.LinuxLauncher;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.util.CompositeProxy;
import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.XmlUtils;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
/**
* Helper to create files for desktop integration.
@ -77,7 +78,7 @@ final class DesktopIntegration extends ShellCustomAction {
// Need desktop and icon files if one of conditions is met:
// - there are file associations configured
// - user explicitly requested to create a shortcut
boolean withDesktopFile = !associations.isEmpty() || launcher.shortcut().orElse(false);
boolean withDesktopFile = !associations.isEmpty() || toRequest(launcher.shortcut()).orElse(false);
var curIconResource = createLauncherIconResource(pkg.app(), launcher,
env::createResource);
@ -132,7 +133,7 @@ final class DesktopIntegration extends ShellCustomAction {
nestedIntegrations = pkg.app().additionalLaunchers().stream().map(v -> {
return (LinuxLauncher)v;
}).filter(l -> {
return l.shortcut().orElse(true);
return toRequest(l.shortcut()).orElse(true);
}).map(toFunction(l -> {
return new DesktopIntegration(env, pkg, l);
})).toList();
@ -225,6 +226,9 @@ final class DesktopIntegration extends ShellCustomAction {
}
private Map<String, String> createDataForDesktopFile() {
var installedLayout = pkg.asInstalledPackageApplicationLayout().orElseThrow();
Map<String, String> data = new HashMap<>();
data.put("APPLICATION_NAME", launcher.name());
data.put("APPLICATION_DESCRIPTION", launcher.description());
@ -232,8 +236,7 @@ final class DesktopIntegration extends ShellCustomAction {
f -> f.installPath().toString()).orElse(null));
data.put("DEPLOY_BUNDLE_CATEGORY", pkg.menuGroupName());
data.put("APPLICATION_LAUNCHER", Enquoter.forPropertyValues().applyTo(
pkg.asInstalledPackageApplicationLayout().orElseThrow().launchersDirectory().resolve(
launcher.executableNameWithSuffix()).toString()));
installedLayout.launchersDirectory().resolve(launcher.executableNameWithSuffix()).toString()));
return data;
}
@ -481,7 +484,7 @@ final class DesktopIntegration extends ShellCustomAction {
private final BuildEnv env;
private final LinuxPackage pkg;
private final Launcher launcher;
private final LinuxLauncher launcher;
private final List<LinuxFileAssociation> associations;

View File

@ -29,16 +29,14 @@ import static jdk.jpackage.internal.FromParams.createApplicationBuilder;
import static jdk.jpackage.internal.FromParams.createApplicationBundlerParam;
import static jdk.jpackage.internal.FromParams.createPackageBuilder;
import static jdk.jpackage.internal.FromParams.createPackageBundlerParam;
import static jdk.jpackage.internal.FromParams.findLauncherShortcut;
import static jdk.jpackage.internal.LinuxPackagingPipeline.APPLICATION_LAYOUT;
import static jdk.jpackage.internal.StandardBundlerParam.SHORTCUT_HINT;
import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB;
import static jdk.jpackage.internal.model.StandardPackageType.LINUX_RPM;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.LinuxApplication;
import jdk.jpackage.internal.model.LinuxLauncher;
@ -51,11 +49,10 @@ final class LinuxFromParams {
private static LinuxApplication createLinuxApplication(
Map<String, ? super Object> params) throws ConfigException, IOException {
final var launcherFromParams = new LauncherFromParams();
final var app = createApplicationBuilder(params, toFunction(launcherParams -> {
final var launcher = launcherFromParams.create(launcherParams);
final var shortcut = Stream.of(SHORTCUT_HINT, LINUX_SHORTCUT_HINT).map(param -> {
return param.findIn(launcherParams);
}).filter(Optional::isPresent).map(Optional::get).findFirst();
final var shortcut = findLauncherShortcut(LINUX_SHORTCUT_HINT, params, launcherParams);
return LinuxLauncher.create(launcher, new LinuxLauncherMixin.Stub(shortcut));
}), APPLICATION_LAYOUT).create();
return LinuxApplication.create(app);

View File

@ -24,6 +24,7 @@
*/
package jdk.jpackage.internal.model;
import java.util.HashMap;
import java.util.Map;
import jdk.jpackage.internal.util.CompositeProxy;
@ -36,9 +37,11 @@ public interface LinuxLauncher extends Launcher, LinuxLauncherMixin {
@Override
default Map<String, String> extraAppImageFileData() {
return shortcut().map(v -> {
return Map.of("shortcut", Boolean.toString(v));
}).orElseGet(Map::of);
Map<String, String> map = new HashMap<>();
shortcut().ifPresent(shortcut -> {
shortcut.store(SHORTCUT_ID, map::put);
});
return map;
}
/**
@ -52,4 +55,6 @@ public interface LinuxLauncher extends Launcher, LinuxLauncherMixin {
public static LinuxLauncher create(Launcher launcher, LinuxLauncherMixin mixin) {
return CompositeProxy.create(LinuxLauncher.class, launcher, mixin);
}
public static final String SHORTCUT_ID = "linux-shortcut";
}

View File

@ -32,24 +32,20 @@ import java.util.Optional;
public interface LinuxLauncherMixin {
/**
* Gets the start menu shortcut setting of this application launcher.
* Gets the start menu shortcut of this application launcher.
* <p>
* Returns <code>true</code> if this application launcher was requested to have
* the start menu shortcut.
* <p>
* Returns <code>false</code> if this application launcher was requested not to
* have the start menu shortcut.
* <p>
* Returns an empty {@link Optional} instance if there was no request about the
* start menu shortcut for this application launcher.
* Returns a non-empty {@link Optional} instance if a request about the start
* menu shortcut for this application launcher was made and an empty
* {@link Optional} instance if there was no request about the start menu
* shortcut for this application launcher.
*
* @return the start menu shortcut setting of this application launcher
* @return the start menu shortcut of this application launcher
*/
Optional<Boolean> shortcut();
Optional<LauncherShortcut> shortcut();
/**
* Default implementation of {@link LinuxLauncherMixin} interface.
*/
record Stub(Optional<Boolean> shortcut) implements LinuxLauncherMixin {
record Stub(Optional<LauncherShortcut> shortcut) implements LinuxLauncherMixin {
}
}

View File

@ -36,8 +36,6 @@ import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.Arguments.CLIOptions;
import static jdk.jpackage.internal.StandardBundlerParam.LAUNCHER_DATA;
import static jdk.jpackage.internal.StandardBundlerParam.APP_NAME;
import static jdk.jpackage.internal.StandardBundlerParam.MENU_HINT;
import static jdk.jpackage.internal.StandardBundlerParam.SHORTCUT_HINT;
/*
* AddLauncherArguments
@ -135,16 +133,16 @@ class AddLauncherArguments {
Arguments.putUnlessNull(bundleParams,
CLIOptions.WIN_CONSOLE_HINT.getId(),
getOptionValue(CLIOptions.WIN_CONSOLE_HINT));
Arguments.putUnlessNull(bundleParams, SHORTCUT_HINT.getID(),
Arguments.putUnlessNull(bundleParams, CLIOptions.WIN_SHORTCUT_HINT.getId(),
getOptionValue(CLIOptions.WIN_SHORTCUT_HINT));
Arguments.putUnlessNull(bundleParams, MENU_HINT.getID(),
Arguments.putUnlessNull(bundleParams, CLIOptions.WIN_MENU_HINT.getId(),
getOptionValue(CLIOptions.WIN_MENU_HINT));
}
if (OperatingSystem.isLinux()) {
Arguments.putUnlessNull(bundleParams, CLIOptions.LINUX_CATEGORY.getId(),
getOptionValue(CLIOptions.LINUX_CATEGORY));
Arguments.putUnlessNull(bundleParams, SHORTCUT_HINT.getID(),
Arguments.putUnlessNull(bundleParams, CLIOptions.LINUX_SHORTCUT_HINT.getId(),
getOptionValue(CLIOptions.LINUX_SHORTCUT_HINT));
}

View File

@ -24,6 +24,9 @@
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.Arguments.CLIOptions.LINUX_SHORTCUT_HINT;
import static jdk.jpackage.internal.Arguments.CLIOptions.WIN_MENU_HINT;
import static jdk.jpackage.internal.Arguments.CLIOptions.WIN_SHORTCUT_HINT;
import static jdk.jpackage.internal.StandardBundlerParam.ABOUT_URL;
import static jdk.jpackage.internal.StandardBundlerParam.ADD_LAUNCHERS;
import static jdk.jpackage.internal.StandardBundlerParam.ADD_MODULES;
@ -63,6 +66,8 @@ import jdk.jpackage.internal.model.ApplicationLayout;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.ExternalApplication.LauncherInfo;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.LauncherShortcut;
import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory;
import jdk.jpackage.internal.model.PackageType;
import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.util.function.ThrowingFunction;
@ -165,6 +170,32 @@ final class FromParams {
jdk.jpackage.internal.model.Package.class.getName()));
}
static Optional<LauncherShortcut> findLauncherShortcut(
BundlerParamInfo<Boolean> shortcutParam,
Map<String, ? super Object> mainParams,
Map<String, ? super Object> launcherParams) {
Optional<Boolean> launcherValue;
if (launcherParams == mainParams) {
// The main launcher
launcherValue = Optional.empty();
} else {
launcherValue = shortcutParam.findIn(launcherParams);
}
return launcherValue.map(withShortcut -> {
if (withShortcut) {
return Optional.of(LauncherShortcutStartupDirectory.DEFAULT);
} else {
return Optional.<LauncherShortcutStartupDirectory>empty();
}
}).or(() -> {
return shortcutParam.findIn(mainParams).map(_ -> {
return Optional.of(LauncherShortcutStartupDirectory.DEFAULT);
});
}).map(LauncherShortcut::new);
}
private static ApplicationLaunchers createLaunchers(
Map<String, ? super Object> params,
Function<Map<String, ? super Object>, Launcher> launcherMapper) {
@ -195,8 +226,9 @@ final class FromParams {
// mainParams), APP_NAME.fetchFrom(launcherParams)));
launcherParams.put(DESCRIPTION.getID(), DESCRIPTION.fetchFrom(mainParams));
}
return AddLauncherArguments.merge(mainParams, launcherParams, ICON.getID(), ADD_LAUNCHERS
.getID(), FILE_ASSOCIATIONS.getID());
return AddLauncherArguments.merge(mainParams, launcherParams, ICON.getID(),
ADD_LAUNCHERS.getID(), FILE_ASSOCIATIONS.getID(), WIN_MENU_HINT.getId(),
WIN_SHORTCUT_HINT.getId(), LINUX_SHORTCUT_HINT.getId());
}
static final BundlerParamInfo<Application> APPLICATION = createApplicationBundlerParam(null);

View File

@ -307,24 +307,6 @@ final class StandardBundlerParam {
true : Boolean.valueOf(s)
);
static final BundlerParamInfo<Boolean> SHORTCUT_HINT =
new BundlerParamInfo<>(
"shortcut-hint", // not directly related to a CLI option
Boolean.class,
params -> true, // defaults to true
(s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
true : Boolean.valueOf(s)
);
static final BundlerParamInfo<Boolean> MENU_HINT =
new BundlerParamInfo<>(
"menu-hint", // not directly related to a CLI option
Boolean.class,
params -> true, // defaults to true
(s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
true : Boolean.valueOf(s)
);
static final BundlerParamInfo<Path> RESOURCE_DIR =
new BundlerParamInfo<>(
Arguments.CLIOptions.RESOURCE_DIR.getId(),

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.model;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
/**
* A shortcut to launch an application launcher.
*/
public record LauncherShortcut(Optional<LauncherShortcutStartupDirectory> startupDirectory) {
public LauncherShortcut {
Objects.requireNonNull(startupDirectory);
}
public LauncherShortcut(LauncherShortcutStartupDirectory startupDirectory) {
this(Optional.of(startupDirectory));
}
public LauncherShortcut() {
this(Optional.empty());
}
void store(String propertyName, BiConsumer<String, String> sink) {
Objects.requireNonNull(propertyName);
Objects.requireNonNull(sink);
if (startupDirectory.isEmpty()) {
sink.accept(propertyName, Boolean.FALSE.toString());
} else {
startupDirectory.ifPresent(v -> {
sink.accept(propertyName, v.asStringValue());
});
}
}
/**
* Converts the given shortcut into a shortcut request.
* <p>
* Returns <code>true</code> if shortcut was explicitly requested.
* <p>
* Returns <code>false</code> if no shortcut was explicitly requested.
* <p>
* Returns an empty {@link Optional} instance if there was no shortcut request.
*
* @return shortcut request
*/
public static Optional<Boolean> toRequest(Optional<LauncherShortcut> shortcut) {
return shortcut.map(v -> v.startupDirectory().isPresent());
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.model;
import java.util.Objects;
/**
* The directory in which to run an application launcher when it is started from
* a shortcut.
*/
public enum LauncherShortcutStartupDirectory {
/**
* Platform-specific default value.
* <p>
* On Windows, it indicates that the startup directory should be the package's
* installation directory.
* <p>
* On Linux, it indicates that a shortcut doesn't have the startup directory
* configured explicitly.
*/
DEFAULT("true");
LauncherShortcutStartupDirectory(String stringValue) {
this.stringValue = Objects.requireNonNull(stringValue);
}
public String asStringValue() {
return stringValue;
}
private final String stringValue;
}

View File

@ -24,24 +24,19 @@
*/
package jdk.jpackage.internal;
import static java.util.stream.Collectors.toSet;
import static jdk.jpackage.internal.BundlerParamInfo.createBooleanBundlerParam;
import static jdk.jpackage.internal.BundlerParamInfo.createStringBundlerParam;
import static jdk.jpackage.internal.FromParams.createApplicationBuilder;
import static jdk.jpackage.internal.FromParams.createApplicationBundlerParam;
import static jdk.jpackage.internal.FromParams.createPackageBuilder;
import static jdk.jpackage.internal.FromParams.createPackageBundlerParam;
import static jdk.jpackage.internal.StandardBundlerParam.MENU_HINT;
import static jdk.jpackage.internal.FromParams.findLauncherShortcut;
import static jdk.jpackage.internal.StandardBundlerParam.RESOURCE_DIR;
import static jdk.jpackage.internal.StandardBundlerParam.SHORTCUT_HINT;
import static jdk.jpackage.internal.WinPackagingPipeline.APPLICATION_LAYOUT;
import static jdk.jpackage.internal.model.StandardPackageType.WIN_MSI;
import static jdk.jpackage.internal.model.WinLauncherMixin.WinShortcut.WIN_SHORTCUT_DESKTOP;
import static jdk.jpackage.internal.model.WinLauncherMixin.WinShortcut.WIN_SHORTCUT_START_MENU;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import jdk.jpackage.internal.model.ConfigException;
@ -63,18 +58,11 @@ final class WinFromParams {
final boolean isConsole = CONSOLE_HINT.findIn(launcherParams).orElse(false);
final var shortcuts = Map.of(WIN_SHORTCUT_DESKTOP, List.of(SHORTCUT_HINT,
WIN_SHORTCUT_HINT), WIN_SHORTCUT_START_MENU, List.of(MENU_HINT,
WIN_MENU_HINT)).entrySet().stream().filter(e -> {
final var startMenuShortcut = findLauncherShortcut(WIN_MENU_HINT, params, launcherParams);
final var shortcutParams = e.getValue();
final var desktopShortcut = findLauncherShortcut(WIN_SHORTCUT_HINT, params, launcherParams);
return shortcutParams.get(0).findIn(launcherParams).orElseGet(() -> {
return shortcutParams.get(1).findIn(launcherParams).orElse(false);
});
}).map(Map.Entry::getKey).collect(toSet());
return WinLauncher.create(launcher, new WinLauncherMixin.Stub(isConsole, shortcuts));
return WinLauncher.create(launcher, new WinLauncherMixin.Stub(isConsole, startMenuShortcut, desktopShortcut));
}), APPLICATION_LAYOUT).create();

View File

@ -25,12 +25,9 @@
package jdk.jpackage.internal;
import jdk.jpackage.internal.model.WinLauncher;
import jdk.jpackage.internal.model.WinMsiPackage;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.model.ApplicationLayout;
import jdk.jpackage.internal.util.PathGroup;
import static java.util.stream.Collectors.toMap;
import static jdk.jpackage.internal.util.CollectionUtils.toCollection;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
@ -50,7 +47,6 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toMap;
import java.util.stream.Stream;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
@ -60,15 +56,19 @@ import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import static jdk.jpackage.internal.util.CollectionUtils.toCollection;
import jdk.jpackage.internal.model.WinLauncherMixin.WinShortcut;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.ApplicationLayout;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.model.FileAssociation;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.LauncherShortcut;
import jdk.jpackage.internal.model.WinLauncher;
import jdk.jpackage.internal.model.WinMsiPackage;
import jdk.jpackage.internal.util.PathGroup;
import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.XmlUtils;
import jdk.jpackage.internal.util.XmlConsumer;
import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer;
import jdk.jpackage.internal.util.XmlUtils;
import org.w3c.dom.NodeList;
/**
@ -352,7 +352,7 @@ final class WixAppImageFragmentBuilder extends WixFragmentBuilder {
private final Config cfg;
private final Id id;
};
}
private static void addComponentGroup(XMLStreamWriter xml, String id,
List<String> componentIds) throws XMLStreamException, IOException {
@ -469,7 +469,18 @@ final class WixAppImageFragmentBuilder extends WixFragmentBuilder {
launcher.executableNameWithSuffix());
if (folder.isRequestedFor(launcher)) {
String componentId = addShortcutComponent(xml, launcherPath, folder);
var workDirectory = folder.shortcut(launcher).startupDirectory().map(v -> {
switch (v) {
case DEFAULT -> {
return INSTALLDIR;
}
default -> {
throw new AssertionError();
}
}
}).orElseThrow();
String componentId = addShortcutComponent(xml, launcherPath, folder, workDirectory);
if (componentId != null) {
Path folderPath = folder.getPath(this);
@ -499,23 +510,26 @@ final class WixAppImageFragmentBuilder extends WixFragmentBuilder {
}
private String addShortcutComponent(XMLStreamWriter xml, Path launcherPath,
ShortcutsFolder folder) throws XMLStreamException, IOException {
ShortcutsFolder folder, Path shortcutWorkDir) throws XMLStreamException, IOException {
Objects.requireNonNull(folder);
if (!INSTALLDIR.equals(launcherPath.getName(0))) {
throw throwInvalidPathException(launcherPath);
}
if (!INSTALLDIR.equals(shortcutWorkDir.getName(0))) {
throw throwInvalidPathException(shortcutWorkDir);
}
String launcherBasename = PathUtils.replaceSuffix(
IOUtils.getFileName(launcherPath), "").toString();
Path shortcutPath = folder.getPath(this).resolve(launcherBasename);
return addComponent(xml, shortcutPath, Component.Shortcut, unused -> {
xml.writeAttribute("Name", launcherBasename);
xml.writeAttribute("WorkingDirectory", INSTALLDIR.toString());
xml.writeAttribute("WorkingDirectory", Id.Folder.of(shortcutWorkDir));
xml.writeAttribute("Advertise", "no");
xml.writeAttribute("Target", String.format("[#%s]",
Component.File.idOf(launcherPath)));
xml.writeAttribute("Target", String.format("[#%s]", Id.File.of(launcherPath)));
});
}
@ -906,15 +920,15 @@ final class WixAppImageFragmentBuilder extends WixFragmentBuilder {
}
enum ShortcutsFolder {
ProgramMenu(PROGRAM_MENU_PATH, WinShortcut.WIN_SHORTCUT_START_MENU,
ProgramMenu(PROGRAM_MENU_PATH, WinLauncher::startMenuShortcut,
"JP_INSTALL_STARTMENU_SHORTCUT", "JpStartMenuShortcutPrompt"),
Desktop(DESKTOP_PATH, WinShortcut.WIN_SHORTCUT_DESKTOP,
Desktop(DESKTOP_PATH, WinLauncher::desktopShortcut,
"JP_INSTALL_DESKTOP_SHORTCUT", "JpDesktopShortcutPrompt");
private ShortcutsFolder(Path root, WinShortcut shortcutId,
private ShortcutsFolder(Path root, Function<WinLauncher, Optional<LauncherShortcut>> shortcut,
String property, String wixVariableName) {
this.root = root;
this.shortcutId = shortcutId;
this.shortcut = shortcut;
this.wixVariableName = wixVariableName;
this.property = property;
}
@ -927,7 +941,11 @@ final class WixAppImageFragmentBuilder extends WixFragmentBuilder {
}
boolean isRequestedFor(WinLauncher launcher) {
return launcher.shortcuts().contains(shortcutId);
return LauncherShortcut.toRequest(shortcut.apply(launcher)).orElse(false);
}
LauncherShortcut shortcut(WinLauncher launcher) {
return shortcut.apply(launcher).orElseThrow();
}
String getWixVariableName() {
@ -947,7 +965,7 @@ final class WixAppImageFragmentBuilder extends WixFragmentBuilder {
private final Path root;
private final String property;
private final String wixVariableName;
private final WinShortcut shortcutId;
private final Function<WinLauncher, Optional<LauncherShortcut>> shortcut;
}
private boolean systemWide;

View File

@ -24,9 +24,8 @@
*/
package jdk.jpackage.internal.model;
import static java.util.stream.Collectors.toMap;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import jdk.jpackage.internal.resources.ResourceLocator;
@ -47,10 +46,20 @@ public interface WinLauncher extends Launcher, WinLauncherMixin {
@Override
default Map<String, String> extraAppImageFileData() {
return shortcuts().stream().collect(toMap(WinShortcut::name, v -> Boolean.toString(true)));
Map<String, String> map = new HashMap<>();
desktopShortcut().ifPresent(shortcut -> {
shortcut.store(SHORTCUT_DESKTOP_ID, map::put);
});
startMenuShortcut().ifPresent(shortcut -> {
shortcut.store(SHORTCUT_START_MENU_ID, map::put);
});
return map;
}
public static WinLauncher create(Launcher launcher, WinLauncherMixin mixin) {
return CompositeProxy.create(WinLauncher.class, launcher, mixin);
}
public static final String SHORTCUT_START_MENU_ID = "win-menu";
public static final String SHORTCUT_DESKTOP_ID = "win-shortcut";
}

View File

@ -24,30 +24,37 @@
*/
package jdk.jpackage.internal.model;
import java.util.Set;
import java.util.Optional;
public interface WinLauncherMixin {
boolean isConsole();
enum WinShortcut {
WIN_SHORTCUT_DESKTOP("shortcut"),
WIN_SHORTCUT_START_MENU("menu"),
;
/**
* Gets the start menu shortcut of this application launcher.
* <p>
* Returns a non-empty {@link Optional} instance if a request about the start
* menu shortcut for this application launcher was made and an empty
* {@link Optional} instance if there was no request about the start menu
* shortcut for this application launcher.
*
* @return the start menu shortcut of this application launcher
*/
Optional<LauncherShortcut> startMenuShortcut();
WinShortcut(String name) {
this.name = name;
}
/**
* Gets the desktop shortcut of this application launcher.
* <p>
* Returns a non-empty {@link Optional} instance if a request about the desktop
* shortcut for this application launcher was made and an empty {@link Optional}
* instance if there was no request about the desktop shortcut for this
* application launcher.
*
* @return the start menu shortcut of this application launcher
*/
Optional<LauncherShortcut> desktopShortcut();
public String getName() {
return name;
}
private final String name;
}
Set<WinShortcut> shortcuts();
record Stub(boolean isConsole, Set<WinShortcut> shortcuts) implements WinLauncherMixin {
record Stub(boolean isConsole, Optional<LauncherShortcut> startMenuShortcut,
Optional<LauncherShortcut> desktopShortcut) implements WinLauncherMixin {
}
}

View File

@ -92,7 +92,7 @@ public enum LauncherShortcut {
}
public String appImageFilePropertyName() {
return propertyName.substring(propertyName.indexOf('-') + 1);
return propertyName;
}
public String optionName() {

View File

@ -77,6 +77,11 @@ public final class LauncherVerifier {
VERIFY_UNINSTALLED((verifier, cmd) -> {
verifier.verifyInstalled(cmd, false);
}),
VERIFY_APP_IMAGE_FILE((verifier, cmd) -> {
if (cmd.isImagePackageType()) {
verifier.verifyInAppImageFile(cmd);
}
}),
EXECUTE_LAUNCHER(LauncherVerifier::executeLauncher),
;
@ -91,7 +96,7 @@ public final class LauncherVerifier {
private final BiConsumer<LauncherVerifier, JPackageCommand> action;
static final List<Action> VERIFY_APP_IMAGE = List.of(
VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED
VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED, VERIFY_APP_IMAGE_FILE
);
static final List<Action> VERIFY_DEFAULTS = Stream.concat(
@ -279,6 +284,45 @@ public final class LauncherVerifier {
}
}
private void verifyInAppImageFile(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.IMAGE);
if (!isMainLauncher()) {
Stream<LauncherShortcut> shortcuts;
if (TKit.isWindows()) {
shortcuts = Stream.of(LauncherShortcut.WIN_DESKTOP_SHORTCUT, LauncherShortcut.WIN_START_MENU_SHORTCUT);
} else if (TKit.isLinux()) {
shortcuts = Stream.of(LauncherShortcut.LINUX_SHORTCUT);
} else {
shortcuts = Stream.of();
}
var aif = AppImageFile.load(cmd.outputBundle());
var aifFileName = AppImageFile.getPathInAppImage(Path.of("")).getFileName();
var aifProps = Objects.requireNonNull(aif.addLaunchers().get(name));
shortcuts.forEach(shortcut -> {
var recordedShortcut = aifProps.get(shortcut.appImageFilePropertyName());
properties.flatMap(props -> {
return props.findProperty(shortcut.propertyName());
}).ifPresentOrElse(expectedShortcut -> {
TKit.assertNotNull(recordedShortcut, String.format(
"Check shortcut [%s] of launcher [%s] is recorded in %s file",
shortcut, name, aifFileName));
TKit.assertEquals(
StartupDirectory.parse(expectedShortcut),
StartupDirectory.parse(recordedShortcut),
String.format("Check the value of shortcut [%s] of launcher [%s] recorded in %s file",
shortcut, name, aifFileName));
}, () -> {
TKit.assertNull(recordedShortcut, String.format(
"Check shortcut [%s] of launcher [%s] is NOT recorded in %s file",
shortcut, name, aifFileName));
});
});
}
}
private void executeLauncher(JPackageCommand cmd) throws IOException {
Path launcherPath = cmd.appLauncherPath(name);