Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .agents/docs/2026-08-13-build-optimization-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,38 @@ README。手工重建确实能观察到 `mcpp build` 0.56s 返回、`cc1plus`
macOS 的 bench 格子因此进 `excluded`,原因写在 matrix.json 里。**没有继续猜** ——
本机是 Linux,复现不了,而这条已经让我烧掉好几轮 CI。

### 9a-3. 根因与修法(2026-08-16,已修)

上面两条把现象记全了,也记了"没有继续猜"。issue #437 把最后一步补上了 ——
**污染是真的,但可以绕开:不要用那个会被污染的链接器。**

根因链(对着当前源码核过):

1. `build_program.cppm:154` 把 host helper 的 `cfgBypass` 设成 `LinuxOnly`;
2. 于是 macOS 上 `bypassCfg == false`,`hostflags.cppm` 命中
`else if (dm.hasCfg) return out;` —— **返回空 link token**,没有 `-fuse-ld=lld`;
3. clang++(payload)于是用默认链接器 = Xcode 的 `/usr/bin/ld`;
4. 那个 `ld` **自己就是链 libc++ 的 Mach-O**,而它跑在 payload 工具链设好的
`DYLD_*` 里,dyld 把它的 libc++ 解析到 payload 那份 —— 缺 `__ZdaPv`,
**ld 还没开始链接就 abort**。

**为什么主构建不挂**:`flags.cppm` 的 macOS 分支**刻意**用 `-fuse-ld=lld`,
注释原文就写着 *"Xcode 15.4's ld aborting at launch on macos-14 CI when its
libc++ resolution was diverted"*。也就是说 —— **同一个坑,主构建早就绕过去了,
host helper 这条路没跟上。**

> 判据上这一条最说明问题:**mcpp 用 llvm 能把整个工程构建出来,却构建不了
> 自己的 `build.mcpp`。** 同一个工具链、同一个环境,两条链接路径给出不同结果,
> 那不是"macOS 环境问题",是这两条路径没有对齐。

**修法(已实施)**:`hostflags.cppm::host_link_tokens` 的 trust-cfg 分支在 macOS 上
追加 `-fuse-ld=lld`。cfg 选的是 runtime,从来没选过链接器;lld 随做编译的那套
工具链一起发布,不可能被解析到它没链过的 libc++ 上。

**没有配单元测试**:判断在 `if constexpr (is_macos)` 里,在 Linux 上整段编译掉,
测试不可能失败。判据是 macOS CI 的 e2e(`89/92/110/111/125/143/144/145/181/186/194`
都带 `build.mcpp`)与 issue 报告者的复现。

### 9b. mbedtls 的 registry 源码包缺 `framework/` 子模块

mbedtls-3.6.1/CMakeLists.txt:304
Expand Down
163 changes: 90 additions & 73 deletions .agents/docs/2026-08-16-toolchain-architecture-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,41 +26,40 @@

---

## 1. `@system` 只对 MSVC 存在 —— 这是**对的**,但它的代价被摊在了 26 处 ⭐
## 1. 一个平台特例,摊在 26 处 ⭐

### 现状
### ⚠️ 本节初稿是错的,先说清楚

初稿把 `is_system_toolchain()` 里的 `Family::Msvc` 读成"不对称,应该推广到所有族",
提出支持 `gcc@system`。**方向反了,而且那个东西根本不存在。**

`registry.cppm:428`:
xlings 是**用户态 OS**,mcpp 建立在它之上,整条设计就是**把 host 依赖降到最低**:
工具链来自 payload,这才使得"manifest 写 14.44.35207"在**每台**机器上成立,
而不是只在恰好装了它的那台上成立。`msvc@system` 是 **Windows 的让步**
(Visual Studio 常常已经装了、又不总能重新分发),不是一种可推广的能力。

