Skip to content

原生分发包

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

请阅读以下内容的详细信息:

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 文件示例:

kotlin
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 依赖项。
runmainClass 中指定的入口点在本地运行应用程序。run 任务启动一个未打包的 JVM 应用程序,带有完整的运行时。 与使用精简运行时创建紧凑二进制镜像相比,此方法更快且更易于调试。 要运行最终二进制镜像,请改用 runDistributable 任务。
createDistributable创建最终应用程序镜像而不创建安装程序。
runDistributable运行预打包的应用程序镜像。

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

包含 JDK 模块

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

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

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

kotlin
compose.desktop {
    application {
        nativeDistributions {
            modules("java.sql")
            // Alternatively: 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 文档。

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

kotlin
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, pkgMAJOR[.MINOR][.PATCH]
  • MAJOR 是一个大于 0 的整数
  • MINOR 是一个可选的非负整数
  • PATCH 是一个可选的非负整数
msi, exeMAJOR.MINOR.BUILD
  • MAJOR 是一个最大值为 255 的非负整数
  • MINOR 是一个最大值为 255 的非负整数
  • BUILD 是一个最大值为 65535 的非负整数
deb[EPOCH:]UPSTREAM_VERSION[-DEBIAN_REVISION]
  • EPOCH 是一个可选的非负整数
  • UPSTREAM_VERSION:
    • 只能包含字母数字字符以及 .+-~ 字符
    • 必须以数字开头
  • DEBIAN_REVISION:
    • 可选
    • 只能包含字母数字字符以及 .+~ 字符
有关更多详情,请参见 Debian 文档
rpm任何格式版本不得包含 -(破折号)字符。

JDK 版本

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

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

  • 通过 DSL 设置 javaHome 属性:

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

输出目录

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

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

启动器属性

要调整应用程序启动过程,你可以自定义以下属性:

属性描述
mainClass包含 main 方法的类的完全限定名称。
args应用程序 main 方法的实参。
jvmArgs应用程序 JVM 的实参。

以下是一个配置示例:

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

元数据

nativeDistributions DSL 代码块中,你可以配置以下属性:

属性描述默认值
packageName应用程序的名称。Gradle 项目的 名称
packageVersion应用程序的版本。Gradle 项目的 版本
description应用程序的描述。
copyright应用程序的版权信息。
vendor应用程序的供应商。
licenseFile应用程序的许可证文件。

以下是一个配置示例:

kotlin
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::getResourceClass::getResourceAsStream 访问 src/main/resources 目录中的文件。

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

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

在这些情况下,你可以配置 Gradle 插件以在安装目录中包含额外的资源文件。 按如下所示使用 DSL 指定根资源目录:

kotlin
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 系统属性访问包含的资源:

kotlin
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 源代码集

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

使用 Kotlin JVM 目标

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

手动:

  • 使用 disableDefaultConfiguration 禁用默认设置。
  • 使用 fromFiles 指定要包含的文件。
  • 指定 mainJar 文件属性以指向包含主类的 .jar 文件。
  • 使用 dependsOn 将任务依赖项添加到所有插件任务。
kotlin
compose.desktop {
    application {
        disableDefaultConfiguration()
        fromFiles(project.fileTree("libs/") { include("**/*.jar") })
        mainJar.set(project.file("main.jar"))
        dependsOn("mainJarTask")
    }
}

应用程序图标

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

  • .icns 用于 macOS
  • .ico 用于 Windows
  • .png 用于 Linux
kotlin
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 代码块进行配置:

kotlin
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 来在安装过程中启用自定义路径。
LinuxpackageName = "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 特有的包版本。详情请参见包版本部分。
macOSbundleID 指定唯一的应用程序标识符,只能包含字母数字字符 (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 部分。
Windowsconsole = 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 中。

示例:深度链接

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

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

    kotlin
    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)
            }
        }
    }
  3. 执行 runDistributable 任务:./gradlew runDistributable

结果是,像 compose://foo/bar 这样的链接现在可以从浏览器重定向到你的应用程序。

精简与混淆

Compose Multiplatform Gradle 插件内置支持 ProGuard。 ProGuard 是一个用于代码精简和混淆的开源工具

对于每个默认(不带 ProGuard)打包任务,Gradle 插件提供一个发布任务(带 ProGuard):

Gradle 任务描述

默认: createDistributable

发布: createReleaseDistributable

创建捆绑了 JDK 和资源的应用程序镜像。

默认: runDistributable

发布: runReleaseDistributable

运行捆绑了 JDK 和资源的应用程序镜像。

默认: run

发布: runRelease

使用 Gradle JDK 运行非打包应用程序 .jar

默认: package<FORMAT_NAME>

发布: packageRelease<FORMAT_NAME>

将应用程序镜像打包成 <FORMAT_NAME> 文件。

默认: packageDistributionForCurrentOS

发布: packageReleaseDistributionForCurrentOS

将应用程序镜像打包成与当前操作系统兼容的格式。

默认: packageUberJarForCurrentOS

发布: packageReleaseUberJarForCurrentOS

将应用程序镜像打包成一个超级 (胖) .jar

默认: notarize<FORMAT_NAME>

发布: notarizeRelease<FORMAT_NAME>

上传 <FORMAT_NAME> 应用程序镜像以进行公证(仅限 macOS)。

默认: checkNotarizationStatus

发布: checkReleaseNotarizationStatus

检测公证是否成功(仅限 macOS)。

默认配置启用了一些预定义 ProGuard 规则:

  • 应用程序镜像被精简,即移除了未使用的类。
  • compose.desktop.application.mainClass 用作入口点。
  • 包含多条 keep 规则以确保 Compose 运行时保持功能性。

在大多数情况下,你不需要任何额外的配置即可获得精简的应用程序。 然而,ProGuard 可能不会跟踪字节码中的某些用法,例如,当一个类通过反射使用时。 如果你遇到只在 ProGuard 处理后出现的问题,你可能需要添加自定义规则。

要指定自定义配置文件,请按如下所示使用 DSL:

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

有关 ProGuard 规则和配置选项的更多信息,请参阅 Guardsquare 手册

混淆默认禁用。要启用它,请通过 Gradle DSL 设置以下属性:

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

ProGuard 的优化默认启用。要禁用它们,请通过 Gradle DSL 设置以下属性:

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

生成超级 JAR 默认禁用,ProGuard 会为每个输入 .jar 生成相应的 .jar 文件。要启用它,请通过 Gradle DSL 设置以下属性:

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

下一步?

探索关于桌面组件的教程。