Kotlin Multiplatform Development Help

Native distributions

Here, you'll learn about native distributions: how to create installers and packages for all of the supported systems, and how to run an application locally with the same settings as for distributions.

Read on for the details about the following topics:

Gradle plugin

This guide is primarily focused on packaging Compose applications using the Compose Multiplatform Gradle plugin. The org.jetbrains.compose plugin provides tasks for basic packaging, obfuscation, and macOS code signing.

The plugin simplifies the process of packaging applications into native distributions using jpackage and running an application locally. Distributable applications are self-contained, installable binaries that include all the necessary Java runtime components, without requiring a JDK to be installed on the target system.

To minimize package size, the Gradle plugin uses the jlink tool that ensures bundling only the necessary Java modules in the distributable package. However, you still must configure the Gradle plugin to specify which modules you need. For more information, see the Configuring included JDK modules section.

As an alternative, you can use Conveyor, an external tool not developed by JetBrains. Conveyor supports online updates, cross-building, and various other features but requires a license for non-open source projects. For more information, refer to the Conveyor documentation.

Basic tasks

The basic configurable unit in the plugin is an application. The application DSL method defines a shared configuration for a set of final binaries, which means it allows you to pack a collection of files, together with a JDK distribution, into a set of compressed binary installers in various formats.

The following formats are available for the supported operating systems:

  • macOS: .dmg (TargetFormat.Dmg), .pkg (TargetFormat.Pkg)

  • Windows: .exe (TargetFormat.Exe), .msi (TargetFormat.Msi)

  • Linux: .deb (TargetFormat.Deb), .rpm (TargetFormat.Rpm)

Here is an example of a build.gradle.kts file with a basic desktop configuration:

