原生分发包
本部分你将了解原生分发包:如何为所有支持的系统创建安装程序和软件包,以及如何在本地以与分发包相同的设置运行应用程序。
请阅读以下内容的详细信息:
- 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 插件以指定你需要哪些模块。 有关更多信息,请参见 undefined 部分。
作为替代方案,你可以使用 Conveyor,这是一个不由 JetBrains 开发的外部工具。 Conveyor 支持在线更新、交叉构建和各种其他特性,但非开源项目需要许可证。 有关更多信息,请参考 Conveyor 文档。
基本任务
Compose Multiplatform Gradle 插件中可配置的基本单元是 application
(不要与已弃用的 Gradle application 插件混淆)。
application
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 用作 compile 、implementation 或 runtime 依赖项。 |
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")
// Alternatively: includeAllModules = true
}
}
}
你可以手动指定所需的模块,或运行 suggestModules
。suggestModules
任务使用 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 上版本控制的更多信息, 请参见 CFBundleShortVersionString
和 CFBundleVersion
文档。
以下是按优先级顺序指定包版本的模板:
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 = "..."
}
}
}
}
要定义包版本,请遵循以下规则:
文件类型 | 版本格式 | 详情 |
dmg , pkg | MAJOR[.MINOR][.PATCH] |
|
msi , exe | MAJOR.MINOR.BUILD |
|
deb | [EPOCH:]UPSTREAM_VERSION[-DEBIAN_REVISION] |
|
rpm | 任何格式 | 版本不得包含 - (破折号)字符。 |
JDK 版本
该插件使用 jpackage
,它要求 JDK 版本不低于 JDK 17。 在指定 JDK 版本时,请确保你满足以下至少一项要求:
JAVA_HOME
环境变量指向兼容的 JDK 版本。通过 DSL 设置
javaHome
属性:kotlincompose.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 资源加载或将文件添加到打包的应用程序。
资源库
为项目设置资源最直接的方法是使用资源库。 通过资源库,你可以在所有支持的平台上的通用代码中访问资源。 详情请参见多平台资源。
JVM 资源加载
Compose Multiplatform for desktop 在 JVM 平台上运行,这意味着你可以使用 java.lang.Class
API 从 .jar
文件加载资源。你可以通过 Class::getResource
或 Class::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 插件将按如下方式包含资源子目录中的文件:
通用资源: 位于
<RESOURCES_ROOT_DIR>/common
中的文件将包含在所有包中,无论目标操作系统或架构如何。操作系统特有资源: 位于
<RESOURCES_ROOT_DIR>/<OS_NAME>
中的文件将只包含在为特定操作系统构建的包中。<OS_NAME>
的有效值为:windows
、macos
和linux
。操作系统和架构特有资源: 位于
<RESOURCES_ROOT_DIR>/<OS_NAME>-<ARCH_NAME>
中的文件将只包含在为操作系统和 CPU 架构的特定组合构建的包中。<ARCH_NAME>
的有效值为:x64
和arm64
。 例如,<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.jvm
或 org.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 {
// Options for macOS
}
windows {
// Options for Windows
}
linux {
// Options for 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 = "[email protected]" | 指定包维护者的电子邮件。 | |
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 应用程序所需的授权。有关为 App Store 构建时使用的默认文件,请参见 sandbox.plist。请注意,此默认文件可能因你的 JDK 版本而异。 如果未指定文件,插件将使用 jpackage 提供的默认授权。 详情请参见 macOS 上的分发包签名与公证 教程。 | |
runtimeEntitlementsFile.set(File("PATH_R_ENT")) | 指定包含签名 JVM 运行时时使用的授权文件的路径。当你提供自定义文件时, 请确保添加 Java 应用程序所需的授权。有关为 App Store 构建时使用的默认文件,请参见 sandbox.plist。请注意,此默认文件可能因你的 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,该 ID 允许用户通过安装程序更新应用程序, 当有比已安装版本更新的版本时。该值对于单个应用程序必须保持不变。 详情请参见 How To: Generate a GUID。 | |
msiPackageVersion = "MSI_VERSION" | 设置 MSI 特有的包版本。详情请参见包版本部分。 | |
exePackageVersion = "EXE_VERSION" | 设置 EXE 特有的包版本。详情请参见包版本部分。 |
macOS 特有配置
macOS 上的签名与公证
现代 macOS 版本不允许用户执行从互联网下载的未签名应用程序。如果你尝试运行此类应用程序,你将遇到 以下错误:“YourApp is damaged and can't be open. You should eject the disk image”(你的应用已损坏,无法打开。你应该弹出磁盘镜像)。
要了解如何签名和公证你的应用程序,请参见我们的教程。
macOS 上的信息属性列表
虽然 DSL 支持基本的平台特有自定义,但仍可能存在超出所提供功能的情况。 如果你需要指定 DSL 中未表示的 Info.plist
值, 你可以包含一段原始 XML 作为变通方法。此 XML 将附加到应用程序的 Info.plist
中。
示例:深度链接
在
build.gradle.kts
文件中定义一个自定义 URL 方案:kotlincompose.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> """
在
src/main/main.kt
文件中使用java.awt.Desktop
类设置 URI 处理程序:kotlinimport 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) } } }
执行
runDistributable
任务:./gradlew runDistributable
。
结果是,像 compose://foo/bar
这样的链接现在可以从浏览器重定向到你的应用程序。
精简与混淆
Compose Multiplatform Gradle 插件内置支持 ProGuard。 ProGuard 是一个用于代码精简和混淆的开源工具。
对于每个默认(不带 ProGuard)打包任务,Gradle 插件提供一个发布任务(带 ProGuard):
Gradle 任务 | 描述 |
默认: 发布: | 创建捆绑了 JDK 和资源的应用程序镜像。 |
默认: 发布: | 运行捆绑了 JDK 和资源的应用程序镜像。 |
默认: 发布: | 使用 Gradle JDK 运行非打包应用程序 .jar 。 |
默认: 发布: | 将应用程序镜像打包成 <FORMAT_NAME> 文件。 |
默认: 发布: | 将应用程序镜像打包成与当前操作系统兼容的格式。 |
默认: 发布: | 将应用程序镜像打包成一个超级 (胖) .jar 。 |
默认: 发布: | 上传 <FORMAT_NAME> 应用程序镜像以进行公证(仅限 macOS)。 |
默认: 发布: | 检测公证是否成功(仅限 macOS)。 |
默认配置启用了一些预定义 ProGuard 规则:
- 应用程序镜像被精简,即移除了未使用的类。
compose.desktop.application.mainClass
用作入口点。- 包含多条
keep
规则以确保 Compose 运行时保持功能性。
在大多数情况下,你不需要任何额外的配置即可获得精简的应用程序。 然而,ProGuard 可能不会跟踪字节码中的某些用法,例如,当一个类通过反射使用时。 如果你遇到只在 ProGuard 处理后出现的问题,你可能需要添加自定义规则。
要指定自定义配置文件,请按如下所示使用 DSL:
compose.desktop {
application {
buildTypes.release.proguard {
configurationFiles.from(project.file("compose-desktop.pro"))
}
}
}
有关 ProGuard 规则和配置选项的更多信息,请参阅 Guardsquare 手册。
混淆默认禁用。要启用它,请通过 Gradle DSL 设置以下属性:
compose.desktop {
application {
buildTypes.release.proguard {
obfuscate.set(true)
}
}
}
ProGuard 的优化默认启用。要禁用它们,请通过 Gradle DSL 设置以下属性:
compose.desktop {
application {
buildTypes.release.proguard {
optimize.set(false)
}
}
}
生成超级 JAR 默认禁用,ProGuard 会为每个输入 .jar
生成相应的 .jar
文件。要启用它,请通过 Gradle DSL 设置以下属性:
compose.desktop {
application {
buildTypes.release.proguard {
joinOutputJars.set(true)
}
}
}
下一步?
探索关于桌面组件的教程。