leestijd : 11 minutes

Je kloont een Gradle-plugin repository. Je opent een terminal. Wat typt u daarna ?./gradlew tasks, natuurlijk. Maar in welke map? De hoofdmap? De submodule? Moet je eerst een README lezen om te weten hoe je moet bouwen? Als je twijfelt, zou het Een seconde, de architectuur van het project is kapot.

Zo heb ik dit probleem eenmaal en voor altijd opgelost — en waarom dit patroon is vandaag de handtekening van al mijn Gradle-plugins in`foundry/public/`.

Het Probleem: Drie Architecturen Die Mijn Tijd Hebben Gekost

Voordat ik convergeerde naar het huidige patroon, heb ik getast tussen drie benaderingen klassiek voor het organiseren van een Gradle-plugin. Elk had een fatale gebrek.

Optie 1 : het geïsoleerde plugin (geen dogfooding)

Het project bevat alleen de plugin. Geen voorbeeld van verbruik. Geen project dat het uitoefent. Om het te testen, moet je een extern project aanmaken, y verwijzen naar de plugin via`mavenLocal`of een samengestelde build, en alleen daar controleren of het werkt

$ git clone mon-plugin
$ cd mon-plugin
$ ./gradlew build          # le plugin compile
$ # ... et maintenant ? comment je l'essaie ?

Een Gradle-plugin zonder voorbeeld van consumptie, is een bibliotheek zonder integratietests. Je weet nooit of de laatste wijziging heeft de gebruikerservaring verbroken.

Optie 2 : De klassieke monorepo (include(":plugin"))

Gradle`init`genereert`settings.gradle.kts`met`include("plugin")`. Root en het submodule delen dezelfde daemon en dezelfde configuraties, dezelfde catalogi. Handig, maar gekoppeld.

.
├── settings.gradle.kts   → include("plugin")
├── build.gradle.kts      → plugins { id("mon-plugin") }
├── plugin/
│   └── build.gradle.kts  → java-gradle-plugin
└── gradle/
    └── libs.versions.toml

Wat knelt:

  • De rootmoetdezelfde versie van Gradle hebben als de submodule

  • `libs.versions.toml`wordt gedeeld — gemeenschappelijke versie-catalogi, afhankelijkheden

die van het ene module naar het andere vluchten

  • Onmogelijk de plugin onafhankelijk van de root te bouwen.

  • De CI moet de twee modules bouwen, zelfs als alleen het plugin is gewijzigd.

Optie 3: de Composite Build (includeBuild())

We scheiden de twee in afzonderlijke Gradle-builds en verbinden ze via includeBuild("mon-plugin")`in`settings.gradle.kts.

Het is al beter. De bouw van de plugin is geïsoleerd. Maar de consument moet expliciet verwijzen naar de externe build — en de uitvoer van `./gradlew tasks`bij de wortel afhankelijk van de juiste configuratie van de composiet. De kloning is niet zero-config: Men moet weten dat de plugin is in een aparte map, dat de root ernaar verwijst, enz.

.
├── settings.gradle.kts   → includeBuild("plugin-build/")
├── build.gradle.kts      → plugins { id("mon-plugin") }
└── plugin-build/
    ├── settings.gradle.kts
    └── build.gradle.kts

Ik zocht beter. Veel beter.

De Oplossing: twee onafhankelijke builds, één verbruikerswortel

Dit is het patroon dat ik uiteindelijk heb aangenomen :

.
├── 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

De sleutel:`{name}-plugin/`is een volledig autonoom Gradle-project. Hij heeft zijn eigen wrapper, zijn eigen settings, zijn eigen catalogus van versies. Het wordt gekloond, gebouwd, getest en gepubliceerd zonder dat de root op de hoogte te zijn van zijn bestaan

De root, hij, doet niets anders dan een ding : de plugin toepassen

plugins {
    alias(libs.plugins.bakery)
}

repositories {
    mavenLocal()
    mavenCentral()
}

bakery { configPath = file("site.yml").absolutePath }

Drie regels in het geval van`bakery-gradle`. Niets meer. Nul`include(), nul`includeBuild(), nul subproject. Een klassieke Gradle-build die een plugin toepast zoals elk Welke consument zou dat doen?

De werkstroom

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12

left to right direction

package "RACINE (Verbruiker)" #CCFFCC {
    usecase "Kloon de opslagplaats" as Clone
    usecase "./gradlew taken" as Tasks
    usecase "./gradlew bak" as Dogfood
    note bottom of Dogfood
