mcpField
G
gradle-to-kotlin-toolchain-project

gradle-to-kotlin-toolchain-project

github.com/singleton11开发541 次安装10 周前更新
代码仓库

关于 gradle-to-kotlin-toolchain-project

这是一份将 Gradle 项目迁移到 Kotlin Toolchain 的指南,强调保留应用源代码。它提供构建盘点、插件替换策略、实施流程和 CI 验证步骤,也涵盖没有原生替代方案的 Gradle 插件。

主要功能

  1. 01盘点 Gradle 构建配置
  2. 02将插件映射为原生或本地替代项
  3. 03把约定和依赖包转换为模板
  4. 04指导复用或编写本地插件
  5. 05重写并验证 CI 工作流

适用场景

  1. 01规划 JVM 项目的 Gradle 迁移
  2. 02替换没有原生支持的 Gradle 插件
  3. 03迁移构建约定和依赖包
  4. 04更新 CI 命令与制品路径

安装

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

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

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

附带文件references/examples.md

详细介绍

Gradle → Kotlin Toolchain 项目迁移

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

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

原则

  1. 保留源代码。 如果迁移迫使你修改业务逻辑,很可能可以通过本地插件来弥补。通过 classpath 读取 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-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 可能会拒绝它)。模式见 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。
  • 如果必须解析 stdout,请匹配任务坐标:awk '/<task>@<plugin>/ { v = $NF } END { print v }'。
测试资源会覆盖 generated.resources

当插件生成 release.properties,且存在 src/test/resources/release.properties 时,类路径顺序会让测试固件优先。通常这样也能正常工作,但这种做法很脆弱——如果测试断言特定值,请注入存根服务,而不要依赖优先顺序。

Dependabot 需要存根 build.gradle.kts

Dependabot 的 Gradle 文件获取器要求在配置的目录中存在 build.gradle(.kts),之后才会扫描 gradle/libs.versions.toml;如果没有,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-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