Skip to content

Commit c7be19e

Browse files
authored
fix(scanner): skip raw-string-literal bodies when detecting imports (#138)
The default regex module scanner (scan_file in modgraph/scanner.cppm) scanned each line as raw text, stripping only line comments. A line whose trimmed text began with 'import' was treated as a real module import even when it lived inside a multi-line raw-string literal — e.g. the 'mcpp new --template gui' skeleton stored as R"GUI( ... import imgui.core; ... )GUI" in scaffold/create.cppm. That produced spurious 'module imgui.core imported but not provided in this build' warnings on every mcpp build. Track raw-string state across lines (strip_raw_strings) and blank out raw bodies before import/module matching. Ordinary "..." strings are left as is: the matcher only fires on lines that *start with* the keyword, which a string body can only do across lines (a raw string). Adds two regression tests; verified the real create.cppm no longer warns. Also lands the follow-ups design doc (.agents/docs/2026-06-22).
1 parent 2a67dd4 commit c7be19e

3 files changed

Lines changed: 263 additions & 1 deletion

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# mcpp 后续修复:统一汇总 + 方案设计 + PR 拆分
2+
3+
> 状态:设计 / 待实施
4+
> 前置:`mcpp-index#43`(xcb 配方)与 `mcpp#137`(InstallStash 回滚)**已合入**
5+
> 范围:仅 **mcpp 仓库**的遗留项(xim-res 打包类 T-a/k/l 见 §6,不在此)。
6+
> 关联:根因分析见 `2026-06-21-xcb-and-install-integrity-cross-repo-fix.md`
7+
8+
本次把今天讨论中**未做的 mcpp 相关项**全部汇总,逐项给出根因(带 `file:line`)、方案、风险、测试,最后给 **PR 拆分建议**
9+
10+
---
11+
12+
## 1. 待办项总览(mcpp 仓库)
13+
14+
| ID || 子系统 | 风险 |
15+
|---|---|---|---|
16+
| T-b | bootstrap/装包进度反馈(现 `>/dev/null` 静默,假死在 "Bootstrap ninja") | 首跑/xlings ||
17+
| T-c | 首个 install 前显式 `Fetching package index…` | 首跑/xlings ||
18+
| T-d | 镜像默认 `CN` 首跳慢/不稳;联网无超时/重试 | 首跑/xlings ||
19+
| T-e | `.mcpp_ok` 写入前校验产物清单;新装禁用 `looks_complete_legacy` 打标记 | 安装完整性 ||
20+
| T-f | `mcpp self doctor` 扫工具链 RUNPATH / subos 悬空软链 | 安装完整性 ||
21+
| T-g | ABI 报错可操作化(指出工具链来源,给对的命令) | 工具链解析 ||
22+
| T-h | `toolchain default`/`list *` 与构建实际解析不一致 | 工具链解析 ||
23+
| T-i | 模块扫描器把字符串字面量里的 `import` 当真(imgui 误报) | 构建图 ||
24+
| T-j | registry 依赖被重装后 build.ninja 陈旧 → `missing and no known rule` | 构建图/缓存 | 中高 |
25+
26+
---
27+
28+
## 2. 逐项方案(含代码定位)
29+
30+
### T-b/T-c/T-d — 首跑 bootstrap UX
31+
32+
**根因**
33+
- `install_with_progress()`(`src/xlings.cppm:870-899`)**优先走直装路径** `xlings install … >/dev/null 2>&1`(`silent_redirect`,`src/platform/shell.cppm:30`),成功即返回;能渲染进度的 NDJSON 回调路径只是**失败兜底**,正常路径永不触发 → 全程无输出、`std::system` 阻塞、屏幕定格在最后印出的 "Bootstrap ninja"。
34+
- 镜像默认 `CN`:`seed_xlings_json(..., mirror="CN")`(`src/xlings.cppm:236-238`,带 `TODO(mirror-default)` 注释 `:223-235`)。mcpp 这层**无重试/超时**,联网延迟方差直接变"假死"时长。
35+
36+
**方案**
37+
- **T-b**:交互式 bootstrap/toolchain 安装**以 NDJSON 进度路径为主**(`install_with_progress` 调换顺序:先 NDJSON 拿 `download_progress` 回调,直装退为兜底);退一步至少在 `std::system` 阻塞期间打 **spinner + elapsed 计时器**
38+
- **T-c**:首个真正 `xlings install` 前显式 `print_status("Fetching", "package index (one-time)")`(`ensure_init`/`ensure_ninja` 附近,`src/xlings.cppm:1026-1108`)。
39+
- **T-d**:`seed_xlings_json` 默认镜像改为**自动探测**(按 `LANG`/对 github vs ghproxy 一次 tight-timeout HEAD 探测,见现有 TODO 的 (b));并给联网步骤加超时+有限重试。
40+
41+
**风险**:低(T-b/c 纯输出)~ 中(T-d 改默认镜像行为,需保留 `mcpp self config --mirror` 覆盖)。
42+
**测试**:首跑录屏/日志快照;`--mirror CN/GLOBAL` 两路冒烟;离线/慢网模拟下不再无限静默。
43+
44+
---
45+
46+
### T-e/T-f — 安装完整性加固(#137 的延续 P2-b/P2-c)
47+
48+
**根因**
49+
- 标记写入只证"进程退出/布局像"非"产物正确":`mark_install_complete``inst->exitCode==0 && verdir 存在`(`src/pm/package_fetcher.cppm:740-743`)或 copy 兜底的 `looks_complete_legacy` 布局启发式(`:754-758`)时就写。
50+
- doctor 现有 `doctor_report()`(`src/doctor.cppm:46+`,有 `ok/warn/err` 助手)未覆盖工具链 RUNPATH / subos 悬空软链(zlib 被删后 `subos/default/lib/libz.so.1` 悬空就是没人体检到)。
51+
52+
**方案**
53+
- **T-e(a)**:配方可声明 `verify = { "include/xcb/xproto.h", ... }`(产物相对 install dir 的必存路径)。解析挂在 `src/manifest.cppm`(现有 `sources`@1851 / `generated_files`@1882 / `targets`@1903 处加 `verify` key);`mark_install_complete` 前逐一 `exists` 校验,缺则判失败、清理、不写标记。
54+
- **T-e(b)**:**新装路径禁用 `looks_complete_legacy` 打标记**(`package_fetcher.cppm:754-758` / `779-784` 的 copy 兜底分支)——布局启发式只应用于"老包一次性收编",不能作为刚装完的完整性判定。
55+
- **T-f**:`doctor_report()` 加一节:遍历已安装工具链二进制,`readelf -d` 取 RUNPATH,校验每个目录存在;扫 `subos/default/lib` 软链是否悬空;发现即 `warn` + 可操作提示(指向缺失的 `xim-x-*` 包)。
56+
57+
**风险**:中(T-e 触及安装标记,与 #137 同区,需回归既有安装/copy-fallback 路径);低(T-f 只读体检)。
58+
**测试**:gtest——配方声明 verify 但产物缺失→install 判失败、不写 `.mcpp_ok`;copy-fallback 不再凭布局打标记;doctor 能报出被删的 `xim-x-zlib`。复用 #137`tests/unit/test_install_integrity.cpp`
59+
60+
---
61+
62+
### T-g/T-h — 工具链解析透明化 & ABI 可操作报错(同根,合一)
63+
64+
**根因(关键:多源真相)**
65+
构建期解析顺序(`src/build/prepare.cppm`,后者覆盖前者):
66+
1. `:487` 项目 `mcpp.toml [toolchain]`(`manifest.cppm:91 for_platform`)——**最先,直接 shadow 全局**;
67+
2. `:490-491` 全局 `config.toml [toolchain] default`(`config.cppm:501`);
68+
3. `:505` / `:516-520` `[target.<triple>]` 覆盖 + `*-musl` 约定**硬编码** `gcc@15.1.0-musl`;
69+
4. `:595-596,648-651` 内建默认 `gcc@16.1.0`(仅首跑分支,且会持久化)。
70+
71+
`mcpp toolchain default <spec>`(`src/toolchain/lifecycle.cppm:420,430`)和 `list``*` 标记(`lifecycle.cppm:180-181,195`,`matches_default_toolchain` `registry.cppm:190-199`)**只读/只写全局 config.toml**,完全不看项目 `[toolchain]`/`[target.*]`
72+
→ 于是 `default``gcc@16.1.0``list *` 跟全局,而 `run` 实际可能走项目/target 覆盖的 `gcc@15.1.0-musl`,**三者各执一词**
73+
74+
ABI 报错(`prepare.cppm:2453-2458`)已建议 `mcpp toolchain default <glibc>`,但当 musl 来自 `[target.*]`/`*-musl` 约定时,**这条命令改不了结果**(target 覆盖优先级更高)——所以是"误导性建议"。
75+
76+
**方案**
77+
- **T-h**:抽出**单一"有效工具链解析"函数**(输入:cfg + manifest + 可选 target),让 `toolchain list``*``mcpp self env`、build 三处**共用**它;`list`/`env` 显示**有效解析结果**并标注来源(project `[toolchain]` / `[target.x]` / `*-musl` 约定 / 全局 default)。
78+
- **T-g**:ABI 报错里**点名工具链来源**(复用上面解析函数返回的 source),只有当来源是"全局 default"时才建议 `mcpp toolchain default …`;来源是 `[target.*]`/约定时,提示改 target 覆盖或换 `--target`
79+
80+
**风险**:中(改解析展示,核心解析逻辑不动,只是统一读取)。
81+
**测试**:构造带 `[toolchain]` + `[target.*]` 的项目,断言 `list *`/`env`/build 三者一致;ABI 报错文案按来源分支。
82+
83+
---
84+
85+
### T-i — 模块扫描器误报(imgui)
86+
87+
**根因**:默认正则扫描器 `scan_file`(`src/modgraph/scanner.cppm:202-332`)是**逐行裸文本扫描**,只剥行注释(`strip_line_comment` `:151-155,226`),**不识别字符串/原始字符串/块注释**`create.cppm:221-224``R"GUI(… import imgui.core; import imgui.app; …)GUI"` 模板文本里的 `import``:287-323` 被当成真 import → `resolve_graph``:425-429` 报 "module 'imgui.core' imported but not provided"(打印于 `prepare.cppm:2169-2171`)。(`MCPP_SCANNER=p1689` 的编译器路径 `p1689.cppm:318-396` 不受骗,但非默认。)
88+
89+
**方案**:在 `scan_file` 读取循环(`:224-227`)加**跨行词法状态**:`in_raw`(+活动 `R"delim(`/`)delim"` 定界)、块注释 `/* */`、普通字符串 `"…"`,被其吞掉的行不进入 `:287` 的 import 匹配。状态需跨 `getline` 持有(类比现有 `if_depth` `:221`)。
90+
91+
**风险**:****,高度隔离、易测;只影响扫描,不改产物。**快赢项**
92+
**测试**:新 gtest——含 `R"(import foo.bar;)"` 原始字符串 + `/* import x; */` 块注释的 `.cppm` 不产生 require/warning;真实顶层 `import` 仍被识别。
93+
94+
---
95+
96+
### T-j — registry 依赖变动后 build.ninja 陈旧
97+
98+
**根因**:指纹 `compute_fingerprint`(`src/toolchain/fingerprint.cppm:90-115`)的依赖项里 **`dependencyLockHash` 被硬编码为 `""`**(`prepare.cppm:2200`,注 "M2");`canonical_package_build_metadata`(`prepare.cppm:98-143`)只折入依赖的**清单声明身份**(`name@version`、flags、include、generated 内容),**不哈希 registry 里依赖的实际磁盘状态**。于是依赖被重装但**版本串不变**`fp.hex` 不变 → `outputDir` 不变 → 复用旧 `build.ninja`。fast-path `try_fast_build`(`src/build/execute.cppm:262-362`)只 stat `projectRoot/src/``mcpp.toml`(`:305-321`),**不 stat 依赖目录**;且 ninja 的 `missing and no known rule` 错误**不被** `is_stale_ninja_failure`(`:154-159`)识别 → 不自动重生 → 硬失败(需手动 `mcpp clean`)。
99+
100+
**方案(两处互补)**
101+
- ****:`prepare.cppm:2200`**真实哈希**替代空串——对每个已解析依赖的 registry 安装状态(install root 下源文件 mtime/内容,via `fingerprint.cppm:36/82 hash_file`)求哈希,喂入 `dependencyLockHash`。依赖重装即改 `fp.hex` → 干净重生到新目录。
102+
- **兜底**:把 `"missing and no known rule to make"` 加入 `is_stale_ninja_failure`(`execute.cppm:154-159`),让陈旧图触发一次性重生而非硬失败;可选再让 `try_fast_build` 的新鲜度扫描覆盖依赖根目录(`:309-321`)。
103+
104+
**风险**:**中高**——动指纹会影响缓存命中率与重建行为,需防"过度失效"(每次重装全量重编)。建议主方案用**内容哈希而非纯 mtime**,保证同内容重装不变指纹。
105+
**测试**:e2e——装好→构建→原地重装同版本依赖(改内容)→ 构建应重生而非报 missing;同内容重装→指纹不变、命中缓存。
106+
107+
---
108+
109+
## 3. PR 拆分建议
110+
111+
**结论:拆成 5 个 PR,不要并成一个。** 五项分属 5 个互不重叠的子系统、文件不交叉、风险档次差异大;并成一个会让 review 困难、把无关风险耦合在一起。
112+
113+
| PR | 标题 || 主要文件 | 风险 | 建议次序 |
114+
|---|---|---|---|---|---|
115+
| **PR-1** | scanner: 跳过字符串字面量 | T-i | `src/modgraph/scanner.cppm` || **1(快赢)** |
116+
| **PR-2** | install-integrity: 产物校验 + 禁用 legacy 标记 + doctor 体检 | T-e, T-f | `package_fetcher.cppm` `install_integrity.cppm` `manifest.cppm` `doctor.cppm` || 2(接 #137) |
117+
| **PR-3** | toolchain: 有效解析统一 + ABI 可操作报错 | T-g, T-h | `prepare.cppm` `toolchain/lifecycle.cppm` `toolchain/registry.cppm` || 3 |
118+
| **PR-4** | first-run UX: 进度可见 + 索引提示 + 镜像/超时 | T-b, T-c, T-d | `xlings.cppm` `config.cppm` | 低-中 | 4 |
119+
| **PR-5** | build-graph: 依赖指纹 + stale-ninja 自愈 | T-j | `prepare.cppm` `execute.cppm` `fingerprint.cppm` | 中高 | **5(最后,需缓存回归)** |
120+
121+
**分组依据**
122+
- 子系统内聚:每个 PR 只动一个子系统,文件集基本不交叉(唯一轻微交叉:PR-3 与 PR-5 都碰 `prepare.cppm`,但区域不同——`:487-520/2453` vs `:2200`——可顺序合入避免冲突)。
123+
- 风险隔离:PR-1(纯隔离)、PR-5(动缓存指纹)放两端;PR-2 紧接 #137 同区一起 review。
124+
- 独立可合:5 个互不依赖,任意顺序都能单独合;每个都随**下个 mcpp release**生效(都是核心二进制改动)。
125+
126+
**可选合并**:若想减数量,PR-1+PR-5 可并为"build-graph 正确性"一个 PR(都是今天暴露的 build.ninja/模块图问题),但风险档次差异大(T-i 极安全、T-j 需缓存测试),**建议仍分开**
127+
128+
---
129+
130+
## 4. 生效方式
131+
132+
全部 5 个 PR 都是 **mcpp 核心二进制改动** → 生效路径同 #137:
133+
134+
```
135+
合入 mcpp/main → cut release → 镜像 xlings-res(gh+gtc)
136+
→ 更新 xim-pkgindex + bump bootstrap pin → 用户 `xlings install`/升级
137+
```
138+
139+
**需发新 mcpp 版本**,用户升级后随二进制生效。无一可走"改索引即生效"的免发版路径(那只属于配方类,如 #43)。
140+
141+
---
142+
143+
## 5. 不在本设计内(xim-res 打包类,另行处理)
144+
145+
- **T-a** strip 工具链(cc1/cc1plus 未 strip,490MB→~150MB,首跑 ~3× 提速)——xim-res 重新打包。
146+
- **T-k** 去重 bundled gcc tarball(同包 10 份 = 3.4GB)——缓存清理/打包。
147+
- **T-l** `.tar.gz``zstd` 并行解压——xim-res 打包。
148+
149+
生效方式:在 xim-res 重新打包工具链 → 用户重装/新装工具链时生效(非 mcpp 二进制)。
150+
151+
---
152+
153+
## 6. 进度
154+
155+
- [x] **PR-1 scanner 跳字符串(T-i)** — 实现 + 2 项 gtest 回归 + 实测 create.cppm 不再误报;开 PR
156+
- [ ] PR-2 install-integrity 加固(T-e, T-f)
157+
- [ ] PR-3 toolchain 解析统一 + ABI 报错(T-g, T-h)
158+
- [ ] PR-4 first-run UX(T-b, T-c, T-d)
159+
- [ ] PR-5 build-graph 失效(T-j)+ 版本 bump → 0.0.58
160+
161+
> 次序:PR-1(快赢)先;PR-2/PR-3/PR-4 文件不交叉 → 并行实现;PR-5 最后(碰 prepare.cppm 不同区域 + 带版本 bump)。

src/modgraph/scanner.cppm

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,53 @@ std::string_view strip_line_comment(std::string_view s) {
154154
return s.substr(0, p);
155155
}
156156

157+
// Remove C++ raw-string-literal bodies from a line, tracking multi-line raw
158+
// strings across calls via (in_raw, raw_close). Returns the code-only portion
159+
// with raw-string contents blanked out.
160+
//
161+
// Without this, a template that embeds source text — e.g. the `mcpp new
162+
// --template gui` skeleton stored as R"GUI( ... import imgui.core; ... )GUI"
163+
// in scaffold/create.cppm — has its inner `import` lines misdetected as real
164+
// module imports, producing spurious "imported but not provided" warnings.
165+
// Ordinary "..." strings are intentionally left as-is: the import/module
166+
// matcher only fires on lines whose trimmed text *starts with* the keyword,
167+
// which a string body can only do when it spans lines (i.e. a raw string).
168+
std::string strip_raw_strings(std::string_view line, bool& in_raw,
169+
std::string& raw_close) {
170+
std::string out;
171+
std::size_t i = 0;
172+
while (i < line.size()) {
173+
if (in_raw) {
174+
auto p = line.find(raw_close, i);
175+
if (p == std::string_view::npos) return out; // rest of line is raw body
176+
i = p + raw_close.size();
177+
in_raw = false;
178+
raw_close.clear();
179+
continue;
180+
}
181+
// Raw-string opener: R"delim( ... )delim" (delim is up to 16 chars,
182+
// no '(' / whitespace per the standard). Optional u8/u/U/L prefixes
183+
// precede the R; we only need to spot the R" boundary.
184+
if (line[i] == 'R' && i + 1 < line.size() && line[i + 1] == '"') {
185+
std::size_t d = i + 2;
186+
std::string delim;
187+
while (d < line.size() && line[d] != '(' && (d - (i + 2)) < 16) {
188+
delim.push_back(line[d]);
189+
++d;
190+
}
191+
if (d < line.size() && line[d] == '(') {
192+
raw_close = ")" + delim + "\"";
193+
in_raw = true;
194+
i = d + 1;
195+
continue;
196+
}
197+
}
198+
out.push_back(line[i]);
199+
++i;
200+
}
201+
return out;
202+
}
203+
157204
bool is_module_name_char(char c) {
158205
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '.' || c == ':';
159206
}
@@ -220,10 +267,15 @@ std::expected<SourceUnit, ScanError> scan_file(const std::filesystem::path& file
220267

221268
int if_depth = 0; // #if/#ifdef nesting
222269
std::size_t lineno = 0;
270+
bool in_raw = false; // inside a multi-line raw string
271+
std::string raw_close; // active )delim" terminator
223272
std::string line;
224273
while (std::getline(is, line)) {
225274
++lineno;
226-
std::string_view sv = strip_line_comment(line);
275+
// Blank out raw-string-literal bodies first so embedded source text
276+
// (e.g. scaffold templates) isn't misparsed as imports.
277+
std::string code = strip_raw_strings(line, in_raw, raw_close);
278+
std::string_view sv = strip_line_comment(code);
227279
sv = trim(sv);
228280
if (sv.empty()) continue;
229281

tests/unit/test_modgraph.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,55 @@ TEST(Scanner, ProvidesAndRequires) {
4646
std::filesystem::remove_all(dir);
4747
}
4848

49+
// Regression: `import` lines that live INSIDE a multi-line raw-string literal
50+
// (e.g. a `mcpp new --template gui` skeleton embedded as R"GUI( ... )GUI") must
51+
// not be detected as real module imports. Before the fix this produced a
52+
// spurious "module 'imgui.core' imported but not provided" warning.
53+
TEST(Scanner, IgnoresImportsInsideRawStringLiteral) {
54+
auto dir = make_tempdir("mcpp-scanner-raw");
55+
write(dir / "src" / "gen.cppm",
56+
"export module gen;\n"
57+
"import std;\n"
58+
"const char* tmpl = R\"GUI(\n"
59+
"import imgui.core;\n"
60+
"import imgui.app;\n"
61+
"int main() { return 0; }\n"
62+
")GUI\";\n"
63+
"import bar;\n" // a real import AFTER the raw string
64+
"export void f();\n");
65+
66+
auto u = scan_file(dir / "src" / "gen.cppm", "pkg");
67+
ASSERT_TRUE(u.has_value()) << u.error().format();
68+
ASSERT_TRUE(u->provides.has_value());
69+
EXPECT_EQ(u->provides->logicalName, "gen");
70+
// Only the two genuine top-level imports — NOT imgui.core / imgui.app.
71+
ASSERT_EQ(u->requires_.size(), 2u);
72+
EXPECT_EQ(u->requires_[0].logicalName, "std");
73+
EXPECT_EQ(u->requires_[1].logicalName, "bar");
74+
for (auto& r : u->requires_) {
75+
EXPECT_NE(r.logicalName, "imgui.core");
76+
EXPECT_NE(r.logicalName, "imgui.app");
77+
}
78+
79+
std::filesystem::remove_all(dir);
80+
}
81+
82+
// A single-line raw string with an embedded import-looking body stays code.
83+
TEST(Scanner, IgnoresImportInsideSingleLineRawString) {
84+
auto dir = make_tempdir("mcpp-scanner-raw1");
85+
write(dir / "src" / "one.cppm",
86+
"export module one;\n"
87+
"const char* s = R\"(import nope;)\";\n"
88+
"import real;\n");
89+
90+
auto u = scan_file(dir / "src" / "one.cppm", "pkg");
91+
ASSERT_TRUE(u.has_value()) << u.error().format();
92+
ASSERT_EQ(u->requires_.size(), 1u);
93+
EXPECT_EQ(u->requires_[0].logicalName, "real");
94+
95+
std::filesystem::remove_all(dir);
96+
}
97+
4998
TEST(Scanner, RecordsPackageLocalIncludeDirs) {
5099
auto dir = make_tempdir("mcpp-scanner-includes");
51100
write(dir / "src" / "foo.cpp",

0 commit comments

Comments
 (0)