Skip to content

Commit e68706e

Browse files
committed
docs: 添加 Provider 与练习脚手架参考(目录约定、判定机制、已知缺口)
1 parent 58d3645 commit e68706e

1 file changed

Lines changed: 262 additions & 0 deletions

File tree

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# d2mcpp Provider 与练习脚手架 参考
2+
3+
- 日期:2026-07-20
4+
- 分支:`feat/mcpp-provider`
5+
- 配套调研:[`2026-07-19-mcpp-replace-xmake-research.md`](2026-07-19-mcpp-replace-xmake-research.md)(xmake → mcpp 的可行性论证与 rustlings/cargo 横向对照)
6+
- d2x 侧协议规范:d2x 仓库 `.agents/docs/2026-07-20-d2x-architecture-reference.md`
7+
- 本文定位:**当前实现的参考手册**——目录约定、生成物、判定机制、维护须知
8+
9+
---
10+
11+
## 1. 全景
12+
13+
xmake 已完全退役。构建链路是:
14+
15+
```
16+
d2x(通用框架,零内置后端)
17+
└─ Provider Protocol (NDJSON) ─→ d2x/buildtools/mcpp/ ← 本仓库自己的 Provider(C++26)
18+
├─ 目录约定发现练习
19+
├─ 生成 mcpp workspace 清单
20+
├─ 调 mcpp build/run
21+
└─ 读侧信道判定通过
22+
23+
mcpp(未做任何改动)
24+
```
25+
26+
### 提交进仓库的包
27+
28+
```
29+
mcpp.toml [workspace] members = ["d2x/buildtools/mcpp", "dslings/harness"]
30+
d2x/buildtools/mcpp/ Provider,standard = "c++26"
31+
dslings/harness/ 练习脚手架库,standard = "c++23"
32+
```
33+
34+
### 生成物(gitignore)
35+
36+
```
37+
.d2x/build/mcpp.toml 独立 workspace 根
38+
.d2x/build/<std>/mcpp.toml 该标准下全部 target —— 供 clangd
39+
.d2x/build/_current/mcpp.toml 只含当前一题 —— 供 checker
40+
.d2x/build/_current/result.ndjson 判定用侧信道
41+
.d2x/state.json 学员进度(d2x 写)
42+
```
43+
44+
---
45+
46+
## 2. 引导:零脚本、Windows 安全
47+
48+
`.d2x.json`
49+
50+
```json
51+
{ "buildtools": "mcpp run -q -p d2x/buildtools/mcpp --" }
52+
```
53+
54+
d2x 拼接后得到 `mcpp run -q -p d2x/buildtools/mcpp -- check <id>`**从仓库根执行、无需 cd、首次自动构建 Provider**,暖开销约 26ms。
55+
56+
d2x 从不 `chdir`(它在静态初始化时捕获 CWD),所以免 `cd` 是硬要求。注意 `mcpp``-p` 匹配的是**目录 basename 或完整相对路径**,不是包名。
57+
58+
### ⚠️ 嵌套 mcpp 与 `LD_LIBRARY_PATH`
59+
60+
`mcpp run` 会把 `LD_LIBRARY_PATH` 指向它私有的 glibc 并注入子进程。Provider 再去 spawn 嵌套的 `mcpp` 时被迫加载错配的 glibc,**在动态链接器里段错误**(冷启动稳定复现,输出只剩 `<pid>:\t__vdso_time` 这样的 trace 残片)。
61+
62+
`runner.cppm` 在每次 spawn 前 `unsetenv("LD_LIBRARY_PATH")` 绕过;mcpp 会为它自己的子进程重新设置正确的值。
63+
64+
d2x 侧对同一问题早有相同处理(`platform.cppm`),仓库里还留着 `workaround_ld_library_path_issue` 分支——**这是 mcpp 的既有问题,值得单独向上游报**
65+
66+
---
67+
68+
## 3. 练习发现:目录约定 + 就近指令
69+
70+
**没有声明文件。** 放一个 `.cpp` 进去就是全部工作。
71+
72+
rustlings 最贵的一课是 PR #1355:Rust edition 同时写在 `rustc` 参数和 `rust-project.json` 里,两边漂移酿成 bug。**任何独立声明文件都是第二套真相源。** 这里的真相只有两处,且都无法漂移:目录结构,和练习文件自己的头部注释。
73+
74+
### 目录布局
75+
76+
```
77+
dslings/hello-mcpp.cpp 入门练习(chapter = intro)
78+
dslings/<std>/NN-topic-K.cpp zh 练习
79+
dslings/en/<std>/NN-topic-K.cpp en 练习(lang=en 时启用,与 zh 互斥)
80+
solutions/<std>/NN-topic-K.cpp 参考答案(zh/en 共用同一份)
81+
```
82+
83+
### id / order / chapter 的推导
84+
85+
| 文件 | id | chapter | order |
86+
|---|---|---|---|
87+
| `dslings/cpp11/00-auto-and-decltype-0.cpp` | `cpp11-00-auto-and-decltype-0` | `cpp11/00-auto-and-decltype` | 1100000 |
88+
| `dslings/cpp11/03-trailing-return-type.cpp` | `cpp11-03-trailing-return-type` | `cpp11/03-trailing-return-type` | 1100300 |
89+
| `dslings/hello-mcpp.cpp` | `hello-mcpp` | `intro` | -100000 |
90+
91+
`order = std_rank × 100000 + 章节号 × 100 + 练习序号`。尾部 `-<数字>` 是序号,没有就当 0(仓库里 03/04 两章确实缺 `-0` 后缀,规则把它们归一化)。
92+
93+
### 每练习编译选项
94+
95+
写在练习文件自己的头部注释里(只扫前 40 行):
96+
97+
```cpp
98+
// d2x:cxxflags: -O0 -fno-elide-constructors
99+
```
100+
101+
仓库现存用例:`04-rvalue-references`(观察移动)、`07-constexpr-0``-Wpedantic -Werror` 让 VLA 成为错误)。
102+
103+
### id 白名单
104+
105+
id 只允许 `[A-Za-z0-9._-]`,违反者**被拒绝并打印到 stderr**,不进入枚举。
106+
107+
因为 id 有两个危险去向:d2x 拼进 shell 命令、我们写进生成的 TOML(`[targets.<id>]`)。在源头挡住比在两个下游各自转义更可靠——这类文件名本就是笔误或恶意 PR,与其想办法安全传递,不如让作者改名。
108+
109+
---
110+
111+
## 4. 双 member 布局
112+
113+
**硬约束:dslings 的练习默认就编译不过**(49 个带 `D2X_YOUR_ANSWER`),而 mcpp 的 `build``run` 都会先全量构建整个包——一个坏兄弟拖垮全部且零产物。实测:
114+
115+
```
116+
member 含全部 3 题(后两题未做完)→ mcpp build -p → exit=1 ← 当前这题被拖挂
117+
只生成当前这一题 → 0.071s,exit=0
118+
```
119+
120+
**member 粒度(标准/特性/练习)本身不解决逐题隔离。** 所以分两个:
121+
122+
| 目录 | 内容 | 用途 |
123+
|---|---|---|
124+
| `.d2x/build/<std>/` | 该标准下全部 target | clangd —— 即使多数练习编译不过,mcpp 仍生成完整 `compile_commands.json`,IDE 对每题都有正确编译参数 |
125+
| `.d2x/build/_current/` | 只含要验证的那一题 | checker |
126+
127+
实测动态改写清单代价极低:切题 0.118s、切回 0.018s,**fingerprint 目录始终只有 1 个**(改写 target 集合不会让缓存爆炸)。
128+
129+
### 其他生成细节
130+
131+
- `main``../../../` 逃逸出包根,**练习源文件原地不动**
132+
- 脚手架走 `[dependencies] path` 依赖,而非把仓库根塞进 include 搜索路径(后者会让练习能 `#include` 仓库里任何文件)
133+
- `-fmacro-prefix-map=<repo_root>/=``__FILE__` 相对仓库根,学员看到的报错不再顶着一长串 `/home/...`
134+
- 所有清单**内容比对后才落盘**,不推进 mtime,保住 mcpp 的快速路径
135+
- `check` 会先 `write_full``write_current`——全新仓库上学员直接跑 `d2x checker` 时根清单还不存在,否则 mcpp 报退出码 2
136+
137+
---
138+
139+
## 5. 判定机制
140+
141+
### 侧信道,不是扫 stdout
142+
143+
判定原先 100% 是带内信号:断言失败**并不改变退出码**(实测 exit=0),所以只能扫 stdout 找 ``。这会误判——一个断言全过的正确解答,只要在说明文字里打了个 `` 就被判失败(已复现)。反过来,输出截断或 Windows 控制台代码页不对,判定同样失效。
144+
145+
现在 stdout 回归「给人看」,判定走侧信道:
146+
147+
```jsonc
148+
{"kind":"assert","ok":false,"expr":"a == b","expected":"1","actual":"2",
149+
"file":"dslings/cpp11/xx.cpp","line":33}
150+
{"kind":"wait","file":"dslings/cpp11/xx.cpp","line":40}
151+
```
152+
153+
**逐条追加而非退出时统一写** —— 练习段错误时,崩溃之前的断言结果照样保留。
154+
155+
`D2X_RESULT_FILE` 环境变量未设置时 harness 只打印不写文件:**学员直接跑二进制零摩擦**
156+
157+
### 判定顺序
158+
159+
| 侧信道内容 | Verdict |
160+
|---|---|
161+
|`ok:false` | `fail` + 每条失败转一个 `Diagnostic` |
162+
| 无失败但退出码非 0 | `fail` |
163+
| 无失败、有 `wait` | `blocked` |
164+
| **文件不存在**(练习没用 harness) | 退回「编译通过 + 退出 0 = `pass`|
165+
166+
最后一条让 **harness 自动变成可选的**,不需要额外机制:纯观察型练习(现有 18 个只用 `D2X_WAIT`)可以写成零依赖的纯 C++ 文件,学员能原样拷进 Compiler Explorer。
167+
168+
### 路径:展示 vs 定位
169+
170+
侧信道里的 `file``-fmacro-prefix-map` 是相对路径(学员看着舒服),但协议要求绝对路径(d2x 靠它开编辑器、监听变更)。**在协议边界上还原成绝对。**
171+
172+
---
173+
174+
## 6. 练习脚手架 `dslings/harness/`
175+
176+
按库设计,目前住在本仓库,将来可整体抽成独立 mcpp 包给其他 C++ 课程复用。
177+
178+
### 两条路径不等价
179+
180+
| 设施 | `#include <d2x/cpp/common.hpp>` | `import d2x.harness;` |
181+
|---|---|---|
182+
| 断言 | `d2x_assert` / `d2x_assert_eq`| `d2x::check` / `check_eq``source_location` 自动带 file/line |
183+
| 路障 | `D2X_WAIT` | `d2x::wait()` |
184+
| 填空占位 | `D2X_YOUR_ANSWER` | **无等价物** |
185+
186+
**宏无法跨模块导出**,这是 C++ 的硬约束而非疏漏。`D2X_YOUR_ANSWER` 必须展开为空才能制造编译错误,本质就是宏。**cpp20/cpp23 等模块化章节需要另设填空约定——这是个尚未决策的缺口。**
187+
188+
`include/` 下保持 `d2x/cpp/common.hpp` 这个既有路径,所以 104 个练习和书本一行都不用改。路径改名可以以后单独做。
189+
190+
### 维护须知
191+
192+
`d2x_assert_eq`**所有分支**都必须走 `d2x::detail::show()`,不能用裸 `std::to_string`——后者没有 `std::string` / `const char*` / scoped enum 的重载,一旦有练习比较这些类型就会硬编译失败。(曾经上报分支用了 `show()` 而日志分支没用,是个潜伏的地雷。)
193+
194+
---
195+
196+
## 7. 测试
197+
198+
### `d2x/buildtools/mcpp/tests/e2e.sh`
199+
200+
断言两件事,缺一不可:**每个练习未完成时不通过****每个参考答案放进去后通过**。等价于 rustlings 的 `cargo dev check --require-solutions`
201+
202+
走的是 `d2x checker` 内部同一条 Provider 路径,所以绿灯等价于学员本地能跑通,而非另一条平行路径。
203+
204+
**两道防线:**
205+
206+
1. **防空转**——`pass == 0` 时直接判失败。旧 CI 就是这么绿了很久的:它只挑 `-ref` 目标,而 `solutions/` 早被注释掉,循环一次都没进,job 照样退出 0;第 60 行的 `wc -l` 对空串还打印 "Found 1 reference targets" 把空转掩盖了。同类问题在本脚本自己身上也犯过一次(`${sol#en/}` 剥离顺序错,导致所有英文答案静默 SKIP)。
207+
208+
2. **脏树检查**——练习目录有未提交改动时**拒绝运行**并列出文件。脚本会把参考答案覆盖到练习上再还原,天然会吃掉未提交的改动,这个陷阱咬过两次。
209+
210+
### 当前状态
211+
212+
|| 结果 |
213+
|---|---|
214+
| Provider 枚举 | 52 题(zh:1 + 49 + 2;en 同样 52) |
215+
| 端到端 zh | 51/51 参考答案通过 |
216+
| 端到端 en | 51/51 参考答案通过 |
217+
| d2x session 单测 | 28/28 |
218+
| 事件流 | 纯 JSON,0 污染 |
219+
| 注入防护 | 恶意文件名被拒绝,命令未执行 |
220+
221+
---
222+
223+
## 8. C++ 标准与教学漂移
224+
225+
**所有练习按 `c++23` 编译。** mcpp 目前硬拒 `c++11/14/17/20``src/manifest/types.cppm` 的白名单),本方案选择不改 mcpp 上游。因此 `cppNN/` 目录现在表示**特性引入于哪个标准**,不再改变编译参数。
226+
227+
### 已知代价
228+
229+
`04-rvalue-references` 的移动构造教学点。C++17 起 prvalue 直接初始化目标(保证复制省略),`-fno-elide-constructors` 再也无法让 `Object obj = Object();` 产生移动构造。
230+
231+
**修法:** 改成从具名对象 `std::move`(任何标准下都必然调用移动构造),并补一条 `d2x_assert(move_ctor_calls >= 1)`
232+
233+
**更重要的是那条断言。** 漂移之所以能静默发生,正是因为从前没有任何断言检查它——输出少一行没人发现。现在它是 CI 可检测的事实。
234+
235+
> **教训:教学点如果没有断言覆盖,它的消失就是不可见的。** 新增练习时应问:这一课的核心观测点,有断言在守着吗?
236+
237+
量化过的风险面:51 个参考答案在「原标准 vs c++23」下逐一编译并 diff 运行输出,**50 个完全一致,仅此 1 个漂移**。但随着课程往 cpp17/20 扩写,这类静默漂移只会变多。
238+
239+
---
240+
241+
## 9. 撰稿流程的变化
242+
243+
`.agents/skills/d2mcpp-authoring/` 已同步更新:
244+
245+
- **练习不再需要注册**——放进 `dslings/<std>/` 即被目录约定发现
246+
- 新增 `<std>` 章节只需建目录 + 改两个 `SUMMARY.md`,无构建文件接线
247+
- 每练习编译选项改为文件头部 `// d2x:cxxflags:` 指令
248+
- 验证命令改为 `mcpp run -q -p d2x/buildtools/mcpp -- check <id>` 或整体 `bash d2x/buildtools/mcpp/tests/e2e.sh`
249+
250+
---
251+
252+
## 10. 已知缺口
253+
254+
| 缺口 | 影响 |
255+
|---|---|
256+
| **模块化练习的填空占位符无约定** | `D2X_YOUR_ANSWER` 是宏,跨不了模块边界;cpp20/cpp23 章节需先决策 |
257+
| **macOS / Windows 从未验证** | `_popen``_putenv_s``#ifdef` 分支全是纸面推断 |
258+
| **新 CI 从未真跑过** | workflow 是手写的,`xlings install -y` 在 CI 环境能否装上 mcpp 未验证 |
259+
| **`diagnostics` 只覆盖运行期断言** | 编译错误尚未解析成结构化诊断 |
260+
| **Provider 侧无单测** | discovery 的 id/order/chapter 推导、manifest 生成都只被端到端间接覆盖 |
261+
| **`hello-mcpp` 无参考答案** | e2e 只能 SKIP 它,入门练习实际未被验证 |
262+
| **`solutions/` 无 en 镜像** | 设计如此(共用一份),但意味着 en 专有的措辞错误无法被答案校验发现 |

0 commit comments

Comments
 (0)