स्वतंत्र प्लग‑इन + उपभोक्ता मूल आर्किटेक्चर: मेरे Gradle बिल्ड्स क्यों दोहराते हैं
प्रकाशित: 14 May 2026
- समस्या: तीन आर्किटेक्चर जिन्होंने मेरे समय को बर्बाद किया
- समाधान : दो स्वतंत्र बिल्ड्स, एक उपभोक्ता जड़
- क्यों इस आर्किटेक्चर ने मुझे जीत लिया
- एनाटॉमी उप-फ़ोल्डर का `{name}-plugin/
- हम जो नहीं करते
- तुलना : तीन वास्तुकलाएँ Dogfooding के सामने
- DAG कॉन्ट्रैक्ट : एक बिल्ड रूट कभी भी किसी उप-फ़ोल्डर से आयात नहीं किया जाता
- निष्कर्ष: प्रतीत होने वाली प्रतिलिपि जो समय बचाती है
- संदर्भों
आप Gradle प्लगइन भंडार को क्लोन कर रहे हैं। आप एक टर्मिनल खोल रहे हैं। आप क्या टाइप कर रहे हैं फिर?./gradlew tasks, ज़रूर. लेकिन किस फ़ोल्डर में? मूल? उपमॉड्यूल? क्या पहले README पढ़ना चाहिए ताकि पता लग सकें कि बिल्ड कैसे करें? यदि आप हिचकिचाते हैं तो यह नहीं होगा? एक सेकंड, परियोजना की वास्तुकला टूट गई है।
यहाँ मैंने इस समस्या को एक बार के लिए हल कर दिया — और क्यों यह पैटर्न आज मेरे सभी Gradle प्लगिनों का हस्ताक्षर है में`foundry/public/`.
समस्या: तीन आर्किटेक्चर जिन्होंने मेरे समय को बर्बाद किया
वर्तमान पैटर्न की ओर जाने से पहले, मैं तीन दृष्टिकोणों के बीच भटक गया। एक Gradle प्लगइन को व्यवस्थित करने के लिए पारंपरिक तरीके। प्रत्येक में एक घातक दोष था।
विकल्प 1 : अलग-थलग प्लगइन (डॉगफ़ूडिंग नहीं)
प्रोजेक्ट में केवल प्लग़िन है। उपयोग का कोई उदाहरण नहीं। कोई नहीं प्रोजेक्ट जो इसे अभ्यास कराता है। इसका परीक्षण करने के लिए, एक बाहरी प्रोजेक्ट बनाना आवश्यक है, y प्लगिन के माध्यम से सूचीबद्ध करें`mavenLocal`या एक संयुक्त बॉयल्ड, और केवल वहाँ जाँच करना कि यह काम करता है।
$ git clone mon-plugin
$ cd mon-plugin
$ ./gradlew build # le plugin compile
$ # ... et maintenant ? comment je l'essaie ?
|
एक Gradle प्लगिन बिना उपभोग के उदाहरण के, यह एक लाइब्रेरी बिना है एकीकरण परीक्षण। आप कभी नहीं जानते कि अंतिम संशोधन है उपयोगकर्ता अनुभव को तोड़ दिया। |
विकल्प 2 : क्लासिक मोनोरेपो (include(":plugin"))
Gradle`init`उत्पन्न करता है`settings.gradle.kts`के साथ`include("plugin")`. रूट और उप-मॉड्यूल एक ही डेमोन, समान कॉन्फ़िगरेशन साझा करते हैं, एक ही प्रकार के कैटलॉग। सुविधाजनक, लेकिन संयोजित।
.
├── settings.gradle.kts → include("plugin")
├── build.gradle.kts → plugins { id("mon-plugin") }
├── plugin/
│ └── build.gradle.kts → java-gradle-plugin
└── gradle/
└── libs.versions.toml
क्या अटक गया:
-
रूटचाहिएसब-मॉड्यूल के समान Gradle संस्करण रखना।
-
`libs.versions.toml`साझा किया गया है — सामान्य कैटलॉग संस्करण, निर्भरताएँ
जो एक मॉड्यूल से दूसरे मॉड्यूल में भाग रहे हैं।
-
प्लगइन को root के बिना स्वतंत्र रूप से बिल्ड करना असंभव है।
-
CI को दोनों मॉड्यूल्स को बिल्ड करना चाहिए, भले ही केवल प्लगइन बदल गया हो।
विकल्प 3 : कॉम्पोज़िट बिल्ड (includeBuild())
हम दोनों को अलग‑अलग Gradle बिल्ड में अलग करते हैं और फिर उन्हें के माध्यम से जोड़ते हैं includeBuild("mon-plugin")`में`settings.gradle.kts.
अब यह बेहतर है। प्लगइन का बिल्ड अलग-थलग है। लेकिन उपभोक्ता स्पष्ट रूप से बाह्य बिल्ड को संदर्भित करना चाहिए — और आउटपुट का `./gradlew tasks`रूट पर सही कॉन्फ़िगरेशन पर निर्भर है कॉम्पोज़िट। क्लोनिंग ज़ीरो-कॉन्फ़िगरेशन नहीं है : आपको जानना चाहिए कि प्लगिन एक अलग फ़ोल्डर में है, जो रूट इसे संदर्भित करता है, आदि।
.
├── settings.gradle.kts → includeBuild("plugin-build/")
├── build.gradle.kts → plugins { id("mon-plugin") }
└── plugin-build/
├── settings.gradle.kts
└── build.gradle.kts
मैं बेहतर की तलाश कर रहा था। बहुत बेहतर।
समाधान : दो स्वतंत्र बिल्ड्स, एक उपभोक्ता जड़
यहाँ वह पैटर्न है जिसे मैंने अंततः अपनाया :
.
├── 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 परियोजना संपूर्ण और स्वतंत्र है। उसके पास अपना wrapper, अपने settings, अपने catalogue का संस्करणों. यह खुद को क्लोन करता है, बनाया जाता है, परीक्षण किया जाता है, और प्रकाशित किया जाता है बिना रूट के वह उसकी मौजूदगी से अवगत हो |
रूट, वह, केवल एक काम करता है: प्लगइन लागू करना।
plugins {
alias(libs.plugins.bakery)
}
repositories {
mavenLocal()
mavenCentral()
}
bakery { configPath = file("site.yml").absolutePath }
तीन पंक्तियाँ के मामले में`bakery-gradle`. कुछ नहीं। शून्य`include(), शून्य`includeBuild(), शून्य उप-परियोजना। एक क्लासिक Gradle बिल्ड जो किसी भी प्लगइन की तरह एक प्लगइन लागू करता है कौन सा उपभोक्ता इसे करेगा?
कार्यप्रवाह
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12
left to right direction
package "RACINE (उपभोक्ता)" #CCFFCC {
usecase "रिपॉजिटरी क्लोन करें" as Clone
usecase "./gradlew कार्य" as Tasks
usecase "./gradlew बेक" as Dogfood
note bottom of Dogfood
प्लगइन का अभ्यास करें
त्वरित फीडबैक
ज़ीरो कॉन्फ़िग
end note
}
package "{name}-plugin/ (स्वतंत्र निर्माण)" #CCE5FF {
usecase "./gradlew निर्माण" as BuildPlugin
usecase "./gradlew publishToMavenLocal" as MavenLocal
usecase "./gradlew परीक्षण" as TestPlugin
note bottom of BuildPlugin
अलग जीवन चक्र
इकाई परीक्षण + Cucumber
समर्पित CI संभव
end note
}
Clone -down-> Tasks
Tasks -down-> Dogfood
Dogfood ..> MavenLocal : "निर्भर है प्लगिन\nप्रकाशित स्थानीय"
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.
क्यों इस आर्किटेक्चर ने मुझे जीत लिया
तीन लाभ जो, संयोजित होने पर, « डुप्लिकेशन » की लागत के बराबर हैं:
नेटिव डॉगफ़ूडिंग, तत्कालिक प्रतिक्रिया
एक Gradle प्लगइन का सबसे अच्छा परीक्षण है कि आप उसका उपयोग करें। एक मॉक यूनिट टेस्ट नहीं। एक नहीं`GradleRunner`एक परियोजना के साथ टेस्ट का। एक सच्चा बिल्ड जो वास्तविक फाइलों पर प्लगइन लागू करता है।
$ git clone bakery-gradle
$ cd bakery-gradle
$ ./gradlew bake # ← le plugin est exercé immédiatement
यदि प्लगिन टूटा हुआ है, तो रूट बिल्ड कहता है। जाने की आवश्यकता नहीं। एक बाहरी परीक्षण परियोजना खोजें। डॉगफूडिंग पहली है। एक नए योगदानकर्ता द्वारा शुरू किया गया कार्य। यह अंतिम स्मोक टेस्ट है।
|
नियम सरल है: यदि जड़ संकलित होती है और`./gradlew tasks` यह आपके प्लगइन के कार्यों को दिखाता है, प्लगइन कार्यात्मक है। कोई आश्चर्य नहीं। उत्पादन में। |
2. क्लोनिंग जीरो-कॉन्फ़िग
Un `git clone && ./gradlew tasks`और नवागंतुक सब कुछ देखता है कुछ भी कॉन्फ़िगर किए बिना चलना। Le`build.gradle.kts`जड़ है प्लगइन के उपयोग की जीवंत दस्तावेज़। ले`site.yml`बगल में यह अपेक्षित कॉन्फ़िगरेशन दिखाता है।
विकल्प के साथ तुलना करें: एक तीन पराग्राफ वाला README जो प्लगइन को कैसे बनाएं और प्रोजेक्ट को कैसे बनाएं का test. एक नया योगदानकर्ता README को विकर्ण में पढ़ता है, गलती करता है, एक इश्यू खोलता है — जबकि जानकारी हो सकती है होना निष्पादन योग्य।
|
सबसे मज़बूत दस्तावेज़ वही नहीं है जिसे हम पढ़ते हैं। यह वह है जो हमनिष्पादित. रूट बिल्ड दस्तावेज़ है प्लगिन का एक्सेक्यूटेबल |
3. अलगाव वाले बिल्ड, स्वतंत्र सीआई
प्लगइन का अपना Gradle wrapper है, उसका अपना जीवन चक्र है, अपने स्वयं के परीक्षण। आप कर सकते हैं :
-
प्लगिन में Gradle को अपग्रेड करें, रूट को छुए बिना
-
प्लगिन में एक निर्भरता जोड़ें, जिससे यह रूट में लीक न हो
-
प्लगिन को तोड़ना बिना रूट बिल्ड को प्रभावित किए (जब तक आप नहीं
टूटी हुई संस्करण प्रकाशित न करें)
-
एक CI जो प्लगइन को बिल्ड/टेस्ट करती है, और एक जो इसका अभ्यास करती है
root — स्वतंत्र रूप से
.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
एनाटॉमी उप-फ़ोल्डर का `{name}-plugin/
प्लगइन फ़ोल्डर में क्या स्थित है, उसेcloser से देखते हैं:
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
सब कुछ यहाँ है। मूल और उप-फ़ोल्डर के बीच कोई बिखराव नहीं। प्लगिन पर काम करने वाला डेवलपर कभी भी आवश्यकता नहीं रखता छोड़ना`codebase-plugin/`. प्लगिन का उपयोग करने वाला डेवलपर जड़ को ही देखता है — और`build.gradle.kts`3 पंक्तियों की वह उसे बताता है कि उसे जानने की जरूरत क्या है।
संस्करण कैटलॉग : दो अलग-अलग फ़ाइलें
|
|
|
निर्भरताओं की संख्या |
2-3 (प्लगिन + रीडमी वैकल्पिक) |
30+ (langchain4j, pgvector, cucumber…) |
भूमिका |
प्लगइन का उपयोग करें |
प्लगइन बनाएँ |
कौन इसे पढ़ता है |
प्लगिन का उपयोगकर्ता |
प्लगिन के डेवलपर |
रूट के पास जानबूझकर न्यूनतम सूची है। प्लगइन के पास सूची है पूरा. भ्रम असंभव है: हर बिल्ड का अपना दायरा है। निर्भरताओं का.
|
यदि आप पहले से ही एक घंटा एक निर्भरता संघर्ष को डिबग करने में बिता चुके हैं आपके प्लगइन और आपके परीक्षण प्रोजेक्ट के बीच, आप इसका मूल्य समझते हैं यह अलगाव। स्वतंत्र कैटलॉग इस समस्या को समाप्त करते हैं निर्माण द्वारा |
हम जो नहीं करते
यह पैटर्न जादुई नहीं है। यह एक प्रतिबंध लागू करता है जो मैं वह खुशी-खुशी मुझ पर थोपता/थोपती है :
रूट प्लगइन नहीं बनाता. आपको`publishToMavenLocal` ou रिपॉजिटरी पर तैनात करें, इससे पहले कि रूट इसे उपयोग कर सके।
यह एक न्यूनतम लागत है, और यह अच्छी बाध्यता है। जड़ प्लगिन externos क्लाइंट की तरह उपभोग करता है - Maven के माध्यम से। बिल्कुल जैसे एक तीसरे पक्षीय प्रोजेक्ट करेगा। यदि प्लगिन प्रकाशनयोग्य नहीं है, रूट आपको तुरंत बताता है।
# La seule "friction" du pattern
$ cd codebase-plugin && ./gradlew publishToMavenLocal && cd ..
$ ./gradlew tasks --group=codebase
तुलना : तीन वास्तुकलाएँ Dogfooding के सामने
पृथक प्लगइन |
Monorepo`include()` |
कॉम्पोज़िट`includeBuild()` |
रूट + स्वतंत्र प्लगइन। |
|
`git clone && gradlew tasks`प्लगइन के कार्य देता है |
❌ |
✅ |
✅ |
✅ |
रूट से स्वतंत्र प्लगइन बनाएँ |
✅ |
❌ |
✅ |
✅ |
कैटालॉग संस्करण अलग |
✅ |
❌ |
✅ |
✅ |
कोई नहीं`include()` ni |
✅ |
❌ |
❌ |
(Empty output) |
Dogfood नेटिव बिना कॉन्फ़िगरेशन |
❌ |
✅ |
⚠️ |
✅ |
CI प्लगइन स्वतंत्र |
✅ |
❌ |
✅ |
✅ |
रूट एक वास्तविक conso का उदाहरण है |
❌ |
⚠️ |
✅ |
✅ |
दाएँ कॉलम सभी बक्से चेक करता है। इसलिए मैं नहीं मैं पीछे और अधिक जाऊँगा।
DAG कॉन्ट्रैक्ट : एक बिल्ड रूट कभी भी किसी उप-फ़ोल्डर से आयात नहीं किया जाता
यह वास्तुकला DAG N0→N3 में स्वाभाविक रूप से एकीकृत होती है मेरे वर्कस्पेस से। पैटर्न है: प्लगिन N2 हब है अपनी निर्भरताओं से, जड़ N3 एक टर्मिनल है जो हब्स लागू करता है
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11
title अनुबंध DAG — मूल उपभोक्ता vs स्वतंत्र प्लगइन
package "N3 — RACINE (जमा)" #FFCCCC {
[build.gradle.kts (3 lignes)] as ROOT
[libs.versions.toml (minimal)] as ROOT_TOML
note right of ROOT
plugins { bakery }
bakery { configPath = ... }
कोई व्यावसायिक तर्क नहीं
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
इकाई परीक्षण + Cucumber
समर्पित सीआई
end note
}
ROOT_TOML --> ROOT : "वर्शन प्लगइन"
ROOT -down-> PLUGIN : "publishToMavenLocal"
@enduml
Le codebase-gradle/build.gradle.kts`किया 6 पंक्तियाँ. नहीं`src/, कोई नहीं`buildSrc/, नहीं`gradle/rag-bench.gradle.kts. सही plugins { alias(libs.plugins.codebase) }`और रिपॉज़िटरीज़. सारी जटिलता बसती है`codebase-plugin/.
निष्कर्ष: प्रतीत होने वाली प्रतिलिपि जो समय बचाती है
जब मैं किसी को इस संरचना दिखाता हूँ, तो पहला प्रतिक्रिया अक्सर : « लेकिन तुम्हारे पास दो`gradlew`, दो`settings.gradle.kts`, दो`libs.versions.toml`— यह डुप्लिकेशन है!
हाँ। और नहीं।
डुप्लिकेशन », का अर्थ है एक ही जानकारी को दो स्थानों पर दोहराना। यहाँ, ये दो विभिन्न फ़ाइलें जो दो विभिन्न उपयोगों के लिए हैं : प्लगइन कैटलॉग (बिल्डर के लिए 30+ निर्भरताएँ) और कैटलॉग जड़ से (2-3 निर्भरताएँ उपभोग करने के लिए). प्लगइन का रैपर (विकास के लिए लॉक की गई संस्करण) और रूट रैपर (संभावित रूप से भिन्न संस्करण, प्लगइन अभ्यास के लिए).
यह डुप्लीकेशन नहीं है। यह विभाजन का जिम्मेदारियों बिल्ड सिस्टम पर लागू की गई। प्रत्येक बिल्ड यह केवल एक काम करता है। जड़ संसाधन खप्त करती है। प्लगइन बनाया जाता है।
लागत क्या? एक आदेश`publishToMavenLocal`प्लगिन के बिल्ड के बीच और मूल का बिल्ड। लाभ? एक वास्तुशिल्प स्पष्टता जो डाउनस्ट्रीम डिबगिंग के घंटों को हटाता है।
जिसने मैंने इस पैटर्न को लागू किया है उसके बाद से`bakery-gradle`, plantuml-gradle, codebase-gradle, और अन्य प्लगिन का`foundry/public/, मेरे पास नहीं मैंने अपने किसी भी रिपॉज़िटरी में टर्मिनल खोलते समय कभी हिचकिचाया नहीं। Le पहला प्रतिक्रिया —./gradlew tasks`— हमेशा चलता रहता है, हमेशा देता रहता है अच्छे कार्य, और यह तुरंत बताता है कि सब कुछ स्वस्थ है।
ये ही सही वास्तुकला है। इसे किसी README में नहीं पढ़ा जा सकता। वह टर्मिनल में अनुभव करती है, दस सेकंड से कम समय में।