Building Native Installers for Java Applications with JPackage

Creating distributable installers for Java desktop applications has traditionally been a complex process requiring third-party tools and extensive configuration. JPackage, introduced in Java 14 and stabilized in later versions, eliminates this complexity by providing a built-in command-line tool that generates platform-native installers directly from Java applications.

Understanding JPackage

JPackage is a powerful packaging tool that bundles Java applications with a custom Java Runtime Environment (JRE), producing platform-specific installers such as EXE and MSI for Windows, DMG and PKG for macOS, and DEB and RPM for Linux distributions. This approach ensures end users can install and run applications without requiring a separate Java installation on their systems.

The tool integrates with jlink under the hood to create optimized runtime images, resulting in smaller distribution packages that include only the necessary Java modules. JPackage also supports desktop integration features including application shortcuts, Start Menu entries, file associations, and custom icons.

Prerequisites and System Requirements

Before using JPackage, ensure the following components are available on the development system:

Java Development Kit: JDK 14 or later (Java 17 or 21 recommended for production use)

Application Packaging: Applications must be packaged as fat JAR files with all dependencies bundled

Platform-Specific Tools:

  • Windows: WiX Toolset 3.x for creating MSI/EXE installers
  • Linux: dpkg (Debian/Ubuntu) or rpm (RHEL/Fedora/CentOS) package managers
  • macOS: Xcode Command Line Tools for DMG/PKG generation

Important Note: Cross-platform packaging is not supported — Windows installers must be built on Windows, macOS packages on macOS, and Linux packages on their respective distributions.

Using JPackage via Command Line

The command-line interface provides direct control over the packaging process. Here are platform-specific examples assuming a fat JAR named myapp-jar-with-dependencies.jar:

Windows Installer

<code>jpackage \
  --type exe \
  --input . \
  --name MyApplication \
  --main-jar myapp-jar-with-dependencies.jar \
  --main-class com.example.Main \
  --icon myapp.ico \
  --app-version 1.0.0 \
  --vendor "Your Company" \
  --win-menu \
  --win-shortcut \
  --win-dir-chooser

macOS Package

<code>jpackage \
  --type dmg \
  --input . \
  --name MyApplication \
  --main-jar myapp-jar-with-dependencies.jar \
  --main-class com.example.Main \
  --icon myapp.icns \
  --app-version 1.0.0 \
  --mac-package-identifier com.example.myapp \
  --mac-package-name "My Application"

Linux Package

<code>jpackage \
  --type deb \
  --input . \
  --name MyApplication \
  --main-jar myapp-jar-with-dependencies.jar \
  --main-class com.example.Main \
  --icon myapp.png \
  --app-version 1.0.0 \
  --linux-package-name myapplication \
  --linux-app-category "Utility"

Maven Plugin Integration

Integrating JPackage into the Maven build lifecycle provides a streamlined, automated approach to creating installers. The configuration requires three key plugins working in sequence.

Complete Maven Configuration

<properties>
    <app.name>My Application Name</app.name>
    <app.version>1.0.0</app.version>
    <vendor.name>Your Company</vendor.name>
    <copyright>Copyright © 2025 Your Company</copyright>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>${maven.compiler.source}</maven.compiler.target>
</properties>

<build>
    <finalName>${app.name}-${app.version}</finalName>
    <plugins>
        <!-- Compile application with specified JDK version -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <source>${maven.compiler.source}</source>
                <target>${maven.compiler.target}</target>
                <release>${maven.compiler.source}</release>
            </configuration>
        </plugin>

        <!-- Create fat JAR with all dependencies -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>3.7.1</version>
            <configuration>
                <archive>
                    <manifest>
                        <mainClass>com.example.app.Main</mainClass>
                    </manifest>
                </archive>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
            <executions>
                <execution>
                    <id>make-assembly</id>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>

        <!-- Generate native installer -->
        <plugin>
            <groupId>com.github.akman</groupId>
            <artifactId>jpackage-maven-plugin</artifactId>
            <version>0.1.5</version>
            <executions>
                <execution>
                    <id>jpackage-installer</id>
                    <phase>verify</phase>
                    <goals>
                        <goal>jpackage</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <name>${app.name}</name>
                <appversion>${app.version}</appversion>
                <copyright>${copyright}</copyright>
                <vendor>${vendor.name}</vendor>
                <mainjar>${app.name}-${app.version}-jar-with-dependencies.jar</mainjar>
                <input>target/</input>
                <mainclass>com.example.app.Main</mainclass>
                <type>MSI</type>
                <dest>installers/</dest>
                <icon>${project.basedir}/src/main/resources/app-icon.ico</icon>
                
                <!-- Windows-specific options -->
                <winconsole>false</winconsole>
                <winmenu>true</winmenu>
                <winshortcut>true</winshortcut>
                <windirchooser>true</windirchooser>
                
                <!-- JVM options -->
                <javaoptions>
                    <javaoption>--enable-preview</javaoption>
                    <javaoption>-Xmx2048m</javaoption>
                </javaoptions>
            </configuration>
        </plugin>
    </plugins>
