|
| 1 | +# CI 工具链缓存优化分析 |
| 2 | + |
| 3 | +> 日期: 2026-06-01 |
| 4 | +> 状态: draft |
| 5 | +> 范围: Linux CI/e2e 中工具链 registry 与 xpkg payload 的重复下载、临时 `MCPP_HOME` 隔离策略,以及后续可扩展的工具链缓存架构。 |
| 6 | +
|
| 7 | +## 0. 结论 |
| 8 | + |
| 9 | +当前 CI 慢点不是单纯的 actions/cache 命中率问题。Linux workflow 已经缓存了 `~/.mcpp` 和 `~/.xlings`,但部分 e2e 脚本为了隔离配置会创建新的 `MCPP_HOME`。如果这些临时 home 没有继承已安装的 xpkg payload,测试内部的 `mcpp build` / first-run auto-install 就会把同一份大工具链下载到临时目录,测试结束后又被删除。 |
| 10 | + |
| 11 | +最典型的触发链: |
| 12 | + |
| 13 | +```text |
| 14 | +ci-linux |
| 15 | + -> persistent ~/.mcpp 已有/将安装 gcc、musl-gcc |
| 16 | + -> tests/e2e/run_all.sh |
| 17 | + -> 29_toolchain_partial_versions.sh 创建 $TMP/h2 |
| 18 | + -> first-run auto-install gcc@15.1.0-musl 到 $TMP/h2 |
| 19 | + -> trap 删除 $TMP/h2 |
| 20 | + -> 31_transitive_deps.sh 再创建 $TMP/mcpp-home |
| 21 | + -> 再次安装/下载 gcc@15.1.0-musl |
| 22 | + -> 后续 "Toolchain: musl-gcc" step 在 persistent ~/.mcpp 里还可能再安装一次 |
| 23 | +``` |
| 24 | + |
| 25 | +合理修法不是取消测试隔离,而是把隔离拆成两层: |
| 26 | + |
| 27 | +1. 配置状态隔离: 每个测试仍可拥有自己的 `config.toml`、lock/cache、project state。 |
| 28 | +2. 工具链 payload 复用: 大体积、只读的 `registry/data/xpkgs` 从 persistent sandbox 或 xlings cache 继承。 |
| 29 | + |
| 30 | +## 1. 现状证据 |
| 31 | + |
| 32 | +### 1.1 workflow 已经缓存 persistent sandbox |
| 33 | + |
| 34 | +- `.github/workflows/ci-linux.yml`: `Cache mcpp sandbox` 缓存 `~/.mcpp`,用于保留 musl-gcc、binutils、glibc、linux-headers、patchelf、ninja 等 payload。 |
| 35 | +- 同一个 workflow 也缓存 `~/.xlings`,用于保留 xlings 自己安装的包。 |
| 36 | +- E2E step 会设置 `MCPP_HOME=/home/runner/.mcpp`,并设置 `MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL`。 |
| 37 | + |
| 38 | +所以 CI 的正确方向应该是让临时 home 继承这两处 cache,而不是在临时 home 冷启动。 |
| 39 | + |
| 40 | +### 1.2 e2e 的临时 home 有两种语义 |
| 41 | + |
| 42 | +有些脚本创建临时 `MCPP_HOME` 是为了验证“空配置”或“fresh sandbox”行为,例如: |
| 43 | + |
| 44 | +- `14_toolchain_fallback.sh`: 验证无 toolchain 且 `MCPP_NO_AUTO_INSTALL=1` 时的硬错误。 |
| 45 | +- `26_toolchain_management.sh`: 显式验证 `toolchain install/list/default/remove`。 |
| 46 | +- `29_toolchain_partial_versions.sh`: 第一段验证 partial version/default 解析,第二段验证 first-run auto-install。 |
| 47 | + |
| 48 | +这类测试不能直接复制全局 `config.toml`,否则会掩盖被测行为。但它们通常可以继承 `registry/data/xpkgs`,因为 payload 是否已经存在不应改变“配置为空时会设置默认 toolchain”的语义。 |
| 49 | + |
| 50 | +另一些脚本创建临时 `MCPP_HOME` 只是为了隔离 BMI、git/cache 或测试产物,例如: |
| 51 | + |
| 52 | +- `31_transitive_deps.sh`: 目标是验证传递依赖 include_dirs,不是验证工具链安装。 |
| 53 | +- LLVM/import std/BMI cache 类测试: 目标是编译行为或 cache 行为,不是下载行为。 |
| 54 | + |
| 55 | +这类测试应该默认继承 payload 和必要配置。 |
| 56 | + |
| 57 | +### 1.3 `_inherit_toolchain.sh` 的模型不完整 |
| 58 | + |
| 59 | +旧 helper 只优先继承 `$HOME/.mcpp/registry/data/xpkgs`,但 `run_all.sh` 的能力检测同时承认: |
| 60 | + |
| 61 | +- `$HOME/.xlings/data/xpkgs/xim-x-musl-gcc/...` |
| 62 | +- `$MCPP_HOME/registry/data/xpkgs/xim-x-musl-gcc/...` |
| 63 | + |
| 64 | +这会导致能力检测认为 musl 可用,但临时 `MCPP_HOME` helper 没有把 `.xlings` payload 继承进去,脚本实际构建时仍可能走下载路径。 |
| 65 | + |
| 66 | +### 1.4 慢点掩盖了 31 的真实功能失败 |
| 67 | + |
| 68 | +Linux PR CI 的失败链显示 `31_transitive_deps.sh` 先在临时 home 下载了 `xim:musl-gcc@15.1.0` 的 808 MB payload,然后才失败在: |
| 69 | + |
| 70 | +```text |
| 71 | +child/ch/src/ch.cppm:2:10: fatal error: gc/gc.h: No such file or directory |
| 72 | +``` |
| 73 | + |
| 74 | +这说明 CI 慢不是唯一问题。即使下载复用做好,31 仍会失败,因为依赖 include_dirs 的传播模型也不完整: 依赖解析只把 dep 的 include_dirs 追加到 root manifest,而没有追加到实际发起依赖的 consumer package。`top -> ch -> gc` 里真正需要 `<gc/gc.h>` 的是 `ch`,不是 root `top`。 |
| 75 | + |
| 76 | +## 2. 本 PR 的优化策略 |
| 77 | + |
| 78 | +### 2.1 payload 继承从“整目录 symlink”改为“逐 package merge” |
| 79 | + |
| 80 | +`tests/e2e/_inherit_toolchain.sh` 现在会从以下来源继承 xpkg payload: |
| 81 | + |
| 82 | +- `$HOME/.mcpp/registry/data/xpkgs` |
| 83 | +- `$HOME/.xlings/data/xpkgs` |
| 84 | +- Windows/Git Bash 下的 `$USERPROFILE/.xlings/data/xpkgs` |
| 85 | + |
| 86 | +目标目录是临时 home 的: |
| 87 | + |
| 88 | +```text |
| 89 | +$MCPP_HOME/registry/data/xpkgs |
| 90 | +``` |
| 91 | + |
| 92 | +逐 package merge 比整目录 symlink 更稳,因为 CI 可能同时存在两套 payload 来源: mcpp 自己安装的 toolchain 在 `~/.mcpp`,xlings bootstrap 安装的包在 `~/.xlings`。逐项链接可以把两边合并进临时 home,而不会因为先 symlink 了一个根目录导致另一个来源无法补充。 |
| 93 | + |
| 94 | +### 2.2 first-run 测试只继承 payload,不继承配置 |
| 95 | + |
| 96 | +`29_toolchain_partial_versions.sh` 的 second home 继续保持: |
| 97 | + |
| 98 | +```text |
| 99 | +无 config/default state |
| 100 | +无 inherited subos |
| 101 | +``` |
| 102 | + |
| 103 | +但会继承 xpkg payload。这样仍能验证 first-run auto-install 的用户语义: |
| 104 | + |
| 105 | +- 生成项目没有 `[toolchain]`。 |
| 106 | +- 第一次 `mcpp build` 会出现 First run。 |
| 107 | +- 默认选择 `gcc@15.1.0-musl`。 |
| 108 | +- default 会被持久化,第二次 build 不再打印 First run。 |
| 109 | + |
| 110 | +区别是 install 阶段可以发现 payload 已存在,不再把大归档下载到临时目录。 |
| 111 | + |
| 112 | +### 2.3 传递依赖测试不再承担工具链冷启动 |
| 113 | + |
| 114 | +`31_transitive_deps.sh` 的目标是验证: |
| 115 | + |
| 116 | +- top 只声明 child。 |
| 117 | +- child 自己声明 grandchild。 |
| 118 | +- grandchild 的 `[build].include_dirs` 能传到 child 编译命令。 |
| 119 | + |
| 120 | +这个测试不应该下载工具链。现在它会继承 payload-only,并在没有可复用 musl xpkg payload 时直接 skip。musl 的安装/构建路径由 workflow 的专门 toolchain step 覆盖。 |
| 121 | + |
| 122 | +### 2.4 Linux CI 预热一次 musl-gcc |
| 123 | + |
| 124 | +Linux e2e step 在运行 `tests/e2e/run_all.sh` 前执行: |
| 125 | + |
| 126 | +```bash |
| 127 | +"$MCPP" toolchain install gcc 15.1.0-musl |
| 128 | +``` |
| 129 | + |
| 130 | +这有两个作用: |
| 131 | + |
| 132 | +1. 让 29/31 这类临时 home 测试通过 helper 复用 persistent payload。 |
| 133 | +2. 让后续 `"Toolchain: musl-gcc — build mcpp (--target)"` 复用同一份安装。 |
| 134 | + |
| 135 | +冷 cache 时最多下载一次 musl-gcc;热 cache 时这个命令应快速命中本地 payload。 |
| 136 | + |
| 137 | +### 2.5 include_dirs 按 dependency edge 传播 |
| 138 | + |
| 139 | +`src/cli.cppm` 的依赖解析现在把 include_dirs 当作 edge 属性处理: |
| 140 | + |
| 141 | +```text |
| 142 | +consumer package -> dependency package |
| 143 | +``` |
| 144 | + |
| 145 | +每个 unique dependency 仍只解析/扫描一次,但每个 consumer 都会获得该 dependency 的 public include dirs。这样: |
| 146 | + |
| 147 | +- root 直接依赖 header-providing package 时,root compile units 能看到 headers。 |
| 148 | +- child 依赖 grandchild 时,child compile units 能看到 grandchild headers。 |
| 149 | +- 同一个 dependency 被多个 package 复用时,每条边都能得到 include dirs,而不会因为 resolved map 命中就跳过传播。 |
| 150 | + |
| 151 | +这比“全部追加到 root 全局 flags”更接近 package-owned build metadata 的长期方向,也避免传递依赖的 header 只在 root 上可见、在真正 consumer 上不可见。 |
| 152 | + |
| 153 | +## 3. 通用架构建议 |
| 154 | + |
| 155 | +### 3.1 把 e2e home 分成三种模式 |
| 156 | + |
| 157 | +建议后续显式化 e2e helper API: |
| 158 | + |
| 159 | +```bash |
| 160 | +source tests/e2e/_home.sh payload-only |
| 161 | +source tests/e2e/_home.sh payload-and-config |
| 162 | +source tests/e2e/_home.sh empty |
| 163 | +``` |
| 164 | + |
| 165 | +语义: |
| 166 | + |
| 167 | +| 模式 | 继承 xpkgs | 继承 config | 用途 | |
| 168 | +|---|---:|---:|---| |
| 169 | +| `payload-only` | 是 | 否 | first-run、空配置、install/default 语义测试 | |
| 170 | +| `payload-and-config` | 是 | 是 | 普通编译、BMI、dependency、import std 测试 | |
| 171 | +| `empty` | 否 | 否 | 专门验证冷启动、错误提示、install 下载路径 | |
| 172 | + |
| 173 | +这样每个测试脚本不用手写 `MCPP_INHERIT_CONFIG=0 MCPP_INHERIT_SUBOS=0`,也能避免未来新增测试重新引入冷下载。 |
| 174 | + |
| 175 | +### 3.2 把“下载路径测试”集中到少数专门 job |
| 176 | + |
| 177 | +大体积工具链下载只应该出现在这些地方: |
| 178 | + |
| 179 | +1. `26_toolchain_management.sh`: CLI install/list/default/remove。 |
| 180 | +2. Linux workflow 的 toolchain matrix: GCC、musl-gcc、LLVM。 |
| 181 | +3. fresh-install workflow: 验证发布包在空环境中的安装体验。 |
| 182 | + |
| 183 | +其他 e2e 默认应复用 payload。这样失败定位也更清楚: |
| 184 | + |
| 185 | +- 下载失败: 看 toolchain/fresh-install job。 |
| 186 | +- build/module/dependency 失败: 看 e2e。 |
| 187 | + |
| 188 | +### 3.3 cache key 和 install marker 要区分 payload 与配置 |
| 189 | + |
| 190 | +长期建议把工具链 install 状态拆开: |
| 191 | + |
| 192 | +```text |
| 193 | +registry/data/xpkgs/<pkg>/<version>/ # payload, content-addressable-ish |
| 194 | +registry/toolchains/<name>@<version>.json # mcpp view: compiler path, target, stdlib, source payload |
| 195 | +config.toml # user default and mirror |
| 196 | +``` |
| 197 | + |
| 198 | +临时 home 可以安全 symlink/copy payload,但不必继承 default toolchain。`toolchain install` 应该在 payload 已存在时只补 mcpp 的 toolchain metadata,不重新下载。 |
| 199 | + |
| 200 | +### 3.4 CI 可观测性 |
| 201 | + |
| 202 | +建议后续给 e2e runner 增加轻量统计: |
| 203 | + |
| 204 | +```text |
| 205 | +downloads_before=<count> |
| 206 | +downloads_after=<count> |
| 207 | +toolchain_install_seconds=<duration> |
| 208 | +``` |
| 209 | + |
| 210 | +可以先用日志 grep 实现: |
| 211 | + |
| 212 | +- `Downloading xim:` |
| 213 | +- `Downloading compat.` |
| 214 | +- `Installing ...` |
| 215 | + |
| 216 | +目标不是精确计费,而是让 PR 上能直接看到“这次 e2e 是否触发了工具链冷下载”。 |
| 217 | + |
| 218 | +## 4. 验证计划 |
| 219 | + |
| 220 | +本 PR 应至少验证: |
| 221 | + |
| 222 | +1. `bash -n tests/e2e/_inherit_toolchain.sh tests/e2e/29_toolchain_partial_versions.sh tests/e2e/31_transitive_deps.sh` |
| 223 | +2. `29_toolchain_partial_versions.sh` 日志不再在临时 home 冷下载 musl-gcc。 |
| 224 | +3. `31_transitive_deps.sh` 在可复用 musl payload 存在时通过;不存在时 skip,而不是下载。 |
| 225 | +4. Linux CI e2e 和后续 musl target step 都通过。 |
0 commit comments