وقت القراءة : 11 minutes

أنت تستنسخ مستودع مكوّن Gradle. تفتح محطة. ماذا تكتب؟ ثم?./gradlew tasks, بالطبع. لكن أي مجلد؟ الجذر؟ الوحدة الفرعية؟ هل يجب قراءة ملف README أولاً لمعرفة كيفية البناء؟ إذا كنت مترددًا حتى في ثانية واحدة، الهندسة المعمارية للمشروع معطلة.

هذا هو كيف حلت هذه المشكلة مرة واحدة وإلى الأبد — ولماذا هذا النمط هو اليوم توقيع جميع إضافات Gradle الخاصة بي في`foundry/public/`.

المشكلة: ثلاث معماريات أضاعت وقتي

قبل أن أتقارب إلى النمط الحالي، كنت أتلمس بين ثلاث مقاربات التقليدية لتنظيم إضافة Gradle. كل واحدة كانت تحتوي على عيب قاتل.

الخيار 1 : البرنامج المساعد المعزول (بدون dogfooding)

لا يحتوي المشروع سوى على المكون الإضافي. لا يوجد مثال للاستخدام. لا يوجد المشروع الذي يمارسه. لاختباره، يجب إنشاء مشروع خارجي، 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")`. الجذر والوحدة الفرعية يشتركان نفس daemon، والإعدادات نفسها، الكتالوجات نفسها. عملي، لكن مقترن.

