Création de plugins pour la Kotlin Toolchain
Les plugins locaux constituent la sortie officielle de la déclaration YAML : un module jvm/amper-plugin livrant des actions de tâche, des paramètres et des sources/ressources générées aux côtés de votre projet. Les modèles de code à adapter se trouvent dans references/examples.md.
Quand écrire un plugin
Écrivez-en un si vous avez besoin :
- D'une étape de compilation qu'un workflow de bibliothèque attend (génération de code, compilation de schéma, transformation de ressources, horodatage de version).
- D'une vérification personnalisée intégrée à la compilation (vérifications de pré-publication, validation de schéma, tests de contrat).
- D'une valeur de compilation publiée dans le classpath JAR ou les tâches en aval — l'équivalent le plus proche du
project.versionde Gradle. - D'une commande CLI nommée pour un workflow répété (
./kotlin do release).
N'en écrivez pas si module.yaml le couvre déjà (dépendances, approvisionnement JDK, disposition des sources, empaquetage basique), et jamais pour réutiliser un plugin Gradle — la Kotlin Toolchain ne peut pas les consommer.
Disposition
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, ... }
Conservez au moins un module consommateur dans le dépôt — c'est le seul moyen de tester le plugin de bout en bout, et les plugins ne peuvent pas encore être publiés sur aucun registre public.
project.yaml
modules:
- consumer-app
- plugins/<name>
plugins:
- ./plugins/<name>
Sans le bloc plugins: de niveau supérieur, l'ID du plugin est impossible à résoudre depuis n'importe quel consommateur.
module.yaml — le module du plugin
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
Les valeurs par défaut vont dans les getters des propriétés de l'interface ; les blocs imbriqués deviennent des interfaces @Configurable imbriquées.
@Configurable
interface Settings {
val someValue: String get() = "default"
val checks: ChecksSettings
}
@Configurable
interface ChecksSettings {
val strict: Boolean get() = true
}
Les consommateurs écrasent ce dont ils ont besoin dans module.yaml ; les valeurs omises reviennent à la valeur par défaut du getter :
plugins:
<plugin-id>:
enabled: true
someValue: "override"
checks:
strict: false
@TaskAction
Les actions de tâche sont des funs de niveau supérieur, appelées quand l'entrée correspondante dans plugin.yaml s'exécute.
@TaskAction
fun foo(
@Input moduleRootDir: Path,
@Output outputDir: Path,
settings: Settings,
) {
// body
}
@Input path: Path— entrée déclarée ; la Kotlin Toolchain effectue un snapshot de son contenu pour l'évitement d'exécution.@Output path: Path— répertoire de sortie déclaré ; la Kotlin Toolchain le crée et passe le chemin. Écrivez sur lePathexact que vous avez reçu, sinon les références en aval ne trouveront pas le résultat.settings: Settings(ou tout@Configurable) — configuration typée, connectée dansplugin.yaml.Pathsimple / primitives — passées littéralement depuisplugin.yaml.println(...)est le canal de sortie ; la Kotlin Toolchain capture stdout.
Évitement d'exécution
Une @TaskAction est sautée quand ses entrées déclarées sont inchangées. Les tâches dont les vraies entrées sont l'historique Git, le réseau ou les variables d'environnement ne peuvent pas être fingerprinted, alors opt out :
@TaskAction(executionAvoidance = ExecutionAvoidance.Disabled)
fun foo(@Output outputDir: Path, settings: Settings) { /* ... */ }
Les tâches sans @Output ne sont jamais en cache et s'exécutent toujours — correct pour les tâches purement avec effets secondaires (publications, déploiements, poussées).
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
| Référence | Résout à |
|---|---|
${module.rootDir} |
Répertoire contenant le module.yaml du consommateur. Passez comme @Input pour inspecter l'arborescence du consommateur. |
${taskOutputDir} |
Répertoire de sortie par tâche géré par la Toolchain. Passez comme @Output. |
${pluginSettings} |
L'objet @Configurable construit à partir du module.yaml du consommateur. |
${tasks.<task>.action.<param>} |
Le paramètre d'une autre tâche — utilisé dans generated.* et pour connecter l'@Input d'une tâche à l'@Output d'une autre. |
generated.resources / generated.sources
Les deux enregistrent un répertoire (généralement l'@Output d'une tâche) comme contribution à la compilation du consommateur, et les deux connectent automatiquement la tâche productrice pour s'exécuter en premier :
generated.resources— ajouté au classpath JAR, accessible viagetResourceAsStream("/path/in/jar").generated.sources— ajouté comme racine source Kotlin et compilé avec lesrc/du consommateur.
Tâches vs commandes
Les tâches sont l'implémentation, adressées comme ./kotlin task :<module>:<task>@<plugin-id> — la documentation déconseille de compter sur ce nom transformé. Les commandes sont l'API publique : ./kotlin do <command-name>, listées via ./kotlin show commands (-m <module> pour limiter la portée).
- Une tâche dont l'
@Outputalimentegenerated.resources/generated.sourcesest un contributeur au graphe de compilation. Gardez-la hors decommands:; elle s'exécute automatiquement et l'exposer incite les utilisateurs à l'exécuter manuellement. - Une tâche que les utilisateurs invoquent directement doit être dans
commands:.
Communication entre tâches basée sur des fichiers
Il n'y a pas d'état de compilation mutable partagé — pas de project.version, pas de cartes de propriétés d'extension. Les tâches communiquent via des chemins appariés :
- Le producteur prend
@Output outputDir: Pathet écrit des fichiers dedans. plugin.yamlpointe l'@Inputd'une tâche consommateur à${tasks.<producer>.action.outputDir}/<file>.- La Toolchain déduit la dépendance du fichier appariée — aucune API
dependsOnnécessaire.
Le même répertoire @Output peut servir les consommateurs de compilation (via @Input) et les consommateurs d'exécution (enregistrés sous generated.resources, lus via getResourceAsStream) simultanément.
Remplacements d'exécution via variables d'environnement
Il n'y a pas de -Pkey=value. Lisez les variables d'env à l'intérieur de l'action pour les remplacements éphémères :
val forced = System.getenv("MYPLUGIN_FORCE_VALUE")?.takeIf { it.isNotBlank() }
val skipChecks = System.getenv("MYPLUGIN_SKIP_CHECKS")?.equals("true", ignoreCase = true) == true
Passez la carte d'env en tant que paramètre de constructeur plutôt que d'appeler System.getenv() profondément dans la pile d'appels, afin que la logique reste testable en unité. Documentez chaque variable reconnue dans le README du plugin. Les variables d'env sont des remplacements éphémères, pas une limite de confiance — validez une valeur avant de l'utiliser dans un chemin de fichier ou un argument de processus.
Partage de logique entre actions de tâche
Les tâches partagent souvent des étapes (vérifier → créer → pousser). Ne composez pas une tâche atomique face à l'utilisateur à partir d'une chaîne de tâches de graphe de compilation : les invocations séparées réouvrent les ressources partagées et ouvrent une fenêtre où un autre processus observe l'état intermédiaire.
Limitations à considérer dans la conception
- Les plugins sont locaux uniquement ; pas encore de publication de registre public.
- Les plugins sont au niveau du module ; il n'y a pas de plugin à l'échelle du projet. Chaque module consommateur le liste sous
plugins:, et les effets inter-modules circulent par les fichiers. - Pas d'interpolation
${...}dansmodule.yaml(à partir de 0.11) — les paramètres du consommateur sont des valeurs littérales. - Pas de
Project.afterEvaluate, pas de grapheProvider/Propertyparesseux. Calculez les valeurs dérivées dans le corps de l'action. -h/--helpne liste pas les commandes des plugins ; utilisez./kotlin show commands.
Valider par rapport à un consommateur
- Ajoutez un petit module consommateur (
demo-app/,sample/) activant le plugin avec des paramètres réalistes. - Faites en sorte que son
main.ktou un test lisent ce que le plugin publie. - Mettez les commandes exactes et la sortie attendue dans le README du plugin, afin qu'un clone récent puisse coller et comparer.
Documentation des plugins : https://kotlin-toolchain.org/dev/user-guide/plugins/