From bb2c49144ba38ffcae131e3e138c66c8dba63a50 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 17 Oct 2025 09:48:46 -0400 Subject: [PATCH 01/39] Make Launcher interface provide complete information about the source of the launcher icon; Before this change, the function getting a resource for an additional launcher icon took both the additional launcher and the main launcher as input. Now its result doesn't depend on the main launcher. --- .../jpackage/internal/DesktopIntegration.java | 27 +--- .../jpackage/internal/LinuxFromParams.java | 5 +- .../internal/LinuxPackagingPipeline.java | 25 +++- .../jdk/jpackage/internal/MacFromParams.java | 4 +- .../jpackage/internal/ApplicationBuilder.java | 107 ++++++++++++++ .../internal/ApplicationImageUtils.java | 47 +++---- .../jdk/jpackage/internal/FromParams.java | 14 +- .../jpackage/internal/LauncherBuilder.java | 13 ++ .../internal/model/CustomLauncherIcon.java | 4 +- .../internal/model/DefaultLauncherIcon.java | 10 +- .../jdk/jpackage/internal/model/Launcher.java | 8 +- .../jpackage/internal/model/LauncherIcon.java | 2 +- .../model/ResourceDirLauncherIcon.java | 73 ++++++++++ .../internal/util/CompositeProxy.java | 130 +++++++++++++++--- .../jdk/jpackage/internal/WinFromParams.java | 5 +- .../internal/WinPackagingPipeline.java | 36 ++++- 16 files changed, 423 insertions(+), 87 deletions(-) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ResourceDirLauncherIcon.java 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 790d5d877aaed..dbaa5e3eec67c 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 @@ 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 @@ private DesktopIntegration(BuildEnv env, LinuxPackage pkg, LinuxLauncher launche // - 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 @@ private DesktopIntegration(BuildEnv env, LinuxPackage pkg, LinuxLauncher launche 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/LinuxFromParams.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java index a8d109220dccf..e9d1416b5c324 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.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 @@ private static LinuxApplication createLinuxApplication( 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 dd29338655d5d..cc72fcabeff39 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackagingPipeline.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackagingPipeline.java @@ -24,18 +24,24 @@ */ package jdk.jpackage.internal; +import static jdk.jpackage.internal.ApplicationBuilder.normalizeLauncherProperty; 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.ApplicationLaunchers; import jdk.jpackage.internal.model.ApplicationLayout; +import jdk.jpackage.internal.model.LauncherShortcut; +import jdk.jpackage.internal.model.LinuxLauncher; +import jdk.jpackage.internal.model.LinuxLauncherMixin; import jdk.jpackage.internal.resources.ResourceLocator; final class LinuxPackagingPipeline { @@ -46,13 +52,26 @@ enum LinuxAppImageTaskID implements TaskID { } static PackagingPipeline.Builder build() { - return PackagingPipeline.buildStandard() + 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(); + + return builder; + } + + static ApplicationLaunchers normalizeShortcuts(ApplicationLaunchers appLaunchers) { + return normalizeLauncherProperty(appLaunchers, launcher -> { + // Return "true" if shortcut is not configured for the launcher. + return launcher.shortcut().isEmpty(); + }, (LinuxLauncher launcher) -> { + return launcher.shortcut().flatMap(LauncherShortcut::startupDirectory); + }, (launcher, shortcut) -> { + return LinuxLauncher.create(launcher, new LinuxLauncherMixin.Stub(Optional.of(new LauncherShortcut(shortcut)))); + }); } private static void writeLauncherLib( @@ -69,7 +88,7 @@ private static void writeLauncherIcons( AppImageBuildEnv env) throws IOException { for (var launcher : env.app().launchers()) { - createLauncherIconResource(env.app(), launcher, env.env()::createResource).ifPresent(iconResource -> { + createLauncherIconResource(launcher, env.env()::createResource).ifPresent(iconResource -> { String iconFileName = launcher.executableName() + ".png"; Path iconTarget = env.resolvedLayout().desktopIntegrationDirectory().resolve(iconFileName); try { 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 e2d8750e39c93..72c33ef64756f 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 @@ private static MacApplication createMacApplication( 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 9b5ed5b3b0817..76a5fc1a50cc7 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.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.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 @@ ApplicationBuilder contentDirs(List v) { 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 @@ static Application overrideAppImageLayout(Application app, AppImageLayout appIma 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 cda5b6c79ef28..3d2ffbfdc7cba 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.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 8cfab61c9c099..df9cc5284392c 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.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.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 @@ 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 @@ static ApplicationBuilder createApplicationBuilder(Map p 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 5ce98165a4a15..0f6b5d6ac8d31 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.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 @@ private String deriveNonNullName() { 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 132165356803d..5819c42fcdaed 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.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 4275b84ba80de..91b7cf50e2f63 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 @@ /** * 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 static Optional fromLauncherIcon(LauncherIcon icon) /** * Singleton. */ - public static DefaultLauncherIcon INSTANCE = new DefaultLauncherIcon() {}; + public static DefaultLauncherIcon INSTANCE = new Details.Impl(); + + static final class Details { + private final static 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 ac60b503fe4d6..4c72907283926 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 @@ default boolean hasDefaultIcon() { } /** - * 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 bd7a9955b9006..dee20013c313a 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 @@ /** * 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 0000000000000..835fcb4b6bd41 --- /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 4fe5fe9039edd..39a9d319468b8 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 @@ private static T createCompositeProxy(Class interfaceType, BinaryOperator return proxy; } + private record InterfaceDispatchBuilder(Set> interfaces, Collection slices) { + + InterfaceDispatchBuilder { + Objects.requireNonNull(interfaces); + Objects.requireNonNull(slices); + + if (interfaces.isEmpty()) { + throw new IllegalArgumentException("No interfaces to dispatch"); + } + + if (slices.isEmpty()) { + throw createInterfaceNotImplementedException(interfaces); + } + } + + 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) { - 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))); - })); - - if (interfaceDispatch.size() != interfaces.length) { - final List> missingInterfaces = new ArrayList<>(Set.of(interfaces)); - missingInterfaces.removeAll(interfaceDispatch.keySet()); - throw createInterfaceNotImplementedException(missingInterfaces); + 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 Stream.of(interfaces).flatMap(iface -> { + 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 @@ private static Stream> unfoldInterface(Class interfaceType) { } 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 29c1b665f8685..2d4225f5a5b0c 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.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 @@ private static WinApplication createWinApplication( 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 2fa7dd895c3f2..63c9347ecce50 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinPackagingPipeline.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinPackagingPipeline.java @@ -24,19 +24,26 @@ */ package jdk.jpackage.internal; +import static jdk.jpackage.internal.ApplicationBuilder.normalizeLauncherProperty; import static jdk.jpackage.internal.ApplicationImageUtils.createLauncherIconResource; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; import jdk.jpackage.internal.PackagingPipeline.AppImageBuildEnv; import jdk.jpackage.internal.PackagingPipeline.BuildApplicationTaskID; import jdk.jpackage.internal.PackagingPipeline.CopyAppImageTaskID; import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID; import jdk.jpackage.internal.PackagingPipeline.TaskID; +import jdk.jpackage.internal.model.ApplicationLaunchers; import jdk.jpackage.internal.model.ApplicationLayout; +import jdk.jpackage.internal.model.LauncherShortcut; import jdk.jpackage.internal.model.PackagerException; import jdk.jpackage.internal.model.WinApplication; import jdk.jpackage.internal.model.WinLauncher; +import jdk.jpackage.internal.model.WinLauncherMixin; final class WinPackagingPipeline { @@ -53,10 +60,37 @@ static PackagingPipeline.Builder build() { .applicationAction(WinPackagingPipeline::rebrandLaunchers).add(); } + static ApplicationLaunchers normalizeShortcuts(ApplicationLaunchers appLaunchers) { + + appLaunchers = normalizeShortcuts(appLaunchers, WinLauncher::startMenuShortcut, (launcher, shortcut) -> { + return new WinLauncherMixin.Stub(launcher.isConsole(), shortcut, launcher.desktopShortcut()); + }); + + appLaunchers = normalizeShortcuts(appLaunchers, WinLauncher::desktopShortcut, (launcher, shortcut) -> { + return new WinLauncherMixin.Stub(launcher.isConsole(), launcher.startMenuShortcut(), shortcut); + }); + + return appLaunchers; + } + + private static ApplicationLaunchers normalizeShortcuts( + ApplicationLaunchers appLaunchers, + Function> shortcutGetter, + BiFunction, WinLauncherMixin> shortcutOverrider) { + return normalizeLauncherProperty(appLaunchers, launcher -> { + // Return "true" if shortcut is not configured for the launcher. + return shortcutGetter.apply(launcher).isEmpty(); + }, (WinLauncher launcher) -> { + return shortcutGetter.apply(launcher).flatMap(LauncherShortcut::startupDirectory); + }, (launcher, shortcut) -> { + return WinLauncher.create(launcher, shortcutOverrider.apply(launcher, Optional.of(new LauncherShortcut(shortcut)))); + }); + } + 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 { From 3dc46b3fbf87606c6a2de49c93edd4f60c9476f5 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 25 Jun 2025 01:16:53 -0400 Subject: [PATCH 02/39] LauncherStartupInfoBuilder: bugfix; CfgFile: remove null defaults --- .../jdk/jpackage/internal/CfgFile.java | 19 ++++++++++--------- .../internal/LauncherStartupInfoBuilder.java | 8 ++++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CfgFile.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CfgFile.java index 6b4ba3c441025..9cb9fb5cba0e7 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CfgFile.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/CfgFile.java @@ -30,7 +30,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Objects; import java.util.stream.Stream; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLayout; @@ -47,10 +47,14 @@ final class CfgFile { CfgFile(Application app, Launcher launcher) { startupInfo = launcher.startupInfo().orElseThrow(); outputFileName = launcher.executableName() + ".cfg"; - version = app.version(); + version = Objects.requireNonNull(app.version()); } void create(ApplicationLayout appLayout) throws IOException { + Objects.requireNonNull(appLayout); + + Objects.requireNonNull(startupInfo.qualifiedClassName()); + List> content = new ArrayList<>(); final var refs = new Referencies(appLayout); @@ -58,7 +62,7 @@ void create(ApplicationLayout appLayout) throws IOException { content.add(Map.entry("[Application]", SECTION_TAG)); if (startupInfo instanceof LauncherModularStartupInfo modularStartupInfo) { - content.add(Map.entry("app.mainmodule", modularStartupInfo.moduleName() + content.add(Map.entry("app.mainmodule", Objects.requireNonNull(modularStartupInfo.moduleName()) + "/" + startupInfo.qualifiedClassName())); } else if (startupInfo instanceof LauncherJarStartupInfo jarStartupInfo) { Path mainJarPath = refs.appDirectory().resolve(jarStartupInfo.jarPath()); @@ -67,16 +71,13 @@ void create(ApplicationLayout appLayout) throws IOException { content.add(Map.entry("app.mainjar", mainJarPath)); } else { content.add(Map.entry("app.classpath", mainJarPath)); - } - - if (!jarStartupInfo.isJarWithMainClass()) { content.add(Map.entry("app.mainclass", startupInfo.qualifiedClassName())); } } else { throw new UnsupportedOperationException(); } - for (var value : Optional.ofNullable(startupInfo.classPath()).orElseGet(List::of)) { + for (var value : startupInfo.classPath()) { content.add(Map.entry("app.classpath", refs.appDirectory().resolve(value).toString())); } @@ -88,7 +89,7 @@ void create(ApplicationLayout appLayout) throws IOException { "java-options", "-Djpackage.app-version=" + version)); // add user supplied java options if there are any - for (var value : Optional.ofNullable(startupInfo.javaOptions()).orElseGet(List::of)) { + for (var value : startupInfo.javaOptions()) { content.add(Map.entry("java-options", value)); } @@ -98,7 +99,7 @@ void create(ApplicationLayout appLayout) throws IOException { content.add(Map.entry("java-options", refs.appModsDirectory())); } - var arguments = Optional.ofNullable(startupInfo.defaultParameters()).orElseGet(List::of); + var arguments = startupInfo.defaultParameters(); if (!arguments.isEmpty()) { content.add(Map.entry("[ArgOptions]", SECTION_TAG)); for (var value : arguments) { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java index 5273f2d251c2e..26ce198a61e38 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java @@ -26,6 +26,7 @@ import java.nio.file.Path; import java.util.List; +import java.util.Optional; import java.util.function.UnaryOperator; import jdk.jpackage.internal.model.LauncherJarStartupInfo; import jdk.jpackage.internal.model.LauncherJarStartupInfoMixin; @@ -37,8 +38,11 @@ final class LauncherStartupInfoBuilder { LauncherStartupInfo create() { - return decorator.apply(new Stub(qualifiedClassName, javaOptions, - defaultParameters, classPath)); + return decorator.apply(new Stub( + qualifiedClassName, + Optional.ofNullable(javaOptions).orElseGet(List::of), + Optional.ofNullable(defaultParameters).orElseGet(List::of), + classPath)); } LauncherStartupInfoBuilder launcherData(LauncherData launcherData) { From 8afbd8f196d9bfa9e0c2e5509f4809fa1ab103da Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Tue, 10 Jun 2025 18:15:53 -0400 Subject: [PATCH 03/39] Add LauncherModularStartupInfoMixin.moduleVersion() --- .../internal/LauncherStartupInfoBuilder.java | 10 ++++++---- .../model/LauncherModularStartupInfoMixin.java | 13 ++++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java index 26ce198a61e38..c210c0601519b 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder.java @@ -32,8 +32,8 @@ import jdk.jpackage.internal.model.LauncherJarStartupInfoMixin; import jdk.jpackage.internal.model.LauncherModularStartupInfo; import jdk.jpackage.internal.model.LauncherModularStartupInfoMixin; -import jdk.jpackage.internal.model.LauncherStartupInfo.Stub; import jdk.jpackage.internal.model.LauncherStartupInfo; +import jdk.jpackage.internal.model.LauncherStartupInfo.Stub; final class LauncherStartupInfoBuilder { @@ -47,7 +47,8 @@ LauncherStartupInfo create() { LauncherStartupInfoBuilder launcherData(LauncherData launcherData) { if (launcherData.isModular()) { - decorator = new ModuleStartupInfo(launcherData.moduleName()); + decorator = new ModuleStartupInfo(launcherData.moduleName(), + Optional.ofNullable(launcherData.getAppVersion())); } else { decorator = new JarStartupInfo(launcherData.mainJarName(), launcherData.isClassNameFromMainJar()); @@ -67,12 +68,13 @@ LauncherStartupInfoBuilder defaultParameters(List v) { return this; } - private static record ModuleStartupInfo(String moduleName) implements UnaryOperator { + private static record ModuleStartupInfo(String moduleName, + Optional moduleVersion) implements UnaryOperator { @Override public LauncherStartupInfo apply(LauncherStartupInfo base) { return LauncherModularStartupInfo.create(base, - new LauncherModularStartupInfoMixin.Stub(moduleName)); + new LauncherModularStartupInfoMixin.Stub(moduleName, moduleVersion)); } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherModularStartupInfoMixin.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherModularStartupInfoMixin.java index 4a72d93a4b7d1..c558b787bc495 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherModularStartupInfoMixin.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/LauncherModularStartupInfoMixin.java @@ -24,6 +24,8 @@ */ package jdk.jpackage.internal.model; +import java.util.Optional; + /** * Details of application launcher startup configuration using Java module. */ @@ -35,10 +37,19 @@ public interface LauncherModularStartupInfoMixin { */ String moduleName(); + /** + * Gets the main module version if available or an empty {@link Optional} + * otherwise. + * + * @return the main module version if available or an empty {@link Optional} + * otherwise + */ + Optional moduleVersion(); + /** * Default implementation of {@link LauncherModularStartupInfoMixin} interface. */ - record Stub(String moduleName) implements LauncherModularStartupInfoMixin { + record Stub(String moduleName, Optional moduleVersion) implements LauncherModularStartupInfoMixin { } } From 2af85efbf06d3793b599de9990c22dfa2f39514a Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 13 Jun 2025 17:11:40 -0400 Subject: [PATCH 04/39] FileAssociationGroup: add FileAssociationGroup.Builder.description() --- .../classes/jdk/jpackage/internal/FileAssociationGroup.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociationGroup.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociationGroup.java index 349a09f237ce9..ed89ffe1ef642 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociationGroup.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociationGroup.java @@ -111,6 +111,10 @@ Builder description(String v) { return this; } + Optional description() { + return Optional.ofNullable(description); + } + Builder mimeTypes(Collection v) { mimeTypes = Set.copyOf(v); return this; From 7163799f01aeaff8601bbba4b1cccadd189f7380 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 5 Jun 2025 20:11:35 -0400 Subject: [PATCH 05/39] Add JPackageException to the model --- .../internal/model/JPackageException.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/JPackageException.java diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/JPackageException.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/JPackageException.java new file mode 100644 index 0000000000000..00c1ce0a0ddda --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/JPackageException.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, 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; + +/** + * Base class for exceptions explicitly emitted by jpackage. + */ +public class JPackageException extends RuntimeException { + + public JPackageException(String msg) { + super(Objects.requireNonNull(msg)); + } + + public JPackageException(String msg, Throwable cause) { + super(Objects.requireNonNull(msg), cause); + } + + private static final long serialVersionUID = 1L; +} From 16c62047538e192c9a262c37bb461bef166b97ad Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 5 Jun 2025 18:34:53 -0400 Subject: [PATCH 06/39] Make ConfigException and PackagerException extend RuntimeException; Fix PackagingPipeline, *Bundler and StandardBundlerParam.getDefaultAppVersion accordingly --- .../classes/jdk/jpackage/internal/MacDmgBundler.java | 6 +----- .../classes/jdk/jpackage/internal/MacPkgBundler.java | 6 +----- .../classes/jdk/jpackage/internal/AppImageBundler.java | 6 +----- .../classes/jdk/jpackage/internal/PackagingPipeline.java | 2 -- .../jdk/jpackage/internal/StandardBundlerParam.java | 7 ++----- .../jdk/jpackage/internal/model/ConfigException.java | 2 +- .../jdk/jpackage/internal/model/PackagerException.java | 2 +- 7 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java index 0ddb987dbee87..3c4ef77e14d18 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java @@ -60,11 +60,7 @@ public boolean validate(Map params) return true; } catch (RuntimeException re) { - if (re.getCause() instanceof ConfigException) { - throw (ConfigException) re.getCause(); - } else { - throw new ConfigException(re); - } + throw ConfigException.rethrowConfigException(re); } } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java index e827f238db330..ea2ad1e955913 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java @@ -62,11 +62,7 @@ public boolean validate(Map params) return true; } catch (RuntimeException re) { - if (re.getCause() instanceof ConfigException) { - throw (ConfigException) re.getCause(); - } else { - throw new ConfigException(re); - } + throw ConfigException.rethrowConfigException(re); } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageBundler.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageBundler.java index 192630a565693..48a0486f3703b 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageBundler.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageBundler.java @@ -72,11 +72,7 @@ public final boolean validate(Map params) paramsValidator.validate(params); } } catch (RuntimeException re) { - if (re.getCause() instanceof ConfigException) { - throw (ConfigException) re.getCause(); - } else { - throw new ConfigException(re); - } + throw ConfigException.rethrowConfigException(re); } return true; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java index 64a37878bd881..223286fc59846 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java @@ -508,8 +508,6 @@ private void execute(TaskContext context) throws PackagerException { throw new PackagerException(ex.getCause()); } catch (RuntimeException ex) { throw ex; - } catch (PackagerException ex) { - throw ex; } catch (Exception ex) { throw new PackagerException(ex); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java index 2b35a6830f870..1fe148c138b65 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java @@ -490,11 +490,8 @@ private static String getDefaultAppVersion(Map params) { LauncherData launcherData = null; try { launcherData = LAUNCHER_DATA.fetchFrom(params); - } catch (RuntimeException ex) { - if (ex.getCause() instanceof ConfigException) { - return appVersion; - } - throw ex; + } catch (ConfigException ex) { + return appVersion; } if (launcherData.isModular()) { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ConfigException.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ConfigException.java index a2a799d7f1b92..f8d6d8e92a774 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ConfigException.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ConfigException.java @@ -46,7 +46,7 @@ * .create(); * } */ -public class ConfigException extends Exception { +public class ConfigException extends RuntimeException { private static final long serialVersionUID = 1L; private final String advice; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/PackagerException.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/PackagerException.java index 4ac2599a38173..4c28e6327eb58 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/PackagerException.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/PackagerException.java @@ -38,7 +38,7 @@ * throw buildLocalizedException(i18n).message("error.no.name").create(); * } */ -public class PackagerException extends Exception { +public class PackagerException extends RuntimeException { private static final long serialVersionUID = 1L; From c217ce1e700def878c7133a02a5fe3f9a53c379b Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Tue, 13 May 2025 10:54:10 -0400 Subject: [PATCH 07/39] Add a builder for LauncherStartupInfo with the unit tests. --- .../internal/LauncherStartupInfoBuilder2.java | 239 ++++++++++++++++++ .../LauncherStartupInfoBuilderTest.java | 155 ++++++++++++ 2 files changed, 394 insertions(+) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder2.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/LauncherStartupInfoBuilderTest.java diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder2.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder2.java new file mode 100644 index 0000000000000..684daea8af24c --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherStartupInfoBuilder2.java @@ -0,0 +1,239 @@ +/* + * 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; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import jdk.jpackage.internal.model.JPackageException; +import jdk.jpackage.internal.model.LauncherJarStartupInfo; +import jdk.jpackage.internal.model.LauncherJarStartupInfoMixin; +import jdk.jpackage.internal.model.LauncherModularStartupInfo; +import jdk.jpackage.internal.model.LauncherModularStartupInfoMixin; +import jdk.jpackage.internal.model.LauncherStartupInfo; + +final class LauncherStartupInfoBuilder2 { + + LauncherStartupInfo create() { + if (moduleName != null) { + return createModular(); + } else if (mainJar != null) { + return createNonModular(); + } else { + throw new JPackageException(I18N.format("ERR_NoEntryPoint")); + } + } + + LauncherStartupInfoBuilder2 inputDir(Path v) { + inputDir = v; + return this; + } + + LauncherStartupInfoBuilder2 javaOptions(List v) { + if (v != null) { + v.forEach(Objects::requireNonNull); + } + javaOptions = v; + return this; + } + + LauncherStartupInfoBuilder2 defaultParameters(List v) { + if (v != null) { + v.forEach(Objects::requireNonNull); + } + defaultParameters = v; + return this; + } + + LauncherStartupInfoBuilder2 mainJar(Path v) { + mainJar = v; + return this; + } + + LauncherStartupInfoBuilder2 mainClassName(String v) { + mainClassName = v; + return this; + } + + LauncherStartupInfoBuilder2 predefinedRuntimeImage(Path v) { + cookedRuntimePath = v; + return this; + } + + LauncherStartupInfoBuilder2 moduleName(String v) { + if (v == null) { + moduleName = null; + } else { + var slashIdx = v.indexOf('/'); + if (slashIdx < 0) { + moduleName = v; + } else { + moduleName = v.substring(0, slashIdx); + if (slashIdx < v.length() - 1) { + mainClassName(v.substring(slashIdx + 1)); + } + } + } + return this; + } + + LauncherStartupInfoBuilder2 modulePath(List v) { + modulePath = v; + return this; + } + + private Optional inputDir() { + return Optional.ofNullable(inputDir); + } + + private Optional mainClassName() { + return Optional.ofNullable(mainClassName); + } + + private Optional cookedRuntimePath() { + return Optional.ofNullable(cookedRuntimePath); + } + + private LauncherStartupInfo createLauncherStartupInfo(String mainClassName, List classpath) { + Objects.requireNonNull(mainClassName); + classpath.forEach(Objects::requireNonNull); + return new LauncherStartupInfo.Stub(mainClassName, + Optional.ofNullable(javaOptions).orElseGet(List::of), + Optional.ofNullable(defaultParameters).orElseGet(List::of), + classpath); + } + + private static List createClasspath(Path inputDir, Set excludes) { + excludes.forEach(Objects::requireNonNull); + try (final var walk = Files.walk(inputDir)) { + return walk.filter(Files::isRegularFile) + .filter(file -> file.getFileName().toString().endsWith(".jar")) + .map(inputDir::relativize) + .filter(Predicate.not(excludes::contains)) + .distinct() + .toList(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private LauncherModularStartupInfo createModular() { + final var fullModulePath = getFullModulePath(); + + // Try to find the module in the specified module path list. + final var moduleInfo = JLinkRuntimeBuilder.createModuleFinder(fullModulePath).find(moduleName) + .map(ModuleInfo::fromModuleReference).or(() -> { + // Failed to find the module in the specified module path list. + return cookedRuntimePath().flatMap(cookedRuntime -> { + // Lookup the module in the external runtime. + return ModuleInfo.fromCookedRuntime(moduleName, cookedRuntime); + }); + }).orElseThrow(() -> { + return I18N.buildConfigException("error.no-module-in-path", moduleName).create(); + }); + + final var effectiveMainClassName = mainClassName().or(moduleInfo::mainClass).orElseThrow(() -> { + return I18N.buildConfigException("ERR_NoMainClass").create(); + }); + + // If module is located in the file system, exclude it from the classpath. + final var classpath = inputDir().map(theInputDir -> { + var classpathExcludes = moduleInfo.fileLocation().filter(moduleFile -> { + return moduleFile.startsWith(theInputDir); + }).map(theInputDir::relativize).map(Set::of).orElseGet(Set::of); + return createClasspath(theInputDir, classpathExcludes); + }).orElseGet(List::of); + + return LauncherModularStartupInfo.create( + createLauncherStartupInfo(effectiveMainClassName, classpath), + new LauncherModularStartupInfoMixin.Stub(moduleInfo.name(), moduleInfo.version())); + } + + private List getFullModulePath() { + return cookedRuntimePath().map(runtimeImage -> { + return Stream.of(modulePath(), List.of(runtimeImage.resolve("lib"))).flatMap(List::stream).toList(); + }).orElse(modulePath()); + } + + private List modulePath() { + return Optional.ofNullable(modulePath).orElseGet(List::of); + } + + private LauncherJarStartupInfo createNonModular() { + final var theInputDir = inputDir().orElseThrow(); + + final var mainJarPath = theInputDir.resolve(mainJar); + + if (!Files.exists(mainJarPath)) { + throw I18N.buildConfigException() + .message("error.main-jar-does-not-exist", mainJar) + .advice("error.main-jar-does-not-exist.advice") + .create(); + } + + final var effectiveMainClassName = mainClassName().or(() -> { + try (final var jf = new JarFile(mainJarPath.toFile())) { + return Optional.ofNullable(jf.getManifest()).map(Manifest::getMainAttributes).map(attrs -> { + return attrs.getValue(Attributes.Name.MAIN_CLASS); + }); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }).orElseThrow(() -> { + return I18N.buildConfigException() + .message("error.no-main-class-with-main-jar", mainJar) + .advice("error.no-main-class-with-main-jar.advice", mainJar) + .create(); + }); + + return LauncherJarStartupInfo.create( + createLauncherStartupInfo(effectiveMainClassName, createClasspath(theInputDir, Set.of(mainJar))), + new LauncherJarStartupInfoMixin.Stub(mainJar, mainClassName().isEmpty())); + } + + // Modular options + private String moduleName; + private List modulePath; + + // Non-modular options + private Path mainJar; + + // Common options + private Path inputDir; + private String mainClassName; + private List javaOptions; + private List defaultParameters; + private Path cookedRuntimePath; +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/LauncherStartupInfoBuilderTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/LauncherStartupInfoBuilderTest.java new file mode 100644 index 0000000000000..01a7e4f73832a --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/LauncherStartupInfoBuilderTest.java @@ -0,0 +1,155 @@ +/* + * 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. + * + * 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; +import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.LauncherJarStartupInfo; +import jdk.jpackage.internal.model.LauncherJarStartupInfoMixin; +import jdk.jpackage.internal.model.LauncherModularStartupInfo; +import jdk.jpackage.internal.model.LauncherModularStartupInfoMixin; +import jdk.jpackage.internal.model.LauncherStartupInfo; +import jdk.jpackage.test.Annotations.ParameterSupplier; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.HelloApp; +import jdk.jpackage.test.JUnitAdapter; +import jdk.jpackage.test.JavaAppDesc; +import jdk.jpackage.test.ObjectMapper; +import jdk.jpackage.test.TKit; + +public class LauncherStartupInfoBuilderTest extends JUnitAdapter { + + public record TestSpec(JavaAppDesc javaAppDesc, boolean withMainClass, Object expectedInfo) { + public TestSpec { + Objects.requireNonNull(javaAppDesc); + Objects.requireNonNull(expectedInfo); + } + + void test() throws ConfigException { + final var workDir = TKit.createTempDirectory("input"); + + HelloApp.createBundle(javaAppDesc, workDir); + + final var builder = new LauncherStartupInfoBuilder2(); + + builder.inputDir(workDir); + + if (withMainClass) { + builder.mainClassName(javaAppDesc.className()); + } + + Optional.ofNullable(javaAppDesc.moduleName()).ifPresentOrElse(moduleName -> { + if (javaAppDesc.isWithMainClass()) { + builder.moduleName(moduleName + "/" + javaAppDesc.className()); + } else { + builder.moduleName(moduleName); + } + builder.modulePath(List.of(workDir)); + }, () -> { + builder.mainJar(Path.of(javaAppDesc.jarFileName())); + }); + + final var actualInfo = builder.create(); + + assertEquals(expectedInfo, OM.map(actualInfo)); + } + + static final class Builder { + TestSpec create() { + return new TestSpec(javaAppDesc, withMainClass, OM.map(createInfo())); + } + + Builder javaAppDesc(String v) { + javaAppDesc = JavaAppDesc.parse(v); + return this; + } + + Builder withMainClass(boolean v) { + withMainClass = v; + return this; + } + + private LauncherStartupInfo createInfo() { + final var base = createBaseInfo(); + if (javaAppDesc.moduleName() != null) { + return LauncherModularStartupInfo.create(base, createModularMixin()); + } else { + return LauncherJarStartupInfo.create(base, createJarMixin()); + } + } + + private LauncherStartupInfo createBaseInfo() { + return new LauncherStartupInfo.Stub(javaAppDesc.className(), List.of(), List.of(), classPath); + } + + private LauncherJarStartupInfoMixin createJarMixin() { + return new LauncherJarStartupInfoMixin.Stub(Path.of(javaAppDesc.jarFileName()), + !withMainClass && javaAppDesc.isWithMainClass()); + } + + private LauncherModularStartupInfoMixin createModularMixin() { + return new LauncherModularStartupInfoMixin.Stub(javaAppDesc.moduleName(), + Optional.ofNullable(javaAppDesc.moduleVersion())); + } + + private JavaAppDesc javaAppDesc; + private boolean withMainClass = true; + private List classPath = new ArrayList<>(); + } + } + + @Test + @ParameterSupplier + public static void test(TestSpec spec) throws ConfigException { + spec.test(); + } + + public static Collection test() { + return Stream.of( + build(""), + build("foo.jar:foo.bar.U"), + build("foo.jar:foo.bar.U!"), + build("foo.jar:foo.bar.U!").withMainClass(false), + build("a.b/d.c.O").withMainClass(true), + build("a.b/d.c.O@3.5.7-beta").withMainClass(true), + build("a.b/d.c.O!"), + build("a.b/d.c.O!").withMainClass(false) + ).map(TestSpec.Builder::create).map(v -> { + return new Object[] {v}; + }).toList(); + } + + private static TestSpec.Builder build(String javaAppDesc) { + return new TestSpec.Builder().javaAppDesc(javaAppDesc); + } + + private static final ObjectMapper OM = ObjectMapper.standard().create(); +} From 41d93972944e9bc70eaee651741c236df1ae9070 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 17 May 2025 13:36:10 -0400 Subject: [PATCH 08/39] Add SetBuilder utility class --- .../jpackage/internal/util/SetBuilder.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/SetBuilder.java diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/SetBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/SetBuilder.java new file mode 100644 index 0000000000000..b9d3140c5f93e --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/SetBuilder.java @@ -0,0 +1,87 @@ +/* + * 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. + * + * 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.util; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public final class SetBuilder { + + public static SetBuilder build(Class type) { + return new SetBuilder<>(); + } + + public SetBuilder set(Collection v) { + return clear().add(v); + } + + @SafeVarargs + @SuppressWarnings("varargs") + public final SetBuilder set(T... v) { + return set(List.of(v)); + } + + public SetBuilder add(Collection v) { + values.addAll(v); + return this; + } + + @SafeVarargs + @SuppressWarnings("varargs") + public final SetBuilder add(T... v) { + return add(List.of(v)); + } + + public SetBuilder remove(Collection v) { + values.removeAll(v); + return this; + } + + @SafeVarargs + @SuppressWarnings("varargs") + public final SetBuilder remove(T... v) { + return remove(List.of(v)); + } + + public SetBuilder clear() { + values.clear(); + return this; + } + + public SetBuilder emptyAllowed(boolean v) { + emptyAllowed = v; + return this; + } + + public Set create() { + if (values.isEmpty() && !emptyAllowed) { + throw new UnsupportedOperationException(); + } + return Set.copyOf(values); + } + + private boolean emptyAllowed; + private final Set values = new HashSet<>(); +} From 55db526489fd7fa51ccd158004dc75297218dd5e Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 5 Jun 2025 19:59:47 -0400 Subject: [PATCH 09/39] Use I18N.buildConfigException() to create ConfigException instances --- .../jdk/jpackage/internal/MacAppBundler.java | 4 +-- .../internal/JLinkRuntimeBuilder.java | 4 +-- .../jdk/jpackage/internal/LauncherData.java | 29 +++++++++---------- .../internal/model/ConfigException.java | 4 +++ 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java index cce35ece117d5..c2aa3d641c809 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java @@ -72,9 +72,7 @@ private static void doValidate(Map params) if (StandardBundlerParam.hasPredefinedAppImage(params)) { if (!Optional.ofNullable( SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) { - throw new ConfigException( - I18N.getString("error.app-image.mac-sign.required"), - null); + throw I18N.buildConfigException("error.app-image.mac-sign.required").create(); } } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java index 2273d385936e1..c0167e0ec9dac 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java @@ -36,7 +36,6 @@ import java.lang.module.ResolvedModule; import java.nio.file.Files; import java.nio.file.Path; -import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -177,8 +176,7 @@ private static List createJLinkCmdline(List modulePath, Set { - throw new ConfigException(MessageFormat.format(I18N.getString( - "error.blocked.option"), option), null); + throw I18N.buildConfigException("error.blocked.option", option).create(); } default -> { args.add(option); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherData.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherData.java index 488e9106479d2..7896aca8cb534 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherData.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/LauncherData.java @@ -151,14 +151,13 @@ private static LauncherData createModular(String mainModule, } if (launcherData.moduleInfo == null) { - throw new ConfigException(MessageFormat.format(I18N.getString( - "error.no-module-in-path"), moduleName), null); + throw I18N.buildConfigException("error.no-module-in-path", moduleName).create(); } if (launcherData.qualifiedClassName == null) { launcherData.qualifiedClassName = launcherData.moduleInfo.mainClass().orElse(null); if (launcherData.qualifiedClassName == null) { - throw new ConfigException(I18N.getString("ERR_NoMainClass"), null); + throw I18N.buildConfigException("ERR_NoMainClass").create(); } } @@ -179,10 +178,10 @@ private static LauncherData createNonModular( if (launcherData.mainJarName != null && mainJarDir != null) { mainJarPath = mainJarDir.resolve(launcherData.mainJarName); if (!Files.exists(mainJarPath)) { - throw new ConfigException(MessageFormat.format(I18N.getString( - "error.main-jar-does-not-exist"), - launcherData.mainJarName), I18N.getString( - "error.main-jar-does-not-exist.advice")); + throw I18N.buildConfigException() + .message("error.main-jar-does-not-exist", launcherData.mainJarName) + .advice("error.main-jar-does-not-exist.advice") + .create(); } } else { mainJarPath = null; @@ -190,8 +189,10 @@ private static LauncherData createNonModular( if (launcherData.qualifiedClassName == null) { if (mainJarPath == null) { - throw new ConfigException(I18N.getString("error.no-main-class"), - I18N.getString("error.no-main-class.advice")); + throw I18N.buildConfigException() + .message("error.no-main-class") + .advice("error.no-main-class.advice") + .create(); } try (JarFile jf = new JarFile(mainJarPath.toFile())) { @@ -206,12 +207,10 @@ private static LauncherData createNonModular( } if (launcherData.qualifiedClassName == null) { - throw new ConfigException(MessageFormat.format(I18N.getString( - "error.no-main-class-with-main-jar"), - launcherData.mainJarName), MessageFormat.format( - I18N.getString( - "error.no-main-class-with-main-jar.advice"), - launcherData.mainJarName)); + throw I18N.buildConfigException() + .message("error.no-main-class-with-main-jar", launcherData.mainJarName) + .advice("error.no-main-class-with-main-jar.advice", launcherData.mainJarName) + .create(); } return launcherData; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ConfigException.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ConfigException.java index f8d6d8e92a774..71fb966b6116c 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ConfigException.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/ConfigException.java @@ -60,6 +60,10 @@ public ConfigException(String msg, String advice, Throwable cause) { this.advice = advice; } + public ConfigException(String msg, Throwable cause) { + this(msg, null, cause); + } + public ConfigException(Throwable cause) { super(cause); this.advice = null; From acfd263f616eadc49b9b6315ad67f79631ecc209 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 5 Jun 2025 19:12:00 -0400 Subject: [PATCH 10/39] Better exception handling in AppImageFile. Better test coverage. Use ObjectMapper in tests. Change signature of AppImageFile.load(Path appImageDir, ApplicationLayout appLayout) to AppImageFile.load(ApplicationLayout appLayout), appImageDir parameter is redundant. --- .../jdk/jpackage/internal/AppImageFile.java | 38 +++--- .../internal/StandardBundlerParam.java | 2 +- .../resources/MainResources.properties | 6 +- .../jpackage/internal/AppImageFileTest.java | 114 ++++++++++++++---- .../jpackage/share/AppImagePackageTest.java | 6 +- 5 files changed, 123 insertions(+), 43 deletions(-) diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java index 8b8a22edc5696..19b57f65ba161 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java @@ -28,6 +28,7 @@ import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet; import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; +import jdk.jpackage.internal.util.function.ExceptionBox; import java.io.IOException; import java.nio.file.Files; @@ -45,8 +46,8 @@ import javax.xml.xpath.XPathFactory; import jdk.internal.util.OperatingSystem; import jdk.jpackage.internal.model.Application; +import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.ApplicationLayout; -import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.ExternalApplication; import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.util.XmlUtils; @@ -146,22 +147,28 @@ void save(ApplicationLayout appLayout) throws IOException { } /** - * Returns path to application image info file. - * @param appLayout - application layout + * Returns the path to the application image info file in the given application layout. + * + * @param appLayout the application layout */ static Path getPathInAppImage(ApplicationLayout appLayout) { return appLayout.appDirectory().resolve(FILENAME); } /** - * Loads application image info from application image. - * @param appImageDir - path at which to resolve the given application layout - * @param appLayout - application layout + * Loads application image info from the specified application layout. + * + * @param appLayout the application layout */ - static AppImageFile load(Path appImageDir, ApplicationLayout appLayout) throws ConfigException, IOException { - var srcFilePath = getPathInAppImage(appLayout.resolveAt(appImageDir)); + static AppImageFile load(ApplicationLayout appLayout) { + Objects.requireNonNull(appLayout); + + final var appImageDir = appLayout.rootDirectory(); + final var appImageFilePath = getPathInAppImage(appLayout); + final var relativeAppImageFilePath = appImageDir.relativize(appImageFilePath); + try { - final Document doc = XmlUtils.initDocumentBuilder().parse(Files.newInputStream(srcFilePath)); + final Document doc = XmlUtils.initDocumentBuilder().parse(Files.newInputStream(appImageFilePath)); final XPath xPath = XPathFactory.newInstance().newXPath(); @@ -198,15 +205,18 @@ static AppImageFile load(Path appImageDir, ApplicationLayout appLayout) throws C } catch (XPathExpressionException ex) { // This should never happen as XPath expressions should be correct - throw new RuntimeException(ex); + throw ExceptionBox.rethrowUnchecked(ex); } catch (SAXException ex) { - // Exception reading input XML (probably malformed XML) - throw new IOException(ex); + // Malformed input XML + throw new JPackageException(I18N.format("error.malformed-app-image-file", relativeAppImageFilePath, appImageDir), ex); } catch (NoSuchFileException ex) { - throw I18N.buildConfigException("error.foreign-app-image", appImageDir).create(); + // Don't save the original exception as its error message is redundant. + throw new JPackageException(I18N.format("error.missing-app-image-file", relativeAppImageFilePath, appImageDir)); } catch (InvalidAppImageFileException ex) { // Invalid input XML - throw I18N.buildConfigException("error.invalid-app-image", appImageDir, srcFilePath).create(); + throw new JPackageException(I18N.format("error.invalid-app-image-file", relativeAppImageFilePath, appImageDir)); + } catch (IOException ex) { + throw new JPackageException(I18N.format("error.reading-app-image-file", relativeAppImageFilePath, appImageDir), ex); } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java index 1fe148c138b65..741d962ce8c58 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java @@ -103,7 +103,7 @@ final class StandardBundlerParam { ExternalApplication.class, params -> { if (hasPredefinedAppImage(params)) { var appImage = PREDEFINED_APP_IMAGE.fetchFrom(params); - return AppImageFile.load(appImage, PLATFORM_APPLICATION_LAYOUT); + return AppImageFile.load(PLATFORM_APPLICATION_LAYOUT.resolveAt(appImage)); } else { return null; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties index 2a81b1c102cb2..10c84d430341e 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties @@ -78,8 +78,10 @@ error.blocked.option=jlink option [{0}] is not permitted in --jlink-options error.no.name=Name not specified with --name and cannot infer one from app-image error.no.name.advice=Specify name with --name -error.foreign-app-image=Error: Missing .jpackage.xml file in app-image dir "{0}" -error.invalid-app-image=Error: app-image dir "{0}" generated by another jpackage version or malformed "{1}" file +error.missing-app-image-file=Error: "{0}" file is missing in the predefined app image "{1}" +error.invalid-app-image-file=Error: "{0}" file in the predefined app image "{1}" is corrupted or was created by another version of jpackage +error.malformed-app-image-file=Error: "{0}" file in the predefined app image "{1}" contains malformed XML data +error.reading-app-image-file=Error: Failed to read "{0}" file in the predefined app image "{1}" error.invalid-install-dir=Invalid installation directory "{0}" diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java index 2bbb16c71d39a..b6fe909d568c1 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java @@ -24,8 +24,11 @@ package jdk.jpackage.internal; import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -36,22 +39,25 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLaunchers; import jdk.jpackage.internal.model.ApplicationLayout; -import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.ExternalApplication; import jdk.jpackage.internal.model.ExternalApplication.LauncherInfo; +import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.model.LauncherStartupInfo; +import jdk.jpackage.test.ObjectMapper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; public class AppImageFileTest { @@ -126,8 +132,9 @@ AppImageFile create() { void createInDir(Path dir) { final var file = create(); final var copy = toSupplier(() -> { - file.save(DUMMY_LAYOUT.resolveAt(dir)); - return AppImageFile.load(dir, DUMMY_LAYOUT); + var layout = DUMMY_LAYOUT.resolveAt(dir); + file.save(layout); + return AppImageFile.load(layout); }).get(); assertEquals(file, copy); @@ -163,27 +170,92 @@ public void testAdditionalLaunchers() { .createInDir(tempFolder); } + @Test + public void testMalformedXml() throws IOException { + var ex = assertThrowsExactly(JPackageException.class, () -> createFromXml(List.of(""))); + Assertions.assertEquals(I18N.format("error.malformed-app-image-file", ".jpackage.xml", tempFolder), ex.getMessage()); + assertNotNull(ex.getCause()); + } + + @Test + public void testNoSuchFile() throws IOException { + var ex = assertThrowsExactly(JPackageException.class, () -> AppImageFile.load(DUMMY_LAYOUT.resolveAt(tempFolder))); + Assertions.assertEquals(I18N.format("error.missing-app-image-file", ".jpackage.xml", tempFolder), ex.getMessage()); + assertNull(ex.getCause()); + } + + @Test + public void testDirectory() throws IOException { + Files.createDirectory(AppImageFile.getPathInAppImage(DUMMY_LAYOUT.resolveAt(tempFolder))); + + var ex = assertThrowsExactly(JPackageException.class, () -> AppImageFile.load(DUMMY_LAYOUT.resolveAt(tempFolder))); + Assertions.assertEquals(I18N.format("error.reading-app-image-file", ".jpackage.xml", tempFolder), ex.getMessage()); + assertNotNull(ex.getCause()); + } + + @Test + @EnabledOnOs(value = OS.WINDOWS, disabledReason = "Can reliably lock a file using FileLock to cuase an IOException on Windows only") + @SuppressWarnings("try") + public void testGenericIOException() throws IOException { + + final var appImageFile = AppImageFile.getPathInAppImage(DUMMY_LAYOUT.resolveAt(tempFolder)); + Files.writeString(appImageFile, ""); + + try (var out = new FileOutputStream(appImageFile.toFile()); var lock = out.getChannel().lock()) { + var ex = assertThrowsExactly(JPackageException.class, () -> AppImageFile.load(DUMMY_LAYOUT.resolveAt(tempFolder))); + Assertions.assertEquals(I18N.format("error.reading-app-image-file", ".jpackage.xml", tempFolder), ex.getMessage()); + assertNotNull(ex.getCause()); + } + } + @ParameterizedTest @MethodSource public void testInavlidXml(List xmlData) throws IOException { - assertThrowsExactly(ConfigException.class, () -> createFromXml(xmlData), () -> { - return I18N.format("error.invalid-app-image", tempFolder, ".jpackage.xml"); - }); + var ex = assertThrowsExactly(JPackageException.class, () -> createFromXml(xmlData)); + Assertions.assertEquals(I18N.format("error.invalid-app-image-file", ".jpackage.xml", tempFolder), ex.getMessage()); + assertNull(ex.getCause()); } private static Stream> testInavlidXml() { return Stream.of(List.of(""), - List.of(""), - createXml(), + createValidBodyWithHeader(null, null), + createValidBodyWithHeader("foo", "foo"), + createValidBodyWithHeader(null, "foo"), + createValidBodyWithHeader("foo", null), + createValidBodyWithHeader(AppImageFile.getPlatform(), null), + createValidBodyWithHeader(AppImageFile.getPlatform(), "foo"), + createValidBodyWithHeader(null, AppImageFile.getVersion()), + createValidBodyWithHeader("foo", AppImageFile.getVersion()), createXml(""), createXml("Foo", ""), createXml("A") ); } + private static List createValidBodyWithHeader(String platform, String version) { + + var sb = new StringBuilder(); + sb.append(" { + sb.append(String.format(" platform=\"%s\"", v)); + }); + Optional.ofNullable(version).ifPresent(v -> { + sb.append(String.format(" version=\"%s\"", v)); + }); + sb.append(">"); + + return List.of( + sb.toString(), + "D", + "100", + "Hello", + "" + ); + } + @ParameterizedTest @MethodSource - public void testValidXml(AppImageFile expected, List xmlData) throws IOException, ConfigException { + public void testValidXml(AppImageFile expected, List xmlData) throws IOException { final var actual = createFromXml(xmlData); assertEquals(expected, actual); } @@ -230,14 +302,15 @@ private static Stream testValidXml() { "foofootrue", "false", "AB") + ), + Arguments.of(build().version("100").launcherName("D").mainClass("Hello").create(), + createValidBodyWithHeader(AppImageFile.getPlatform(), AppImageFile.getVersion()) ) ); } - private AppImageFile createFromXml(List xmlData) throws IOException, ConfigException { + private AppImageFile createFromXml(List xmlData) throws IOException { Path path = AppImageFile.getPathInAppImage(DUMMY_LAYOUT.resolveAt(tempFolder)); - path.toFile().mkdirs(); - Files.delete(path); List data = new ArrayList<>(); data.add(""); @@ -246,26 +319,18 @@ private AppImageFile createFromXml(List xmlData) throws IOException, Con Files.write(path, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - AppImageFile image = AppImageFile.load(tempFolder, DUMMY_LAYOUT); + AppImageFile image = AppImageFile.load(DUMMY_LAYOUT.resolveAt(tempFolder)); return image; } private static void assertEquals(AppImageFile expected, AppImageFile actual) { - assertPropertyEquals(expected, actual, AppImageFile::getAppVersion); - assertPropertyEquals(expected, actual, AppImageFile::getLauncherName); - assertPropertyEquals(expected, actual, AppImageFile::getMainClass); - assertPropertyEquals(expected, actual, AppImageFile::getExtra); - Assertions.assertEquals(additionaLaunchersAsMap(expected), additionaLaunchersAsMap(actual)); + Assertions.assertEquals(OM.map(expected), OM.map(actual)); } private static Map additionaLaunchersAsMap(AppImageFile file) { return file.getAddLaunchers().stream().collect(Collectors.toMap(AppImageFile.LauncherInfo::name, x -> x)); } - private static void assertPropertyEquals(T expected, T actual, Function getProperty) { - Assertions.assertEquals(getProperty.apply(expected), getProperty.apply(actual)); - } - private static final List createXml(String ...xml) { final List content = new ArrayList<>(); content.add(String.format("", AppImageFile.getPlatform(), AppImageFile.getVersion())); @@ -277,5 +342,8 @@ private static final List createXml(String ...xml) { @TempDir private Path tempFolder; + private static final ObjectMapper OM = ObjectMapper.standard().subst(ExternalApplication.class, "getAddLaunchers", obj -> { + return additionaLaunchersAsMap((AppImageFile)obj); + }).create(); private static final ApplicationLayout DUMMY_LAYOUT = ApplicationLayout.build().setAll("").create(); } diff --git a/test/jdk/tools/jpackage/share/AppImagePackageTest.java b/test/jdk/tools/jpackage/share/AppImagePackageTest.java index aacb76b122b29..6d1a6302af247 100644 --- a/test/jdk/tools/jpackage/share/AppImagePackageTest.java +++ b/test/jdk/tools/jpackage/share/AppImagePackageTest.java @@ -184,7 +184,7 @@ public static void testBadAppImageFile() { final var appImageDir = appImageCmd.outputBundle(); final var expectedError = JPackageStringBundle.MAIN.cannedFormattedString( - "error.invalid-app-image", appImageDir, AppImageFile.getPathInAppImage(appImageDir)); + "error.invalid-app-image-file", AppImageFile.getPathInAppImage(Path.of("")), appImageDir); configureBadAppImage(appImageDir, expectedError).addRunOnceInitializer(() -> { appImageCmd.execute(); @@ -196,8 +196,8 @@ public static void testBadAppImageFile() { } private static PackageTest configureBadAppImage(Path appImageDir) { - return configureBadAppImage(appImageDir, - JPackageStringBundle.MAIN.cannedFormattedString("error.foreign-app-image", appImageDir)); + return configureBadAppImage(appImageDir, JPackageStringBundle.MAIN.cannedFormattedString( + "error.missing-app-image-file", AppImageFile.getPathInAppImage(Path.of("")), appImageDir)); } private static PackageTest configureBadAppImage(Path appImageDir, CannedFormattedString expectedError) { From a7cf7c022de0b48ad302af22689471034581da3b Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 13 Oct 2025 12:27:16 -0400 Subject: [PATCH 11/39] the cli: old help output --- .../jdk/jpackage/internal/cli/help-linux.txt | 197 +++++++++++++++ .../jdk/jpackage/internal/cli/help-macos.txt | 237 ++++++++++++++++++ .../jpackage/internal/cli/help-windows.txt | 204 +++++++++++++++ 3 files changed, 638 insertions(+) create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt new file mode 100644 index 0000000000000..802082b1d2f3d --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt @@ -0,0 +1,197 @@ +Usage: jpackage + +Sample usages: +-------------- + Generate an application package suitable for the host system: + For a modular application: + jpackage -n name -p modulePath -m moduleName/className + For a non-modular application: + jpackage -i inputDir -n name \ + --main-class className --main-jar myJar.jar + From a pre-built application image: + jpackage -n name --app-image appImageDir + Generate an application image: + For a modular application: + jpackage --type app-image -n name -p modulePath \ + -m moduleName/className + For a non-modular application: + jpackage --type app-image -i inputDir -n name \ + --main-class className --main-jar myJar.jar + To provide your own options to jlink, run jlink separately: + jlink --output appRuntimeImage -p modulePath \ + --add-modules moduleName \ + --no-header-files [...] + jpackage --type app-image -n name \ + -m moduleName/className --runtime-image appRuntimeImage + Generate a Java runtime package: + jpackage -n name --runtime-image + +Generic Options: + @ + Read options and/or mode from a file + This option can be used multiple times. + --type -t + The type of package to create + Valid values are: {"app-image", "rpm", "deb"} + If this option is not specified a platform dependent + default type will be created. + --app-version + Version of the application and/or package + --copyright + Copyright for the application + --description + Description of the application + --help -h + Print the usage text with a list and description of each valid + option for the current platform to the output stream, and exit + --icon + Path of the icon of the application package + (absolute path or relative to the current directory) + --name -n + Name of the application and/or package + --dest -d + Path where generated output file is placed + (absolute path or relative to the current directory) + Defaults to the current working directory. + --temp + Path of a new or empty directory used to create temporary files + (absolute path or relative to the current directory) + If specified, the temp dir will not be removed upon the task + completion and must be removed manually. + If not specified, a temporary directory will be created and + removed upon the task completion. + --vendor + Vendor of the application + --verbose + Enables verbose output + --version + Print the product version to the output stream and exit. + +Options for creating the runtime image: + --add-modules [,...] + A comma (",") separated list of modules to add + This module list, along with the main module (if specified) + will be passed to jlink as the --add-module argument. + If not specified, either just the main module (if --module is + specified), or the default set of modules (if --main-jar is + specified) are used. + This option can be used multiple times. + --module-path -p ... + A : separated list of paths + Each path is either a directory of modules or the path to a + modular jar. + (Each path is absolute or relative to the current directory.) + This option can be used multiple times. + --jlink-options + A space separated list of options to pass to jlink + If not specified, defaults to "--strip-native-commands + --strip-debug --no-man-pages --no-header-files". + This option can be used multiple times. + --runtime-image + Path of the predefined runtime image that will be copied into + the application image + (absolute path or relative to the current directory) + If --runtime-image is not specified, jpackage will run jlink to + create the runtime image using options: + --strip-debug, --no-header-files, --no-man-pages, and + --strip-native-commands. + +Options for creating the application image: + --input -i + Path of the input directory that contains the files to be packaged + (absolute path or relative to the current directory) + All files in the input directory will be packaged into the + application image. + --app-content [,...] + A comma separated list of paths to files and/or directories + to add to the application payload. + This option can be used more than once. + +Options for creating the application launcher(s): + --add-launcher = + Name of launcher, and a path to a Properties file that contains + a list of key, value pairs + (absolute path or relative to the current directory) + The keys "module", "main-jar", "main-class", "description", + "arguments", "java-options", "app-version", "icon", + "launcher-as-service", + "win-console", "win-shortcut", "win-menu", + "linux-app-category", and "linux-shortcut" can be used. + These options are added to, or used to overwrite, the original + command line options to build an additional alternative launcher. + The main application launcher will be built from the command line + options. Additional alternative launchers can be built using + this option, and this option can be used multiple times to + build multiple additional launchers. + --arguments
+ Command line arguments to pass to the main class if no command + line arguments are given to the launcher + This option can be used multiple times. + --java-options + Options to pass to the Java runtime + This option can be used multiple times. + --main-class + Qualified name of the application main class to execute + This option can only be used if --main-jar is specified. + --main-jar
+ The main JAR of the application; containing the main class + (specified as a path relative to the input path) + Either --module or --main-jar option can be specified but not + both. + --module -m [/
] + The main module (and optionally main class) of the application + This module must be located on the module path. + When this option is specified, the main module will be linked + in the Java runtime image. Either --module or --main-jar + option can be specified but not both. + +Options for creating the application package: + --about-url + URL of the application's home page + --app-image + Location of the predefined application image that is used + to build an installable package + (absolute path or relative to the current directory) + --file-associations + Path to a Properties file that contains list of key, value pairs + (absolute path or relative to the current directory) + The keys "extension", "mime-type", "icon", and "description" + can be used to describe the association. + This option can be used multiple times. + --install-dir + Absolute path of the installation directory of the application + --license-file + Path to the license file + (absolute path or relative to the current directory) + --resource-dir + Path to override jpackage resources + Icons, template files, and other resources of jpackage can be + over-ridden by adding replacement resources to this directory. + (absolute path or relative to the current directory) + --runtime-image + Path of the predefined runtime image to install + (absolute path or relative to the current directory) + Option is required when creating a runtime package. + --launcher-as-service + Request to create an installer that will register the main + application launcher as a background service-type application. + +Platform dependent options for creating the application package: + --linux-package-name + Name for Linux package, defaults to the application name + --linux-deb-maintainer + Maintainer for .deb package + --linux-menu-group + Menu group this application is placed in + --linux-package-deps + Required packages or capabilities for the application + --linux-rpm-license-type + Type of the license ("License: " of the RPM .spec) + --linux-app-release + Release value of the RPM .spec file or + Debian revision value of the DEB control file + --linux-app-category + Group value of the RPM .spec file or + Section value of DEB control file + --linux-shortcut + Creates a shortcut for the application. diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt new file mode 100644 index 0000000000000..801d72636b4d8 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt @@ -0,0 +1,237 @@ +Usage: jpackage + +Sample usages: +-------------- + Generate an application package suitable for the host system: + For a modular application: + jpackage -n name -p modulePath -m moduleName/className + For a non-modular application: + jpackage -i inputDir -n name \ + --main-class className --main-jar myJar.jar + From a pre-built application image: + jpackage -n name --app-image appImageDir + Generate an application image: + For a modular application: + jpackage --type app-image -n name -p modulePath \ + -m moduleName/className + For a non-modular application: + jpackage --type app-image -i inputDir -n name \ + --main-class className --main-jar myJar.jar + To provide your own options to jlink, run jlink separately: + jlink --output appRuntimeImage -p modulePath \ + --add-modules moduleName \ + --no-header-files [...] + jpackage --type app-image -n name \ + -m moduleName/className --runtime-image appRuntimeImage + Generate a Java runtime package: + jpackage -n name --runtime-image + Sign the predefined application image: + jpackage --type app-image --app-image \ + --mac-sign [...] + Note: the only additional options that are permitted in this mode are: + the set of additional mac signing options and --verbose + +Generic Options: + @ + Read options and/or mode from a file + This option can be used multiple times. + --type -t + The type of package to create + Valid values are: {"app-image", "dmg", "pkg"} + If this option is not specified a platform dependent + default type will be created. + --app-version + Version of the application and/or package + --copyright + Copyright for the application + --description + Description of the application + --help -h + Print the usage text with a list and description of each valid + option for the current platform to the output stream, and exit + --icon + Path of the icon of the application package + (absolute path or relative to the current directory) + --name -n + Name of the application and/or package + --dest -d + Path where generated output file is placed + (absolute path or relative to the current directory) + Defaults to the current working directory. + --temp + Path of a new or empty directory used to create temporary files + (absolute path or relative to the current directory) + If specified, the temp dir will not be removed upon the task + completion and must be removed manually. + If not specified, a temporary directory will be created and + removed upon the task completion. + --vendor + Vendor of the application + --verbose + Enables verbose output + --version + Print the product version to the output stream and exit. + +Options for creating the runtime image: + --add-modules [,...] + A comma (",") separated list of modules to add + This module list, along with the main module (if specified) + will be passed to jlink as the --add-module argument. + If not specified, either just the main module (if --module is + specified), or the default set of modules (if --main-jar is + specified) are used. + This option can be used multiple times. + --module-path -p ... + A : separated list of paths + Each path is either a directory of modules or the path to a + modular jar. + (Each path is absolute or relative to the current directory.) + This option can be used multiple times. + --jlink-options + A space separated list of options to pass to jlink + If not specified, defaults to "--strip-native-commands + --strip-debug --no-man-pages --no-header-files". + This option can be used multiple times. + --runtime-image + Path of the predefined runtime image that will be copied into + the application image + (absolute path or relative to the current directory) + If --runtime-image is not specified, jpackage will run jlink to + create the runtime image using options: + --strip-debug, --no-header-files, --no-man-pages, and + --strip-native-commands. + +Options for creating the application image: + --input -i + Path of the input directory that contains the files to be packaged + (absolute path or relative to the current directory) + All files in the input directory will be packaged into the + application image. + --app-content [,...] + A comma separated list of paths to files and/or directories + to add to the application payload. + This option can be used more than once. + Note: The value should be a directory with the "Resources" + subdirectory (or any other directory that is valid in the "Contents" + directory of the application bundle). Otherwise, jpackage may produce + invalid application bundle which may fail code signing and/or + notarization. + +Options for creating the application launcher(s): + --add-launcher = + Name of launcher, and a path to a Properties file that contains + a list of key, value pairs + (absolute path or relative to the current directory) + The keys "module", "main-jar", "main-class", "description", + "arguments", "java-options", "app-version", "icon", + "launcher-as-service", + "win-console", "win-shortcut", "win-menu", + "linux-app-category", and "linux-shortcut" can be used. + These options are added to, or used to overwrite, the original + command line options to build an additional alternative launcher. + The main application launcher will be built from the command line + options. Additional alternative launchers can be built using + this option, and this option can be used multiple times to + build multiple additional launchers. + --arguments
+ Command line arguments to pass to the main class if no command + line arguments are given to the launcher + This option can be used multiple times. + --java-options + Options to pass to the Java runtime + This option can be used multiple times. + --main-class + Qualified name of the application main class to execute + This option can only be used if --main-jar is specified. + --main-jar
+ The main JAR of the application; containing the main class + (specified as a path relative to the input path) + Either --module or --main-jar option can be specified but not + both. + --module -m [/
] + The main module (and optionally main class) of the application + This module must be located on the module path. + When this option is specified, the main module will be linked + in the Java runtime image. Either --module or --main-jar + option can be specified but not both. + --mac-package-identifier + An identifier that uniquely identifies the application for macOS + Defaults to the main class name. + May only use alphanumeric (A-Z,a-z,0-9), hyphen (-), + and period (.) characters. + --mac-package-name + Name of the application as it appears in the Menu Bar + This can be different from the application name. + This name must be less than 16 characters long and be suitable for + displaying in the menu bar and the application Info window. + Defaults to the application name. + --mac-package-signing-prefix + When signing the application package, this value is prefixed + to all components that need to be signed that don't have + an existing package identifier. + --mac-sign + Request that the package or the predefined application image be + signed. + --mac-signing-keychain + Name of the keychain to search for the signing identity + If not specified, the standard keychains are used. + --mac-signing-key-user-name + Team or user name portion of Apple signing identities. For direct + control of the signing identity used to sign application images or + installers use --mac-app-image-sign-identity and/or + --mac-installer-sign-identity. This option cannot be combined with + --mac-app-image-sign-identity or --mac-installer-sign-identity. + --mac-app-image-sign-identity + Identity used to sign application image. This value will be passed + directly to --sign option of "codesign" tool. This option cannot + be combined with --mac-signing-key-user-name. + --mac-installer-sign-identity + Identity used to sign "pkg" installer. This value will be passed + directly to --sign option of "productbuild" tool. This option + cannot be combined with --mac-signing-key-user-name. + --mac-app-store + Indicates that the jpackage output is intended for the + Mac App Store. + --mac-entitlements + Path to file containing entitlements to use when signing + executables and libraries in the bundle. + --mac-app-category + String used to construct LSApplicationCategoryType in + application plist. The default value is "utilities". + +Options for creating the application package: + --about-url + URL of the application's home page + --app-image + Location of the predefined application image that is used + to build an installable package or to sign the predefined + application image + (absolute path or relative to the current directory) + --file-associations + Path to a Properties file that contains list of key, value pairs + (absolute path or relative to the current directory) + The keys "extension", "mime-type", "icon", and "description" + can be used to describe the association. + This option can be used multiple times. + --install-dir + Absolute path of the installation directory of the application + --license-file + Path to the license file + (absolute path or relative to the current directory) + --resource-dir + Path to override jpackage resources + Icons, template files, and other resources of jpackage can be + over-ridden by adding replacement resources to this directory. + (absolute path or relative to the current directory) + --runtime-image + Path of the predefined runtime image to install + (absolute path or relative to the current directory) + Option is required when creating a runtime package. + --launcher-as-service + Request to create an installer that will register the main + application launcher as a background service-type application. + +Platform dependent options for creating the application package: + --mac-dmg-content [,...] + Include all the referenced content in the dmg. + This option can be used multiple times. diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt new file mode 100644 index 0000000000000..e104fe5435045 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt @@ -0,0 +1,204 @@ +Usage: jpackage + +Sample usages: +-------------- + Generate an application package suitable for the host system: + For a modular application: + jpackage -n name -p modulePath -m moduleName/className + For a non-modular application: + jpackage -i inputDir -n name \ + --main-class className --main-jar myJar.jar + From a pre-built application image: + jpackage -n name --app-image appImageDir + Generate an application image: + For a modular application: + jpackage --type app-image -n name -p modulePath \ + -m moduleName/className + For a non-modular application: + jpackage --type app-image -i inputDir -n name \ + --main-class className --main-jar myJar.jar + To provide your own options to jlink, run jlink separately: + jlink --output appRuntimeImage -p modulePath \ + --add-modules moduleName \ + --no-header-files [...] + jpackage --type app-image -n name \ + -m moduleName/className --runtime-image appRuntimeImage + Generate a Java runtime package: + jpackage -n name --runtime-image + +Generic Options: + @ + Read options and/or mode from a file + This option can be used multiple times. + --type -t + The type of package to create + Valid values are: {"app-image", "exe", "msi"} + If this option is not specified a platform dependent + default type will be created. + --app-version + Version of the application and/or package + --copyright + Copyright for the application + --description + Description of the application + --help -h + Print the usage text with a list and description of each valid + option for the current platform to the output stream, and exit + --icon + Path of the icon of the application package + (absolute path or relative to the current directory) + --name -n + Name of the application and/or package + --dest -d + Path where generated output file is placed + (absolute path or relative to the current directory) + Defaults to the current working directory. + --temp + Path of a new or empty directory used to create temporary files + (absolute path or relative to the current directory) + If specified, the temp dir will not be removed upon the task + completion and must be removed manually. + If not specified, a temporary directory will be created and + removed upon the task completion. + --vendor + Vendor of the application + --verbose + Enables verbose output + --version + Print the product version to the output stream and exit. + +Options for creating the runtime image: + --add-modules [,...] + A comma (",") separated list of modules to add + This module list, along with the main module (if specified) + will be passed to jlink as the --add-module argument. + If not specified, either just the main module (if --module is + specified), or the default set of modules (if --main-jar is + specified) are used. + This option can be used multiple times. + --module-path -p ... + A ; separated list of paths + Each path is either a directory of modules or the path to a + modular jar. + (Each path is absolute or relative to the current directory.) + This option can be used multiple times. + --jlink-options + A space separated list of options to pass to jlink + If not specified, defaults to "--strip-native-commands + --strip-debug --no-man-pages --no-header-files". + This option can be used multiple times. + --runtime-image + Path of the predefined runtime image that will be copied into + the application image + (absolute path or relative to the current directory) + If --runtime-image is not specified, jpackage will run jlink to + create the runtime image using options: + --strip-debug, --no-header-files, --no-man-pages, and + --strip-native-commands. + +Options for creating the application image: + --input -i + Path of the input directory that contains the files to be packaged + (absolute path or relative to the current directory) + All files in the input directory will be packaged into the + application image. + --app-content [,...] + A comma separated list of paths to files and/or directories + to add to the application payload. + This option can be used more than once. + +Options for creating the application launcher(s): + --add-launcher = + Name of launcher, and a path to a Properties file that contains + a list of key, value pairs + (absolute path or relative to the current directory) + The keys "module", "main-jar", "main-class", "description", + "arguments", "java-options", "app-version", "icon", + "launcher-as-service", + "win-console", "win-shortcut", "win-menu", + "linux-app-category", and "linux-shortcut" can be used. + These options are added to, or used to overwrite, the original + command line options to build an additional alternative launcher. + The main application launcher will be built from the command line + options. Additional alternative launchers can be built using + this option, and this option can be used multiple times to + build multiple additional launchers. + --arguments
+ Command line arguments to pass to the main class if no command + line arguments are given to the launcher + This option can be used multiple times. + --java-options + Options to pass to the Java runtime + This option can be used multiple times. + --main-class + Qualified name of the application main class to execute + This option can only be used if --main-jar is specified. + --main-jar
+ The main JAR of the application; containing the main class + (specified as a path relative to the input path) + Either --module or --main-jar option can be specified but not + both. + --module -m [/
] + The main module (and optionally main class) of the application + This module must be located on the module path. + When this option is specified, the main module will be linked + in the Java runtime image. Either --module or --main-jar + option can be specified but not both. + +Platform dependent option for creating the application launcher: + --win-console + Creates a console launcher for the application, should be + specified for application which requires console interactions + +Options for creating the application package: + --about-url + URL of the application's home page + --app-image + Location of the predefined application image that is used + to build an installable package + (absolute path or relative to the current directory) + --file-associations + Path to a Properties file that contains list of key, value pairs + (absolute path or relative to the current directory) + The keys "extension", "mime-type", "icon", and "description" + can be used to describe the association. + This option can be used multiple times. + --install-dir + Relative sub-path under the default installation location + --license-file + Path to the license file + (absolute path or relative to the current directory) + --resource-dir + Path to override jpackage resources + Icons, template files, and other resources of jpackage can be + over-ridden by adding replacement resources to this directory. + (absolute path or relative to the current directory) + --runtime-image + Path of the predefined runtime image to install + (absolute path or relative to the current directory) + Option is required when creating a runtime package. + --launcher-as-service + Request to create an installer that will register the main + application launcher as a background service-type application. + +Platform dependent options for creating the application package: + --win-dir-chooser + Adds a dialog to enable the user to choose a directory in which + the product is installed. + --win-help-url + URL where user can obtain further information or technical support + --win-menu + Request to add a Start menu shortcut for this application + --win-menu-group + Start Menu group this application is placed in + --win-per-user-install + Request to perform an install on a per-user basis + --win-shortcut + Request to add desktop shortcut for this application + --win-shortcut-prompt + Adds a dialog to enable the user to choose if shortcuts + will be created by installer. + --win-update-url + URL of available application update information + --win-upgrade-uuid + UUID associated with upgrades for this package From e7a70bbbadf266090b61f4ff10ecf474be00f9d4 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 13 Oct 2025 12:31:11 -0400 Subject: [PATCH 12/39] the cli: old help output reordered and trailing whitespace trimmed --- .../jdk/jpackage/internal/cli/help-linux.txt | 62 ++++---- .../jdk/jpackage/internal/cli/help-macos.txt | 138 +++++++++--------- .../jpackage/internal/cli/help-windows.txt | 46 +++--- 3 files changed, 123 insertions(+), 123 deletions(-) diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt index 802082b1d2f3d..ddf772de3180e 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt @@ -27,12 +27,12 @@ Sample usages: jpackage -n name --runtime-image Generic Options: - @ - Read options and/or mode from a file + @ + Read options and/or mode from a file This option can be used multiple times. - --type -t + --type -t The type of package to create - Valid values are: {"app-image", "rpm", "deb"} + Valid values are: {"app-image", "rpm", "deb"} If this option is not specified a platform dependent default type will be created. --app-version @@ -41,7 +41,11 @@ Generic Options: Copyright for the application --description Description of the application - --help -h + --dest -d + Path where generated output file is placed + (absolute path or relative to the current directory) + Defaults to the current working directory. + --help -h Print the usage text with a list and description of each valid option for the current platform to the output stream, and exit --icon @@ -49,10 +53,6 @@ Generic Options: (absolute path or relative to the current directory) --name -n Name of the application and/or package - --dest -d - Path where generated output file is placed - (absolute path or relative to the current directory) - Defaults to the current working directory. --temp Path of a new or empty directory used to create temporary files (absolute path or relative to the current directory) @@ -73,20 +73,20 @@ Options for creating the runtime image: This module list, along with the main module (if specified) will be passed to jlink as the --add-module argument. If not specified, either just the main module (if --module is - specified), or the default set of modules (if --main-jar is + specified), or the default set of modules (if --main-jar is specified) are used. This option can be used multiple times. + --jlink-options + A space separated list of options to pass to jlink + If not specified, defaults to "--strip-native-commands + --strip-debug --no-man-pages --no-header-files". + This option can be used multiple times. --module-path -p ... A : separated list of paths Each path is either a directory of modules or the path to a modular jar. (Each path is absolute or relative to the current directory.) This option can be used multiple times. - --jlink-options - A space separated list of options to pass to jlink - If not specified, defaults to "--strip-native-commands - --strip-debug --no-man-pages --no-header-files". - This option can be used multiple times. --runtime-image Path of the predefined runtime image that will be copied into the application image @@ -97,15 +97,15 @@ Options for creating the runtime image: --strip-native-commands. Options for creating the application image: + --app-content [,...] + A comma separated list of paths to files and/or directories + to add to the application payload. + This option can be used more than once. --input -i Path of the input directory that contains the files to be packaged (absolute path or relative to the current directory) All files in the input directory will be packaged into the application image. - --app-content [,...] - A comma separated list of paths to files and/or directories - to add to the application payload. - This option can be used more than once. Options for creating the application launcher(s): --add-launcher = @@ -122,7 +122,7 @@ Options for creating the application launcher(s): The main application launcher will be built from the command line options. Additional alternative launchers can be built using this option, and this option can be used multiple times to - build multiple additional launchers. + build multiple additional launchers. --arguments
Command line arguments to pass to the main class if no command line arguments are given to the launcher @@ -160,6 +160,9 @@ Options for creating the application package: This option can be used multiple times. --install-dir Absolute path of the installation directory of the application + --launcher-as-service + Request to create an installer that will register the main + application launcher as a background service-type application. --license-file Path to the license file (absolute path or relative to the current directory) @@ -172,26 +175,23 @@ Options for creating the application package: Path of the predefined runtime image to install (absolute path or relative to the current directory) Option is required when creating a runtime package. - --launcher-as-service - Request to create an installer that will register the main - application launcher as a background service-type application. Platform dependent options for creating the application package: - --linux-package-name - Name for Linux package, defaults to the application name + --linux-app-category + Group value of the RPM .spec file or + Section value of DEB control file + --linux-app-release + Release value of the RPM .spec file or + Debian revision value of the DEB control file --linux-deb-maintainer Maintainer for .deb package --linux-menu-group Menu group this application is placed in --linux-package-deps Required packages or capabilities for the application + --linux-package-name + Name for Linux package, defaults to the application name --linux-rpm-license-type Type of the license ("License: " of the RPM .spec) - --linux-app-release - Release value of the RPM .spec file or - Debian revision value of the DEB control file - --linux-app-category - Group value of the RPM .spec file or - Section value of DEB control file --linux-shortcut Creates a shortcut for the application. diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt index 801d72636b4d8..88cd3f2818cf8 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt @@ -32,12 +32,12 @@ Sample usages: the set of additional mac signing options and --verbose Generic Options: - @ - Read options and/or mode from a file + @ + Read options and/or mode from a file This option can be used multiple times. - --type -t + --type -t The type of package to create - Valid values are: {"app-image", "dmg", "pkg"} + Valid values are: {"app-image", "dmg", "pkg"} If this option is not specified a platform dependent default type will be created. --app-version @@ -46,7 +46,11 @@ Generic Options: Copyright for the application --description Description of the application - --help -h + --dest -d + Path where generated output file is placed + (absolute path or relative to the current directory) + Defaults to the current working directory. + --help -h Print the usage text with a list and description of each valid option for the current platform to the output stream, and exit --icon @@ -54,10 +58,6 @@ Generic Options: (absolute path or relative to the current directory) --name -n Name of the application and/or package - --dest -d - Path where generated output file is placed - (absolute path or relative to the current directory) - Defaults to the current working directory. --temp Path of a new or empty directory used to create temporary files (absolute path or relative to the current directory) @@ -78,20 +78,20 @@ Options for creating the runtime image: This module list, along with the main module (if specified) will be passed to jlink as the --add-module argument. If not specified, either just the main module (if --module is - specified), or the default set of modules (if --main-jar is + specified), or the default set of modules (if --main-jar is specified) are used. This option can be used multiple times. + --jlink-options + A space separated list of options to pass to jlink + If not specified, defaults to "--strip-native-commands + --strip-debug --no-man-pages --no-header-files". + This option can be used multiple times. --module-path -p ... A : separated list of paths Each path is either a directory of modules or the path to a modular jar. (Each path is absolute or relative to the current directory.) This option can be used multiple times. - --jlink-options - A space separated list of options to pass to jlink - If not specified, defaults to "--strip-native-commands - --strip-debug --no-man-pages --no-header-files". - This option can be used multiple times. --runtime-image Path of the predefined runtime image that will be copied into the application image @@ -102,11 +102,6 @@ Options for creating the runtime image: --strip-native-commands. Options for creating the application image: - --input -i - Path of the input directory that contains the files to be packaged - (absolute path or relative to the current directory) - All files in the input directory will be packaged into the - application image. --app-content [,...] A comma separated list of paths to files and/or directories to add to the application payload. @@ -116,6 +111,11 @@ Options for creating the application image: directory of the application bundle). Otherwise, jpackage may produce invalid application bundle which may fail code signing and/or notarization. + --input -i + Path of the input directory that contains the files to be packaged + (absolute path or relative to the current directory) + All files in the input directory will be packaged into the + application image. Options for creating the application launcher(s): --add-launcher = @@ -132,7 +132,7 @@ Options for creating the application launcher(s): The main application launcher will be built from the command line options. Additional alternative launchers can be built using this option, and this option can be used multiple times to - build multiple additional launchers. + build multiple additional launchers. --arguments
Command line arguments to pass to the main class if no command line arguments are given to the launcher @@ -154,50 +154,6 @@ Options for creating the application launcher(s): When this option is specified, the main module will be linked in the Java runtime image. Either --module or --main-jar option can be specified but not both. - --mac-package-identifier - An identifier that uniquely identifies the application for macOS - Defaults to the main class name. - May only use alphanumeric (A-Z,a-z,0-9), hyphen (-), - and period (.) characters. - --mac-package-name - Name of the application as it appears in the Menu Bar - This can be different from the application name. - This name must be less than 16 characters long and be suitable for - displaying in the menu bar and the application Info window. - Defaults to the application name. - --mac-package-signing-prefix - When signing the application package, this value is prefixed - to all components that need to be signed that don't have - an existing package identifier. - --mac-sign - Request that the package or the predefined application image be - signed. - --mac-signing-keychain - Name of the keychain to search for the signing identity - If not specified, the standard keychains are used. - --mac-signing-key-user-name - Team or user name portion of Apple signing identities. For direct - control of the signing identity used to sign application images or - installers use --mac-app-image-sign-identity and/or - --mac-installer-sign-identity. This option cannot be combined with - --mac-app-image-sign-identity or --mac-installer-sign-identity. - --mac-app-image-sign-identity - Identity used to sign application image. This value will be passed - directly to --sign option of "codesign" tool. This option cannot - be combined with --mac-signing-key-user-name. - --mac-installer-sign-identity - Identity used to sign "pkg" installer. This value will be passed - directly to --sign option of "productbuild" tool. This option - cannot be combined with --mac-signing-key-user-name. - --mac-app-store - Indicates that the jpackage output is intended for the - Mac App Store. - --mac-entitlements - Path to file containing entitlements to use when signing - executables and libraries in the bundle. - --mac-app-category - String used to construct LSApplicationCategoryType in - application plist. The default value is "utilities". Options for creating the application package: --about-url @@ -215,6 +171,9 @@ Options for creating the application package: This option can be used multiple times. --install-dir Absolute path of the installation directory of the application + --launcher-as-service + Request to create an installer that will register the main + application launcher as a background service-type application. --license-file Path to the license file (absolute path or relative to the current directory) @@ -227,11 +186,52 @@ Options for creating the application package: Path of the predefined runtime image to install (absolute path or relative to the current directory) Option is required when creating a runtime package. - --launcher-as-service - Request to create an installer that will register the main - application launcher as a background service-type application. Platform dependent options for creating the application package: + --mac-app-category + String used to construct LSApplicationCategoryType in + application plist. The default value is "utilities". + --mac-app-image-sign-identity + Identity used to sign application image. This value will be passed + directly to --sign option of "codesign" tool. This option cannot + be combined with --mac-signing-key-user-name. + --mac-app-store + Indicates that the jpackage output is intended for the + Mac App Store. --mac-dmg-content [,...] Include all the referenced content in the dmg. - This option can be used multiple times. + This option can be used multiple times. + --mac-entitlements + Path to file containing entitlements to use when signing + executables and libraries in the bundle. + --mac-installer-sign-identity + Identity used to sign "pkg" installer. This value will be passed + directly to --sign option of "productbuild" tool. This option + cannot be combined with --mac-signing-key-user-name. + --mac-package-identifier + An identifier that uniquely identifies the application for macOS + Defaults to the main class name. + May only use alphanumeric (A-Z,a-z,0-9), hyphen (-), + and period (.) characters. + --mac-package-name + Name of the application as it appears in the Menu Bar + This can be different from the application name. + This name must be less than 16 characters long and be suitable for + displaying in the menu bar and the application Info window. + Defaults to the application name. + --mac-package-signing-prefix + When signing the application package, this value is prefixed + to all components that need to be signed that don't have + an existing package identifier. + --mac-sign + Request that the package or the predefined application image be + signed. + --mac-signing-key-user-name + Team or user name portion of Apple signing identities. For direct + control of the signing identity used to sign application images or + installers use --mac-app-image-sign-identity and/or + --mac-installer-sign-identity. This option cannot be combined with + --mac-app-image-sign-identity or --mac-installer-sign-identity. + --mac-signing-keychain + Name of the keychain to search for the signing identity + If not specified, the standard keychains are used. diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt index e104fe5435045..830c0dd5ac855 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt @@ -27,12 +27,12 @@ Sample usages: jpackage -n name --runtime-image Generic Options: - @ - Read options and/or mode from a file + @ + Read options and/or mode from a file This option can be used multiple times. - --type -t + --type -t The type of package to create - Valid values are: {"app-image", "exe", "msi"} + Valid values are: {"app-image", "exe", "msi"} If this option is not specified a platform dependent default type will be created. --app-version @@ -41,7 +41,11 @@ Generic Options: Copyright for the application --description Description of the application - --help -h + --dest -d + Path where generated output file is placed + (absolute path or relative to the current directory) + Defaults to the current working directory. + --help -h Print the usage text with a list and description of each valid option for the current platform to the output stream, and exit --icon @@ -49,10 +53,6 @@ Generic Options: (absolute path or relative to the current directory) --name -n Name of the application and/or package - --dest -d - Path where generated output file is placed - (absolute path or relative to the current directory) - Defaults to the current working directory. --temp Path of a new or empty directory used to create temporary files (absolute path or relative to the current directory) @@ -73,20 +73,20 @@ Options for creating the runtime image: This module list, along with the main module (if specified) will be passed to jlink as the --add-module argument. If not specified, either just the main module (if --module is - specified), or the default set of modules (if --main-jar is + specified), or the default set of modules (if --main-jar is specified) are used. This option can be used multiple times. + --jlink-options + A space separated list of options to pass to jlink + If not specified, defaults to "--strip-native-commands + --strip-debug --no-man-pages --no-header-files". + This option can be used multiple times. --module-path -p ... A ; separated list of paths Each path is either a directory of modules or the path to a modular jar. (Each path is absolute or relative to the current directory.) This option can be used multiple times. - --jlink-options - A space separated list of options to pass to jlink - If not specified, defaults to "--strip-native-commands - --strip-debug --no-man-pages --no-header-files". - This option can be used multiple times. --runtime-image Path of the predefined runtime image that will be copied into the application image @@ -97,15 +97,15 @@ Options for creating the runtime image: --strip-native-commands. Options for creating the application image: + --app-content [,...] + A comma separated list of paths to files and/or directories + to add to the application payload. + This option can be used more than once. --input -i Path of the input directory that contains the files to be packaged (absolute path or relative to the current directory) All files in the input directory will be packaged into the application image. - --app-content [,...] - A comma separated list of paths to files and/or directories - to add to the application payload. - This option can be used more than once. Options for creating the application launcher(s): --add-launcher = @@ -122,7 +122,7 @@ Options for creating the application launcher(s): The main application launcher will be built from the command line options. Additional alternative launchers can be built using this option, and this option can be used multiple times to - build multiple additional launchers. + build multiple additional launchers. --arguments
Command line arguments to pass to the main class if no command line arguments are given to the launcher @@ -165,6 +165,9 @@ Options for creating the application package: This option can be used multiple times. --install-dir Relative sub-path under the default installation location + --launcher-as-service + Request to create an installer that will register the main + application launcher as a background service-type application. --license-file Path to the license file (absolute path or relative to the current directory) @@ -177,9 +180,6 @@ Options for creating the application package: Path of the predefined runtime image to install (absolute path or relative to the current directory) Option is required when creating a runtime package. - --launcher-as-service - Request to create an installer that will register the main - application launcher as a background service-type application. Platform dependent options for creating the application package: --win-dir-chooser From 22373225f85bd14a2c1f12e2ccfea4860ad0274f Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 13 Oct 2025 12:47:57 -0400 Subject: [PATCH 13/39] the cli: new help output --- .../jdk/jpackage/internal/cli/help-linux.txt | 34 +++++++++---------- .../jdk/jpackage/internal/cli/help-macos.txt | 31 +++++++++-------- .../jpackage/internal/cli/help-windows.txt | 27 ++++++++------- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt index ddf772de3180e..f3b57063b9d5d 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt @@ -10,6 +10,7 @@ Sample usages: --main-class className --main-jar myJar.jar From a pre-built application image: jpackage -n name --app-image appImageDir + Generate an application image: For a modular application: jpackage --type app-image -n name -p modulePath \ @@ -23,6 +24,7 @@ Sample usages: --no-header-files [...] jpackage --type app-image -n name \ -m moduleName/className --runtime-image appRuntimeImage + Generate a Java runtime package: jpackage -n name --runtime-image @@ -32,7 +34,7 @@ Generic Options: This option can be used multiple times. --type -t The type of package to create - Valid values are: {"app-image", "rpm", "deb"} + Valid values are: {"app-image", "deb", "rpm"} If this option is not specified a platform dependent default type will be created. --app-version @@ -41,11 +43,11 @@ Generic Options: Copyright for the application --description Description of the application - --dest -d + --dest -d Path where generated output file is placed (absolute path or relative to the current directory) Defaults to the current working directory. - --help -h + --help -h -? Print the usage text with a list and description of each valid option for the current platform to the output stream, and exit --icon @@ -81,7 +83,7 @@ Options for creating the runtime image: If not specified, defaults to "--strip-native-commands --strip-debug --no-man-pages --no-header-files". This option can be used multiple times. - --module-path -p ... + --module-path -p [:...] A : separated list of paths Each path is either a directory of modules or the path to a modular jar. @@ -112,11 +114,9 @@ Options for creating the application launcher(s): Name of launcher, and a path to a Properties file that contains a list of key, value pairs (absolute path or relative to the current directory) - The keys "module", "main-jar", "main-class", "description", - "arguments", "java-options", "app-version", "icon", - "launcher-as-service", - "win-console", "win-shortcut", "win-menu", - "linux-app-category", and "linux-shortcut" can be used. + The keys "arguments", "description", "icon", "java-options", + "launcher-as-service", "linux-shortcut", "main-class", "main-jar", + "module", "win-console", "win-menu", and "win-shortcut" can be used. These options are added to, or used to overwrite, the original command line options to build an additional alternative launcher. The main application launcher will be built from the command line @@ -152,7 +152,7 @@ Options for creating the application package: Location of the predefined application image that is used to build an installable package (absolute path or relative to the current directory) - --file-associations + --file-associations [:...] Path to a Properties file that contains list of key, value pairs (absolute path or relative to the current directory) The keys "extension", "mime-type", "icon", and "description" @@ -177,21 +177,21 @@ Options for creating the application package: Option is required when creating a runtime package. Platform dependent options for creating the application package: - --linux-app-category + --linux-app-category Group value of the RPM .spec file or Section value of DEB control file - --linux-app-release + --linux-app-release Release value of the RPM .spec file or Debian revision value of the DEB control file --linux-deb-maintainer Maintainer for .deb package - --linux-menu-group + --linux-menu-group Menu group this application is placed in - --linux-package-deps + --linux-package-deps Required packages or capabilities for the application - --linux-package-name + --linux-package-name Name for Linux package, defaults to the application name - --linux-rpm-license-type + --linux-rpm-license-type Type of the license ("License: " of the RPM .spec) - --linux-shortcut + --linux-shortcut Creates a shortcut for the application. diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt index 88cd3f2818cf8..9737c4ac9acb2 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt @@ -10,6 +10,7 @@ Sample usages: --main-class className --main-jar myJar.jar From a pre-built application image: jpackage -n name --app-image appImageDir + Generate an application image: For a modular application: jpackage --type app-image -n name -p modulePath \ @@ -23,8 +24,10 @@ Sample usages: --no-header-files [...] jpackage --type app-image -n name \ -m moduleName/className --runtime-image appRuntimeImage + Generate a Java runtime package: jpackage -n name --runtime-image + Sign the predefined application image: jpackage --type app-image --app-image \ --mac-sign [...] @@ -46,11 +49,11 @@ Generic Options: Copyright for the application --description Description of the application - --dest -d + --dest -d Path where generated output file is placed (absolute path or relative to the current directory) Defaults to the current working directory. - --help -h + --help -h -? Print the usage text with a list and description of each valid option for the current platform to the output stream, and exit --icon @@ -86,7 +89,7 @@ Options for creating the runtime image: If not specified, defaults to "--strip-native-commands --strip-debug --no-man-pages --no-header-files". This option can be used multiple times. - --module-path -p ... + --module-path -p [:...] A : separated list of paths Each path is either a directory of modules or the path to a modular jar. @@ -122,11 +125,9 @@ Options for creating the application launcher(s): Name of launcher, and a path to a Properties file that contains a list of key, value pairs (absolute path or relative to the current directory) - The keys "module", "main-jar", "main-class", "description", - "arguments", "java-options", "app-version", "icon", - "launcher-as-service", - "win-console", "win-shortcut", "win-menu", - "linux-app-category", and "linux-shortcut" can be used. + The keys "arguments", "description", "icon", "java-options", + "launcher-as-service", "linux-shortcut", "main-class", "main-jar", + "module", "win-console", "win-menu", and "win-shortcut" can be used. These options are added to, or used to overwrite, the original command line options to build an additional alternative launcher. The main application launcher will be built from the command line @@ -163,7 +164,7 @@ Options for creating the application package: to build an installable package or to sign the predefined application image (absolute path or relative to the current directory) - --file-associations + --file-associations [:...] Path to a Properties file that contains list of key, value pairs (absolute path or relative to the current directory) The keys "extension", "mime-type", "icon", and "description" @@ -188,7 +189,7 @@ Options for creating the application package: Option is required when creating a runtime package. Platform dependent options for creating the application package: - --mac-app-category + --mac-app-category String used to construct LSApplicationCategoryType in application plist. The default value is "utilities". --mac-app-image-sign-identity @@ -208,25 +209,25 @@ Platform dependent options for creating the application package: Identity used to sign "pkg" installer. This value will be passed directly to --sign option of "productbuild" tool. This option cannot be combined with --mac-signing-key-user-name. - --mac-package-identifier + --mac-package-identifier An identifier that uniquely identifies the application for macOS Defaults to the main class name. May only use alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.) characters. - --mac-package-name + --mac-package-name Name of the application as it appears in the Menu Bar This can be different from the application name. This name must be less than 16 characters long and be suitable for displaying in the menu bar and the application Info window. Defaults to the application name. - --mac-package-signing-prefix + --mac-package-signing-prefix When signing the application package, this value is prefixed to all components that need to be signed that don't have an existing package identifier. - --mac-sign + --mac-sign -s Request that the package or the predefined application image be signed. - --mac-signing-key-user-name + --mac-signing-key-user-name Team or user name portion of Apple signing identities. For direct control of the signing identity used to sign application images or installers use --mac-app-image-sign-identity and/or diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt index 830c0dd5ac855..d564f757ae5ea 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt @@ -10,6 +10,7 @@ Sample usages: --main-class className --main-jar myJar.jar From a pre-built application image: jpackage -n name --app-image appImageDir + Generate an application image: For a modular application: jpackage --type app-image -n name -p modulePath \ @@ -23,6 +24,7 @@ Sample usages: --no-header-files [...] jpackage --type app-image -n name \ -m moduleName/className --runtime-image appRuntimeImage + Generate a Java runtime package: jpackage -n name --runtime-image @@ -41,11 +43,11 @@ Generic Options: Copyright for the application --description Description of the application - --dest -d + --dest -d Path where generated output file is placed (absolute path or relative to the current directory) Defaults to the current working directory. - --help -h + --help -h -? Print the usage text with a list and description of each valid option for the current platform to the output stream, and exit --icon @@ -81,7 +83,7 @@ Options for creating the runtime image: If not specified, defaults to "--strip-native-commands --strip-debug --no-man-pages --no-header-files". This option can be used multiple times. - --module-path -p ... + --module-path -p [;...] A ; separated list of paths Each path is either a directory of modules or the path to a modular jar. @@ -112,11 +114,9 @@ Options for creating the application launcher(s): Name of launcher, and a path to a Properties file that contains a list of key, value pairs (absolute path or relative to the current directory) - The keys "module", "main-jar", "main-class", "description", - "arguments", "java-options", "app-version", "icon", - "launcher-as-service", - "win-console", "win-shortcut", "win-menu", - "linux-app-category", and "linux-shortcut" can be used. + The keys "arguments", "description", "icon", "java-options", + "launcher-as-service", "linux-shortcut", "main-class", "main-jar", + "module", "win-console", "win-menu", and "win-shortcut" can be used. These options are added to, or used to overwrite, the original command line options to build an additional alternative launcher. The main application launcher will be built from the command line @@ -157,14 +157,15 @@ Options for creating the application package: Location of the predefined application image that is used to build an installable package (absolute path or relative to the current directory) - --file-associations + --file-associations [;...] Path to a Properties file that contains list of key, value pairs (absolute path or relative to the current directory) The keys "extension", "mime-type", "icon", and "description" can be used to describe the association. This option can be used multiple times. --install-dir - Relative sub-path under the default installation location + Relative sub-path of the installation location of + the application such as "Program Files" or "AppData". --launcher-as-service Request to create an installer that will register the main application launcher as a background service-type application. @@ -187,18 +188,18 @@ Platform dependent options for creating the application package: the product is installed. --win-help-url URL where user can obtain further information or technical support - --win-menu + --win-menu Request to add a Start menu shortcut for this application --win-menu-group Start Menu group this application is placed in --win-per-user-install Request to perform an install on a per-user basis - --win-shortcut + --win-shortcut Request to add desktop shortcut for this application --win-shortcut-prompt Adds a dialog to enable the user to choose if shortcuts will be created by installer. --win-update-url URL of available application update information - --win-upgrade-uuid + --win-upgrade-uuid UUID associated with upgrades for this package From 5a47732bf1f99777aa23658f1cedbf1a2ededa3d Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Tue, 14 Oct 2025 18:08:00 -0400 Subject: [PATCH 14/39] Add the "cli". --- .../internal/cli/AdditionalLauncher.java | 32 + .../cli/BundlingOperationModifier.java | 42 + .../cli/BundlingOperationOptionScope.java | 38 + .../internal/cli/CliBundlingEnvironment.java | 47 + .../jpackage/internal/cli/HelpFormatter.java | 179 +++ .../jdk/jpackage/internal/cli/I18N.java | 55 + .../cli/JOptSimpleOptionsBuilder.java | 903 +++++++++++ .../jdk/jpackage/internal/cli/Main.java | 193 +++ .../internal/cli/MessageFormatUtils.java | 79 + .../jdk/jpackage/internal/cli/Option.java | 99 ++ .../cli/OptionArrayValueConverter.java | 41 + .../internal/cli/OptionIdentifier.java | 56 + .../jdk/jpackage/internal/cli/OptionName.java | 68 + .../jpackage/internal/cli/OptionScope.java | 32 + .../jpackage/internal/cli/OptionSource.java | 69 + .../jdk/jpackage/internal/cli/OptionSpec.java | 171 +++ .../internal/cli/OptionSpecBuilder.java | 468 ++++++ .../cli/OptionSpecMapperOptionScope.java | 158 ++ .../jpackage/internal/cli/OptionValue.java | 192 +++ .../internal/cli/OptionValueConverter.java | 273 ++++ .../cli/OptionValueExceptionFactory.java | 183 +++ .../jdk/jpackage/internal/cli/Options.java | 275 ++++ .../internal/cli/OptionsAnalyzer.java | 430 ++++++ .../internal/cli/OptionsProcessor.java | 427 ++++++ .../cli/StandardAppImageFileOption.java | 194 +++ .../cli/StandardBundlingOperation.java | 173 +++ .../internal/cli/StandardFaOption.java | 110 ++ .../internal/cli/StandardHelpFormatter.java | 398 +++++ .../jpackage/internal/cli/StandardOption.java | 789 ++++++++++ .../internal/cli/StandardOptionContext.java | 68 + .../StandardOptionValueExceptionFactory.java | 79 + .../internal/cli/StandardValidator.java | 124 ++ .../internal/cli/StandardValueConverter.java | 83 + .../jpackage/internal/cli/StringToken.java | 57 + .../jdk/jpackage/internal/cli/Utils.java | 64 + .../jdk/jpackage/internal/cli/Validator.java | 265 ++++ .../jpackage/internal/cli/ValueConverter.java | 68 + .../resources/HelpResources.properties | 375 +++++ .../resources/MainResources.properties | 23 + test/jdk/tools/jpackage/junit/TEST.properties | 8 +- .../jdk/jpackage/internal/cli/HelpTest.java | 181 +++ .../cli/JOptSimpleOptionsBuilderTest.java | 1355 +++++++++++++++++ .../cli/MockupCliBundlingEnvironment.java | 160 ++ .../internal/cli/OptionIdentifierTest.java | 62 + .../jpackage/internal/cli/OptionNameTest.java | 152 ++ .../cli/OptionSpecMutatorOptionScopeTest.java | 144 ++ .../jpackage/internal/cli/OptionSpecTest.java | 341 +++++ .../jdk/jpackage/internal/cli/OptionTest.java | 55 + .../cli/OptionValueConverterTest.java | 216 +++ .../cli/OptionValueExceptionFactoryTest.java | 152 ++ .../internal/cli/OptionValueTest.java | 216 +++ .../internal/cli/OptionsProcessorTest.java | 786 ++++++++++ .../jpackage/internal/cli/OptionsTest.java | 254 +++ .../cli/OptionsValidationFailTest.excludes | 47 + .../cli/OptionsValidationFailTest.java | 244 +++ .../cli/StandardBundlingOperationTest.java | 103 ++ .../internal/cli/StandardOptionTest.java | 646 ++++++++ .../internal/cli/StandardValidatorTest.java | 197 +++ .../cli/StandardValueConverterTest.java | 161 ++ .../internal/cli/StringTokenTest.java | 49 + .../jdk/jpackage/internal/cli/TestUtils.java | 179 +++ .../jdk/jpackage/internal/cli/UtilsTest.java | 67 + .../jpackage/internal/cli/ValidatorTest.java | 284 ++++ .../jpackage/internal/cli/jpackage-options.md | 63 + 64 files changed, 13501 insertions(+), 1 deletion(-) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/AdditionalLauncher.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationModifier.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationOptionScope.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/CliBundlingEnvironment.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/HelpFormatter.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/I18N.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilder.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/MessageFormatUtils.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Option.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionArrayValueConverter.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionIdentifier.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionName.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionScope.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSource.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSpec.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSpecBuilder.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSpecMapperOptionScope.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionValue.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionValueConverter.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionValueExceptionFactory.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Options.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionsAnalyzer.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionsProcessor.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardAppImageFileOption.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardBundlingOperation.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardFaOption.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardHelpFormatter.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOptionContext.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOptionValueExceptionFactory.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValidator.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValueConverter.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StringToken.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Utils.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Validator.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/ValueConverter.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/HelpTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilderTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MockupCliBundlingEnvironment.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionIdentifierTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionNameTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionSpecMutatorOptionScopeTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionSpecTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionValueConverterTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionValueExceptionFactoryTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionValueTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsProcessorTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.excludes create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardBundlingOperationTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardValidatorTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardValueConverterTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StringTokenTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/TestUtils.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/UtilsTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/ValidatorTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/jpackage-options.md diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/AdditionalLauncher.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/AdditionalLauncher.java new file mode 100644 index 0000000000000..34019ff9e8178 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/AdditionalLauncher.java @@ -0,0 +1,32 @@ +/* + * 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.cli; + +import java.nio.file.Path; + + +record AdditionalLauncher(String name, Path propertyFile) { +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationModifier.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationModifier.java new file mode 100644 index 0000000000000..33525f3e54db2 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationModifier.java @@ -0,0 +1,42 @@ +/* + * 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.cli; + +/** + * Modifiers for jpackage operations. + */ +enum BundlingOperationModifier implements OptionScope { + /** + * Create runtime native bundle. + */ + BUNDLE_RUNTIME, + + /** + * Create native bundle from the predefined app image. + */ + BUNDLE_PREDEFINED_APP_IMAGE, + + ; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationOptionScope.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationOptionScope.java new file mode 100644 index 0000000000000..04af5865baa84 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/BundlingOperationOptionScope.java @@ -0,0 +1,38 @@ +/* + * 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.cli; + + +import jdk.jpackage.internal.model.BundlingOperationDescriptor; + +/** + * Bundling operation scope. + *

