χρόνος ανάγνωσης : 11 minutes

Κλωνοποιήστε ένα αποθετήριο προσθήκης Gradle. Άνοιξτε ένα τερματικό. Τι πληκτρολογείτε; Επόμενο;`./gradlew tasks`, βεβαίως. Αλλά σε ποilon φάκελο ? Η ρίζα ? Η υποενότητα ? Απαιτείται πρώτα να διαβάσουμε ένα README για να μάθουμε πώς να κατασκευάσουμε; Αν διστάσετε, δεν θα…​ Μια δευτερόλεπτο, η αρχιτεκτονική του έργου είναι χαμένη.

Αυτό είναι πώς έλυσα αυτό το πρόβλημα μια για πάντα — και γιατί αυτό το pattern είναι σήμερα η υπογραφή όλων των plugins Gradle μου σε`foundry/public/`.

Το πρόβλημα: Τρεις αρχιτεκτονικές που μου έκαναν να χάσω χρόνο

Πριν από τη σύγκλιση στον τρέχοντα μοτίβο, έχω δοκιμάσει μεταξύ τριών προσεγγίσεων. κλασικοί τρόποι για να οργανώσετε ένα πρόσθετο Gradle. Κάθεμία είχε έναν αδύνατο έλλιπτος.

Επιλογή 1 : Το Απομόνωμενο Πλαγκιν (χωρίς dogfooding)

Το έργο περιέχει μόνο το πλαγκιντ. Δεν υπάρχει παράδειγμα κατανάλωσης. Δεν έργο που το ασκεί. Για να το δοκιμάσουμε, πρέπει να δημιουργήσουμε ένα εξωτερικό έργο, y αναφορά του plugin μέσω`mavenLocal`ή ένα συνθέτο build, και μόνο εδώ ελέγξτε ότι λειτουργεί

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

Ένα πρόσθετο Gradle χωρίς παράδειγμα χρήσης, είναι μια βιβλιοθήκη χωρίς δοκιμές ενσωμάτωσης. Ποτέ δεν ξέρεις αν η τελευταία αλλαγή έχει σπάσει την εμπειρία του χρήστη.

Επιλογή 2 : Το κλασικό Monorepo (include(":plugin"))

Gradle`init`génère → Actually we need Greek: " γεννάει". Let’s output exactly that.

</think>

γεννάει`settings.gradle.kts`με`include("plugin")`. Ο root και το υπομέρος μοιράζονται τον ίδιο daemon και τις ίδιες ρυθμίσεις, τα ίδια κατάλογοι. Πρακτικό, αλλά συνδεδεμένο.

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

Το που είναι κολλημένο:

  • Ο rootπρέπεινα έχει την ίδια έκδοση του Gradle με το υπο-μέρος.

  • `libs.versions.toml`είναι κοινό — κοινοί καταλόγοι εκδόσεων, εξαρτήσεις

οι οποίοι φεύγουν από ένα module σε ένα άλλο.

  • Δεν είναι δυνατόν να χτίσετε το plugin ανεξάρτητα από το root.

  • Η CI πρέπει να χτίζει τα δύο modules, ακόμα κι αν μόνο το plugin έχει αλλάξει.

Επιλογή 3 : Η σύνθετη κατασκευή (includeBuild())

Χωρίζουμε τα δύο σε ξεχωριστά builds Gradle και τα συνδέουμε μέσω includeBuild("mon-plugin")`σε`settings.gradle.kts.

Αυτό είναι ήδη καλύτερο. Η κατασκευή του plugin είναι απομονωμένη. Αλλά ο καταναλωτής πρέπει να αναφέρεται ρηττά το εξωτερικό build — και την έξοδο του `./gradlew tasks`εξαρτάται από τη σωστή ρύθμιση της ρίζας συστατικό. Το κλώνημα δεν είναι zero-config: πρέπει να ξέρετε ότι το το plugin είναι σε ξεχωριστό φάκελο, που το root το αναφορά, κ.λπ.

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

ψάχνα καλύτερο. Πολύ καλύτερο.

