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:

bash
jpackage --type app-image -n kasirapp -i input \
  --main-class toko.AplikasiKasir --main-jar toko-fat.jar --dest out
out/kasirapp/bin/kasirapp

Output yang Dihasilkan:

text
aplikasi-kasir 2.1.0
Rp250,000.00

Struktur folder out/kasirapp hasil perintah tersebut:

text
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 layar

Launcher 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:

text
[Application]
app.classpath=$APPDIR/toko-fat.jar
app.mainclass=toko.AplikasiKasir

[JavaOptions]
java-options=-Djpackage.app-version=1.0

Variabel $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:

text
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 packages

Matriks lengkapnya:

Platform--typeArtefakPrasyarat
Linuxapp-imagefolder citratanpa alat tambahan
Linuxdebkasirapp_1.0.0-1_amd64.debdpkg-deb + fakeroot
Linuxrpmkasirapp-1.0.0-1.x86_64.rpmrpmbuild
Windowsmsi, exeinstaller WiXWiX Toolset 3.x
macOSdmgkasirapp-1.0.0.dmgalat bawaan macOS
macOSpkgkasirapp-1.0.0.pkgpkgbuild

Contoh lengkap untuk installer Linux dengan metadata paket dan ikon:

bash Membangun installer .deb (butuh fakeroot terpasang)
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 out

Opsi 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:

java PetaPaket.java
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:

text
== 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-image

Mari kita bedah bagiannya:

  1. 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.
  2. PER_PLATFORM: peta platform ke daftar formatnya. Map.of plus LinkedHashMap salinan di main menjaga urutan cetak Linux → Windows → macOS tetap stabil.
  3. bangunCfg(...): pembangkit berkas .cfg — bagian [Application] menunjuk JAR dan class utama lewat $APPDIR, sedangkan [JavaOptions] menampung versi aplikasi dan opsi JVM tambahan persis seperti keluaran jpackage nyata pada citra kasirapp kita.
  4. pilihFormat(...): pengganti logika pemilihan bundler — iterasi format lalu mengembalikan tipe pertama yang prasyaratnya terpenuhi; karena fakeroot dan rpmbuild tidak tersedia, keputusan jatuh ke app-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 berkas
java
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));
    }
}