Время чтения: 11 minutes

Вы клонируете репозиторий плагина Gradle. Вы открываете терминал. Что вы вводите потом ?./gradlew tasks, конечно. Но в какой папке? Корень? Подмодуль? Нужно ли сначала прочитать README, чтобы узнать, как собрать ? Если вы сомневаетесь, не будет ли в одну секунду, архитектура проекта сломана.

Вот как я решил эту проблему раз и навсегда — и почему эта pattern сегодня является сигнатурой всех моих плагинов Gradle в`foundry/public/`.

Проблема: Три архитектуры, которые заставили меня потерять время

Прежде чем сойтись к текущему паттерну, я перебирал три подхода Классические способы организации плагина Gradle. У каждого был фатальный недостаток.

Опция 1: Изолированный плагин (без dogfooding)

В проекте содержится только плагин. Нет примера использования. Нет проект, который он осуществляет. Чтобы его протестировать, нужно создать внешний проект, y указать плагин через`mavenLocal`ou композитная сборка, и только там проверить, что он работает

$ 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`est partagé — общие каталоги версий, зависимости

которые бегут от одного модуля к другому.

  • Невозможно собрать плагин независимо от root.

  • CI должна собрать оба модуля, даже если изменился только плагин.

Вариант 3: Композитная сборка (includeBuild())

Мы разделяем их на отдельные сборки Gradle и связываем их через includeBuild("mon-plugin")`в`settings.gradle.kts.

Это уже лучше. Сборка плагина изолирована. Но потребитель должен явно ссылаться на внешнюю сборку — и вывод из `./gradlew tasks`в корне зависит от правильной конфигурации Клонирование не является zero-config: нужно знать, что le плагин находится в отдельной папке, root на него ссылается, и т.д.

.
├── 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, свой собственный каталог версии. Он клонируется, собирается, тестируется и публикуется без прав root будь в курсе его существования

Корень, он, делает лишь одну вещь: применяет плагин.

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 tasks`" as Tasks
    usecase "`./gradlew bake`" as Dogfood
    note bottom of Dogfood
Запустите плагин
Мгновенная обратная связь
Ноль конфигураций
    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
Изолированный жизненный цикл
Модульные тесты + 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.

Почему эта архитектура покорила меня

Три преимущества, которые, объединенные, оправдывают затраты на «дублирование» :

1. Нативное dogfooding, мгновенная обратная связь

Лучший тест плагина Gradle — это использовать его. Это не мок-юнит-тест. Не`GradleRunner`с проектом Один настоящий build, который применяет плагин к настоящим файлам.

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

Если плагин сломан, корневая сборка об этом сообщает. Нет необходимости идти. Искать внешний тестовый проект. Dogfooding это первое Задача, которую запускает новый участник. Это окончательный smoke test.

Правило простое: если корень компилируется и`./gradlew tasks` Отображает ваши задачи плагина, плагин функционален. Никаких сюрпризов в эксплуатации.

2. Клонирование Zero-Config

Un `git clone && ./gradlew tasks`и новичок видит всё ходить без какой-либо настройки. Ле`build.gradle.kts`корень есть живая документация по использованию плагина.`site.yml`рядом показывает ожидаемую конфигурацию

Сравните с альтернативой: README из трёх абзацев, который объясни как построить плагин и как построить проект де тест. Новый участник читает README по диагонали, ошибается, открывает issue — хотя информация могла бы быть исполняемым

Самая надежная документация — это не та, которую читают. Это та которую мывыполняет. Корневая сборка — это документация исполняемый файл плагина

3. Изолированные сборки, независимые CI

У плагина есть свой собственный Gradle‑обёртка, свой собственный жизненный цикл, свои тесты. Вы можете:

  • Обновить Gradle в плагине без изменения корня

  • Добавить зависимость в плагин без её утечки в корень

  • Сломать плагин, не влияя на корневой билд (пока вы не

Не публикуйте сломанную версию)

  • Иметь CI, которая собирает/тестирует плагин, и другую CI, которая упражняет его

корень — независимо

.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`из трёх строк Он говорит ему всё, что ему нужно знать.

Каталог версий: два отдельных файла

gradle/libs.versions.toml(корень)

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

Количество зависимостей

2-3 (плагин + readme опционально)

30+ (langchain4j, pgvector, cucumber…​)

Роль

Потреблять плагин

Сборка плагина

Кто это читает?

Пользователь плагина

Разработчик плагина

Корень имеет преднамеренно минимальный каталог. Плагин имеет каталог. Полностью. Смешивание невозможно: каждая сборка имеет свой собственный scope зависимостей.

Если вы уже потратили час на отладку конфликта зависимостей между вашим плагином и вашим тестовым проектом, вы понимаете ценность Это разделение. Независимые каталоги устраняют эту проблему по построению

Что мы не делаем

Этот паттерн не волшебный. Он накладывает ограничение, которое я Он с удовольствием накладывает на меня :

Корень не собирает плагин. Вы должны`publishToMavenLocal` ou развернуть в репозитории до того, как root сможет его потребить.

Это минимальная стоимость, и это правильное ограничение. Корень потребляет плагин как внешний клиент — через Maven. Точно как бы это сделал сторонний проект. Если плагин не подлежит публикации, root сразу вам говорит.

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

Сравнение: три архитектуры против догфудинга

Изолированный плагин

монобэпо`include()`

композит`includeBuild()`

Корень + независимый плагин.

`git clone && gradlew tasks`дает задачи плагина

❌

✅

✅

✅

Создать плагин, независимый от root

✅

(Empty output)

(empty)

✅

Каталог отдельных версий

✅

❌

✅

✅

Нет`include()` ni includeBuild()

❌

❌

✅

Dogfood нативный без конфигурации

❌

✅

⚠️

✅

независимый плагин CI

✅

❌

✅

✅

Root — это пример реального потребления

❌

⚠️

✅

✅

Правая колонка отмечает все флажки. Это потому что я не Я вернусь ещё дальше назад.

Контракт DAG : Сборка корня никогда не будет из подпапки

Эта архитектура естественно интегрируется в DAG N0→N3 из моего workspace. Шаблон: плагин N2 — это центр Из его собственных зависимостей, корень N3 является терминалом. который применяет хабы.

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11

title Контракт DAG — Корень-Потребитель против Независимого Плагина
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
подпись + публикация
Юнит-тесты + Cucumber
выделенный CI
    end note
}

ROOT_TOML --> ROOT : "version plugin"
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+ зависимостей для builder) и каталог из корня (2-3 зависимости для потребления). Обёртка плагина (версия заблокирована для разработки) и оболочка корня (версия, потенциально отличающаяся, для упражнения плагина).

Это не дублирование. Это разделение обязанности применённая к системе сборки. Каждая сборка Он делает одну вещь, и только одну. Корень потребляет. Плагин собирается.

Стоимость? Заказ`publishToMavenLocal`между сборкой плагина и сборка корня. Выгода? Архитектурная ясность которой устраняет часы отладки вниз по потоку

С тех пор, как я развернул этот паттерн на`bakery-gradle`, plantuml-gradle, codebase-gradle, и другие плагины de`foundry/public/, у меня нет Я больше никогда не колебался, открывая терминал в одном из моих репозиториев. Ле Первый рефлекс —./gradlew tasks`— всегда идёт, всегда даёт хорошие задачи, и мгновенно говорит мне, всё ли в порядке.

Это и есть хорошая архитектура. Её нельзя прочитать в README. Она проверяется в терминале, менее чем за десять секунд.

Похожие статьи