</build>

Building the Installer

Execute the following Maven commands to create the installer:

<code>mvn clean install
mvn jpackage:jpackage<span style="background-color: initial; font-family: inherit; font-size: inherit; text-align: initial;"></span>

The first command compiles the application and creates the fat JAR, while the second generates the platform-specific installer in the configured destination directory.

Essential JPackage Parameters

Following key parameters enables fine-tuned control over the packaging process:

ParameterDescriptionExample
--inputDirectory containing application JAR filestarget/
--nameApplication name displayed to usersMyApplication
--main-jarPrimary JAR file to executeapp-1.0.0.jar
--main-classFully qualified main class namecom.example.Main
--typePackage format (exe, msi, dmg, pkg, deb, rpm)msi
--iconApplication icon file (.ico, .icns, .png)app-icon.ico
--app-versionVersion number for the installer1.0.0
--vendorCompany or developer nameYour Company
--destOutput directory for generated installerinstallers/
--java-optionsJVM arguments passed at runtime-Xmx2048m
--win-menuAdd Windows Start Menu shortcuttrue
--win-shortcutCreate Windows desktop shortcuttrue
--win-dir-chooserAllow installation directory selectiontrue
--mac-package-identifierUnique macOS bundle identifiercom.example.app
--linux-package-nameLinux package name (lowercase)myapplication

Common Pitfalls and Troubleshooting

Several common issues can arise during the packaging process:

Incorrect JAVA_HOME Configuration: Ensure the JAVA_HOME environment variable points to JDK 14 or later containing the jpackage tool. Verify by running jpackage --version.

Missing Platform Tools: Windows requires WiX Toolset 3.x to be installed and available on the system PATH. Linux systems need dpkg or rpm package managers installed.

Fat JAR Dependencies: All application dependencies must be included in the JAR file specified by --main-jar. Standard JAR files without bundled dependencies will fail at runtime.

Icon Format Requirements: Windows requires .ico format, macOS uses .icns, and Linux accepts .png. Using incorrect formats causes packaging failures.

Version Number Format: The --app-version parameter must follow semantic versioning (e.g., 1.0.0). Invalid formats cause installer creation to fail.

Module Path Issues: For modular applications, ensure all required modules are specified or use --add-modules ALL-MODULE-PATH.

Advantages of Using JPackage

JPackage offers several significant benefits for Java application distribution:

Simplified Distribution: Creates professional installers that match platform conventions, eliminating manual installation steps for end users.

No Java Installation Required: Bundled JRE ensures applications run on systems without pre-installed Java.

Reduced Package Size: Integration with jlink creates custom runtime images containing only necessary modules, significantly reducing installer size.

Native Integration: Automatic creation of desktop shortcuts, Start Menu entries, and application uninstallers provides a professional user experience.

Single Build Tool: Eliminates dependency on commercial third-party packaging tools, reducing costs and simplifying the build pipeline.

Best Practices for Production Use

Following these practices ensures robust, maintainable packaging workflows:

Automate the Build Process: Integrate JPackage into CI/CD pipelines using Maven or Gradle plugins for consistent, repeatable builds.

Test on Target Platforms: Always test generated installers on clean systems matching the target platform to verify dependencies and functionality.

Version Installers Appropriately: Use semantic versioning and include version numbers in installer filenames for clear version tracking.

Sign Installers: Code-sign Windows and macOS installers to avoid security warnings and build user trust.

Document System Requirements: Clearly communicate minimum operating system versions and any additional dependencies to end users.

Optimize Runtime Images: Use jlink’s module optimization to minimize installer size while maintaining required functionality.

Conclusion

JPackage represents a significant advancement in Java application distribution, providing built-in tooling that matches the capabilities of commercial packaging solutions. By bundling applications with custom runtime environments and generating platform-native installers, JPackage dramatically simplifies the end-user installation experience while reducing distribution complexity for developers.

The Maven plugin integration enables seamless incorporation into existing build workflows, making installer creation as straightforward as building a JAR file. For desktop Java applications requiring professional distribution, JPackage has become the de facto standard approach in modern Java development.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.