关于 kotlin-tooling-kotlin-toolchain-plugin-authoring
面向 Kotlin Toolchain 插件作者的技能指南,说明如何编写本地插件、任务、配置和生成内容,并通过消费模块验证。
主要功能
- 01本地插件项目布局
- 02配置接口与任务动作
- 03任务输入输出及执行避免
- 04生成源码与资源注册
- 05命令定义和任务通信
适用场景
- 01为构建流程添加代码生成步骤
- 02加入自定义构建验证
- 03将构建期值发布到 JAR
- 04定义可重复调用的 CLI 命令
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ kotlin/ kotlin-agent-skills --skill kotlin-tooling-kotlin-toolchain-plugin-authoring安装后在 Claude Code 中输入/kotlin-tooling-kotlin-toolchain-plugin-authoring确认已加载。
附带文件references/examples.md
详细介绍
Kotlin Toolchain 插件编写
本地插件是声明式 YAML 的官方扩展方式:在 jvm/amper-plugin 模块中提供任务操作、设置,以及与项目一同生成的源代码/资源。可参考 references/examples.md 中的代码模式进行调整。
何时编写插件
在以下情况下编写插件:
- 库的工作流所需的构建时步骤(代码生成、架构编译、资源转换、版本标记)。
- 接入构建流程的自定义验证(发布前检查、架构验证、契约测试)。
- 发布到 JAR 类路径或下游任务的构建时值——最接近 Gradle 的
project.version。 - 用于重复工作流的命名 CLI 命令(
./kotlin do release)。
如果 module.yaml 已涵盖相关功能(依赖项、JDK 配置、源代码布局、基本打包),就不要编写插件;也绝不要为了复用 Gradle 插件而编写——Kotlin Toolchain 无法使用它们。
目录结构
repo-root/
├── kotlin, kotlin.bat # wrappers (from `kotlin init`)
├── project.yaml # registers the plugin
├── plugins/<name>/
│ ├── module.yaml # product: jvm/amper-plugin
│ ├── plugin.yaml # tasks: + commands: + generated:
│ └── src/
│ ├── Settings.kt # @Configurable interface
│ ├── tasks/ # one @TaskAction per file
│ │ ├── Foo.kt
│ │ └── FooSteps.kt # internal shared helpers (not @TaskAction)
│ └── <domain logic>/
└── <consumer-module>/
└── module.yaml # plugins: { <name>: enabled: true, ... }
在仓库中至少保留一个使用者模块——这是端到端测试插件的唯一方式,而且目前还无法将插件发布到任何公共注册表。
project.yaml
modules:
- consumer-app
- plugins/<name>
plugins:
- ./plugins/<name>
如果没有顶层的 plugins: 块,任何使用者都无法解析该插件 ID。
module.yaml — 插件模块
product: jvm/amper-plugin # marks the module as a plugin
dependencies:
- <coordinate>:<version>
- <coordinate>:<version>: runtime-only # required only at runtime
- <coordinate>:<version>: compile-only
pluginInfo:
id: <plugin-id> # what consumers write under `plugins:`
settingsClass: <fully.qualified.Settings> # the @Configurable interface
settings:
jvm:
jdk:
version: 21
kotlin:
languageVersion: 2.1
@Configurable interface Settings
默认值写在接口属性的 getter 中;嵌套块则写成嵌套的 @Configurable 接口。
@Configurable
interface Settings {
val someValue: String get() = "default"
val checks: ChecksSettings
}
@Configurable
interface ChecksSettings {
val strict: Boolean get() = true
}
使用者在 module.yaml 中覆盖所需的值;未指定的值则回退到 getter 的默认值:
plugins:
<plugin-id>:
enabled: true
someValue: "override"
checks:
strict: false
@TaskAction
任务操作是顶层 fun,在执行匹配的 plugin.yaml 条目时调用。
@TaskAction
fun foo(
@Input moduleRootDir: Path,
@Output outputDir: Path,
settings: Settings,
) {
// body
}
@Input path: Path— 声明输入;Kotlin Toolchain 会对其内容创建快照,以避免重复执行。@Output path: Path— 声明输出目录;Kotlin Toolchain 会创建该目录并将路径传入。请写入收到的确切Path,否则下游引用将无法找到结果。settings: Settings(或任何@Configurable)— 类型化配置,在plugin.yaml中接线。- 普通的
Path/ 基本类型 — 按原样从plugin.yaml传入。 println(...)是输出通道;Kotlin Toolchain 会捕获标准输出。
执行避免
当声明的输入未发生变化时,@TaskAction 会跳过执行。实际输入为 Git 历史、网络或环境变量的任务无法创建指纹,因此应选择退出:
@TaskAction(executionAvoidance = ExecutionAvoidance.Disabled)
fun foo(@Output outputDir: Path, settings: Settings) { /* ... */ }
没有 @Output 的任务永远不会被缓存,每次都会重新运行——这适用于纯副作用任务(发布、部署、推送)。
plugin.yaml
tasks:
foo:
action: !<fully.qualified.foo>
moduleRootDir: ${module.rootDir}
outputDir: ${taskOutputDir}
settings: ${pluginSettings}
bar:
action: !<fully.qualified.bar>
input: ${tasks.foo.action.outputDir}/result.txt
settings: ${pluginSettings}
generated:
resources:
- directory: ${tasks.foo.action.outputDir}
commands:
- foo
| 引用 | 解析为 |
|---|---|
${module.rootDir} | 包含使用者 module.yaml 的目录。作为 @Input 传入,以检查使用者的目录树。 |
${taskOutputDir} | Toolchain 管理的每任务输出目录。作为 @Output 传入。 |
${pluginSettings} | 根据使用者的 module.yaml 构建的 @Configurable 对象。 |
${tasks.<task>.action.<param>} | 另一个任务的参数——用于 generated.*,并将一个任务的 @Input 接到另一个任务的 @Output。 |
generated.resources / generated.sources
两者都会将某个目录(通常是任务的 @Output)注册为使用者构建的组成部分,并自动接线,使生成任务先运行:
generated.resources— 添加到 JAR 类路径,可通过getResourceAsStream("/path/in/jar")访问。generated.sources— 添加为 Kotlin 源代码根目录,并与使用者的src/一同编译。
任务与命令
任务是实现,调用方式为 ./kotlin task :<module>:<task>@<plugin-id>——文档建议不要依赖这个经过改写的名称。命令是公共 API:./kotlin do <command-name>,可通过 ./kotlin show commands 查看(使用 -m <module> 指定范围)。
- 如果某个任务的
@Output会提供给generated.resources/generated.sources,那么它就是构建图的组成部分。不要将它放进commands:;它会自动运行,将其公开会诱使用户手动运行。 - 用户需要直接调用的任务必须列在
commands:中。
基于文件的任务间通信
没有共享的可变构建状态——没有 project.version,也没有扩展属性映射。任务通过匹配路径进行通信:
- 生产者接收
@Output outputDir: Path,并向其中写入文件。 plugin.yaml将消费者任务的@Input指向${tasks.<producer>.action.outputDir}/<file>。- Toolchain 会根据路径匹配推断依赖关系——无需使用
dependsOnAPI。
同一个 @Output 目录可以同时供构建时消费者(通过 @Input)和运行时消费者(在 generated.resources 下注册,通过 getResourceAsStream 读取)使用。
通过环境变量覆盖运行时设置
没有 -Pkey=value。如需临时覆盖设置,请在操作中读取环境变量:
val forced = System.getenv("MYPLUGIN_FORCE_VALUE")?.takeIf { it.isNotBlank() }
val skipChecks = System.getenv("MYPLUGIN_SKIP_CHECKS")?.equals("true", ignoreCase = true) == true
将环境变量映射作为构造函数参数传入,而不是在调用栈深处调用 System.getenv(),这样逻辑仍可进行单元测试。在插件的 README 中记录每个可识别的变量。环境变量是临时覆盖方式,不是信任边界——在将变量值用于文件路径或进程参数前,务必进行验证。
在任务操作之间共享逻辑
任务通常会共享步骤(验证 → 创建 → 推送)。不要通过一串构建图任务来组合成一个面向用户的原子任务:分开调用会重新打开共享资源,并造成其他进程观察到中间状态的时机窗口。
设计时需要考虑的限制
- 插件仅限本地使用;目前还不能发布到公共注册表。
- 插件作用于模块级别;不存在项目级插件。每个使用插件的模块都必须在
plugins:下列出它,跨模块的影响通过文件传递。 module.yaml中不支持${...}插值(截至 0.11)——消费者设置必须是字面值。- 不支持
Project.afterEvaluate,也没有惰性的Provider/Property图。请在操作主体中计算派生值。 -h/--help不会列出插件命令;请使用./kotlin show commands。
针对消费者进行验证
- 添加一个启用该插件且设置贴近实际的小型消费者模块(
demo-app/、sample/)。 - 在其
main.kt或测试中读取插件发布的内容。 - 在插件的 README 中列出准确的命令和预期输出,方便用户克隆项目后粘贴并比对。