Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 22 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 22 minutes free
Free: An app image for macOS · The options you set first · Platform limits · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 15 minutes.

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

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:

A macOS app bundle from two modular JARs Python · appimage_session.py
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/Kirana is the native launcher, Contents/app holds the launcher’s configuration, Contents/runtime holds the linked Java runtime, and Contents/Resources/Kirana.icns is the default icon, used because no --icon was given.
  • The options went where users see them. --name named the bundle and the launcher, and --app-version 1.0.0 became the version in Info.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.

jpackage turns modular JARs into an app image with a launcher and its own runtime, then into the installers of the current platform.mods/kirana.app.jarkirana.bill.jarjpackage --type app-imageruns jlink for the runtimeKirana.app/Contents/MacOS/Kirana (launcher)app/Kirana.cfgruntime/ (linked JDK modules)Installers, one platform eachOn macOS: .dmg or .pkgOn Windows: .msi or .exeOn Linux: .deb or .rpm--type app-imagejpackage --app-image--type …

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.

  1. The input is the application: here two modular JARs, kirana.app.jar and kirana.bill.jar, in a folder called mods.
  2. jpackage with --type app-image runs jlink to build a runtime from the modules the application needs.
  3. 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.
  4. 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.

  1. Question 1 of 5 You build on a Mac. Can jpackage there produce the .msi installer for Windows users?

    Choose one answer.

    Show the answer to question 1

    Answer: No: each format must be built on the platform it is for, so the .msi comes from a Windows machine

    The jpackage documentation says each format must be built on the platform it runs on, with no cross-platform support. A product for three systems needs three builds.

  2. Question 2 of 5 What does --type app-image produce on macOS?

    Choose one answer.

    Show the answer to question 2

    Answer: An .app bundle with a native launcher, its configuration and a linked Java runtime, but no installer

    The app image is the application ready to run: in the lesson, Kirana.app with Contents/MacOS/Kirana, Contents/app and Contents/runtime. Installers are built from it with --app-image.

  3. Question 3 of 5 Which option sets the version that macOS shows for the app?

    Choose one answer.

    Show the answer to question 3

    Answer: --app-version

    jpackage writes the value of --app-version into the bundle's Info.plist; the lesson read it back with plutil as 1.0.0.

  4. Question 4 of 5 How many MiB did the lesson's Kirana.app bundle measure, runtime included?

    Type a number.

    Show the answer to question 4

    Answer: 55.5 MiB (anything from 55.4 to 55.6 counts)

    The app image session measured 55.5 MiB for the whole bundle, on Temurin 25.0.4.1 for macOS on Apple silicon.

  5. Question 5 of 5 Which step does the JDK not perform for a Mac app you distribute to the public?

    Choose one answer.

    Show the answer to question 5

    Answer: Notarisation with Apple's notary service

    jpackage links the runtime, writes the launcher and builds installers, and it can sign with --mac-sign. Notarisation goes through Apple's own tools, notarytool and stapler, which come with Xcode.

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.

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.

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.

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).

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • jpackage bundles an application with a linked Java runtime into an app image, then into a native installer.
  • --type app-image is the quickest check: on macOS it made a 55.5 MiB bundle that runs without Java installed.
  • --name, --app-version, --icon and --module or --main-jar set what users see and what starts.
  • Every installer format is built on its own platform; signing and notarisation are separate platform steps.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.