Skip to content

Latest commit

 

History

History
240 lines (188 loc) · 12.2 KB

File metadata and controls

240 lines (188 loc) · 12.2 KB

宿主编译单一生产者 — 实施计划

设计:.agents/docs/2026-08-02-host-compile-single-producer-design.md 基线:main @ 7169332(2026.8.2.1 已发布)· 本批次发 2026.8.2.2

Goal: 让「宿主编译 flag」只有一个生产者,token 是真源、字符串由 token 渲染; build.mcpp 因此自动获得主构建的全部能力(方言、模块、引号、deployment target), MSVC 下的模块门可以删除。


Global Constraints

  • 版本号只改两处:mcpp.toml:3src/toolchain/fingerprint.cppm:212026.8.2.2.xlings.json 是 bootstrap pin,发布并上架后才单独 bump。
  • 第 1 阶段是纯重构,必须零行为变化:归一化 build.ninja diff 为空, 且 stdmodstd_build_commands 逐字节相同 —— 否则所有用户的 std BMI 缓存 失效重编(缓存目录名派生自含该命令的元数据,stdmod.cppm:102,135)。
  • 新模块独立:生产者放 src/toolchain/hostflags.cppm,不往 build_program.cppm 的匿名 namespace 加函数(设计 §6.2)。
  • 差异必须显式:三处装配今天有真实分歧(见下表),共享后要成为具名选项, 不是被抹平,也不是保留成无理由的开关。
  • 每轮验证双工具链 + 清缓存:rm -rf target && mcpp cache clean, gcc@16.1.0 与 llvm@22.1.8 各跑一遍;先在基线 commit 上做同法对照。

今天的三处分歧(必须保留并显式化)