Oefen de plugin
Directe feedback
Geen configuratie
    end note
}

package "{name}-plugin/ (Onafhankelijke build)" #CCE5FF {
    usecase "./gradlew bouw" as BuildPlugin
    usecase "./gradlew publishToMavenLocal" as MavenLocal
    usecase "./gradlew testen" as TestPlugin
    note bottom of BuildPlugin
Geïsoleerde levenscyclus
Unit-tests + Cucumber
Dedicated CI mogelijk
    end note
}

Clone -down-> Tasks
Tasks -down-> Dogfood
Dogfood ..> MavenLocal : "afhangt van de plugin\ngepubliceerd lokaal"
BuildPlugin -up-> MavenLocal
TestPlugin -up-> BuildPlugin

@enduml

Het concreet 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.

Waarom heeft deze architectuur mij veroverd?

Drie voordelen die, gecombineerd, de kosten van de « duplicatie » waard zijn :

1. Natief dogfooding, directe feedback

De beste test van een Gradle-plugin is om het te gebruiken. Geen gemockte eenheidstest. Geen`GradleRunner`met een project van test. Een echte build die de plugin toepast op echte bestanden.

$ git clone bakery-gradle
$ cd bakery-gradle
$ ./gradlew bake    # ← le plugin est exercé immédiatement

Als de plug‑in stuk is, zegt de root-build het. Geen reden om te gaan. zoek een extern testproject. Dogfooding is de eerste taak gelanceerd door een nieuwe bijdrager. Dat is de definitieve smoke test.

De regel is eenvoudig: als de wortel compileert en`./gradlew tasks` toont uw plugin-taken, de plugin is functioneel. Geen verrassing in productie.

Kloning Zero-Config

Un `git clone && ./gradlew tasks`en de nieuwkomer ziet alles lopen zonder iets te configureren. De`build.gradle.kts`wortel is de levende documentatie van het gebruik van de plugin. Het`site.yml`naast toont de verwachte configuratie.

Vergelijk met het alternatief: een README van drie alinea’s die leg uit hoe je de plugin bouwt en hoe je het project bouwt van test. Een nieuwe contributor leest de README diagonaal, vergist zich, opent een issue — terwijl de informatie zou kunnen zijn uitvoerbaar.

De robuustste documentatie is niet degene die men leest. Dat is degene die wevoert uit. De root-build is de documentatie uitvoerbare van de plugin

3. Geïsoleerde builds, onafhankelijke CI

De plug‑in heeft zijn eigen Gradle‑wrapper, zijn eigen levenscyclus, zijn eigen tests. Je kunt:

  • Gradle upgraden in de plugin zonder de root aan te raken

  • Voeg een afhankelijkheid toe in de plug‑in zonder dat deze in de root lekt.

  • Breken van de plugin zonder de root build te beïnvloeden (zolang je niet

Publiceer de kapotte versie niet)

  • Een CI hebben die de plugin build/test, en een andere die de

root — ongeacht

.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

