kotlin-tooling-kotlin-toolchain-plugin-authoring

Par kotlin · kotlin-agent-skills

Charger lors de la création, de l'écriture ou de la conception d'un plugin local Kotlin Toolchain pour étendre le build déclaratif avec de la génération de code, du traitement au moment du build, de la vérification personnalisée ou du packaging que `module.yaml` ne peut pas exprimer, ou lors de références à `@TaskAction`, `@Configurable`, `plugin.yaml` ou `jvm/amper-plugin`. À ignorer pour le portage d'un plugin Gradle existant.

npx skills add https://github.com/kotlin/kotlin-agent-skills --skill kotlin-tooling-kotlin-toolchain-plugin-authoring

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.version de 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 le Path exact 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 dans plugin.yaml.
  • Path simple / primitives — passées littéralement depuis plugin.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 via getResourceAsStream("/path/in/jar").
  • generated.sources — ajouté comme racine source Kotlin et compilé avec le src/ 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'@Output alimente generated.resources/generated.sources est un contributeur au graphe de compilation. Gardez-la hors de commands: ; 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 :

  1. Le producteur prend @Output outputDir: Path et écrit des fichiers dedans.
  2. plugin.yaml pointe l'@Input d'une tâche consommateur à ${tasks.<producer>.action.outputDir}/<file>.
  3. La Toolchain déduit la dépendance du fichier appariée — aucune API dependsOn né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 ${...} dans module.yaml (à partir de 0.11) — les paramètres du consommateur sont des valeurs littérales.
  • Pas de Project.afterEvaluate, pas de graphe Provider/Property paresseux. Calculez les valeurs dérivées dans le corps de l'action.
  • -h/--help ne liste pas les commandes des plugins ; utilisez ./kotlin show commands.

Valider par rapport à un consommateur

  1. Ajoutez un petit module consommateur (demo-app/, sample/) activant le plugin avec des paramètres réalistes.
  2. Faites en sorte que son main.kt ou un test lisent ce que le plugin publie.
  3. 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/

Skills similaires