Η Λύση : Δύο ανεξάρτητες Builds, Μια ρίζα καταναλωτή

Εδώ είναι το pattern που τελικά υιοθέτησα:

.
├── 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/` est un projet Gradle complet et autonome. Έχει το δικό του καπάκι, τις δικές του ρυθμίσεις, το δικό του κατάλογο του Εκδόσεις. Αντιγράφεται, κατασκευάζεται, δοκιμάζεται και δημοσιεύεται χωρίς τον root. να είναι ενημερωμένος/η για την ύπαρξή του/της

Η ρίζα, αυτή, κάνει μόνο ένα πράγμα: να εφαρμόσει το plugin.

plugins {
    alias(libs.plugins.bakery)
}

repositories {
    mavenLocal()
    mavenCentral()
}

bakery { configPath = file("site.yml").absolutePath }
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12

left to right direction

package "ΡΙΖΑ (Καταναλωτής)" #CCFFCC {
    usecase "Κλωνοποιήστε το αποθετήριο" as Clone
    usecase "./gradlew tasks" as Tasks
    usecase "./gradlew bake" as Dogfood
    note bottom of Dogfood
Εκτέλεσε το plugin
Άμεση ανατροφοδότηση
Μηδενική ρύθμιση
    end note
}

package "{name}-plugin/ (Ανεξάρτητο Build)" #CCE5FF {
    usecase "./gradlew build" as BuildPlugin
    usecase "./gradlew publishToMavenLocal" as MavenLocal
    usecase "./gradlew test" as TestPlugin
    note bottom of BuildPlugin
Απομονωμένος κύκλος ζωής
Μονάδες ελέγχου (Unit tests) + Cucumber
Δυνατότητα ייShapesdedicated CI
    end note
}

Clone -down-> Tasks
Tasks -down-> Dogfood
Dogfood ..> MavenLocal : "εξαρτάται από το plugin\nπου δημοσιεύθηκε τοπικά"
BuildPlugin -up-> MavenLocal
TestPlugin -up-> BuildPlugin

@enduml

Η ροή εργασίας

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12

left to right direction

package "RACINE (Κατανάλωτης)" #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 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 : "εξαρτάται από το plugin\nΔημοσιεύτηκε τοπικά"
BuildPlugin -up-> MavenLocal
TestPlugin -up-> BuildPlugin

@enduml

Ο συγκεκριμένος 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.

Γιατί αυτή η αρχιτεκτονική μου κατέλαβε;

Τρεις οφέλη που, συνδυασμένοι, αξίζουν το κόστος της «διπλοποίησης» :

1. Εγγενής Dogfooding, άμεσος Feedback

Ο καλύτερος έλεγχος ενός plugin Gradle είναι να το χρησιμοποιήσετε. Δεν είναι ένας mocked unit test. Δεν είναι`GradleRunner`με ένα έργο του test. Ένας αληθινός build ο οποίος εφαρμόζει το plugin σε αληθινά αρχεία.

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

Αν το plugin είναι σπασμένο, το root build το λέει. Δεν χρειάζεται να πάτε ψάχνε για ένα εξωτερικό έργο δοκιμής. Το dogfooding είναι το πρώτο Η εργασία που εκκινεί ένας νέος συνεισφέρον. Αυτό είναι ο τελικός smoke test.

Ο κανόνας είναι απλός: αν η ρίζα μεταγλωττίζει και`./gradlew tasks` Εμφανίζει τις εργασίες του plugin, το plugin είναι λειτουργικό. Χωρίς έκπληξη εν παραγωγή.

2. Κλωνοποίηση χωρίς ρύθμιση

Un `git clone && ./gradlew tasks`και ο πρόσφατα φθάσας βλέπει όλο περπατώ χωρίς να ρυθμίσω κάτι. ο`build.gradle.kts`η ρίζα είναι το ζωντανό έγγραφο χρήσης του plugin. Το`site.yml`παρά Εμφαίνει την αναμενόμενη ρύθμιση.

Συγκρίνετε με το εναλλακτικό : ένα README τριών παραγράφων που Εξηγεί πώς builder το plugin και πώς builder το έργο από δοκιμή. Ένας νέος συνεισφέρον διαβάζει το README σε διάγώνιο, κάνει λάθος, ανοίγει ένα issue — ενώ η πληροφορία θα μπορούσε να είναι executable.

Η πιο αξιόπιστη τεκμηρίωση δεν είναι αυτή που διαβάζεται. Αυτό είναι αυτή πουΕκτέλε. Η ρίζα του build είναι η τεκμηρίωση εκτελέσιμο του plugin.

3. Απομονωμένες κατασκευές, ανεξάρτητο CI

Το plugin έχει το δικό του Gradle wrapper, το δικό του κύκλο ζωής, τα δικά του τεστ. Μπορείτε:

  • Αναβαθμίστε το Gradle στο πρόσθετο χωρίς να αγγίξετε τη ρίζα

  • Προσθέστε μια εξάρτηση στο πρόσθετο χωρίς να διαρρεύει στον ριζικό

  • Σπάτε το plugin χωρίς να επηρεάξετε το ριζικό build (όσο μη

Μη δημοσιεύετε τη σπασμένη έκδοση)

  • Να έχετε μια CI που κάνει build/test το plugin, και μια άλλη που ασκεί το plugin

ριζα — ανεξαρτήτως

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

Ας δούμε πιο κοντά το τι ζει στον φάκελο του 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

Όλα είναι εδώ. Δεν υπάρχει διάσπορα μεταξύ της ρίζας και του υποκαταλόγου. Ο προγραμματιστής που εργάζεται στο plugin δεν χρειάζεται ποτέ να φεύγω`codebase-plugin/`. Ο προγραμματιστής που χρησιμοποιεί το πρόσθετο κοιτάει μόνο τη ρίζα — και το`build.gradle.kts`3 γραμμών αυτός του λέει ό,τι χρειάζεται να γνωρίζει.

Κατάλογος εκδόσεων: Δύο διαφορετικά αρχεία

gradle/libs.versions.toml(ρίζα)

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

Αριθμός εξαρτήσεων

2-3 (plugin + readme προαιρετικά)

30+ (langchain4j, pgvector, cucumber…​)

ρόλος

Κατανάλωση του plugin

Κτίσε το plugin

Ποιος το διαβάζει

Ο χρήστης του plugin

Ο προγραμματιστής του plugin

Το root έχει ένα κατάλογο εθελοντικά ελαχιστό. Το plugin έχει ένα κατάλογο ολοκληρωμένο. Η σύγχυση είναι αδύνατη: κάθε build έχει το δικό του scope εξαρτήσεων.

Αν έχετε ήδη περάσει μια ώρα να αποσφαλματώνετε μια σύγκρουση εξαρτήσεων μεταξύ του πρόσθετου σας και του δοκιμαστικού σας έργου, καταλαβαίνετε την αξία του Αυτή η διάσπαση. Τα ανεξάρτητα κατάλογοι εξαλείφουν αυτό το πρόβλημα. κατὰ κατασκευή.

Τι δεν κάνουμε

Αυτό το pattern δεν είναι μαγικό. Επιβάλλει ένα περιορισμό που εγώ Μου το επιβάλλει ευχάριστα:

Ο root δεν χτίζει το plugin. Πρέπει να`publishToMavenLocal` ou να αναπτύξετε σε ένα αποθετήριο πριν ο root να το καταναλώσει

Είναι ένα ελάχιστο κόστος, και είναι ο καλός περιορισμός. Η ρίζα Καταναλώνει το plugin ως εξωτερικό πελάτη — μέσω Maven. Ακριβώς. όπως θα έκανε ένα project τρίτο. Αν το plugin δεν είναι διαξιφημιστικό, ο root σου το λέει αμέσως.

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

Σύγκριση : Οι Τρεις Αρχιτεκτονικές εναντίον του Dogfooding

Plugin απομονωμένο

Μονόαποθήκη`include()`

σύνθετο`includeBuild()`

Ρίζα + ανεξάρτητο πρόσθετο.

`git clone && gradlew tasks`δίνει τις εργασίες του plugin

❌

✅

✅

Παρακαλώ παρέχουμε το κείμενο στα γαλλικά για μετάφραση. Δε λάβαμε κείμενο για μεταγλώττιση.

Κατασκευή προσθέτου ανεξάρτητο του root

✅

❌

✅

✅

Κατάλογος εκδόσεις χωριστό

✅

❌

✅

✅

Δεν`include()` ni includeBuild()

✅

❌

❌

✅

Εγγενές dogfood χωρίς ρύθμιση

❌

</think>

⚠️

✅

πλάگین CI ανεξάρτητο

✅

❌

[No output - empty string]

✅

Η ρίζα είναι ένα παράδειγμα της πραγματικής κατανάλωσης.

❌

⚠️

✅

✅

Η στήλη δεξία σημειώνει όλα τα πλαίσια. Είναι για αυτό που δεν Θα επιστρέψω πιο πίσω.

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11

title Συμβόλαιο DAG — Ρίζα Καταναλωτή έναντι Ανεξάρτητου Plugin
package "N3 — ΡΙΖΑ (αποθετήριο)" #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}-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
υπογραφή + δημοσίευση
Μονάδες ελέγχου (Unit Tests) + Cucumber
Αφιερωμένο CI
    end note
}

ROOT_TOML --> ROOT : "έκδοση plugin"
ROOT -down-> PLUGIN : "publishToMavenLocal"
@enduml

Αυτή η αρχιτεκτονική ενσωματώνεται φυσικά στον DAG N0→N3 από το χώρο εργασίας μου. Το μοτίβο είναι : το plugin N2 είναι ο κέντρο από τις δικές του εξαρτήσεις, η ρίζα N3 είναι ένα τερματικό που εφαρμόζει τους hubs*

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11

title Σύμβαση DAG — Ρίζα καταναλωτή vs Ανεξάρτητο πρόσθετο
package "N3 — ΡΙΖΑ (αποθήκη)" #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 : "έκδοση προσθέτου"
ROOT -down-> PLUGIN : "Δημοσίευση στο Maven Local"
@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`— είναι διπλοποίηση !

