Kotlin Multiplatform Development Help

原生分发

在这里,您将了解原生分发的相关知识:如何为所有支持的系统创建安装程序和软件包,以及如何以与分发相同的设置本地运行应用程序。

继续阅读以下主题的详细信息:

  • 什么是 Compose Multiplatform Gradle 插件

  • 关于基本任务 (如本地运行应用程序)和高级任务 (如代码压缩和混淆)的详细信息。

  • 如何包含 JDK 模块并处理 ClassNotFoundException

  • 如何指定分发属性 :软件包版本、JDK 版本、输出目录、启动器属性和元数据。

  • 如何管理资源 :使用资源库、JVM 资源加载或将文件添加到打包的应用程序中。

  • 如何自定义源集 :使用 Gradle 源集、Kotlin JVM 目标或手动配置。

  • 如何为每个操作系统指定应用程序图标

  • 平台特定选项 ,例如 Linux 上的软件包维护者电子邮件和 macOS 上 Apple App Store 的应用程序类别。

  • macOS 特定配置 :签名、公证和 Info.plist

Gradle 插件

本指南主要介绍如何使用 Compose Multiplatform Gradle 插件打包 Compose 应用程序。 org.jetbrains.compose 插件提供了基本打包、混淆和 macOS 代码签名的任务。

该插件简化了使用 jpackage 将应用程序打包为原生分发以及在本地运行应用程序的过程。可分发的应用程序是自包含的可安装二进制文件,包含所有必要的 Java 运行时组件,无需在目标系统上安装 JDK。

为了最小化软件包大小,Gradle 插件使用 jlink 工具,确保仅将必要的 Java 模块捆绑在可分发包中。然而,您仍然需要配置 Gradle 插件以指定所需的模块。更多信息,请参阅 配置包含的 JDK 模块 部分。

作为替代方案,您可以使用 Conveyor ,这是一个非 JetBrains 开发的外部工具。Conveyor 支持在线更新、交叉构建和各种其他功能,但非开源项目需要许可证 。更多信息,请参阅 Conveyor 文档

基本任务

插件中的基本可配置单元是 applicationapplication DSL 方法定义了一组最终二进制文件的共享配置,这意味着它允许您将一组文件与 JDK 分发一起打包成各种格式的压缩二进制安装程序。

支持的操作系统有以下格式:

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

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

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

以下是一个带有基本桌面配置的 build.gradle.kts 文件示例:

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) } } }

构建项目时,插件会创建以下任务:

Gradle 任务

描述

package<FormatName>

将应用程序打包为相应的 FormatName 二进制文件。目前不支持交叉编译,这意味着您只能使用相应的兼容操作系统构建特定格式。例如,要构建 .dmg 二进制文件,您必须在 macOS 上运行 packageDmg 任务。如果任何任务与当前操作系统不兼容,默认情况下会跳过它们。

packageDistributionForCurrentOS

聚合应用程序的所有打包任务。这是一个生命周期任务

packageUberJarForCurrentOS

为当前操作系统创建一个包含所有依赖项的单个 jar 文件。该任务期望 compose.desktop.currentOS 用作 compileimplementationruntime 依赖项。

run

mainClass 中指定的入口点本地运行应用程序。 run 任务启动一个非打包的 JVM 应用程序,具有完整的运行时。与创建带有精简运行时的紧凑二进制映像相比,这种方法更快且更易于调试。要运行最终的二进制映像,请改用 runDistributable 任务。

createDistributable

创建最终的应用程序映像,而不创建安装程序。

runDistributable

运行预打包的应用程序映像。

所有可用任务都列在 Gradle 工具窗口中。执行任务后,Gradle 会在 ${project.buildDir}/compose/binaries 目录中生成输出二进制文件。

包含 JDK 模块

为了减少分发大小,Gradle 插件使用 jlink 来帮助仅捆绑必要的 JDK 模块。

目前,Gradle 插件不会自动确定必要的 JDK 模块。虽然这不会导致编译问题,但未能提供必要的模块可能会导致运行时出现 ClassNotFoundException

如果在运行打包应用程序或 runDistributable 任务时遇到 ClassNotFoundException ,您可以使用 modules DSL 方法包含其他 JDK 模块:

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

您可以手动指定所需的模块,或运行 suggestModulessuggestModules 任务使用 jdeps 静态分析工具来确定可能缺少的模块。请注意,该工具的输出可能不完整或列出不必要的模块。

如果分发大小不是关键因素并且可以忽略,您可以选择使用 includeAllModules DSL 属性包含所有运行时模块。