import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { kotlin("jvm") id("org.jetbrains.compose") } dependencies { implementation(compose.desktop.currentOs) } compose.desktop { application { mainClass = "example.MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe) } } }

When you build a project, the plugin creates the following tasks:

Gradle task

Description

package<FormatName>

Packages the application into the corresponding FormatName binary. Cross-compilation is currently not supported, meaning you can build the specific format using the corresponding compatible OS only. For example, to build a .dmg binary, you have to run the packageDmg task on macOS. If any tasks are incompatible with the current OS, they are skipped by default.

packageDistributionForCurrentOS

Aggregates all package tasks for an application. It is a lifecycle task.

packageUberJarForCurrentOS

Creates a single jar file containing all dependencies for the current operating system. The task expects compose.desktop.currentOS to be used as a compile, implementation, or runtime dependency.

run

Runs an application locally from the entry point specified in mainClass. The run task starts a non-packaged JVM application with the full runtime. This approach is faster and easier to debug compared to creating a compact binary image with a minified runtime. To run a final binary image, use the runDistributable task instead.

createDistributable

Creates a final application image without creating an installer.

runDistributable

Runs a prepackaged application image.

All available tasks are listed in the Gradle tool window. After you execute a task, Gradle generates output binaries in the ${project.buildDir}/compose/binaries directory.

Including JDK modules

To reduce the distributable size, the Gradle plugin uses jlink that helps bundle only the necessary JDK modules.

For now, the Gradle plugin does not automatically determine necessary JDK Modules. While this will not cause compilation issues, failure to provide the necessary modules can lead to ClassNotFoundException at runtime.

If you encounter a ClassNotFoundException when running a packaged application or the runDistributable task, you can include additional JDK modules using the modules DSL method:

compose.desktop { application { nativeDistributions { modules("java.sql") // Alternatively: includeAllModules = true } } }

You can specify the required modules manually, or run suggestModules. The suggestModules task uses the jdeps static analysis tool to determine possible missing modules. Note that the tool's output might be incomplete or list unnecessary modules.

If the size of the distributable is not a crucial factor and can be ignored, you may opt to include all runtime modules by using the includeAllModules DSL property.

Specifying distribution properties

Package version

Native distribution packages must have specific package versions. To specify the package version, you can use the following DSL properties, listed from the highest priority level to the lowest:

  • nativeDistributions.<os>.<packageFormat>PackageVersion specifies a version for a single package format.

  • nativeDistributions.<os>.packageVersion specifies a version for a single target OS.

  • nativeDistributions.packageVersion specifies a version for all packages.

On macOS, you can also specify the build version using the following DSL properties, listed, again, from the highest priority level to the lowest:

  • nativeDistributions.macOS.<packageFormat>PackageBuildVersion specifies a build version for a single package format.

  • nativeDistributions.macOS.packageBuildVersion specifies a build version for all macOS packages.

If you don't specify a build version, Gradle uses the package version instead. For more information about versioning on macOS, see the CFBundleShortVersionString and CFBundleVersion documentation.

Here is a template for specifying package versions in order of priority:

compose.desktop { application { nativeDistributions { // Version for all packages packageVersion = "..." macOS { // Version for all macOS packages packageVersion = "..." // Version for the dmg package only dmgPackageVersion = "..." // Version for the pkg package only pkgPackageVersion = "..." // Build version for all macOS packages packageBuildVersion = "..." // Build version for the dmg package only dmgPackageBuildVersion = "..." // Build version for the pkg package only pkgPackageBuildVersion = "..." } windows { // Version for all Windows packages packageVersion = "..." // Version for the msi package only msiPackageVersion = "..." // Version for the exe package only exePackageVersion = "..." } linux { // Version for all Linux packages packageVersion = "..." // Version for the deb package only debPackageVersion = "..." // Version for the rpm package only rpmPackageVersion = "..." } } } }

To define a package version, follow these rules:

File Type

Version format

Details

dmg, pkg

MAJOR[.MINOR][.PATCH]

  • MAJOR is an integer > 0

  • MINOR is an optional non-negative integer

  • PATCH is an optional non-negative integer

msi, exe

MAJOR.MINOR.BUILD

  • MAJOR is a non-negative integer with a maximum value of 255

  • MINOR is a non-negative integer with a maximum value of 255

  • BUILD is a non-negative integer with a maximum value of 65535

deb

[EPOCH:]UPSTREAM_VERSION[-DEBIAN_REVISION]

  • EPOCH is an optional non-negative integer

  • UPSTREAM_VERSION:

    • May contain only alphanumerics and the ., +, -, ~ characters

    • Must start with a digit

  • DEBIAN_REVISION:

    • Optional

    • May contain only alphanumerics and the ., +, ~ characters

For more details, see Debian documentation.

rpm

Any format

Version must not contain the - (dash) character.

JDK version

The plugin uses jpackage, which requires a JDK version not lower than JDK 17. When specifying a JDK version, ensure you meet at least one of the following requirements:

  • The JAVA_HOME environment variable points to the compatible JDK version.

  • The javaHome property is set via the DSL:

    compose.desktop { application { javaHome = System.getenv("JDK_17") } }

Output directory

To use custom output directory for native distributions, configure the outputBaseDir property as shown below:

compose.desktop { application { nativeDistributions { outputBaseDir.set(project.layout.buildDirectory.dir("customOutputDir")) } } }

Launcher properties

To tailor the application startup process, you can customize the following properties:

Property

Description

mainClass

The fully-qualified name of the class containing the main method.

args

Arguments for the application's main method.

jvmArgs

Arguments for the application's JVM.

Here's an example configuration:

compose.desktop { application { mainClass = "MainKt" args += listOf("-customArgument") jvmArgs += listOf("-Xmx2G") } }

Metadata

Within the nativeDistributions DSL block, you can configure the following properties:

Property

Description

Default value

packageName

The application's name.

The Gradle project's name

packageVersion

The application's version.

The Gradle project's version

description

The application's description.

None

copyright

The application's copyright information.

None

vendor

The application's vendor.

None

licenseFile

The application's license file.

None

Here's an example configuration:

compose.desktop { application { nativeDistributions { packageName = "ExampleApp" packageVersion = "0.1-SNAPSHOT" description = "Compose Multiplatform App" copyright = "© 2024 My Name. All rights reserved." vendor = "Example vendor" licenseFile.set(project.file("LICENSE.txt")) } } }

Managing resources

To package and load resources, you can use the Compose Multiplatform resources library, the JVM resource loading, or add files to packaged applications.

Resources library

The most straightforward way to set up the resources for your project is to use the resources library. With the resources library, you can access resources in common code across all supported platforms. See Multiplatform resources for details.

JVM resource loading

Compose Multiplatform for desktop operates on the JVM platform, meaning you can load resources from a .jar file using the java.lang.Class API. You can access a file in the src/main/resources directory via Class::getResource or Class::getResourceAsStream.

Adding files to packaged application

There are scenarios where loading resources from .jar files may be less practical, for example, when you have target-specific assets and need to include files only in a macOS package but not in a Windows one.

In these cases, you can configure the Gradle plugin to include additional resource files in the installation directory. Specify a root resource directory using the DSL as follows:

compose.desktop { application { mainClass = "MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageVersion = "1.0.0" appResourcesRootDir.set(project.layout.projectDirectory.dir("resources")) } } }

In the example above, the root resource directory is defined as <PROJECT_DIR>/resources.

The Gradle plugin will include files from the resources subdirectories as follows:

  1. Common resources: Files located in <RESOURCES_ROOT_DIR>/common will be included in all packages regardless of the target OS or architecture.

  2. OS-specific resources: Files located in <RESOURCES_ROOT_DIR>/<OS_NAME> will be included only in packages built for a specific operating system. Valid values for <OS_NAME> are: windows, macos, and linux.

  3. OS and architecture-specific resources: Files located in <RESOURCES_ROOT_DIR>/<OS_NAME>-<ARCH_NAME> will be included only in packages built for a specific combination of operating system and CPU architecture. Valid values for <ARCH_NAME> are: x64 and arm64. For example, files in <RESOURCES_ROOT_DIR>/macos-arm64 will be included in packages intended for Apple Silicon Macs only.

You can access included resources using the compose.application.resources.dir system property:

import java.io.File val resourcesDir = File(System.getProperty("compose.application.resources.dir")) fun main() { println(resourcesDir.resolve("resource.txt").readText()) }

Custom source sets

You can rely on default configuration, if you use org.jetbrains.kotlin.jvm or org.jetbrains.kotlin.multiplatform plugins:

  • Configuration with org.jetbrains.kotlin.jvm includes content from the main source set.

  • Configuration with org.jetbrains.kotlin.multiplatform includes content from a single JVM target. If you define multiple JVM targets, the default configuration is disabled. In this case, you need to configure the plugin manually, or specify a single target (see below).

If the default configuration is ambiguous or insufficient, you can customize it in several ways:

Using a Gradle source set:
plugins { kotlin("jvm") id("org.jetbrains.compose") } val customSourceSet = sourceSets.create("customSourceSet") compose.desktop { application { from(customSourceSet) } }
Using a Kotlin JVM target:
plugins { kotlin("multiplatform") id("org.jetbrains.compose") } kotlin { jvm("customJvmTarget") {} } compose.desktop { application { from(kotlin.targets["customJvmTarget"]) } }
Manually:
  • Use disableDefaultConfiguration to disable the default settings.

  • Use fromFiles to specify files to include.

  • Specify the mainJar file property to point to a .jar file containing the main class.

  • Use dependsOn to add task dependencies to all plugin tasks.

compose.desktop { application { disableDefaultConfiguration() fromFiles(project.fileTree("libs/") { include("**/*.jar") }) mainJar.set(project.file("main.jar")) dependsOn("mainJarTask") } }

Application icon

Make sure your app icon is available in the following OS-specific formats:

  • .icns for macOS

  • .ico for Windows

  • .png for Linux

compose.desktop { application { nativeDistributions { macOS { iconFile.set(project.file("icon.icns")) } windows { iconFile.set(project.file("icon.ico")) } linux { iconFile.set(project.file("icon.png")) } } } }

Platform-specific options

Platform-specific settings can be configured using the corresponding DSL blocks:

compose.desktop { application { nativeDistributions { macOS { // Options for macOS } windows { // Options for Windows } linux { // Options for Linux } } } }

The following table describes all supported platform-specific options. It is not recommended to use undocumented properties.

Platform

Option

Description

All platforms

iconFile.set(File("PATH_TO_ICON"))

Specifies the path to a platform-specific icon for the application. For details, see the Application icon section.

packageVersion = "1.0.0"

Sets a platform-specific package version. For details, see the Package version section.

installationPath = "PATH_TO_INST_DIR"

Specifies the absolute or relative path to the default installation directory. On Windows, you can also use dirChooser = true to enable customizing the path during installation.

Linux

packageName = "custom-package-name"

Overrides the default application name.

debMaintainer = "maintainer@example.com"

Specifies the email of the package maintainer.

menuGroup = "my-example-menu-group"

Defines a menu group for the application.

appRelease = "1"

Sets a release value for the rpm package or a revision value for the deb package.

appCategory = "CATEGORY"

Assigns a group value for the rpm package or a section value for the deb package.

rpmLicenseType = "TYPE_OF_LICENSE"

Indicates the type of license for the rpm package.

debPackageVersion = "DEB_VERSION"

Sets a deb-specific package version. For details, see the Package version section.

rpmPackageVersion = "RPM_VERSION"

Sets an rpm-specific package version. For details, see the Package version section.

macOS

bundleID

Specifies a unique application identifier, which can contain only alphanumeric characters (A-Z, a-z, 0-9), hyphen (-), and period (.). It is recommended to use reverse DNS notation (com.mycompany.myapp).

packageName

The name of the application.

dockName

The name of the application as displayed in the menu bar, the "About <App>" menu item, and in the dock. Default value is packageName.

minimumSystemVersion

The minimum macOS version required to run the application. For details, see LSMinimumSystemVersion.

signing, notarization, provisioningProfile, runtimeProvisioningProfile

See the Signing and notarizing distributions for macOS tutorial.

appStore = true

Specifies whether to build and sign the app for the Apple App Store. Requires at least JDK 17.

appCategory

The category of the app for the Apple App Store. When building for the App Store, default value is public.app-category.utilities, otherwise it is Unknown. See LSApplicationCategoryType for a list of valid categories.

entitlementsFile.set(File("PATH_ENT"))

Specifies the path to the file containing entitlements used when signing. When you provide a custom file, make sure to add the entitlements required for Java applications. See sandbox.plist for the default file used when building for the App Store. Note that this default file may differ depending on your JDK version. If no file is specified, the plugin will use the default entitlements provided by jpackage. For details, see the Signing and notarizing distributions for macOS tutorial.

runtimeEntitlementsFile.set(File("PATH_R_ENT"))

Specifies the path to the file containing entitlements used when signing the JVM runtime. When you provide a custom file, make sure to add the entitlements required for Java applications. See sandbox.plist for the default file used when building for the App Store. Note that this default file may differ depending on your JDK version. If no file is specified, the plugin will use the default entitlements provided by jpackage. For details, see the Signing and notarizing distributions for macOS tutorial.

dmgPackageVersion = "DMG_VERSION"

Sets the DMG-specific package version. For details, see the Package version section.

pkgPackageVersion = "PKG_VERSION"

Sets the PKG-specific package version. For details, see the Package version section.

packageBuildVersion = "DMG_VERSION"

Sets the package build version. For details, see the Package version section.

dmgPackageBuildVersion = "DMG_VERSION"

Sets the DMG-specific package build version. For details, see the Package version section.

pkgPackageBuildVersion = "PKG_VERSION"

Sets the PKG-specific package build version. For details, see the Package version section.

infoPlist

See the Info.plist on macOS section.

Windows

console = true

Adds a console launcher for the application.

dirChooser = true

Enables customizing the installation path during installation.

perUserInstall = true

Enables installing the application on a per-user basis.

menuGroup = "start-menu-group"

Adds the application to the specified Start menu group.

upgradeUuid = "UUID"

Specifies a unique ID which enables users to update the application via the installer, when there is a version newer than an installed version. The value must remain constant for a single application. For details, see How To: Generate a GUID.

msiPackageVersion = "MSI_VERSION"

Sets the MSI-specific package version. For details, see the Package version section.

exePackageVersion = "EXE_VERSION"

Sets the EXE-specific package version. For details, see the Package version section.

macOS-specific configuration

Signing and notarization on macOS

Modern macOS versions do not permit users to execute unsigned applications downloaded from the internet. If you attempt to run such an application, you'll encounter the following error: "YourApp is damaged and can't be open. You should eject the disk image".

To learn how to sign and notarize your application, see our tutorial.

Information property list on macOS

While the DSL supports essential platform-specific customizations, there can still be cases beyond the provided capabilities. If you need to specify Info.plist values that are not represented in the DSL, you can include a snippet of raw XML as a workaround. This XML will be appended to the application's Info.plist.

Example: Deep linking

  1. Define a custom URL scheme in the build.gradle.kts file:

compose.desktop { application { mainClass = "MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg) packageName = "Deep Linking Example App" macOS { bundleID = "org.jetbrains.compose.examples.deeplinking" infoPlist { extraKeysRawXml = macExtraPlistKeys } } } } } val macExtraPlistKeys: String get() = """ <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLName</key> <string>Example deep link</string> <key>CFBundleURLSchemes</key> <array> <string>compose</string> </array> </dict> </array> """
  1. Use the java.awt.Desktop class to set up a URI handler in the src/main/main.kt file:

import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.window.singleWindowApplication import java.awt.Desktop fun main() { var text by mutableStateOf("Hello, World!") try { Desktop.getDesktop().setOpenURIHandler { event -> text = "Open URI: " + event.uri } } catch (e: UnsupportedOperationException) { println("setOpenURIHandler is unsupported") } singleWindowApplication { MaterialTheme { Text(text) } } }
  1. Execute the runDistributable task: ./gradlew runDistributable.

As a result, links like compose://foo/bar can now be redirected from a browser to your application.

Minification and obfuscation

The Compose Multiplatform Gradle plugin includes built-in support for ProGuard. ProGuard ia an open-source tool for code minification and obfuscation.

For each default (without ProGuard) packaging task, the Gradle plugin provides a release task (with ProGuard):

Gradle task

Description

Default: createDistributable

Release: createReleaseDistributable

Creates an application image with bundled JDK and resources.

Default: runDistributable

Release: runReleaseDistributable

Runs an application image with bundled JDK and resources.

Default: run

Release: runRelease

Runs a non-packaged application .jar using the Gradle JDK.

Default: package<FORMAT_NAME>

Release: packageRelease<FORMAT_NAME>

Packages an application image into a <FORMAT_NAME> file.

Default: packageDistributionForCurrentOS

Release: packageReleaseDistributionForCurrentOS

Packages an application image into a format compatible with the current OS.

Default: packageUberJarForCurrentOS

Release: packageReleaseUberJarForCurrentOS

Packages an application image into an uber (fat) `.jar`.

Default: notarize<FORMAT_NAME>

Release: notarizeRelease<FORMAT_NAME>

Uploads a <FORMAT_NAME> application image for notarization (macOS only).

Default: checkNotarizationStatus

Release: checkReleaseNotarizationStatus

Checks if notarization succeeded (macOS only).

The default configuration enables some pre-defined ProGuard rules:

  • The application image is minified, meaning unused classes are removed.

  • compose.desktop.application.mainClass is used as the entry point.

  • Several keep rules are included to ensure the Compose runtime remains functional.

In most cases, you don't need any additional configurations to get a minified application. However, ProGuard might not track certain usages in bytecode, for example, when a class is used via reflection. If you encounter issues that occur only after ProGuard processing, you may need to add custom rules.

To specify a custom configuration file, use the DSL as follows:

compose.desktop { application { buildTypes.release.proguard { configurationFiles.from(project.file("compose-desktop.pro")) } } }

For more information about ProGuard rules and configuration options, refer to the Guardsquare manual.

Obfuscation is disabled by default. To enable it, set the following property via the Gradle DSL:

compose.desktop { application { buildTypes.release.proguard { obfuscate.set(true) } } }

ProGuard's optimizations are enabled by default. To disable them, set the following property via the Gradle DSL:

compose.desktop { application { buildTypes.release.proguard { optimize.set(false) } } }

Producing an uber JAR is disabled by default, and ProGuard produces a corresponding .jar file for every input .jar. To enable it, set the following property via the Gradle DSL:

compose.desktop { application { buildTypes.release.proguard { joinOutputJars.set(true) } } }

What's next?

Explore the tutorials about desktop components.

Last modified: 13 December 2024