mcpField
K
kotlin-tooling-kotlin-toolchain-plugin-authoring

kotlin-tooling-kotlin-toolchain-plugin-authoring

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

关于 kotlin-tooling-kotlin-toolchain-plugin-authoring

面向 Kotlin Toolchain 插件作者的技能指南,说明如何编写本地插件、任务、配置和生成内容,并通过消费模块验证。

主要功能

  1. 01本地插件项目布局
  2. 02配置接口与任务动作
  3. 03任务输入输出及执行避免
  4. 04生成源码与资源注册
  5. 05命令定义和任务通信

适用场景

  1. 01为构建流程添加代码生成步骤
  2. 02加入自定义构建验证
  3. 03将构建期值发布到 JAR
  4. 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,也没有扩展属性映射。任务通过匹配路径进行通信:

  1. 生产者接收 @Output outputDir: Path,并向其中写入文件。
  2. plugin.yaml 将消费者任务的 @Input 指向 ${tasks.<producer>.action.outputDir}/<file>。
  3. Toolchain 会根据路径匹配推断依赖关系——无需使用 dependsOn API。

同一个 @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。

针对消费者进行验证

  1. 添加一个启用该插件且设置贴近实际的小型消费者模块(demo-app/、sample/)。
  2. 在其 main.kt 或测试中读取插件发布的内容。
  3. 在插件的 README 中列出准确的命令和预期输出,方便用户克隆项目后粘贴并比对。

插件文档:https://kotlin-toolchain.org/dev/user-guide/plugins/

评论

支持 Markdown