阅读时间:11 minutes

您克隆了一个 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行 告诉他他需要知道的一切。

版本目录 : 两个不同的文件

gradle/libs.versions.toml(根)

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

依赖数量

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 includeBuild()

✅

❌

❌

✅

原生 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 中读到。 它可以在终端中十秒内得到验证。

相关文章