指定分发属性

软件包版本

原生分发软件包必须具有特定的软件包版本。要指定软件包版本,您可以使用以下 DSL 属性,按优先级从高到低排列:

  • nativeDistributions.<os>.<packageFormat>PackageVersion 为单个软件包格式指定版本。

  • nativeDistributions.<os>.packageVersion 为单个目标操作系统指定版本。

  • nativeDistributions.packageVersion 为所有软件包指定版本。

在 macOS 上,您还可以使用以下 DSL 属性指定构建版本,同样按优先级从高到低排列:

  • nativeDistributions.macOS.<packageFormat>PackageBuildVersion 为单个软件包格式指定构建版本。

  • nativeDistributions.macOS.packageBuildVersion 为所有 macOS 软件包指定构建版本。

如果未指定构建版本,Gradle 将使用软件包版本。有关 macOS 版本控制的更多信息,请参阅 CFBundleShortVersionStringCFBundleVersion 文档。

以下是按优先级指定软件包版本的模板:

compose.desktop { application { nativeDistributions { // 所有软件包的版本 packageVersion = "..." macOS { // 所有 macOS 软件包的版本 packageVersion = "..." // 仅 dmg 软件包的版本 dmgPackageVersion = "..." // 仅 pkg 软件包的版本 pkgPackageVersion = "..." // 所有 macOS 软件包的构建版本 packageBuildVersion = "..." // 仅 dmg 软件包的构建版本 dmgPackageBuildVersion = "..." // 仅 pkg 软件包的构建版本 pkgPackageBuildVersion = "..." } windows { // 所有 Windows 软件包的版本 packageVersion = "..." // 仅 msi 软件包的版本 msiPackageVersion = "..." // 仅 exe 软件包的版本 exePackageVersion = "..." } linux { // 所有 Linux 软件包的版本 packageVersion = "..." // 仅 deb 软件包的版本 debPackageVersion = "..." // 仅 rpm 软件包的版本 rpmPackageVersion = "..." } } } }

定义软件包版本时,请遵循以下规则:

文件类型

版本格式

详情

dmg, pkg

MAJOR[.MINOR][.PATCH]

  • MAJOR 是大于 0 的整数

  • MINOR 是可选的非负整数

  • PATCH 是可选的非负整数

msi, exe

MAJOR.MINOR.BUILD

  • MAJOR 是非负整数,最大值为 255

  • MINOR 是非负整数,最大值为 255

  • BUILD 是非负整数,最大值为 65535

deb

[EPOCH:]UPSTREAM_VERSION[-DEBIAN_REVISION]

  • EPOCH 是可选的非负整数

  • UPSTREAM_VERSION:

    • 只能包含字母数字和 ., +, -, ~ 字符

    • 必须以数字开头

  • DEBIAN_REVISION:

    • 可选

    • 只能包含字母数字和 ., +, ~ 字符

更多详情,请参阅 Debian 文档

rpm

任何格式

版本不能包含 - (破折号)字符。

JDK 版本

插件使用 jpackage ,它需要不低于 JDK 17 的 JDK 版本。指定 JDK 版本时,请确保满足以下至少一个要求:

  • JAVA_HOME 环境变量指向兼容的 JDK 版本。

  • 通过 DSL 设置 javaHome 属性:

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

输出目录

要为原生分发使用自定义输出目录,请按如下方式配置 outputBaseDir 属性:

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

启动器属性

为了定制应用程序启动过程,您可以自定义以下属性:

属性

描述

mainClass

包含 main 方法的类的完全限定名称。

args

应用程序 main 方法的参数。

jvmArgs

应用程序 JVM 的参数。

以下是一个配置示例:

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

元数据

nativeDistributions DSL 块中,您可以配置以下属性:

属性

描述

默认值

packageName

应用程序的名称。

Gradle 项目的 名称

packageVersion

应用程序的版本。

Gradle 项目的 版本

description

应用程序的描述。

copyright

应用程序的版权信息。

vendor

应用程序的供应商。

licenseFile

应用程序的许可证文件。

以下是一个配置示例:

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")) } } }

管理资源

要打包和加载资源,您可以使用 Compose Multiplatform 资源库、JVM 资源加载或将文件添加到打包的应用程序中。

资源库

为项目设置资源的最简单方法是使用资源库。通过资源库,您可以在所有支持的平台的通用代码中访问资源。详情请参阅 Multiplatform resources

JVM 资源加载

Compose Multiplatform for desktop 运行在 JVM 平台上,这意味着您可以使用 java.lang.Class API 从 .jar 文件中加载资源。您可以通过 Class::getResourceClass::getResourceAsStream 访问 src/main/resources 目录中的文件。

将文件添加到打包的应用程序

在某些情况下,从 .jar 文件加载资源可能不太实用,例如当您有特定于目标的资源并且需要仅在 macOS 软件包中包含文件而不在 Windows 中包含时。

在这种情况下,您可以配置 Gradle 插件以在安装目录中包含其他资源文件。使用 DSL 指定根资源目录,如下所示:

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

在上面的示例中,根资源目录定义为 <PROJECT_DIR>/resources

Gradle 插件将按以下方式包含资源子目录中的文件:

  1. 通用资源: 位于 <RESOURCES_ROOT_DIR>/common 的文件将包含在所有软件包中,无论目标操作系统或架构如何。

  2. 操作系统特定资源: 位于 <RESOURCES_ROOT_DIR>/<OS_NAME> 的文件将仅包含在为特定操作系统构建的软件包中。 <OS_NAME> 的有效值为: windowsmacoslinux

  3. 操作系统和架构特定资源: 位于 <RESOURCES_ROOT_DIR>/<OS_NAME>-<ARCH_NAME> 的文件将仅包含在为特定操作系统和 CPU 架构组合构建的软件包中。 <ARCH_NAME> 的有效值为: x64arm64。 例如, <RESOURCES_ROOT_DIR>/macos-arm64 中的文件将仅包含在为 Apple Silicon Mac 设计的软件包中。

您可以使用 compose.application.resources.dir 系统属性访问包含的资源:

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

自定义源集

如果您使用 org.jetbrains.kotlin.jvmorg.jetbrains.kotlin.multiplatform 插件,您可以依赖默认配置:

  • 使用 org.jetbrains.kotlin.jvm 的配置包括来自 main 源集 的内容。

  • 使用 org.jetbrains.kotlin.multiplatform 的配置包括来自单个 JVM 目标 的内容。如果定义多个 JVM 目标,默认配置将被禁用。在这种情况下,您需要手动配置插件或指定单个目标(见下文)。

如果默认配置不明确或不充分,您可以通过以下几种方式自定义:

使用 Gradle 源集

plugins { kotlin("jvm") id("org.jetbrains.compose") } val customSourceSet = sourceSets.create("customSourceSet") compose.desktop { application { from(customSourceSet) } }

使用 Kotlin JVM 目标

plugins { kotlin("multiplatform") id("org.jetbrains.compose") } kotlin { jvm("customJvmTarget") {} } compose.desktop { application { from(kotlin.targets["customJvmTarget"]) } }

手动配置:

  • 使用 disableDefaultConfiguration 禁用默认设置。

  • 使用 fromFiles 指定要包含的文件。

  • 指定 mainJar 文件属性以指向包含主类的 .jar 文件。

  • 使用 dependsOn 为所有插件任务添加任务依赖。

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

应用程序图标

确保您的应用程序图标以以下操作系统特定格式提供:

  • .icns 用于 macOS

  • .ico 用于 Windows

  • .png 用于 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")) } } } }

平台特定选项

平台特定设置可以使用相应的 DSL 块进行配置:

compose.desktop { application { nativeDistributions { macOS { // macOS 选项 } windows { // Windows 选项 } linux { // Linux 选项 } } } }

下表描述了所有支持的平台特定选项。 不建议使用未记录的属性。

平台

选项

描述

所有平台

iconFile.set(File("PATH_TO_ICON"))

指定应用程序的平台特定图标的路径。详情请参阅 应用程序图标 部分。

packageVersion = "1.0.0"

设置平台特定的软件包版本。详情请参阅 软件包版本 部分。

installationPath = "PATH_TO_INST_DIR"

指定默认安装目录的绝对或相对路径。在 Windows 上,您还可以使用 dirChooser = true 以在安装期间启用自定义路径。

Linux

packageName = "custom-package-name"

覆盖默认的应用程序名称。

debMaintainer = "maintainer@example.com"

指定软件包维护者的电子邮件。

menuGroup = "my-example-menu-group"

为应用程序定义一个菜单组。

appRelease = "1"

为 rpm 软件包设置发布值或为 deb 软件包设置修订值。

appCategory = "CATEGORY"

为 rpm 软件包分配组值或为 deb 软件包分配部分值。

rpmLicenseType = "TYPE_OF_LICENSE"

指示 rpm 软件包的许可证类型。

debPackageVersion = "DEB_VERSION"

设置 deb 特定的软件包版本。详情请参阅 软件包版本 部分。

rpmPackageVersion = "RPM_VERSION"

设置 rpm 特定的软件包版本。详情请参阅 软件包版本 部分。

macOS

bundleID

指定唯一的应用程序标识符,只能包含字母数字字符(A-Z, a-z, 0-9 )、连字符(- )和点(. )。建议使用反向 DNS 表示法(com.mycompany.myapp)。

packageName

应用程序的名称。

dockName

在菜单栏、“关于 <App>”菜单项和 Dock 中显示的应用程序名称。默认值为 packageName

minimumSystemVersion

运行应用程序所需的最低 macOS 版本。详情请参阅 LSMinimumSystemVersion

signing, notarization, provisioningProfile, runtimeProvisioningProfile

请参阅 macOS 上的签名和公证分发 教程。

appStore = true

指定是否为 Apple App Store 构建和签名应用程序。至少需要 JDK 17。

appCategory

Apple App Store 的应用程序类别。为 App Store 构建时,默认值为 public.app-category.utilities ,否则为 Unknown。 请参阅 LSApplicationCategoryType 获取有效类别列表。

entitlementsFile.set(File("PATH_ENT"))

指定包含签名时使用的权限的文件路径。提供自定义文件时,请确保添加 Java 应用程序所需的权限。请参阅 sandbox.plist 获取为 App Store 构建时使用的默认文件。请注意,此默认文件可能因 JDK 版本而异。如果未指定文件,插件将使用 jpackage 提供的默认权限。详情请参阅 macOS 上的签名和公证分发 教程。

runtimeEntitlementsFile.set(File("PATH_R_ENT"))

指定包含签名 JVM 运行时使用的权限的文件路径。提供自定义文件时,请确保添加 Java 应用程序所需的权限。请参阅 sandbox.plist 获取为 App Store 构建时使用的默认文件。请注意,此默认文件可能因 JDK 版本而异。如果未指定文件,插件将使用 jpackage 提供的默认权限。详情请参阅 macOS 上的签名和公证分发 教程。

dmgPackageVersion = "DMG_VERSION"

设置 DMG 特定的软件包版本。详情请参阅 软件包版本 部分。

pkgPackageVersion = "PKG_VERSION"

设置 PKG 特定的软件包版本。详情请参阅 软件包版本 部分。

packageBuildVersion = "DMG_VERSION"

设置软件包的构建版本。详情请参阅 软件包版本 部分。

dmgPackageBuildVersion = "DMG_VERSION"

设置 DMG 特定的软件包构建版本。详情请参阅 软件包版本 部分。

pkgPackageBuildVersion = "PKG_VERSION"

设置 PKG 特定的软件包构建版本。详情请参阅 软件包版本 部分。

infoPlist

请参阅 macOS 上的 Info.plist 部分。

Windows

console = true

为应用程序添加控制台启动器。

dirChooser = true

在安装期间启用自定义安装路径。

perUserInstall = true

启用按用户安装应用程序。

menuGroup = "start-menu-group"

将应用程序添加到指定的“开始”菜单组。

upgradeUuid = "UUID"

指定一个唯一 ID,使用户能够通过安装程序更新应用程序,当有比已安装版本更新的版本时。该值必须为单个应用程序保持恒定。详情请参阅 如何生成 GUID

msiPackageVersion = "MSI_VERSION"

设置 MSI 特定的软件包版本。详情请参阅 软件包版本 部分。

exePackageVersion = "EXE_VERSION"

设置 EXE 特定的软件包版本。详情请参阅 软件包版本 部分。

macOS 特定配置

macOS 上的签名和公证

现代 macOS 版本不允许用户执行从互联网下载的未签名应用程序。如果您尝试运行此类应用程序,您会遇到以下错误:“YourApp 已损坏,无法打开。您应该弹出磁盘映像”。

要了解如何签名和公证您的应用程序,请参阅我们的教程

macOS 上的信息属性列表

虽然 DSL 支持基本的平台特定自定义,但仍可能存在超出提供功能的情况。如果需要指定 DSL 中未表示的 Info.plist 值,您可以包含原始 XML 片段作为解决方法。此 XML 将附加到应用程序的 Info.plist

示例:深度链接

  1. build.gradle.kts 文件中定义自定义 URL 方案:

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. 使用 java.awt.Desktop 类在 src/main/main.kt 文件中设置 URI 处理程序:

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. 执行 `

23 四月 2025