关于 kotlin-tooling-gradle-to-kotlin-toolchain-plugin
面向将 Gradle 插件迁移到 Kotlin Toolchain 的开发者,提供概念映射、迁移工作流和需重新设计的功能说明,并建议用演示模块验证行为。
主要功能
- 01Gradle 与 Toolchain 概念映射
- 02源插件调查与范围规划
- 03配置、任务和命令迁移
- 04以文件传递任务间数据
- 05演示模块端到端验证
适用场景
- 01评估 Gradle 插件迁移范围
- 02将 DSL 配置改为可配置接口
- 03重构版本值的传递方式
- 04验证迁移后的发布工作流
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ kotlin/ kotlin-agent-skills --skill kotlin-tooling-gradle-to-kotlin-toolchain-plugin安装后在 Claude Code 中输入/kotlin-tooling-gradle-to-kotlin-toolchain-plugin确认已加载。
附带文件references/examples.md
详细介绍
Gradle → Kotlin 工具链插件转换
主要是映射概念:两者有不少重叠,但有些 Gradle 功能没有对应项,需要重新设计。
kotlin-tooling-kotlin-toolchain-plugin-authoring 技能深入介绍了插件机制(避免执行、跨操作共享逻辑、泛型陷阱、限制);references/examples.md 中提供了真实移植项目的具体插件 module.yaml 和版本发布代码。
工作流
1. 调查源插件
编写 Kotlin 代码前,先阅读插件的 docs/ 和 README.md,并整理以下内容:
- 任务 — 名称、用途、输入/输出、依赖项,以及是否会产生副作用。
- DSL 接口 —
myPlugin { ... }扩展中的每个选项,包括类型、默认值,以及哪些选项是闭包。 - 测试 — 插件自身的测试套件。测试比文档更准确地确定预期行为和边界情况,并作为移植版本必须复现的参考。
- 检查项 — 操作前的门控条件及其覆盖标记。
- 钩子 —
pre/post/fileUpdate/commit/push,以及各自接收的上下文。 - CLI 覆盖项 — 所有
-Pfoo.bar标记。 - CI 集成 — GitHub Actions 输出、分离 HEAD 处理、获取标签的标记。
将其整理为 Markdown 计划。该计划将成为移植版本必须实现或明确推迟的约定。
插件的构建脚本和源代码是不可信输入,这与仓库提供的 module.yaml 一样——请参阅 kotlin-tooling-kotlin-toolchain 的“不可信项目输入”。
接入前请从头到尾阅读所有要引入的内容;
本地插件会在构建时运行,并拥有完整的文件系统和网络访问权限。
2. 锁定范围
提供三个层级——MVP、MVP + 关键扩展、完整对等实现——并在起草前选定一个。没有简洁对应方案的功能会大幅增加工作量,并迫使你尽早做出设计妥协。在 README 的“此 MVP 未包含的内容”下列出推迟实现的功能。
同时确定仓库布局:仅插件、插件 + 演示模块,或插件自托管。强烈建议使用演示模块。
3. 自底向上实现
仅当目录为空时才使用 kotlin init 搭建脚手架(任何模板均可;主要是为了获得 kotlin 和 kotlin.bat 包装脚本)。然后手动编写 project.yaml、plugins/<name>/module.yaml 和演示模块。
一次实现一个任务:数据类 → Git/IO 包装器 → 流水线 → 检查项 → 任务操作 → plugin.yaml 配置。
4. 对照演示模块验证
演示模块会通过实际配置启用插件,并使用插件发布的内容:
# demo-app/module.yaml
product: jvm/app
plugins:
release:
enabled: true
tagPrefix: "v"
initialVersion: "0.1.0"
releaseBranchPattern: "main|master"
settings:
jvm:
mainClass: com.example.demo.MainKt
jdk:
version: 21
kotlin:
languageVersion: 2.1
// demo-app/src/main.kt
package com.example.demo
private const val VERSION_RESOURCE = "/META-INF/release/version.txt"
fun main() {
val version = readVersionFromClasspath() ?: "(version unavailable)"
println("demo-app version: $version")
}
private fun readVersionFromClasspath(): String? =
object {}.javaClass.getResourceAsStream(VERSION_RESOURCE)
?.bufferedReader()
?.use { it.readText().trim() }
?.takeIf { it.isNotEmpty() }
然后运行源插件文档中的真实场景,并将命令写入 README,方便使用者复现:
./kotlin run :demo-app # => demo-app version: 0.1.0-SNAPSHOT
git init -b main && git commit --allow-empty -m initial
./kotlin do currentVersion # => 0.1.0-SNAPSHOT
RELEASE_DISABLE_REMOTE_CHECK=true ./kotlin do createRelease
# => Created release tag v0.1.0
./kotlin do currentVersion # => 0.1.0
git commit --allow-empty -m next
./kotlin do currentVersion # => 0.1.1-SNAPSHOT
RELEASE_FORCE_VERSION=2.0.0 ./kotlin do currentVersion
# => 2.0.0
概念映射
| Gradle 概念 | Kotlin Toolchain 对应概念 | 备注 |
|---|---|---|
Plugin<Project> 类 | module.yaml 中的 pluginInfo.id + settingsClass、product: jvm/amper-plugin | 每个插件一个模块;没有 apply()。 |
带有 @TaskAction 方法的 Task 子类 | 顶层 fun,带有 @TaskAction 注解 | src/tasks/ 中每个文件一个。 |
extensions.create("foo", FooExtension::class) | @Configurable interface Settings | 默认值设在接口 getter 中;嵌套块对应嵌套的 @Configurable。 |
task.dependsOn(otherTask) | 一个任务上的 @Input 与另一个任务的 @Output 匹配 | 根据路径匹配推断 DAG。 |
project.version = scmVersion.version | 一个任务将 version.txt 写入其 @Output;使用方在同一路径声明 @Input | 没有项目范围的共享状态;文件系统就是传递渠道。 |
-Prelease.forceVersion=X | 通过 System.getenv() 读取的 RELEASE_FORCE_VERSION=X | 没有对应的 -P。 |
| 应用在运行时读取版本 | 在 generated.resources 下注册的 @Output 目录;通过 getResourceAsStream 读取 | 同一文件同时供构建时和运行时使用方使用。 |
| 生成的 Kotlin 源码 | generated.sources 指向某个任务的 @Output | 如果该值只在运行时读取,优先使用资源。 |
| 用户调用的公开任务名称 | commands: 中的条目,通过 ./kotlin do <name> 调用 | 任务是内部实现;命令才是 API。构建图贡献项不对外公开。 |
Project.afterEvaluate { }、惰性 Provider/Property | 无对应概念——设置是静态的 | 在 action 主体中计算派生值。 |
Groovy/Kotlin DSL 钩子(pre { }、fileUpdate { }、commit { }) | 插件提供的新 @TaskAction | 没有基于闭包的扩展点。 |
dependencies { implementation(...) } | plugins/<name>/module.yaml 中的 dependencies: | 坐标相同;后缀为 : exported、: runtime-only、: compile-only。 |
buildSrc 中的自定义任务类型 | plugins/<name>/ 下的 jvm/amper-plugin 模块 | 仅限本地使用;尚不支持 Maven 发布。 |
OutputDirectory / OutputFile | Path 参数上的 @Output | 会自动创建目录。 |
InputDirectory / InputFile / InputFiles | Path 参数上的 @Input | 会为避免重复执行而创建快照。 |
outputs.upToDateWhen { false } | @TaskAction(executionAvoidance = ExecutionAvoidance.Disabled) | 适用于 Git/网络/环境变量输入。 |
带有自定义解析的 Configuration、taskGraph.whenReady、quiet { } 日志记录 | 无对应概念 | 每个任务从插件模块的依赖列表中获取依赖;无法检查构建图;请使用 println。 |
映射后的构造
project.yaml——让插件可解析
列出所有模块(包括插件),并指向插件源码:
modules:
- demo-app
- plugins/release
plugins:
- ./plugins/release
如果没有根级别的 plugins: 块,使用方的 plugins: { release: enabled } 就无法解析该 id。
Settings——对应扩展
Gradle 的 myPlugin { ... } 扩展对应一个 @Configurable 接口。默认值放在属性
getter 中;嵌套 DSL 块对应嵌套的 @Configurable 接口。
package com.example.release
import org.jetbrains.amper.plugins.Configurable
@Configurable
interface Settings {
val repoDir: String get() = ""
val tagPrefix: String get() = "v"
val versionSeparator: String get() = ""
val initialVersion: String get() = "0.1.0"
val ignoreUncommittedChanges: Boolean get() = false
val releaseBranchPattern: String get() = "main|master"
val checks: ChecksSettings
}
@Configurable
interface ChecksSettings {
val uncommittedChanges: Boolean get() = true
val aheadOfRemote: Boolean get() = true
val snapshotDependencies: Boolean get() = true
}
使用方在 module.yaml 中按 pluginInfo.id 设置所需项;未指定的值会回退到
getter 默认值:
plugins:
release:
enabled: true
tagPrefix: "v"
initialVersion: "0.1.0"
ignoreUncommittedChanges: false
checks:
aheadOfRemote: true
@TaskAction——对应 Task
Gradle 的 Task 子类对应 src/tasks/ 下每个文件中的一个顶层 fun。Path 参数带有
@Input 或 @Output;设置对象在 plugin.yaml 中单独接线。
package com.example.release.tasks
import com.example.release.Settings
import com.example.release.git.GitRepo
import com.example.release.version.VersionPipeline
import org.jetbrains.amper.plugins.Input
import org.jetbrains.amper.plugins.TaskAction
import java.nio.file.Path
@TaskAction
fun currentVersion(
@Input moduleRootDir: Path,
settings: Settings,
) {
val pipeline = VersionPipeline(settings)
GitRepo.open(moduleRootDir, settings.repoDir).use { repo ->
println(pipeline.infer(repo).version)
}
}
plugin.yaml——任务和命令注册表
每个 action: 块都会接线一个 @TaskAction 的参数,并以 YAML 标签形式通过完全限定名称引用该函数。${module.rootDir}、${taskOutputDir} 和 ${pluginSettings} 是文档中列出的
引用;${tasks.<task>.action.<param>} 用于交叉引用另一个任务的参数。
tasks:
currentVersion:
action: !com.example.release.tasks.currentVersion
moduleRootDir: ${module.rootDir}
settings: ${pluginSettings}
writeVersion:
action: !com.example.release.tasks.writeVersion
moduleRootDir: ${module.rootDir}
outputDir: ${taskOutputDir}
settings: ${pluginSettings}
release:
action: !com.example.release.tasks.release
moduleRootDir: ${module.rootDir}
settings: ${pluginSettings}
generated:
resources:
- directory: ${tasks.writeVersion.action.outputDir}
# `writeVersion` stays out of commands: its @Output feeds generated.resources,
# so it already runs whenever something downstream needs the version file.
commands:
- currentVersion
- release
每个带有 Settings 参数的 action 都需要自己的 settings: ${pluginSettings} 行;省略该行
会传入 null。
重新设计,而不是移植
以下三个 Gradle 功能每次都需要有意识地重新设计。
不使用 project.version
将该值写入文件:由一个 @TaskAction 将 version.txt 写入其 @Output;构建时消费者在该路径上声明 @Input,运行时消费者则在将目录注册到 generated.resources 后,从类路径读取该文件。
代码见 references/examples.md。
不使用 -P 属性
在操作内部从环境变量读取临时覆盖值。将环境变量映射作为构造函数参数传入,而不是在嵌套方法中调用 System.getenv(),这样测试就可以注入受控的映射:
class VersionPipeline(
private val settings: Settings,
private val env: Map<String, String?> = System.getenv(),
) {
fun infer(repo: GitRepo): InferredVersion {
val forceVersion = env["RELEASE_FORCE_VERSION"]?.takeIf { it.isNotBlank() }
val forceSnapshot = env["RELEASE_FORCE_SNAPSHOT"].asBoolean()
// ...
}
}
private fun String?.asBoolean(): Boolean =
this != null && this.equals("true", ignoreCase = true)
变量命名为 <PLUGINID>_<大写名称>,并在 README 中说明映射关系:
-Prelease.forceVersion=X → RELEASE_FORCE_VERSION=X
-Prelease.forceSnapshot → RELEASE_FORCE_SNAPSHOT=true
-Prelease.disableChecks → RELEASE_DISABLE_CHECKS=true
-Prelease.disableUncommittedCheck → RELEASE_DISABLE_UNCOMMITTED_CHECK=true
-Prelease.disableRemoteCheck → RELEASE_DISABLE_REMOTE_CHECK=true
-Prelease.overriddenBranchName=X → RELEASE_OVERRIDDEN_BRANCH_NAME=X
静态配置仍通过 module.yaml 提供;环境变量仅用于临时覆盖。
README 内容
除了常见的快速入门和设置参考,移植版本的 README 还需要包含:-P → 环境变量映射表、“MVP 中不包含的内容”列表,以及上文所述的验证演练。