feat: 彻底移除 xmake,改用 mcpp Provider 驱动练习#84
Draft
Sunrisepeak wants to merge 7 commits into
Draft
Conversation
d2x 改为通用练习框架后,构建工具由具体课程实现。本 Provider 用 C++26 + mcpp 编写——教 C++ 的仓库用 C++ 写自己的工具,且 mcpp 本就是 必需品,零新增运行时依赖。 发现方式刻意不引入声明文件:真相只有目录结构和练习文件自己的头部注释 (// d2x:cxxflags:)。rustlings PR #1355 的教训是 edition 同时写在 rustc 参数和 rust-project.json 里、两边漂移酿成 bug——任何独立声明文件都是 第二套真相源。 双 member 布局解决一个硬约束:dslings 的练习默认就编译不过,而 mcpp 的 build/run 都会先全量构建整个包,一个坏兄弟拖垮全部且零产物。于是 .d2x/build/<std>/ 持全量 target 供 clangd 拿到完整 compile_commands.json, .d2x/build/_current/ 每次只写当前一题供 checker。清单写入前做内容比对, 不推进 mtime,mcpp 的快速路径得以保留。 练习源文件原地不动:main 用 ../ 逃逸出包根。 引导零脚本、Windows 安全:.d2x.json 指向 `mcpp run -q -p d2x/buildtools/mcpp --`,从仓库根执行无需 cd,首次自动 构建 Provider。 runner 在每次 spawn 前清 LD_LIBRARY_PATH:mcpp run 会把它指向自己私有的 glibc 并注入子进程,嵌套的 mcpp 被迫加载错配运行时后会在动态链接器里 段错误(冷启动稳定复现)。d2x 侧对同一问题早有相同处理。 tests/e2e.sh 断言每个参考答案通过、每个练习不通过——rustlings `dev check --require-solutions` 的等价物。实测 51/51 通过。
d2x 改为通用练习框架、构建由 mcpp Provider 承担后,xmake 已无调用方。 删除 11 个 xmake.lua(根 / dslings / dslings/en / solutions 各级)、 d2x/buildtools/xmake/ 插件、d2x/cpp/common.lua(xmake 侧的语言探测辅助, 早已被 main.lua 读 .d2x.json 取代)、以及只写着 TODO 的 cmake 适配器占位。 d2x/buildtools/README.md 一并删除——它描述的 list/build/run 契约已被 Provider 协议(describe/exercises/check)取代,留着会误导。 .xlings.json 去掉 xmake 3.0.7 的 pin(该版本已从 registry 移除,是 d2x CI 里那条 "用真 xmake 而非 xlings shim" workaround 的根因),同时去掉 gcc 与 mingw-w64 —— mcpp 自带工具链沙箱,Windows 侧由它的 MinGW/MSVC 覆盖。 CI 重写:旧流水线只挑 `-ref` 结尾的 xmake target,而 solutions/ 在 xmake.lua 里被注释掉,grep 返回空、循环全跳过、job 退出 0 —— 实际校验零个 目标,且第 60 行的 wc -l 对空串仍打印 "Found 1 reference targets" 把空转 掩盖了。新流水线跑 e2e.sh,断言每个参考答案通过、每个练习不通过,走的是 `d2x checker` 内部同一条 Provider 路径。 撰稿 skill 同步更新:练习不再需要注册,放进 dslings/<std>/ 即可被目录约定 发现;per-exercise 编译选项改为练习文件头部的 // d2x:cxxflags: 指令。 复验:Provider 枚举 52 题、端到端 51/51 参考答案通过 0 失败、d2x checker 全链路正常。
判定原先 100% 是带内信号:断言失败并不改变退出码(实测退出码仍为 0), 所以只能扫 stdout 找 ❌。这会误判——一个断言全过的正确解答,只要在说明 文字里打了个 ❌ 就被判失败(已复现)。反过来,输出截断或 Windows 控制台 代码页不对,判定同样失效。 改为:stdout 回归「给人看」,判定走侧信道 NDJSON。 harness 独立成 mcpp 库包 dslings/harness/,同时提供两条路径: - #include <d2x/cpp/common.hpp> 宏路径。可见输出与旧版逐字节一致, ✅/❌ 的逐条对照是教学的一部分,不动。 - import d2x.harness; 模块路径。用 std::source_location 自动带 file/line,比 __LINE__ 更准,正好是 Verdict.diagnostics 需要的东西。 两条路径不等价,这是 C++ 的硬约束而非疏漏:宏无法跨模块导出,所以 D2X_YOUR_ANSWER 没有模块等价物——它必须展开为空才能制造编译错误。 模块化章节需要另一套填空约定。 include/ 下保持 d2x/cpp/common.hpp 这个既有路径,104 个练习和书本一行 不用改;路径改名可以以后单独做,不必和机制改造捆在一起。 生成的 member 清单改用 [dependencies] path 依赖,干掉了 include_dirs = ["../../.."] 那个把整个仓库根塞进搜索路径的 hack—— 原先练习能 #include 仓库里任何文件。 侧信道逐条追加而非退出时统一写:练习段错误时崩溃前的断言结果照样保留。 D2X_RESULT_FILE 未设置时 harness 只打印不写文件,学员直接跑二进制零摩擦。 Provider 判定顺序:有 ok:false → Fail 且每条失败转成一个 Diagnostic; 无失败但有 wait → Blocked;侧信道文件不存在 → 退回「编译通过 + 退出 0」。 最后一条让 harness 自动变成可选的——纯观察型练习(现有 18 个只用 D2X_WAIT)可以写成零依赖的纯 C++ 文件,学员能原样拷进 Compiler Explorer。 顺带删掉死代码 d2x_is_invocable(104 个练习中 0 处使用)和坏掉的模块桩 d2x/mcpp/common.cppm(宏不能跨模块导出,import 进去什么也拿不到)。 e2e.sh 的清理钩子收窄为逐个练习目录:harness 现在也住在 dslings/ 下, 原先的 `git checkout -- dslings/` 会把开发中的改动一起抹掉(已踩过)。 实测:51/51 参考答案通过;四态判定正确(未完成→fail、参考答案→pass、 答案对且输出含❌→pass、答案对留路障→blocked);真实断言失败产出带 行号与期望/实际值的 diagnostics;模块路径与「无 harness 退回退出码」均通过。
三个真缺陷,都不是理论风险:
1. 练习 id 注入。id 直接取自文件名,有两个危险去向:d2x 把它拼进 shell
命令,我们把它写进生成的 TOML([targets.<id>])。带反引号、`]` 或引号
的文件名在任一处都能越界 —— 对社区课程仓库来说,一个恶意 PR 文件名就
足以在任何跑 checker 的人机器上执行命令。
在 discovery 源头做白名单校验并拒绝,而不是在两个下游各自转义:这类
文件名本就是笔误或恶意,与其想办法安全地传递,不如让作者改名。
实测:`99-evil`touch pwned_marker`.cpp` 被拒绝,命令未执行。
2. e2e.sh 把所有英文参考答案静默 SKIP。前缀剥离顺序错了 —— `${sol#en/}`
执行时 sol 已经以 "solutions/" 开头,匹配不到任何东西,是个静默 no-op,
于是每道英文练习都因找不到 solutions/en/... 而跳过。
这正是本脚本头部注释里说要防的那种空转,和旧 CI 一模一样的毛病。
除了修顺序,另加一道防线:pass==0 时直接判失败,杜绝「0 失败」蒙混。
实测修复后 en 也是 51/51 真验证(此前是 0 通过 / 52 跳过)。
3. d2x_assert_eq 的日志分支仍用裸 std::to_string,而上报分支已改用
SFINAE 安全的 show()。std::to_string 没有 std::string / const char* /
scoped enum 的重载,下一个比较字符串或强类型枚举的练习会直接编译失败 ——
show() 存在的意义就是避免这个,却只用了一半。
教学漂移(04-rvalue-references):
C++17 起 prvalue 直接初始化目标(保证复制省略),-fno-elide-constructors
再也无法让 `Object obj = Object();` 产生移动构造。全仓库改按 c++23 编译后,
这节课的核心观测点被静默抹掉。
改成从具名对象 std::move —— 这在任何标准下都必然调用移动构造。更重要的是
补了一条 d2x_assert(move_ctor_calls >= 1):漂移之所以能静默发生,正是因为
从前没有任何断言检查它,输出少一行没人发现。现在它是 CI 可检测的事实。
上一次提交只带上了 solutions/ 侧 —— 练习侧的补丁被 e2e.sh 的清理钩子 还原掉了,我没复查就提交了。 清理钩子会把参考答案覆盖到练习上再还原,所以它天然会吃掉练习目录里未提交 的改动。这个陷阱已经咬过两次(一次丢了脚手架,一次丢了刚修好的练习), 所以加一道前置检查:练习目录不干净就拒绝运行,并列出是哪些文件。 宁可拒绝,也不能悄悄丢掉别人的工作。
学员看到的断言报错原先顶着一长串 /home/... 前缀 —— mcpp 传给编译器的是 绝对路径,__FILE__ 便是绝对的。生成的清单加上 -fmacro-prefix-map 让它 相对仓库根,噪声消失。 但协议要求绝对路径:d2x 靠 diagnostics.file 打开编辑器、监听文件变更。 所以在协议边界上还原成绝对。展示归展示,定位归定位。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
d2x 改为通用框架后,构建工具由具体课程实现。本 PR 用 C++26 + mcpp 写了 d2mcpp 自己的 Provider,并彻底移除 xmake。
可行性论证与 rustlings/cargo 横向对照见
.agents/docs/2026-07-19-mcpp-replace-xmake-research.md。主要变更
新增
d2x/buildtools/mcpp/—— Provider(C++26):目录约定发现练习、生成 mcpp 清单、调用 mcpp、判定通过dslings/harness/—— 练习脚手架独立成 mcpp 库包,同时提供#include与import两条路径d2x/buildtools/mcpp/tests/e2e.sh—— 断言每个参考答案通过、每个练习不通过删除:11 个
xmake.lua、d2x/buildtools/xmake/插件、d2x/cpp/common.lua、只写着 TODO 的 cmake 占位、以及描述旧契约的d2x/buildtools/README.md。.xlings.json去掉 xmake 3.0.7 的 pin(该版本已从 registry 移除,正是 d2x CI 里那条 workaround 的根因),同时去掉 gcc / mingw-w64 —— mcpp 自带工具链沙箱。关键设计
目录约定,无声明文件。 放一个
.cpp进去就是全部工作。rustlings PR #1355 的教训是 edition 同时写在两处导致漂移 —— 任何独立声明文件都是第二套真相源。每练习编译选项写在文件头部// d2x:cxxflags:指令里。双 member 布局。 dslings 的练习默认就编译不过,而 mcpp 的 build/run 都会先全量构建整个包,一个坏兄弟拖垮全部且零产物。所以
.d2x/build/<std>/持全量 target 供 clangd 拿到完整compile_commands.json,.d2x/build/_current/每次只写当前一题供 checker。练习源文件原地不动(main用../逃逸出包根)。判定改走侧信道。 原先断言失败不改变退出码(实测 exit=0),只能扫 stdout 找
❌—— 一个断言全过的正确解答只要在说明文字里打了个❌就被判失败(已复现)。现在 stdout 回归"给人看",判定读 NDJSON 侧信道,逐条追加所以段错误前的结果也保留。侧信道文件不存在时退回"编译通过 + 退出 0",于是脚手架自动变成可选的 —— 纯观察型练习可以是零依赖的纯 C++ 文件。修复的缺陷
e2e.sh把所有英文参考答案静默 SKIP:前缀剥离顺序错,${sol#en/}是个 no-op。这正是本脚本存在的理由所要防的那种空转,和旧 CI 一模一样的毛病。 除修顺序外加防线:pass == 0直接判失败。d2x_assert_eq的日志分支仍用裸std::to_string,而上报分支已改用 SFINAE 安全的show()—— 下一个比较字符串或 scoped enum 的练习会硬编译失败。mcpp run泄漏LD_LIBRARY_PATH导致嵌套 mcpp 段错误(冷启动稳定复现)。已在 Provider 侧unsetenv绕过。这是 mcpp 的既有问题,建议单独向上游报。-fmacro-prefix-map让__FILE__相对仓库根,但协议边界上仍还原为绝对(d2x 靠它开编辑器、监听文件)。CI 重写
旧流水线是静默空转的:只挑
-ref结尾的 xmake target,而solutions/在xmake.lua里被注释掉,grep 返回空、循环全跳过、job 退出 0 —— 实际校验零个目标,第 60 行的wc -l对空串还打印 "Found 1 reference targets" 把空转掩盖了。新流水线跑
e2e.sh,走d2x checker内部同一条 Provider 路径。教学漂移
全仓库按 c++23 编译后,
04-rvalue-references的移动构造观测点被 C++17 保证复制省略静默抹掉。改为从具名对象std::move,并补d2x_assert(move_ctor_calls >= 1)。更重要的是那条断言 —— 漂移之所以能静默发生,正是因为从前没有任何断言检查它。量化过:51 个参考答案在「原标准 vs c++23」下逐一 diff,仅此 1 个漂移。
验证
Provider 枚举 52 题 · 端到端 zh 51/51、en 51/51 · 四态判定正确 · 注入防护生效 · 与 d2x 全链路联调通过。
已知缺口(未在本 PR 解决)
D2X_YOUR_ANSWER是宏,跨不了模块边界,cpp20/cpp23 章节需先决策hello-mcpp无参考答案故 e2e 只能 SKIP 它详见
.agents/docs/2026-07-20-mcpp-provider-reference.md。