Skip to content

fix(build): 多段 glob 源文件路径在 Windows 上保持原生分隔符 (#390) - #391

Merged
Sunrisepeak merged 3 commits into
mcpp-community:mainfrom
ZheFeng7110:fix/cdb-native-separators
Aug 8, 2026
Merged

fix(build): 多段 glob 源文件路径在 Windows 上保持原生分隔符 (#390)#391
Sunrisepeak merged 3 commits into
mcpp-community:mainfrom
ZheFeng7110:fix/cdb-native-separators

Conversation

@ZheFeng7110

@ZheFeng7110 ZheFeng7110 commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

compile_commands.json 在 Windows 上对多段 glob 源(generated/modules/**/*.cppm)输出混合分隔符路径 ...\generated/modules\x.cppm,CLion 无法解析。根因:MSVC 的 std::filesystem::path 保留输入 / 原样,glob_literal_prefix 用原始 glob 串构造 path 后,root / prefix 与目录迭代把混合形式传入 CompileUnit::source → CDB 的 file/-c(ninja 因一律 generic_string() 不受影响)。

修复(含 review 补全)

  • 摄入点归一化(收敛同一决策的全部推导,不依赖「摄入点找全」):
    • modgraph/glob.cppm 新增 native_path_from_generic(make_preferred 实现),应用于 glob_literal_prefix / expand_dir_glob 快速路径 / scan_one_into 绝对源分支
    • directives.cppm::abs_against(build.mcpp 指令路径;归一化会改写拼写 → 声明输入指纹一次性失效 → 一次多余重建,预期行为)
    • plan.cppm::expand_manifest_include_entry(绝对分支 + generated/ 裸拼接回退 —— 正是 fix: Windows compile_commands.json 多段 glob 源路径混用分隔符,CLion 无法解析 #390 场景;该函数从匿名命名空间提出并导出以供单测)
    • scanner.cppm::rewrite_rel_copy([build] cxxflags = ["-Ithird_party/inc"] 通道)
    • flags.cppm [build] include_dirs 全局 cxxflags 通道
    • include_dirs 绝对分支直接 make_preferred,不再 generic_string() 窄串往返(ANSI 代码页拼不出的名字会抛,mcpp#230)
  • emitter 兜底:emit_compile_commandsfile / directory / -c / -o / -I 统一 make_preferred,对 CDB 契约给出无条件保证。
  • P2 合并去重自愈:merge_compile_commands 去重键改为归一化路径(lexically_normal + make_preferred),旧 CDB 的混合拼写与 fresh 原生拼写视为同一文件 → 升级后第一次真正重建即自愈,无需手删 CDB。已在注入旧条目的工程上实测(重建后混合条目归零)。
  • 引号问题由 8.8.4 修复(feat: 机器可读输出协议 —— 信封 + 效应 + --format 归一 (2026.8.8.4) #385),本 PR 不再重复,但为其补了 jq 无关的 e2e 47 回归断言。

测试

  • 单测 +4:NormalizedFileKeysHealStaleSeparatorSpellings(合并自愈)、EmittedPathsUseNativeSeparators(emitter 兜底)、Plan.ExpandManifestIncludeEntryNativeSpellingScanner.RelativeIncludeFlagsAbsolutized 平台化;另更新受归一化影响的既有断言(build_flags / ninja_backend)。
  • e2e 76:多段 glob 源 + extra.cpp 必须入 CDB 的 grep 守卫 + 平台无关混合分隔符断言(消掉 os.name 依赖,避免 MSYS python 假绿)+ python3 缺失时显式 SKIP。
  • 实测:cmscript 重建后 9 个混合分隔符条目与 38 个带引号参数全部归零;带旧 CDB 的注入工程重建后混合条目归零。

Closes #390

Test plan

  • mcpp build 自举成功(Windows, clang)
  • mcpp test:68/68 通过(含新增单测)
  • e2e 47、76 通过(76 的 python 校验段本机用 python 复核正/反两例)
  • CI(linux / macOS / windows)e2e 与单测待跑

manifest glob(如 `generated/modules/**/*.cppm`)用 `/` 拼路径;
MSVC 的 std::filesystem::path 保留输入分隔符原样,于是
`root / prefix` 与目录迭代子路径都是混合形式
(`...\9bca0b44ae3aa660\generated/modules\ccc.when.cppm`),
一路流入 CompileUnit::source → compile_commands.json 的 file/-c 字段,
CLion 拒绝解析。ninja 侧一律 generic_string()(全正斜杠)所以构建正常,
CDB 是第一个 .string() 消费者,缺陷只在那里显现。

新增 mcpp::modgraph::native_path_from_generic(glob.cppm),在
glob_literal_prefix / expand_dir_glob / scan_one_into / 绝对 include dir
分支 / directives::abs_against 摄入点归一化;顺带修复 build.mcpp 指令
路径与 TOML 绝对 include dir(C:/SDL2/include)同类混合问题。

单测:GlobLiteralPrefixUsesNativeSeparators / NativePathFromGeneric /
ExpandGlobMultiSegmentUsesNativeSeparators /
ExpandDirGlobMultiSegmentUsesNativeSeparators。
e2e:76 增加多段 glob 源 + Windows 原生分隔符断言;47 增加无残留引号断言。

@speak-agent speak-agent left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一、这个 PR 做了什么

根因诊断准确:MSVC 的 std::filesystem::path 不归一化构造串里的分隔符,于是 glob_literal_prefix("generated/modules/**/*.cppm") 返回的 generated/modules 原样带着 /,root / prefix 与其上的 recursive_directory_iterator 子路径就一路是 C:\...\generated/modules\x.cppm。ninja 侧一律 generic_string() 所以无感,compile_commands.cppm第一个 .string() 消费者,缺陷只在 CDB 显现 —— 这条因果链逐段核过,成立。

修法是「在摄入点归一化」,新增 mcpp::modgraph::native_path_from_generic,改了 5 处摄入点。

特别值得肯定的一点:同时改 directives.cppm::abs_against 不是顺手,而是必须role=Source 的 action 输出经 abs_against 绝对化后被 adoptActionOutputs 塞进 modules.sources,再走 scan_one_into 的绝对分支。这两处只改一处,同一个文件就会拿到两种拼写 —— 而 ninja 是按声明字符串识别节点的,那是 "multiple rules generate / missing and no known rule" 级别的回归。这对耦合被一起改了。

二、主要问题

P1 — 同一决策还有第三、第四处推导,都没改,都直达 CDB

这正是这个仓库反复付学费的形状(「同一决策两处推导」)。这次是四处,PR 只改了两处:

(a) src/build/plan.cppm:379 expand_manifest_include_entry —— 第三份 include_dirs 摄入逻辑,两个洞:

if (inc.is_absolute()) return { inc };          // :382  TOML 里的 C:/SDL2/include 原样混合
...
if (expanded.empty() && glob.find('*') == npos)
    expanded.push_back(root / inc);              // :386  C:\proj + generated/inc → 混合

第二个洞尤其要命:它的注释写着「为稍后由 build step 创建的目录保留裸拼接」—— 也就是 generated/ 这一类目录,恰好就是 #390 的场景。结果经 local_include_dirs_for_manifestmain_cu.localIncludeDirs(plan.cppm:1176)→ local_include_args → CDB 的 -I

(b) src/modgraph/scanner.cppm:504 rewrite_rel_copy:

std::filesystem::path fp(p);
if (fp.has_root_path()) return p;
return (root / fp).string();                     // :507

[build] cxxflags = ["-Ithird_party/inc"]normalize_include_flags 走到这里 → C:\proj\third_party/inc → 进 packageCxxflagspackage_flag_args → CDB arguments。同一个缺陷,同一个终点。

建议:这四处应该收敛,而不是逐个打补丁。要么把 expand_manifest_include_entrylocal_include_dirs_for 合并成一个函数(它们本来就是同一个决策),要么至少在 emit_compile_commands 里加一层兜底归一化(file / directory / -c / -o / -I 统一 .make_preferred())。后者只有几行,且能对 CDB 契约给出无条件保证 —— 不依赖「所有摄入点都被找全」这个不可证伪的前提。倾向两者都做:摄入点归一化保证 ninja 节点身份一致,emitter 归一化保证对外契约。

P2 — merge_compile_commands 不自愈:升级后旧的混合条目会被保留,并变成重复条目

compile_commands.cppm:229/241file 字段的字面字符串做去重键:

freshFiles.insert(e["file"].get<std::string>());   // 新的:native
if (freshFiles.contains(f)) continue;              // 旧的:mixed → 不命中
if (!fileExists(fs::path(f))) continue;            // Windows API 吃混合分隔符 → 存在
merged.push_back(e);                               // → 保留

#390 咬到的用户,工程根下已经有一份带 9 条混合路径的 compile_commands.json。升级后 mcpp build:新条目是原生的,旧的混合条目因为拼写不同而不被视作同一文件,又因为文件真实存在而不被剪掉 —— 于是同一个源文件出现两条,其中一条仍是 CLion 解析不了的那种。用户不手动删 CDB 就看不到修复效果。

PR 描述说实测「9 个混合条目全部归零」,这大概率是在干净 CDB 上量的。请在已有旧 CDB 的工程上复测一次。

修法很轻:比较时归一化(fs::path(f).lexically_normal().make_preferred().string()),或直接丢弃 file 同时含 \/ 的历史条目。

P3 — e2e 76 的 Windows 断言很可能没跑(假绿),且没有断言目标源真的进了 CDB

两道门叠在一起:

if command -v python3 >/dev/null 2>&1; then     # 门 1
    ...
    if os.name == "nt" and "/" in e["file"]:    # 门 2
  • 门 1:Windows CI 走 shell: bash(Git Bash)。仓库里现有的 Windows 判别一律是 uname -s + MINGW*|MSYS*|CYGWIN*(02、118、147、199),没有先例证明 python3 这个名字在 Git Bash 下能解析到。解析不到 → 整段 JSON 校验(含新断言)静默跳过。
  • 门 2:即使解析到,若拿到的是 MSYS 的 python,os.name'posix',断言再次静默跳过。

两道门都是跳过而不是失败,所以 e2e 76 在 Windows 上「通过」完全不能证明断言执行过。建议:

  1. 判据换成平台无关的,消掉 os.name 依赖 —— 混合本身就是可判的:
    f = e["file"]
    assert not ("\\" in f and "/" in f), "mixed separators: %r" % f
    POSIX 上恒真,Windows 上恰好咬住这个 bug。
  2. 加一条 grep -q 'extra\.cpp' "$cdb" —— 否则多段 glob 一旦没匹配上,测试照样绿,而覆盖是零。这一条比断言本身更重要。
  3. 门 1 退化时至少 echo "SKIP: ...",别静默。

另外 76 里追加 sources = [...] 会把 src/main.cpp 显式 glob 进来,plan 走的是 packages[0].privateBuild 分支而非 local_include_dirs_for_manifest 分支 —— 对本测试目的无碍,但顺带说明:P1(a) 那条路径现在没有任何测试覆盖

三、次要意见

native_path_from_generic 可以直接用标准库。 标准早就有 path::make_preferred(),语义完全一致,且作用在 native(宽字符)串上:

std::filesystem::path native_path_from_generic(std::string_view s) {
    std::filesystem::path p(s);
    p.make_preferred();
    return p;
}

手写字节替换本身没错(0x2F 不会是任何 Windows ANSI 代码页的尾字节),但没有理由不用现成的。

更值得改的是 inc.generic_string() 往返:prepare.cppm:3001/3025scanner.cppm:695/714 对一个已经是 path 的绝对路径先转窄串再转回去。而这个文件顶上 path_matches_glob 的注释(mcpp#230)明明白白记着:MSVC 的窄转换在 ANSI 代码页拼不出该名字时会std::system_error。这里既没有 try 也没有必要 —— 直接:

auto n = inc; n.make_preferred();
appendUniquePath(dirs, n);

风险确实低(这些 path 本来就来自窄串 TOML,往返基本无损),但这是白拿的鲁棒性,而且这个项目的用户路径里出现中文的概率不低。

abs_against 的改动会改写 build.mcpp 指令路径的拼写,进而让声明输入的指纹一次性失效 → 一次多余重建。可接受,但值得在 PR 描述里写一句。

单测形状是对的(直接测 glob_literal_prefix 契约 + 端到端测 expand_glob/expand_dir_glob,并用 if constexpr 让 Windows 特有断言在 POSIX 上仍参与编译检查)。小冗余:GlobLiteralPrefixUsesNativeSeparators 第一行与 GlobLiteralPrefixDerivation 里改过的那行完全重复。

残留同类:prepare.cppm:5042${mcpp.out_dir} 替换把 outputDir.string()(原生)拼进用户写的 ${mcpp.out_dir}/gen/x.cppm,产出混合。这些 action 输出因为含 ${mcpp. 而不会被 adoptActionOutputs 采纳成源,所以够不到 CDB,但它是同一个类里未清的一员。可以另开 issue,不必阻塞本 PR。

四、其他

  • 性能、安全性:无影响(纯路径拼写,零新增 I/O)。
  • e2e 47 新增的两条 grep 守卫写法正确(\\" 在 BRE 里匹配字面反斜杠),放在 jq 门之前也对。属于顺带补的回归网,不反对。
  • 这个修复只有随版本发布才能到 #390 报告者手里 —— 本 PR 没有 bump MCPP_VERSION,按仓库惯例是攒到发版 PR,确认一下即可。

五、结论

方向正确,但建议先补完再合。 阻塞项两条:

  1. P1 —— 修复不完整。plan.cppm:382/386scanner.cppm:507 是同一缺陷的另外两个出口,都直达 CDB 的 -I,其中 root / inc 那条正好落在 #390generated/ 场景上。
  2. P3(2) —— e2e 76 必须断言 extra.cpp 真的进了 CDB,否则这是一个覆盖可以静默归零的测试。

P2 建议一并处理(几行),否则受影响用户升级后症状不消失。P3(1) 与 make_preferred() 是明确改进,但不阻塞。

按 review(mcpp-community#391) 补全同一决策的全部推导点,不再依赖「所有摄入点都被找全」:

- P1: 补上 plan.cppm expand_manifest_include_entry(绝对分支与
  generated/ 裸拼接回退)、scanner.cppm rewrite_rel_copy(cxxflags 的
  -Ithird_party/inc 通道)、flags.cppm [build] include_dirs 全局 cxxflags
  通道;并在 emit_compile_commands 加最后一层兜底:file/directory/-c/-o/-I
  统一 make_preferred,对 CDB 契约给出无条件保证。
- P2: merge_compile_commands 去重键改为归一化路径(lexically_normal +
  make_preferred),旧 CDB 里的混合分隔符条目与 fresh 原生拼写视为同一
  文件 → 升级后第一次 build 即自愈,用户无需手删 compile_commands.json。
  已在带旧条目的工程上实测:注入混合条目后重建,归零。
- P3: e2e 76 的 Windows 断言改为平台无关的「同时含 \ 与 / 即失败」
  (消掉 os.name 依赖,避免 MSYS python 假绿);新增 extra.cpp 必须进
  CDB 的 grep 守卫;python3 缺失时显式 SKIP。
- native_path_from_generic 改用标准库 make_preferred;include_dirs 绝对
  分支不再做 generic_string 窄串往返(ANSI 代码页拼不出的名字会抛,
  mcpp#230)。
- 单测:NormalizedFileKeysHealStaleSeparatorSpellings(合并自愈)、
  EmittedPathsUseNativeSeparators(emitter 兜底)、
  Plan.ExpandManifestIncludeEntryNativeSpelling(plan.cppm 摄入点,
  expand_manifest_include_entry 为此从匿名命名空间提出并导出)。

注:directives::abs_against 归一化会改写 build.mcpp 指令路径的拼写,
声明输入指纹一次性失效 → 一次多余重建,属预期。
@ZheFeng7110

Copy link
Copy Markdown
Member Author

已按 review 补全,提交 661a7d1。逐条回应:

P1(阻塞)—— 修复不完整:同意,已收敛全部推导点。除 PR 原两处外,补上:

  • plan.cppm:expand_manifest_include_entry(绝对分支 + generated/ 裸拼接回退 —— 正是 fix: Windows compile_commands.json 多段 glob 源路径混用分隔符,CLion 无法解析 #390 场景;函数从匿名命名空间提出并导出,新增单测 Plan.ExpandManifestIncludeEntryNativeSpelling)
  • scanner.cppm:rewrite_rel_copy(-Ithird_party/inc cxxflags 通道)
  • flags.cppm [build] include_dirs 全局 cxxflags 通道(第五处推导)
  • emitter 兜底:emit_compile_commandsfile/directory/-c/-o/-I 统一 make_preferred,对 CDB 契约给出无条件保证 —— 两者都做了。

P2 —— 合并不自愈:已修。merge_compile_commands 去重键改为 lexically_normal + make_preferred,旧混合条目与 fresh 原生拼写视为同一文件 → 升级后第一次真正重建即自愈。已在注入旧条目的工程上实测:重建后混合条目归零(单测 NormalizedFileKeysHealStaleSeparatorSpellings 锁行为;注意需真实重建而非 fast-path)。

P3 —— e2e 76 假绿:三条全改。

  1. 判据改为平台无关:assert not ("\\\\" in f and "/" in f),消掉 os.name 依赖(本机 python 正/反两例都验证过:混合条目被准确抓住)。
  2. 新增 grep -q 'extra\.cpp' "$cdb" 守卫 —— 多段 glob 没匹配上直接 FAIL。
  3. python3 缺失时显式 echo SKIP,不再静默。

次要意见:全部采纳。native_path_from_generic 改用标准库 make_preferred();include_dirs 绝对分支直接 make_preferred、去掉 generic_string 窄串往返(mcpp#230);GlobLiteralPrefixUsesNativeSeparators 重复行已删。

prepare.cppm:5042${mcpp.out_dir} 同类残留按建议另开 issue 跟踪,不阻塞本 PR。

Sunrisepeak

This comment was marked as off-topic.

@speak-agent speak-agent left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review @ 661a7d1

P1 / P2 / P3 的生产代码改动我逐处核过,都对:五处摄入点收敛齐了(含我漏提的 flags.cppm 第五处,补得好)、native_path_from_generic 换成 make_preferred()generic_string() 窄串往返去掉、norm_key 的自愈逻辑正确、e2e 76 三条全改到位(extra.cpp 守卫 + 平台无关判据 + 显式 SKIP)。expand_manifest_include_entry 的前向声明确认落在 export namespace mcpp::build(plan.cppm:19)内,单测可见。

新增的测试里有一条是惰性的,而且恰好是 P2 那条修复的唯一守卫。


F1(合入前应处理)—— NormalizedFileKeysHealStaleSeparatorSpellings两个平台上都没测到东西

auto p = std::filesystem::path("/p") / "generated" / "modules" / "a.cpp";
auto fresh    = cdb({ entry(p.string(),         "-O2-FRESH") });
auto existing = cdb({ entry(p.generic_string(), "-O0-STALE") });

Windows:p.string()/p\generated\modules\a.cppentry()std::format 把它未经 JSON 转义直接拼进字符串字面量:

"file":"/p\generated\modules\a.cpp"

\g 不是合法的 JSON 转义(RFC 8259)。我用同一套转义规则实测确认:

PARSE ERROR -> Invalid \escape: line 1 column 30 (char 29)

于是 merge_compile_commands 的第一句就短路:

auto freshJ = nlohmann::json::parse(fresh, nullptr, /*allow_exceptions=*/false);
if (freshJ.is_discarded() || !freshJ.is_array())
    return std::string(fresh);        // ← 直接从这里返回

merged == fresh,于是 count(merged,"generated")==1 ✓、-O2-FRESH 在 ✓、-O0-STALE 不在 ✓ —— 三条断言全部平凡通过,norm_key 一次都没被调用

POSIX:p.string() == p.generic_string(),两个条目的 file 字节相同 —— 换回修改前的字面串键这个测试同样通过。

注释里已经诚实写了「On POSIX the two spellings are byte-identical」,但结论下反了:那不是「退化成 fresh-wins 契约」,而是自愈这条唯一的守卫在三平台都没有咬合。生产逻辑本身是对的(lexically_normal + make_preferred 双侧归一,我核过),问题纯在测试。

修法,两处一起:

  1. entry()file 做 JSON 转义(或直接用 nlohmann::json 构造条目,别手拼)。
  2. stale 拼写换成每个平台都需要归一化的形式,让 lexically_normal 那一半也有覆盖:
    auto stale = "/p/generated/./modules/a.cpp";   // POSIX 上也必须折叠才能命中
    这样 Windows 测分隔符、POSIX 测 lexically_normal,两条腿都站住。

F2 —— emitter 兜底的覆盖面被高估了,而它的测试也是空转

PR 描述说兜底「对 CDB 契约给出无条件保证」。实际 emit_compile_commands 里只有这些位置过了 native_string:file / directory / output / -c 的值 / -o 的值 / local_include_args-I-idirafter

没过的(compile_commands.cppm:205-210):

args.push_back(compiler.string());                 // 编译器路径
for (auto& f : split_flags(flagStr))     ...        // f.cxx —— [build] include_dirs 走 compute_flags 的那份 -I、
                                                    // 依赖的 -isystem、-fmodule-file=、-fprebuilt-module-path=、--sysroot=
for (auto& f : package_flag_args(cu, ...)) ...      // packageCxxflags 里的 -I

这些仍然只由摄入点保证。这不是缺陷(摄入点都补齐了),但「无条件」的说法会让下一个人以为兜底能兜住 argv 全部,建议把描述改成准确的:兜底覆盖 CDB schema 的路径字段 + emitter 自己构造的那几个 argv 位置,flag 串仍归摄入点负责(而且也只能如此——对任意 flag 载荷做路径归一化本来就不安全,比如 -DPATH="/etc/x")。

顺带,EmittedPathsUseNativeSeparators 里这个循环在当前 fixture 下遍历不到任何含路径的参数:

for (auto const& a : e["arguments"])
    if (a.get<std::string>().starts_with("-"))
        EXPECT_EQ(a.get<std::string>().find('/'), std::string::npos);

flags.cxxpackageCxxflags 都是空的,arguments 只有 ["/usr/bin/g++", "-c", <file>, "-o", <output>],以 - 开头的只有 -c / -o 两个裸 token —— 断言恒真。建议 fixture 里填上 .localIncludeDirs = { "C:/inc/a" },断言那条 -I 是原生 —— 那才是兜底真正覆盖的部分。(另外这条规则不宜推广成「所有 - 开头参数不得含 /」,-DPATH="/etc/x" 就会误伤。)


F3 —— flags.cppm 里同一决策还剩最后两份未归一化的副本

flags.cppm:540/547,NASM 的 include 循环:

auto abs = inc.is_absolute() ? inc : (plan.projectRoot / inc);   // :540
auto abs = ip.is_absolute()  ? ip  : (plan.projectRoot / ip);    // :547

与刚改好的 :325/:336 是同一个 join,同一个文件,相隔 200 行,写法当场不一致。

够不到 CDB(compile_commands.cppm:198 显式 if (cu.source.extension() == ".asm") continue;),所以不是本 issue 的缺陷 —— 但 Windows + NASM 的 ninja 命令行会留混合拼写,而且这正是本 PR 想终结的那类债。建议抽一个局部 helper 给四处共用:

auto abs_native = [&](const std::filesystem::path& inc) {
    auto p = inc.has_root_path() ? inc : (plan.projectRoot / inc);
    p.make_preferred();
    return p;
};

那才叫收敛;否则下一个人看到同文件两种写法,还得再判断一次哪种是对的。


F4 —— rewrite_rel_copy 的绝对分支新引入了一次窄串往返

if (fp.has_root_path()) {
    fp.make_preferred();
    return fp.string();      // path(p) 窄→宽,.string() 宽→窄
}

改前是 return p;,零转换。本次提交刚在 prepare.cppm / scanner.cppm 删掉 generic_string() 往返并注明理由(mcpp#230:MSVC 的窄转换对 ANSI 代码页拼不出的名字会抛 std::system_error),这里等于又加回一次。

风险确实低(同码页往返、相对分支本来就有这个往返),不阻塞;只是与本提交自陈的原则不一致。想保守的话:仅当 p.find('/') != npos 时才重写,否则原样返回。


其他(提示,非缺陷)

  • plan.cppm 把三个函数移出匿名 namespace —— 声明位置确认无误。但这个仓库有过 clang22 + modules 下匿名 namespace 结构变化导致邻居函数误编译的记录(PR#332 / #334,当时的约束是「别再往那个匿名 ns 加代码」)。本机绿之前别下结论,等三平台 CI。
  • 指纹失效范围比 PR 描述写的大:描述只提了 abs_against,但 flags.cppm-I 拼写变化同样进 ninja 命令行 ⇒ Windows 上带多段 include_dirs 的工程升级后也是一次全量重建。建议描述里一并写上。
  • TEST(Plan, ExpandManifestIncludeEntryNativeSpelling) 放在 test_ninja_backend.cpp,且用固定名 temp_directory_path()/"mcpp-plan-inc-entry" 而非仓库惯用的 make_tempdir —— 并发/残留下会互删。
  • 同一测试里 expand_manifest_include_entry(root, "C:/SDL2/include") 在 POSIX 上走的是回退分支(C:/... 在 POSIX 不是绝对路径),ASSERT_EQ(abs.size(), 1u) 两平台都成立但成立的理由不同。无害,知道就行。

结论

方向和实现都对了。 唯一建议在合入前处理的是 F1 —— P2 那条自愈修复目前没有任何有效测试,而它恰恰是「用户升级后能不能看到修复」的那一条。F2 顺手把描述和 fixture 改准。F3 / F4 可跟随本 PR 也可另开,不阻塞。

prepare.cppm:5042${mcpp.out_dir} 残留同意另开 issue。

review 第二轮的四条:

1) NormalizedFileKeysHealStaleSeparatorSpellings 之前测不到东西。
   测试 helper `entry()` 用 std::format 手拼 JSON,而 Windows 的
   `file` 值带反斜杠 —— `...\generated\...` 里的 `\g` 不是合法 JSON
   转义,整份 fixture 无法解析。merge_compile_commands 对解析失败的
   回答是 `return std::string(fresh)`,于是三条断言在没有跑到 norm_key
   的情况下全部通过。POSIX 上则是另一种空转:stale 用
   p.generic_string(),与 fresh 的 p.string() 逐字节相同,换回修改前
   的字面串键一样能过。
   修法两处:entry() 改由 nlohmann 序列化(转义按构造保证);stale
   拼写换成 `/p/generated/./modules/a.cpp` —— `/./` 在 POSIX 上也必须
   经 lexically_normal 折叠才能与 fresh 对齐,Windows 上再叠一层
   make_preferred。cdb() 里加一条 fixture 自检,让「拼出来的 JSON 不
   可解析」这类空转以后直接红。
   已验证先红:把 norm_key 临时退回字面串键,该测试在 Linux 上失败
   (正是它原来空转的平台),恢复后 68/68。

2) emitter 兜底的范围说清楚。它覆盖 CDB schema 的路径字段与 emitter
   自己构造的 argv 位置,不覆盖 split_flags(f.cxx) 与 package 的
   cflags/cxxflags —— 对任意 flag 载荷做路径归一化本来就不安全
   (`-DPATH="/etc/x"` 里的斜杠是真的)。注释按实际范围改写。
   EmittedPathsUseNativeSeparators 里那个「以 - 开头的参数不得含 /」
   的循环在原 fixture 下遍历不到任何带路径的参数(flags 全空,只剩
   裸 -c/-o),换成填上 localIncludeDirs / localIncludeDirsAfter 后
   逐条断言 -I 与 -idirafter,POSIX 分支也一并断言。

3) flags.cppm 的 include join 收敛成一个 abs_native lambda,四处共用
   ({include_dirs, include_dirs_after} × {C/C++ 通道, NASM 通道})。
   NASM 那两处此前既没归一化分隔符,谓词也用的是 is_absolute() 而非
   has_root_path() —— 同一个 manifest 键在两个通道产出不同的路径。
   两者只在 Windows 的根相对拼法(`/x`)上有别,现在与编译器通道一致。
   够不到 CDB(.asm 单元被 emitter 显式跳过),但这正是本 PR 要终结的
   那类债。新增 NasmIncludeDirsMatchTheCxxChannelSpelling 钉住契约
   (POSIX 上是防回归守卫,Windows 上才是新断言)。

4) rewrite_rel_copy 的绝对分支不再无谓往返:没有 `/` 可改时直接返回
   原字节,不经 path 的窄转换 —— 那个转换对 ANSI 代码页拼不出的名字
   会抛 std::system_error(mcpp#230),而本 PR 刚以同一理由删掉别处的
   generic_string() 往返。

本机:mcpp build 自举通过,mcpp test 68/68,e2e 47 / 76 / 105 /
141 / 148 / 179 / 25 / 51 通过。
@speak-agent

Copy link
Copy Markdown
Member

已按第二轮 review 直接在本分支推了 fe82dd3,四条都处理了。

F1(主要) —— NormalizedFileKeysHealStaleSeparatorSpellings 之前两个平台都在空转,现已修好并验证过先红:把 norm_key 临时退回字面串键,该测试在 Linux 上失败(正是它原来空转的那个平台),恢复后 68/68。

修了两处成因:

  • entry() 改由 nlohmann 序列化。手拼 JSON 时 Windows 的 file 值带反斜杠,...\generated\... 里的 \g 不是合法 JSON 转义,整份 fixture 无法解析 → merge_compile_commandsreturn std::string(fresh) → 断言在没跑到 norm_key 的情况下全过。
  • stale 拼写换成 /p/generated/./modules/a.cpp/./ 在 POSIX 上也必须经 lexically_normal 折叠才能与 fresh 对齐,Windows 上再叠一层 make_preferred —— 两条腿都站住。原来的 p.generic_string() 在 POSIX 上与 fresh 逐字节相同。
  • 另在 cdb() 里加了一条 fixture 自检,让「拼出来的 JSON 不可解析」这类空转以后直接红。

F2 —— 兜底范围按实际改写(它覆盖 CDB schema 的路径字段 + emitter 自己构造的 argv 位置,不覆盖 split_flags(f.cxx) 与 package cflags/cxxflags;对任意 flag 载荷做归一化本来就不安全)。EmittedPathsUseNativeSeparators 里那个「以 - 开头的参数不得含 /」的循环在原 fixture 下遍历不到任何带路径的参数,已换成填上 localIncludeDirs/localIncludeDirsAfter 后逐条断言 -I-idirafter,POSIX 分支也一并断言。

F3 —— flags.cppm 的 include join 收敛成一个 abs_native lambda,四处共用。顺带说明一个我在 review 里没写、改的时候才看清的点:NASM 那两处不只是没归一化分隔符,谓词还用的是 is_absolute() 而非 has_root_path() —— 同一个 include_dirs 键在两个通道会产出不同的路径。两者只在 Windows 的根相对拼法(/x)上有别,现在与编译器通道一致。新增 NasmIncludeDirsMatchTheCxxChannelSpelling 钉住契约(说明白:POSIX 上它是防回归守卫,Windows 上才是新断言)。

F4 —— rewrite_rel_copy 绝对分支没有 / 可改时直接返回原字节,不经 path 的窄转换。

本机:自举构建通过,mcpp test 68/68,e2e 47 / 76 / 105 / 141 / 148 / 179 / 25 / 51 全过。等三平台 CI。

prepare.cppm:5042${mcpp.out_dir} 残留仍按约定另开 issue,不在本 PR。

@Sunrisepeak
Sunrisepeak merged commit 80291ca into mcpp-community:main Aug 8, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: Windows compile_commands.json 多段 glob 源路径混用分隔符,CLion 无法解析

3 participants