From 91dfca53adeca494a7df2e2b990e0cb9a749492e Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Tue, 28 Oct 2025 14:52:45 -0400 Subject: [PATCH 01/55] LinuxPackagingPipeline, WinPackagingPipeline: add normalizeShortcuts() --- .../internal/LinuxPackagingPipeline.java | 16 ++++++++ .../internal/WinPackagingPipeline.java | 37 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) 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 6f6013b3091fb..c6743cb729802 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackagingPipeline.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackagingPipeline.java @@ -24,6 +24,7 @@ */ package jdk.jpackage.internal; +import static jdk.jpackage.internal.ApplicationBuilder.normalizeLauncherProperty; import static jdk.jpackage.internal.ApplicationImageUtils.createLauncherIconResource; import java.io.IOException; @@ -36,8 +37,12 @@ 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.Launcher; +import jdk.jpackage.internal.model.LauncherShortcut; +import jdk.jpackage.internal.model.LinuxLauncher; +import jdk.jpackage.internal.model.LinuxLauncherMixin; import jdk.jpackage.internal.model.LinuxPackage; import jdk.jpackage.internal.resources.ResourceLocator; @@ -64,6 +69,17 @@ static PackagingPipeline.Builder build(Optional pkg) { 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( AppImageBuildEnv env) throws IOException { 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 f359a61ce7b46..1c1b8b34b54ab 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,25 @@ */ 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.PackagerException; +import jdk.jpackage.internal.model.LauncherShortcut; import jdk.jpackage.internal.model.WinApplication; import jdk.jpackage.internal.model.WinLauncher; +import jdk.jpackage.internal.model.WinLauncherMixin; final class WinPackagingPipeline { @@ -53,8 +59,35 @@ 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 { + throws IOException { for (var launcher : env.app().launchers()) { final var iconTarget = createLauncherIconResource(launcher, env.env()::createResource).map(iconResource -> { var iconDir = env.env().buildRoot().resolve("icons"); From be6d51392a3a9576fa8cbc3b7f969eaae272c427 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 25 Jun 2025 01:16:53 -0400 Subject: [PATCH 02/55] 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 3ecd49284d78e9bbe942290e6a802ee16c5b51f5 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Tue, 10 Jun 2025 18:15:53 -0400 Subject: [PATCH 03/55] 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 863675088d8f7d7ab13fa20a9bfa832c2aa4035c Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 13 Jun 2025 17:11:40 -0400 Subject: [PATCH 04/55] 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 53fbadb7848f1446e0ef14b374e5af67cb1f7fd3 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 5 Jun 2025 20:11:35 -0400 Subject: [PATCH 05/55] 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 5f075a714b612ff27999fb2493eef6cd39330970 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 5 Jun 2025 18:34:53 -0400 Subject: [PATCH 06/55] 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 6f4e0d0d2d89e..627cca805957d 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java @@ -512,8 +512,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 629510f1f50e86bd768abc1217dbe898885bccbb Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 7 Nov 2025 12:44:07 -0500 Subject: [PATCH 07/55] ConfigException: extend JPackageException; JPackageException: update javadoc --- .../jdk/jpackage/internal/model/ConfigException.java | 7 +++---- .../jdk/jpackage/internal/model/JPackageException.java | 2 +- .../classes/jdk/jpackage/internal/WinFromParams.java | 6 +----- 3 files changed, 5 insertions(+), 10 deletions(-) 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..d7e636ac0db9e 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 RuntimeException { +public class ConfigException extends JPackageException { private static final long serialVersionUID = 1L; private final String advice; @@ -60,9 +60,8 @@ public ConfigException(String msg, String advice, Throwable cause) { this.advice = advice; } - public ConfigException(Throwable cause) { - super(cause); - this.advice = null; + public ConfigException(String msg, Throwable cause) { + this(msg, null, cause); } public String getAdvice() { 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 index 00c1ce0a0ddda..9727f21d2d75f 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/JPackageException.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/JPackageException.java @@ -28,7 +28,7 @@ import java.util.Objects; /** - * Base class for exceptions explicitly emitted by jpackage. + * Generic jpackage exception with non-null message. */ public class JPackageException extends RuntimeException { 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 2d4225f5a5b0c..626d6a69b363c 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java @@ -95,11 +95,7 @@ private static WinMsiPackage createWinMsiPackage(Map par }); } - try { - UPGRADE_UUID.findIn(params).map(UUID::fromString).ifPresent(pkgBuilder::upgradeCode); - } catch (IllegalArgumentException ex) { - throw new ConfigException(ex); - } + UPGRADE_UUID.findIn(params).map(UUID::fromString).ifPresent(pkgBuilder::upgradeCode); return pkgBuilder.create(); } From 2bf3f38459994c85422ecf2ca060942731835cac Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Tue, 13 May 2025 10:54:10 -0400 Subject: [PATCH 08/55] 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 faa0175f64f8d4df1ef6f8e484c7a6a6a9ac5515 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Sat, 17 May 2025 13:36:10 -0400 Subject: [PATCH 09/55] 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 07186c0f385d373de5dbc320f1396d46e176f26a Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 5 Jun 2025 19:59:47 -0400 Subject: [PATCH 10/55] Use I18N.buildConfigException() to create ConfigException instances --- .../jdk/jpackage/internal/MacAppBundler.java | 4 +-- .../internal/JLinkRuntimeBuilder.java | 4 +-- .../jdk/jpackage/internal/LauncherData.java | 29 +++++++++---------- 3 files changed, 16 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; From b2a40e5ec944ad3a11306966ea01e73f76ed9400 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 5 Jun 2025 19:12:00 -0400 Subject: [PATCH 11/55] 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 94eb086e4c61d..513ff3c96e794 100644 --- a/test/jdk/tools/jpackage/share/AppImagePackageTest.java +++ b/test/jdk/tools/jpackage/share/AppImagePackageTest.java @@ -173,7 +173,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(); @@ -185,8 +185,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 5a5aa0e070dacd344bd50c1114f62f61ae3117fb Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 13 Oct 2025 12:27:16 -0400 Subject: [PATCH 12/55] 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 ac22bcaf1ba2c5dfd785f0e9ca5e448e21a49437 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 13 Oct 2025 12:31:11 -0400 Subject: [PATCH 13/55] 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 358754d279de7a2fc16c4c049e9242bb5947cf0c Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 13 Oct 2025 12:47:57 -0400 Subject: [PATCH 14/55] the cli: new help output --- .../jdk/jpackage/internal/cli/help-linux.txt | 26 ++++++++--------- .../jdk/jpackage/internal/cli/help-macos.txt | 29 ++++++++++--------- .../jpackage/internal/cli/help-windows.txt | 25 ++++++++-------- 3 files changed, 41 insertions(+), 39 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..3a191c563e0fb 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 @@ -45,7 +47,7 @@ Generic Options: 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,10 +177,10 @@ 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 @@ -191,7 +191,7 @@ Platform dependent options for creating the application package: Required packages or capabilities for the application --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..851ea5864cb2b 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 [...] @@ -50,7 +53,7 @@ Generic Options: 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..4c826b9d42236 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 @@ -45,7 +47,7 @@ Generic Options: 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 3cb2cdce3015e6577016afce83d295af6733d695 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Tue, 14 Oct 2025 18:08:00 -0400 Subject: [PATCH 15/55] Add the "cli". --- .../internal/cli/AdditionalLauncher.java | 32 + .../cli/BundlingOperationModifier.java | 42 + .../cli/BundlingOperationOptionScope.java | 38 + .../internal/cli/CliBundlingEnvironment.java | 47 + .../jpackage/internal/cli/DefaultOptions.java | 191 +++ .../jpackage/internal/cli/HelpFormatter.java | 179 +++ .../jdk/jpackage/internal/cli/I18N.java | 55 + .../cli/JOptSimpleOptionsBuilder.java | 815 ++++++++++ .../jdk/jpackage/internal/cli/Main.java | 224 +++ .../internal/cli/MessageFormatUtils.java | 79 + .../jdk/jpackage/internal/cli/Option.java | 54 + .../cli/OptionArrayValueConverter.java | 41 + .../internal/cli/OptionIdentifier.java | 53 + .../jdk/jpackage/internal/cli/OptionName.java | 68 + .../jpackage/internal/cli/OptionScope.java | 32 + .../jpackage/internal/cli/OptionSource.java | 68 + .../jdk/jpackage/internal/cli/OptionSpec.java | 180 +++ .../internal/cli/OptionSpecBuilder.java | 475 ++++++ .../cli/OptionSpecMapperOptionScope.java | 157 ++ .../jpackage/internal/cli/OptionValue.java | 190 +++ .../internal/cli/OptionValueConverter.java | 273 ++++ .../cli/OptionValueExceptionFactory.java | 183 +++ .../jdk/jpackage/internal/cli/Options.java | 196 +++ .../internal/cli/OptionsAnalyzer.java | 430 +++++ .../internal/cli/OptionsProcessor.java | 428 +++++ .../cli/StandardAppImageFileOption.java | 174 ++ .../cli/StandardBundlingOperation.java | 173 ++ .../internal/cli/StandardFaOption.java | 111 ++ .../internal/cli/StandardHelpFormatter.java | 398 +++++ .../jpackage/internal/cli/StandardOption.java | 804 ++++++++++ .../internal/cli/StandardOptionContext.java | 68 + .../StandardOptionValueExceptionFactory.java | 79 + .../internal/cli/StandardValidator.java | 133 ++ .../internal/cli/StandardValueConverter.java | 83 + .../jpackage/internal/cli/StringToken.java | 57 + .../jdk/jpackage/internal/cli/Utils.java | 109 ++ .../jdk/jpackage/internal/cli/Validator.java | 265 +++ .../jpackage/internal/cli/ValueConverter.java | 68 + .../internal/cli/WithOptionIdentifier.java | 38 + .../cli/WithOptionIdentifierStub.java | 36 + .../resources/HelpResources.properties | 375 +++++ .../resources/MainResources.properties | 23 + test/jdk/tools/jpackage/junit/TEST.properties | 8 +- .../internal/cli/DefaultOptionsTest.java | 57 + .../internal/cli/ExpectedOptions.java | 94 ++ .../jdk/jpackage/internal/cli/HelpTest.java | 181 +++ .../cli/JOptSimpleOptionsBuilderTest.java | 1428 +++++++++++++++++ .../cli/MockupCliBundlingEnvironment.java | 160 ++ .../internal/cli/OptionIdentifierTest.java | 63 + .../jpackage/internal/cli/OptionNameTest.java | 152 ++ .../cli/OptionSpecMutatorOptionScopeTest.java | 144 ++ .../jpackage/internal/cli/OptionSpecTest.java | 365 +++++ .../cli/OptionValueConverterTest.java | 216 +++ .../cli/OptionValueExceptionFactoryTest.java | 152 ++ .../internal/cli/OptionValueTest.java | 212 +++ .../internal/cli/OptionsProcessorTest.java | 786 +++++++++ .../jpackage/internal/cli/OptionsTest.java | 256 +++ .../cli/OptionsValidationFailTest.excludes | 42 + .../cli/OptionsValidationFailTest.java | 245 +++ .../cli/StandardBundlingOperationTest.java | 103 ++ .../internal/cli/StandardOptionTest.java | 646 ++++++++ .../internal/cli/StandardValidatorTest.java | 227 +++ .../cli/StandardValueConverterTest.java | 161 ++ .../internal/cli/StringTokenTest.java | 49 + .../jdk/jpackage/internal/cli/TestUtils.java | 179 +++ .../jdk/jpackage/internal/cli/UtilsTest.java | 91 ++ .../jpackage/internal/cli/ValidatorTest.java | 284 ++++ .../jpackage/internal/cli/jpackage-options.md | 63 + 68 files changed, 13887 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/DefaultOptions.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 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/WithOptionIdentifier.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/WithOptionIdentifierStub.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/DefaultOptionsTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/ExpectedOptions.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/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/DefaultOptions.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/DefaultOptions.java new file mode 100644 index 0000000000000..91342500277ec --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/DefaultOptions.java @@ -0,0 +1,191 @@ +/* + * 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.toUnmodifiableMap; +import static java.util.stream.Collectors.toUnmodifiableSet; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + + +final class DefaultOptions implements Options { + + DefaultOptions(Map values) { + this(values, Optional.empty()); + } + + DefaultOptions( + Map values, + Predicate optionNamesFilter) { + + this(values, Optional.of(optionNamesFilter)); + } + + DefaultOptions( + Map values, + Optional> optionNamesFilter) { + + map = values.entrySet().stream().collect(toUnmodifiableMap(e -> { + return e.getKey().id(); + }, e -> { + return new OptionIdentifierWithValue(e.getKey(), e.getValue()); + })); + + var optionNamesStream = optionNames(values.keySet().stream()); + optionNames = optionNamesFilter.map(optionNamesStream::filter).orElse(optionNamesStream) + .collect(toUnmodifiableSet()); + } + + private DefaultOptions(Snapshot snapshot) { + map = snapshot.map(); + optionNames = snapshot.optionNames(); + } + + static DefaultOptions create(Snapshot snapshot) { + var options = new DefaultOptions(snapshot); + + var mapOptionNames = optionNames( + options.map.values().stream().map(OptionIdentifierWithValue::withId) + ).collect(toUnmodifiableSet()); + + for (var e : options.map.entrySet()) { + if (e.getKey() != e.getValue().withId().id()) { + throw new IllegalArgumentException("Corrupted options map"); + } + } + + if (!mapOptionNames.containsAll(snapshot.optionNames())) { + throw new IllegalArgumentException("Unexpected option names"); + } + return options; + } + + @Override + public Optional find(OptionIdentifier id) { + return Optional.ofNullable(map.get(Objects.requireNonNull(id))).map(OptionIdentifierWithValue::value); + } + + @Override + public boolean contains(OptionName optionName) { + return optionNames.contains(Objects.requireNonNull(optionName)); + } + + @Override + public Set ids() { + return Collections.unmodifiableSet(map.keySet()); + } + + @Override + public DefaultOptions copyWithout(Iterable ids) { + return copy(StreamSupport.stream(ids.spliterator(), false), false); + } + + @Override + public DefaultOptions copyWith(Iterable ids) { + return copy(StreamSupport.stream(ids.spliterator(), false), true); + } + + DefaultOptions add(DefaultOptions other) { + return new DefaultOptions(new Snapshot(Stream.of(this, other).flatMap(v -> { + return v.map.values().stream(); + }).collect(toUnmodifiableMap(OptionIdentifierWithValue::id, x -> x, (first, _) -> { + return first; + })), Stream.of(this, other) + .map(DefaultOptions::optionNames) + .flatMap(Collection::stream) + .collect(toUnmodifiableSet()))); + } + + Set optionNames() { + return optionNames; + } + + Set withOptionIdentifierSet() { + return map.values().stream() + .map(OptionIdentifierWithValue::withId) + .collect(toUnmodifiableSet()); + } + + record Snapshot(Map map, Set optionNames) { + Snapshot { + Objects.requireNonNull(map); + Objects.requireNonNull(optionNames); + } + } + + record OptionIdentifierWithValue(WithOptionIdentifier withId, Object value) { + OptionIdentifierWithValue { + Objects.requireNonNull(withId); + Objects.requireNonNull(value); + } + + OptionIdentifier id() { + return withId.id(); + } + + OptionIdentifierWithValue copyWithValue(Object value) { + return new OptionIdentifierWithValue(withId, value); + } + } + + private DefaultOptions copy(Stream ids, boolean includes) { + var includeIds = ids.collect(toUnmodifiableSet()); + return new DefaultOptions(map.values().stream().filter(v -> { + return includeIds.contains(v.id()) == includes; + }).collect(toUnmodifiableMap(OptionIdentifierWithValue::withId, OptionIdentifierWithValue::value))); + } + + private static Stream optionNames(Stream options) { + return options.map(v -> { + Optional> spec; + switch (v) { + case Option option -> { + spec = Optional.of(option.spec()); + } + case OptionValue optionValue -> { + spec = optionValue.asOption().map(Option::spec); + } + default -> { + spec = Optional.empty(); + } + } + return spec; + }).filter(Optional::isPresent).map(Optional::get).map(OptionSpec::names).flatMap(Collection::stream); + } + + static final DefaultOptions EMPTY = new DefaultOptions(Map.of()); + + private final Map map; + private final Set optionNames; +} 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..9978b98ef514d --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/JOptSimpleOptionsBuilder.java @@ -0,0 +1,815 @@ +/* + * 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.toUnmodifiableMap; +import static java.util.stream.Collectors.toUnmodifiableSet; + +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.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.DefaultOptions.OptionIdentifierWithValue; +import jdk.jpackage.internal.cli.DefaultOptions.Snapshot; +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 v) { + v.stream().map(u -> { + switch (u) { + case Option o -> { + return o; + } + case OptionValue ov -> { + return ov.getOption(); + } + default -> { + throw new IllegalArgumentException(); + } + } + }).forEach(options::add); + return this; + } + + JOptSimpleOptionsBuilder options(WithOptionIdentifier... v) { + return options(List.of(v)); + } + + JOptSimpleOptionsBuilder optionSpecMapper(UnaryOperator> v) { + optionSpecMapper = v; + return this; + } + + JOptSimpleOptionsBuilder jOptSimpleParserErrorHandler(Function v) { + jOptSimpleParserErrorHandler = v; + return this; + } + + private JOptSimpleParser createJOptSimpleParser() { + return JOptSimpleParser.create(options, Optional.ofNullable(optionSpecMapper), + Optional.ofNullable(jOptSimpleParserErrorHandler)); + } + + + static final class ConvertedOptionsBuilder { + + private ConvertedOptionsBuilder(TypedOptions options) { + impl = Objects.requireNonNull(options); + } + + Options create() { + return impl; + } + + ConvertedOptionsBuilder copyWithExcludes(Collection v) { + return new ConvertedOptionsBuilder(impl.copyWithout(v)); + } + + List nonOptionArguments() { + return impl.nonOptionArguments(); + } + + List detectedOptions() { + return impl.detectedOptions(); + } + + private final TypedOptions impl; + } + + + static final class OptionsBuilder { + + private OptionsBuilder(UntypedOptions options) { + impl = Objects.requireNonNull(options); + } + + Result convertedOptions() { + return impl.toTypedOptions().map(ConvertedOptionsBuilder::new); + } + + Options create() { + return impl; + } + + OptionsBuilder copyWithExcludes(Collection v) { + return new OptionsBuilder(impl.copyWithout(v)); + } + + List nonOptionArguments() { + return impl.nonOptionArguments(); + } + + List detectedOptions() { + return impl.detectedOptions(); + } + + private final UntypedOptions impl; + } + + + enum JOptSimpleErrorType { + + // jdk.internal.joptsimple.UnrecognizedOptionException + UNRECOGNIZED_OPTION(() -> { + new OptionParser(false).parse("--foo"); + }), + + // jdk.internal.joptsimple.OptionMissingRequiredArgumentException + OPTION_MISSING_REQUIRED_ARGUMENT(() -> { + var parser = new OptionParser(false); + parser.accepts("foo").withRequiredArg(); + parser.parse("--foo"); + }), + ; + + JOptSimpleErrorType(Runnable initializer) { + try { + initializer.run(); + throw new AssertionError(); + } catch (jdk.internal.joptsimple.OptionException ex) { + type = ex.getClass(); + } + } + + private final Class type; + } + + + record JOptSimpleError(JOptSimpleErrorType type, OptionName optionName) { + + JOptSimpleError { + Objects.requireNonNull(type); + Objects.requireNonNull(optionName); + } + + static JOptSimpleError create(jdk.internal.joptsimple.OptionException ex) { + var optionName = OptionName.of(ex.options().getFirst()); + return Stream.of(JOptSimpleErrorType.values()).filter(v -> { + return v.type.isInstance(ex); + }).findFirst().map(v -> { + return new JOptSimpleError(v, optionName); + }).orElseThrow(); + } + } + + + private record JOptSimpleParser( + OptionParser parser, + Map> optionMap, + Optional> jOptSimpleParserErrorHandler) { + + private JOptSimpleParser { + Objects.requireNonNull(parser); + Objects.requireNonNull(optionMap); + Objects.requireNonNull(jOptSimpleParserErrorHandler); + } + + Result parse(String... args) { + return applyParser(parser, args).map(optionSet -> { + final OptionSet mergerOptionSet; + if (optionMap.values().stream().allMatch(spec -> spec.names().size() == 1)) { + // No specs with multiple names, merger not needed. + mergerOptionSet = optionSet; + } else { + final var parser2 = createOptionParser(); + final var optionSpecApplier = new OptionSpecApplier(); + for (final var spec : optionMap.values()) { + optionSpecApplier.applyToParser(parser2, spec); + } + + mergerOptionSet = parser2.parse(args); + } + return new OptionsBuilder(new UntypedOptions(optionSet, mergerOptionSet, optionMap)); + }); + } + + static JOptSimpleParser create(Iterable