Ναι. Και όχι.

Η « διπλοποίηση » είναι η δημιουργία ενός αντίγραφο της ίδιας πληροφορίας σε δύο τοποθεσίες. Εδώ, είναι δύο αρχεία διακριτά που εξυπηρετούν δύο διακριτές χρήσεις: το κατάλογο του plugin (30+ εξαρτήσεις για τον builder) και το κατάλογο της ρίζας (2-3 εξαρτήσεις για κατανάλωση). Το wrapper του plugin (κλειδωμένη έκδοση για την ανάπτυξη) και το wrapper της ρίζας (πιθανώς διαφορετική έκδοση, για άσκηση του plugin).

Αυτό δεν είναι διπλοποίηση. Είναι διαχωρισμού του υπευθυνότητες εφαρμοσμένη στο σύστημα build. Κάθε build Κάνει ένα πράγμα, και μόνο ένα. Η ρίζα καταναλώνει. Το πρόσθετο χτίζεται.

Το κόστος; Μια παραγγελία`publishToMavenLocal`μεσα στο build του plugin και το build της ρίζας. Το κέρδος; Μια αρχιτεκτονική σαφήνεια που διαγράφει ώρες αποσφαλμάτωσης στα επόμενα βήματα.

Αφού υλοποίησα αυτό το μοτίβο στο`bakery-gradle`, plantuml-gradle, codebase-gradle, και τα άλλα plugins του`foundry/public/, δεν έχω πιο ποτέ δεν έχω διστάσει όταν ανοίγω ένα τερματικό σε ένα από τα αποθετήριά μου. Ο πρώτος αντανακλαστικός —./gradlew tasks`— περιπατησε παντα, δωσε παντα τα σωστά εργασιές, και μου λέει αμέσως αν όλα είναι καλά

Αυτό είναι η σωστή αρχιτεκτονική. Δεν διαβάζεται στο README. Δοκιμάζεται σε ένα τερματικό, σε λιγότερο από δέκα δεuterόλεπτα.

Σχετικά άρθρα