被纠正之后我又去给自己发明的拼写写了个"显式拒绝",那是**从自己的错误里长出来的
新范围**,已经作废(mcpp#441 已关闭)。而且那条拒绝消息还说错了一件事 ——
它写"只有 `msvc@system` 存在",可 `prepare.cppm:1376` 有一条**故意保留**的裸
`system` 逃生口:

```cpp
bool is_system_toolchain(const ToolchainSpec& spec) {
return spec.family == Family::Msvc
&& (spec.version.empty() || spec.version == "system");
}
} else if (tcSpec.has_value() && *tcSpec == "system") {
// Explicit user opt-in to system PATH compiler — kept as escape hatch.
```

**族被写进了判据。**

> **先说清楚:这个限制本身是对的,不要改。**
> xlings 的原则是**能不依赖 host 就不依赖 host** —— 工具链应当由 xim 提供,
> 这样"声明什么就用什么"在每台机器上成立。`gcc@system` / `llvm@system`
> **不该**支持:那是把不确定性请回来。
>
> `msvc@system` 是 **Windows 平台的特例**,不是通用能力的一个实例:
> Visual Studio 常常已经装在机器上、又不总是能被重新分发,所以必须能用它。
>
> 我这份 review 的初稿建议"把 `@system` 通用化到所有族",**那是错的**,
> 方向正好反了。下面改成正确的问题。
**真正提供 host 依赖的那一处,拼写是不带族的 `system`。** 记在这里,
免得下一份 review 再把它当成"缺失的能力"。

真正的问题不是"为什么只有 MSVC 有",而是:**一个平台特例,凭什么要在 26 个
地方各写一遍。**
### 剩下的、与那个错误无关的部分

代价不是"四处特判"这么轻。把两个文件扫一遍:**lifecycle.cppm 里 13 处、
prepare.cppm 里 13 处** msvc 专有分支,而 gcc / llvm / mingw 三个族在
lifecycle.cppm 里**一处都没有**(mingw 更是零分支 —— 它整个被表达成一个
*target*,`x86_64-windows-gnu`,那才是这套设计想要的形状)。
`registry.cppm:428` 的判据本身没问题。问题是**这个平台特例的代价被摊开了**:
lifecycle.cppm **13 处**、prepare.cppm **13 处** msvc 专有分支,
而 gcc / llvm / mingw 在 lifecycle.cppm 里**一处都没有**
(mingw 更是零分支 —— 它整个被表达成一个 *target*,`x86_64-windows-gnu`,
那才是这套设计想要的形状)。

其中直接由 `is_system_toolchain` 触发的四处:
其中由 `is_system_toolchain` 直接触发的四处:

| 位置 | 内容 |
|---|---|
Expand All @@ -69,59 +68,30 @@ lifecycle.cppm 里**一处都没有**(mingw 更是零分支 —— 它整个被
| `lifecycle.cppm:931` | `toolchain remove` 拒绝 |
| `prepare.cppm:1260` | 构建期短路整条 xim 路径 |

另外三处是同一条规则的**重复拼写**,值得单独记:
`prepare.cppm:1257` 那个变量叫 `tcSpecIsMsvc`,但它其实是 `is_system_toolchain`
的结果 —— 名字说的是"族",值说的是"来源"。

- `xim_tool()` + `installation_at()` + 那段错误信息,在
`prepare.cppm:1322-1341` 和 `lifecycle.cppm:730-755` **几乎逐字重复**。
第三份出现的时候就是下一个 #436。
- sysroot 依赖规则有两份:`prepare.cppm:1471-1480`(只看 musl)
vs `lifecycle.cppm:694-702`(musl + PE + 宿主),**两者并不等价**,
而 `lifecycle.cppm:692` 的注释声称它们互相对应。
- `prepare.cppm:1002-1005` 那段注释说解析是 4 步,实际链路有 **9 个输入**
(manifest → `--toolchain` → 全局 default → Windows 无 VS 的 target 种子 →
`[target.X].toolchain` → 词表 pin → 四个互斥的获取分支 → MSVC ABI 修复门 →
`build.mcpp` 的宿主解析)。注释比代码老,而这段代码是**决定用哪个编译器**的。
另外三处是同一条规则的**重复拼写**,和特例本身无关,纯粹是复制:

`prepare.cppm:1257` 那个变量还叫 `tcSpecIsMsvc`,但它其实是
`is_system_toolchain` 的结果 —— 名字说的是"族",值说的是"来源"。
**名字和值不是一回事,这正是这一轮所有缺陷的形状。**

### 建议:**收拢这个特例,而不是推广它**
1. `xim_tool()` + `installation_at()` + 错误信息 —— `prepare.cppm:1322-1341`
与 `lifecycle.cppm:730-755` 几乎逐字重复 → 提一个
`resolve_managed_msvc(env, pkg)`。第三份出现的时候就是下一个 #436。
2. sysroot 依赖规则两份且**不等价**(`prepare.cppm:1471-1480` 只看 musl,
vs `lifecycle.cppm:694-702` musl + PE + 宿主)→ 合成一个谓词。
`lifecycle.cppm:692` 的注释声称它们互相对应。
3. `prepare.cppm:1002-1005` 的注释说解析是 4 步,实际链路有 **9 个输入**。
注释比代码老,而这段代码是**决定用哪个编译器**的。

保持 `@system` 只有 MSVC 能用(甚至值得在 `parse_toolchain_spec` 里
**显式拒绝** `gcc@system`,并给一句"xlings 不依赖 host 工具链,用
`gcc@<version>`"—— 让原则**可执行**,而不是靠没人去写)。
### 建议:收拢,不推广

要减的是**摊开的代价**,做法是把"来源"变成一个显式的、解析一次的东西:
"来源"变成一个显式的、**解析一次**的东西,而不是每个子命令自己再问一遍:

```cpp
enum class Origin { Managed, SystemMsvc }; // 只有两种,而且只会有两种

struct ResolvedToolchain {
Origin origin;
std::filesystem::path compiler; // payload 里的 cl/gcc,或机器上的 cl
// ……
};
```

`prepare` / `lifecycle` 各自解析一次 `Origin`,后面按它分发,而不是每个子命令
自己再问一遍 `is_system_toolchain`。

配套把三处**重复拼写**消掉(这几处和特例本身无关,纯粹是复制):

1. `xim_tool()` + `installation_at()` + 错误信息 —— `prepare.cppm:1322-1341`
与 `lifecycle.cppm:730-755` 几乎逐字重复 → 提一个
`resolve_managed_msvc(env, pkg)`。
2. sysroot 依赖规则两份且**不等价**(`prepare.cppm:1471-1480` vs
`lifecycle.cppm:694-702`)→ 合成一个谓词。
3. `prepare.cppm:1002-1005` 的注释说 4 步、实际 9 个输入 → 要么补齐,
要么把顺序变成一张数据表,让注释无处可撒谎。

**收益**:特例仍然是特例(符合原则),但只在**一处**被认出来;
26 处分支里绝大多数变成"按 Origin 分发"的普通代码。
`gcc@system` 从"没实现"变成"**明确拒绝并说明理由**"。

---
`prepare` / `lifecycle` 各解析一次,之后按它分发。特例仍然是特例,
但只在**一处**被认出来。

## 2. SDK 是"依赖",却被当成"搜索路径" ⭐ 高优先级

Expand Down Expand Up @@ -319,6 +289,31 @@ ELF 上靠 `ldd` 闭包歪打正着,PE 上则完全没有这一步。

---

## 3e. 主构建和 host helper 用不同的链接策略(#437) 🔴 已修

同一个工具链、同一个环境,两条链接路径给出不同结果:

- **主构建**在 macOS 上刻意 `-fuse-ld=lld`(`flags.cppm` 的 macOS 分支,
注释原文:*"Xcode 15.4's ld aborting at launch ... when its libc++
resolution was diverted"*);
- **host helper**(`build.mcpp`)走 `hostflags.cppm` 的 trust-cfg 分支,
**返回空 link token**,于是用 Xcode 的 `/usr/bin/ld` —— 那个 ld 自己链 libc++,
跑在 payload 的 `DYLD_*` 里,dyld 把它的 libc++ 解析到 payload 那份,
缺 `__ZdaPv`,**还没开始链接就 abort**。

> **mcpp 用 llvm 能把整个工程构建出来,却构建不了自己的 `build.mcpp`** ——
> 这不是 macOS 环境问题,是两条路径没对齐。主构建早就绕过了这个坑,
> host helper 这条没跟上。

修法:trust-cfg 分支在 macOS 上也追加 `-fuse-ld=lld`。cfg 选的是 runtime,
**从来没选过链接器**。

**它和这份 review 其余部分是同一个形状**:一条规则有两处实现,其中一处知道
真相、另一处不知道 —— 和 §1 那三处重复拼写、§3b 的 doctor 第四份拷贝完全同类。
根因记在 `2026-08-13-build-optimization-status.md` §9a-3。

---

## 4. `installed()` 的语义:必须是"这份 recipe 产出的状态" ⭐ 跨仓库规则

这一轮九层缺陷里,真正致命的几层全是这一条。
Expand Down Expand Up @@ -411,6 +406,27 @@ mcpp 有 —— 否则每个消费者都要自己重写一遍,而写错的方式

---

## 8b. 落地状态(2026-08-16 当天)

| 项 | 状态 |
|---|---|
| §3b doctor 用 `payload_frontend` | ✅ mcpp#440 已合入 |
| §3c 拆开 `has_usable_msvc` → `msvc_available_here` | ✅ mcpp#440 已合入 |
| §3 `vc_redist_dir` → `mcpp run` 的 PATH(5 个测试) | ✅ mcpp#440 已合入 |
| §3e host helper 链接策略对齐(#437) | 🔄 mcpp#442 CI 中 |
| §4 `installed()` 规则写进 skill + lint(3 条各自对着回归跑过) | ✅ index#640 已合入 |
| §5 windows-test 真的运行声明的程序 | ✅ index#640 已合入 |
| §6 park-and-sweep 下沉到 xlings 卸载路径 | 🔄 xlings#551 CI 中 |
| §1 收拢 Origin + 消三处重复 | ⬜ 未做(动 spec 层,单独一轮) |
| §2 SDK 绑定 + `windows_sdk` 版本轴 | ⬜ 未做(要改 manifest schema) |
| §3d PE 版 `pack` + 让 pack 读 Contract | ⬜ 未做(最大一块) |
| §7 索引发布修订号 | ⬜ 未做(跨仓库) |

**没做的四项都有共同点**:要么动 spec/schema 层,要么跨仓库 —— 都不适合在
一次发版窗口里塞进去。§1 §2 §3d 三条在这份文档里已经写到可以直接开工的程度。

---

## 9. 建议的落地顺序

| # | 项 | 规模 | 影响面 | 依赖 |
Expand All @@ -422,15 +438,16 @@ mcpp 有 —— 否则每个消费者都要自己重写一遍,而写错的方式
| 4 | §3c 拆开 `has_usable_msvc` | 小 | mcpp | 无 |
| 5 | §2 SDK 绑定 + 版本轴 | 中 | mcpp | 无 |
| 6 | §6 park-and-sweep 下沉 | 小 | xlings | 无 |
| 7 | §1 收拢 Origin + 消三处重复(**不**通用化 `@system`) | 大 | mcpp | 建议在 §2/§3c 之后 —— 都动 spec 层 |
| 7 | §1 收拢 Origin + 消三处重复 | 大 | mcpp | 建议在 §2/§3c 之后 —— 都动 spec 层 |
| 8 | §3d PE 版 `pack` | 大 | mcpp | 排在 §3 之后:§3 先解决"能跑" |
| 9 | §7 索引修订号 | 大 | xlings + 索引 | 收益最分散 |

第 0–4 项互不依赖,可以并行,合计不大。§1 和 §3d 是两块真正的工作量,
建议各自单独一轮 —— 尤其 §1 动的是 spec 层,改完之后其余几项的 diff 会更小。

⚠️ §1 的方向已修正:**收拢特例,不是推广它**。初稿建议的 `gcc@system`
与 xlings"能不依赖 host 就不依赖 host"的原则相悖,已作废。
⚠️ §1 初稿的 `gcc@system` 提案已整段作废(mcpp#441 已关闭)——
那是我读错了设计意图凭空造出来的东西。xlings 是用户态 OS,
host 依赖是要**降到最低**的,不是要补齐的能力。

**如果只做三件**:§3b(一行)、§4(规则,挡住的最多)、§3(让干净 Windows
上 `mcpp run` 真的能跑)。
Expand Down
25 changes: 24 additions & 1 deletion src/toolchain/hostflags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,30 @@ std::vector<std::string> host_link_tokens(const Toolchain& tc,
if (bypassCfg) {
for (auto& t : dm.link_tokens(esc)) out.push_back(t);
} else if (dm.hasCfg) {
return out; // trusting the cfg: it already selects the linker/runtimes
// Trusting the cfg — with ONE exception, and it is not a preference.
//
// The cfg picks runtimes; it does not pick a linker, so on macOS the
// default is Xcode's /usr/bin/ld. That binary is itself a C++ Mach-O
// linked against libc++, and it runs inside the same DYLD_* the
// payload toolchain sets up, so dyld resolves ITS libc++ to the
// payload's. When that copy lacks a symbol Apple's ld needs, ld
// aborts before it links anything:
//
// dyld: Symbol not found: __ZdaPv (operator delete[])
// Referenced from: .../XcodeDefault.xctoolchain/usr/bin/ld
// Expected in: .../xim-x-llvm/22.1.8/lib/libc++.1.0.dylib
//
// The MAIN build already refuses to use Xcode's ld for exactly this,
// and says so at flags.cppm's macOS branch: "Xcode 15.4's ld aborting
// at launch on macos-14 CI when its libc++ resolution was diverted".
// The host helper links in the same environment, so it cannot be
// allowed to differ — a toolchain that builds the project but not its
// build.mcpp is not a working toolchain (mcpp#437).
//
// lld ships with the very toolchain doing the compile, so it cannot
// be diverted to a libc++ it was not built against.
if constexpr (mcpp::platform::is_macos) out.push_back("-fuse-ld=lld");
return out;
}

for (auto& t : lm.link_tokens(esc)) out.push_back(t);
Expand Down
Loading