.
├── 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`يتم مشاركته — كتالوجات الإصدار المشتركة، الاعتماديات

الذين يفرون من وحدة إلى أخرى.

  • لا يمكن بناء المكون الإضافي بشكل مستقل عن الجذر.

  • يجب أن يقوم CI ببناء الوحدتين، حتى لو تغير المكوّن الإضافي فقط.

الخيار 3 : البناء المركب (includeBuild())

نفصل بينهما في بناءات Gradle متميزة ونربطهما عبر includeBuild("mon-plugin")`في`settings.gradle.kts.

هذا أفضل الآن. بناء المكون الإضافي معزول. لكن المستهلك يجب الإشارة صراحةً إلى البناء الخارجي — ومخرجات `./gradlew tasks`في الجذر يعتمد على التكوين الصحيح لل مُركَّب. الاستنساخ ليس zero-config : يجب معرفة أن الملحق موجود في مجلد منفصل، بحيث يشير الجذر إليه، وما إلى ذلك.

.
├── 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 كامل ومستقل. لديه غلافه الخاص، وإعداداته الخاصة، وفهرسه الخاص من الإصدارات. يتم استنساخه وبناءه واختباره ونشره دون الحاجة إلى الجذر. أن يكون على علم بوجوده

الجذر، هو، لا يفعل سوى واحد شيء : تطبيق المكوّن الإضافي.

plugins {
    alias(libs.plugins.bakery)
}

repositories {
    mavenLocal()
    mavenCentral()
}

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

ثلاثة أسطر في حالة`bakery-gradle`. لا شيء آخر. صفر`include(), صفر`includeBuild(), صفر مشروع فرعي بناء Gradle كلاسيكي يطبق مكوّنًا مثل أي ما هو المستهلك الذي سيفعل ذلك؟

سير العمل

Failed to generate image: PlantUML preprocessing failed: [From <input> (line 32) ]

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12

left to right direction

package "جذر (مستهلك)" #CCFFCC {
    usecase "استنسخ المستودع" as Clone
    usecase "./gradlew tasks" as Tasks
    usecase "./gradlew اخبز" as Dogfood
    note bottom of Dogfood
        Exerce le plugin
        Feedback immédiat
        Zéro config
    end note
}

package "{name}-plugin/ (البناء المستقل)" #CCE5FF {
    usecase "./gradlew بناء" as BuildPlugin
    usecase "./gradlew publishToMavenLocal" as MavenLocal
    usecase "./gradlew اختبار" 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 : "يعتمد على المكوّن الإضافي
منشور محليًا"
^^^^^
 Syntax Error? (Assumed diagram type: component)

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12

left to right direction

package "جذر (مستهلك)" #CCFFCC {
    usecase "استنسخ المستودع" as Clone
    usecase "./gradlew tasks" as Tasks
    usecase "./gradlew اخبز" as Dogfood
    note bottom of Dogfood
        Exerce le plugin
        Feedback immédiat
        Zéro config
    end note
}

package "{name}-plugin/ (البناء المستقل)" #CCE5FF {
    usecase "./gradlew بناء" as BuildPlugin
    usecase "./gradlew publishToMavenLocal" as MavenLocal
    usecase "./gradlew اختبار" 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 : "يعتمد على المكوّن الإضافي
منشور محليًا"
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.

لماذا هذه العمارة أسرتني

ثلاثة فوائد، عند دمجها، تساوي تكلفة «التكرار» :

1. Dogfooding أصلي، ملاحظات فورية

أفضل اختبار لإضافة Gradle هو استخدامها. ليس اختبار وحدة مزيف. ليس`GradleRunner`مع مشروع من الاختبار. بناء حقيقي يطبق البرنامج المساعد على ملفات حقيقية.

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

إذا كان المكوّن الإضافي معطلاً، فإن البناء الجذري يخبر بذلك. لا حاجة للذهاب البحث عن مشروع اختبار خارجي. dogfooding هو الأولى مهمة يطلقها مساهم جديد. هذا هو اختبار الدخان النهائي

القاعدة بسيطة: إذا نجح تجميع الجذر و`./gradlew tasks` يعرض مهام البرنامج المساعد الخاص بك، البرنامج المساعد يعمل. لا مفاجآت في الإنتاج

2. نسخ بدون إعدادات

Un `git clone && ./gradlew tasks`والوافد الجديد يرى كل شيء امشي دون ضبط أي شيء. ال`build.gradle.kts`جذر هو التوثيق الحي لاستخدام البرنامج الإضافي. ال`site.yml`بجانب تُظهر التكوين المتوقع

قارن مع البديل: ملف README من ثلاث فقرات اشرح كيف نبني الملحق وكيف نبني المشروع من الاختبار. مساهم جديد يقرأ ملف README قطريًا، هو مخطئ، يفتح مسألة — بينما يمكن أن تكون المعلومات أن يكون قابلاً للتنفيذ.

أقوى توثيق ليس هو الذي يُقرأ هذه التيينفّذ. بناء الجذر هو الوثائق الملف القابل للتنفيذ للإضافة.

3. بنّاءات معزولة، CI مستقل

تحتوي الإضافة على ملف Gradle Wrapper الخاص بها، ودورة حياة خاصة بها, اختباراته الخاصة. يمكنك :

  • ترقية Gradle في الإضافة دون لمس الجذر

  • إضافة تبعية في المكوّن الإضافي دون أن تتسرب إلى الجذر

  • كسر الإضافة دون التأثير على بناء الجذر (طالما أنكم لا

لا تنشر النسخة المكسورة)

  • الحصول على CI التي تقوم بـ build/test للمكوّن، وأخرى تمارس le

الجذر — بشكل مستقل

.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/

لنلق نظرةً أقرب على ما يعيش في مجلد الإضافة :

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 أسطر يقول له كل ما يحتاج إلى معرفته.

كتالوج الإصدارات: ملفين منفصلين

gradle/libs.versions.toml(جذر)

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

عدد التبعيات

2-3 (إضافة + ملف readme اختيارياً)

30+ (langchain4j, pgvector, cucumber…​)

دور

استهلاك المكوّن الإضافي

ابن الإضافة

من يقرأه

مستخدم الإضافة

مطور الإضافة

الجذر يحتوي على كتالوج مقصود بحد أدنى. الملحق يحتوي على كتالوج مكتمل. لا يمكن أن يحدث الارتباك: كل بناء له نطاقه الخاص من التبعيات.

إذا قضيت بالفعل ساعة في تصحيح تصادم الاعتمادات بين إضافتك ومشروع الاختبار الخاص بك، أنت تفهم القيمة من هذا الانفصال. الكتالوجات المستقلة تزيل هذه المشكلة عن طريق البناء

ما لا يفعله

هذا النمط ليس سحريًا. يفرض قيدًا أنني يفرض عليّ طواعية :

الجذر لا يقوم ببناء الإضافة. يجب أن`publishToMavenLocal` ou نشر على مستودع قبل أن يتمكن الجذر من استهلاكه

إنه تكلفة ضئيلة، وهي القيود الجيدة. الجذر يستخدم المكوّن كعميل خارجي — عبر Maven. بالضبط كما سيقوم به مشروع طرف ثالث. إذا كانت الإضافة غير قابلة للنشر، الجذر يخبرك فوراً.

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

المقارنة: العمارات الثلاث في مواجهة Dogfooding

الإضافة المنفصلة

مستودع أحادي`include()`

مركب`includeBuild()`

جذر + إضافة مستقلة

`git clone && gradlew tasks`يقدم مهام البرنامج المساعد

❌

✅

✅

✅

بناء مكوّن إضافي مستقل من الجذر

�✅

❌

✅

✅

كتالوج الإصدارات مفصول

✅

�❌

�✅

�✅

لا`include()` ni includeBuild()

(Empty)

❌

�❌

�✅

Dogfood أصلي بدون تكوين

❌

✅

⚠️

(Empty)

إضافة CI مستقلة

✅

❌

�✅

✅

الجذر هو مثال على الاستهلاك الحقيقي

�❌

⚠️

✅

�✅

العمود الأيمن يحدد جميع المربعات. هذا هو السبب الذي لا سأرجع إلى الخلف أكثر

عقد DAG: لا يتم إنشاء بناء Racin أبدًا من دليل فرعي

يتم دمج هذا الهيكل بشكل طبيعي في DAG من N0 إلى N3 من مساحة العمل الخاصة بي. النمط هو : الملحق N2 هو المحور من تبعياتها الخاصة، الجذر N3 هو طرف الذي يطبق مراكز

contrat dag architecture

Le codebase-gradle/build.gradle.kts`يبلغ 6 أسطر. لا`src/, لا`buildSrc/, لا`gradle/rag-bench.gradle.kts. عادل plugins { alias(libs.plugins.codebase) }`والمستودعات. كل التعقيد يعيش في`codebase-plugin/.

Conclusion : الاستنساخ الظاهري الذي يوفر الوقت

عندما أُظهر هذا الهيكل لشخص ما، فإن رد الفعل الأول غالبًا ما يكون : « لكن لديك اثنين`gradlew`, اثنين`settings.gradle.kts`, اثنين`libs.versions.toml`— هذا تكرار !

نعم. ولا

تعني « duplication » إعادة إنتاج نفس المعلومات في مكانين. هنا, هذان الملفان مختلفان اللذان يستخدمان استخدمن مختلفين : كتالوج الملحق (30+ اعتماد للبنّاء) والكتالوج من الجذر (2-3 تبعيات لاستهلاكها). غلاف الإضافة (إصدار مقفل للتطوير) وغلاف الجذر (نسخة محتملة مختلفة، لتمارين الإضافة).

ليس هذا تكرارًا. إنه فصل المسؤوليات مطبّقة إلى نظام البناء. كل بناء يفعل شيئًا واحدًا فقط. الجذر يستهلك. الإضافة تُبنى.

التكلفة؟ طلب`publishToMavenLocal`entre بناء المكوّن وبناء الجذر. ما هو المكسب؟ وضوحًا معماريًا الذي يحذف ساعات من تصحيح الأخطاء في المراحل اللاحقة.

منذ أن نشرت هذا النمط على`bakery-gradle`, plantuml-gradle, codebase-gradle, والإضافات الأخرى من`foundry/public/, ليس لديّ لم أتردد أبداً عند فتح محطة في أحد مستودعاتي. ال الانعكاس الأول —./gradlew tasks`— دائمًا يمشي، دائمًا يعطي المهام الجيدة، ويخبرني فورًا إذا كان كل شيء سليمًا.

هذا هو، الهندسة المعمارية الجيدة. لا تُقرأ في ملف README. تُختبر في طرفية، بأقل من عشر ثوانٍ.

Articles connexes