Arsitektur « Plugin Independen + Akar Konsumen » : Mengapa Build Gradle Saya Menduplikat?
Diterbitkan 14 May 2026
- Masalah: Tiga Arsitektur yang Menyita Waktu Saya
- Solusi: Dua Build Independen, Akar Konsumen
- Mengapa arsitektur ini menaklukkan saya
- Anatomi dari Subdirektori `{name}-plugin/
- Apa yang tidak kita lakukan
- Perbandingan: Tiga Arsitektur Terhadap Dogfooding
- Kontrak DAG: Build akar tidak pernah berasal dari subfolder
- Conclusion : Duplikasi Tampak yang Menghemat Waktu
- Referensi
Anda mengklon repositori plugin Gradle. Anda membuka terminal. Apa yang Anda ketik? Selanjutnya?./gradlew tasks, tentunya. Namun dalam folder mana? Akar? Sub-modul? Apakah harus membaca README terlebih dahulu untuk mengetahui cara membangun? Jika Anda ragu, bukankah dalam sekejap, arsitektur proyeknya rusak
Ini adalah bagaimana saya sudah menyelesaikan masalah ini sekali untuk selamanya — dan mengapa ini pattern saat ini adalah tanda tangan semua plugin Gradle saya di`foundry/public/`.
Masalah: Tiga Arsitektur yang Menyita Waktu Saya
Sebelum mencapai pola saat ini, saya mencoba-coba antara tiga pendekatan Klasik untuk mengatur plugin Gradle. Masing-masing memiliki kelemahan yang tidak dapat diterima.
Opsi 1: Plugin Terisolasi (tidak ada dogfooding)
Proyek hanya berisi plugin. Tidak ada contoh konsumsi. Tidak ada proyek yang mengerjakannya. Untuk mengujinya, harus membuat proyek eksternal, y mereferensikan plugin melalui`mavenLocal`atau satu build komposit, dan hanya periksa bahwa itu berfungsi
$ git clone mon-plugin
$ cd mon-plugin
$ ./gradlew build # le plugin compile
$ # ... et maintenant ? comment je l'essaie ?
|
Plugin Gradle tanpa contoh konsumsi adalah sebuah pustaka tanpa tes integrasi. Anda tidak pernah tahu jika perubahan terakhir a merusak pengalaman pengguna |
Opsi 2 : Monorepo Klasik (include(":plugin"))
Gradle`init`menghasilkan`settings.gradle.kts`dengan`include("plugin")`. Root dan sub-modul membagikan daemon yang sama, konfigurasi yang sama, katalog yang sama. Praktis, tetapi terhubung.
.
├── settings.gradle.kts → include("plugin")
├── build.gradle.kts → plugins { id("mon-plugin") }
├── plugin/
│ └── build.gradle.kts → java-gradle-plugin
└── gradle/
└── libs.versions.toml
Yang menyangkut:
-
akarharusmemiliki versi Gradle yang sama dengan sub-modul
-
`libs.versions.toml`dibagikan — katalog versi umum, dependensi
yang melarikan diri dari satu modul ke modul lain
-
Tidak dapat membangun plugin secara independen dari root.
-
CI harus membangun kedua modul, meski hanya plugin yang berubah.
Opsi 3 : Build Komposit (includeBuild())
Kami memisahkan keduanya menjadi build Gradle terpisah dan kami menghubungkannya melalui includeBuild("mon-plugin")`dalam`settings.gradle.kts.
Ini sudah lebih baik. Build plugin terisolasi. Tapi konsumen harus secara eksplisit merujuk pada build eksternal — dan output dari `./gradlew tasks`pada akar bergantung pada konfigurasi yang benar dari komposit. Kloning bukan zero-config : Anda harus mengetahui bahwa plugin berada di folder terpisah, yang direferensi oleh root, etc.
.
├── settings.gradle.kts → includeBuild("plugin-build/")
├── build.gradle.kts → plugins { id("mon-plugin") }
└── plugin-build/
├── settings.gradle.kts
└── build.gradle.kts
Saya mencari yang lebih baik. Lebih baik lagi.
Solusi: Dua Build Independen, Akar Konsumen
Berikut adalah pola yang akhirnya saya mengadopsi:
.
├── 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
|
Kunci :`{name}-plugin/`adalah sebuah proyek Gradle yang lengkap dan mandiri Dia memiliki wrapper sendiri, settings sendiri, katalog de versi. Ia diklon, dibuild, ditest, dan dipublikasikan tanpa akar tahu akan keberadaannya |
Root, hanya satu hal: menerapkan plugin.
plugins {
alias(libs.plugins.bakery)
}
repositories {
mavenLocal()
mavenCentral()
}
bakery { configPath = file("site.yml").absolutePath }
Tiga baris dalam kasus`bakery-gradle`. Tidak ada lagi. Nol`include(), nol`includeBuild(), nol sub-proyek. Sebuah build Gradle klasik yang menerapkan plugin seperti apa pun Konsumen mana yang akan melakukannya?
Alur Kerja
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12
left to right direction
package "RACINE (Konsumen)" #CCFFCC {
usecase "Klona repositori" as Clone
usecase "./gradlew tasks" 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/ (Build Mandiri)" #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 : "bergantung pada plugin
diterbitkan secara lokal"
BuildPlugin -up-> MavenLocal
TestPlugin -up-> BuildPlugin
@enduml
Workflow konkret
# 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.
Mengapa arsitektur ini menaklukkan saya
Tiga manfaat yang, ketika digabungkan, setara dengan biaya « duplication» :
Dogfooding Asli, Umpan Balik Instan
Uji terbaik sebuah plugin Gradle adalah menggunakannya. Bukan sebuah unit test yang dimock. Bukan sebuah`GradleRunner`dengan sebuah proyek uji. Sebuah build nyata yang menerapkan plugin pada file yang nyata.
$ git clone bakery-gradle
$ cd bakery-gradle
$ ./gradlew bake # ← le plugin est exercé immédiatement
Jika plugin rusak, build root memberitahu. Tidak perlu pergi Mencari proyek pengujian eksternal. Dogfooding adalah pertama. Tugas yang diluncurkan oleh kontributor baru. Ini adalah smoke test muktamad.
|
Aturan ini sederhana: jika akar tersebut dikompilasi dan`./gradlew tasks` Menampilkan tugas plugin Anda, plugin berfungsi. Tidak ada kejutan. dalam produksi. |
2. Kloning Tanpa Konfigurasi
Un `git clone && ./gradlew tasks`dan orang baru melihat semuanya berjalan tanpa mengonfigurasi apa pun. Yang`build.gradle.kts`akar adalah dokumentasi hidup penggunaan plugin. Yang`site.yml`di samping menunjukkan konfigurasi yang diharapkan.
Bandingkan dengan alternatif: sebuah README dengan tiga paragraf yang jelaskan bagaimana builder plugin dan bagaimana builder proyek dari uji. Seorang kontributor baru membaca README secara diagonal, salah, membuka sebuah isu — padahal informasi tersebut mungkin adalah dapat dieksekusi.
|
Documentation yang paling kuat bukan yang dibaca. Itu adalah yang kitaMenjalankan. Build akar adalah dokumentasi file eksekusi plugin. |
3. Builds terisolasi, CI mandiri
Plugin memiliki wrapper Gradle sendiri, siklus hidupnya sendiri, Tes-tes pribadinya. Anda dapat:
-
Mengupgrade Gradle di plugin tanpa menyentuh root
-
Menambahkan dependensi di dalam plugin tanpa bocor ke root
-
Menghancurkan plugin tanpa memengaruhi build dasar (selama Anda tidak
jangan publikasikan versi yang rusak)
-
Memiliki sebuah CI yang membangun dan menguji plugin, dan yang lain yang menjalankan
root — secara independen
.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
Anatomi dari Subdirektori `{name}-plugin/
Mari kita lihat lebih dekat apa yang ada di folder plugin :
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
Semua ada di sini. Tidak ada dispersi antara akar dan sub-direktori. Pengembang yang bekerja pada plugin tidak pernah membutuhkan meninggalkan`codebase-plugin/`. Pengembang yang menggunakan plugin hanya melihat akar — dan`build.gradle.kts`dari 3 baris Dia memberitahunya semua yang perlu diketahui.
Katalog Versi: Dua File Terpisah
|
|
|
Jumlah dependensi |
2-3 (plugin + readme opsional) |
30+ (langchain4j, pgvector, cucumber…) |
peran |
Mengonsumi plugin |
Bangun plugin |
Siapa yang membacanya? |
Pengguna plugin |
Pengembang plugin |
root memiliki katalog yang secara sengaja minimal. plugin memiliki katalog Lengkap. Kebingungan tidak mungkin: setiap build memiliki ruang lingkupnya sendiri. ketergantungan.
|
Jika Anda telah menghabiskan satu jam untuk men-debug tabrakan dependensi antara plugin Anda dan proyek pengujian Anda, Anda memahami nilai dari ini pemisahan. Katalog independen menghilangkan masalah ini dari konstruksi. |
Apa yang tidak kita lakukan
Pola ini bukan ajaib. Ia mengimpos sebuah batas yang saya mengakibatkan saya dengan sukarela:
Root tidak membangun plugin. Anda harus`publishToMavenLocal` ou men-deploy pada repositori sebelum root dapat menggunakannya
Ini adalah biaya kecil, dan ini adalah kontrain yang baik. Root Menggunakan plugin sebagai klien eksternal — melalui Maven. Tepat seperti yang dilakukan oleh proyek pihak ketiga. Jika plugin tidak dapat dipublikasikan, root memberitahu Anda itu segera.
# La seule "friction" du pattern
$ cd codebase-plugin && ./gradlew publishToMavenLocal && cd ..
$ ./gradlew tasks --group=codebase
Perbandingan: Tiga Arsitektur Terhadap Dogfooding
Plugin terisolasi |
repositori tunggal`include()` |
komposit`includeBuild()` |
Akar + plugin independen |
|
`git clone && gradlew tasks`memberikan tugas plugin |
❌ |
✅ |
✅ |
✅ |
Bangun plugin independen dari root |
✅ |
❌ |
✅ |
✅ |
Katalog versi terpisah |
✅ |
❌ |
✅ |
✅ |
Tidak`include()` ni |
✅ |
❌ |
❌ |
✅ |
Dogfood native tanpa konfigurasi |
❌ |
✅ |
Terjemahkan teks berikut dari Prancis ke Indonesia, dengan menjaga semua rentang kode yang dikutip dengan backtick ( |
✅ |
CI plugin independen |
✅ |
❌ |
✅ |
(Empty) |
Kolom kanan mencentang semua kotak. Itu sebabnya saya tidak Saya akan kembali lebih ke belakang.
Kontrak DAG: Build akar tidak pernah berasal dari subfolder
Arsitektur ini terintegrasi secara alami dalam DAG N0→N3 dari workspace saya. Pola adalah: * plugin N2 adalah hub dari ketergantungannya sendiri, akar N3 adalah sebuah terminal yang menerapkan hubs*
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11
title Kontrak DAG — Konsumen Root vs Plugin Independen
package "N3 — AKAR (setoran)" #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}-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
signing + publish
Tests unitaires + Cucumber
CI dédiée
end note
}
ROOT_TOML --> ROOT : "versi plugin"
ROOT -down-> PLUGIN : "Publikasikan ke repositori Maven lokal"
@enduml
Le codebase-gradle/build.gradle.kts`buat 6 baris. Tidak ada`src/, tidak ada`buildSrc/, tidak ada`gradle/rag-bench.gradle.kts. hanya plugins { alias(libs.plugins.codebase) }`dan repositori. Semua kompleksitas berada di`codebase-plugin/.
Conclusion : Duplikasi Tampak yang Menghemat Waktu
Saat saya menunjukkan struktur ini kepada seseorang, reaksi pertama sering : « Tapi kamu memiliki dua`gradlew`, dua`settings.gradle.kts`, dua`libs.versions.toml`— ini adalah duplikasi!
Ya. Dan tidak.
Duplikasi » berarti mereproduksi informasi yang sama di dua tempat. Di sini, ini adalah dua file distincts yang melayani dua kegunaan distincts : katalog plugin (30+ dependensi untuk builder) dan katalog dari akar (2-3 dependensi untuk dikonsumsi). wrapper plugin (versi terkunci untuk pengembangan) dan wrapper dari akar (versi yang berpotensi berbeda, untuk latihan plugin).
Bukan duplikasi. Ini pemisahan tanggung jawab diterapkan ke sistem build. Setiap build Melakukan satu hal, dan hanya satu. Akar mengkonsumsi. Plugin membangun.
Biaya? Satu pesanan`publishToMavenLocal`antara build plugin dan pembangunan akar. Keuntungan? Kejelasan arsitektur yang menghemat jam-jam waktu debugging di hilir
Sejak saya men-deploy pola ini di`bakery-gradle`, plantuml-gradle, codebase-gradle, dan plugins lain`foundry/public/, saya tidak punya Tidak pernah lagi ragu ketika membuka terminal di salah satu repositori saya. Yang refleks pertama —./gradlew tasks`— selalu berjalan, selalu memberi tugas yang baik, dan memberitahu saya secara instan jika semuanya sehat.
Ini dia, arsitektur yang baik. Dia tidak dapat dibaca dalam README. Dia diuji dalam terminal, dalam kurang dari sepuluh detik.
Referensi
-
Bagian 0105 — Mengintegrasikan Graphify dalam alur kerja Gradle
-
Artikel 0117 — Granularisasi OSS/CSS