L'Architecture « 插件独立+消费者根 » : 为什么我的Gradle构建会重复?
发布于 14 May 2026
您克隆了一个 Gradle 插件仓库。您打开一个终端。您在输入什么? 然后?./gradlew tasks, 当然。但请问是哪个目录?根目录?还是子模块? 是否应该先阅读 README 以了解如何构建?如果你犹豫的话 仅仅一秒,项目的架构就坏了。
这是我一次性解决这个问题的方法 — 以及为什么这 pattern 今天是我所有 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:经典的 Monorepo (include(":plugin"))
Gradle`init`生成`settings.gradle.kts`与`include("plugin")`。 root 和子模块共享相同的守护进程和相同的配置, 同样的目录。 实用,但已耦合。
.
├── 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`位于根部取决于正确的配置 复合的。克隆不是零配置:必须知道该 插件位于单独的文件夹中,根目录引用了它,等等。
.
├── 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 项目。 他有自己的包装器,自己的设置,自己的目录的 versions. 它会自行克隆、编构、测试,并在不需要 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 任务" 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.
为什么这种架构征服了我
这三个好处合在一起,值得付出“复制”的成本:
1. Dogfooding 原生, Feedback 即时
测试 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, 搞错了,打开了一个 issue — 而实际上信息可能 可执行的
|
最强大的文档不是我们阅读的那份。 这是那个我们执行. 根构建是文档 插件的可执行文件。 |
3. 孤立的构建,独立的 CI
插件具有自己的 Gradle 包装器,自己的生命周期, 它自己的测试。您可以:
-
在插件中升级 Gradle,而不修改根项目
-
在插件中添加依赖,而不让其泄漏到根目录
-
在不影响根构建的情况下破坏插件 (只要你不
不要发布损坏的版本)
-
拥有一个构建/测试插件的 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`3行 告诉他他需要知道的一切。
版本目录 : 两个不同的文件
|
|
|
依赖数量 |
2-3 (plugin + readme 可选) |
30+ (langchain4j, pgvector, cucumber…) |
角色 |
使用插件 |
构建插件 |
谁在读它 |
插件用户 |
插件开发者 |
根用户有一个故意最小的目录。插件有一个目录。 完整。混淆是不可能的:每个构建都有其自己的作用域 依赖。
|
如果您已经花了一小时来调试依赖冲突 在您的插件和测试项目之间,您理解其价值 这种分离。独立的目录消除了这个问题。 由构造。 |
我们不做的
这个模式并不是魔法的。它施加了一种约束,我 他/她乐意对我施加:
Root 不 构建 插件。 你必须`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`给出插件的任务 |
❌ |
✅ |
✅ |
✅ |
构建独立于根的插件 |
✅ |
❌ |
✅ |
您是专业翻译员。请将法语翻译成中文。 保持所有反引号代码跨度( |
目录版本分离 |
✅ |
❌ |
✅ |
✅ |
没有`include()` ni |
✅ |
❌ |
❌ |
✅ |
原生 Dogfood 无需配置 |
❌ |
✅ |
⚠️ |
[No output - empty string] |
独立的 CI 插件 |
✅ |
❌ |
✅ |
✅ |
根是实际消耗的一个例子 |
❌ |
⚠️ |
✅ |
✅ |
右侧的列勾选了所有复选框。这就是我没有 我会回得更远。
DAG 合约:根构建永不来自子目录
此架构自然地融入到DAG N0→N3中 我的工作区。模式是:le plugin 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
签名 + 发布
单元测试 + Cucumber
专用 CI
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 中读到。 它可以在终端中十秒内得到验证。
相关文章
31 May 2026
12 May 2026