kotlin-tooling-kotlin-toolchain-plugin-authoring
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseKotlin Toolchain Plugin Authoring
Kotlin Toolchain 插件开发
Local plugins are the official escape hatch from declarative YAML: a module shipping
task actions, settings, and generated sources/resources alongside your project. Code patterns to adapt are
in references/examples.md.
jvm/amper-plugin本地插件是声明式YAML的官方逃逸通道:它是一个模块,随项目一起提供任务动作、配置项以及生成的源码/资源。可复用的代码模式见references/examples.md。
jvm/amper-pluginWhen to write a plugin
何时编写插件
Write one when you need:
- A build-time step a library's workflow expects (code generation, schema compilation, resource transformation, version stamping).
- Custom verification wired into the build (pre-release checks, schema validation, contract tests).
- A build-time value published into the JAR classpath or downstream tasks — the closest analog to Gradle's
.
project.version - A named CLI command for a repeated workflow ().
./kotlin do release
Don't write one when already covers it (dependencies, JDK provisioning, source layouts,
basic packaging), and never to reuse a Gradle plugin — the Kotlin Toolchain cannot consume them.
module.yaml当你需要以下能力时可以编写插件:
- 库工作流所需的构建时步骤(代码生成、schema编译、资源转换、版本打标)。
- 嵌入构建流程的自定义校验(发布前检查、schema验证、契约测试)。
- 发布到JAR类路径或下游任务的构建时值——最接近Gradle的的能力。
project.version - 用于重复工作流的命名CLI命令()。
./kotlin do release
如果已经覆盖了相关需求(依赖管理、JDK预置、源码布局、基础打包),则不需要编写插件;另外绝对不要为了复用Gradle插件而编写本插件——Kotlin Toolchain无法使用Gradle插件。
module.yamlLayout
目录结构
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, ... }Keep at least one consumer module in the repo — it is the only way to exercise the plugin end-to-end, and
plugins cannot be published to any public registry yet.
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
project.yamlproject.yaml
project.yamlyaml
modules:
- consumer-app
- plugins/<name>
plugins:
- ./plugins/<name>Without the top-level block the plugin id is unresolvable from any consumer.
plugins:yaml
modules:
- consumer-app
- plugins/<name>
plugins:
- ./plugins/<name>如果没有顶层的块,任何消费模块都无法解析该插件的ID。
plugins:module.yaml
— the plugin module
module.yamlmodule.yaml
—— 插件模块
module.yamlyaml
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.1yaml
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
@Configurable interface Settings@Configurable
接口 Settings
@ConfigurableDefaults go in interface property getters; nested blocks become nested interfaces.
@Configurablekotlin
@Configurable
interface Settings {
val someValue: String get() = "default"
val checks: ChecksSettings
}
@Configurable
interface ChecksSettings {
val strict: Boolean get() = true
}Consumers override what they need in ; omitted values fall back to the getter default:
module.yamlyaml
plugins:
<plugin-id>:
enabled: true
someValue: "override"
checks:
strict: false默认值定义在接口属性的getter中;嵌套的配置块对应嵌套的接口。
@Configurablekotlin
@Configurable
interface Settings {
val someValue: String get() = "default"
val checks: ChecksSettings
}
@Configurable
interface ChecksSettings {
val strict: Boolean get() = true
}消费模块在中覆盖需要的配置;未配置的项会回退到getter中定义的默认值:
module.yamlyaml
plugins:
<plugin-id>:
enabled: true
someValue: "override"
checks:
strict: false@TaskAction
@TaskAction@TaskAction
任务动作
@TaskActionTask actions are top-level s, called when the matching entry executes.
funplugin.yamlkotlin
@TaskAction
fun foo(
@Input moduleRootDir: Path,
@Output outputDir: Path,
settings: Settings,
) {
// body
}- — declared input; Kotlin Toolchain snapshots its contents for execution avoidance.
@Input path: Path - — declared output directory; Kotlin Toolchain creates it and passes the path in. Write to the exact
@Output path: Pathyou received, or downstream references won't find the result.Path - (or any
settings: Settings) — typed configuration, wired in@Configurable.plugin.yaml - Plain / primitives — passed literally from
Path.plugin.yaml - is the output channel; Kotlin Toolchain captures stdout.
println(...)
任务动作是顶层函数,当中对应的条目执行时会调用这些函数。
funplugin.yamlkotlin
@TaskAction
fun foo(
@Input moduleRootDir: Path,
@Output outputDir: Path,
settings: Settings,
) {
// body
}- —— 声明输入;Kotlin Toolchain会对其内容生成快照,用于执行规避。
@Input path: Path - —— 声明的输出目录;Kotlin Toolchain会创建该目录并传入路径。请严格写入你接收到的
@Output path: Path路径,否则下游引用将找不到结果。Path - (或任何
settings: Settings类型)—— 类型化配置,在@Configurable中进行绑定。plugin.yaml - 普通/基本类型 —— 直接从
Path中按字面量传入。plugin.yaml - 是输出通道;Kotlin Toolchain会捕获标准输出。
println(...)
Execution avoidance
执行规避
A is skipped when its declared inputs are unchanged. Tasks whose real inputs are Git history,
the network, or environment variables cannot be fingerprinted, so opt out:
@TaskActionkotlin
@TaskAction(executionAvoidance = ExecutionAvoidance.Disabled)
fun foo(@Output outputDir: Path, settings: Settings) { /* ... */ }Tasks with no are never cached and always re-run — correct for purely side-effecting tasks
(releases, deployments, pushes).
@Output当声明的输入未发生变化时,任务会被跳过。如果任务的实际输入是Git历史、网络请求或环境变量,就无法生成指纹,因此需要禁用执行规避:
@TaskActionkotlin
@TaskAction(executionAvoidance = ExecutionAvoidance.Disabled)
fun foo(@Output outputDir: Path, settings: Settings) { /* ... */ }没有的任务永远不会被缓存,且总是重新执行——这适用于纯副作用类任务(发布、部署、推送)。
@Outputplugin.yaml
plugin.yamlplugin.yaml
plugin.yamlyaml
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| Reference | Resolves to |
|---|---|
| Directory containing the consumer's |
| Toolchain-managed per-task output directory. Pass as |
| The |
| Another task's parameter — used in |
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| 引用 | 解析为 |
|---|---|
| 消费模块 |
| 由Toolchain管理的每个任务的输出目录。作为 |
| 从消费模块 |
| 另一个任务的参数——用于 |
generated.resources
/ generated.sources
generated.resourcesgenerated.sourcesgenerated.resources
/ generated.sources
generated.resourcesgenerated.sourcesBoth register a directory (usually a task's ) as a contribution to the consumer's build, and both
auto-wire the producing task to run first:
@Output- — added to the JAR classpath, reachable via
generated.resources.getResourceAsStream("/path/in/jar") - — added as a Kotlin source root and compiled with the consumer's
generated.sources.src/
这两个配置项都会将一个目录(通常是任务的)注册为消费模块构建的贡献项,并且都会自动绑定生成任务优先执行:
@Output- —— 添加到JAR类路径中,可通过
generated.resources访问。getResourceAsStream("/path/in/jar") - —— 添加为Kotlin源码根目录,随消费模块的
generated.sources一起编译。src/
Tasks vs commands
任务与命令
Tasks are the implementation, addressed as — the docs advise
against relying on that mangled name. Commands are the public API: , listed via
( to scope).
./kotlin task :<module>:<task>@<plugin-id>./kotlin do <command-name>./kotlin show commands-m <module>- A task whose feeds
@Output/generated.resourcesis a build-graph contributor. Keep it out ofgenerated.sources; it runs automatically and exposing it invites users to run it by hand.commands: - A task users invoke directly must be in .
commands:
任务是实现层,通过调用——官方文档建议不要依赖这种拼接出来的名称。命令是公开API:,可通过列出(加可限定范围)。
./kotlin task :<module>:<task>@<plugin-id>./kotlin do <command-name>./kotlin show commands-m <module>- 如果任务的用于提供
@Output/generated.resources,那它是构建图的贡献项。不要把它加入generated.sources;它会自动运行,暴露出来反而可能导致用户手动执行它。commands: - 用户需要直接调用的任务必须加入。
commands:
File-based task communication
基于文件的任务通信
There is no shared mutable build state — no , no extension property maps. Tasks talk
through matched paths:
project.version- The producer takes and writes files into it.
@Output outputDir: Path - points a consumer task's
plugin.yamlat@Input.${tasks.<producer>.action.outputDir}/<file> - The Toolchain infers the dependency from the path match — no API needed.
dependsOn
The same directory can serve build-time consumers (via ) and runtime consumers
(registered under , read via ) simultaneously.
@Output@Inputgenerated.resourcesgetResourceAsStream不存在共享的可变构建状态——没有,也没有扩展属性映射。任务通过匹配的路径进行通信:
project.version- 生产任务接收并将文件写入该目录。
@Output outputDir: Path - 将消费任务的
plugin.yaml指向@Input。${tasks.<producer>.action.outputDir}/<file> - Toolchain会根据路径匹配推断依赖关系——不需要API。
dependsOn
同一个目录可以同时服务于构建时消费者(通过)和运行时消费者(通过注册,通过读取)。
@Output@Inputgenerated.resourcesgetResourceAsStreamRuntime overrides via environment variables
通过环境变量进行运行时覆盖
There is no . Read env vars inside the action for ephemeral overrides:
-Pkey=valuekotlin
val forced = System.getenv("MYPLUGIN_FORCE_VALUE")?.takeIf { it.isNotBlank() }
val skipChecks = System.getenv("MYPLUGIN_SKIP_CHECKS")?.equals("true", ignoreCase = true) == truePass the env map in as a constructor parameter rather than calling deep in the call stack,
so logic stays unit-testable. Document every recognised variable in the plugin's README. Env vars are
ephemeral overrides, not a trust boundary — validate a value before using it in a file path or process
argument.
System.getenv()没有这类参数。在任务动作内部读取环境变量来实现临时覆盖:
-Pkey=valuekotlin
val forced = System.getenv("MYPLUGIN_FORCE_VALUE")?.takeIf { it.isNotBlank() }
val skipChecks = System.getenv("MYPLUGIN_SKIP_CHECKS")?.equals("true", ignoreCase = true) == true请将环境变量映射作为构造函数参数传入,而不是在调用栈深处直接调用,这样逻辑才能保持单元可测试性。请在插件的README中记录所有支持的环境变量。环境变量是临时覆盖手段,而非信任边界——在文件路径或进程参数中使用某个值之前,请先对其进行校验。
System.getenv()Sharing logic across task actions
在任务动作间共享逻辑
Tasks often share steps (verify → create → push). Don't compose an atomic user-facing task from a chain of
build-graph tasks: separate invocations re-open shared resources and open a window where another process
observes intermediate state.
任务之间通常会共享步骤(验证→创建→推送)。不要用一系列构建图任务来组合成一个面向用户的原子任务:多次独立调用会重新打开共享资源,并且会出现时间窗口,导致其他进程可能观察到中间状态。
Limitations to design around
需要规避的设计限制
- Plugins are local-only; no public registry publishing yet.
- Plugins are module-level; there is no project-wide plugin. Every consumer module lists it under
, and cross-module effects flow through files.
plugins: - No interpolation in
${...}(as of 0.11) — consumer settings are literal values.module.yaml - No , no lazy
Project.afterEvaluate/Providergraph. Compute derived values in the action body.Property - /
-hdoes not list plugin commands; use--help../kotlin show commands
- 插件仅支持本地使用;目前还不能发布到公共仓库。
- 插件是模块级别的;不存在项目级插件。每个消费模块都需要在下声明该插件,跨模块影响通过文件传递。
plugins: - 中不支持
module.yaml插值(0.11版本为止)——消费端配置都是字面量值。${...} - 没有,没有惰性
Project.afterEvaluate/Provider图。派生值需要在任务动作体内计算。Property - /
-h不会列出插件命令;请使用--help。./kotlin show commands
Validate against a consumer
用消费模块验证
- Add a small consumer module (,
demo-app/) enabling the plugin with realistic settings.sample/ - Have its or a test read whatever the plugin publishes.
main.kt - Put the exact commands and expected output in the plugin's README, so a fresh clone can paste and compare.
Plugin docs: https://kotlin-toolchain.org/dev/user-guide/plugins/
- 添加一个小型消费模块(、
demo-app/),用贴近实际的配置启用插件。sample/ - 让它的或测试代码读取插件发布的所有内容。
main.kt - 将确切的命令和预期输出写入插件的README,这样新克隆的仓库可以直接粘贴命令并对比结果。