Migration de projet Gradle → Kotlin Toolchain
Deux tâches à la fois : une traduction mécanique des dépendances et de la configuration, plus un remplacement pour chaque plugin Gradle auquel la Toolchain n'a pas de réponse native. Les templates à adapter se trouvent dans references/examples.md, tirés d'une véritable migration.
Appuyez-vous sur les skills complémentaires pour les sous-problèmes de forme plugin : kotlin-tooling-kotlin-toolchain pour la syntaxe, kotlin-tooling-gradle-to-kotlin-toolchain-plugin pour le workflow de portage par plugin (vous l'exécuterez une fois par plugin Gradle sans équivalent natif), kotlin-tooling-kotlin-toolchain-plugin-authoring pour les plugins écrit de zéro.
Principes
- Préservez le code source. Si la migration force des modifications à la logique métier, un plugin local peut probablement combler le vide à la place. Le code qui lit
release.propertiesdu classpath doit continuer à fonctionner — publiez ce fichier tel quel, ne le réécrivez pas. - Tout plugin Gradle tiers est drop, native, ou réimplémenté — jamais « garder le plugin Gradle ». Vos propres plugins de convention sont différents : les scripts précompilés
buildSrc/build-logicsont de la configuration partagée, ils deviennent donc des templates de module, pas des plugins locaux — voir Pas debuildSrc/ plugins de convention. - Cherchez sur GitHub avant de créer un plugin local. Quelqu'un l'a probablement déjà écrit ; vendoriser une implémentation fonctionnelle bat un portage de zéro chaque fois. Où chercher et ce qu'il faut chercher : Phase 2 « Portée ». N'écrivez que si la recherche n'aboutit à rien. Lisez en entier ce que vous vendorisez avant de le brancher — il s'exécute au moment de la construction avec un accès complet au système de fichiers et au réseau.
- Toutes les dépendances doivent résider dans le catalogue de versions.
gradle/libs.versions.tomlsubsiste en tant que catalogue$libs.*intégré. Chaque coordonnée dans chaque YAML module/plugin doit finir en référence$libs.*.[bundles]n'a pas d'équivalent Toolchain et devient un template de module — voir Pas de bundles de catalogue de versions.
Flux de travail
Phase 1 — Inventoriez la construction Gradle
Écrivez l'inventaire (par exemple MIGRATION_PLAN.md) avant tout YAML ; il devient la checklist que la description de la PR vérifie.
- Plugins dans le bloc
plugins { }, chacun trié en plugin natif / plugin local. Native couvreorg.jetbrains.kotlin.jvm,kotlin.plugin.serialization, lamainClassdu pluginapplication, les toolchains JDK, les imports BOM et les qualificateurs de portée. - Plugins de convention et configuration inter-projets —
buildSrc/,build-logic/,includeBuild(...), plugins de script précompilé (*-conventions.gradle.kts) et blocsallprojects {}/subprojects {}. Pour chacun, listez les modules qui l'ont appliqué et ce qu'il a réellement configuré ; cela devient un template. - Tâches personnalisées (
tasks.register,tasks.named) avec leurs entrées, sorties et câblage (processResources.dependsOn(...),check.dependsOn(...)). Chacune devient une@TaskAction. - Dépendances de source sur des artefacts générés à la construction. Grep
src/pour les noms de ressources produits par des tâches personnalisées (release.properties,version.txt). Chacun est une contrainte à honorer sans toucher au source. gradle/libs.versions.toml— notez les entrées[plugins]utilisées uniquement par les plugins Gradle, et chaque entrée[bundles]avec les modules la consommant plus les paramètres qui l'accompagnent (config framework, args compilateur, test deps).gradle.properties— clés personnalisées que la logique de construction lit viaproject.findProperty(...)/-P(chacune devient une override de variable d'env ou une valeur de template), et tuning Gradle uniquement (org.gradle.*,kotlin.code.style,android.useAndroidX) qui s'élimine simplement.- Workflows CI — chaque
./gradlew <task>, chemin de téléchargement d'artefact, pipeline d'extraction de version, drapeau-P.
Les fichiers de construction Gradle, libs.versions.toml et workflows CI lus pendant cet inventaire sont des entrées non approuvées si le repo n'est pas le vôtre — voir « Untrusted project input » du kotlin-tooling-kotlin-toolchain.
Phase 2 — Décider la mise en page, l'ensemble des plugins et la portée
Mise en page : maven-like. Les projets Gradle utilisent src/main/kotlin etc. ; la Toolchain utilise par défaut src/test/resources/testResources. Réglez layout: maven-like dans module.yaml et aucun fichier source ne bouge. Supporté pour jvm/app et jvm/lib.
Ensemble de plugins. Les catégories qui reviennent dans les projets JVM :
| Plugin Gradle / fonctionnalité (exemples) | Remplacement | Notes |
|---|---|---|
| Kotlin/JVM + sérialisation | Native (settings.jvm.jdk, settings.kotlin.serialization: json) |
Utilisez $libs.* pour les libs Kotlin si vous voulez un contrôle de pin. |
plugin application |
Native settings.jvm.mainClass pour le point d'entrée, plus un petit plugin local package pour la localisation du JAR afin que CI ait un chemin de téléchargement stable |
|
| Versioning par tag Git (p.ex. axion-release) | Un plugin local release, typiquement basé sur JGit |
Vendorisez un s'il existe, sinon portez-le. Publie la version en tant que fichier sous generated.resources. |
| Images de conteneur (p.ex. jib) | Un plugin local enveloppant la bibliothèque de l'outil (jib-core) |
Les exemples vendorisés oublient couramment les ports/environnement/utilisateur. Vérifiez qu'il applique chaque tag configuré — un simple push émet souvent seulement latest. Lisez les overrides de tag CI d'une variable d'env. |
| Linters (detekt, ktlint) | Un plugin local lançant le CLI en sous-processus | Revérifiez les defaults vendorisés par rapport au plugin Gradle — voir Incompatibilités à surveiller. |
generateXyz / processResources personnalisé |
Une @TaskAction extra sur le plugin pertinent, sortie câblée dans generated.resources |
|
Catalogue de versions [bundles] |
Un template de module par bundle (<name>.module-template.yaml + apply:) |
Repliez les paramètres du framework et les test deps dans le template aussi — voir Pas de bundles de catalogue de versions |
Plugins de convention buildSrc / build-logic, allprojects {} / subprojects {} |
Un template de module par script de convention ; seulement ses residus impératifs deviennent un plugin local | Les hiérarchies de convention se mappent sur des templates imbriqués — voir Pas de buildSrc / plugins de convention |
Chaque plugin local est un module jvm/amper-plugin.
Portée. Pour chaque plugin sur la liste, cherchez sur GitHub avant d'écrire du Kotlin. L'écosystème est petit, mais les plugins récurrents existent déjà quelque part. Ordre de recherche :
JetBrains/kotlin-toolchain→build-sources/(detekt,dokka,binary-compatibility-validator,protobuf,generate-build-properties,project-commands) — les plugins locaux avec lesquels la Toolchain se construit elle-même, c'est-à-dire les implémentations de référence de facto. Aussiplugin-samples/etdocs/dans le même repo.- Recherche de code GitHub sur le marqueur du plugin plutôt que le nom de l'outil :
"product: jvm/amper-plugin",path:plugin.yaml "@TaskAction","jvm/amper-plugin" jib. - Le repo de l'outil lui-même — la plupart des linters/packagers expédient un artefact
-cliou-core, c'est tout ce qu'un plugin wrapper fin a besoin, donc « aucun plugin n'existe » signifie souvent encore « un wrapper de 50 lignes existe ».
Puis choisissez par plugin : vendor (copier une correspondance tel quel ; gardez son en-tête de licence et ajoutez un commentaire avec l'URL source + commit pour qu'il puisse être re-synchronisé), extend (vendor + champs Settings extra), ou author (rien trouvé, ou le besoin est bespoke). Default sensé pour une PR : vendorisez ce qui existe, écrivez les petits bespokes (un plugin package, un thin linter wrapper), déférez le reste. Diffez toujours le comportement d'un plugin vendorisé par rapport au plugin Gradle qu'il remplace avant de lui faire confiance — voir Un plugin linter vendorisé peut être plus strict que le plugin Gradle.
Phase 3 — Implémentez
- Copiez
kotlin/kotlin.batd'un projet Toolchain de référence (par exemple celui-ci) oukotlin initdans un répertoire scratch. Épinglezkotlintoolchain=<version>dans.sdkmanrcpour correspondre au wrapper. - Écrivez
project.yamlavec tous les chemins de modules plugin. - Par plugin : cherchez sur GitHub d'abord (Phase 2 « Portée »), puis vendorisez ou écrivez-le sous
plugins/<name>/, exécutant./kotlin show modulesaprès chacun pour confirmer que le modèle charge encore. - Écrivez les templates à la racine du projet : un
<name>.module-template.yamlpar script de convention et par entrée[bundles]avec deux consommateurs ou plus (dépendances plus les paramètres, test deps et repositories qui l'accompagnent), imbriqués comme les conventions Gradle l'étaient. - Écrivez le
module.yamlracine :product: jvm/app,layout: maven-like, dépendances$libs.*, une listeapply:pour les templates et un blocplugins:activant chaque plugin local avec ses paramètres non-default. - Validez chaque plugin en isolation :
./kotlin task :<module>:<task>@<plugin>ou./kotlin do <command>. - Réécrivez CI (Phase 4).
- Supprimez
build.gradle.kts,gradlew,gradlew.bat,gradle/wrapper/— mais gardezgradle/libs.versions.toml. - Balayez le catalogue : supprimez le bloc
[plugins]entier, toute clé[versions]qui ne l'alimentait que, et[bundles]une fois que chaque consommateur applique un template. - Balayez chaque
module.yaml/plugin.yamlpour les coordonnées Maven littérales et remplacez-les par$libs.<key>.
Phase 4 — Réécrivez CI
- Réglez
KOTLIN_CLI_NO_WELCOME_BANNER: "1". ./gradlew build && ./gradlew check→./kotlin build && ./kotlin check(qui exécute chaque inscriptionchecks:du plugin plus les tests)../gradlew jib -Djib.to.tags=…→./kotlin do jib, avec les tags passés via une variable d'env que le plugin lit. Les plugins jib-style vendorisés manquent généralement de ce hook — ajoutez-le lors de la vendorisation.- Les chemins de téléchargement d'artefact changent :
build/libs/<name>.jarde Gradle est parti etjarJvmécrit dans un chemin interne Toolchain. Créez un pluginpackagemettant en scène le JAR à${module.rootDir}/build/libs/${module.name}.jarafin que télécharger des artefacts reste simple.
Phase 5 — Validez end-to-end
Exécutez chaque commande orientée utilisateur en local et enregistrez la sortie dans le plan de test du PR :
./kotlin show modules # le modèle de projet charge, tous les modules attendus listés
./kotlin clean && ./kotlin build
./kotlin test
./kotlin check # linters + tests ; attendez-vous à zéro violations après la Phase 3
./kotlin do currentVersion # le cas échéant
./kotlin do jib # ou jibBuildTar pour éviter de pousser localement (le cas échéant)
./kotlin do package # vérifiez build/libs/<name>.jar et son manifest Main-Class (le cas échéant)
./kotlin do ktlintFormat # le cas échéant
Incompatibilités à surveiller
Pas d'interpolation ${...} dans module.yaml
configFile: ${module.rootDir}/detekt.yml est pris littéralement. Utilisez un chemin relatif au module (configFile: detekt.yml). L'interpolation fonctionne uniquement dans plugin.yaml.
Le nom du module provient du nom du répertoire
Il n'y a pas de champ name:. actions/checkout clone dans un répertoire nommé d'après le repo GitHub, tandis qu'un worktree local peut résoudre ${module.name} à quelque chose d'autre. Ne codez jamais en dur le nom du module dans le chemin de sortie d'une tâche plugin — utilisez ${module.name}.
Les paramètres de plugin sans enabled: true sont ignorés
plugins:
release: enabled # shorthand — valide uniquement sans autres paramètres
jib: # long form — requis dès qu'un paramètre est présent
enabled: true
container:
mainClass: com.example.App
Les paramètres sans enabled: true produisent seulement un avertissement (« Plugin X is not enabled, but has some explicit configuration ») et le plugin est ignoré.
Un plugin linter vendorisé peut être plus strict que le plugin Gradle
Diffez les drapeaux que le plugin vendorisé passe par rapport à la tâche default du plugin Gradle, et gatez tout ce qui est plus strict derrière un paramètre opt-in afin que le default corresponde à l'ancien comportement.
Le cas canonical : le plugin detekt upstream (amper/build-sources/detekt/) passe toujours --classpath à detekt-cli, activant la résolution de type, ce que la tâche default detekt de Gradle ne fait pas. Utilisé tel quel, il surface des violations que Gradle n'a jamais rapportées (notamment UnreachableCode sur elvis-with-return). Correction : ajoutez useTypeResolution: Boolean get() = false aux Settings du plugin et gatez le drapeau dessus — patch dans references/examples.md.
Pas de bundles de catalogue de versions
Seules les clés [libraries] résolvent ($libs.<key>). $libs.bundles.<name> n'existe pas et [bundles] dans le catalogue est une config morte que la Toolchain ne lit jamais. La réponse officielle (KTC-4759) est un template de module par bundle, appliqué partout où le bundle était utilisé :
# gradle/libs.versions.toml — avant
[bundles]
ktor-server = ["ktor-server-core", "ktor-server-netty", "ktor-server-content-negotiation"]
# ktor-server.module-template.yaml — à la racine du projet ; les templates ne peuvent pas déclarer product:
dependencies:
- $libs.ktor.server.core
- $libs.ktor.server.netty
- $libs.ktor.server.content.negotiation
settings:
kotlin:
serialization: json
test-dependencies:
- $libs.ktor.server.test.host
# module.yaml
product: jvm/app
apply:
- //ktor-server.module-template.yaml
Les templates sont la meilleure cible, pas juste un workaround : un bundle porte seulement des coordonnées, tandis que le framework qu'il active a généralement aussi besoin de settings (kotlin.serialization, springBoot, freeCompilerArgs), test-dependencies et parfois repositories. Mettez tout dans le template afin qu'une seule ligne apply: cède un framework fonctionnant au lieu d'un classpath nu.
Règles qui pourraient mordre :
- La notation de chemin est
//<name>.module-template.yaml, relatif à la racine du projet (où se trouveproject.yaml). - Pas de
product:dans un template. Les templates peuventapply:d'autres templates ; chacun est appliqué une fois même s'il est atteint via deux chemins, donc les dépendances listées n'apparaissent qu'une fois. - Sémantique de fusion : les listes s'ajoutent, les scalaires sont overridés, et
module.yamlgagne toujours indépendamment de l'endroit oùapply:se situe dans le fichier. - Deux templates frères définissant le même scalar (p.ex.
settings.jvm.release) est une erreur dure (« Conflicting values for property »). Résolvez en régalant la valeur dans lemodule.yamlconsommant, ou dans un troisième template qui applique les deux. - Ne convertissez pas un bundle à consommateur unique. Les templates payent à partir du deuxième module ; en dessous, inlinisez la liste
$libs.*. - Les templates expriment l'union des sections qualifiées par plateforme aussi (
dependencies@jvm,settings@android), donc un bundle KMP split sur les source sets tient toujours un template.
Pas de buildSrc / plugins de convention — la config partagée va dans les templates
buildSrc/, build-logic/ et includeBuild(...) n'ont pas d'équivalent, et project.yaml porte seulement modules: et plugins: — il n'y a pas d'héritage de projet root et nulle part pour mettre la logique partagée de construction impérative. Un plugin local est aussi la mauvaise cible : les plugins contribuent des tâches, ils n'injectent pas de configuration de module. Un script de plugin précompilé est surtout déclaratif, donc il se traduit en un <name>.module-template.yaml, appliqué par les modules qui ont utilisé plugins { id("<name>") } :
// buildSrc/src/main/kotlin/service-conventions.gradle.kts
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
}
kotlin { jvmToolchain(21) }
repositories { maven("https://jitpack.io") }
dependencies {
implementation(libs.ktor.server.core)
testImplementation(libs.kotest.runner.junit5)
}
# service-conventions.module-template.yaml
settings:
jvm:
jdk:
version: 21
kotlin:
serialization: json
repositories:
- id: jitpack
url: https://jitpack.io
dependencies:
- $libs.ktor.server.core
test-dependencies:
- $libs.kotest.runner.junit5
| Construction de script de convention | Équivalent du template |
|---|---|
plugins { kotlin("jvm"), kotlin("plugin.serialization"), id("org.springframework.boot") } |
native settings: (settings.kotlin.*, settings.springBoot, …) |
kotlin { jvmToolchain(21) }, java { targetCompatibility } |
settings.jvm.jdk.version, settings.jvm.release |
dependencies { implementation / api / testImplementation } |
dependencies: (: exported pour api) et test-dependencies: |
repositories { } |
repositories: |
tasks.withType<Test> { useJUnitPlatform() } |
intégré / settings.junit |
| un script de convention appliquant un autre script de convention | templates imbriqués (apply: à l'intérieur du template) |
allprojects {} / subprojects {} dans le script root |
un template que chaque module.yaml applique |
tasks.register(…), doLast { }, n'importe quoi d'impératif |
le résidu — un plugin local, un par comportement |
Aussi :
- Supprimez
buildSrc/complètement. Ses accesseurs de cataloguelibssont remplacés par$libs.*utilisés directement dans les templates. - Diviser un script de convention gros en plusieurs petits templates (
jvm,ktor-server,testing) est généralement la meilleure forme, et les templates bundle s'effondrent dans la même hiérarchie — voir Pas de bundles de catalogue de versions pour les règles de fusion/conflit, qui s'appliquent identiquement ici. - L'activation du plugin local à l'intérieur d'un template (un bloc
plugins:dans un*.module-template.yaml) est non vérifiée. Si un script de convention a activé un plugin que vous avez réimplémenté, gardez le blocplugins:dans chaquemodule.yamljusqu'à avoir confirmé que la forme template charge (./kotlin show modulesplus une exécution de tâche réelle).
Pas d'exclusions de dépendances
Il n'y a pas d'équivalent de Gradle exclude(group, module). Toute exclusion transitive disparaît silencieusement et la bibliothèque atterrit sur le classpath runtime. Notez le trade-off dans le PR (généralement quelques centaines de KB inutilisés).
Pas de dépendances de plugin à plugin
Les plugins sont isolés. Si deux plugins appartiennent logiquement ensemble, mettez les deux actions de tâche dans le même module plugin ou extrayez la bibliothèque.
Pas de CLI overrides -P / -D
Lisez les overrides éphémères (force-version, skip-checks, tags d'image dynamiques) depuis les variables d'env à l'intérieur de la @TaskAction ; ne comptez pas sur un drapeau --setting non plus (le CLI épinglé peut le rejeter). Pattern dans la skill kotlin-tooling-gradle-to-kotlin-toolchain-plugin.
Capturer une valeur de sortie de commande
println d'une @TaskAction n'est pas un canal lisible par machine. La Toolchain l'enveloppe comme <ts> INFO :<module>:<task>@<plugin> <value> et ajoute une bannière <task> successful, donc ./kotlin do currentVersion | tail -n1 cède la bannière — et baisser --log-level à error/off supprime la ligne de valeur entièrement, puisqu'elle est émise à INFO. À la place :
- Faites la tâche écrire la valeur dans un fichier nommé par une variable d'env et
catle. De plus, d'autres tâches peuvent réutiliser cette valeur plus loin - Ou lisez l'état git que la commande a produit :
git describe --tags --exact-match HEAD. - Si vous devez parser stdout, comparez la coordonnée de tâche :
awk '/<task>@<plugin>/ { v = $NF } END { print v }'.
Les ressources de test ombrágent generated.resources
Quand un plugin émet release.properties et src/test/resources/release.properties existe, le classement du classpath met le fixture de test en premier. Cela fait généralement la bonne chose mais est fragile — si un test affirme une valeur spécifique, injectez un service stub au lieu de compter sur la précédence.
Dependabot a besoin d'un stub build.gradle.kts
Le fetcher de fichier gradle de Dependabot requiert un build.gradle(.kts) dans le répertoire configuré avant qu'il scanne gradle/libs.versions.toml ; sans lui, package-ecosystem: "gradle" no-ops silencieusement. Gardez un build.gradle.kts vide à la racine avec un commentaire expliquant pourquoi. La Toolchain l'ignore.
Images de conteneur amd64 uniquement sur Apple Silicon
Des images comme mongo:3.2 s'écrasent sous Rosetta/QEMU sur les Macs M-series (runtime: failed to create new OS thread (have 2 already; errno=22)). Inchangé par la migration — la même image échoue sous ./gradlew check. Marquez-le comme pré-existant ; CI sur ubuntu-latest n'est pas affecté.
iOS (KMP) : un Info.plist migré perd ses clés CFBundle*
Une app iOS Gradle/KMP garde les métadonnées de bundle dans le iosApp.xcodeproj maintenu à la main (GENERATE_INFOPLIST_FILE = YES), donc Xcode synthétise les clés CFBundle* et le Info.plist enregistré est partiellement intentionnel. La Toolchain ignore ce .xcodeproj, génère le sien et utilise votre plist verbatim — rien ne synthétise les clés et le .app n'a pas de bundle id :
Simulator device failed to install the application. Missing bundle ID.
Correction : rendez le plist auto-contenu, gardant vos clés app-specific à côté de celles-ci :
<key>CFBundleDevelopmentRegion</key><string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key><string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key><string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
<key>CFBundleVersion</key><string>1</string>
PRODUCT_BUNDLE_IDENTIFIER est régalé sur la cible générée. Comportement sous-jacent : la section « iOS apps » de la skill kotlin-tooling-kotlin-toolchain.
KMP : emportez les dépendances de chaque source set
Traduisez chaque source set Gradle (commonMain, jvmMain, androidMain, iosMain, commonTest, jvmTest, …) dans son équivalent Amper — dependencies, dependencies@jvm/@android/@ios, test-dependencies, test-dependencies@<platform>. Ne cherry-pickez pas les dépendances de bibliothèque évidentes :
-
Une dépendance
*Mainest aussi sur le classpath de test de cette cible (jvmTestétendjvmMain), donc en laisser tomber une peut casser les tests sans erreur de compilation. -
Une dépendance peut ressembler à une autre dépendance de module et être toujours load-bearing. Cas canonical :
jvmMain { implementation(compose.desktop.currentOs) }dans une bibliothèque partagée ressemble à une dépendance app-desktop, mais elle fournit le runtime natif de Skiko (skiko-awt-runtime-<os>aveclibskiko-<os>.dylib+.sha256) que les tests Compose UI propres du module (compose.uiTest/runComposeUiTest) chargent au runtime.compose.ui:ui-testtire seulement les classes Skiko, jamais les natives. Le laisser tomber compile bien, puis échoue avec :org.jetbrains.skiko.LibraryLoadException: Cannot find libskiko-macos-arm64.dylib.sha256, proper native dependency missing.Pas un bug de Toolchain : Gradle échoue identiquement sans lui. Restaurez sous
dependencies@jvm, ou scopez-le àtest-dependencies@jvmpour garder les natives Skiko hors des classpaths des consommateurs.
Gardez-vous des chutes : diffez chaque liste de dépendances du source set Gradle par rapport à sa section Kotlin Toolchain (noms et compte), puis exécutez ./kotlin show dependencies -m <module> et comparez avec la construction Gradle.
Pièges courants
- Pas de plugin
package. CI finit par téléchargerbuild/artifacts/CompiledJvmArtifact/(un arbre de fichier-classe interne) au lieu d'un JAR. Ajoutez le plugin avant de swapper le chemin de téléchargement. - Committer
build/. Ajoutez-le à.gitignoretôt ; supprimez l'ancienne entrée.gradle/. - Interpolation de string à l'intérieur des coordonnées Maven.
com.example:foo:${pluginSettings.version}dansplugin.yamléchoue avec « Value of type 'ShadowDependency' doesn't support string interpolation ». Utilisez$libs.foo.
Docs Kotlin Toolchain : https://kotlin-toolchain.org/dev/