Архитектура « Plugin nezavisni + koren potrošač » : Зашто моје builds Gradle се дублирају
Објављено 14 May 2026
- Problem: Tri arhitekture koje su mi potrošile vreme
- Решение: Два независних БИЛДА, Један потрошачки корењ
- Zašto mi je ova arhitektura osvojila
- Anatomija poddirektorijuma `{name}-plugin/
- Шта не радимо
- Uporedjenje : Tri arhitekture protiv dogfoodinga
- Ugovor DAG: Jedna korenjska izgradnja nikada ne uvozi iz poddirektorijuma
- Zaključak: Izvidljiva duplikacija koja ušteda vreme
- Референце
Klonirate depo Gradle plugin-a. Otvarate terminal. Šta ukucavate zatim ?./gradlew tasks, naravno. Ali u kojem direktorijumu ? Koren ? Potmodul ? Треба ли прво прочитати README да бисте знали како да builder? Ако ви колебаљете се не би било Za samo sekundu, arhitektura projekta je pokvarena.
Ovde je kako sam rešio ovaj problem jednom za sve — i zašto ovo pattern je danas potpis svih mojih Gradle plugina u`foundry/public/`.
Problem: Tri arhitekture koje su mi potrošile vreme
Pre nego što se konvergirao ka trenutnom pattern-u, pokušavao sam između tri pristupa класични за организацију Gradle plugина. Svaka je imala fatalан недостатак.
Option 1 : Izoljirani plugin (bez dogfoodinga)
Projekt sadrži samo dodatak. Nema primer upotrebe. Nema Пројекат који га извршава. Да би се тестирао, треба створити споњи пројекат, y navesti dodatak preko`mavenLocal`ili jedan kompozitni build, i samo proveri tamo da radi
$ git clone mon-plugin
$ cd mon-plugin
$ ./gradlew build # le plugin compile
$ # ... et maintenant ? comment je l'essaie ?
|
Gradle dodatak bez primera konsumacije je biblioteka bez testovi integracije. Nikada ne znate da li je poslednja izmena pokvario korisničko iskustvo. |
Opcija 2 : Klasicni Monorepo (include(":plugin"))
Gradle`init`генерише`settings.gradle.kts`са`include("plugin")`. Root i potmodul dele isti demon, iste konfiguracije, исти каталози. Практično, али повезано.
.
├── settings.gradle.kts → include("plugin")
├── build.gradle.kts → plugins { id("mon-plugin") }
├── plugin/
│ └── build.gradle.kts → java-gradle-plugin
└── gradle/
└── libs.versions.toml
Шта пречи:
-
Коренtrebaimati istu verziju Gradle-ja kao podmodul
-
`libs.versions.toml`je podeljeno — zajednički katalogi verzija, zavisnosti
који бегу из једног модула у другом.
-
Nemoguće je izgraditi plugin nezavisno od root.
-
CI mora da izgradi oba modula, čak i ako se promenio samo plugin.
Opcija 3 : Kompozitni build (includeBuild())
Podelimo ih u dva zasebna Gradle builda i povežemo ih putem includeBuild("mon-plugin")`у`settings.gradle.kts.
Већ је боље. Сграда plugina је изолирана. Али потрошач. Mora eksplicitno referencirati vanjski build — i izlaz iz `./gradlew tasks`u korenju zavisi od ispravne konfiguracije kompozitno. Kloniranje nije zero-config: moraš znati da je plugin se nalazi u posebnom folderu, što ga root referencuje, itd.
.
├── settings.gradle.kts → includeBuild("plugin-build/")
├── build.gradle.kts → plugins { id("mon-plugin") }
└── plugin-build/
├── settings.gradle.kts
└── build.gradle.kts
Tražio sam bolje. Mnogo bolje.
Решение: Два независних БИЛДА, Један потрошачки корењ
Ovo je obrazac koji sam na kraju prihvatio:
.
├── 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
|
Кључ :`{name}-plugin/`је Gradle пројекат пун и самодостајан. On poseduje svoj wrapper, svoje postavke, svoj katalog od verzije. On se klonira, se gradi, se testira i se objavljuje bez root. da bude u toku sa njenim postojanjem |
Root, on, ne radi ništa osim: primene plugina.
plugins {
alias(libs.plugins.bakery)
}
repositories {
mavenLocal()
mavenCentral()
}
bakery { configPath = file("site.yml").absolutePath }
Три линије у случају`bakery-gradle`. Ništa više. Nula`include(), nula`includeBuild(), nult podprojekat. običan Gradle build koji primenjuje plugin kao bolo Koji potrošač bi ga uradio.
Radni tok
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12
left to right direction
package "korijen (potrošač)" #CCFFCC {
usecase "Клонирај репозиторијум" as Clone
usecase "Зачувај све код спанове у обратним наводнима (`...`) тако да остају не промењени — никогда не мењај садржај, размак или позицију у обратним наводнима." as Tasks
usecase "./gradlew bake" as Dogfood
note bottom of Dogfood
Exerce le plugin
Feedback immédiat
Zéro config
end note
}
package "{name}-plugin/ (Nezavisna gradnja)" #CCE5FF {
usecase "./gradlew build" as BuildPlugin
usecase "./gradlew publishToMavenLocal" as MavenLocal
usecase "./gradlew test" as TestPlugin
note bottom of BuildPlugin
Cycle de vie isolé
Tests unitaires + Cucumber
CI dédiée possible
end note
}
Clone -down-> Tasks
Tasks -down-> Dogfood
Dogfood ..> MavenLocal : "Zavisno od plugina
Objavljeno lokalno"
BuildPlugin -up-> MavenLocal
TestPlugin -up-> BuildPlugin
@enduml
конкретан воркфлоу
# 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.
Zašto mi je ova arhitektura osvojila
Tri prednosti koje, kombinovane, vredne su troška «duplikacije» :
1. Natvorno dogfooding, instant feedback
Najbolji test Gradle plugina je da ga koristite. Nije mockovan unit test. Nije`GradleRunner`са један пројекат od test. Jedna stvarna gradnja koja primenjuje plugin na stvarne datoteke.
$ git clone bakery-gradle
$ cd bakery-gradle
$ ./gradlew bake # ← le plugin est exercé immédiatement
Ako je plugin pokvaren, koren build to kaže. Nije potrebno ići. претрага vanjског тест пројекта. Dogfooding је први Zadatak pokrenut od novog saradnika. To je konačni smoke test.
|
Pravilo je jednostavno: ako koren kompajlira i`./gradlew tasks` Prikazuje vaše zadatke plugina, plugin je funkcionalan. Nema iznenađenja u proizvodnji |
2. Клонирање без конфигурације
Un `git clone && ./gradlew tasks`и нови доњац види све ходети без конфигурисања. (space after period) Actually I’ll output exactly: "ходети без конфигурисања. " (including trailing space). Let’s produce that.
</think> ходети без конфигурисања.`build.gradle.kts`koren je жива документација за коришћење додатка. Ле`site.yml`pored Prikazuje očekivanu konfiguraciju.
Uporedite sa alternativom: jedan README od tri paragrafa koji objasni kako da se gradi plugin i kako da se gradi projekat од тест. Један нови садарник чита README у дијагонали, greši, otvara problem — dok informacija mogla бити извршено.
|
Najjača dokumentacija nije ona koju se čita. To je ona kojaизвршава. Корениј build je dokumentacija. Izvršni fajl plugina. |
3. Izolovane gradnje, neovisni CI
Plugin ima svoj Gradle wrapper, svoj životni ciklus, svoje testove. Možete:
-
Ažuriraj Gradle u pluginu bez dotaćanja root-a
-
Dodajte zavisnost u plugin bez da protekne u koren
-
pokvariti plugin bez uticaja na korenski build (dokle ne
Ne objavljivajte pokvarenu verziju)
-
Da postoji CI koja build/test plugin, i druga koja exerce
root — nezavisno
.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
Anatomija poddirektorijuma `{name}-plugin/
Pogledajmo bliže šta živi u folderu dodatka:
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
Sve je ovde. Nema disperzije između korena i podfolderska. Razvijalac koji radi na pluginu nikada ne treba оставити`codebase-plugin/`. Razvojnik koji koristi plugin gleda samo koren — i`build.gradle.kts`od 3 linija on mu kaže sve što mu je potrebno znati.
Katalog verzija: Dva različita fajla
|
|
|
Broj zavisnosti |
2-3 (plugin + readme eventualno) |
30+ (langchain4j, pgvector, cucumber…) |
Улога |
Potrositi plugin |
Builder plugin |
Ko ga čita |
Korisnik plugina |
Развојник плагина |
Root ima namerno minimalan katalog. Plugin ima katalog kompletan. Zbunjenje je nemoguće: svaki build ima svoj opseg zavisnosti.
|
Ako ste već provedli sat u debagovanju kolizije zavisnosti među vašim pluginom i vašim test projekatom, razumete vrednost Ово раздвојање. Независни каталози уклањају овај проблем. po konstrukciji |
Шта не радимо
Ovaj patern nije magičan. On postavlja ograničenje koje ja On mi volontersko zadava bol :
Root ne gradi plugin. Morate`publishToMavenLocal` ou разврстати на репозиторијум пре него што рут може га да потроши.
Ovo je manji trošak, i ovo je dobar ograničenje. Korijen potrošava plugin kao vanjskog klijenta — putem Maven. Tačno kao bi to uradilo projekat treće strane. Ako plugin nije objavljiv, koren vam to odmah kaže
# La seule "friction" du pattern
$ cd codebase-plugin && ./gradlew publishToMavenLocal && cd ..
$ ./gradlew tasks --group=codebase
Uporedjenje : Tri arhitekture protiv dogfoodinga
Izoljiran plugin |
Monorepo`include()` |
kompozit`includeBuild()` |
Корен + независни плагин |
|
`git clone && gradlew tasks`даје задатке плагина |
❌ |
da |
(Note: Since no French text was provided for translation, the output is empty.) |
✅ |
Izgradite plugin nezavisno od root |
</think> |
❌ |
✅ |
✅ |
Posebne verzije kataloga |
✅ |
❌ |
(nothing) |
✅ |
Нема`include()` ni |
✅ |
❌ |
❌ |
✅ |
Natiran dogfood bez konfiguracije |
❌ |
✅ |
⚠️ |
✅ |
CI plugin nezavisna |
✅ |
❌ |
✅ |
✅ |
Root je primer realne potrošnje. |
❌ |
⚠️ |
✅ |
✅ |
Desna kolona označava sva polja. Zato što ja ne Vratiću se više unazad.
Ugovor DAG: Jedna korenjska izgradnja nikada ne uvozi iz poddirektorijuma
Ова архитектура се интегрише натурално у DAG-у N0→N3 mojeg radnog prostora. Oblik je : plugin N2 je hub Iz sopstvenih zavisnosti, koren N3 je terminal koji primenjuje hubove
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11
title Ugovor DAG — Korenski potrošač vs nezavisni plugin
package "N3 — KORIJEN (skladište)" #FFCCCC {
[build.gradle.kts (3 lignes)] as ROOT
[libs.versions.toml (minimal)] as ROOT_TOML
note right of ROOT
plugins { bakery }
bakery { configPath = ... }
Aucune logique métier
end note
}
package "N2 — {name}-додатак/" #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
signing + publish
Tests unitaires + Cucumber
CI dédiée
end note
}
ROOT_TOML --> ROOT : "verzija plugina"
ROOT -down-> PLUGIN : "publishToMavenLocal"
@enduml
Le codebase-gradle/build.gradle.kts`Pravi 6 linija. Nema`src/, nema`buildSrc/, nema`gradle/rag-bench.gradle.kts. Тачно plugins { alias(libs.plugins.codebase) }`i repozitorijumi. Sva kompleksnost živi u`codebase-plugin/.
Zaključak: Izvidljiva duplikacija koja ušteda vreme
Kada pokazujem ovu strukturu nekom, prva reakcija je često : « Ali imaš dva`gradlew`, dva`settings.gradle.kts`, два`libs.versions.toml`— Ovo je dupliranje!
Да. И не.
Dupliciranje je reprodukcija iste informacije na dva mesta. Ovo su dva različita fajla koja sluze dva različita namena: каталог плагина (30+ зависимости за builder) и каталог od korena (2-3 zavisnosti za konzumiranje). Omotač plugina (zaključana verzija za razvoj) i omotač korena (potencijalno različita verzija, za vežbu plugina).
Ово није дублирање. Ово је раздвајање одговорности примењена у систем од градње. Свака градна Uradi jedno, i samo jedno. Koren konsumira. Plugin se gradi.
Cena? Narudžba`publishToMavenLocal`među izgradnjom plugina i izgradnja korena. Dobit? Arhitektonska jasnoća koja uklanja sate debagovanja u nizu.
Od kada sam primenio ovaj pattern na`bakery-gradle`, plantuml-gradle, codebase-gradle, и остали плугини`foundry/public/, нисам имао Никад више није размишљао при отварању терминала у једном од мојих репозиторијума. prvi refleks —./gradlew tasks`— хода увек, дај уvek добра задатке, и одмах ми каже да ли је све у redu.
To je to, dobra arhitektura. Ne se čita u README-u. Она се тестира у терминалу, у мање од десет секунди.
Референце
-
Članak 0123 — Knowledge Graph kao motor preporuke