关注点 flags.cppm build_program.cppm stdmod.cppm
clang 有 cfg 时是否绕过 全平台绕过 只在 Linux 绕过;macOS/Windows 信 cfg 并返回空(:214-221 有理由:macOS 链接要 needs_explicit_libcxx 那条路负责 libc++abi/unwind) 全平台绕过(:232--no-default-config 在所有平台安全)
binutils -B 有(非 musl/mingw 且 libstdc++) 有(非 musl)
tc.linkRuntimeDirs depRuntimeLibraryDirs 另一条路 -L +(仅 ELF)-rpath
deployment target 有(#332 才补)

⇒ 选项面:clangCfgBypass{Always,LinuxOnly}binutilsB{bool}runtimeDirs{bool}每个选项在头文件里必须写清"谁需要它、为什么",否则 就是把三处推导变成一处推导 + 三个魔法开关。


Task 0:分支与版本号

  • 建分支 feat/host-compile-single-producer
  • mcpp.toml:3fingerprint.cppm:212026.8.2.2;.xlings.json 不动
  • bash .github/tools/check_version_pins.sh 通过
  • 提交

Task 1:基线快照(零 diff 的对照物)

动任何代码之前先取基线,否则无法证明零 diff。

  • Step 1: 对 gcc 与 llvm 各生成一次 build.ninja 并归一化保存
snap() {  # $1 = 标签
  rm -rf target; mcpp cache clean >/dev/null
  mcpp build >/dev/null
  find target -name build.ninja | head -1 | xargs cat \
    | sed -E 's#/[^ ]*/target/[0-9a-f]{16}#<FP>#g' > /tmp/ninja-$1.txt
}
snap base-gcc
sed -i 's|^default = "gcc@16.1.0"$|default = "llvm@22.1.8"|' mcpp.toml
snap base-llvm
git checkout -- mcpp.toml
  • Step 2: 保存 std_build_commands 基线
find ~/.mcpp/build-cache -name '*.json' -path '*std*' \
  | xargs -I{} sh -c 'python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print(sys.argv[1]);print(d.get(\"std_build_commands\"))" {}' \
  > /tmp/stdcmd-base.txt

Task 2:token 优先的 seam(linkmodel.cppm)

Files: src/toolchain/linkmodel.cppm

Produces:

  • std::vector<std::string> LinkModel::compile_tokens(const PathEscape&) const

  • std::vector<std::string> ClangDriverModel::compile_tokens(const PathEscape&) const

  • 现有 compile_flags() 改为 render_tokens(compile_tokens(esc))

  • Step 1:render_tokens(每个 token 前置一个空格后拼接)

// token 是真源,字符串是渲染结果 —— 不是反过来。历史上只有字符串形态,
// 于是需要 argv 的消费者(build_program)只能自己重写一遍装配;
// #332 的 split_ws 是同一个形状的产物。
inline std::string render_tokens(const std::vector<std::string>& tokens) {
    std::string out;
    for (auto const& t : tokens) { out += ' '; out += t; }
    return out;
}
  • Step 2: LinkModel::compile_tokens —— 把现有 compile_flags 逐句改成 push_back,顺序不变:--sysroot=<p>、每个 -isystem<p> / -idirafter<p>
  • Step 3: ClangDriverModel::compile_tokens —— --no-default-config-nostdinc++(两个独立 token)、每个 -isystem<p>
  • Step 4: 两个 compile_flags 改为 return render_tokens(compile_tokens(esc));
  • Step 5: 单测:对同一 LinkModel / ClangDriverModel, render_tokens(compile_tokens(esc)) == 旧实现的字符串(把旧实现内联进测试当预言)
  • Step 6: 提交

Task 3:hostflags.cppm 生产者

Files: 新建 src/toolchain/hostflags.cppm

Produces:

export module mcpp.toolchain.hostflags;

struct HostFlagOptions {
    // clang 自带 cfg 时是否绕过它。主构建全平台绕过(可复现、不依赖
    // 安装期生成物);build.mcpp 的宿主 helper 只在 Linux 绕过 —— macOS 上
    // 链接 libc++abi/unwind 由主构建的 needs_explicit_libcxx 负责,helper
    // 自己重复一遍会产生 undefined __cxa_*(build_program.cppm 原注释)。
    enum class CfgBypass { Always, LinuxOnly } cfgBypass = CfgBypass::Always;
    // binutils -B:GCC/libstdc++ payload 才需要(musl 与 mingw 自带 as/ld)。
    bool binutilsPrefix = true;
    // tc.linkRuntimeDirs 的 -L(+ELF 上的 rpath):让产物能加载私有运行库。
    bool runtimeLibDirs = false;
    std::string macosDeploymentTarget;   // 已解析值;空 = 不发
};

// 宿主编译 flag,token 形态。三个消费者的唯一生产者。
std::vector<std::string> host_compile_tokens(const Toolchain& tc,
                                             const HostFlagOptions& opt,
                                             const PathEscape& esc);
  • Step 1: 实现,顺序严格照抄 flags.cppm:319-352 的现有顺序 (dm → deployment → lm),因为顺序影响渲染出的字符串
  • Step 2: 单测 tests/unit/test_hostflags.cpp: - 每个 CompilerId 都返回自洽结果(方言可取、无空 token) - CfgBypass::LinuxOnly 在非 Linux 上对 clang 返回空 - deployment target 只在 macOS 且非空时出现
  • Step 3: 提交

Task 4:三个消费者接入(零 diff 验收)

  • Step 1: build_program.cppm::host_base_flags → 调 host_compile_tokens(tc, {CfgBypass::LinuxOnly, !isMusl, true, dt}, plainEsc), 函数体删空。注意 plainEsc 是恒等转义(argv 不需要 ninja/shell 转义)。
  • Step 2: stdmod.cppm:238-253render_tokens(host_compile_tokens(tc, {CfgBypass::Always, false, false, dt}, shellEsc))
  • Step 3: flags.cppm:319-337编译侧render_tokens( host_compile_tokens(tc, {CfgBypass::Always, …, false, dt}, ninjaEsc)); 链接侧(link_toolchain_flags / f.sysroot / llvmRootForStdlib)保持不动
  • Step 4: 零 diff 验收
snap after-gcc;  diff /tmp/ninja-base-gcc.txt  /tmp/ninja-after-gcc.txt   # 必须空
snap after-llvm; diff /tmp/ninja-base-llvm.txt /tmp/ninja-after-llvm.txt  # 必须空
# std_build_commands 逐字节
diff /tmp/stdcmd-base.txt /tmp/stdcmd-after.txt                            # 必须空

非空即说明抽错了 —— 回到 Task 2/3 找顺序或分支差异,不要改基线迁就。

  • Step 5: 单测 + e2e(89/92/110/111/112/124/125/143/144/145/164/168/179/181), gcc 与 llvm 各一遍
  • Step 6: 提交

Task 5:host_program_argv + 删 MSVC 模块门

Files: src/toolchain/hostflags.cppm(或新建 hostprogram.cppm)、 src/build/build_program.cppm

  • Step 1: 把 build.mcpp 的 argv 组装(方言、forceCxxLangArgv、输出前缀、 静态运行时、模块 BMI 处理)收进生产者侧,build_program.cppm 只负责 「读源文件 → 判定 imports → 调用 → 执行 → 解析指令」
  • Step 2: 模块处理按 bmi_traits(tc) 分派,含 MSVC 的 /interface /TP /ifcOutput + /reference <name>=<ifc> —— 与主构建同一张表,不新写
  • Step 3: 删除 build_program.cppmimport mcpp; / import std; not yet supported under MSVC 那道门
  • Step 4: tests/e2e/180_msvc_build_mcpp.sh 第三段: 从「断言拒绝」改为「断言 import std; + import mcpp; 可用」
  • Step 5: e2e 181 参数化补 MSVC 一列(# requires: windows msvc, 单独脚本 183 或在 180 内)
  • Step 6: 提交

Task 6:能力矩阵守卫

  • Step 1: tests/unit/test_hostflags.cpp 增一条:遍历所有已知 CompilerId,断言 host_compile_tokens + dialect_for + bmi_traits 三者都能给出完整答案(方言 id 非空、forceCxxLangArgv 非空、 outputExePrefix 非空、bmi 的 bmiDir/bmiExt 非空)。 新增工具链族忘了接 = 测试失败,而不是运行期 not yet supported
  • Step 2: 提交

Task 7:文档

  • docs/07-build-mcpp.md / docs/zh/07-build-mcpp.md:删掉 「MSVC 下不支持 import」的说明,改为陈述已支持
  • 设计文档 §5 能力矩阵勾掉 MSVC 一列
  • 提交

Task 8:PR → CI 全绿 → 合入

  • 本机全量:mcpp testtests/e2e/run_all.shcheck_version_pins.sh
  • 开 PR,盯 CI(重点看 macOS/Windows:零 diff 只在本机证过两条工具链, Windows 的 MSVC 腿只有 CI 能验)
  • gh pr merge --squash --admin --delete-branch

Task 9:Release + 生态闭环

  • 触发 release.yml,四平台产物
  • 镜像 xlings-res 双端;失败则本地 gtc 补传 (token 在 ~/.config/gitcode-tool/config.json,用 repo 的 gtc + /usr/bin/python3)
  • 两端独立 GET + sha256 核验(不信 workflow 绿灯)
  • xim-pkgindex bump PR(Sunrisepeak 账号合),sha256 逐个对照
  • 隔离 workspace 真装 2026.8.2.2,跑 e2e 179/181/89/112
  • 包上架后才 bump .xlings.json bootstrap pin,单独 PR (索引 artifact 传播滞后会让这个 PR 假红:not found ≠ 回归,等传播后重跑)

实施记录(只有 CI 能发现的)

发现 教训
扩大 build_program.cppm 的匿名 ns 同样触发 clang 误编译 build_mcpp_module 在原地改写加大 → macOS 全部 build.mcpp e2e 段错误,和 PR#332 一模一样。约束不是「别加新函数」,是**「别再往那个 ns 加代码」**。修法=整块搬到 src/build/hostprogram.cppm
「扫缓存比对字节等价」是假验证 std 缓存共享且累积,两次快照都含陈旧条目,diff 恒为空 —— 我因此放过了一次真实的字符串改动(-stdlib=libc++ 位置)。把主张写成测试:用合成的 ClangDriverModel/ToolchainLinkModel 直接断言渲染出的字面串
-stdlib=libc++ 的位置是兼容面 它进 std_build_commands → 进 metadata → 决定 std 缓存目录名。挪一个 flag = 让每个用户的 std BMI 全量失效
「信任 cfg」不等于「什么都不发」 生产者在 trust-cfg 分支提前 return,把 deployment target 也跳过了;而 macOS 上 build.mcpp 走的正是这条分支 → std BMI 配置不匹配。旧的手写实现把它放在最前、无条件,注释还专门写了 "FIRST and unconditionally" —— 重构时要读懂那句话为什么在
删掉能力门 = 死代码变活代码 -x none 常年无条件发出,只因 MSVC 到不了那里才无害。门一删,cl 立刻 D9002: ignoring unknown option '-x'删门时要把门后所有「反正到不了」的分支重新审一遍

Self-Review

设计覆盖:§4.1 token 生产者 → Task 2/3;§4.2 三种渲染 → Task 4; §4.3 build.mcpp 一等消费者 → Task 5;§5 能力矩阵 → Task 5/6; §6.1 字节等价 → Task 1 + Task 4 Step 4;§6.2 独立模块 → Task 3 约束; §6.4 双工具链验证 → Global Constraints。

已知风险:compute_flags 服务每一次构建,是全仓库 blast radius 最大的函数 之一。缓解就是零 diff 硬约束 —— 它把"重构对不对"变成一个可机器判定的问题。