Emscripten 的 git subtree 外部库管理设计:从 mimalloc 试点到 musl 迁移的完整方案
【免费下载链接】emscriptenEmscripten: An LLVM-to-WebAssembly Compiler项目地址: https://gitcode.com/gh_mirrors/em/emscripten
本文基于 Emscripten 仓库中的设计文档 04-git-subtrees.md(状态为 Draft),系统讲解 Emscripten 如何采用git subtree统一管理与版本化system/lib/下的外部库:包括现状痛点、候选库分析、--squash与 tag 管理等关键设计决策、以mimalloc为例的四步迁移实操、日常 diff/升级/回补操作,以及面向musl与 LLVM 运行时库的三阶段路线图。读完本文,你可以理解 Emscripten 供应链管理的演进逻辑,并能独立复现 subtree 初始化与上游版本升级的完整流程。
1. 背景:Emscripten 外部库管理的现状与痛点
Emscripten 在 system/lib/ 下 vendor(内置)了多个外部库来提供核心运行时能力,包括 C/C++ 标准库、内存分配器和编译器运行时。在设计文档提出之前,这些库通过四种彼此割裂的机制维护:
- 外部 Fork + 同步脚本
musl(system/lib/libc/musl):通过emscripten-core/musl外部 fork 仓库维护,并依赖两个同步脚本 push_musl_changes.py 与 update_musl.py;- LLVM 运行时库(
compiler-rt、libcxx、libcxxabi、libunwind、llvm-libc、openmp):从emscripten-core/llvm-project的分支同步,使用 push_llvm_changes.py 以及各自的update_*.py脚本(如 update_compiler_rt.py)。
- 手动文件拷贝
mimalloc(system/lib/mimalloc):树内跟踪了 Emscripten 专属的原语实现,但升级新上游版本时只是把文件直接拷贝进目录,没有任何自动化工具。
- 静态单文件 vendor
dlmalloc(system/lib/dlmalloc.c)与stb_image(system/lib/stb_image.c):低变更频率的单个源文件。
这一现状带来的四个具体痛点,设计文档逐条列出:
- 失同步(Desynchronization):直接在 Emscripten PR 中修改 vendor 文件(尤其是
system/lib/libc/musl)时,作者经常忘记把改动回推到下游 fork 仓库; - 丢失 Git 历史与署名:用
shutil.copytree或手动替换文件拷贝整棵目录树,会丢弃提交历史、作者署名以及两个仓库之间的中间变更上下文; - 更新流程复杂:升级一个外部库需要同时摆弄多个本地 checkout、在外部仓库里做合并、跑自定义拷贝脚本、再跨仓库协调开 PR;
- Diff 与审计困难:想核对 Emscripten 的 vendor 代码与上游版本之间的精确差异,必须在不同的 clone 之间做人工比对。
Prior Art:通过git subtree管理 vendor 系统库的做法,此前已在wasi-libc(WebAssembly 官方的 wasi-libc 项目,见其 PR #797)中被成功采用,为 Emscripten 提供了直接的先例。
当前仓库中的实际状态与文档描述一致:上述同步脚本(push_musl_changes.py、update_musl.py、push_llvm_changes.py、update_*.py系列)仍然全部存在于 system/lib/ 下,说明 subtree 迁移在该仓库当前快照中尚未落地。与此同时,mimalloc目录里有一份版本说明文件 README.emscripten,明确记录当前树内版本:
This contains mimalloc 34fbd7e7cd4627424490afe19b20f8066bfc537d (v3.5.1) with Emscripten-specific changes. Origin: https://github.com/microsoft/mimalloc For the Emscripten port design see src/prim/emscripten/prim.c这正是设计文档选择mimalloc作为试点的直接依据——文档中提到的“当前 Emscripten 中的版本 v3.5.1”与树内文件记录完全吻合。
2. 目标与非目标
目标(Goals)
- 使用
git subtree标准化树内外部库的管理; - 让日常对 vendor 库的编辑完全自包含在 Emscripten 仓库内(标准 commit 和 PR 直接改动 vendor 目录,不再依赖树外脚本);
- 升级到上游新版本或挑选(cherry-pick)修复时,支持单命令三方合并(
git subtree pull --squash); - 简化与上游版本、tag 的 diff 与补丁审计(
git diff <tag>: HEAD:<prefix>); - 建立分阶段推进策略:先以低风险、自包含的试点库
mimalloc起步,再做musl,最后评估 LLVM 运行时库; - 弃用并删除自定义同步脚本(如
push_musl_changes.py和update_musl.py)。
非目标(Non-Goals)
- 不在一次大变更中同时迁移所有外部库;
- 不使用
git submodule(子模块要求递归 clone、要管理 detachedHEAD,会复杂化贡献者工作流); - 不替换单文件、高度定制或已休眠的 vendor 代码(如
dlmalloc.c),这类库上游变更极少,subtree 收益很低。
3. 候选库分析:哪些适合 subtree 模型
设计文档用一张完整表格对各候选库逐一评估(上游仓库以名称指代):
| 库 | 树内路径 | 上游仓库 | 当前工作流 | 复杂度 / 建议 |
|---|---|---|---|---|
mimalloc | system/lib/mimalloc | microsoft/mimalloc | 手动拷贝粘贴 | 推荐试点。独立仓库、干净的 release tag、体量小、自包含 |
musl | system/lib/libc/musl | git.musl-libc.org/musl | Fork +update_musl.py | 高优先级。有wasi-libc先例,可消除同步脚本和 fork 漂移 |
compiler-rt | system/lib/compiler-rt | llvm/llvm-project | Fork +update_compiler_rt.py | 中优先级。上游是 monorepo,需要 subtree split 或跟踪分支/fork |
libcxx/libcxxabi | system/lib/libcxx{,abi} | llvm/llvm-project | Fork +update_libcxx*.py | 中优先级。同上 monorepo 考量 |
libunwind | system/lib/libunwind | llvm/llvm-project | Fork +update_libunwind.py | 中优先级。同上 monorepo 考量 |
llvm-libc | system/lib/llvm-libc | llvm/llvm-project | Fork +update_llvm_libc.py | 中优先级。同上 monorepo 考量 |
openmp | system/lib/openmp | llvm/llvm-project | Fork +update_openmp.py | 中优先级。同上 monorepo 考量 |
dlmalloc | system/lib/dlmalloc.c | Doug Lea (v2.8.6) | 单文件 vendor | 低优先级 / 跳过。上游不活跃,Emscripten 定制很深 |
stb_image | system/lib/stb_image.c | nothings/stb | 单文件 vendor | 低优先级。更新频率低,单文件 |
为什么从mimalloc起步?
文档给出五条理由:
- 隔离且低风险:与 libc 不同,
mimalloc出问题不会危及基础运行时或核心工具链启动; - 上游独立、干净:上游是独立的 GitHub 仓库(
microsoft/mimalloc),有标准的 release tag(v3.5.1、v3.4.1等),远程管理 straightforward; - 没有遗留 fork 包袱:
mimalloc不存在emscripten-core外部 fork 仓库或既有同步脚本需要拆除,一直靠手动拷贝更新; - 定制补丁集小且集中:Emscripten 专属支持集中在
system/lib/mimalloc/src/prim/emscripten/prim.c(其中相当部分已回馈上游)以及tools/system_libs.py中的编译参数调整; - 上游变更活跃:
mimalloc发版频繁(近期数月内 v3.3.1 -> v3.4.1 -> v3.5.1),subtree 对后续版本升级能立即产生价值。
当前仓库的源码可以从三个方向佐证这一判断:
- 移植设计集中在单一文件:system/lib/mimalloc/src/prim/emscripten/prim.c 开头有一段完整的设计注释,说明 mimalloc 构建在 emmalloc 之上、emmalloc 构建在 sbrk 之上的三层结构(因为 sbrk 只能单向伸缩、不能“跳过”区域,无法像 POSIX
mmap那样正确归还系统内存)。该文件版权头为 “Copyright (c) 2018-2026, Microsoft Research, Daan Leijen, Alon Zakai”,印证了文档所说“Emscripten 的移植工作相当部分已上游化”; - 构建侧的 Emscripten 定制就是文档所说的“编译参数调整”:tools/system_libs.py 中的
libmimalloc类集中定义了一组-D编译开关,例如-DMI_ENABLE_LARGE_PAGES=0(默认禁用大页)、-DMI_ARENA_SLICE_SHIFT=(12+MI_SIZE_SHIFT)(wasm64 下把页大小减半为 32KiB、wasm32 下为 16KiB)、-DMI_MAX_ALIGN_SIZE=8、-DMI_DEFAULT_ARENA_RESERVE=65536(以 64 MiB 为单位预留内存,并注明需与-sINITIAL_HEAP默认值保持同步)、-DMI_LIBC_MUSL、-DMI_USE_BUILTIN_THREAD_POINTER等;该类还设置了force_object_files = True(malloc/free/calloc 是运行时函数,可在 LTO 阶段生成,因此自身不能参与 LTO); - 测试侧已有守护用例:test/test_other.py 的
test_mimalloc_no_asan断言mimalloc与-fsanitize=address组合会报错(与 tools/system_libs.py 中 “mimalloc is not compatible with -fsanitize=address” 的守卫逻辑对应);test/test_other.py 的test_mimalloc_headers则以-sMALLOC=mimalloc编译并运行一段#include <mimalloc.h>的程序,验证头文件可用。MALLOC=mimalloc选项本身也在 src/settings.js 中被文档化为 “a powerful multithreaded allocator”。
这些证据共同说明:mimalloc在树内已经是“上游目录结构 + 集中式构建定制 + 独立测试”的形态,迁移到 subtree 的改动面确实很小。
4. Subtree 架构总览(以mimalloc为例)
方案使用git subtree并固定配合--squash标志,把外部库 vendor 到指定目录前缀之下。对mimalloc而言:
- 上游仓库:
microsoft/mimalloc - 树内前缀路径:
system/lib/mimalloc
文档给出的数据流示意:
Upstream (microsoft/mimalloc) │ git fetch / git subtree pull ▼ ┌─────────────────────────────────────────────────────────────┐ │ emscripten-core/emscripten │ │ │ │ ├── system/lib/mimalloc/ ◄── Managed via subtree │ │ │ ├── include/ ◄── Tracked in-tree │ │ │ └── src/ │ │ │ └── prim/emscripten/ ◄── Emscripten primitives │ │ └── tools/system_libs.py ◄── Compiles libmimalloc │ └─────────────────────────────────────────────────────────────┘ │ git subtree split / push (Optional) ▼ Upstream PR / Fork (e.g. microsoft/mimalloc)即:上游通过git fetch/git subtree pull流入 Emscripten 的system/lib/mimalloc前缀;本地改动日常直接提交在 Emscripten 仓库中;只有当补丁成熟准备回馈时,才用git subtree split抽取成分支推给上游。
5. 关键设计决策
5.1 始终使用--squash
上游仓库动辄数万提交。不加--squash时,完整上游历史会直接灌进 Emscripten 的 git log;加--squash后,git 会合成一个代表合并点上游快照的单提交,并在提交信息中嵌入 subtree 元数据(git-subtree-dir与git-subtree-split),从而保留将来三方合并所需的正确祖先关系。
5.2 跟踪完整的上游目录结构
通过 subtree 导入上游仓库时,保留其上游目录结构,不做裁剪:
mimalloc、musl这类库本身紧凑(几 MB 量级);- 裁剪或排除未使用的目录,会在未来
git subtree pull时制造人为的合并冲突; - tools/system_libs.py 中的构建逻辑显式挑选为 WebAssembly 编译哪些源文件——以
libmimalloc为例,它对system/lib/mimalloc/src下的*.c做 glob 并排除alloc-override.c、free.c、page-queue.c、static.c(这些文件在源码层面被其他文件#include),再显式追加prim/prim.c、prim/prim-tls.c、树外的emmalloc.c和 libc 的sbrk.c。未被编译的文件既不影响二进制产物,也不影响缓存体积。
从源码结构看,这正好验证了设计文档“多导入文件无害、构建侧负责筛选”的判断——目录完整性服务于 merge 正确性,编译范围由MTLibrary类单独控制。
5.3 Emscripten 仓库作为唯一事实来源(Single Source of Truth)
本地改动、bug 修复、移植适配直接提交进 Emscripten 仓库。独立 fork 仓库(如emscripten-core/musl)不再充当中间同步枢纽。当本地补丁准备回馈上游时,用git subtree split把前缀内的历史干净地抽取成一个适合开上游 PR 的分支。
5.4 远程与 tag 管理:防止 tag 冲突
默认情况下,git fetch加--tags会把上游所有 tag(如v1.2.6、v3.5.1)导入本地refs/tags/*命名空间,这带来两个问题:
- Tag 冲突与噪音:不同上游的通用版本号 tag(如
v1.0、v2.0)可能互相冲突,或 clutter 掉 Emscripten 的 tag 列表和 shell 自动补全; - 污染
git describe:自动化工具和构建脚本用git describe --tags判定编译器版本时,可能命中可达的上游 tag 而非 Emscripten 的 release tag。
为此文档约定三条措施:
1) 配置 remote 时使用--no-tags(或设置remote.<name>.tagOpt = --no-tags):
git remote add --no-tags mimalloc-upstream \ https://github.com/microsoft/mimallocgit subtree pull并不依赖本地已有 tag;Git 可以把指定 tag 或 commit 按需取到FETCH_HEAD,而不必落进refs/tags/*。
2) 命名空间 refspec(可选,供本地访问上游 tag 的维护者使用):
# 隔离到 refs/remotes/ 下(对 git tag 和 git describe 不可见) git config --add remote.mimalloc-upstream.fetch \ '+refs/tags/*:refs/remotes/mimalloc-upstream/tags/*'或者用子目录前缀放进refs/tags/:
git config --add remote.mimalloc-upstream.fetch \ '+refs/tags/*:refs/tags/mimalloc/*'3) 基于 URL 的临时拉取(wasi-libc 模式):普通贡献者的日常工作区完全不需要配置上游 remote;执行版本升级的维护者可以直接从仓库 URL 拉取:
git subtree pull --prefix=system/lib/mimalloc \ https://github.com/microsoft/mimalloc v3.5.2 --squash \ -m "Update mimalloc to v3.5.2"这样导入压缩后的提交,而不在仓库中留下任何外部 remote 或 tag。
6. 迁移实操:把mimalloc初始化为 subtree(不丢失现有改动)
以下是文档给出的完整四步流程,核心思路是:先在上游 v3.5.1 之上叠一份当前树内文件作为“初始化分支”,再删除树内散落的文件、用git subtree add重新挂上该分支,从而把既有补丁无损地纳入 subtree 管理。
Step 1:配置上游 remote
git remote add --no-tags mimalloc-upstream https://github.com/microsoft/mimalloc git fetch mimalloc-upstream tag v3.5.1Step 2:准备带本地补丁的初始分支
基于上游v3.5.1(当前 Emscripten 中的版本)创建临时分支,并覆盖当前树内文件以保留本地补丁:
# Check out base upstream release tag git checkout -b temp-mimalloc-init FETCH_HEAD # Overlay current Emscripten mimalloc directory to preserve local patches cp -r /path/to/emscripten/system/lib/mimalloc/* . git add -A git commit -m "Apply Emscripten modifications on top of mimalloc v3.5.1"Step 3:在 Emscripten 中初始化 subtree
在 Emscripten 工作分支上:
git checkout -b init-mimalloc-subtree # 1. Remove previously un-tracked mimalloc directory git rm -rf system/lib/mimalloc git commit -m "[lib] Remove loose mimalloc files for subtree initialization" # 2. Add the subtree using the prepared branch git subtree add --prefix=system/lib/mimalloc temp-mimalloc-init --squash \ -m "Initialize system/lib/mimalloc as git subtree tracking mimalloc v3.5.1" # 3. Clean up the temporary branch git branch -D temp-mimalloc-initStep 4:用测试验证
./test/runner test_other.test_mimalloc_headers ./test/runner test_other.test_mimalloc_no_asan ./test/runner test_hello_world -sMALLOC=mimalloc这三条验证命令在当前仓库中都有对应实现:前两个用例位于 test/test_other.py,第三个则用-sMALLOC=mimalloc走 src/settings.js 中声明的 mimalloc 分配器路径完成 hello world 级冒烟验证。
7. 日常操作手册(以mimalloc为例)
7.1 查看与上游的 diff
核对 Emscripten 树与某个上游 tag 之间的精确差异:
git fetch --no-tags mimalloc-upstream tag v3.5.1 # Diff entire subtree against upstream v3.5.1 git diff FETCH_HEAD: HEAD:system/lib/mimalloc # Diff a specific file git diff v3.5.1:src/prim/emscripten/prim.c \ HEAD:system/lib/mimalloc/src/prim/emscripten/prim.c若要以独立提交日志的形式查看 Emscripten 的本地改动:
git subtree split --prefix=system/lib/mimalloc -b mimalloc-local-changes git log v3.5.1..mimalloc-local-changes这直接解决了痛点 #4(diff 与审计困难):原来要在多个 clone 之间人工比对,现在一条git diff或git log即可完成。
7.2 升级到新的上游版本(如v3.5.2)
单条命令完成:
# Option A: Pull by remote name (with remote configured with --no-tags) git subtree pull --prefix=system/lib/mimalloc \ mimalloc-upstream v3.5.2 --squash \ -m "Update mimalloc to v3.5.2" # Option B: Pull directly by repository URL (no local remote or tags stored) git subtree pull --prefix=system/lib/mimalloc \ https://github.com/microsoft/mimalloc v3.5.2 --squash \ -m "Update mimalloc to v3.5.2" # Resolve any merge conflicts in-place if necessary # Run tests ./test/runner test_other.test_mimalloc_headers7.3 Cherry-pick 上游修复
在新版本发布之前先回补某个上游 commit:
git fetch mimalloc-upstream git subtree pull --prefix=system/lib/mimalloc \ mimalloc-upstream <commit-sha> --squash \ -m "Backport upstream mimalloc commit <commit-sha>"7.4 把本地修复回馈上游
把 Emscripten 前缀内的本地提交抽取成独立分支,向microsoft/mimalloc开 PR:
git subtree split --prefix=system/lib/mimalloc -b mimalloc-upstream-pr # Push to personal GitHub fork and open upstream PR git push git@github.com:<user>/mimalloc.git mimalloc-upstream-pr:my-fix8. 第二阶段:应用到musl并退役同步脚本
mimalloc验证成功后,同一套流程适用于system/lib/libc/musl:
- 配置 remote:
git remote add --no-tags musl-upstream git://git.musl-libc.org/musl git fetch musl-upstream tag v1.2.6 - 基于
v1.2.6准备初始分支(覆盖system/lib/libc/musl的当前内容); - 用下面的命令替换
system/lib/libc/musl:git subtree add --prefix=system/lib/libc/musl temp-musl-init --squash \ -m "Initialize system/lib/libc/musl as git subtree tracking musl v1.2.6" - 跑 libc 测试套件(
./test/runner test_libc*); - 删除过时的同步脚本:
git rm system/lib/push_musl_changes.py system/lib/update_musl.py
这一步完成后,痛点 #1(失同步)与 #3(跨仓库协调)在 libc 上即被消除——改动直接落在 Emscripten 仓库里,fork 漂移不复存在。
9. LLVM 运行时库评估:monorepo 带来的特殊挑战
迁移 LLVM 运行时(compiler-rt、libcxx、libcxxabi、libunwind、llvm-libc、openmp)面临上游是多 GB 级 monorepo(llvm/llvm-project)的独特问题:
- 对着 monorepo 子目录做 subtree 的可行性:直接对 monorepo 根跑
git subtree pull不现实——每个库都嵌套在子目录(runtimes/、libcxx/等)中,拉根仓库会把整个 LLVM 历史拖进来; - 中间 split 分支:较干净的思路是在
emscripten-core/llvm-project维护轻量级 split 分支,或用 GitHub Actions 按上游 release tag 自动生成“每运行时一个 subtree 分支”(例如llvmorg-22.1.8-libcxx这类命名); - 分阶段推进:鉴于上述考量,LLVM 运行时应放到 Phase 3,待
mimalloc与musl积累经验后再评估。在那之前,现有的update_*.py与push_llvm_changes.py脚本可继续运行——当前仓库中这些脚本也确实在位。
10. 新旧工作流对比
设计文档用下表总结了迁移前后的差异:
| 操作 | 旧工作流 | 新的git subtree工作流 |
|---|---|---|
| 日常补丁 | 在 Emscripten 里改,跑 push 脚本,推到 fork 仓库 | 直接在 Emscripten 里改文件并提交 |
| 上游升级 | 在 fork 仓库合并、解冲突,再跑拷贝脚本 | 在 Emscripten 里直接跑git subtree pull --squash |
| 冲突解决 | 在外部 clone 中解决,再用脚本盲目拷贝 | 在 Emscripten 中就地解决,使用标准 git 工具 |
| 审计 diff | 人工 diff 两个独立 checkout | git diff <tag>: HEAD:<prefix> |
| 唯一事实来源 | 分裂在 fork 仓库与 Emscripten 仓库之间 | Emscripten 仓库即唯一事实来源 |
11. 推进路线图(Rollout Roadmap)
- Phase 1(试点):把
mimalloc初始化为 git subtree,验证日常开发、本地补丁与一次上游 release 升级的完整闭环; - Phase 2(
musl):按已验证的模式把system/lib/libc/musl转为 git subtree,并退役push_musl_changes.py与update_musl.py; - Phase 3(LLVM 运行时评估):调研迁移
compiler-rt、libcxx等其他 LLVM 运行时,重点是解决 monorepo 的 subtree 抽取策略。
12. 小结
这份 Draft 状态的设计文档(目录约定见 docs/design/README.md,各文档需标注 Draft/Accepted/Completed 状态)给出了一个完整的供应链治理方案:以git subtree --squash为统一机制,以“Emscripten 仓库为唯一事实来源”为核心原则,以mimalloc(低风险、独立上游、补丁集小)为先导、musl(高价值、有 wasi-libc 先例)为主力、LLVM 运行时(monorepo 难题)为后续评估对象。配合树内已有的构建定制(tools/system_libs.py 的libmimalloc类)与守护测试(test/test_other.py 的test_mimalloc_headers/test_mimalloc_no_asan),该方案在工程上是可复现、可验证的:从git subtree add初始化到git subtree pull升级,每一步都有对应的仓库内代码与测试作为落点。
【免费下载链接】emscriptenEmscripten: An LLVM-to-WebAssembly Compiler项目地址: https://gitcode.com/gh_mirrors/em/emscripten
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考