De anatomie van de submap `{name}-plugin/

Laten we eens nader kijken wat leeft in de pluginmap:

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 is daar. Geen verspreiding tussen de wortel en de submap. De ontwikkelaar die aan de plugin werkt heeft nooit nodig vertrekken`codebase-plugin/`. De ontwikkelaar die de plug-in gebruikt kijkt alleen naar de wortel — en de`build.gradle.kts`van 3 lijnen Hij vertelt hem alles wat hij moet weten.

De Versiecatalogus: twee afzonderlijke bestanden

gradle/libs.versions.toml(wortel)

{name}-plugin/gradle/libs.versions.toml

Aantal afhankelijkheden

2-3 (plugin + readme eventueel)

30+ (langchain4j, pgvector, cucumber…​)

Rol

Consumeer de plugin

Bouw de plugin

Wie het leest

De gebruiker van de plugin

De ontwikkelaar van de plugin

De root heeft een bewust minimaal catalogus. De plugin heeft een catalogus volledig. Verwarring is onmogelijk: elke build heeft zijn eigen scope van afhankelijkheden.

Als je al een uur hebt besteed aan het debuggen van een afhankelijkheidsconflict tussen uw plug-in en uw testproject begrijpt u de waarde van deze scheiding. Onafhankelijke catalogi elimineren dit probleem door constructie

Wat we niet doen

Dit patroon is niet magisch. Het legt een beperking op die ik hij/zij toebrengt mij graag:

De root bouwt de plugin niet. U moet`publishToMavenLocal` ou voordat root het kan gebruiken.

Het is een kleine kost, en het is de goede beperking. De root Gebruikt de plugin als een externe client — via Maven. Exact zoals een derde partijproject zou doen. Als de plugin niet publiceerbaar is, de root zegt het je meteen.

# La seule "friction" du pattern
$ cd codebase-plugin && ./gradlew publishToMavenLocal && cd ..
$ ./gradlew tasks --group=codebase

Vergelijking : De drie architecturen geconfronteerd met dogfooding

Geïsoleerde plugin

Monorepo`include()`

Samengesteld`includeBuild()`

Root + onafhankelijke plugin.

`git clone && gradlew tasks`geeft de taken van de plugin

❌

✅

Begrepen.

✅

Root-onafhankelijke build-plugin

✅

❌

✅

✅

Gescheiden catalogusversies

✅

❌

Begrepen.

✅

Geen`include()` ni includeBuild()

Ready.

❌

❌

✅

Native dogfooding zonder config

❌

✅

⚠️

✅

Onafhankelijke CI-plugin

Begrepen.

❌

✅

✅

De root is een voorbeeld van een werkelijk verbruik

❌

⚠️

✅

✅

De kolom rechts vinkt alle vakjes aan. Daarom ik niet Ik zal verder terugkomen.

Het DAG-contract: Een Build Racin is nooit van belang sinds een submap

Deze architectuur past zich van nature aan in de DAG N0→N3 van mijn werkruimte. Het patroon is: het plugin N2 is de hub Van zijn eigen afhankelijkheden is de N3-wortel een terminal. die hubs toepast.

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11

title Overeenkomst DAG — Basis consument vs onafhankelijke plugin
package "N3 — RACINE (depot)" #FFCCCC {
    [build.gradle.kts (3 lignes)] as ROOT
    [libs.versions.toml (minimal)] as ROOT_TOML
    note right of ROOT
plugins { bakery }
bakery { configPath = ... }
Geen zakelijke logica
    end note
}

package "N2 — {name}-plugin/" #CCFFCC {
    [build.gradle.kts (complet)] as PLUGIN
    [src/main/kotlin/] as SRC
    [gradle/libs.versions.toml] as PLUGIN_TOML
    [buildSrc/] as BUILDSRC
    note bottom of PLUGIN
java-gradle-plugin
ondertekenen + publiceren
Unittesten + Cucumber
Toegewijde CI
    end note
}

ROOT_TOML --> ROOT : "versie plugin"
ROOT -down-> PLUGIN : "publishToMavenLocal"
@enduml

Le codebase-gradle/build.gradle.kts`maakt 6 regels. Geen`src/, geen`buildSrc/, geen`gradle/rag-bench.gradle.kts. rechtvaardig plugins { alias(libs.plugins.codebase) }`en de repositories. De hele complexiteit leeft in`codebase-plugin/.

Conclusie: De schijnbare duplicatie die tijd bespaart

Wanneer ik deze structuur aan iemand laat zien, is de eerste reactie is vaak: « Maar je hebt twee`gradlew`, twee`settings.gradle.kts`, twee`libs.versions.toml`— het is duplicatie !

Ja. En nee.

De 'duplicatie' is het reproduceren van dezelfde informatie op twee plaatsen. Hier zijn twee distincts bestanden die twee distincts doeleinden dienen : de catalogus van de plug-in (30+ afhankelijkheden voor builder) en de catalogus van de wortel (2-3 afhankelijkheden om te consumeren). De wrapper van de plugin (vergrendelde versie voor ontwikkeling) en de wrapper van de hoofdmap (potentieel verschillende versie, voor de plugin-oefening)

Het is de scheiding van de verantwoordelijkheden toegepast op het build-systeem. Elke build Het doet één ding, en één ding alleen. De wortel consumeert. De plugin bouwt.

De kost? Een bestelling`publishToMavenLocal`tussen de build van de plugin en de build van de root. Het voordeel? Een architecturale helderheid die verwijderd uren aan downstream debugging

Sinds ik dit patroon heb geïmplementeerd op`bakery-gradle`, plantuml-gradle, codebase-gradle, en de andere plugins van`foundry/public/, ik heb nooit meer getwijfeld bij het openen van een terminal in een van mijn repositories. De eerste reflex —./gradlew tasks`— loopt altijd, geeft altijd de juiste taken, en zegt me onmiddellijk of alles gezond is.

Dat is het, de goede architectuur. Je leest het niet in een README. Het wordt getest in een terminal, in minder dan tien seconden.

Gerelateerde artikelen