Skip to content

Commit 3f237ed

Browse files
authored
feat: mcpp build --configure-only —— 不编译就发布 compile_commands.json (#387)
新增 `mcpp build --configure-only`:复用真实 `prepare_build()` 与真实 `BuildPlan`, 以 Ninja dry-run 收尾 —— 编译参数只有一条推导路径,selector(`-p` / `--workspace` / `--profile` / `--target` / `--features` / `--cap` / `--static`)与普通构建同解。 CDB 同时覆盖普通源码与 `tests/**/*.cpp`,测试 TU 带 dev-dependencies 与匹配的 `[build].flags`,所以源码还编不过时 clangd 就已经能索引。 它不是只读操作:`build.mcpp`、缺失依赖/工具链、lock 与构建目录元数据仍可能被写, 只在可信 workspace 中运行。稳定契约只有退出码和 `compile_commands.json` 两项。 CDB 改为原子发布:同目录临时文件 + `rename` / `MoveFileExW(MOVEFILE_REPLACE_EXISTING)`, 从不先删目标,发布失败时磁盘上留下的仍是上一份完整可用的 CDB;符号链接形式替换的是 链接目标而非链接本身;内容不变则不写。普通 build/test 发布失败只警告并继续, `--configure-only` 直接失败 —— 它唯一的产物就是那个文件。 `run_tests()` 的测试发现抽成 `mcpp.build.test_targets` 供两边共用,顺带把 `fs::relative` 换成 `lexically_relative`(前者解析符号链接)。 Review 修复(在原 PR 基础上): - **configure-only 不再毒化构建快路径。** backend 在 `dryRun` 早退之前就写了 `build.ninja`,而配置计划的图带着 test targets 与 dev-deps —— 它的 `default` 行是 测试二进制,包自己的 target 根本不在图里。`.build_cache` 没动过,而快路径判新鲜 只比 build.ninja 与源码的 mtime、从不看图本身,于是下一次普通 `mcpp build` 会重放 它:链测试、不链 target、还报 `Finished`。现在在 backend 跑之前丢掉指向该构建目录 的快路径条目,只作用于被重写的那一个 outputDir。实测 red→green。 (`mcpp test` 有同形的既存缺陷,见 #407。) - e2e 改名 `211_configure_only_cdb.sh`(`202` 与 `202_machine_output_contract.sh` 撞号)。 - workspace fan-out 的报错补上 member 名。 - CHANGELOG 记入在飞的 `[2026.8.10.1]` 段。 设计见 `.agents/docs/2026-08-08-configure-only-cdb-design.md`。 Refs #379 Refs mcpp-community/mcpp-vscode#5 Co-authored-by: wellwei <96378453+wellwei@users.noreply.github.com>
1 parent 3724102 commit 3f237ed

22 files changed

Lines changed: 2649 additions & 90 deletions
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# `build --configure-only` 与可靠 CDB 设计
2+
3+
## 1. 背景
4+
5+
普通 `mcpp build` 已在启动 Ninja 前生成 `compile_commands.json`。因此源码存在
6+
语法错误时仍能得到 CDB;真正缺失的是一个只完成项目解析、工具链解析、构建计划
7+
生成和 CDB 发布,而不编译普通翻译单元、不链接最终目标的入口。
8+
9+
PR #372 同时实现了 IDE snapshot、NDJSON 生命周期、内容寻址 reply、发布状态机和
10+
CDB 可靠性。RFC #379 决定拆分这些关注点:本设计只保留只有 mcpp 能可靠完成的
11+
构建配置能力,通用机器协议继续在 RFC 中讨论。
12+
13+
## 2. 目标
14+
15+
新增:
16+
17+
```text
18+
mcpp build --configure-only [现有 build selectors]
19+
```
20+
21+
成功时必须满足:
22+
23+
1. 复用真实 `prepare_build()``BuildPlan`,不复制编译参数推导。
24+
2. 根 CDB 覆盖所选 package 的普通源码与 `tests/**/*.cpp`
25+
3. 测试 TU 带有 dev-dependencies 和匹配的 `[build].flags`
26+
4. 准备标准库 BMI,并只物化已经命中全局缓存的依赖 BMI。
27+
5. 不启动 Ninja,不生成普通对象文件,不链接可执行文件或库。
28+
6. CDB 通过跨平台原子替换发布;失败时旧 CDB 保持不变。
29+
7. 仅当新 CDB 已成功发布时返回 0。
30+
31+
该命令不是只读操作。`prepare_build()` 仍可能执行 `build.mcpp`、安装缺失依赖或
32+
工具链、写 `mcpp.lock`、resolution、缓存及构建目录元数据。IDE 插件必须继续受
33+
workspace trust 约束。
34+
35+
## 3. 非目标
36+
37+
本 PR 不实现:
38+
39+
- JSON 或 NDJSON stdout、wire envelope、protocol version。
40+
- `ide snapshot``ide configure` 或 daemon。
41+
- project/configuration/snapshot ID。
42+
- `.mcpp/ide/current.json`、内容寻址 reply、跨文件发布事务或发布锁。
43+
- `invalidatedBy`、manifest metadata 或依赖图。
44+
- 构建未缓存的项目或依赖模块 BMI。
45+
- 新的 toolchain、profile、feature、capability 或 workspace selector 语义。
46+
- `.xlings.json` pin 更新。
47+
48+
## 4. 方案选择
49+
50+
### 方案 A:复用 Ninja backend 的内部 dry-run,推荐
51+
52+
`prepare_build()` 生成包含普通目标和测试目标的计划,再以
53+
`BuildOptions::dryRun=true` 调用现有 Ninja backend。backend 仍生成 `build.ninja`
54+
和 CDB,但在启动 Ninja 前返回。
55+
56+
优点:编译命令只有一个生成路径;现有 selector、工具链和平台逻辑全部复用。
57+
风险:不能调用普通 `run_build_plan()`,否则会错误填充 BMI cache 和 build fast-path
58+
cache。需要独立的 `run_configure_plan()` 收口配置模式。
59+
60+
### 方案 B:在 CLI 直接调用 `emit_compile_commands()`
61+
62+
代码更短,但会绕过 backend 的 manifest、命令长度检查和后续编译命令演进,形成
63+
第二条 CDB 路径。不采用。
64+
65+
### 方案 C:保留 #372`ide configure`
66+
67+
能提供更丰富状态,但需要 NDJSON、snapshot 发布和长期协议兼容,且插件当前只消费
68+
CDB 路径与成功结果。不采用。
69+
70+
## 5. CLI 与执行流
71+
72+
`build` 增加布尔选项 `--configure-only`,其余选项继续由现有 parser 和
73+
`BuildOverrides` 处理:`-p/--package``--workspace``--profile``--target`
74+
`--features``--cap``--cache``--offline``--strict``--static`
75+
76+
执行流:
77+
78+
```text
79+
cmd_build
80+
-> workspace_fanout_members(沿用现有语义)
81+
-> discover_test_targets(按 member 发现测试)
82+
-> prepare_build(includeDevDeps = !tests.empty(), extraTargets = tests)
83+
-> stage_cached_module_prerequisites
84+
-> run_configure_plan
85+
-> NinjaBackend::build(dryRun=true, requireCompileDatabase=true)
86+
-> 生成 build.ninja
87+
-> 原子发布 compile_commands.json
88+
-> 在 spawn Ninja 前返回
89+
```
90+
91+
配置模式必须跳过 build fast path。它也不得:
92+
93+
- 调用 `bmi_cache::populate_from()`
94+
-`target/.build_cache`
95+
- 输出 `Finished ...` 这种表示已完成编译的状态;
96+
- 把不存在的最终产物报告为 produced artifacts。
97+
98+
workspace fan-out 继续逐 member 调用相同流程。每个 member 的新 CDB 内容与已有根
99+
CDB 合并,fresh entry 优先,最终得到一个覆盖 workspace 的根 CDB。任一 member
100+
失败时沿用现有 continue-on-failure 和首个非零结果规则。
101+
102+
## 6. 测试目标发现
103+
104+
`run_tests()` 抽取 `discover_test_targets()`,由 `mcpp test` 和配置模式共同调用。
105+
该函数负责:
106+
107+
- 将 package selector 解析到同一个 member 根目录;
108+
- 展开 `tests/**/*.cpp`
109+
- 用相对 `tests/` 的无扩展路径生成稳定测试名;
110+
- 检测重复测试名;
111+
- 把匹配的 `[build].flags` 中 defines、cflags、cxxflags 附到合成 target。
112+
113+
`mcpp test --list` 现有的 best-effort 行为必须保留:源码或 manifest 尚未可构建时,
114+
只要能够发现测试文件就继续列出;严格 manifest 与依赖校验仍由后续
115+
`prepare_build()` 完成。
116+
117+
配置模式只有在发现测试 target 时启用 dev-dependencies。没有测试的项目不应仅因
118+
IDE 配置而安装无关开发依赖。
119+
120+
## 7. 模块前置产物
121+
122+
`prepare_build()` 已保证当前工具链需要的标准库模块可用。配置模式还会把计划中
123+
`servedFromCache && providesModule` 的依赖 BMI 从全局缓存物化到计划引用的构建目录。
124+
125+
该步骤只复制现有 BMI,不复制缓存对象文件,也不编译任何缺失模块。物化失败发生在
126+
CDB 发布前,并保留旧 CDB。项目自身模块或未缓存依赖模块保持 pending;用户显式
127+
执行普通 `mcpp build` 后才获得完整模块语义。
128+
129+
## 8. CDB 发布与错误处理
130+
131+
新增平台原语 `platform::fs::replace_file(source, destination, error_code)`
132+
133+
- POSIX 使用同文件系统 `rename`
134+
- Windows 使用 `MoveFileExW(..., MOVEFILE_REPLACE_EXISTING)`
135+
- Windows 遇到短暂 sharing violation 时有限退避重试;
136+
- 不先删除 destination;
137+
- 函数为 `noexcept` 风格,通过 `error_code` 报错。
138+
139+
CDB 写入流程:
140+
141+
1. 生成并解析 JSON,要求顶层为数组。
142+
2. 与现有有效条目合并并删除已不存在源文件的旧条目。
143+
3. 内容未变化时不写文件,避免无意义触发 clangd 重索引。
144+
4. 若根 CDB 是文件符号链接,解析并原子更新其目标,保留链接本身。
145+
5. 在同目录完成带跨进程随机量的临时文件写入和 flush。
146+
6. 通过 `replace_file` 原子替换目标。
147+
7. 替换失败时清理临时文件,返回错误,旧文件保持不变。
148+
149+
`write_compile_commands()` 改为返回结构化成功或错误。为保持普通构建兼容性:
150+
151+
- 普通 build/test 遇到 CDB 发布失败时输出 warning,继续真实构建;
152+
- `--configure-only` 设置 `requireCompileDatabase=true`,发布失败直接返回非零。
153+
154+
不加入 `FileLock`。单文件原子替换已保证 clangd 不会读到半截 JSON;并发配置采用
155+
last-writer-wins,但每个可见版本都是完整文件。只有未来重新引入多文件事务时才需要
156+
发布锁。
157+
158+
## 9. 输出契约
159+
160+
本 PR 只提供人类输出和进程退出码,不承诺机器可读 stdout。建议成功信息为:
161+
162+
```text
163+
Configured <package> (<N> compile commands)
164+
```
165+
166+
错误继续通过现有 stderr/UI 通道报告。插件稳定依赖仅有:
167+
168+
- 进程退出码;
169+
- 工程或 workspace 根目录的 `compile_commands.json`
170+
171+
未来 RFC 可以向同一命令增加 `--format json`,默认人类输出不需要变化。
172+
173+
## 10. 测试策略
174+
175+
### 单元测试
176+
177+
- CDB 合并继续覆盖 fresh-wins、删除失效文件、损坏旧 JSON 回退。
178+
- 原子 writer 拒绝非数组 JSON。
179+
- 内容不变时不改 mtime。
180+
- `replace_file` 成功替换旧文件。
181+
- source 不存在时替换失败且 destination 内容保持不变。
182+
- 测试发现覆盖 nested names、重复名、member scoping 和 `[build].flags`
183+
- 损坏 manifest 下 `test --list` 仍保持 best-effort inventory。
184+
185+
### E2E
186+
187+
- 语法错误源码:配置成功、CDB 含源码、没有普通对象或最终二进制。
188+
- 测试 TU:CDB 含 `tests/**/*.cpp`,并具有 dev-dependency include/define。
189+
- workspace:默认 virtual workspace fan-out 和 `-p <member>` 均生成正确条目。
190+
- selector:profile、target、features、capability 与普通 build 进入同一 BuildPlan。
191+
- 失败保留:新 CDB 发布失败时旧 CDB 内容不变且命令返回非零。
192+
- Windows:真实 MSVC 配置与 `MoveFileExW` 替换由 Windows CI 覆盖。
193+
194+
## 11. 对核心构建的影响
195+
196+
共享变化限定为三处:
197+
198+
1. 测试发现从 `run_tests()` 抽为共享模块,行为由原有测试锁定。
199+
2. CDB 从截断写改为临时文件加原子替换;普通构建失败策略保持非致命 warning。
200+
3. backend 增加 `requireCompileDatabase` 选项,默认 false;所有现有调用语义不变。
201+
202+
配置模式使用独立执行函数,不写 fast-path cache 或全局 BMI cache,因此不会让后续
203+
真实 build 错误命中不存在的产物。
204+
205+
## 12. 后续工作
206+
207+
以下事项留在 RFC #379,不阻塞本 PR:
208+
209+
1. 未知 format 的统一退出码、stdout 和诊断结构。
210+
2. `--format json` 与旧 `--json` 的兼容规则。
211+
3. `self env` JSON、`xpkg parse` schema version。
212+
4. read-only metadata 与 manifest 合法键/枚举词汇表。
213+
5. resolved metadata、依赖图与 CDB `invalidatedBy`
214+
6. 至少三个稳定消费者出现后再抽取通用 `mcpp.wire`

0 commit comments

Comments
 (0)