mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 12:09:14 +00:00
8370100: Redundant .png files in Linux app-image cause unnecessary bloat
Reviewed-by: almatvee
This commit is contained in:
parent
460a69bd50
commit
327b7c3cd8
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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<String, ? super Object> params, Path outputParentDir) throws PackagerException {
|
||||
|
||||
var pkg = LinuxFromParams.DEB_PACKAGE.fetchFrom(params);
|
||||
|
||||
return Packager.<LinuxDebPackage>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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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<LinuxPackage> 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<Application, ApplicationLayout> 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(
|
||||
|
||||
@ -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<String, ? super Object> params, Path outputParentDir) throws PackagerException {
|
||||
|
||||
var pkg = LinuxFromParams.RPM_PACKAGE.fetchFrom(params);
|
||||
|
||||
return Packager.<LinuxRpmPackage>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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 <T extends Launcher> ApplicationLaunchers normalizeIcons(
|
||||
ApplicationLaunchers appLaunchers, Optional<Path> resourceDir, BiFunction<T, Launcher, T> launcherOverrideCtor) {
|
||||
|
||||
Objects.requireNonNull(resourceDir);
|
||||
|
||||
return normalizeLauncherProperty(appLaunchers, Launcher::hasDefaultIcon, (T launcher) -> {
|
||||
return resourceDir.<LauncherIcon>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 <T, U extends Launcher> ApplicationLaunchers normalizeLauncherProperty(
|
||||
ApplicationLaunchers appLaunchers,
|
||||
Predicate<U> needsNormalization,
|
||||
Function<U, Optional<T>> normalizedPropertyValueFinder,
|
||||
BiFunction<U, T, U> propertyOverrider) {
|
||||
|
||||
return normalizeLauncherProperty(
|
||||
appLaunchers,
|
||||
needsNormalization,
|
||||
normalizedPropertyValueFinder,
|
||||
launcher -> {
|
||||
return normalizedPropertyValueFinder.apply(launcher).orElseThrow();
|
||||
},
|
||||
propertyOverrider);
|
||||
}
|
||||
|
||||
static <T, U extends Launcher> ApplicationLaunchers normalizeLauncherProperty(
|
||||
ApplicationLaunchers appLaunchers,
|
||||
Predicate<U> needsNormalization,
|
||||
Function<U, Optional<T>> normalizedPropertyValueFinder,
|
||||
Function<U, T> normalizedPropertyValueGetter,
|
||||
BiFunction<U, T, U> 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<String> javaOptions() {
|
||||
|
||||
@ -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<OverridableResource> createLauncherIconResource(Application app,
|
||||
Launcher launcher,
|
||||
static Optional<OverridableResource> createLauncherIconResource(Launcher launcher,
|
||||
Function<String, OverridableResource> 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<Application, ApplicationLayout> createWriteRuntimeAction() {
|
||||
|
||||
@ -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<String, ? super Object> params,
|
||||
static <T extends Launcher> ApplicationBuilder createApplicationBuilder(Map<String, ? super Object> params,
|
||||
Function<Map<String, ? super Object>, Launcher> launcherMapper,
|
||||
BiFunction<T, Launcher, T> 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<String, ? super Object> params,
|
||||
static <T extends Launcher> ApplicationBuilder createApplicationBuilder(Map<String, ? super Object> params,
|
||||
Function<Map<String, ? super Object>, Launcher> launcherMapper,
|
||||
BiFunction<T, Launcher, T> launcherOverrideCtor,
|
||||
ApplicationLayout appLayout, RuntimeLayout runtimeLayout,
|
||||
Optional<RuntimeLayout> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<String, OverridableResource> 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 -> {
|
||||
|
||||
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
|
||||
@ -29,11 +29,11 @@ import java.util.Optional;
|
||||
/**
|
||||
* Default application launcher icon.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,16 +151,18 @@ public interface Launcher {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if this launcher has a custom icon.
|
||||
* Returns <code>true</code> if this launcher has non-default icon.
|
||||
* <p>
|
||||
* A custom icon can be sourced from an external file or from the resource directory.
|
||||
*
|
||||
* @return <code>true</code> if this launcher has a custom icon
|
||||
* @return <code>true</code> 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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -27,5 +27,5 @@ package jdk.jpackage.internal.model;
|
||||
/**
|
||||
* Application launcher icon.
|
||||
*/
|
||||
public interface LauncherIcon {
|
||||
public sealed interface LauncherIcon permits DefaultLauncherIcon, ResourceDirLauncherIcon, CustomLauncherIcon {
|
||||
}
|
||||
|
||||
@ -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.
|
||||
* <p>
|
||||
* 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 <code>null</null>
|
||||
* @return the given icon as {@link ResourceDirLauncherIcon} type or an empty {@link Optional} instance
|
||||
*/
|
||||
public static Optional<ResourceDirLauncherIcon> 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 {
|
||||
}
|
||||
}
|
||||
@ -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<Class<?>, Object> createInterfaceDispatch(Class<?>[] interfaces, Object[] slices) {
|
||||
private record InterfaceDispatchBuilder(Set<? extends Class<?>> interfaces, Collection<Object> slices) {
|
||||
|
||||
final Map<Class<?>, 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<Class<?>> 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<? extends Class<?>, List<Object>> 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<? extends Class<?>, Object> dispatch, Set<? extends Class<?>> unservedInterfaces, Collection<Object> 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<IdentityWrapper<Object>> toIdentitySet(Collection<Object> v) {
|
||||
return v.stream().map(IdentityWrapper::new).collect(toSet());
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<Class<?>, Object> createInterfaceDispatch(Class<?>[] interfaces, Object[] slices) {
|
||||
|
||||
if (interfaces.length == 0) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<Class<?>, 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<Class<?>> missingInterfaces) {
|
||||
Collection<? extends Class<?>> missingInterfaces) {
|
||||
return new IllegalArgumentException(String.format("none of the slices implement %s", missingInterfaces));
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ final class WinPackagingPipeline {
|
||||
private static void rebrandLaunchers(AppImageBuildEnv<WinApplication, ApplicationLayout> 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 {
|
||||
|
||||
@ -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 -> {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}).<Path>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<AppImageFile> 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<Scriptlet, List<String>> scriptlets = getScriptlets(cmd);
|
||||
scriptlets.entrySet().stream().forEach(e -> verifyIconInScriptlet(
|
||||
e.getKey(), e.getValue(), mimeTypeIcon));
|
||||
}
|
||||
Map<Scriptlet, List<String>> scriptlets = getScriptlets(cmd);
|
||||
scriptlets.entrySet().stream().forEach(e -> verifyIconInScriptlet(
|
||||
e.getKey(), e.getValue(), mimeTypeIcon));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user