Die Architektur « Unabhängiges Plugin + Konsumenten‑Wurzel » : Warum duplizieren sich meine Gradle-Builds?
Publié le 14 May 2026
- Das Problem: Drei Architekturen, die mir Zeit gekostet haben.
- Die Lösung: Zwei Unabhängige Builds, Eine Konsumentenwurzel
- Warum hat mich diese Architektur erobert?
- Die Anatomie des Unterordners `{name}-plugin/
- Was man nicht macht
- Vergleich:
- Der DAG-Vertrag: Ein Root-Build importiert niemals aus einem Unterordner
- Fazit: Die scheinbare Duplikation, die Zeit spart
- Referenzen
Sie klonen ein Gradle-Plugin-Repository. Sie öffnen ein Terminal. Was geben Sie ein? Dann ?./gradlew tasks, natürlich. Aber in welchem Ordner? Die Wurzel? Das Untermodul? Muss man zuerst ein README lesen, um zu wissen, wie man baut? Wenn Sie zögern, wäre es nicht Nur eine Sekunde, die Architektur des Projekts ist kaputt.
So habe ich dieses Problem einmal für alle gelöst — und warum dies Pattern ist heute die Signatur aller meiner Gradle-Plugins in`foundry/public/`.
Das Problem: Drei Architekturen, die mir Zeit gekostet haben.
Bevor ich zum aktuellen Muster konvergierte, habe ich zwischen drei Ansätzen herumgetastet. klassisch für das Organisieren eines Gradle-Plugins. Jede hatte ein Ausschlusskriterium.
Option 1: Das isolierte Plugin (kein Dogfooding)
Das Projekt enthält nur das Plugin. Kein Verbrauchsbeispiel. Kein Um es zu testen, muss man ein externes Projekt erstellen, y das Plugin über`mavenLocal`oder ein zusammengesetzter Build, und nur dort überprüfen, dass es funktioniert.
$ git clone mon-plugin
$ cd mon-plugin
$ ./gradlew build # le plugin compile
$ # ... et maintenant ? comment je l'essaie ?
|
Ein Gradle-Plugin ohne Verwendungsbeispiel ist eine Bibliothek ohne Integrationstests. Sie wissen nie ob die letzte Änderung hat die Benutzererfahrung zerstört. |
Option 2 : Das klassische Monorepo (include(":plugin"))
Gradle`init`erzeugt`settings.gradle.kts`mit`include("plugin")`. Der Root und das Untermodul teilen denselben Daemon, dieselben Konfigurationen, Die gleichen Kataloge. Praktisch, aber gekoppelt.
.
├── settings.gradle.kts → include("plugin")
├── build.gradle.kts → plugins { id("mon-plugin") }
├── plugin/
│ └── build.gradle.kts → java-gradle-plugin
└── gradle/
└── libs.versions.toml
Was klemmt :
-
Der rootmussdie gleiche Gradle-Version wie das Untermodul haben
-
`libs.versions.toml`ist geteilt — gemeinsame Versionskataloge, Abhängigkeiten
die von einem Modul zum anderen fliehen
-
Es ist nicht möglich, das Plugin unabhängig von root zu bauen.
-
Die CI muss beide Module bauen, auch wenn nur das Plugin geändert wurde.
Option 3: Der Composite Build (includeBuild())
Wir trennen die beiden in getrennte Gradle-Builds und verbinden sie über includeBuild("mon-plugin")`in`settings.gradle.kts.
Das ist schon besser. Der Build des Plugins ist isoliert. Aber der Verbraucher muss den externen Build explizit referenzieren — und die Ausgabe von `./gradlew tasks`an der Wurzel hängt von der richtigen Konfiguration des ab komposit. Das Klonen ist keine Zero-Konfiguration: man muss wissen, dass das Das Plugin befindet sich in einem separaten Ordner, das Root verweist darauf, usw.
.
├── settings.gradle.kts → includeBuild("plugin-build/")
├── build.gradle.kts → plugins { id("mon-plugin") }
└── plugin-build/
├── settings.gradle.kts
└── build.gradle.kts
Ich suchte etwas Besseres. Viel besser.
Die Lösung: Zwei Unabhängige Builds, Eine Konsumentenwurzel
Hier ist das Muster, das ich schließlich übernommen habe :
.
├── settings.gradle.kts ← racine consommateur
├── build.gradle.kts ← 3 lignes : apply plugin + dogfood
├── gradle/
│ └── libs.versions.toml ← catalogue du consommateur
├── {name}-plugin/ ← BUILD INDÉPENDANT
│ ├── gradlew ← son propre wrapper
│ ├── settings.gradle.kts ← rootProject.name = "{name}-plugin"
│ ├── build.gradle.kts ← java-gradle-plugin, signing, publish
│ ├── gradle/
│ │ ├── libs.versions.toml ← catalogue du plugin
│ │ └── wrapper/
│ ├── src/ ← sources du plugin
│ ├── .agents/ ← gouvernance agent
│ └── *.adoc ← AGENT, PROMPT_REPRISE, snapshot, etc.
└── site.yml / slides-context.yml / ... ← configs dogfood
|
Der Schlüssel:`{name}-plugin/`ist ein Gradle-Projekt vollständig und eigenständig. Er hat seinen eigenen Wrapper, seine eigenen Settings, seinen eigenen Katalog von Versionen. Es klont sich, baut sich, testet sich und veröffentlicht sich, ohne dass root erforderlich ist. Bescheid zu wissen über seine Existenz. |
Der Root, er, macht nur eine Sache: das Plugin anwenden.
plugins {
alias(libs.plugins.bakery)
}
repositories {
mavenLocal()
mavenCentral()
}
bakery { configPath = file("site.yml").absolutePath }
Drei Zeilen im Fall von`bakery-gradle`. Nichts mehr. Null`include(), null`includeBuild(), kein Unterprojekt. Ein standardmäßiger Gradle-Build, der ein Plugin wie jedes andere anwendet Welcher Konsument würde das tun?
Der Workflow
Der konkrete Workflow
# 1. Builder le plugin
$ cd codebase-plugin
$ ./gradlew publishToMavenLocal
# 2. L'exercer depuis la racine
$ cd ..
$ ./gradlew indexCodebase queryCodebase snapshot
# La boucle est fermée. Le plugin est testé dans des conditions
# réelles de consommation, par le projet même qui l'héberge.
Warum hat mich diese Architektur erobert?
Drei Vorteile, die kombiniert den Kosten der „Duplizierung“ entsprechen:
Natürliches Dogfooding, sofortiges Feedback
Der beste Test eines Gradle-Plugins besteht darin, ihn zu verwenden. Kein gemockter Einzeltest. Kein`GradleRunner`mit einem Projekt von Test. Ein echter Build der das Plugin auf echte Dateien appliziert.
$ git clone bakery-gradle
$ cd bakery-gradle
$ ./gradlew bake # ← le plugin est exercé immédiatement
Ist das Plugin kaputt, sagt das Root-Build das. Es muss nicht weitergehen. Ein externes Testprojekt suchen. Das Dogfooding ist das erste. Aufgabe, die ein neuer Mitwirkender startet. Das ist der definitive Smoke-Test.
|
Die Regel ist einfach: Wenn die Wurzel kompiliert und`./gradlew tasks` zeigt deine Plugin-Aufgaben an, das Plugin ist funktionsfähig. Keine Überraschung in Produktion. |
2. Zero-Config-Klonierung
Un `git clone && ./gradlew tasks`und der Neuling sieht alles gehen ohne irgendetwas zu konfigurieren. Der`build.gradle.kts`Wurzel ist Die lebendige Nutzungsdokumentation des Plugins. Der`site.yml`daneben zeigt die erwartete Konfiguration.
Vergleiche mit der Alternative: ein README mit drei Absätzen, das erkläre wie builder das plugin ET wie builder das projekt de test. Ein neuer Beitragender liest das README flüchtig, irrt, öffnet ein Issue — obwohl die Information könnte ausführbar sein.
|
Die robusteste Dokumentation ist nicht die, die man liest. Das ist die, dieführt aus. Der Stamm-Build ist die Dokumentation ausführbare Datei des Plugins. |
3. Isolierte Builds, unabhängige CI
Das Plugin hat seinen eigenen Gradle-Wrapper, seinen eigenen Lebenszyklus, seine eigenen Tests. Sie können:
-
Gradle im Plugin aktualisieren ohne den Root zu berühren
-
Eine Abhängigkeit im Plugin hinzufügen, ohne dass sie im Root austritt
-
Das Plugin zerstören, ohne den Root-Build zu beeinflussen (solange du nicht
Veröffentlichen Sie nicht die kaputte Version)
-
Eine CI, die das Plugin erstellt/testet, und eine andere, die es ausführt
Wurzel — unabhängig
.github/workflows/
├── test-plugin.yml → codebase-plugin/.gradlew build
├── test-root.yml → .gradlew tasks (vérifie que le plugin est consommable)
└── publish.yml → codebase-plugin/.gradlew publish
Die Anatomie des Unterordners `{name}-plugin/
Lassen Sie uns einen näheren Blick darauf werfen, was im Plugin-Ordner lebt:
codebase-plugin/
├── gradlew ← wrapper indépendant
├── settings.gradle.kts ← @Suppress("UnstableApiUsage")
│ + foobar-resolver-convention
├── build.gradle.kts ← java-gradle-plugin + signing + publish
├── gradle/
│ ├── libs.versions.toml ← catalogue complet (langchain4j, pgvector...)
│ └── wrapper/
├── buildSrc/ ← classes utilitaires buildSrc
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ ├── codebase/ ← CodebaseYmlAnonymizer, CodebaseConfiguration
│ ├── benchmark/ ← BenchmarkConfig, BenchmarkProtocol
│ ├── readme/ ← ReadmeYmlAnonymizer
│ ├── site/ ← SiteYmlAnonymizer
│ ├── slider/ ← SliderYmlAnonymizer
│ └── snapshot/ ← SnapshotManager
├── src/
│ ├── main/kotlin/codebase/
│ │ ├── CodebasePlugin.kt ← class Plugin<Project>
│ │ ├── rag/ ← pgvector, embedding, anonymization...
│ │ ├── benchmark/ ← BenchmarkRunner, export, comparison...
│ │ └── walker/ ← WorkspaceWalker
│ └── test/
│ ├── kotlin/codebase/scenarios/ ← steps Cucumber
│ ├── features/ ← .feature files
│ └── resources/datasets/ ← fixtures .adoc, .yml, .json
├── .agents/ ← gouvernance agent (INDEX, SESSIONS, etc.)
├── AGENT.adoc ← règles agent
├── PROMPT_REPRISE.adoc ← mission session
├── BACKLOG.adoc ← backlog produit
├── snapshot.adoc ← snapshot auto-généré du projet
└── embeds.yml ← config RAG embeds
Alles ist da. Keine Streuung zwischen Wurzel und Unterordner. Der Entwickler, der am Plugin arbeitet, benötigt nie verlassen`codebase-plugin/`. Der Entwickler der das Plugin verwendet sieht nur die Wurzel — und das`build.gradle.kts`von 3 Zeilen Er sagt ihm alles, was er wissen muss.
Der Versionskatalog: Zwei verschiedene Dateien
|
|
|
Anzahl der Abhängigkeiten |
2-3 (Plugin + Readme eventuell) |
30+ (langchain4j, pgvector, cucumber…) |
Rolle |
Das Plugin verbrauchen |
Erstelle das Plugin |
Wer es liest |
Der Benutzer des Plugins |
Der Entwickler des Plugins |
Der Root hat einen absichtlich minimalen Katalog. Das Plugin hat einen Katalog vollständig. Eine Verwechslung ist unmöglich: jeder Build hat seinen eigenen Scope. von Abhängigkeiten.
|
Wenn du bereits eine Stunde damit verbracht hast, einen Abhängigkeitskonflikt zu debuggen Zwischen Ihrem Plugin und Ihrem Testprojekt verstehen Sie den Wert von Diese Trennung. Unabhängige Kataloge eliminieren dieses Problem. durch Konstruktion. |
Was man nicht macht
Dieses Muster ist nicht magisch. Es legt eine Beschränkung fest, die ich Er belastet mich gerne :
Der Root erstellt das Plugin nicht. Sie müssen`publishToMavenLocal` ou Auf einem Repository bereitstellen, bevor root es konsumieren kann.
Es sind geringe Kosten, und es ist die bonne Beschränkung. Der root Verwendet das Plugin wie ein externer Client — über Maven. Genau. wie es ein Drittanbieter-Projekt tun würde. Wenn das Plugin nicht veröffentlichbar ist, der root sagt es Ihnen sofort.
# La seule "friction" du pattern
$ cd codebase-plugin && ./gradlew publishToMavenLocal && cd ..
$ ./gradlew tasks --group=codebase
Vergleich:
isoliertes Plugin |
Monorepo`include()` |
Komposit`includeBuild()` |
Wurzel + unabhängiges Plugin |
|
`git clone && gradlew tasks`gibt die Aufgaben des Plugins |
❌ |
�✅ |
Bitte stellen Sie den französischen Text bereit, den Sie übersetzen möchten, damit ich die Übersetzung ins Deutsche erstellen und alle Backticks unverändert lassen kann. |
✅ |
unabhängiges Build-Plugin vom Root |
�✅ |
❌ |
(no content) |
�✅ |
Getrennte Katalogversionen |
�✅ |
❌ |
✅ |
✅ |
Keine`include()` ni |
✅ |
❌ |
❌ |
�✅ |
nativer Dogfood ohne Konfiguration |
❌ |
✅ |
⚠️ |
✅ |
unabhängiges CI-Plugin |
(output) |
(empty) |
�✅ |
(Empty response) |
Root ist ein Beispiel für echten Konsum |
�❌ |
⚠️ |
�✅ |
Could you please provide the French text you would like translated into German? |
Die rechte Spalte setzt Häkchen in alle Kästchen. Das ist der Grund, warum ich nicht Ich werde weiter zurückgehen.
Der DAG-Vertrag: Ein Root-Build importiert niemals aus einem Unterordner
Diese Architektur fügt sich natürlich in das DAG N0→N3 ein. von meinem workspace. Das Muster ist : das Plugin N2 ist das Hub von seinen eigenen Abhängigkeiten, die Wurzel N3 ist ein Terminal der die Hubs anwendet
Le codebase-gradle/build.gradle.kts`macht 6 Zeilen. Keine`src/, keine`buildSrc/, kein`gradle/rag-bench.gradle.kts. Genau plugins { alias(libs.plugins.codebase) }`und die Repositories. Die ganze Komplexität lebt in`codebase-plugin/.
Fazit: Die scheinbare Duplikation, die Zeit spart
Wenn ich jemandem diese Struktur zeige, die erste Reaktion ist oft : « Aber du hast zwei`gradlew`, zwei`settings.gradle.kts`, zwei`libs.versions.toml`— Das ist Duplikation !
Ja. Und nein.
Die „Duplizierung“ bedeutet, dieselbe Information an zwei Stellen zu reproduzieren. Hier sind es zwei unterschiedliche Dateien, die zwei unterschiedliche Zwecke dienen: der Katalog des Plugins (30+ Abhängigkeiten für builder) und der Katalog des Wurzels (2-3 Abhängigkeiten zum Verbrauch). Der Wrapper des Plugins (gesperrte Version für die Entwicklung) und der Wrapper der Wurzel (potenziell andere Version, für die Übung des Plugins).
Das ist keine Duplikation. Das ist die Trennung der Verantwortlichkeiten angewendet auf das Build-System. Jeder Build Es erledigt nur eine Sache. Die Wurzel verbraucht. Das Plugin baut sich.
Der Preis? Eine Bestellung`publishToMavenLocal`zwischen dem Build des Plugins Und das Build der Wurzel. Der Vorteil? Eine architektonische Klarheit, die spart stundenlanges Debugging im Nachhinein
Seit ich dieses Muster auf`bakery-gradle`, plantuml-gradle, codebase-gradle, und die anderen Plugins von`foundry/public/, ich habe nicht niemals gezögert beim Öffnen eines Terminals in einem meiner Repositorys. Der erste Reaktion —./gradlew tasks`— geht immer, gibt immer die richtigen Aufgaben und sagt mir sofort, ob alles in Ordnung ist.
Das ist es, die gute Architektur. Sie lässt sich nicht in einem README lesen. Es wird im Terminal getestet, in weniger als zehn Sekunden.
Referenzen
-
Article 0123 — Der Knowledge Graph als Empfehlungsmotor