Runtime jlink plus JAR aplikasi sudah mandiri secara teknis, tetapi bentuknya masih folder yang harus diekstrak manual dan dijalankan lewat skrip. Pengguna akhir desktop mengharapkan artefak yang lebih ramah: installer .msi di Windows, .dmg di macOS, atau .deb di Linux — lengkap dengan launcher asli, ikon, dan entri menu aplikasi. Alat jpackage pada JDK 25 mengerjakan seluruh rantai itu: menjalankan jlink untuk merakit runtime, menyalin aplikasi, membuat launcher native, lalu membungkusnya menjadi format paket platform target. Chapter ini menguji tiap tahap dengan aplikasi kasir kita dan memetakan format yang tersedia per sistem operasi.
Citra Aplikasi dengan app-image #
Titik masuk paling sederhana adalah --type app-image: bukan installer, melainkan folder aplikasi siap pakai yang sudah berisi launcher native dan runtime penuh. Masukan jpackage adalah direktori input berisi JAR eksekusi:
jpackage --type app-image -n kasirapp -i input \
--main-class toko.AplikasiKasir --main-jar toko-fat.jar --dest out
out/kasirapp/bin/kasirappOutput yang Dihasilkan:
aplikasi-kasir 2.1.0
Rp250,000.00Struktur folder out/kasirapp hasil perintah tersebut:
kasirapp/
├── bin/
│ └── kasirapp <- launcher native
└── lib/
├── app/
│ ├── kasirapp.cfg <- konfigurasi launcher
│ └── toko-fat.jar <- aplikasi kita
├── kasirapp.png
├── libapplauncher.so
└── runtime/ <- hasil jlink di balik layarLauncher bin/kasirapp membaca lib/app/kasirapp.cfg untuk tahu class mana yang dieksekusi dan opsi JVM apa yang disertakan. Isi berkas .cfg pada citra kita:
[Application]
app.classpath=$APPDIR/toko-fat.jar
app.mainclass=toko.AplikasiKasir
[JavaOptions]
java-options=-Djpackage.app-version=1.0Variabel $APPDIR menunjuk direktori lib/app sehingga citra bisa dipindahkan ke lokasi mana pun tanpa path absolut. Folder lib/runtime adalah runtime jlink — jpackage menjalankannya otomatis dengan --add-modules yang dihitung dari aplikasi kita.
Format Installer per Platform #
--type menentukan artefak akhir, dan tiap format punya prasyarat alat eksternal milik sistem operasi. Saat bundler deb dicoba di mesin Linux tanpa fakeroot, jpackage menolak dengan pesan nyata:
Bundler DEB Bundle skipped because of a configuration problem:
Can not find fakeroot. Reason: Cannot run program "fakeroot":
Exec failed, error = 2 (No such file or directory)
Advice to fix: Please install required packagesMatriks lengkapnya:
| Platform | --type | Artefak | Prasyarat |
|---|---|---|---|
| Linux | app-image | folder citra | tanpa alat tambahan |
| Linux | deb | kasirapp_1.0.0-1_amd64.deb | dpkg-deb + fakeroot |
| Linux | rpm | kasirapp-1.0.0-1.x86_64.rpm | rpmbuild |
| Windows | msi, exe | installer WiX | WiX Toolset 3.x |
| macOS | dmg | kasirapp-1.0.0.dmg | alat bawaan macOS |
| macOS | pkg | kasirapp-1.0.0.pkg | pkgbuild |
Contoh lengkap untuk installer Linux dengan metadata paket dan ikon:
jpackage --type deb -n kasirapp -i input \
--main-class toko.AplikasiKasir --main-jar toko-fat.jar \
--app-version 1.0.0 \
--java-options "-Xmx256m" \
--linux-package-name kasirapp \
--linux-deb-maintainer "[email protected]" \
--linux-shortcut \
--icon src/main/resources/kasirapp.png \
--dest outOpsi Launcher dan JVM #
Seluruh opsi pada bagian [JavaOptions] berpindah ke baris perintah launcher setiap kali aplikasi dinyalakan — inilah tempat yang tepat untuk --java-options "-Xmx256m" atau --arguments saat aplikasi Anda menerima argumen CLI. Program berikut memetakan format per platform dan membangkitkan isi berkas .cfg dengan cara yang sama seperti jpackage:
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Peta format jpackage per platform dan pembangkit berkas .cfg launcher. */
public class PetaPaket {
/** Satu format installer: tipe --type, nama berkas hasil, prasyarat alat eksternal. */
record Format(String tipe, String berkas, String prasyarat) {}
static final Map<String, List<Format>> PER_PLATFORM = Map.of(
"linux", List.of(
new Format("app-image", "kasirapp/ (folder citra)", "tanpa alat tambahan"),
new Format("deb", "kasirapp_1.0.0-1_amd64.deb", "dpkg-deb + fakeroot"),
new Format("rpm", "kasirapp-1.0.0-1.x86_64.rpm", "rpmbuild")),
"windows", List.of(
new Format("msi", "kasirapp-1.0.0.msi", "WiX Toolset 3.x"),
new Format("exe", "kasirapp-1.0.0.exe", "WiX Toolset 3.x")),
"macos", List.of(
new Format("dmg", "kasirapp-1.0.0.dmg", "alat bawaan macOS"),
new Format("pkg", "kasirapp-1.0.0.pkg", "pkgbuild")));
/** Bangun isi berkas .cfg launcher — format yang sama dengan keluaran jpackage. */
static String bangunCfg(String jarUtama, String kelasUtama, String versi,
List<String> opsiJvm) {
StringBuilder cfg = new StringBuilder();
cfg.append("[Application]\n");
cfg.append("app.classpath=$APPDIR/").append(jarUtama).append("\n");
cfg.append("app.mainclass=").append(kelasUtama).append("\n");
cfg.append("\n[JavaOptions]\n");
cfg.append("java-options=-Djpackage.app-version=").append(versi).append("\n");
for (String opsi : opsiJvm) {
cfg.append("java-options=").append(opsi).append("\n");
}
return cfg.toString();
}
/** Pilih format pertama yang prasyaratnya terpenuhi oleh alat yang tersedia. */
static String pilihFormat(String platform, Map<String, Boolean> alatTersedia) {
for (Format f : PER_PLATFORM.get(platform)) {
if (f.prasyarat().equals("tanpa alat tambahan")
|| alatTersedia.getOrDefault(f.prasyarat(), false)) {
return f.tipe();
}
}
return "app-image";
}
public static void main(String[] args) {
System.out.println("== Matriks --type jpackage per platform ==");
Map<String, List<Format>> urutan = new LinkedHashMap<>();
urutan.put("linux", PER_PLATFORM.get("linux"));
urutan.put("windows", PER_PLATFORM.get("windows"));
urutan.put("macos", PER_PLATFORM.get("macos"));
for (var e : urutan.entrySet()) {
System.out.printf("%-8s :%n", e.getKey().toUpperCase());
for (Format f : e.getValue()) {
System.out.printf(" %-10s -> %-34s (%s)%n",
f.tipe(), f.berkas(), f.prasyarat());
}
}
System.out.println();
System.out.println("== Berkas .cfg launcher untuk kasirapp ==");
System.out.print(bangunCfg("toko-fat.jar", "toko.AplikasiKasir", "1.0.0",
List.of("-Xmx256m", "-Dfile.encoding=UTF-8")));
System.out.println();
System.out.println("== Keputusan bundler di Linux tanpa fakeroot ==");
Map<String, Boolean> alat = new LinkedHashMap<>();
alat.put("dpkg-deb + fakeroot", false);
alat.put("rpmbuild", false);
System.out.println("terpilih: --type " + pilihFormat("linux", alat));
}
}Output yang Dihasilkan:
== Matriks --type jpackage per platform ==
LINUX :
app-image -> kasirapp/ (folder citra) (tanpa alat tambahan)
deb -> kasirapp_1.0.0-1_amd64.deb (dpkg-deb + fakeroot)
rpm -> kasirapp-1.0.0-1.x86_64.rpm (rpmbuild)
WINDOWS :
msi -> kasirapp-1.0.0.msi (WiX Toolset 3.x)
exe -> kasirapp-1.0.0.exe (WiX Toolset 3.x)
MACOS :
dmg -> kasirapp-1.0.0.dmg (alat bawaan macOS)
pkg -> kasirapp-1.0.0.pkg (pkgbuild)
== Berkas .cfg launcher untuk kasirapp ==
[Application]
app.classpath=$APPDIR/toko-fat.jar
app.mainclass=toko.AplikasiKasir
[JavaOptions]
java-options=-Djpackage.app-version=1.0.0
java-options=-Xmx256m
java-options=-Dfile.encoding=UTF-8
== Keputusan bundler di Linux tanpa fakeroot ==
terpilih: --type app-imageMari kita bedah bagiannya:
record Format(...): satu baris data format — tipe--type, nama berkas yang dihasilkan, dan alat eksternal yang menjadi prasyaratnya. Data ini disarikan dari tabel matriks di atas.PER_PLATFORM: peta platform ke daftar formatnya.Map.ofplusLinkedHashMapsalinan dimainmenjaga urutan cetak Linux → Windows → macOS tetap stabil.bangunCfg(...): pembangkit berkas.cfg— bagian[Application]menunjuk JAR dan class utama lewat$APPDIR, sedangkan[JavaOptions]menampung versi aplikasi dan opsi JVM tambahan persis seperti keluaranjpackagenyata pada citrakasirappkita.pilihFormat(...): pengganti logika pemilihan bundler — iterasi format lalu mengembalikan tipe pertama yang prasyaratnya terpenuhi; karenafakerootdanrpmbuildtidak tersedia, keputusan jatuh keapp-image.
Dengan jpackage, rantai distribusi aplikasi desktop tuntas: satu perintah menghasilkan artefak yang bisa dipasang dengan klik ganda. Namun sebagian besar aplikasi Java masa kini tidak berakhir di desktop pengguna, melainkan di server produksi sebagai proses yang dikonfigurasi lewat environment. Chapter berikutnya memisahkan konfigurasi dari kode — prinsip pertama sebelum aplikasi menyentuh produksi.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PetaPaket.java
Unduh berkasimport java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Peta format jpackage per platform dan pembangkit berkas .cfg launcher. */
public class PetaPaket {
/** Satu format installer: tipe --type, nama berkas hasil, prasyarat alat eksternal. */
record Format(String tipe, String berkas, String prasyarat) {}
static final Map<String, List<Format>> PER_PLATFORM = Map.of(
"linux", List.of(
new Format("app-image", "kasirapp/ (folder citra)", "tanpa alat tambahan"),
new Format("deb", "kasirapp_1.0.0-1_amd64.deb", "dpkg-deb + fakeroot"),
new Format("rpm", "kasirapp-1.0.0-1.x86_64.rpm", "rpmbuild")),
"windows", List.of(
new Format("msi", "kasirapp-1.0.0.msi", "WiX Toolset 3.x"),
new Format("exe", "kasirapp-1.0.0.exe", "WiX Toolset 3.x")),
"macos", List.of(
new Format("dmg", "kasirapp-1.0.0.dmg", "alat bawaan macOS"),
new Format("pkg", "kasirapp-1.0.0.pkg", "pkgbuild")));
/** Bangun isi berkas .cfg launcher — format yang sama dengan keluaran jpackage. */
static String bangunCfg(String jarUtama, String kelasUtama, String versi,
List<String> opsiJvm) {
StringBuilder cfg = new StringBuilder();
cfg.append("[Application]\n");
cfg.append("app.classpath=$APPDIR/").append(jarUtama).append("\n");
cfg.append("app.mainclass=").append(kelasUtama).append("\n");
cfg.append("\n[JavaOptions]\n");
cfg.append("java-options=-Djpackage.app-version=").append(versi).append("\n");
for (String opsi : opsiJvm) {
cfg.append("java-options=").append(opsi).append("\n");
}
return cfg.toString();
}
/** Pilih format pertama yang prasyaratnya terpenuhi oleh alat yang tersedia. */
static String pilihFormat(String platform, Map<String, Boolean> alatTersedia) {
for (Format f : PER_PLATFORM.get(platform)) {
if (f.prasyarat().equals("tanpa alat tambahan")
|| alatTersedia.getOrDefault(f.prasyarat(), false)) {
return f.tipe();
}
}
return "app-image";
}
public static void main(String[] args) {
System.out.println("== Matriks --type jpackage per platform ==");
Map<String, List<Format>> urutan = new LinkedHashMap<>();
urutan.put("linux", PER_PLATFORM.get("linux"));
urutan.put("windows", PER_PLATFORM.get("windows"));
urutan.put("macos", PER_PLATFORM.get("macos"));
for (var e : urutan.entrySet()) {
System.out.printf("%-8s :%n", e.getKey().toUpperCase());
for (Format f : e.getValue()) {
System.out.printf(" %-10s -> %-34s (%s)%n",
f.tipe(), f.berkas(), f.prasyarat());
}
}
System.out.println();
System.out.println("== Berkas .cfg launcher untuk kasirapp ==");
System.out.print(bangunCfg("toko-fat.jar", "toko.AplikasiKasir", "1.0.0",
List.of("-Xmx256m", "-Dfile.encoding=UTF-8")));
System.out.println();
System.out.println("== Keputusan bundler di Linux tanpa fakeroot ==");
Map<String, Boolean> alat = new LinkedHashMap<>();
alat.put("dpkg-deb + fakeroot", false);
alat.put("rpmbuild", false);
System.out.println("terpilih: --type " + pilihFormat("linux", alat));
}
}