Java Module 17 – Modules, packaging and distribution
jpackage: native Java app installers
Turn a Java app into a macOS app bundle, .dmg and .pkg with jpackage, set its name, version and launcher, and see what Windows and Linux packages need.
What you will learn
- Create an app image and a native installer with jpackage
- Set the application name, version, icon and main class
- Explain the platform limits of jpackage
Before you start
What you’ll learn · Before you start
What you will learn
- Create an app image and a native installer with jpackage
- Set the application name, version, icon and main class
- Explain the platform limits of jpackage
Before you start
On this page
jpackage turns a Java application into a native application for one operating system: an application image
with its own Java runtime and a native launcher, and from that image an installer in the platform’s own format,
.dmg or .pkg on macOS, .msi or .exe on Windows, .deb or .rpm on Linux. It builds the runtime with jlink,
or copies one you made with --runtime-image, so users install an ordinary app and never install Java. The tool
arrived with JDK 16 (JEP 392). Its documentation sets the main limit plainly: each format must be built on the
platform it runs on, and there is no cross-platform packaging. Signing and Apple’s notarisation are platform steps
that the JDK does not perform.
This lesson packages the shop’s billing app from the jlink lesson on a Mac with Apple silicon, with Eclipse Temurin 25.0.4.1, and shows every command with its output.
An app image for macOS
--type app-image builds the application bundle without an installer, which is the quickest way to check a package:
from session import run
# The billing app's two modules, packed as modular JARs, then turned into a macOS application bundle with jpackage;
# its top folders, its launcher run twice, and the version it records.
run(
"""
javac -d out/kirana.bill $(find bill/src -name "*.java")
javac -d out/kirana.app --module-path out $(find app/src -name "*.java")
jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
jar --create --file mods/kirana.app.jar --main-class com.example.kirana.app.Main -C out/kirana.app .
jpackage --type app-image --name Kirana --app-version 1.0.0 --vendor "Example Kirana Stores" --copyright "Example Kirana Stores" --module-path mods --module kirana.app/com.example.kirana.app.Main --dest dist
find dist/Kirana.app -maxdepth 3 -not -name "._*" | sort
dist/Kirana.app/Contents/MacOS/Kirana
dist/Kirana.app/Contents/MacOS/Kirana --xml
plutil -extract CFBundleShortVersionString raw dist/Kirana.app/Contents/Info.plist
java Size.java dist/Kirana.app
"""
) Output
# openjdk version "25.0.4.1" LTS $ javac -d out/kirana.bill $(find bill/src -name "*.java") $ javac -d out/kirana.app --module-path out $(find app/src -name "*.java") $ jar --create --file mods/kirana.bill.jar -C out/kirana.bill . $ jar --create --file mods/kirana.app.jar --main-class com.example.kirana.app.Main -C out/kirana.app . $ jpackage --type app-image --name Kirana --app-version 1.0.0 --vendor "Example Kirana Stores" --copyright "Example Kirana Stores" --module-path mods --module kirana.app/com.example.kirana.app.Main --dest dist Warning: No JDK Modules found $ find dist/Kirana.app -maxdepth 3 -not -name "._*" | sort dist/Kirana.app dist/Kirana.app/Contents dist/Kirana.app/Contents/_CodeSignature dist/Kirana.app/Contents/_CodeSignature/CodeResources dist/Kirana.app/Contents/app dist/Kirana.app/Contents/app/.jpackage.xml dist/Kirana.app/Contents/app/Kirana.cfg dist/Kirana.app/Contents/Info.plist dist/Kirana.app/Contents/MacOS dist/Kirana.app/Contents/MacOS/Kirana dist/Kirana.app/Contents/PkgInfo dist/Kirana.app/Contents/Resources dist/Kirana.app/Contents/Resources/Kirana.icns dist/Kirana.app/Contents/runtime dist/Kirana.app/Contents/runtime/Contents $ dist/Kirana.app/Contents/MacOS/Kirana 3 lines, Sharma Kirana Store: ₹71,400.00 $ dist/Kirana.app/Contents/MacOS/Kirana --xml <bill><line quantity="12" paise="245000">Basmati rice, 25 kg</line><line quantity="4" paise="487500">Toor dal, 30 kg</line><line quantity="10" paise="225000">Sugar, 50 kg</line></bill> $ plutil -extract CFBundleShortVersionString raw dist/Kirana.app/Contents/Info.plist 1.0.0 $ java Size.java dist/Kirana.app Kirana.app 55.5 MiB
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 appimage_session.py
- The warning is about this JDK, not the app. jpackage looked for JMOD files and found none, then linked the runtime from the JDK’s own run-time image, as jlink does on this build; the bundle works.
- A macOS bundle has a fixed layout.
Contents/MacOS/Kiranais the native launcher,Contents/appholds the launcher’s configuration,Contents/runtimeholds the linked Java runtime, andContents/Resources/Kirana.icnsis the default icon, used because no--iconwas given. - The options went where users see them.
--namenamed the bundle and the launcher, and--app-version 1.0.0became the version inInfo.plist, which the Finder’s information window shows. - It is self-contained. The whole bundle measured 55.5 MiB, runtime included, and it runs on a Mac without Java.
The app image is also the input of every installer: build it, start it and test it, then wrap the same image with
--app-image, so that the installer holds exactly what was tested. Its shape depends on the platform. The jpackage
guide shows a folder with the launcher in bin on Linux, and a folder with an .exe launcher at its top on Windows,
where macOS has the .app bundle above. When the runtime needs jlink options that jpackage does not pass, such as
--include-locales, link it yourself and hand it over with --runtime-image.
From modules to an application image, and from the image to installers
Text description of the diagram
The diagram shows what jpackage produces, from top to bottom.
- The input is the application: here two modular JARs, kirana.app.jar and kirana.bill.jar, in a folder called mods.
- jpackage with --type app-image runs jlink to build a runtime from the modules the application needs.
- The result on macOS is a bundle, Kirana.app. Inside its Contents folder are the launcher, MacOS/Kirana; its configuration, app/Kirana.cfg; and the linked runtime in runtime.
- From that image, jpackage builds installers with --app-image and a --type. Each format is built on its own platform: .dmg or .pkg on macOS, .msi or .exe on Windows, .deb or .rpm on Linux.
The options you set first
| Option | What it does |
|---|---|
--type app-image, dmg, pkg … |
What to build; without it, the platform’s default installer type |
--name, --app-version |
The application’s name and version, shown by the operating system |
--vendor, --copyright, --description |
Text the installer and the system show about the app |
--icon |
The application’s icon: an .icns file on macOS, .ico on Windows, .png on Linux |
--module-path and --module |
A modular app: where its modules are, and the main module with its main class |
--input and --main-jar |
A plain JAR app: the folder of JARs to package, and the JAR with the main class |
--add-modules |
The JDK modules for the runtime, when the default set is not what you want |
--runtime-image |
A runtime you linked yourself, copied in instead of running jlink |
--java-options, --arguments |
JVM options and default program arguments that the launcher passes |
--add-launcher |
A second launcher in the same image, with its own arguments or main class |
--jlink-options |
The options jpackage passes to jlink, replacing its defaults |
--mac-sign |
Signs the image or package with a signing identity from the keychain |
Platform limits
| Platform | Installer types | Needed besides the JDK |
|---|---|---|
| macOS | dmg, pkg |
Xcode command line tools, when signing with --mac-sign or customising the DMG with --icon |
| Windows | msi, exe |
WiX 3.0 or later |
| Linux | deb, rpm |
fakeroot on Ubuntu for deb, rpm-build on Red Hat for rpm |
The table follows the jpackage guide’s list of prerequisites. Every format is built on its own platform, so a
product for three platforms needs three build machines or three CI runners. --type app-image works everywhere and
makes the platform’s application image without an installer.
Check yourself
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Inside the app image: launchers, configuration and runtime, measured Pro: opens with a Pro, Premium or Ultimate pass
An app image is a folder of plain files, and reading them explains how the app starts and what it will do in the field. This part builds a bundle with an icon and two launchers, reads its configuration, measures its runtime, and reproduces two costly defaults.
In this part:
- An icon, a second launcher and Java options
- The runtime, and what jpackage’s default options strip
- A plain JAR gets a far larger runtime by default
- Settings that point inside the bundle
3 runnable examples (KiranaXml.properties, MakeIcon.java, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
A .dmg and a .pkg built, inspected, and refused by Gatekeeper Pro: opens with a Pro, Premium or Ultimate pass
An installer is what users download, so it is where size, layout and trust are decided. This part builds both macOS formats from the same image, looks inside them, and reproduces what macOS says about an app that is not signed for distribution.
In this part:
- The disk image
- The installer package, and two signatures that are missing
- Choosing between the two formats
2 runnable examples (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Windows and Linux packages, a release checklist, and an exercise Pro: opens with a Pro, Premium or Ultimate pass
A product that runs on three systems ships three sets of installers, each built on its own system. This part gives the commands and tools for Windows and Linux, a checklist that keeps the three builds consistent, and an exercise with a rubric.
In this part:
- The same app on Windows and Linux
- A release checklist for three platforms
- Exercise: package the tiffin app
- Rubric
about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
-
Mid to senior
Users of your desktop app, packaged with jpackage, send crash reports whose stack traces say Unknown Source and end with "Failed to launch JVM". Explain both lines, and change the build so the next release's reports are useful.
-
Senior to staff
Your team develops on Macs, but the product must ship as a .dmg, an .msi and a .deb, built from the same release. Design the build, and say how you keep the three installers consistent.
-
Mid to senior
Your app runs fine on the Mac that built it, but customers who download the .dmg cannot open it, and macOS reports that the app cannot be checked. What is missing, and what must the release pipeline add?
Key takeaways
- jpackage bundles an application with a linked Java runtime into an app image, then into a native installer.
--type app-imageis the quickest check: on macOS it made a 55.5 MiB bundle that runs without Java installed.--name,--app-version,--iconand--moduleor--main-jarset what users see and what starts.- Every installer format is built on its own platform; signing and notarisation are separate platform steps.
References
- JEP 392: Packaging Tool (OpenJDK)
- The jpackage Command (Java SE 25) (Oracle)
- Packaging Tool User Guide (Java SE 25): Packaging Overview (Oracle)
- Packaging Tool User Guide (Java SE 25) (Oracle)
- Notarizing macOS software before distribution (Apple)
- The jlink Command (Java SE 25) (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress