mcpField
K
kotlin-tooling-gradle-to-kotlin-toolchain-project

kotlin-tooling-gradle-to-kotlin-toolchain-project

github.com/kotlin开发1.1k457 次安装7 周前更新
代码仓库

关于 kotlin-tooling-gradle-to-kotlin-toolchain-project

这项迁移技能指导将 Gradle 项目迁移到 Kotlin Toolchain,涵盖依赖与配置转换,以及 Gradle 插件的替换。它提供分阶段的盘点、插件处理、CI 改写和端到端验证流程,并强调保留源代码。

主要功能

  1. 01盘点 Gradle 插件、任务与配置
  2. 02将共享配置转换为模块模板
  3. 03为插件选择原生替代或本地重实现
  4. 04将依赖统一迁移到版本目录
  5. 05改写 CI 构建与检查流程
  6. 06验证迁移后的项目和任务

适用场景

  1. 01规划现有 JVM 项目的迁移
  2. 02处理没有原生替代方案的 Gradle 插件
  3. 03迁移版本目录和共享构建配置
  4. 04更新 CI 并验证构建产物

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/kotlin/kotlin-agent-skills --skill kotlin-tooling-gradle-to-kotlin-toolchain-project

安装后在 Claude Code 中输入/kotlin-tooling-gradle-to-kotlin-toolchain-project确认已加载。

附带文件references/examples.md

详细介绍

Gradle → Kotlin Toolchain 项目迁移

同时完成两项工作:机械地转换依赖和配置,并为 Toolchain 没有原生方案的每个 Gradle 插件提供替代实现。可改编的模板见 references/examples.md,这些模板源自一次真实迁移。

针对插件相关的子问题,请借助配套技能:kotlin-tooling-kotlin-toolchain 负责语法, kotlin-tooling-gradle-to-kotlin-toolchain-plugin 负责逐个移植插件的工作流(对于每个没有原生等效方案的 Gradle 插件,都要运行一次),kotlin-tooling-kotlin-toolchain-plugin-authoring 负责从头编写插件。

原则

  1. 保留源代码。 如果迁移迫使你修改业务逻辑,本地插件很可能就能填补这个缺口。读取类路径中 release.properties 的代码应该继续正常工作——原样发布该文件,不要改写使用它的代码。
  2. 每个第三方 Gradle 插件都必须舍弃、采用原生方案或重新实现——绝不能“保留 Gradle 插件”。 你自己的约定插件则不同:buildSrc / build-logic 预编译脚本属于共享的配置,因此它们应转换为模块模板,而不是本地插件——参见 没有 buildSrc / 约定插件。
  3. 编写本地插件前先搜索 GitHub。 很可能已经有人实现过;引入一个可用实现,总是胜过从头移植。在哪里查找、搜索什么: 阶段 2中的“范围”。只有在搜索无果后才自行编写。接入前要通读引入的代码——它会在构建时运行,并拥有完整的文件系统和网络访问权限。
  4. 所有依赖都应放在版本目录中。 gradle/libs.versions.toml 会作为内置的 $libs.* 目录继续使用。每个模块 / 插件 YAML 中的每个坐标都必须转换为 $libs.* 引用。[bundles] 没有 Toolchain 等效项,应转换为模块模板——参见没有版本目录 bundles。

工作流

阶段 1 — 清点 Gradle 构建

在编写任何 YAML 之前,先记录清单(例如 MIGRATION_PLAN.md);它将成为 PR 描述中核对的检查清单。

  • plugins { } 块中的插件,逐一归类为原生插件或本地插件。原生插件包括 org.jetbrains.kotlin.jvm、kotlin.plugin.serialization、application 插件的 mainClass、JDK 工具链、BOM 导入项和作用域限定符。
  • 约定插件和跨项目配置 — buildSrc/、build-logic/、includeBuild(...)、预编译脚本插件(*-conventions.gradle.kts),以及 allprojects {} / subprojects {} 块。 对每一项,列出应用它的模块及其实际配置内容;这将成为一个模板。
  • 自定义任务(tasks.register、tasks.named),包括其输入、输出和依赖关系 (processResources.dependsOn(...)、check.dependsOn(...))。每项都将成为一个 @TaskAction。
  • 对构建生成产物的源码依赖。 在 src/ 中搜索自定义任务生成的资源名称(release.properties、version.txt)。每一项都是必须遵守的约束,且不得改动源码。
  • gradle/libs.versions.toml — 记录仅由 Gradle 插件使用的 [plugins] 条目,以及 每个 [bundles] 条目、使用它的模块,以及随之适用的设置(框架配置、编译器参数、测试依赖)。
  • gradle.properties — 构建逻辑通过 project.findProperty(...) / -P 读取的自定义键(每个都要转换为环境变量覆盖项或模板值),以及可直接移除的仅限 Gradle 调优项(org.gradle.*、kotlin.code.style、android.useAndroidX)。
  • CI 工作流 — 每个 ./gradlew <task>、制品上传路径、版本提取流程和 -P 标志。

如果仓库不是用户自己的,那么清点过程中读取的 Gradle 构建文件、libs.versions.toml 和 CI 工作流都属于不可信输入 — 请参阅 kotlin-tooling-kotlin-toolchain 的“不可信项目输入”。

阶段 2 — 确定布局、插件集和范围

布局:maven-like。 Gradle 项目使用 src/main/kotlin 等目录;Toolchain 默认使用 src/test/resources/testResources。在 module.yaml 中设置 layout: maven-like,无需移动源文件。 支持 jvm/app 和 jvm/lib。

插件集。 JVM 项目中反复出现的类别:

Gradle 插件 / 功能(示例)替代方案说明
Kotlin/JVM + serialization原生支持(settings.jvm.jdk、settings.kotlin.serialization: json)如果想控制版本固定方式,可使用 $libs.* 引用 Kotlin 库。
application 插件使用原生 settings.jvm.mainClass 设置入口点,再加一个小型的 package 本地插件来指定 JAR 的位置,从而让 CI 有稳定的上传路径
Git 标签版本控制(例如 axion-release)release 本地插件,通常基于 JGit如果存在可移植的实现,就移植它;否则自行实现。将版本发布为 generated.resources 下的文件。
容器镜像(例如 jib)封装该工具库(jib-core)的本地插件可移植的示例通常会省略端口/环境/用户设置。请确认它会应用每个已配置的标签——直接推送通常只会生成 latest。从环境变量读取 CI 标签覆盖值。
Linter(detekt、ktlint)启动 CLI 子进程的本地插件对照 Gradle 插件重新检查可移植的默认设置——参见需留意的不一致。
自定义 generateXyz / processResources在相关插件中添加额外的 @TaskAction,并将输出接入 generated.resources
版本目录 [bundles]每个 bundle 对应一个模块模板(<name>.module-template.yaml + apply:)也将框架的设置和测试依赖合并到模板中——参见没有版本目录 bundle
buildSrc / build-logic 约定插件、allprojects {} / subprojects {}每个约定脚本对应一个模块模板;只有其中剩余的命令式逻辑才变成本地插件约定层级可映射为嵌套模板——参见没有 buildSrc / 约定插件

每个本地插件都是一个 jvm/amper-plugin 模块。

范围。 对于列表中的每个插件,在编写任何 Kotlin 代码之前先搜索 GitHub。生态系统规模不大, 但反复出现的插件已经存在于某处。搜索顺序:

  1. JetBrains/kotlin-toolchain → build-sources/ (detekt、dokka、binary-compatibility-validator、protobuf、generate-build-properties、 project-commands)——Toolchain 自身使用构建的本地插件,也就是事实上的参考实现。同一仓库中还有 plugin-samples/ 和 docs/。
  2. 按插件标记而非工具名称搜索 GitHub 代码: "product: jvm/amper-plugin"、path:plugin.yaml "@TaskAction"、"jvm/amper-plugin" jib。
  3. 搜索工具自己的仓库——大多数 Linter/打包工具都会发布 -cli 或 -core 制品,而薄封装插件所需的全部内容往往就是这些制品,因此“没有插件”通常仍意味着“存在一个 50 行的封装插件”。

然后为每个插件选择:移植(原样复制找到的实现;保留其许可证头,并添加包含来源 URL + commit 的注释,以便重新同步)、扩展(移植 + 额外的 Settings 字段)或自行实现(没有找到实现,或需求是定制的)。一个 PR 的合理默认做法是:移植已有实现,自行实现小型定制插件(package 插件、薄封装 Linter 的插件),其余的推迟处理。在信任移植的插件之前,务必将其行为与它所替代的 Gradle 插件进行差异比较——参见 移植的 Linter 插件可能比 Gradle 插件更严格。

第 3 阶段 — 实现
  1. 从参考工具链项目(例如这个项目)复制 kotlin / kotlin.bat,或在临时目录中运行 kotlin init。在 .sdkmanrc 中固定 kotlintoolchain=<version>,使其与包装器保持一致。
  2. 编写 project.yaml,列出所有插件模块路径。
  3. 对每个插件:先在 GitHub 上搜索(参见第 2 阶段中的“范围”),然后将其引入或编写到 plugins/<name>/ 下;每次操作后运行 ./kotlin show modules,确认模型仍可加载。
  4. 在项目根目录编写模板:为每个约定脚本,以及每个有两个或更多使用者的 [bundles] 条目各编写一个 <name>.module-template.yaml,其中包含相关依赖项,以及随之使用的设置、测试依赖项和仓库;嵌套方式应与 Gradle 约定一致。
  5. 编写根目录的 module.yaml:设置 product: jvm/app、layout: maven-like、$libs.* 依赖项、用于应用模板的 apply: 列表,以及启用每个本地插件并配置其非默认设置的 plugins: 块。
  6. 单独验证每个插件:运行 ./kotlin task :<module>:<task>@<plugin> 或 ./kotlin do <command>。
  7. 重写 CI(参见第 4 阶段)。
  8. 删除 build.gradle.kts、gradlew、gradlew.bat、gradle/wrapper/——但保留 gradle/libs.versions.toml。
  9. 清理目录:删除整个 [plugins] 块、任何仅供其使用的 [versions] 键,以及所有使用者都应用模板后删除 [bundles]。
  10. 检查每个 module.yaml / plugin.yaml,将其中的 Maven 字面坐标替换为 $libs.<key>。
第 4 阶段 — 重写 CI
  • 设置 KOTLIN_CLI_NO_WELCOME_BANNER: "1"。
  • 将 ./gradlew build && ./gradlew check 替换为 ./kotlin build && ./kotlin check(这会运行每个插件的 checks: 注册项和测试)。
  • 将 ./gradlew jib -Djib.to.tags=… 替换为 ./kotlin do jib,并通过插件读取的环境变量传递标签。 通常,引入的 jib 风格插件没有这个钩子——引入时请添加。
  • 构件上传路径会发生变化:Gradle 的 build/libs/<name>.jar 已不存在,而 jarJvm 会写入 Toolchain 内部路径。编写一个 package 插件,将 JAR 暂存到 ${module.rootDir}/build/libs/${module.name}.jar,以便继续简单地上传构件。
第 5 阶段 — 端到端验证

在本地运行每个面向用户的命令,并在 PR 的测试计划中记录输出:

./kotlin show modules            # project model loads, all expected modules listed
./kotlin clean && ./kotlin build
./kotlin test
./kotlin check                   # linters + tests; expect zero violations after Phase 3
./kotlin do currentVersion       # if applicable
./kotlin do jib                  # or jibBuildTar to avoid pushing locally (if applicable)
./kotlin do package              # verify build/libs/<name>.jar and its Main-Class manifest (if applicable)
./kotlin do ktlintFormat         # if applicable

需要注意的差异

module.yaml 中不支持 ${...} 插值

configFile: ${module.rootDir}/detekt.yml 会被按字面值处理。请使用相对于模块的路径 (configFile: detekt.yml)。插值仅在 plugin.yaml 中有效。

模块名称来自目录名称

没有 name: 字段。actions/checkout 会将仓库克隆到以 GitHub 仓库命名的目录中,而本地工作树解析出的 ${module.name} 可能是其他值。切勿在插件任务的输出路径中硬编码模块名称——请使用 ${module.name}。

未设置 enabled: true 的插件设置会被忽略
plugins:
  release: enabled         # shorthand — only valid with no other settings
  jib:                     # long form — required as soon as any setting is present
    enabled: true
    container:
      mainClass: com.example.App

未设置 enabled: true 的设置只会产生警告(“Plugin X is not enabled, but has some explicit configuration”),随后跳过该插件。

引入的代码检查插件可能比 Gradle 插件更严格

对比引入的插件与 Gradle 插件默认任务所传递的标志,并将任何更严格的检查设为可选项,使默认行为与旧行为一致。

典型案例:上游 detekt 插件(amper/build-sources/detekt/)总会向 detekt-cli 传递 --classpath,以启用类型解析,而 Gradle 的默认 detekt 任务不会。直接使用该插件会暴露 Gradle 从未报告过的违规项(尤其是带 return 的 Elvis 表达式中的 UnreachableCode)。修复方法:在插件的 Settings 中添加 useTypeResolution: Boolean get() = false,并通过该设置控制此标志——补丁见 references/examples.md。

没有版本目录 bundle

只有 [libraries] 键可以解析($libs.<key>)。$libs.bundles.<name> 不存在,目录中的 [bundles] 是 Toolchain 永远不会读取的无效配置。官方答复(KTC-4759)是为每个 bundle 创建一个模块模板,并在 bundle 原先使用的位置应用它:

# gradle/libs.versions.toml — before
[bundles]
ktor-server = ["ktor-server-core", "ktor-server-netty", "ktor-server-content-negotiation"]
# ktor-server.module-template.yaml — at the project root; templates cannot declare product:
dependencies:
  - $libs.ktor.server.core
  - $libs.ktor.server.netty
  - $libs.ktor.server.content.negotiation

settings:
  kotlin:
    serialization: json

test-dependencies:
  - $libs.ktor.server.test.host
# module.yaml
product: jvm/app

apply:
  - //ktor-server.module-template.yaml

模板是更好的解决方案,而不只是权宜之计:bundle 只携带坐标,而它所启用的框架通常还需要 settings(kotlin.serialization、springBoot、freeCompilerArgs)、test-dependencies,有时还需要 repositories。将这些内容全部放进模板,这样一行 apply: 就能启用可用的框架,而不只是一个裸类路径。

注意以下规则:

  • 路径格式为 //<name>.module-template.yaml,相对于项目根目录(即 project.yaml 所在目录)。
  • 模板中不能有 product:。模板可以 apply: 其他模板;即使通过两条路径引用,每个模板也只会应用一次,因此依赖列表只会出现一次。
  • 合并语义:列表追加,标量覆盖,并且无论 module.yaml 中 apply: 所在的位置如何,module.yaml 始终具有最高优先级。
  • 如果两个同级模板设置了相同的标量(例如 settings.jvm.release),就会报错(“Conflicting values for property”)。可以在使用方的 module.yaml 中设置该值,或创建一个同时应用这两个模板的第三个模板来解决。
  • 不要转换只有一个使用方的 bundle。模板从第二个模块开始才有收益;少于两个模块时,直接内联 $libs.* 列表。
  • 模板也可以表示按平台限定的部分的并集(如 dependencies@jvm、settings@android),因此,即使 KMP bundle 被拆分到多个源集,也仍可使用一个模板。
没有 buildSrc / convention 插件——共享配置放入模板

buildSrc/、build-logic/ 和 includeBuild(...) 都没有对应形式,而 project.yaml 只包含 modules: 和 plugins:——没有根项目继承机制,也没有地方放置命令式共享构建逻辑。本地插件也不是合适的目标:插件贡献的是任务,不会注入模块配置。预编译脚本插件大多是声明式的,因此可将其转换为一个 <name>.module-template.yaml,并由原先使用 plugins { id("<name>") } 的模块应用:

// buildSrc/src/main/kotlin/service-conventions.gradle.kts
plugins {
    kotlin("jvm")
    kotlin("plugin.serialization")
}
kotlin { jvmToolchain(21) }
repositories { maven("https://jitpack.io") }
dependencies {
    implementation(libs.ktor.server.core)
    testImplementation(libs.kotest.runner.junit5)
}
# service-conventions.module-template.yaml
settings:
  jvm:
    jdk:
      version: 21
  kotlin:
    serialization: json

repositories:
  - id: jitpack
    url: https://jitpack.io

dependencies:
  - $libs.ktor.server.core

test-dependencies:
  - $libs.kotest.runner.junit5
Convention 脚本结构模板对应项
plugins { kotlin("jvm"), kotlin("plugin.serialization"), id("org.springframework.boot") }原生 settings:(settings.kotlin.*、settings.springBoot 等)
kotlin { jvmToolchain(21) }、java { targetCompatibility }settings.jvm.jdk.version、settings.jvm.release
dependencies { implementation / api / testImplementation }dependencies:(api 使用 : exported)和 test-dependencies:
repositories { }repositories:
tasks.withType<Test> { useJUnitPlatform() }内置设置 / settings.junit
一个 convention 脚本应用另一个 convention 脚本嵌套模板(模板中的 apply:)
根脚本中的 allprojects {} / subprojects {}每个 module.yaml 都应用的一个模板
tasks.register(…)、doLast { } 以及任何命令式逻辑剩余部分——每种行为使用一个本地插件

此外:

  • 直接删除 buildSrc/。其 libs 目录访问器由模板中直接使用的 $libs.* 替代。
  • 通常,将一个臃肿的 convention 脚本拆成多个小型模板(jvm、ktor-server、testing)会更合适;bundle 模板也可以并入同一层级结构——合并和冲突规则请参见没有版本目录 bundle,此处同样适用。
  • 尚未验证能否在模板内部启用本地插件(即在 *.module-template.yaml 中使用 plugins: 块)。如果 convention 脚本启用了你重新实现的插件,在确认模板形式可以加载之前,请将 plugins: 块保留在每个 module.yaml 中(运行 ./kotlin show modules,并实际运行一个任务确认)。
无依赖排除功能

没有与 Gradle 的 exclude(group, module) 等效的功能。任何传递依赖排除都会被静默忽略,相关库也会进入运行时类路径。请在 PR 中注明这一权衡(通常会多出几百 KB 未使用的内容)。

不支持插件间依赖

插件彼此隔离。如果两个插件在逻辑上属于一组,请将两个任务操作放在同一个插件模块中,或抽取出库。

不支持 -P / -D 命令行覆盖

请在 @TaskAction 中通过环境变量读取临时覆盖值(强制版本、跳过检查、动态镜像标签);也不要依赖 --setting 标志(固定版本的 CLI 可能会拒绝它)。模式见 kotlin-tooling-gradle-to-kotlin-toolchain-plugin 技能。

获取命令的输出值

@TaskAction 中的 println 不是机器可读的通道。Toolchain 会将其包装为 <ts> INFO :<module>:<task>@<plugin> <value>,并追加 <task> successful 标语,因此 ./kotlin do currentVersion | tail -n1 会得到该标语;而将 --log-level 降至 error/off 会完全丢弃值所在的行,因为它是在 INFO 级别输出的。请改用以下方式:

  • 让任务将值写入由环境变量指定名称的文件,然后用 cat 读取。此外,其他任务也可以在后续步骤中复用此值
  • 或读取命令生成的 git 状态:git describe --tags --exact-match HEAD。
  • 如果必须解析标准输出,请匹配任务坐标:awk '/<task>@<plugin>/ { v = $NF } END { print v }'。
测试资源会覆盖 generated.resources

当插件生成 release.properties,且 src/test/resources/release.properties 存在时,类路径顺序会优先使用测试 fixture。通常这能正常工作,但不够稳妥——如果测试要断言特定值,请注入 stub 服务,而不要依赖优先级。

Dependabot 需要 stub build.gradle.kts

Dependabot 的 Gradle 文件获取器在扫描 gradle/libs.versions.toml 之前,要求配置目录中存在 build.gradle(.kts);如果没有,package-ecosystem: "gradle" 会静默地不执行任何操作。请在根目录保留一个空的 build.gradle.kts,并添加注释说明原因。Toolchain 会忽略它。

Apple Silicon 上仅支持 amd64 的容器镜像

像 mongo:3.2 这样的镜像在 M 系列 Mac 上通过 Rosetta/QEMU 运行时会崩溃 (runtime: failed to create new OS thread (have 2 already; errno=22))。迁移不会改变这一点——同一镜像在 ./gradlew check 下也会失败。请标记为既有问题;在 ubuntu-latest 上运行的 CI 不受影响。

iOS(KMP):迁移后的 Info.plist 会丢失其 CFBundle* 键

Gradle/KMP iOS 应用会将 bundle 元数据保存在手动维护的 iosApp.xcodeproj (GENERATE_INFOPLIST_FILE = YES)中,因此 Xcode 会合成 CFBundle* 键,而已提交的 Info.plist 有意保持不完整。Toolchain 会忽略该 .xcodeproj,自行生成项目,并原样使用你的 plist——不会有任何内容合成这些键,且 .app 没有 bundle id:

Simulator device failed to install the application. Missing bundle ID.

修复方法:让 plist 包含所有必需内容,并在其中保留你的应用专属键,同时添加以下键:

<key>CFBundleDevelopmentRegion</key><string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key><string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key><string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
<key>CFBundleVersion</key><string>1</string>

PRODUCT_BUNDLE_IDENTIFIER 会在生成的目标上设置。有关底层行为,请参阅 kotlin-tooling-kotlin-toolchain 技能的“iOS 应用”部分。

KMP:保留每个源集的全部依赖

将每个 Gradle 源集(commonMain、jvmMain、androidMain、iosMain、commonTest、jvmTest、 ……)转换为对应的 Amper 配置项:dependencies、dependencies@jvm/@android/@ios、test-dependencies、 test-dependencies@<platform>。不要只挑看起来显而易见的库依赖:

  • *Main 中的依赖也会进入对应目标的测试类路径(jvmTest 扩展自 jvmMain),因此删掉某项依赖可能导致测试失败,却不会引发编译错误。

  • 有些依赖看起来属于另一个模块,却仍可能不可或缺。典型情况是:共享库中的 jvmMain { implementation(compose.desktop.currentOs) } 看起来像是桌面应用依赖,但它提供了 Skiko 原生运行时(skiko-awt-runtime-<os>,包含 libskiko-<os>.dylib 和 .sha256),供模块自身的 JVM Compose UI 测试(compose.uiTest / runComposeUiTest)在运行时加载。 compose.ui:ui-test 只会引入 Skiko 的类,不会引入原生库。删掉该依赖后,编译仍会成功,但运行时会报错:

    org.jetbrains.skiko.LibraryLoadException: Cannot find libskiko-macos-arm64.dylib.sha256, proper native dependency missing.
    

    这不是 Toolchain 的问题:不添加该依赖时,Gradle 也会以相同方式失败。将其恢复到 dependencies@jvm,或者将其限定在 test-dependencies@jvm,避免 Skiko 原生库进入使用方的类路径。

防止遗漏:将每个 Gradle 源集的依赖列表与其 Kotlin Toolchain 配置部分进行对比(检查名称和数量),然后运行 ./kotlin show dependencies -m <module>,并与 Gradle 构建进行比较。

常见问题

  • 没有 package 插件。 CI 最终会上传 build/artifacts/CompiledJvmArtifact/(内部类文件目录),而不是 JAR。更改上传路径前,先添加该插件。
  • 提交 build/。 尽早将其添加到 .gitignore;删除旧的 .gradle/ 条目。
  • Maven 坐标中的字符串插值。 plugin.yaml 中的 com.example:foo:${pluginSettings.version} 会报错:“Value of type 'ShadowDependency' doesn't support string interpolation”。请使用 $libs.foo。

Kotlin Toolchain 文档:https://kotlin-toolchain.org/dev/

评论

支持 Markdown