{"code":1,"msg":"成功","time":"1789816248","data":{"title":"用 Git Submodule + Sparse Checkout 管理 RT-Thread：内核、BSP、第三方库与业务代码分层实践","content":"# 用 Git Submodule + Sparse Checkout 管理 RT-Thread：内核、BSP、第三方库与业务代码分层实践\r\n\r\n> 摘要：用 Submodule 锁定 RT-Thread 与第三方依赖版本，用 Sparse Checkout 精简工作区，并保持业务代码、构建配置和上游同步边界清晰。\r\n\r\n\r\n![screenshot_image.png](https:\/\/oss-club.rt-thread.org\/uploads\/20260917\/6fd1190f67dae604b2ce6a6ac951a3c7.png.webp)\r\n\r\n@[toc]\r\n嵌入式固件项目维护时间一长，仓库很容易逐渐变成“一份大拷贝”：RTOS 内核、芯片厂商库、BSP、第三方组件和产品业务代码全部堆在一起。短期看最省事，长期却会暴露几个典型问题：依赖来源难追踪、升级时 diff 巨大、多个项目重复保存相同代码、业务代码和基础设施边界越来越模糊。\r\n\r\n更适合长期维护的做法，不是简单把目录删掉或拆得越碎越好，而是把三个问题分别交给三个机制处理：\r\n\r\n- **Git Submodule**：这个依赖来自哪个仓库，产品当前锁定哪个 commit；\r\n- **Git Sparse Checkout**：一个已经存在的仓库，在当前工作区实际展开哪些目录；\r\n- **Kconfig \/ `rtconfig.h` \/ SCons \/ IDE 工程**：哪些功能和源文件最终参与构建。\r\n\r\n这三个机制分别控制“版本边界”“工作区边界”和“构建边界”。只有先把它们分开，RT-Thread、BSP、第三方库和产品代码的关系才不会混在一起。\r\n\r\n## 先建立正确的心智模型：版本、工作树和构建是三件事\r\n\r\nGit Submodule 和 Sparse Checkout 经常同时出现，但它们解决的问题完全不同。\r\n\r\n| 机制 | 主要解决的问题 | 是否改变上游仓库内容 | 是否锁定依赖版本 |\r\n| --- | --- | --- | --- |\r\n| Git Submodule | 依赖来自哪里，主仓库引用哪个提交 | 否 | 是 |\r\n| Git Sparse Checkout | 当前工作树展开哪些已跟踪路径 | 否 | 否 |\r\n| Kconfig \/ SCons \/ IDE | 哪些功能和源文件参与构建 | 不涉及 | 不涉及 |\r\n\r\nSubmodule 在超级项目（superproject）中通过 **gitlink** 记录子模块应当处于哪个 commit。`.gitmodules` 则保存子模块路径、URL，以及可选的默认跟踪分支等信息。普通 `git submodule update` 的目标是恢复超级项目记录的那个 commit；只有显式使用 `--remote` 时，才会根据子模块远端跟踪分支寻找新的目标提交。\r\n\r\nSparse Checkout 的作用则是把工作树缩减为已跟踪文件的一个子集。它不会生成一份“裁剪后的 RT-Thread 仓库”，也不会改写 commit 历史。被隐藏的目录仍然属于当前 RT-Thread revision，只是不出现在本地工作树中。\r\n\r\n\r\n![screenshot_image.png](https:\/\/oss-club.rt-thread.org\/uploads\/20260917\/0b03f78e31d8a679114b00cacb904b42.png.webp)\r\n\r\n\r\n图中最重要的是三条边界：\r\n\r\n1. 主仓库通过 gitlink 锁定 RT-Thread、littlefs 或团队公共模块的精确 commit；\r\n2. RT-Thread 子模块仍然是完整 Git 仓库，只是在工作区按需展开 `src`、`include`、驱动、CPU 端口和当前 BSP 所需目录；\r\n3. 最后由 Kconfig、`rtconfig.h`、SCons 或 IDE 工程决定真正参与编译的源文件。\r\n\r\n因此，Sparse Checkout 不能代替 Kconfig，Kconfig 也不能代替 Submodule。三者职责不同。\r\n\r\n## 主仓库首先要明确“谁拥有这段代码”\r\n\r\n建议让产品主仓库只直接维护本产品真正拥有的内容，例如：\r\n\r\n```text\r\nfirmware-project\/\r\n├── applications\/          # 产品业务代码\r\n├── board\/                 # 当前产品板级适配\r\n├── drivers\/               # 项目自有驱动\r\n├── services\/              # 产品服务层\r\n├── modules\/               # 可选：团队公共模块\r\n├── third_party\/           # 外部依赖\r\n├── tools\/                 # 依赖初始化、构建辅助工具\r\n├── Kconfig\r\n├── SConscript\r\n└── .gitmodules\r\n```\r\n\r\n这里有一个比“目录是否很大”更重要的判断标准：**是否具有独立生命周期和独立版本边界**。\r\n\r\n只服务于当前产品、修改通常需要和当前产品一起提交的代码，继续留在主仓库最简单；真正会被多个产品复用、拥有独立仓库和发布节奏的公共模块，才适合拆成 Submodule。\r\n\r\n外部 RTOS、文件系统、算法库等依赖则天然适合使用 Submodule，因为产品需要明确回答“当前到底基于哪个版本”。\r\n\r\n## 把 RT-Thread 作为 Submodule 引入\r\n\r\n以官方 RT-Thread 仓库为例，可以把它放到 `third_party\/rt-thread`：\r\n\r\n```bash\r\ngit submodule add -b master \\\r\n    https:\/\/github.com\/RT-Thread\/rt-thread.git \\\r\n    third_party\/rt-thread\r\n```\r\n\r\n执行后，主仓库会生成或更新 `.gitmodules`：\r\n\r\n```ini\r\n[submodule \"third_party\/rt-thread\"]\r\n    path = third_party\/rt-thread\r\n    url = https:\/\/github.com\/RT-Thread\/rt-thread.git\r\n    branch = master\r\n```\r\n\r\n然后提交主仓库：\r\n\r\n```bash\r\ngit add .gitmodules third_party\/rt-thread\r\ngit commit -m \"chore: add RT-Thread submodule\"\r\n```\r\n\r\n需要特别理解 `branch = master` 的含义。它是 `git submodule update --remote` 等操作使用的默认远端跟踪分支，并不意味着普通初始化时会自动追到 `master` 最新提交。\r\n\r\n普通恢复命令仍然是：\r\n\r\n```bash\r\ngit submodule update --init --recursive\r\n```\r\n\r\n在默认 `checkout` 更新策略下，Git 会把子模块检出到超级项目记录的 commit，通常表现为 detached HEAD。这个行为恰恰保证了产品依赖可复现。\r\n\r\n可以用下面的命令确认当前引用：\r\n\r\n```bash\r\ngit submodule status\r\n```\r\n\r\n## 再用 Sparse Checkout 缩小 RT-Thread 工作区\r\n\r\nRT-Thread 主仓库同时包含 `bsp`、`components`、`include`、`libcpu`、`src`、`tools` 等大量目录。对于单一 STM32 Cortex-M7 项目，日常开发通常不需要把其他架构、其他厂商和大量无关 BSP 全部展开到 IDE 文件树中。\r\n\r\n对子模块启用 cone mode：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread sparse-checkout init --cone\r\n```\r\n\r\n然后按当前项目需要设置目录。例如：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread sparse-checkout set \\\r\n    src \\\r\n    include \\\r\n    tools \\\r\n    components\/drivers \\\r\n    components\/finsh \\\r\n    libcpu\/arm\/common \\\r\n    libcpu\/arm\/cortex-m7 \\\r\n    bsp\/stm32\/libraries\/HAL_Drivers\r\n```\r\n\r\n当前稀疏集合可以直接查看：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread sparse-checkout list\r\n```\r\n\r\nGit 的 cone mode 以目录为主要输入，并会保留所需祖先目录中的必要文件。对 RT-Thread 这类目录层级明确的大型源码仓库，比手工维护复杂的 gitignore 风格规则更适合。\r\n\r\n如果后续启用了 DFS、网络栈或其他组件，再扩展目录集合即可：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread sparse-checkout add \\\r\n    components\/dfs \\\r\n    components\/net\r\n```\r\n\r\n需要强调：Sparse Checkout 只决定**工作区看到什么**，不是“删除源码”，也不是“关闭功能”。真正决定组件是否参与编译的仍然是 Kconfig、`rtconfig.h` 和 SCons 构建规则。\r\n\r\n因此，把无关目录加入 `.gitignore` 不能达到相同效果；直接删除 RT-Thread 中暂时不用的目录更不可取，因为那会变成真实源码修改，给后续上游同步制造大量无意义差异。\r\n\r\n## 稀疏目录策略必须进入主仓库，而不是留在某台电脑里\r\n\r\nSparse Checkout 的配置属于本地 Git 工作区状态，不会像普通源码文件一样自然地随产品主仓库传播。团队项目不能依赖每个开发者手工输入一串路径。\r\n\r\n更稳妥的方式，是把项目需要的 RT-Thread 目录保存成普通配置文件，例如：\r\n\r\n```text\r\ntools\/dependency\/rtthread_sparse_paths.txt\r\n```\r\n\r\n内容只保留目录清单：\r\n\r\n```text\r\nsrc\r\ninclude\r\ntools\r\ncomponents\/drivers\r\ncomponents\/finsh\r\nlibcpu\/arm\/common\r\nlibcpu\/arm\/cortex-m7\r\nbsp\/stm32\/libraries\/HAL_Drivers\r\n```\r\n\r\n再提供一个很薄的初始化脚本：\r\n\r\n```python\r\n#!\/usr\/bin\/env python3\r\nfrom pathlib import Path\r\nimport subprocess\r\n\r\nROOT = Path(__file__).resolve().parents[2]\r\nRTTHREAD = ROOT \/ \"third_party\" \/ \"rt-thread\"\r\nCONFIG = ROOT \/ \"tools\" \/ \"dependency\" \/ \"rtthread_sparse_paths.txt\"\r\n\r\npaths = [\r\n    line.strip()\r\n    for line in CONFIG.read_text(encoding=\"utf-8\").splitlines()\r\n    if line.strip() and not line.lstrip().startswith(\"#\")\r\n]\r\n\r\nsubprocess.run(\r\n    [\"git\", \"-C\", str(RTTHREAD), \"sparse-checkout\", \"init\", \"--cone\"],\r\n    check=True,\r\n)\r\n\r\nsubprocess.run(\r\n    [\"git\", \"-C\", str(RTTHREAD), \"sparse-checkout\", \"set\", \"--stdin\"],\r\n    input=\"\\n\".join(paths) + \"\\n\",\r\n    text=True,\r\n    check=True,\r\n)\r\n```\r\n\r\n这样，新开发环境和 CI 的入口就能稳定为：\r\n\r\n```bash\r\ngit clone <firmware-project-url>\r\ncd firmware-project\r\n\r\ngit submodule update --init --recursive\r\npython tools\/dependency\/setup_rtthread_sparse.py\r\n```\r\n\r\n也可以在 clone 时初始化子模块：\r\n\r\n```bash\r\ngit clone --recurse-submodules <firmware-project-url>\r\n```\r\n\r\n但无论使用哪种 clone 方式，Sparse Checkout 目录集合仍然应由主仓库中的配置显式恢复，而不是假定其他电脑会继承本机 Git 状态。\r\n\r\n## 第三方库和团队公共模块沿用同一套边界\r\n\r\n除了 RT-Thread，littlefs、压缩算法、Bootloader 公共库、诊断库等外部依赖，也可以分别作为 Submodule：\r\n\r\n```bash\r\ngit submodule add \\\r\n    https:\/\/example.com\/vendor\/littlefs.git \\\r\n    third_party\/littlefs\r\n\r\ngit submodule add \\\r\n    https:\/\/example.com\/organization\/product-common.git \\\r\n    modules\/product-common\r\n```\r\n\r\n主仓库最终可以形成这样的关系：\r\n\r\n```text\r\nfirmware-project\/\r\n├── applications\/                  # 产品代码，主仓库直接维护\r\n├── board\/                         # 产品板级适配\r\n├── drivers\/                       # 产品自有驱动\r\n├── services\/\r\n├── modules\/\r\n│   └── product-common\/            # 团队公共模块 Submodule\r\n└── third_party\/\r\n    ├── rt-thread\/                 # Submodule + Sparse Checkout\r\n    └── littlefs\/                  # 普通 Submodule\r\n```\r\n\r\n普通小型依赖没有必要为了形式统一而继续做 Sparse Checkout。只有依赖本身很大，而且项目长期稳定地只使用少数目录时，才值得增加这一层工作区裁剪。\r\n\r\n如果某个组件本来通过 RT-Thread package 机制管理，也应先确定唯一依赖所有者。不要同时让 package 系统和 Submodule 管理同一份源码，否则版本来源会变得不清楚。\r\n\r\n## “恢复产品版本”和“主动升级依赖”必须分开\r\n\r\n日常开发首先做的是恢复产品已经锁定的 revision：\r\n\r\n```bash\r\ngit pull\r\n\r\ngit submodule update --init --recursive\r\npython tools\/dependency\/setup_rtthread_sparse.py\r\n```\r\n\r\n这条路径的目标是**复现主仓库已经提交并验证过的依赖组合**。\r\n\r\n主动升级 RT-Thread 则是另一件事。维护者可以显式执行：\r\n\r\n```bash\r\ngit submodule update --remote third_party\/rt-thread\r\n```\r\n\r\n然后检查主仓库中 gitlink 是否发生变化：\r\n\r\n```bash\r\ngit status\r\ngit diff --submodule=log\r\n```\r\n\r\n完成构建和项目回归后，再提交新的子模块引用：\r\n\r\n```bash\r\ngit add third_party\/rt-thread\r\ngit commit -m \"chore: update RT-Thread revision\"\r\n```\r\n\r\n这一步不能省略。否则开发者本地虽然已经把 RT-Thread 拉到了新版本，但超级项目没有记录新的 gitlink，其他开发者和 CI 仍然无法复现这个状态。\r\n\r\n## RT-Thread 需要长期维护补丁时，用 Fork + upstream\r\n\r\n如果产品完全不修改 RT-Thread，Submodule 可以直接指向官方仓库。\r\n\r\n如果产品需要长期维护少量内核、驱动或 BSP 补丁，更清晰的做法是维护团队 fork，把官方仓库配置成 `upstream`，在 fork 自己的集成分支中处理同步和补丁，再由产品主仓库锁定这个 fork 的精确 commit。\r\n\r\n\r\n![screenshot_image.png](https:\/\/oss-club.rt-thread.org\/uploads\/20260917\/e91b688b0b101b60e39da5761a91c140.png.webp)\r\n\r\n\r\n这个流程有两个非常重要的版本边界：\r\n\r\n- **RT-Thread fork 自己负责整合上游历史和团队补丁**；\r\n- **产品主仓库只负责决定当前产品到底使用 fork 中哪个 commit**。\r\n\r\n子模块可以配置成团队 fork：\r\n\r\n```ini\r\n[submodule \"third_party\/rt-thread\"]\r\n    path = third_party\/rt-thread\r\n    url = https:\/\/example.com\/organization\/rt-thread.git\r\n    branch = integration\r\n```\r\n\r\n在子模块仓库中增加官方上游：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread remote add upstream \\\r\n    https:\/\/github.com\/RT-Thread\/rt-thread.git\r\n\r\ngit -C third_party\/rt-thread fetch upstream\r\n```\r\n\r\n整体跟进上游时，可以在团队 `integration` 分支中按约定使用 merge 或 rebase。关键不是两者谁绝对更好，而是**上游同步发生在 RT-Thread fork 的仓库边界内**，不要把官方同步历史和产品业务代码混在一个仓库里。\r\n\r\n完成 fork 中的集成、验证和推送后，再回到产品主仓库更新 gitlink。\r\n\r\n## 当前版本只需要一个官方修复时，用 cherry-pick 精确吸收\r\n\r\n产品维护中经常不能整体升级 RT-Thread，只想吸收官方已经合入的一个 Bug 修复。这时可以使用 `git cherry-pick`。\r\n\r\n先保证子模块工作树干净并获取官方历史：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread status --short\r\ngit -C third_party\/rt-thread fetch upstream\r\ngit -C third_party\/rt-thread switch integration\r\n```\r\n\r\n真正应用之前先检查目标 commit：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread show --stat <commit-id>\r\ngit -C third_party\/rt-thread show <commit-id>\r\n```\r\n\r\n确认范围后再执行：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread cherry-pick -x <commit-id>\r\n```\r\n\r\n`-x` 会在新提交信息中留下原始 commit 来源。对于“把公开上游修复回移到维护分支”这种场景，这个追溯信息很有价值。\r\n\r\n发生冲突时，先处理冲突文件，然后：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread add <resolved-file>\r\ngit -C third_party\/rt-thread cherry-pick --continue\r\n```\r\n\r\n如果确认补丁不适合当前分支：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread cherry-pick --abort\r\n```\r\n\r\n### Sparse Checkout 下 cherry-pick 还要多检查一步\r\n\r\nSparse Checkout 只控制当前工作区展开哪些目录，它不能证明某个上游 commit 只修改这些目录。\r\n\r\n因此 cherry-pick 之前至少要检查：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread show --stat <commit-id>\r\n```\r\n\r\n如果补丁同时修改了当前稀疏集合之外的路径，而这些路径又需要人工解决冲突或参与验证，就先把它们临时加入工作区：\r\n\r\n```bash\r\ngit -C third_party\/rt-thread sparse-checkout add \\\r\n    bsp\/stm32\/libraries \\\r\n    tools\r\n```\r\n\r\n补丁处理结束后，再通过项目保存的 `rtthread_sparse_paths.txt` 恢复标准目录集合。\r\n\r\n这时三种操作的边界就非常清楚：\r\n\r\n| 目标 | 操作 | 结果 |\r\n| --- | --- | --- |\r\n| 恢复产品锁定版本 | `git submodule update --init --recursive` | 回到主仓库记录的精确 commit |\r\n| 整体跟进上游 | fork 内 merge\/rebase | 整合一段上游历史 |\r\n| 只吸收单个修复 | `git cherry-pick -x <commit-id>` | 精确应用指定提交的变更 |\r\n\r\n## Sparse Checkout 不等于减少 clone 下载量\r\n\r\n这是最容易混淆的边界之一。\r\n\r\nSparse Checkout 的目标是减少**工作树中实际出现的文件**。它不会自动等价为“少下载这些目录的 Git 对象”。如果真正目标是减少网络传输量或对象存储，还需要结合 Git 的 Partial Clone、`--filter`、浅克隆等机制设计。\r\n\r\n例如：\r\n\r\n```bash\r\ngit clone --filter=blob:none <repository-url>\r\n```\r\n\r\nPartial Clone 会改变对象获取方式；浅克隆则会限制历史可用范围。这些机制会影响离线开发、历史分析、CI 和后续维护，因此应该独立评估，而不要把它们和 Sparse Checkout 的“工作区裁剪”混为一谈。\r\n\r\n对 RT-Thread 工程而言，Sparse Checkout 最直接的收益通常不是网络流量，而是：\r\n\r\n- IDE 索引更聚焦；\r\n- 全局搜索结果更干净；\r\n- 源码导航更少被无关 BSP 和 CPU 架构干扰；\r\n- 工程中隐藏的跨 BSP 依赖更容易暴露。\r\n\r\n## 对 RT-Thread Studio、Keil 和 SCons 的影响\r\n\r\n对构建系统而言，Sparse Checkout 之后的目录就是实际工作区。被排除的文件不存在于工作树，所以构建脚本和 IDE 工程必须只依赖当前真正展开的路径。\r\n\r\n只要以下关系保持一致，SCons、Keil 和 RT-Thread Studio 仍然可以按正常方式工作：\r\n\r\n- Kconfig \/ `rtconfig.h` 选择的功能；\r\n- `SConscript` \/ `rtconfig.py` 引用的源码路径；\r\n- 当前板级和 HAL 目录；\r\n- IDE 工程中生成或引用的源文件列表。\r\n\r\n这反而可以帮助项目暴露不正确的隐式依赖。例如某个 `SConscript` 无意中引用另一块板卡目录，在完整 RT-Thread 工作区里可能长期不明显；启用合理的 Sparse Checkout 后，这类问题会更快变成可见的路径错误。\r\n\r\n因此比较稳定的职责划分是：\r\n\r\n- **Submodule**：依赖来源与精确 revision；\r\n- **Sparse Checkout**：本地源码可见范围；\r\n- **Kconfig \/ `rtconfig.h`**：功能配置；\r\n- **SCons \/ RT-Thread Studio \/ Keil**：实际构建输入与工程；\r\n- **产品主仓库**：业务代码、板级适配，以及全部依赖 revision 的组合关系。\r\n\r\n## 最终落地时，把规则收敛成少数几条\r\n\r\n长期维护的 MCU\/RTOS 项目不需要把 Git 机制做得很复杂，但需要把边界固定下来：\r\n\r\n1. RT-Thread 和真正独立的外部库使用 Submodule，不再复制一份源码进产品仓库；\r\n2. 产品主仓库必须提交每个 Submodule 的精确 commit，升级依赖后必须更新 gitlink；\r\n3. RT-Thread 仓库很大时再启用 Sparse Checkout，并把目录清单和初始化脚本放进主仓库；\r\n4. 产品专用业务代码继续留在主仓库，只有独立生命周期的公共模块才拆成单独仓库；\r\n5. RT-Thread 如需长期补丁，使用 fork + upstream，让补丁历史留在 RT-Thread 自己的仓库边界内；\r\n6. CI 和新开发环境统一从“初始化 Submodule → 应用 Sparse Checkout → 配置 → 构建”这一条确定路径开始；\r\n7. Sparse Checkout 只负责工作区，不替代 Kconfig，也不等于 Partial Clone。\r\n\r\n最终得到的不是一份“删掉大量目录的 RT-Thread”，而是一套清晰、可追溯、可复现的版本关系：产品主仓库决定组合，Submodule 锁定依赖 revision，Sparse Checkout 控制开发者需要看到什么，而 Kconfig\/SCons\/IDE 决定最后真正编译什么。\r\n\r\n## 参考资料\r\n\r\n- Git Submodule 官方文档：<https:\/\/git-scm.com\/docs\/git-submodule>\r\n- Git Submodules 机制说明：<https:\/\/git-scm.com\/docs\/gitsubmodules>\r\n- Git Sparse Checkout 官方文档：<https:\/\/git-scm.com\/docs\/git-sparse-checkout>\r\n- Git Cherry-pick 官方文档：<https:\/\/git-scm.com\/docs\/git-cherry-pick>\r\n- Git Clone \/ Partial Clone：<https:\/\/git-scm.com\/docs\/git-clone>\r\n- Git Partial Clone 设计说明：<https:\/\/git-scm.com\/docs\/partial-clone>\r\n- RT-Thread 官方仓库：<https:\/\/github.com\/RT-Thread\/rt-thread>","updatetime":"2026-09-18 17:18:45","author":"wdfk_prog"}}