diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java index 790d5d877aa..dbaa5e3eec6 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java @@ -26,12 +26,10 @@ package jdk.jpackage.internal; 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; -import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -82,22 +80,12 @@ final class DesktopIntegration extends ShellCustomAction { // - user explicitly requested to create a shortcut boolean withDesktopFile = !associations.isEmpty() || toRequest(launcher.shortcut()).orElse(false); - var curIconResource = createLauncherIconResource(pkg.app(), launcher, - env::createResource); - - if (curIconResource.isEmpty()) { + if (!launcher.hasIcon()) { // This is additional launcher with explicit `no icon` configuration. withDesktopFile = false; - } else { - try { - if (curIconResource.get().saveToFile((Path)null) != OverridableResource.Source.DefaultResource) { - // This launcher has custom icon configured. - withDesktopFile = true; - } - } catch (IOException ex) { - // Should never happen as `saveToFile((Path)null)` should not perform any actual I/O operations. - throw new UncheckedIOException(ex); - } + } else if (launcher.hasCustomIcon()) { + // This launcher has custom icon configured. + withDesktopFile = true; } desktopFileResource = env.createResource("template.desktop") @@ -119,17 +107,12 @@ final class DesktopIntegration extends ShellCustomAction { if (withDesktopFile) { desktopFile = Optional.of(createDesktopFile(desktopFileName)); iconFile = Optional.of(createDesktopFile(escapedAppFileName + ".png")); - - if (curIconResource.isEmpty()) { - // Create default icon. - curIconResource = createLauncherIconResource(pkg.app(), pkg.app().mainLauncher().orElseThrow(), env::createResource); - } } else { desktopFile = Optional.empty(); iconFile = Optional.empty(); } - iconResource = curIconResource; + iconResource = createLauncherIconResource(launcher, env::createResource); desktopFileData = createDataForDesktopFile(); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java index 992ec630c0e..fe8d6bcf34f 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java @@ -25,13 +25,15 @@ package jdk.jpackage.internal; +import java.util.Optional; + public class LinuxAppBundler extends AppImageBundler { public LinuxAppBundler() { setAppImageSupplier((params, output) -> { // Order is important! var app = LinuxFromParams.APPLICATION.fetchFrom(params); var env = BuildEnvFromParams.BUILD_ENV.fetchFrom(params); - LinuxPackagingPipeline.build() + LinuxPackagingPipeline.build(Optional.empty()) .excludeDirFromCopying(output.getParent()) .create().execute(BuildEnv.withAppImageDir(env, output), app); }); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java index 9e2ea63cc32..76a08519b48 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java @@ -27,6 +27,7 @@ package jdk.jpackage.internal; import java.nio.file.Path; import java.util.Map; +import java.util.Optional; import jdk.jpackage.internal.model.LinuxDebPackage; import jdk.jpackage.internal.model.PackagerException; import jdk.jpackage.internal.model.StandardPackageType; @@ -51,12 +52,14 @@ public class LinuxDebBundler extends LinuxPackageBundler { @Override public Path execute(Map params, Path outputParentDir) throws PackagerException { + var pkg = LinuxFromParams.DEB_PACKAGE.fetchFrom(params); + return Packager.build().outputDir(outputParentDir) - .pkg(LinuxFromParams.DEB_PACKAGE.fetchFrom(params)) + .pkg(pkg) .env(BuildEnvFromParams.BUILD_ENV.fetchFrom(params)) - .pipelineBuilderMutatorFactory((env, pkg, outputDir) -> { + .pipelineBuilderMutatorFactory((env, _, outputDir) -> { return new LinuxDebPackager(env, pkg, outputDir, sysEnv.orElseThrow()); - }).execute(LinuxPackagingPipeline.build()); + }).execute(LinuxPackagingPipeline.build(Optional.of(pkg))); } @Override diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java index a8d109220dc..e9d1416b5c3 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java @@ -43,6 +43,7 @@ import jdk.jpackage.internal.model.LinuxDebPackage; import jdk.jpackage.internal.model.LinuxLauncher; import jdk.jpackage.internal.model.LinuxLauncherMixin; import jdk.jpackage.internal.model.LinuxRpmPackage; +import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.model.StandardPackageType; final class LinuxFromParams { @@ -55,7 +56,9 @@ final class LinuxFromParams { final var launcher = launcherFromParams.create(launcherParams); final var shortcut = findLauncherShortcut(LINUX_SHORTCUT_HINT, params, launcherParams); return LinuxLauncher.create(launcher, new LinuxLauncherMixin.Stub(shortcut)); - }), APPLICATION_LAYOUT).create(); + }), (LinuxLauncher linuxLauncher, Launcher launcher) -> { + return LinuxLauncher.create(launcher, linuxLauncher); + }, APPLICATION_LAYOUT).create(); return LinuxApplication.create(app); } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackagingPipeline.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackagingPipeline.java index dd29338655d..6f6013b3091 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackagingPipeline.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackagingPipeline.java @@ -25,17 +25,20 @@ package jdk.jpackage.internal; import static jdk.jpackage.internal.ApplicationImageUtils.createLauncherIconResource; -import jdk.jpackage.internal.PackagingPipeline.AppImageBuildEnv; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; +import jdk.jpackage.internal.PackagingPipeline.AppImageBuildEnv; import jdk.jpackage.internal.PackagingPipeline.BuildApplicationTaskID; import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID; import jdk.jpackage.internal.PackagingPipeline.TaskID; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLayout; +import jdk.jpackage.internal.model.Launcher; +import jdk.jpackage.internal.model.LinuxPackage; import jdk.jpackage.internal.resources.ResourceLocator; final class LinuxPackagingPipeline { @@ -45,14 +48,20 @@ final class LinuxPackagingPipeline { LAUNCHER_ICONS } - static PackagingPipeline.Builder build() { - return PackagingPipeline.buildStandard() + static PackagingPipeline.Builder build(Optional pkg) { + var builder = PackagingPipeline.buildStandard() .task(LinuxAppImageTaskID.LAUNCHER_LIB) .addDependent(PrimaryTaskID.BUILD_APPLICATION_IMAGE) .applicationAction(LinuxPackagingPipeline::writeLauncherLib).add() .task(LinuxAppImageTaskID.LAUNCHER_ICONS) .addDependent(BuildApplicationTaskID.CONTENT) .applicationAction(LinuxPackagingPipeline::writeLauncherIcons).add(); + + pkg.ifPresent(_ -> { + builder.task(LinuxAppImageTaskID.LAUNCHER_ICONS).noaction().add(); + }); + + return builder; } private static void writeLauncherLib( @@ -68,8 +77,8 @@ final class LinuxPackagingPipeline { private static void writeLauncherIcons( AppImageBuildEnv env) throws IOException { - for (var launcher : env.app().launchers()) { - createLauncherIconResource(env.app(), launcher, env.env()::createResource).ifPresent(iconResource -> { + env.app().launchers().stream().filter(Launcher::hasCustomIcon).forEach(launcher -> { + createLauncherIconResource(launcher, env.env()::createResource).ifPresent(iconResource -> { String iconFileName = launcher.executableName() + ".png"; Path iconTarget = env.resolvedLayout().desktopIntegrationDirectory().resolve(iconFileName); try { @@ -78,7 +87,7 @@ final class LinuxPackagingPipeline { throw new UncheckedIOException(ex); } }); - } + }); } static final LinuxApplicationLayout APPLICATION_LAYOUT = LinuxApplicationLayout.create( diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java index 2337a286011..c134aa91d6a 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java @@ -27,6 +27,7 @@ package jdk.jpackage.internal; import java.nio.file.Path; import java.util.Map; +import java.util.Optional; import jdk.jpackage.internal.model.LinuxRpmPackage; import jdk.jpackage.internal.model.PackagerException; import jdk.jpackage.internal.model.StandardPackageType; @@ -52,12 +53,14 @@ public class LinuxRpmBundler extends LinuxPackageBundler { @Override public Path execute(Map params, Path outputParentDir) throws PackagerException { + var pkg = LinuxFromParams.RPM_PACKAGE.fetchFrom(params); + return Packager.build().outputDir(outputParentDir) - .pkg(LinuxFromParams.RPM_PACKAGE.fetchFrom(params)) + .pkg(pkg) .env(BuildEnvFromParams.BUILD_ENV.fetchFrom(params)) - .pipelineBuilderMutatorFactory((env, pkg, outputDir) -> { + .pipelineBuilderMutatorFactory((env, _, outputDir) -> { return new LinuxRpmPackager(env, pkg, outputDir, sysEnv.orElseThrow()); - }).execute(LinuxPackagingPipeline.build()); + }).execute(LinuxPackagingPipeline.build(Optional.of(pkg))); } @Override diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java index e2d8750e39c..72c33ef6475 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java @@ -92,7 +92,9 @@ final class MacFromParams { final var superAppBuilder = createApplicationBuilder(params, toFunction(launcherParams -> { var launcher = launcherFromParams.create(launcherParams); return MacLauncher.create(launcher); - }), APPLICATION_LAYOUT, RUNTIME_BUNDLE_LAYOUT, predefinedRuntimeLayout.map(RuntimeLayout::unresolve)); + }), (MacLauncher _, Launcher launcher) -> { + return MacLauncher.create(launcher); + }, APPLICATION_LAYOUT, RUNTIME_BUNDLE_LAYOUT, predefinedRuntimeLayout.map(RuntimeLayout::unresolve)); if (hasPredefinedAppImage(params)) { // Set the main launcher start up info. diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java index 9b5ed5b3b08..76a5fc1a50c 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java @@ -32,7 +32,9 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Predicate; import jdk.jpackage.internal.model.AppImageLayout; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLaunchers; @@ -40,7 +42,9 @@ 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.LauncherIcon; import jdk.jpackage.internal.model.LauncherStartupInfo; +import jdk.jpackage.internal.model.ResourceDirLauncherIcon; import jdk.jpackage.internal.model.RuntimeBuilder; final class ApplicationBuilder { @@ -168,6 +172,97 @@ final class ApplicationBuilder { return this; } + static ApplicationLaunchers normalizeIcons( + ApplicationLaunchers appLaunchers, Optional resourceDir, BiFunction launcherOverrideCtor) { + + Objects.requireNonNull(resourceDir); + + return normalizeLauncherProperty(appLaunchers, Launcher::hasDefaultIcon, (T launcher) -> { + return resourceDir.flatMap(dir -> { + var resource = LauncherBuilder.createLauncherIconResource(launcher, _ -> { + return new OverridableResource() + .setResourceDir(dir) + .setSourceOrder(OverridableResource.Source.ResourceDir); + }); + if (resource.probe() == OverridableResource.Source.ResourceDir) { + return Optional.of(ResourceDirLauncherIcon.create(resource.getPublicName().toString())); + } else { + return Optional.empty(); + } + }); + }, launcher -> { + return launcher.icon().orElseThrow(); + }, (launcher, icon) -> { + return launcherOverrideCtor.apply(launcher, overrideIcon(launcher, icon)); + }); + } + + static ApplicationLaunchers normalizeLauncherProperty( + ApplicationLaunchers appLaunchers, + Predicate needsNormalization, + Function> normalizedPropertyValueFinder, + BiFunction propertyOverrider) { + + return normalizeLauncherProperty( + appLaunchers, + needsNormalization, + normalizedPropertyValueFinder, + launcher -> { + return normalizedPropertyValueFinder.apply(launcher).orElseThrow(); + }, + propertyOverrider); + } + + static ApplicationLaunchers normalizeLauncherProperty( + ApplicationLaunchers appLaunchers, + Predicate needsNormalization, + Function> normalizedPropertyValueFinder, + Function normalizedPropertyValueGetter, + BiFunction propertyOverrider) { + + Objects.requireNonNull(appLaunchers); + Objects.requireNonNull(needsNormalization); + Objects.requireNonNull(normalizedPropertyValueFinder); + Objects.requireNonNull(normalizedPropertyValueGetter); + Objects.requireNonNull(propertyOverrider); + + boolean[] modified = new boolean[1]; + + @SuppressWarnings("unchecked") + var newLaunchers = appLaunchers.asList().stream().map(launcher -> { + return (U)launcher; + }).map(launcher -> { + if (needsNormalization.test(launcher)) { + return normalizedPropertyValueFinder.apply(launcher).map(normalizedPropertyValue -> { + modified[0] = true; + return propertyOverrider.apply(launcher, normalizedPropertyValue); + }).orElse(launcher); + } else { + return launcher; + } + }).toList(); + + var newMainLauncher = newLaunchers.getFirst(); + if (!needsNormalization.test(newMainLauncher)) { + // The main launcher doesn't require normalization. + newLaunchers = newLaunchers.stream().map(launcher -> { + if (needsNormalization.test(launcher)) { + var normalizedPropertyValue = normalizedPropertyValueGetter.apply(newMainLauncher); + modified[0] = true; + return propertyOverrider.apply(launcher, normalizedPropertyValue); + } else { + return launcher; + } + }).toList(); + } + + if (modified[0]) { + return ApplicationLaunchers.fromList(newLaunchers).orElseThrow(); + } else { + return appLaunchers; + } + } + static Launcher overrideLauncherStartupInfo(Launcher launcher, LauncherStartupInfo startupInfo) { return new Launcher.Stub( launcher.name(), @@ -195,6 +290,18 @@ final class ApplicationBuilder { app.extraAppImageFileData()); } + private static Launcher overrideIcon(Launcher launcher, LauncherIcon icon) { + return new Launcher.Stub( + launcher.name(), + launcher.startupInfo(), + launcher.fileAssociations(), + launcher.isService(), + launcher.description(), + Optional.of(icon), + launcher.defaultIconResourceName(), + launcher.extraAppImageFileData()); + } + record MainLauncherStartupInfo(String qualifiedClassName) implements LauncherStartupInfo { @Override public List javaOptions() { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationImageUtils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationImageUtils.java index cda5b6c79ef..3d2ffbfdc7c 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationImageUtils.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationImageUtils.java @@ -26,7 +26,6 @@ package jdk.jpackage.internal; import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; -import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; import java.io.IOException; import java.nio.file.Files; @@ -43,44 +42,36 @@ import jdk.jpackage.internal.PackagingPipeline.ApplicationImageTaskAction; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLayout; import jdk.jpackage.internal.model.CustomLauncherIcon; +import jdk.jpackage.internal.model.DefaultLauncherIcon; import jdk.jpackage.internal.model.Launcher; +import jdk.jpackage.internal.model.ResourceDirLauncherIcon; import jdk.jpackage.internal.util.FileUtils; -import jdk.jpackage.internal.util.PathUtils; final class ApplicationImageUtils { - static Optional createLauncherIconResource(Application app, - Launcher launcher, + static Optional createLauncherIconResource(Launcher launcher, Function resourceSupplier) { - final String defaultIconName = launcher.defaultIconResourceName(); - final String resourcePublicName = launcher.executableName() + PathUtils.getSuffix(Path.of(defaultIconName)); - if (!launcher.hasIcon()) { - return Optional.empty(); - } + return launcher.icon().map(icon -> { + var resource = LauncherBuilder.createLauncherIconResource(launcher, resourceSupplier); - OverridableResource resource = resourceSupplier.apply(defaultIconName) - .setCategory("icon") - .setPublicName(resourcePublicName); - - launcher.icon().flatMap(CustomLauncherIcon::fromLauncherIcon).map(CustomLauncherIcon::path).ifPresent(resource::setExternal); - - if (launcher.hasDefaultIcon() && app.mainLauncher().orElseThrow() != launcher) { - // No icon explicitly configured for this launcher. - // Dry-run resource creation to figure out its source. - final Path nullPath = null; - if (toSupplier(() -> resource.saveToFile(nullPath)).get() != OverridableResource.Source.ResourceDir) { - // No icon in resource dir for this launcher, inherit icon - // configured for the main launcher. - return createLauncherIconResource( - app, app.mainLauncher().orElseThrow(), - resourceSupplier - ).map(r -> r.setLogPublicName(resourcePublicName)); + switch (icon) { + case DefaultLauncherIcon _ -> { + resource.setSourceOrder(OverridableResource.Source.DefaultResource); + } + case ResourceDirLauncherIcon v -> { + resource.setSourceOrder(OverridableResource.Source.ResourceDir); + resource.setPublicName(v.name()); + } + case CustomLauncherIcon v -> { + resource.setSourceOrder(OverridableResource.Source.External); + resource.setExternal(v.path()); + } } - } - return Optional.of(resource); + return resource; + }); } static ApplicationImageTaskAction createWriteRuntimeAction() { 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 8cfab61c9c0..df9cc528439 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java @@ -47,6 +47,7 @@ import static jdk.jpackage.internal.StandardBundlerParam.NAME; import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE; import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE_FILE; import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE; +import static jdk.jpackage.internal.StandardBundlerParam.RESOURCE_DIR; import static jdk.jpackage.internal.StandardBundlerParam.SOURCE_DIR; import static jdk.jpackage.internal.StandardBundlerParam.VENDOR; import static jdk.jpackage.internal.StandardBundlerParam.VERSION; @@ -60,6 +61,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Function; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLaunchers; @@ -76,14 +78,16 @@ import jdk.jpackage.internal.util.function.ThrowingFunction; final class FromParams { - static ApplicationBuilder createApplicationBuilder(Map params, + static ApplicationBuilder createApplicationBuilder(Map params, Function, Launcher> launcherMapper, + BiFunction launcherOverrideCtor, ApplicationLayout appLayout) throws ConfigException, IOException { - return createApplicationBuilder(params, launcherMapper, appLayout, RuntimeLayout.DEFAULT, Optional.of(RuntimeLayout.DEFAULT)); + return createApplicationBuilder(params, launcherMapper, launcherOverrideCtor, appLayout, RuntimeLayout.DEFAULT, Optional.of(RuntimeLayout.DEFAULT)); } - static ApplicationBuilder createApplicationBuilder(Map params, + static ApplicationBuilder createApplicationBuilder(Map params, Function, Launcher> launcherMapper, + BiFunction launcherOverrideCtor, ApplicationLayout appLayout, RuntimeLayout runtimeLayout, Optional predefinedRuntimeLayout) throws ConfigException, IOException { @@ -133,7 +137,9 @@ final class FromParams { jlinkOptionsBuilder.apply(); }); - appBuilder.launchers(launchers).runtimeBuilder(runtimeBuilderBuilder.create()); + final var normalizedLaunchers = ApplicationBuilder.normalizeIcons(launchers, RESOURCE_DIR.findIn(params), launcherOverrideCtor); + + appBuilder.launchers(normalizedLaunchers).runtimeBuilder(runtimeBuilderBuilder.create()); } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherBuilder.java index 5ce98165a4a..0f6b5d6ac8d 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherBuilder.java @@ -32,14 +32,18 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; import jdk.internal.util.OperatingSystem; import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.CustomLauncherIcon; +import jdk.jpackage.internal.model.DefaultLauncherIcon; import jdk.jpackage.internal.model.FileAssociation; import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.model.Launcher.Stub; +import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.model.LauncherIcon; import jdk.jpackage.internal.model.LauncherStartupInfo; +import jdk.jpackage.internal.model.ResourceDirLauncherIcon; final class LauncherBuilder { @@ -110,6 +114,15 @@ final class LauncherBuilder { return Optional.ofNullable(name).orElseGet(() -> startupInfo.simpleClassName()); } + static OverridableResource createLauncherIconResource(Launcher launcher, + Function resourceSupplier) { + + var defaultIconResourceName = launcher.defaultIconResourceName(); + return resourceSupplier.apply(defaultIconResourceName) + .setPublicName(launcher.executableName() + PathUtils.getSuffix(Path.of(defaultIconResourceName))) + .setCategory("icon"); + } + static void validateIcon(Path icon) throws ConfigException { switch (OperatingSystem.current()) { case WINDOWS -> { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/CustomLauncherIcon.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/CustomLauncherIcon.java index 13216535680..5819c42fcda 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/CustomLauncherIcon.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/CustomLauncherIcon.java @@ -29,11 +29,11 @@ import java.util.Objects; import java.util.Optional; /** - * Custom application launcher icon. + * Custom application launcher icon sourced from an external file. *

* Use {@link #create(Path)} method to create an instance of this type. */ -public interface CustomLauncherIcon extends LauncherIcon { +public sealed interface CustomLauncherIcon extends LauncherIcon { /** * Returns path to icon file. diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/DefaultLauncherIcon.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/DefaultLauncherIcon.java index 4275b84ba80..51847349458 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/DefaultLauncherIcon.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/DefaultLauncherIcon.java @@ -29,11 +29,11 @@ import java.util.Optional; /** * Default application launcher icon. *

- * Default icon is either loaded from the resources of {@link jdk.jpackage} module or picked from the resource directory. + * Default icon is loaded from the resources of {@link jdk.jpackage} module. *

* Use {@link #INSTANCE} field to get an instance of this type. */ -public interface DefaultLauncherIcon extends LauncherIcon { +public sealed interface DefaultLauncherIcon extends LauncherIcon { /** * Returns the given icon as {@link DefaultLauncherIcon} type or an empty {@link Optional} instance @@ -53,5 +53,9 @@ public interface DefaultLauncherIcon extends LauncherIcon { /** * Singleton. */ - public static DefaultLauncherIcon INSTANCE = new DefaultLauncherIcon() {}; + public static DefaultLauncherIcon INSTANCE = new Details.Impl(); + + static final class Details { + private static final class Impl implements DefaultLauncherIcon {} + } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Launcher.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Launcher.java index ac60b503fe4..4c729072839 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Launcher.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Launcher.java @@ -151,16 +151,18 @@ public interface Launcher { } /** - * Returns true if this launcher has a custom icon. + * Returns true if this launcher has non-default icon. + *

+ * A custom icon can be sourced from an external file or from the resource directory. * - * @return true if this launcher has a custom icon + * @return true if this launcher has non-default icon * @see CustomLauncherIcon * @see #icon() * @see #hasDefaultIcon() * @see #hasIcon() */ default boolean hasCustomIcon() { - return icon().flatMap(CustomLauncherIcon::fromLauncherIcon).isPresent(); + return !hasDefaultIcon() && icon().isPresent(); } /** diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherIcon.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherIcon.java index bd7a9955b90..dee20013c31 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherIcon.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherIcon.java @@ -27,5 +27,5 @@ package jdk.jpackage.internal.model; /** * Application launcher icon. */ -public interface LauncherIcon { +public sealed interface LauncherIcon permits DefaultLauncherIcon, ResourceDirLauncherIcon, CustomLauncherIcon { } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ResourceDirLauncherIcon.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ResourceDirLauncherIcon.java new file mode 100644 index 00000000000..835fcb4b6bd --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ResourceDirLauncherIcon.java @@ -0,0 +1,73 @@ +/* + * 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; + +/** + * Custom application launcher icon sourced from the resource directory. + *

+ * Use {@link #create(String)} method to create an instance of this type. + */ +public sealed interface ResourceDirLauncherIcon extends LauncherIcon { + + /** + * Returns name of the resource referencing an icon file in the resource directory. + * @return name of the resource referencing an icon file in the resource directory + */ + String name(); + + /** + * Returns the given icon as {@link ResourceDirLauncherIcon} type or an empty {@link Optional} instance + * if the given icon object is not an instance of {@link ResourceDirLauncherIcon} type. + * + * @param icon application launcher icon object or null + * @return the given icon as {@link ResourceDirLauncherIcon} type or an empty {@link Optional} instance + */ + public static Optional fromLauncherIcon(LauncherIcon icon) { + if (icon instanceof ResourceDirLauncherIcon customIcon) { + return Optional.of(customIcon); + } else { + return Optional.empty(); + } + } + + /** + * Creates object of type {@link ResourceDirLauncherIcon} from the name of the resource referencing an icon file in the resource directory. + * @param name name of the resource referencing an icon file in the resource directory + * @return {@link ResourceDirLauncherIcon} instance + */ + public static ResourceDirLauncherIcon create(String name) { + Objects.requireNonNull(name); + return new Stub(name); + } + + /** + * Default implementation of {@link ResourceDirLauncherIcon} type. + */ + record Stub(String name) implements ResourceDirLauncherIcon { + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/CompositeProxy.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/CompositeProxy.java index 4fe5fe9039e..39a9d319468 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/CompositeProxy.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/CompositeProxy.java @@ -25,14 +25,17 @@ package jdk.jpackage.internal.util; import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -291,26 +294,119 @@ public final class CompositeProxy { return proxy; } - private static Map, Object> createInterfaceDispatch(Class[] interfaces, Object[] slices) { + private record InterfaceDispatchBuilder(Set> interfaces, Collection slices) { - final Map, Object> interfaceDispatch = Stream.of(interfaces).collect(toMap(x -> x, iface -> { - return Stream.of(slices).filter(obj -> { - return Set.of(obj.getClass().getInterfaces()).contains(iface); - }).reduce((a, b) -> { - throw new IllegalArgumentException( - String.format("both [%s] and [%s] slices implement %s", a, b, iface)); - }).orElseThrow(() -> createInterfaceNotImplementedException(List.of(iface))); - })); + InterfaceDispatchBuilder { + Objects.requireNonNull(interfaces); + Objects.requireNonNull(slices); - if (interfaceDispatch.size() != interfaces.length) { - final List> missingInterfaces = new ArrayList<>(Set.of(interfaces)); - missingInterfaces.removeAll(interfaceDispatch.keySet()); - throw createInterfaceNotImplementedException(missingInterfaces); + if (interfaces.isEmpty()) { + throw new IllegalArgumentException("No interfaces to dispatch"); + } + + if (slices.isEmpty()) { + throw createInterfaceNotImplementedException(interfaces); + } } - return Stream.of(interfaces).flatMap(iface -> { + InterfaceDispatchBuilder(Result result) { + this(result.unservedInterfaces(), result.unusedSlices()); + } + + Map, List> createDispatchGroups() { + return interfaces.stream().collect(toMap(x -> x, iface -> { + return slices.stream().filter(obj -> { + return Stream.of(obj.getClass().getInterfaces()).flatMap(sliceIface -> { + return unfoldInterface(sliceIface); + }).anyMatch(Predicate.isEqual(iface)); + }).toList(); + })); + } + + Result createDispatch() { + var groups = createDispatchGroups(); + + var dispatch = groups.entrySet().stream().filter(e -> { + return e.getValue().size() == 1; + }).collect(toMap(Map.Entry::getKey, e -> { + return e.getValue().getFirst(); + })); + + var unservedInterfaces = groups.entrySet().stream().filter(e -> { + return e.getValue().size() != 1; + }).map(Map.Entry::getKey).collect(toSet()); + + var usedSliceIdentities = dispatch.values().stream() + .map(IdentityWrapper::new) + .collect(toSet()); + + var unusedSliceIdentities = new HashSet<>(toIdentitySet(slices)); + unusedSliceIdentities.removeAll(usedSliceIdentities); + + return new Result(dispatch, unservedInterfaces, unusedSliceIdentities.stream().map(IdentityWrapper::value).toList()); + } + + private record Result(Map, Object> dispatch, Set> unservedInterfaces, Collection unusedSlices) { + + Result { + Objects.requireNonNull(dispatch); + Objects.requireNonNull(unservedInterfaces); + Objects.requireNonNull(unusedSlices); + + if (!Collections.disjoint(dispatch.keySet(), unservedInterfaces)) { + throw new IllegalArgumentException(); + } + + if (!Collections.disjoint(toIdentitySet(dispatch.values()), toIdentitySet(unusedSlices))) { + throw new IllegalArgumentException(); + } + } + } + + private static Collection> toIdentitySet(Collection v) { + return v.stream().map(IdentityWrapper::new).collect(toSet()); + } + } + + private static Map, Object> createInterfaceDispatch(Class[] interfaces, Object[] slices) { + + if (interfaces.length == 0) { + return Collections.emptyMap(); + } + + Map, Object> dispatch = new HashMap<>(); + + var builder = new InterfaceDispatchBuilder(Set.of(interfaces), List.of(slices)); + for (;;) { + var result = builder.createDispatch(); + if (result.dispatch().isEmpty()) { + var unserved = builder.createDispatchGroups(); + for (var e : unserved.entrySet()) { + var iface = e.getKey(); + var ifaceSlices = e.getValue(); + if (ifaceSlices.size() > 1) { + throw new IllegalArgumentException( + String.format("multiple slices %s implement %s", ifaceSlices, iface)); + } + } + + var unservedInterfaces = unserved.entrySet().stream().filter(e -> { + return e.getValue().isEmpty(); + }).map(Map.Entry::getKey).toList(); + throw createInterfaceNotImplementedException(unservedInterfaces); + } else { + dispatch.putAll(result.dispatch()); + if (result.unservedInterfaces().isEmpty()) { + break; + } + } + + builder = new InterfaceDispatchBuilder(result); + } + + return dispatch.keySet().stream().flatMap(iface -> { return unfoldInterface(iface).map(unfoldedIface -> { - return Map.entry(unfoldedIface, interfaceDispatch.get(iface)); + return Map.entry(unfoldedIface, dispatch.get(iface)); }); }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); } @@ -321,7 +417,7 @@ public final class CompositeProxy { } private static IllegalArgumentException createInterfaceNotImplementedException( - Collection> missingInterfaces) { + Collection> missingInterfaces) { return new IllegalArgumentException(String.format("none of the slices implement %s", missingInterfaces)); } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java index 29c1b665f86..2d4225f5a5b 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java @@ -41,6 +41,7 @@ import java.io.IOException; import java.util.Map; import java.util.UUID; import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.model.WinApplication; import jdk.jpackage.internal.model.WinExePackage; import jdk.jpackage.internal.model.WinLauncher; @@ -66,7 +67,9 @@ final class WinFromParams { return WinLauncher.create(launcher, new WinLauncherMixin.Stub(isConsole, startMenuShortcut, desktopShortcut)); - }), APPLICATION_LAYOUT).create(); + }), (WinLauncher winLauncher, Launcher launcher) -> { + return WinLauncher.create(launcher, winLauncher); + }, APPLICATION_LAYOUT).create(); return WinApplication.create(app); } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinPackagingPipeline.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinPackagingPipeline.java index 2fa7dd895c3..f359a61ce7b 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinPackagingPipeline.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinPackagingPipeline.java @@ -56,7 +56,7 @@ final class WinPackagingPipeline { private static void rebrandLaunchers(AppImageBuildEnv env) throws IOException, PackagerException { for (var launcher : env.app().launchers()) { - final var iconTarget = createLauncherIconResource(env.app(), launcher, env.env()::createResource).map(iconResource -> { + final var iconTarget = createLauncherIconResource(launcher, env.env()::createResource).map(iconResource -> { var iconDir = env.env().buildRoot().resolve("icons"); var theIconTarget = iconDir.resolve(launcher.executableName() + ".ico"); try { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java index 576e294874a..eb9cb4b0cb6 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java @@ -32,7 +32,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.TreeMap; -import jdk.jpackage.internal.util.PathUtils; public final class FileAssociations { @@ -75,13 +74,6 @@ public final class FileAssociations { return this; } - Path getLinuxIconFileName() { - if (icon == null) { - return null; - } - return Path.of(getMime().replace('/', '-') + PathUtils.getSuffix(icon)); - } - Path getPropertiesFile() { return file; } @@ -94,6 +86,10 @@ public final class FileAssociations { return "application/x-jpackage-" + suffixName; } + boolean hasIcon() { + return icon != null; + } + public void applyTo(PackageTest test) { test.notForTypes(PackageType.MAC_DMG, () -> { test.addInitializer(cmd -> { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java index 79652a9828e..9d0c97160f7 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java @@ -66,18 +66,11 @@ public final class LauncherIconVerifier { } public void applyTo(JPackageCommand cmd) throws IOException { - final String curLauncherName; - final String label; - if (launcherName == null) { - curLauncherName = cmd.name(); - label = "main"; - } else { - curLauncherName = launcherName; - label = String.format("[%s]", launcherName); - } + final String label = Optional.ofNullable(launcherName).map(v -> { + return String.format("[%s]", v); + }).orElse("main"); - Path iconPath = cmd.appLayout().desktopIntegrationDirectory().resolve( - curLauncherName + TKit.ICON_SUFFIX); + Path iconPath = cmd.appLayout().desktopIntegrationDirectory().resolve(iconFileName(cmd)); if (TKit.isWindows()) { TKit.assertPathExists(iconPath, false); @@ -99,6 +92,14 @@ public final class LauncherIconVerifier { } } + private Path iconFileName(JPackageCommand cmd) { + if (TKit.isLinux()) { + return LinuxHelper.getLauncherIconFileName(cmd, launcherName); + } else { + return Path.of(Optional.ofNullable(launcherName).orElseGet(cmd::name) + TKit.ICON_SUFFIX); + } + } + private String launcherName; private Path expectedIcon; private boolean expectedDefault; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java index 55cb38f21cf..489788b565a 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java @@ -204,7 +204,7 @@ public final class LauncherVerifier { verifier.setExpectedIcon(icon); } }, () -> { - // No "icon" property in the property file + // No "icon" property in the property file. iconInResourceDir(cmd, name).ifPresentOrElse(verifier::setExpectedIcon, () -> { // No icon for this additional launcher in the resource directory. mainLauncherIcon.ifPresentOrElse(verifier::setExpectedIcon, verifier::setExpectedDefaultIcon); @@ -212,6 +212,29 @@ public final class LauncherVerifier { }); } + if (TKit.isLinux()) { + // On Linux, a launcher may have an icon only if it has a corresponding .desktop file. + // In case of "app-image" packaging there are no .desktop files, but jpackage will add icon files + // in the app image anyways so that in two-step packaging jpackage can pick the icons for .desktop files. + // jpackage should not add the default icon to the app image in case of "app-image" packaging. + if (cmd.isImagePackageType()) { + // This is "app-image" packaging. Let's see if, in two-step packaging, + // jpackage creates a .desktop file for this launcher. + if (!withLinuxDesktopFile(cmd.createMutableCopy().setPackageType(PackageType.LINUX_RPM))) { + // No .desktop file in the "future" package for this launcher, + // then don't expect an icon in the app image produced by the `cmd`. + verifier.setExpectedNoIcon(); + } else if (verifier.expectDefaultIcon()) { + // A .desktop file in the "future" package for this launcher, + // but it will use the default icon. + // Don't expect an icon in the app image produced by the `cmd`. + verifier.setExpectedNoIcon(); + } + } else if (!withLinuxDesktopFile(cmd)) { + verifier.setExpectedNoIcon(); + } + } + return verifier; } 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 9776ab5c4c8..4bc8295eaf8 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -22,11 +22,11 @@ */ package jdk.jpackage.test; -import static jdk.jpackage.test.AdditionalLauncher.getAdditionalLauncherProperties; import static java.util.Collections.unmodifiableSortedSet; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet; +import static jdk.jpackage.test.AdditionalLauncher.getAdditionalLauncherProperties; import java.io.IOException; import java.io.UncheckedIOException; @@ -45,6 +45,7 @@ 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; @@ -90,9 +91,7 @@ public final class LinuxHelper { public static Path getDesktopFile(JPackageCommand cmd, String launcherName) { cmd.verifyIsOfType(PackageType.LINUX); - String desktopFileName = String.format("%s-%s.desktop", getPackageName( - cmd), Optional.ofNullable(launcherName).orElseGet( - () -> cmd.name()).replaceAll("\\s+", "_")); + var desktopFileName = getLauncherDesktopFileName(cmd, launcherName); return cmd.appLayout().desktopIntegrationDirectory().resolve( desktopFileName); } @@ -204,6 +203,20 @@ public final class LinuxHelper { } } + private static Path getFaIconFileName(JPackageCommand cmd, String mimeType) { + return Path.of(mimeType.replace('/', '-') + ".png"); + } + + static Path getLauncherDesktopFileName(JPackageCommand cmd, String launcherName) { + return Path.of(String.format("%s-%s.desktop", getPackageName(cmd), + Optional.ofNullable(launcherName).orElseGet(cmd::name).replaceAll("\\s+", "_"))); + } + + static Path getLauncherIconFileName(JPackageCommand cmd, String launcherName) { + return Path.of(String.format("%s.png", + Optional.ofNullable(launcherName).orElseGet(cmd::name).replaceAll("\\s+", "_"))); + } + static PackageHandlers createDebPackageHandlers() { return new PackageHandlers(LinuxHelper::installDeb, LinuxHelper::uninstallDeb, LinuxHelper::unpackDeb); } @@ -423,7 +436,12 @@ public final class LinuxHelper { }); } - static void verifyDesktopFiles(JPackageCommand cmd, boolean installed) { + static void verifyDesktopIntegrationFiles(JPackageCommand cmd, boolean installed) { + verifyDesktopFiles(cmd, installed); + verifyAllIconsReferenced(cmd); + } + + private static void verifyDesktopFiles(JPackageCommand cmd, boolean installed) { final var desktopFiles = getDesktopFiles(cmd); try { if (installed) { @@ -479,6 +497,39 @@ public final class LinuxHelper { }).map(packageDir::relativize); } + private static void verifyAllIconsReferenced(JPackageCommand cmd) { + + var installCmd = Optional.ofNullable(cmd.unpackedPackageDirectory()).map(_ -> { + return cmd.createMutableCopy().setUnpackedPackageLocation(null); + }).orElse(cmd); + + var installedIconFiles = relativePackageFilesInSubdirectory( + installCmd, + ApplicationLayout::desktopIntegrationDirectory + ).filter(path -> { + return ".png".equals(PathUtils.getSuffix(path)); + }).map(installCmd.appLayout().desktopIntegrationDirectory()::resolve).collect(toSet()); + + var referencedIcons = getDesktopFiles(cmd).stream().map(path -> { + return new DesktopFile(path, false); + }).mapMulti((desktopFile, sink) -> { + desktopFile.findQuotedValue("Icon").map(Path::of).ifPresent(sink); + desktopFile.find("MimeType").ifPresent(str -> { + Stream.of(str.split(";")) + .map(mimeType -> { + return getFaIconFileName(cmd, mimeType); + }) + .map(installCmd.appLayout().desktopIntegrationDirectory()::resolve) + .forEach(sink); + }); + }).collect(toSet()); + + var unreferencedIconFiles = Comm.compare(installedIconFiles, referencedIcons).unique1().stream().sorted().toList(); + + // Verify that all package icon (.png) files are referenced from package .desktop files. + 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) { Objects.requireNonNull(cmd); Objects.requireNonNull(predefinedAppImage); @@ -661,16 +712,19 @@ public final class LinuxHelper { }); test.addBundleVerifier(cmd -> { - final Path mimeTypeIconFileName = fa.getLinuxIconFileName(); - if (mimeTypeIconFileName != null) { - // Verify there are xdg registration commands for mime icon file. - Path mimeTypeIcon = cmd.appLayout().desktopIntegrationDirectory().resolve( - mimeTypeIconFileName); + Optional.of(fa).filter(FileAssociations::hasIcon) + .map(FileAssociations::getMime) + .map(mimeType -> { + return getFaIconFileName(cmd, mimeType); + }).ifPresent(mimeTypeIconFileName -> { + // Verify there are xdg registration commands for mime icon file. + Path mimeTypeIcon = cmd.appLayout().desktopIntegrationDirectory().resolve( + mimeTypeIconFileName); - Map> scriptlets = getScriptlets(cmd); - scriptlets.entrySet().stream().forEach(e -> verifyIconInScriptlet( - e.getKey(), e.getValue(), mimeTypeIcon)); - } + Map> scriptlets = getScriptlets(cmd); + scriptlets.entrySet().stream().forEach(e -> verifyIconInScriptlet( + e.getKey(), e.getValue(), mimeTypeIcon)); + }); }); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index 3226811fe36..2e73f43b58d 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -784,7 +784,7 @@ public final class PackageTest extends RunnablePackageTest { } if (isOfType(cmd, LINUX)) { - LinuxHelper.verifyDesktopFiles(cmd, true); + LinuxHelper.verifyDesktopIntegrationFiles(cmd, true); } } @@ -865,7 +865,7 @@ public final class PackageTest extends RunnablePackageTest { } if (isOfType(cmd, LINUX)) { - LinuxHelper.verifyDesktopFiles(cmd, false); + LinuxHelper.verifyDesktopIntegrationFiles(cmd, false); } } diff --git a/test/jdk/tools/jpackage/share/IconTest.java b/test/jdk/tools/jpackage/share/IconTest.java index 03726e524dc..12458eda34b 100644 --- a/test/jdk/tools/jpackage/share/IconTest.java +++ b/test/jdk/tools/jpackage/share/IconTest.java @@ -409,6 +409,14 @@ public class IconTest { iconType = mainLauncherIconType; } + if (TKit.isLinux()) { + var noDefaultIcon = cmd.isImagePackageType() || !cmd.hasArgument("--linux-shortcut"); + + if (noDefaultIcon && iconType == IconType.DefaultIcon) { + iconType = IconType.NoIcon; + } + } + return iconType; }