CLI tool yang sudah ditulis perlu didistribusikan ke pengguna. Banyak pilihan tersedia: Fat JAR yang berisi seluruh dependensi, native binary dengan GraalVM atau Kotlin/Native untuk startup cepat, hingga package manager seperti Homebrew dan apt. Pada bab ini kita akan membandingkan lima strategi distribusi, membedah struktur Fat JAR, dan menyusun checklist publikasi. Pilihan strategi bergantung pada target pengguna (developer vs end-user), platform tujuan, dan kebutuhan startup time.
Lima Strategi Distribusi #
Setiap strategi punya trade-off antara ukuran, startup time, dan kemudahan instalasi. Tabel berikut memetakan lima opsi paling umum:
| Strategi | Ukuran | Startup | Cocok untuk |
|---|---|---|---|
Fat JAR (-include-runtime) | 30-60 MB | 200-400 ms | Distribusi sederhana, satu file |
| GraalVM native-image | 15-30 MB | 5-20 ms | CLI yang sering dipanggil |
| Kotlin/Native | 2-5 MB | 1-5 ms | Self-contained, tanpa JVM |
| Docker image | 100-300 MB | 300-600 ms | Deployment server |
| Homebrew/apt package | 10-50 MB | 200-400 ms | Distribusi publik |
Untuk CLI internal yang dipakai tim kecil, Fat JAR sudah cukup. Untuk CLI publik yang dipakai banyak developer, Homebrew/apt lebih ramah pengguna. Untuk CLI yang harus responsif (misalnya dipanggil di setiap shell prompt), native binary adalah pilihan terbaik.
Fat JAR dengan -include-runtime #
Cara paling sederhana mengemas CLI adalah dengan kotlinc -include-runtime. Flag ini menyertakan Kotlin stdlib ke dalam JAR hasil, sehingga JAR dapat dijalankan dengan java -jar tanpa classpath tambahan.
kotlinc app.kt -include-runtime -d app.jar
java -jar app.jar arg1 arg2Untuk project Gradle yang lebih kompleks, plugin shadow menggabungkan semua dependensi ke satu JAR. Manifest Main-Class ditulis otomatis dari konfigurasi application:
plugins {
kotlin("jvm") version "2.0.21"
application
id("com.github.johnrengelman.shadow") version "8.1.1"
}
application {
mainClass.set("com.example.AppKt")
}Perintah ./gradlew shadowJar menghasilkan build/libs/app-all.jar yang siap distribusi.
Demo Struktur JAR dan Manifest #
Cuplikan berikut membandingkan strategi distribusi, menampilkan struktur Fat JAR yang khas, dan menunjukkan isi manifest sebuah JAR:
// Demo packaging CLI applications: perbandingan strategi distribusi
// dan simulasi struktur JAR. Tidak melakukan kompilasi/packaging nyata
// (itu ditangani oleh Gradle/kotlinc/native-image), tetapi menampilkan
// perintah dan ukuran khas untuk setiap strategi.
import java.io.ByteArrayOutputStream
import java.util.jar.Attributes
import java.util.jar.JarOutputStream
import java.util.jar.Manifest
data class OpsiPackaging(
val strategi: String,
val perintah: String,
val ukuran: String,
val startup: String,
val cocok: String
)
fun main() {
println("=== Packaging CLI Applications ===")
println()
// 1. Strategi distribusi yang umum
val strategi = listOf(
OpsiPackaging(
"Fat JAR (-include-runtime)",
"kotlinc app.kt -include-runtime -d app.jar",
"30-60 MB",
"200-400 ms",
"Distribusi sederhana, satu file"
),
OpsiPackaging(
"Native binary (GraalVM native-image)",
"native-image -jar app.jar app",
"15-30 MB",
"5-20 ms",
"Startup cepat, CLI yang sering dipanggil"
),
OpsiPackaging(
"Kotlin/Native",
"kotlinc-native app.kt -o app",
"2-5 MB",
"1-5 ms",
"Self-contained, tanpa JVM"
),
OpsiPackaging(
"Docker image",
"docker build -t app .",
"100-300 MB",
"300-600 ms",
"Deployment server, isolasi"
),
OpsiPackaging(
"Homebrew/apt package",
"brew install app",
"10-50 MB",
"200-400 ms",
"Distribusi publik ke developer"
)
)
println("=== 1. Strategi Distribusi ===")
strategi.forEach { s ->
println(" ${s.strategi}")
println(" Perintah: ${s.perintah}")
println(" Ukuran: ${s.ukuran}, startup: ${s.startup}")
println(" Cocok: ${s.cocok}")
}
println()
// 2. Struktur Fat JAR yang khas
println("=== 2. Struktur Fat JAR ===")
val struktur = listOf(
"META-INF/" to "[DIR]",
"META-INF/MANIFEST.MF" to "Main-Class: AppKt",
"AppKt.class" to "Class utama hasil kompilasi",
"org/jetbrains/kotlin/" to "[DIR] Kotlin stdlib",
"kotlinx/coroutines/" to "[DIR] (jika pakai coroutines)",
"META-INF/versions/9/" to "[DIR] Multi-release JAR"
)
struktur.forEach { (path, keterangan) ->
println(" %-30s %s".format(path, keterangan))
}
println()
// 3. Demo: tulis JAR manifest ke stream memori (tidak ke disk)
println("=== 3. Demo: Manifest JAR (in-memory) ===")
val manifest = Manifest()
manifest.mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0"
manifest.mainAttributes[Attributes.Name.MAIN_CLASS] = "AppKt"
manifest.mainAttributes.putValue("Implementation-Title", "demo-cli")
manifest.mainAttributes.putValue("Implementation-Version", "1.0.0")
val baos = ByteArrayOutputStream()
val jarOut = JarOutputStream(baos, manifest)
jarOut.close()
val jarBytes = baos.toByteArray()
println(" Ukuran JAR (manifest only): ${jarBytes.size} bytes")
println(" Atribut manifest:")
manifest.mainAttributes.forEach { (k, v) ->
println(" $k: $v")
}
println()
// 4. Perbandingan startup time (deterministik, simulasi)
println("=== 4. Simulasi Startup Time ===")
val startupData = listOf(
Triple("JVM biasa", 250, "100-400 ms (warm-up JIT)"),
Triple("JVM + AppCDS", 180, "150-220 ms (class data sharing)"),
Triple("GraalVM native-image", 10, "5-20 ms (ahead-of-time)"),
Triple("Kotlin/Native", 3, "1-5 ms (binary murni)")
)
startupData.forEach { (nama, nilai, range) ->
val bar = "#".repeat(nilai / 10)
println(" %-25s [%-25s] %d ms (%s)".format(nama, bar, nilai, range))
}
println()
// 5. Checklist publikasi
println("=== 5. Checklist Publikasi CLI ===")
val checklist = listOf(
"1. Tentukan target platform (Linux, macOS, Windows)" to true,
"2. Pilih strategi: Fat JAR, native binary, atau package manager" to true,
"3. Sertakan LICENSE dan NOTICE bila library dipakai" to true,
"4. Set version semantic (semver) di build script" to true,
"5. Generate shell completion (bash/zsh) bila perlu" to false,
"6. Publikasikan ke Homebrew tap atau apt repo" to false,
"7. Buat GitHub Release dengan checksum SHA-256" to true,
"8. Sertakan man page atau --help yang lengkap" to true
)
checklist.forEach { (item, wajib) ->
val tag = if (wajib) "[wajib]" else "[opsional]"
println(" $tag $item")
}
}Output yang Dihasilkan:
=== Packaging CLI Applications ===
=== 1. Strategi Distribusi ===
Fat JAR (-include-runtime)
Perintah: kotlinc app.kt -include-runtime -d app.jar
Ukuran: 30-60 MB, startup: 200-400 ms
Cocok: Distribusi sederhana, satu file
Native binary (GraalVM native-image)
Perintah: native-image -jar app.jar app
Ukuran: 15-30 MB, startup: 5-20 ms
Cocok: Startup cepat, CLI yang sering dipanggil
Kotlin/Native
Perintah: kotlinc-native app.kt -o app
Ukuran: 2-5 MB, startup: 1-5 ms
Cocok: Self-contained, tanpa JVM
Docker image
Perintah: docker build -t app .
Ukuran: 100-300 MB, startup: 300-600 ms
Cocok: Deployment server, isolasi
Homebrew/apt package
Perintah: brew install app
Ukuran: 10-50 MB, startup: 200-400 ms
Cocok: Distribusi publik ke developer
=== 2. Struktur Fat JAR ===
META-INF/ [DIR]
META-INF/MANIFEST.MF Main-Class: AppKt
AppKt.class Class utama hasil kompilasi
org/jetbrains/kotlin/ [DIR] Kotlin stdlib
kotlinx/coroutines/ [DIR] (jika pakai coroutines)
META-INF/versions/9/ [DIR] Multi-release JAR
=== 3. Demo: Manifest JAR (in-memory) ===
Ukuran JAR (manifest only): 243 bytes
Atribut manifest:
Manifest-Version: 1.0
Main-Class: AppKt
Implementation-Title: demo-cli
Implementation-Version: 1.0.0
=== 4. Simulasi Startup Time ===
JVM biasa [#########################] 250 ms (100-400 ms (warm-up JIT))
JVM + AppCDS [################## ] 180 ms (150-220 ms (class data sharing))
GraalVM native-image [# ] 10 ms (5-20 ms (ahead-of-time))
Kotlin/Native [ ] 3 ms (1-5 ms (binary murni))
=== 5. Checklist Publikasi CLI ===
[wajib] 1. Tentukan target platform (Linux, macOS, Windows)
[wajib] 2. Pilih strategi: Fat JAR, native binary, atau package manager
[wajib] 3. Sertakan LICENSE dan NOTICE bila library dipakai
[wajib] 4. Set version semantic (semver) di build script
[opsional] 5. Generate shell completion (bash/zsh) bila perlu
[opsional] 6. Publikasikan ke Homebrew tap atau apt repo
[wajib] 7. Buat GitHub Release dengan checksum SHA-256
[wajib] 8. Sertakan man page atau --help yang lengkapMari kita bedah bagian-bagian kunci dari kode di atas:
data class OpsiPackaging: model data untuk lima strategi distribusi. Fieldperintahmenunjukkan perintah build yang khas untuk masing-masing strategi,ukurandanstartupmemberi estimasi kasar, dancocokmenjelaskan use case ideal.JarOutputStream(baos, manifest): konstruktor yang menerimaManifestakan otomatis menulis entryMETA-INF/MANIFEST.MFke dalam stream.ByteArrayOutputStreammenampung hasil tanpa menulis ke disk — cocok untuk demo yang harus deterministik.- Atribut manifest
Main-Class: atribut inilah yang membuatjava -jar app.jartahu class mana yang harus dijalankan. TanpaMain-Class, JVM akan menolak JAR dengan error “no main manifest attribute”. Implementation-TitledanImplementation-Version: atribut opsional yang dipakai oleh package manager dan tools monitoring untuk identifikasi. Spring Boot Actuator membaca atribut ini untuk endpoint/info.- Simulasi startup time: data deterministik dengan angka tetap (250, 180, 10, 3). Bila angka aktual dibutuhkan, jalankan benchmark dengan
hyperfineuntuk CLI real-world.
GraalVM native-image #
GraalVM native-image mengkonversi JAR menjadi binary native platform (Linux, macOS, Windows). Binary hasil tidak memerlukan JVM dan startup time turun dari ~250 ms ke ~10 ms. Trade-off: build time lebih lama (5-10 menit), binary tidak portable (harus dibuild per platform), dan refleksi dinamis perlu konfigurasi reflect-config.json.
native-image -jar app.jar app
./app arg1 arg2Untuk project Gradle, plugin graalvm-native mengotomatiskan konfigurasi. Library seperti picocli dan http-client-java sudah menyertakan metadata native-image, sehingga CLI berbasis library tersebut langsung compatible.
Kotlin/Native #
Kotlin/Native adalah backend kompilator yang menghasilkan binary native langsung dari kode Kotlin, tanpa melalui bytecode JVM. Binary hasil sangat kecil (2-5 MB) dan startup sangat cepat (1-5 ms). Cocok untuk CLI yang dipakai pada shell prompt atau pipeline CI yang sensitif terhadap latensi.
kotlinc-native app.kt -o app
./app.kexe arg1 arg2Keterbatasan: tidak semua library JVM compatible dengan Kotlin/Native. Library yang memakai JNI, reflection dinamis, atau library Java spesifik tidak dapat dipakai. Untuk CLI yang hanya memerlukan stdlib Kotlin, Kotlin/Native adalah pilihan ideal.
Referensi Lanjutan #
- Kotlin Command Line
— panduan resmi
kotlinCLI untuk packaging aplikasi native dan distribusi biner - kotlinx.cli
— halaman GitHub
kotlinx-cliuntuk deklarasi argumen pada aplikasi CLI yang dikemas
Selanjutnya #
Bagian scripting dan otomasi selesai. Selanjutnya kita beralih ke section performance dan optimization, dimulai dengan profiling aplikasi Kotlin untuk menemukan bottleneck. Materi tersebut ada di chapter Profiling Kotlin Applications.