Distributing applications
kargo package builds a fat jar, which runs anywhere with java -jar, but only where the right Java is installed. To ship a program to people who don’t have Java, --format builds something else:
$ kargo package --release # target/release/jvm/hello-0.1.0-all.jar$ kargo package --release --format jlink # the jars, a trimmed Java runtime and launchers$ kargo package --release --format native-image # a GraalVM native executable$ kargo package --release --format installer # .msi on Windows, .dmg on macOS, .deb on LinuxTo install a tool on your own machine instead, use kargo install, which builds it and puts a launcher on your PATH. Each kargo package command prints the path of what it built on stdout, so a script can pick it up. They all need a main class (the project’s, or --bin <name> for a [[bin]]), and they all go to target/<profile>/jvm/package/.
| Format | Size | Starts in | Needs on the build machine |
|---|---|---|---|
jar |
small | JVM startup | nothing |
jlink |
~35 MB and up | JVM startup | a JDK (the project’s) |
native-image |
~5 MB and up | milliseconds | GraalVM and a C toolchain |
installer |
~35 MB and up | JVM startup | a JDK 16 or newer, plus the OS’s packaging tools |
Runtime images
Section titled “Runtime images”$ kargo package --release --format jlink Compiling hello v0.1.0 (jvm) Packaging hello v0.1.0 runtime image (/home/me/hello/target/release/jvm/package/hello-0.1.0) Finished `release` profile [optimized] target(s) in 14.08s/home/me/hello/target/release/jvm/package/hello-0.1.0$ target/release/jvm/package/hello-0.1.0/bin/helloHello, Kotlin!The image is a directory you can zip and ship:
hello-0.1.0/├── bin/│ ├── hello # launcher for Linux and macOS│ └── hello.bat # launcher for Windows├── lib/ # the program's jar and its dependencies└── runtime/ # a Java runtime with only the modules the program usesKargo asks jdeps which JDK modules the program and its dependencies use, and builds the runtime with jlink --strip-debug --no-header-files --no-man-pages --compress, from the project’s JDK ([toolchain] jdk). A Kotlin program that only uses java.base gets a runtime of about 30 MB. The launchers pass [target.jvm] jvm-args and the profile’s jvm-args to the program, and arguments after them to main.
jdeps can’t see modules that are only loaded through reflection or a service loader, such as java.naming for JNDI or jdk.crypto.ec for some TLS connections. List them in [package.jlink]:
[package.jlink]modules = ["java.naming", "jdk.crypto.ec"]If jdeps fails on a dependency, set jdeps = false and list every module the runtime needs in modules.
Native executables
Section titled “Native executables”--format native-image compiles the program ahead of time with GraalVM’s native-image into one executable that starts in milliseconds and needs no Java. Pin the GraalVM so Kargo downloads it:
[toolchain]graalvm = "25"$ kargo package --release --format native-image Compiling hello v0.1.0 (jvm) Downloading graalvm-community-25.0.4.1.1.zip Installing GraalVM CE 25.0.4.1.1 Packaging hello v0.1.0 native executable (/home/me/hello/target/release/jvm/package/hello) Downloading graalvm-reachability-metadata-1.0.20.zip.../home/me/hello/target/release/jvm/package/hello$ target/release/jvm/package/helloHello, Kotlin!Without [toolchain] graalvm, Kargo uses the GraalVM in GRAALVM_HOME, or a JAVA_HOME or PATH that has native-image. native-image prints its progress on stderr, so the executable’s path stays alone on stdout. See GraalVM for the toolchain itself.
native-image needs a C toolchain: gcc (or clang) and the zlib headers on Linux (build-essential and zlib1g-dev on Debian and Ubuntu), the Xcode command line tools on macOS, and Visual Studio 2022 or its Build Tools with the C++ workload on Windows. On Windows GraalVM finds Visual Studio itself; when only Visual Studio’s cl.exe is on PATH, without a developer prompt’s INCLUDE, Kargo hides it so GraalVM can.
Reflection and resources
Section titled “Reflection and resources”A native image only contains the code native-image can see is reachable. Code that is only reached through reflection, dynamic proxies, resources or JNI needs configuration, which comes from three places:
- Libraries. Many ship it in their jars under
META-INF/native-image/, whichnative-imagereads. - The GraalVM reachability metadata repository. It has configuration for libraries that don’t ship their own (Netty, Logback, H2 and hundreds more). Kargo downloads it once and adds the entry for each dependency version on the runtime classpath. Turn it off with
metadata-repository = falseunder[package.native-image]. - Your program. Run it with GraalVM’s tracing agent, which records what it uses:
$ kargo run --native-agentnote: the GraalVM tracing agent writes the program's native-image configuration to native-image Running `java com.example.hello.MainKt`Hello, Kotlin!The agent writes native-image/reachability-metadata.json in the project. Each later run adds to it, so run the program through every code path that matters (or run it with different arguments several times), then commit native-image/. kargo package --format native-image includes it.
Options
Section titled “Options”[package.native-image]args = ["--enable-url-protocols=https", "-march=native"]initialize-at-build-time = ["kotlinx.serialization"]static = true # Linux only: a fully static executable, linked against muslargs go to native-image as they are. static = true needs musl’s toolchain (x86_64-linux-musl-gcc on PATH), and gives an executable that runs on any Linux, even one without glibc such as an Alpine container.
Installers
Section titled “Installers”--format installer runs the JDK’s jpackage on the program’s jars and a runtime built like the runtime image’s:
[package]name = "hello"version = "1.2.0"description = "Says hello"
[package.installer]name = "Hello"vendor = "Example Inc."icon = { windows = "packaging/hello.ico", macos = "packaging/hello.icns", linux = "packaging/hello.png" }license-file = "LICENSE"menu = trueshortcut = trueupgrade-uuid = "0b5b2d6e-8f5c-4a37-9d1e-3a6c2f4b7e90"bundle-id = "com.example.hello"$ kargo package --release --format installer Packaging Hello v1.2.0 msi installer (C:\hello\target\release\jvm\package)C:\hello\target\release\jvm\package\Hello-1.2.0.msiThe installer type defaults to the host’s: .msi on Windows, .dmg on macOS, .deb on Linux. Set type to exe, pkg or rpm for the others, or to app-image for the installed application’s directory without an installer, which builds on every OS. type can also be a list such as ["msi", "dmg", "deb"]: each OS builds the types in it that it can build, so one kargo.toml serves a CI matrix of Windows, macOS and Linux runners. See [package.installer] for every key.
File associations
Section titled “File associations”[package.installer]file-associations = [ { extension = "note", mime-type = "application/x-note", description = "Note", icon = "packaging/note.ico" },]The installed application opens files of these types, which the OS passes to main as arguments. Application images (app-image) are not installed, so they register none.
Signing and notarizing on macOS
Section titled “Signing and notarizing on macOS”signing-identity makes jpackage sign the application with a certificate from your keychain. To open on other Macs without a Gatekeeper warning, a signed .dmg or .pkg must also be notarized by Apple:
[package.installer]signing-identity = "Developer ID Application: Example Inc (ABCDE12345)"notarize = { apple-id-env = "APPLE_ID", password-env = "APPLE_APP_PASSWORD", team-id = "ABCDE12345" }After jpackage, Kargo runs xcrun notarytool submit --wait on each .dmg and .pkg, with the Apple ID and the app-specific password read from the environment variables that apple-id-env and password-env name, then staples the ticket with xcrun stapler staple. notarize = { keychain-profile = "<name>" } uses credentials saved with xcrun notarytool store-credentials instead. Notarizing takes a few minutes; when Apple rejects the upload, the error shows the service’s answer, and xcrun notarytool log <submission id> explains it.
For Compose Desktop applications, use kargo package --desktop: it builds an installer and adds the JDK modules and options Compose needs, and moves Skiko’s native library out of its jar so that it is signed and notarized too.
What each OS needs:
| OS | Types | Needs |
|---|---|---|
| Windows | msi, exe |
The WiX Toolset on PATH |
| macOS | dmg, pkg |
Xcode command line tools; a signing identity in the keychain for signing-identity |
| Linux | deb, rpm |
dpkg-deb and fakeroot for .deb, rpm-build for .rpm |
Installers need numeric versions, so Kargo passes the numbers at the start of [package] version (1.2.0-SNAPSHOT becomes 1.2.0), or app-version when you set it. Keep upgrade-uuid the same across releases, so that installing a newer .msi replaces the older one instead of installing next to it.
In a workspace
Section titled “In a workspace”Run kargo package --format <format> -p <member> at the root, or kargo package --format <format> in the member’s directory. The output goes to target/<profile>/<member>/jvm/package/.