+ * The scope of bundling operations. E.g., app image or native package bundling. + */ +interface BundlingOperationOptionScope extends OptionScope { + BundlingOperationDescriptor descriptor(); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/CliBundlingEnvironment.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/CliBundlingEnvironment.java new file mode 100644 index 0000000000000..09ff0997c1466 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/CliBundlingEnvironment.java @@ -0,0 +1,47 @@ +/* + * 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.cli; + +import java.util.NoSuchElementException; +import jdk.jpackage.internal.model.BundlingEnvironment; +import jdk.jpackage.internal.model.BundlingOperationDescriptor; + +/** + * CLI bundling environment. + */ +public interface CliBundlingEnvironment extends BundlingEnvironment { + + /** + * Requests to run a bundling operation denoted with the given descriptor with + * the given values of command line options. + * + * @param op the descriptor of the requested bundling operation + * @param cmdline the validated values of the command line options + * @throws NoSuchElementException if the specified descriptor is not one of the + * items in the list returned by + * {@link #supportedOperations()} method + */ + void createBundle(BundlingOperationDescriptor op, Options cmdline); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/HelpFormatter.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/HelpFormatter.java new file mode 100644 index 0000000000000..004f227dc93fd --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/HelpFormatter.java @@ -0,0 +1,179 @@ +/* + * 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.cli; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +/** + * Generic help formatter. + */ +final class HelpFormatter { + + private HelpFormatter(List optionGroups, OptionGroupFormatter formatter) { + this.optionGroups = Objects.requireNonNull(optionGroups); + this.formatter = Objects.requireNonNull(formatter); + + } + + void format(Consumer sink) { + for (var group : optionGroups) { + formatter.format(group, sink); + } + } + + static Builder build() { + return new Builder(); + } + + + static final class Builder { + + private Builder() { + } + + HelpFormatter create() { + return new HelpFormatter(groups, validatedGroupFormatter()); + } + + Builder groups(Collection v) { + groups.addAll(v); + return this; + } + + Builder groups(OptionGroup... v) { + return groups(List.of(v)); + } + + Builder groupFormatter(OptionGroupFormatter v) { + groupFormatter = v; + return this; + } + + private OptionGroupFormatter validatedGroupFormatter() { + return Optional.ofNullable(groupFormatter).orElseGet(Builder::createConsoleFormatter); + } + + private static OptionGroupFormatter createConsoleFormatter() { + return new ConsoleOptionGroupFormatter(new ConsoleOptionFormatter(2, 10)); + } + + private final List groups = new ArrayList<>(); + private OptionGroupFormatter groupFormatter; + } + + + interface OptionFormatter { + + public default void format(OptionSpec optionSpec, Consumer sink) { + format(optionSpec.names().stream().map(OptionName::formatForCommandLine).collect(Collectors.joining(" ")), + optionSpec.valuePattern(), + optionSpec.description(), sink); + } + + void format(String optionNames, Optional valuePattern, String description, Consumer sink); + } + + interface OptionGroupFormatter { + + default void format(OptionGroup group, Consumer sink) { + formatHeader(group.name(), sink); + formatBody(group.options(), sink); + } + + void formatHeader(String gropName, Consumer sink); + + void formatBody(Iterable> optionSpecs, Consumer sink); + } + + + record ConsoleOptionFormatter(int nameOffset, int descriptionOffset) implements OptionFormatter { + + @Override + public void format(String optionNames, Optional valuePattern, String description, Consumer sink) { + sink.accept(" ".repeat(nameOffset)); + sink.accept(optionNames); + valuePattern.map(v -> " " + v).ifPresent(sink); + eol(sink); + final var descriptionOffsetStr = " ".repeat(descriptionOffset); + Stream.of(description.split("\\R")).map(line -> { + return descriptionOffsetStr + line; + }).forEach(line -> { + sink.accept(line); + eol(sink); + }); + } + } + + + record ConsoleOptionGroupFormatter(OptionFormatter optionFormatter) implements OptionGroupFormatter { + + ConsoleOptionGroupFormatter { + Objects.requireNonNull(optionFormatter); + } + + @Override + public void formatHeader(String groupName, Consumer sink) { + Objects.requireNonNull(groupName); + eol(sink); + sink.accept(groupName + ":"); + eol(sink); + } + + @Override + public void formatBody(Iterable> optionSpecs, Consumer sink) { + optionSpecs.forEach(optionSpec -> { + optionFormatter.format(optionSpec, sink); + }); + } + } + + + record OptionGroup(String name, List> options) { + + OptionGroup { + Objects.requireNonNull(name); + Objects.requireNonNull(options); + } + } + + + static Consumer eol(Consumer sink) { + sink.accept(System.lineSeparator()); + return sink; + } + + + private final List optionGroups; + private final OptionGroupFormatter formatter; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/I18N.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/I18N.java new file mode 100644 index 0000000000000..63c2b88039fae --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/I18N.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. 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.cli; + +import java.util.List; +import java.util.Map; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.util.MultiResourceBundle; +import jdk.jpackage.internal.util.StringBundle; + +final class I18N { + + private I18N() { + } + + static String format(String key, Object ... args) { + return BUNDLE.format(key, args); + } + + private static final StringBundle BUNDLE; + + static { + var prefix = "jdk.jpackage.internal.resources."; + BUNDLE = StringBundle.fromResourceBundle(MultiResourceBundle.create( + prefix + "MainResources", + Map.of( + OperatingSystem.LINUX, List.of(prefix + "LinuxResources"), + OperatingSystem.MACOS, List.of(prefix + "MacResources"), + OperatingSystem.WINDOWS, List.of(prefix + "WinResources") + ) + )); + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilder.java new file mode 100644 index 0000000000000..65c48ea359d7c --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilder.java @@ -0,0 +1,903 @@ +/* + * 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.cli; + +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; + +import java.lang.reflect.Array; +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.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import jdk.internal.joptsimple.ArgumentAcceptingOptionSpec; +import jdk.internal.joptsimple.OptionParser; +import jdk.internal.joptsimple.OptionSet; +import jdk.jpackage.internal.cli.OptionSpec.MergePolicy; +import jdk.jpackage.internal.util.Result; + + +/** + * Builds an instance of {@link Options} interface backed with joptsimple command + * line parser. + * + * Two types of command line argument processing are supported: + *

    + *
  1. Parse command line. Parsed data is stored as a map of strings. + *
  2. Convert strings to objects. Parsed data is stored as a map of objects. + *
+ */ +final class JOptSimpleOptionsBuilder { + + Function> create() { + return createJOptSimpleParser()::parse; + } + + JOptSimpleOptionsBuilder options(Collection