+ * On success, command line arguments are automatically cleared.
+ *
+ * @throws Exception if an error occurred
+ */
+ @Override
+ public void execute() throws Exception {
+ if (toolArgs_.isEmpty()) {
+ System.err.println("No " + toolName_ + " command line arguments specified.");
+ throw new ExitStatusException(ExitStatusException.EXIT_FAILURE);
+ }
+
+ var tool = ToolProvider.findFirst(toolName_).orElseThrow(() ->
+ new IllegalStateException("No " + toolName_ + " tool found."));
+
+ var status = tool.run(System.out, System.err, toolArgs_.toArray(new String[0]));
+ if (status != 0) {
+ System.out.println(tool.name() + ' ' + String.join(" ", toolArgs_));
+ }
+
+ ExitStatusException.throwOnFailure(status);
+
+ toolArgs_.clear();
+ }
+
+ /**
+ * Adds arguments to pass to the tool.
+ *
+ * @param arg one or more argument
+ * @return this operation
+ */
+ @SuppressWarnings("unchecked")
+ public T toolArgs(String... arg) {
+ toolArgs(List.of(arg));
+ return (T) this;
+ }
+
+ /**
+ * Adds arguments to pass to the tool.
+ *
+ * @param args the argument to add
+ * @return this operation
+ */
+ @SuppressWarnings({"unchecked", "UnusedReturnValue"})
+ public T toolArgs(List
+ * A copy will be created to allow this list to be independently modifiable.
+ *
+ * @param options the argument-value pairs
+ * @return this operation instance
+ */
+ public JlinkOperation jlinkOptions(Map
+ * This is a modifiable list that can be retrieved and changed.
+ *
+ * @return the map of jlink options
+ */
+ public JlinkOptions jlinkOptions() {
+ return jlinkOptions_;
+ }
+}
diff --git a/src/main/java/rife/bld/operations/JlinkOptions.java b/src/main/java/rife/bld/operations/JlinkOptions.java
new file mode 100644
index 0000000..cbe71cf
--- /dev/null
+++ b/src/main/java/rife/bld/operations/JlinkOptions.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2024 Erik C. Thauvin (https://erik.thauvin.net/)
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ */
+package rife.bld.operations;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Options for jlink tool.
+ *
+ * @author Erik C. Thauvin
+ * @since 2.0.2
+ */
+public class JlinkOptions extends HashMap
+ * Module can also be {@link #ALL_MODULE_PATH}
+ *
+ * @param modules one or more module
+ * @return this map of options
+ */
+ public JlinkOptions addModules(String... modules) {
+ put("--add-modules", String.join(",", modules));
+ return this;
+ }
+
+ /**
+ * Link in service provider modules and their dependencies.
+ *
+ * @param bindServices {@code true} to bind services, {@code false} otherwise
+ * @return this map of options
+ */
+ public JlinkOptions bindServices(boolean bindServices) {
+ if (bindServices) {
+ put("--bind-services");
+ } else {
+ remove("--bind-services");
+ }
+ return this;
+ }
+
+ /**
+ * Compression to use in compressing resources.
+ *
+ * Requires Java 21 or higher. Use {@link #compress(CompressionLevel)} for lower versions.
+ *
+ * Where {@link ZipCompression#ZIP_0 ZIP_0} provides no compression and {@link ZipCompression#ZIP_9 ZIP_9} provides
+ * the best compression.
+ * Default is {@link ZipCompression#ZIP_6 ZIP_6}
+ *
+ * @param compression the {@link ZipCompression compression} level
+ * @return this map of options
+ * @see #compress(ZipCompression)
+ */
+ public JlinkOptions compress(ZipCompression compression) {
+ put("--compress", compression.level);
+ return this;
+ }
+
+ /**
+ * Enable compression of resources.
+ *
+ * Use {@link #compress(ZipCompression)} on Java 21 or higher.
+ *
+ * @param compression the {@link CompressionLevel compression} level
+ * @return this map of options
+ * @see #compress(CompressionLevel)
+ */
+ public JlinkOptions compress(CompressionLevel compression) {
+ put("--compress", compression.level);
+ return this;
+ }
+
+
+ /**
+ * Byte order of generated jimage.
+ *
+ * Default: native
+ *
+ * @param endian the byte order
+ * @return this map of options
+ */
+ public JlinkOptions endian(Endian endian) {
+ put("--endian", endian.byteOrder);
+ return this;
+ }
+
+ /**
+ * Suppress a fatal error when signed modular JARs are linked in the image.
+ *
+ * @param ignoreSigningInformation {@code true} to ignore signing information, {@code false} otherwise
+ * @return this map of options
+ */
+ public JlinkOptions ignoreSigningInformation(boolean ignoreSigningInformation) {
+ if (ignoreSigningInformation) {
+ put("--ignore-signing-information");
+ } else {
+ remove("--ignore-signing-information");
+ }
+ return this;
+ }
+
+ /**
+ * Add a launcher command of the given name for the module.
+ *
+ * @param name the name
+ * @param module the module
+ * @return this map of options
+ */
+ @SuppressWarnings("UnusedReturnValue")
+ public JlinkOptions launcher(String name, String module) {
+ put("--launcher", name + "=" + module);
+ return this;
+ }
+
+ /**
+ * Add a launcher command of the given name for the module and the main class.
+ *
+ * @param name the name
+ * @param module the module
+ * @param mainClass the main class
+ * @return this map of options
+ */
+ public JlinkOptions launcher(String name, String module, String mainClass) {
+ put("--launcher", name + "=" + module + "/" + mainClass);
+ return this;
+ }
+
+ /**
+ * Limit the universe of observable modules.
+ *
+ * @param module one or more module
+ * @return this map of options
+ */
+ public JlinkOptions limitModule(String... module) {
+ put("--limit-modules", String.join(",", module));
+ return this;
+ }
+
+ /**
+ * Module path.
+ *
+ * If not specified, the JDKs jmods directory will be used, if it exists. If specified, but it does not contain the
+ * java.base module, the JDKs jmods directory will be added, if it exists.
+ *
+ * @param path the module path
+ * @return this map of options
+ */
+ public JlinkOptions modulePath(String path) {
+ put("--module-path", path);
+ return this;
+ }
+
+ /**
+ * Exclude include header files.
+ *
+ * @param noHeaderFiles {@code true} to exclude header files, {@code false} otherwise
+ * @return this map of options
+ */
+ public JlinkOptions noHeaderFiles(boolean noHeaderFiles) {
+ if (noHeaderFiles) {
+ put("--no-header-files");
+ } else {
+ remove("--no-header-files");
+ }
+ return this;
+ }
+
+ /**
+ * Exclude man pages.
+ *
+ * @param noManPages {@code true} to exclude man pages, {@code false} otherwise
+ * @return this map of options
+ */
+ public JlinkOptions noManPages(boolean noManPages) {
+ if (noManPages) {
+ put("--no-man-pages");
+ } else {
+ remove("--no-man-pages");
+ }
+ return this;
+ }
+
+ /**
+ * Location of output path.
+ *
+ * @param path the output path
+ * @return this map of options
+ */
+ public JlinkOptions output(String path) {
+ put("--output", path);
+ return this;
+ }
+
+ /**
+ * Associates {@code null} with the specified key in this map. If the map previously contained a mapping for the
+ * key, the old value is replaced.
+ *
+ * @param key key with which the specified value is to be associated
+ */
+ public void put(String key) {
+ put(key, null);
+ }
+
+ /**
+ * Suggest providers that implement the given service types from the module path.
+ *
+ * @param filename the filename
+ * @return this map of options
+ */
+ public JlinkOptions saveOpts(String filename) {
+ put("--save-opts", filename);
+ return this;
+ }
+
+ /**
+ * Strip debug information.
+ *
+ * @param stripDebug {@code true} to strip debug info, {@code false} otherwise
+ * @return this map of options
+ */
+ public JlinkOptions stripDebug(boolean stripDebug) {
+ if (stripDebug) {
+ put("--strip-debug");
+ } else {
+ remove("--strip-debug");
+ }
+ return this;
+ }
+
+ /**
+ * Strip native commands.
+ *
+ * @param stripNativeCommands {@code true} to strip, {@code false} otherwise
+ * @return this map of options
+ */
+ public JlinkOptions stripNativeCommands(boolean stripNativeCommands) {
+ if (stripNativeCommands) {
+ put("--strip-native-commands");
+ } else {
+ remove("--strip-native-commands");
+ }
+ return this;
+ }
+
+ /**
+ * Suggest providers that implement the given service types from the module path.
+ *
+ * @param name one or more provider name
+ * @return this map of options
+ */
+ public JlinkOptions suggestProviders(String... name) {
+ put("--suggest-providers", String.join(",", name));
+ return this;
+ }
+
+ public List
+ * The JMOD file is required.
+ *
+ * @param file the JMOD file
+ * @return this operation instance
+ */
+ public JmodOperation jmodFile(String file) {
+ jmodFile_ = file;
+ return this;
+ }
+
+ /**
+ * Retrieves the list of options for the jmod tool.
+ *
+ * This is a modifiable list that can be retrieved and changed.
+ *
+ * @return the map of jmod options
+ */
+ public JmodOptions jmodOptions() {
+ return jmodOptions_;
+ }
+
+ /**
+ * Provides a list of options to provide to the jmod tool.
+ *
+ * A copy will be created to allow this list to be independently modifiable.
+ *
+ * @param options the list of jmod options
+ * @return this operation instance
+ */
+ public JmodOperation jmodOptions(Map
+ * The operation mode is required.
+ *
+ * @param mode the mode
+ * @return this operation instance
+ */
+ public JmodOperation operationMode(OperationMode mode) {
+ operationMode_ = mode;
+ return this;
+ }
+
+ /**
+ * The operation modes.
+ */
+ public enum OperationMode {
+ /**
+ * Creates a new JMOD archive file.
+ */
+ CREATE("create"),
+ /**
+ * Prints the module details.
+ */
+ DESCRIBE("describe"),
+ /**
+ * Extracts all the files from the JMOD archive file.
+ */
+ EXTRACT("extract"),
+ /**
+ * Determines leaf modules and records the hashes of the dependencies that directly and indirectly require them.
+ */
+ HASH("hash"),
+ /**
+ * Prints the names of all the entries.
+ */
+ LIST("list");
+
+ final String mode;
+
+ OperationMode(String mode) {
+ this.mode = mode;
+ }
+ }
+}
diff --git a/src/main/java/rife/bld/operations/JmodOptions.java b/src/main/java/rife/bld/operations/JmodOptions.java
new file mode 100644
index 0000000..3510655
--- /dev/null
+++ b/src/main/java/rife/bld/operations/JmodOptions.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2024 Erik C. Thauvin (https://erik.thauvin.net/)
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ */
+package rife.bld.operations;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Options for jmod tool.
+ *
+ * @author Erik C. Thauvin
+ * @since 2.0.2
+ */
+public class JmodOptions extends HashMap
+ * Requires Java 20 or higher.
+ *
+ * Where {@link ZipCompression#ZIP_0 ZIP_0} provides no compression and {@link ZipCompression#ZIP_9 ZIP_9} provides the
+ * best compression.
+ *
+ * Default is {@link ZipCompression#ZIP_6 ZIP_6}
+ *
+ * @param compression the {@link ZipCompression compression} level
+ * @return this map of options
+ */
+ public JmodOptions compress(ZipCompression compression) {
+ put("--compress", compression.level);
+ return this;
+ }
+
+ /**
+ * Location of user-editable config files
+ *
+ * @param path the path to the config files
+ * @return this map of options
+ */
+ public JmodOptions config(String path) {
+ put("--config", path);
+ return this;
+ }
+
+ /**
+ * Date and time for the timestamps of entries.
+ *
+ * @param date the date
+ * @return this map of options
+ */
+ public JmodOptions date(ZonedDateTime date) {
+ put("--date", date.truncatedTo(ChronoUnit.SECONDS).format(DateTimeFormatter.ISO_INSTANT));
+ return this;
+ }
+
+ /**
+ * Target directory for extract
+ *
+ * @param path the directory path
+ * @return this map of options
+ */
+ public JmodOptions dir(String path) {
+ put("--dir", path);
+ return this;
+ }
+
+ /**
+ * Exclude from the default root set of modules.
+ *
+ * @param doNotResolveByDefault {@code true} to not resolve, {@code false} otherwise
+ * @return this map of options
+ */
+ public JmodOptions doNotResolveByDefault(boolean doNotResolveByDefault) {
+ if (doNotResolveByDefault) {
+ put("--do-not-resolve-by-default");
+ } else {
+ remove("--do-not-resolve-by-default");
+ }
+ return this;
+ }
+
+ /**
+ * Dry run of hash mode.
+ *
+ * @param dryRun {@code true} for dry run, {@code false} otherwise
+ * @return this list of operation
+ */
+ public JmodOptions dryRun(boolean dryRun) {
+ if (dryRun) {
+ put("--dry-run");
+ } else {
+ remove("--dry-run");
+ }
+ return this;
+ }
+
+ /**
+ * Exclude files matching the supplied pattern list.
+ *
+ * @param pattern one or more pattern
+ * @return the map of options
+ */
+ public JmodOptions exclude(FilePattern... pattern) {
+ var args = new ArrayList
+ * This is a modifiable list that can be retrieved and changed.
+ *
+ * @return the map of jpackage options
+ */
+ public JpackageOptions jpackageOptions() {
+ return jpackageOptions_;
+ }
+
+ /**
+ * Provides a list of options to provide to the jpackage tool.
+ *
+ * A copy will be created to allow this list to be independently modifiable.
+ *
+ * @param options the map of jpackage options
+ * @return this operation instance
+ */
+ public JpackageOperation jpackageOptions(Map
+ * 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 to build multiple
+ * additional launchers.
+ *
+ * @param launcher one or more {@link Launcher}
+ * @return this map of options
+ */
+ public JpackageOptions addLauncher(Launcher... launcher) {
+ for (var l : launcher) {
+ put("--add-launcher", l.name + '=' + l.path);
+ }
+ return this;
+ }
+
+ /**
+ * List of modules to add.
+ *
+ * This module list, along with the main module (if specified) will be passed to jlink as the
+ * {@link JlinkOptions#addModules(String...) addModules} argument. If not specified, either just the main module
+ * (if {@link #module(String, String) module} is specified), or the default set of modules (if
+ * {@link #mainJar(String) mainJar} is specified) are used.
+ *
+ * @param modules one or more module
+ * @return this map of options
+ */
+ public JpackageOptions addModules(String... modules) {
+ put("--add-modules", String.join(",", modules));
+ return this;
+ }
+
+ /**
+ * List of paths to files and/or directories to add to the application payload.
+ *
+ * Requires Java 20 or higher.
+ *
+ * @param additionalContent one or more path
+ * @return this map of options
+ */
+ public JpackageOptions appContent(String... additionalContent) {
+ put("--app-content", String.join(",", additionalContent));
+ return this;
+ }
+
+ /**
+ * Location of the predefined application image that is used to build an installable package.
+ *
+ * @param path absolute path or relative to the current directory
+ * @return this map of options
+ */
+ public JpackageOptions appImage(String path) {
+ put("--app-image", path);
+ return this;
+ }
+
+ /**
+ * Version of the application and/or package.
+ *
+ * @param version the version
+ * @return this map of options
+ */
+ public JpackageOptions appVersion(String version) {
+ put("--app-version", version);
+ return this;
+ }
+
+ /**
+ * Command line arguments to pass to main class if no command line arguments are given to the launcher.
+ *
+ * @param argument one or more argument
+ * @return this map of options
+ */
+ public JpackageOptions arguments(String... argument) {
+ put("--arguments", String.join(" ", argument));
+ return this;
+ }
+
+ /**
+ * Copyright of the application.
+ *
+ * @param copyright the copyright
+ * @return this map of options
+ */
+ public JpackageOptions copyright(String copyright) {
+ put("--copyright", copyright);
+ return this;
+ }
+
+ /**
+ * Description of the application.
+ *
+ * @param description the description
+ * @return this map of options
+ */
+ public JpackageOptions description(String description) {
+ put("--description", description);
+ return this;
+ }
+
+ /**
+ * Path where generated output file is placed.
+ *
+ * Defaults to the current working directory.
+ *
+ * @param path absolute path or relative to the current directory
+ * @return this map of options
+ */
+ public JpackageOptions dest(String path) {
+ put("--dest", path);
+ return this;
+ }
+
+ /**
+ * Path to a Properties file that contains list of key, value pairs.
+ *
+ * The keys {@code extension}, {@code mime-type}, {@code icon}, and {@code description} can be used to describe the
+ * association.
+ *
+ * @param path absolute path or relative to the current directory
+ * @return this map of options
+ */
+ public JpackageOptions fileAssociations(String... path) {
+ put("--file-associations", String.join(",", path));
+ return this;
+ }
+
+ /**
+ * Path of the icon of the application package.
+ *
+ * @param path absolute path or relative to the current directory
+ * @return this map of options
+ */
+ public JpackageOptions icon(String path) {
+ put("--icon", path);
+ return this;
+ }
+
+ /**
+ * Path of the input directory that contains the files to be packaged.
+ *
+ * All files in the input directory will be packaged into the application image.
+ *
+ * @param path absolute path or relative to the current directory
+ * @return this map of options
+ */
+ public JpackageOptions input(String path) {
+ put("--input", path);
+ return this;
+ }
+
+ /**
+ * Absolute path of the installation directory of the application.
+ *
+ * @param path the absolute directory path
+ * @return this map of options
+ */
+ public JpackageOptions installDir(String path) {
+ put("--install-dir", path);
+ return this;
+ }
+
+ /**
+ * Options to pass to the Java runtime.
+ *
+ * @param options the options
+ * @return this map of options
+ */
+ public JpackageOptions javaOptions(String... options) {
+ put("--java-options", String.join(" ", options));
+ return this;
+ }
+
+ /**
+ * List of options to pass to jlink.
+ *
+ * If not specified, defaults to {@link JlinkOptions#stripNativeCommands(boolean) stripNativeCommands}
+ * {@link JlinkOptions#stripDebug(boolean) stripDebug} {@link JlinkOptions#noManPages(boolean) noManPages}
+ * {@link JlinkOptions#noHeaderFiles(boolean) noHeaderFiles}.
+ *
+ * @param options the {@link JlinkOptions}
+ * @return this map of options
+ */
+ public JpackageOptions jlinkOptions(JlinkOptions options) {
+ put("--jlink-options", String.join(" ", options.toList()));
+ return this;
+ }
+
+ /**
+ * Request to create an installer that will register the main application launcher as a background service-type
+ * application.
+ *
+ * Requires Java 20 or higher.
+ *
+ * @param launcherAsService {@code true} to register the launcher as a service; {@code false} otherwise
+ * @return this map of options
+ */
+ public JpackageOptions launcherAsService(boolean launcherAsService) {
+ if (launcherAsService) {
+ put("--launcher-as-service");
+ } else {
+ remove("--launcher-as-service");
+ }
+ return this;
+ }
+
+ /**
+ * Path to the license file.
+ *
+ * @param path absolute path or relative to the current directory
+ * @return this map of options
+ */
+ public JpackageOptions licenseFile(String path) {
+ put("--license-file", path);
+ return this;
+ }
+
+ /**
+ * Group value of the RPM {@code
+ * {@code License:
+ * The default value is {@code utilities}.
+ *
+ * @param appCategory the category
+ * @return this map of options
+ */
+ public JpackageOptions macAppCategory(String appCategory) {
+ put("--mac-app-category", appCategory);
+ return this;
+ }
+
+ /**
+ * Identity used to sign application image.
+ *
+ * This value will be passed directly to {@code --sign} option of {@code codesign} tool.
+ *
+ * This option cannot be combined with {@link #macSigningKeyUserName(String) macSignKeyUserName}.
+ *
+ * @param identity the identity
+ * @return this map of options
+ */
+ public JpackageOptions macAppImageSignIdentity(String identity) {
+ put("--mac-app-image-sign-identity", identity);
+ return this;
+ }
+
+ /**
+ * Indicates that the jpackage output is intended for the Mac App Store.
+ *
+ * @param appStore {@code true} if intended for the Mac App Store, {@code false} otherwise
+ * @return this map of options
+ */
+ public JpackageOptions macAppStore(boolean appStore) {
+ if (appStore) {
+ put("--mac-app-store");
+ } else {
+ remove("--mac-app-store");
+ }
+ return this;
+ }
+
+ /**
+ * Include all the referenced content in the dmg.
+ *
+ * @param additionalContent one or more path
+ * @return this map of options
+ */
+ public JpackageOptions macDmgContent(String... additionalContent) {
+ put("--mac-dmg-content", String.join(",", additionalContent));
+ return this;
+ }
+
+ /**
+ * Path to file containing entitlements to use when signing executables and libraries in the bundle.
+ *
+ * @param path the fie path
+ * @return this map of options
+ */
+ public JpackageOptions macEntitlements(String path) {
+ put("--mac-entitlements", path);
+ return this;
+ }
+
+ /**
+ * Identity used to sign "pkg" installer.
+ *
+ * This value will be passed directly to {@code --sign} option of {@code productbuild} tool.
+ *
+ * This option cannot be combined with {@link #macSigningKeyUserName(String) macSignKeyUserName}.
+ *
+ * @param identity the identity
+ * @return this map of options
+ */
+ public JpackageOptions macInstallerSignIdentity(String identity) {
+ put("--mac-installer-sign-identity", identity);
+ return this;
+ }
+
+ /**
+ * An identifier that uniquely identifies the application for macOS.
+ *
+ * Defaults to the main class name.
+ *
+ * May only use alphanumeric ({@code A-Z,a-z,0-9}), hyphen ({@code -}), and period ({@code .}) characters.
+ *
+ * @param packageIdentifier the package identifier
+ * @return this map of options
+ */
+ public JpackageOptions macPackageIdentifier(String packageIdentifier) {
+ put("--mac-package-identifier", packageIdentifier);
+ return this;
+ }
+
+ /**
+ * 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.
+ *
+ * @param name the package name
+ * @return this map of options
+ */
+ public JpackageOptions macPackageName(String name) {
+ put("--mac-package-name", name);
+ return this;
+ }
+
+ /**
+ * 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.
+ *
+ * @param prefix the signing prefix
+ * @return this map of options
+ */
+ public JpackageOptions macPackageSigningPrefix(String prefix) {
+ put("--mac-package-signing-prefix", prefix);
+ return this;
+ }
+
+ /**
+ * Request that the package or the predefined application image be signed.
+ *
+ * @param sign {@code true} to sign, {@code false} otherwise
+ * @return this map of options
+ */
+ public JpackageOptions macSign(boolean sign) {
+ if (sign) {
+ put("--mac-sign");
+ } else {
+ remove("--mac-sign");
+ }
+ return this;
+ }
+
+ /**
+ * Team or user name portion in Apple signing identities.
+ *
+ * For direct control of the signing identity used to sign application images or installers use
+ * {@link #macAppImageSignIdentity(String) macAppImageSignIdentity} and/or
+ * {@link #macInstallerSignIdentity(String) macInstallerSignIdentity}.
+ *
+ * This option cannot be combined with {@link #macAppImageSignIdentity(String) macAppImageSignIdentity} or
+ * {@link #macInstallerSignIdentity(String) macInstallerSignIdentity}.
+ *
+ * @param username the username
+ * @return this map of options
+ */
+ public JpackageOptions macSigningKeyUserName(String username) {
+ put("--mac-signing-key-user-name", username);
+ return this;
+ }
+
+ /**
+ * Name of the keychain to search for the signing identity.
+ *
+ * If not specified, the standard keychains are used.
+ *
+ * @param keychain the keychain name
+ * @return this map of options
+ */
+ public JpackageOptions macSigningKeychain(String keychain) {
+ put("--mac-signing-keychain", keychain);
+ return this;
+ }
+
+ /**
+ * Qualified name of the application main class to execute.
+ *
+ * This option can only be used if {@link #mainJar(String) mainJar} is specified.
+ *
+ * @param mainClass the main class
+ * @return this map of options
+ */
+ public JpackageOptions mainClass(String mainClass) {
+ put("--main-class", mainClass);
+ return this;
+ }
+
+ /**
+ * The main JAR of the application; containing the main class.
+ *
+ * Either {@link #module(String, String) module} or {@link #mainJar(String) mainJar} option can be specified but
+ * not both.
+ *
+ * @param jar the path relative to the input path
+ * @return this map of options
+ */
+ @SuppressWarnings("JavadocDeclaration")
+ public JpackageOptions mainJar(String jar) {
+ put("--main-jar", jar);
+ return this;
+ }
+
+ /**
+ * The main module and main class of the application.
+ *
+ * This module must be located on the {@link #modulePath(String...) module path}.
+ *
+ * When this option is specified, the main module will be linked in the Java runtime image.
+ *
+ * Either {@link #module(String, String) module} or {@link #mainJar(String) mainJar} option can be specified but
+ * not both.
+ *
+ * @param name the module name
+ * @return this map of options
+ */
+ public JpackageOptions module(String name) {
+ put("--module", name);
+ return this;
+ }
+
+ /**
+ * The main module and main class of the application.
+ *
+ * This module must be located on the {@link #modulePath(String...) module path}.
+ *
+ * When this option is specified, the main module will be linked in the Java runtime image.
+ *
+ * Either {@link #module(String, String) module} or {@link #mainJar(String) mainJar} option can be specified but
+ * not both.
+ *
+ * @param name the module name
+ * @param mainClass the main class
+ * @return this map of options
+ */
+ @SuppressWarnings("JavadocDeclaration")
+ public JpackageOptions module(String name, String mainClass) {
+ put("--module-name", name + "/" + mainClass);
+ return this;
+ }
+
+ /**
+ * List of module 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.
+ *
+ * @param path one or more path
+ * @return this map of options
+ */
+ public JpackageOptions modulePath(String... path) {
+ put("--module-path", String.join(":", path));
+ return this;
+ }
+
+ /**
+ * Name of the application and/or package.
+ *
+ * @param name the name
+ * @return this map of options
+ */
+ public JpackageOptions name(String name) {
+ put("--name", name);
+ return this;
+ }
+
+ /**
+ * Associates {@code null} with the specified key in this map. If the map previously contained a mapping for the
+ * key, the old value is replaced.
+ *
+ * @param key key with which the specified value is to be associated
+ */
+ public void put(String key) {
+ put(key, null);
+ }
+
+ /**
+ * Path to override jpackage resources.
+ *
+ * Icons, template files, and other resources of jpackage can be over-ridden by adding replacement resources to
+ * this directory.
+ *
+ * @param path absolute path or relative to the current directory
+ * @return this map of options
+ */
+ public JpackageOptions resourceDir(String path) {
+ put("--resource-dir", path);
+ return this;
+ }
+
+ /**
+ * Path of the predefined runtime image that will be copied into the application image.
+ *
+ * If not specified, jpackage will run jlink to create the runtime image using options:
+ * {@link JlinkOptions#stripNativeCommands(boolean) stripNativeCommands}
+ * {@link JlinkOptions#stripDebug(boolean) stripDebug} {@link JlinkOptions#noManPages(boolean) noManPages}
+ * {@link JlinkOptions#noHeaderFiles(boolean) noHeaderFiles}
+ *
+ * Option is required when creating a runtime package.
+ *
+ * @param path absolute path or relative to the current directory
+ * @return this map of options
+ */
+ public JpackageOptions runtimeImage(String path) {
+ put("--runtime-image", path);
+ return this;
+ }
+
+ /**
+ * Strip debug information.
+ *
+ * @param stripDebug {@code true} to strip debug info, {@code false} otherwise
+ * @return this map of options
+ */
+ public JpackageOptions stripDebug(boolean stripDebug) {
+ if (stripDebug) {
+ put("--strip-debug");
+ } else {
+ remove("--strip-debug");
+ }
+ return this;
+ }
+
+ /**
+ * Path of a new or empty directory used to create temporary files.
+ *
+ * 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.
+ *
+ * @param path absolute path or relative to the current directory
+ * @return this map of options
+ */
+ public JpackageOptions temp(String path) {
+ put("--temp", path);
+ return this;
+ }
+
+ /**
+ * The type of package to create.
+ *
+ * If this option is not specified a platform dependent default type will be created.
+ *
+ * @param type the package type
+ * @return this map of options
+ */
+ public JpackageOptions type(PackageType type) {
+ put("--type", type.type);
+ return this;
+ }
+
+ /**
+ * Vendor of the application.
+ *
+ * @param vendor the vendor
+ * @return this map of options
+ */
+ public JpackageOptions vendor(String vendor) {
+ put("--vendor", vendor);
+ return this;
+ }
+
+ /**
+ * Enables verbose output.
+ *
+ * @param verbose {@code true} to enable verbose tracing, {@code false} otherwise.
+ * @return this map of options
+ */
+ public JpackageOptions verbose(boolean verbose) {
+ if (verbose) {
+ put("--verbose");
+ } else {
+ remove("--verbose");
+ }
+ return this;
+ }
+
+ /**
+ * Creates a console launcher for the application, should be specified for application which requires console
+ * interactions.
+ *
+ * @param winConsole {@code true} to create a console launcher, {@code false} otherwise
+ * @return this map of options
+ */
+ public JpackageOptions winConsole(boolean winConsole) {
+ if (winConsole) {
+ put("--win-console");
+ } else {
+ remove("--win-console");
+ }
+ return this;
+ }
+
+ /**
+ * Adds a dialog to enable the user to choose a directory in which the product is installed.
+ *
+ * @param winDirChooser {@code true} to let the user choose a directory, {@code false} otherwise
+ * @return this map of options
+ */
+ public JpackageOptions winDirChooser(boolean winDirChooser) {
+ if (winDirChooser) {
+ put("--win-dir-chooser");
+ } else {
+ remove("--win-dir-chooser");
+ }
+ return this;
+ }
+
+ /**
+ * URL where user can obtain further information or technical support.
+ *
+ * @param helpUrl the help URL
+ * @return this map of options
+ */
+ public JpackageOptions winHelpUrl(String helpUrl) {
+ put("--win-help-url", helpUrl);
+ return this;
+ }
+
+ /**
+ * Request to add a Start Menu shortcut for this application.
+ *
+ * @param winMenu {@code true} to add a start menu shortcut, {@code false} otherwise
+ * @return this map of options
+ */
+ public JpackageOptions winMenu(boolean winMenu) {
+ if (winMenu) {
+ put("--win-menu");
+ } else {
+ remove("--win-menu");
+ }
+ return this;
+ }
+
+ /**
+ * Start Menu group this application is placed in.
+ *
+ * @param menuGroup the menu group
+ * @return this map of options
+ */
+ public JpackageOptions winMenuGroup(String menuGroup) {
+ put("--win-menu-group", menuGroup);
+ return this;
+ }
+
+ /**
+ * Request to perform an install on a per-user basis.
+ *
+ * @param winPerUserInstall {@code true} for per-user install, {@code false} otherwise
+ * @return this map of options
+ */
+ public JpackageOptions winPerUserInstall(boolean winPerUserInstall) {
+ if (winPerUserInstall) {
+ put("--win-per-user-install");
+ } else {
+ remove("--win-per-user-install");
+ }
+ return this;
+ }
+
+ /**
+ * Request to create a desktop shortcut for this application.
+ *
+ * @param winShortcut {@code true} to create a shortcut, {@code false} otherwise
+ * @return this map of options
+ */
+ public JpackageOptions winShortcut(boolean winShortcut) {
+ if (winShortcut) {
+ put("--win-shortcut");
+ } else {
+ remove("--win-shortcut");
+ }
+ return this;
+ }
+
+ /**
+ * Adds a dialog to enable the user to choose if shortcuts will be created by installer.
+ *
+ * @param shortcutPrompt {@code true} to add a prompt; {@code false} otherwise
+ * @return this map of options
+ */
+ public JpackageOptions winShortcutPrompt(boolean shortcutPrompt) {
+ if (shortcutPrompt) {
+ put("--win-shortcut-prompt");
+ } else {
+ remove("--win-shortcut-prompt");
+ }
+ return this;
+ }
+
+ /**
+ * URL of available application update information.
+ *
+ * @param url the URL
+ * @return this map of options
+ */
+ public JpackageOptions winUpdateUrl(String url) {
+ put("--win-update-url", url);
+ return this;
+ }
+
+ /**
+ * UUID associated with upgrades for this package.
+ *
+ * @param uuid the uuid
+ * @return this map of options
+ */
+ public JpackageOptions winUpgradeUuid(String uuid) {
+ put("--win-upgrade-uuid", uuid);
+ return this;
+ }
+
+ /**
+ * The package types.
+ */
+ public enum PackageType {
+ APP_IMAGE("app_image"),
+ DEB("deb"),
+ DMG("dmg"),
+ EXE("exe"),
+ MSI("msi"),
+ PKG("pkg"),
+ RPM("rpm");
+
+ final String type;
+
+ PackageType(String type) {
+ this.type = type;
+ }
+ }
+
+ /**
+ * Name of launcher, and a path to a Properties file that contains a list of key, value pairs.
+ *
+ * The keys {@code module}, {@code main-jar}, {@code main-class}, {@code description},
+ * {@code arguments}, {@code java-options}, {@code app-version}, {@code icon},
+ * {@code launcher-as-service}, {@code win-console}, {@code win-shortcut}, {@code win-menu},
+ * {@code linux-app-category}, and {@code 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.
+ *
+ * @param name the name
+ * @param path absolute path or relative to the current directory
+ */
+ public record Launcher(String name, String path) {
+ }
+}
diff --git a/src/main/java/rife/bld/operations/ZipCompression.java b/src/main/java/rife/bld/operations/ZipCompression.java
new file mode 100644
index 0000000..4d5722a
--- /dev/null
+++ b/src/main/java/rife/bld/operations/ZipCompression.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2024 Erik C. Thauvin (https://erik.thauvin.net/)
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ */
+package rife.bld.operations;
+
+/**
+ * The zip compression levels for jlink and jmod.
+ *
+ * @author Erik C. Thauvin
+ * @since 2.0.2
+ */
+public enum ZipCompression {
+ ZIP_0("zip-0"),
+ ZIP_1("zip-1"),
+ ZIP_2("zip-2"),
+ ZIP_3("zip-3"),
+ ZIP_4("zip-4"),
+ ZIP_5("zip-5"),
+ ZIP_6("zip-6"),
+ ZIP_7("zip-7"),
+ ZIP_8("zip-8"),
+ ZIP_9("zip-9");
+
+ public final String level;
+
+ ZipCompression(String level) {
+ this.level = level;
+ }
+}
diff --git a/src/test/java/rife/bld/operations/TestJlinkOperation.java b/src/test/java/rife/bld/operations/TestJlinkOperation.java
new file mode 100644
index 0000000..88086e7
--- /dev/null
+++ b/src/test/java/rife/bld/operations/TestJlinkOperation.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2024 Erik C. Thauvin (https://erik.thauvin.net/)
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ */
+package rife.bld.operations;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import rife.bld.operations.exceptions.ExitStatusException;
+import rife.tools.FileUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static rife.bld.operations.JlinkOptions.CompressionLevel;
+
+public class TestJlinkOperation {
+ private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
+ private final PrintStream stdout = System.out;
+
+ @AfterEach
+ public void tearDown() {
+ System.setOut(stdout);
+ }
+
+ @Test
+ void testArguments() {
+ var args = new HashMap
+ *
+ */
+ public static class CommandLineTokenizer {
+ private final StringBuilder buf_ = new StringBuilder();
+ private final Reader input_;
+ private int ch_;
+
+ public CommandLineTokenizer(Reader input) throws IOException {
+ input_ = input;
+ ch_ = input.read();
+ }
+
+ public String nextToken() throws IOException {
+ trimWhitespaceOrComments();
+ if (ch_ == -1) {
+ return null;
+ }
+
+ buf_.setLength(0); // reset buffer
+
+ char quote = 0;
+ while (ch_ != -1) {
+ if (ch_ == '\'' || ch_ == '"') { // quotes
+ if (quote == 0) { // begin quote
+ quote = (char) ch_;
+ } else if (quote == ch_) { // end quote
+ quote = 0;
+ } else {
+ buf_.append((char) ch_);
+ }
+ } else if (ch_ == '\\') { // escaped
+ ch_ = input_.read();
+ buf_.append(handleEscapeSequence());
+ } else if (quote == 0 && Character.isWhitespace(ch_)) { // whitespaces
+ break;
+ } else {
+ buf_.append((char) ch_);
+ }
+ ch_ = input_.read();
+ }
+ return buf_.toString();
+ }
+
+ private char handleEscapeSequence() {
+ return switch (ch_) {
+ case -1 -> '\\';
+ case 'n' -> '\n';
+ case 'r' -> '\r';
+ case 't' -> '\t';
+ case 'f' -> '\f';
+ default -> (char) ch_;
+ };
+ }
+
+ private void trimWhitespaceOrComments() throws IOException {
+ while (ch_ != -1) {
+ if (Character.isWhitespace(ch_)) { // Skip whitespaces
+ ch_ = input_.read();
+ } else if (ch_ == '#') {
+ // Skip the entire comment until a new line or end of input
+ do {
+ ch_ = input_.read();
+ } while (ch_ != -1 && ch_ != '\n' && ch_ != '\r');
+ } else {
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/rife/bld/operations/JlinkOperation.java b/src/main/java/rife/bld/operations/JlinkOperation.java
new file mode 100644
index 0000000..31aafc7
--- /dev/null
+++ b/src/main/java/rife/bld/operations/JlinkOperation.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 Erik C. Thauvin (https://erik.thauvin.net/)
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ */
+package rife.bld.operations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Create run-time images using the jlink tool.
+ *
+ * @author Erik C. Thauvin
+ * @since 2.0.2
+ */
+public class JlinkOperation extends AbstractToolProviderOperation