Skip to content

Commit 80291ca

Browse files
fix(build): 多段 glob 源文件路径在 Windows 上保持原生分隔符 (#390) (#391)
* fix(build): 多段 glob 源文件路径在 Windows 上保持原生分隔符 (#390) 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 增加无残留引号断言。 * fix(build): #390 修复补全 —— 摄入点收敛 + emitter 兜底 + 合并去重自愈 按 review(#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 指令路径的拼写, 声明输入指纹一次性失效 → 一次多余重建,属预期。 * test(cdb): 自愈守卫此前在两个平台都空转 —— 修好它,顺带收敛 flags.cppm 的第四份 join 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 通过。 --------- Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
1 parent 54d29fc commit 80291ca

13 files changed

Lines changed: 512 additions & 50 deletions

src/build/compile_commands.cppm

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,34 @@ std::vector<std::string> split_flags(std::string_view s) {
142142

143143
namespace {
144144

145+
// NATIVE separators for every path this emitter SPELLS ITSELF. Each ingestion
146+
// point (manifest globs, include_dirs, build.mcpp directives) is normalized at
147+
// the source, but this is the last line for the fields the CDB schema defines
148+
// — a path that slips through with a mixed `root\a/b` spelling (MSVC keeps
149+
// input `/` verbatim) breaks CLion, and "all ingestion points are covered" is
150+
// not a claim that can be proven once and stay true.
151+
//
152+
// It is NOT a whole-argv guarantee: the flag strings (split_flags(f.cxx), the
153+
// package cflags/cxxflags) pass through untouched, because normalizing an
154+
// arbitrary flag payload is unsafe — `-DPATH="/etc/x"` holds real slashes.
155+
// Those channels are normalized where they are ingested instead.
156+
// make_preferred() is a no-op on POSIX.
157+
std::string native_string(const std::filesystem::path& p) {
158+
auto n = p;
159+
n.make_preferred();
160+
return n.string();
161+
}
162+
145163
std::vector<std::string> local_include_args(const CompileUnit& cu) {
146164
std::vector<std::string> args;
147165
args.reserve(cu.localIncludeDirs.size());
148166
for (auto const& inc : cu.localIncludeDirs) {
149-
args.push_back("-I" + inc.string());
167+
args.push_back("-I" + native_string(inc));
150168
}
151169
// #249: after-dirs keep their -idirafter spelling in the compile DB so
152170
// tooling (clangd) reproduces the compiler's search order.
153171
for (auto const& inc : cu.localIncludeDirsAfter) {
154-
args.push_back("-idirafter" + inc.string());
172+
args.push_back("-idirafter" + native_string(inc));
155173
}
156174
return args;
157175
}
@@ -186,7 +204,7 @@ std::string emit_compile_commands(const BuildPlan& plan, const CompileFlags& fla
186204
: isCSource ? flags.cc
187205
: flags.cxx;
188206

189-
auto output_path = (plan.outputDir / cu.object).string();
207+
auto output_path = native_string(plan.outputDir / cu.object);
190208

191209
// Build arguments array.
192210
nlohmann::json args = nlohmann::json::array();
@@ -198,13 +216,13 @@ std::string emit_compile_commands(const BuildPlan& plan, const CompileFlags& fla
198216
for (auto& f : package_flag_args(cu, isCSource))
199217
args.push_back(std::move(f));
200218
args.push_back("-c");
201-
args.push_back(cu.source.string());
219+
args.push_back(native_string(cu.source));
202220
args.push_back("-o");
203221
args.push_back(output_path);
204222

205223
nlohmann::json entry;
206-
entry["directory"] = plan.projectRoot.string();
207-
entry["file"] = cu.source.string();
224+
entry["directory"] = native_string(plan.projectRoot);
225+
entry["file"] = native_string(cu.source);
208226
entry["arguments"] = std::move(args);
209227
entry["output"] = output_path;
210228

@@ -222,11 +240,25 @@ std::string merge_compile_commands(
222240
if (freshJ.is_discarded() || !freshJ.is_array())
223241
return std::string(fresh);
224242

243+
// Dedup key = the file's PATH, spelled the way a fresh plan spells it
244+
// (native separators). A prior CDB written before the mixed-separator
245+
// fix (#390) carries `root\generated/modules\x.cppm` entries that are
246+
// the SAME file as the fresh `root\generated\modules\x.cppm` — a literal
247+
// string comparison would keep both and the user's upgrade would not
248+
// visibly fix anything. Normalizing makes the merge self-healing: the
249+
// stale mixed entry is skipped on the first `mcpp build` after upgrade.
250+
// fileExists still probes the raw spelling — Windows accepts both.
251+
auto norm_key = [](std::string_view f) {
252+
auto p = std::filesystem::path(std::string(f)).lexically_normal();
253+
p.make_preferred();
254+
return p.string();
255+
};
256+
225257
// Files the current plan already covers — those entries are authoritative.
226258
std::set<std::string> freshFiles;
227259
for (auto const& e : freshJ) {
228260
if (e.contains("file") && e["file"].is_string())
229-
freshFiles.insert(e["file"].get<std::string>());
261+
freshFiles.insert(norm_key(e["file"].get<std::string>()));
230262
}
231263

232264
// Keep fresh order, then append still-valid prior entries the plan doesn't
@@ -238,7 +270,7 @@ std::string merge_compile_commands(
238270
for (auto const& e : existingJ) {
239271
if (!e.contains("file") || !e["file"].is_string()) continue;
240272
auto f = e["file"].get<std::string>();
241-
if (freshFiles.contains(f)) continue; // fresh wins
273+
if (freshFiles.contains(norm_key(f))) continue; // fresh wins
242274
if (!fileExists(std::filesystem::path(f))) continue; // pruned
243275
merged.push_back(e);
244276
}

src/build/directives.cppm

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,10 @@ const Def* find_by_tag(std::string_view tag) {
366366
}
367367

368368
std::string abs_against(const fs::path& base, std::string_view p) {
369-
fs::path pp(p);
369+
// Native spelling (see mcpp::modgraph::native_path_from_generic): a
370+
// directive path like `generated/modules/x` would otherwise stay mixed
371+
// on MSVC and leak into include flags / the CDB.
372+
fs::path pp = mcpp::modgraph::native_path_from_generic(p);
370373
if (pp.is_relative()) pp = base / pp;
371374
return pp.lexically_normal().string();
372375
}

src/build/flags.cppm

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,24 @@ CompileFlags compute_flags(const BuildPlan& plan) {
315315
// ninja-$-escape and shell-quote per token (#234) so an include dir
316316
// whose name contains a space can't silently split into two shell words
317317
// once ninja hands the resolved command line to the shell.
318+
// The one place this file turns a manifest include entry into a path.
319+
// make_preferred: a multi-segment TOML entry like `generated/inc` keeps
320+
// its `/` on MSVC, and the bare `projectRoot / inc` join would be MIXED —
321+
// reaching both the ninja command line and the CDB's arguments (via
322+
// f.cxx → split_flags). Same rule as every other manifest-path ingestion
323+
// point (#390); no-op on POSIX. ONE lambda because the same join is needed
324+
// four times in this function — {include_dirs, include_dirs_after} × {the
325+
// C/C++ token list, the NASM one} — and re-deriving it per site is how the
326+
// two channels drifted apart in the first place.
327+
auto abs_native = [&](const std::filesystem::path& inc) {
328+
auto p = inc.has_root_path() ? inc : (plan.projectRoot / inc);
329+
p.make_preferred();
330+
return p;
331+
};
332+
318333
std::vector<std::string> includeTokens;
319334
for (auto& inc : plan.manifest.buildConfig.includeDirs) {
320-
std::filesystem::path p = inc.has_root_path() ? inc : (plan.projectRoot / inc);
321-
includeTokens.push_back(include_token(d, p));
335+
includeTokens.push_back(include_token(d, abs_native(inc)));
322336
}
323337
// #249: `[build] include_dirs_after` — searched AFTER the toolchain's
324338
// system dirs via -idirafter (gcc+clang), so entries can't shadow
@@ -327,10 +341,8 @@ CompileFlags compute_flags(const BuildPlan& plan) {
327341
// (documented degradation; clang-MSVC uses the gnu dialect).
328342
const bool msvcInclude = d.includePrefix == std::string_view("/I");
329343
for (auto& inc : plan.manifest.buildConfig.includeDirsAfter) {
330-
std::filesystem::path ip(inc);
331-
std::filesystem::path p = ip.has_root_path() ? ip : (plan.projectRoot / ip);
332344
includeTokens.push_back(
333-
include_token(d, p, msvcInclude ? "/I" : "-idirafter"));
345+
include_token(d, abs_native(inc), msvcInclude ? "/I" : "-idirafter"));
334346
}
335347
std::string include_flags;
336348
for (auto& t : includeTokens) {
@@ -529,17 +541,22 @@ CompileFlags compute_flags(const BuildPlan& plan) {
529541
// re-spelt with -I regardless of dialect (nasm ≥2.14 inserts a missing
530542
// path separator itself); DWARF debug info exists on ELF only.
531543
if (!plan.nasmPath.empty()) {
544+
// Same abs_native join as the C/C++ channel above — one decision, one
545+
// implementation. Two knock-on effects, both wanted: the entry is now
546+
// spelt with native separators (#390), and the "already rooted?" test
547+
// becomes has_root_path() instead of is_absolute(), so a root-relative
548+
// `/x` entry is left alone here exactly as it is for the C/C++ include
549+
// list. The two predicates only differ on Windows, and only for that
550+
// spelling — where NASM disagreeing with the compiler about the SAME
551+
// `include_dirs` key was the bug, not the feature.
532552
std::string nasm_includes;
533553
for (auto& inc : plan.manifest.buildConfig.includeDirs) {
534-
auto abs = inc.is_absolute() ? inc : (plan.projectRoot / inc);
535-
nasm_includes += " -I" + escape_path(abs);
554+
nasm_includes += " -I" + escape_path(abs_native(inc));
536555
}
537556
// #249: nasm has no system header dirs to defer to — after-dirs
538557
// degrade to plain -I appended at the end.
539558
for (auto& inc : plan.manifest.buildConfig.includeDirsAfter) {
540-
std::filesystem::path ip(inc);
541-
auto abs = ip.is_absolute() ? ip : (plan.projectRoot / ip);
542-
nasm_includes += " -I" + escape_path(abs);
559+
nasm_includes += " -I" + escape_path(abs_native(inc));
543560
}
544561
std::string nasm_debug;
545562
if (prof.debug && plan.nasmFormat.starts_with("elf"))

src/build/plan.cppm

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,14 @@ make_plan(const mcpp::manifest::Manifest& manifest,
220220
// simply makes those units uncacheable.
221221
const std::vector<std::filesystem::path>& storeRoots = {});
222222

223+
// Expand one manifest `include_dirs` entry against the project root — the
224+
// #249 consistency join + the expand_dir_glob the dep path uses. Exported
225+
// (like modgraph's glob_literal_prefix) so unit tests can assert its
226+
// native-separator contract directly; see the definition below.
227+
std::vector<std::filesystem::path>
228+
expand_manifest_include_entry(const std::filesystem::path& root,
229+
const std::filesystem::path& inc);
230+
223231
} // namespace mcpp::build
224232

225233
namespace mcpp::build {
@@ -368,22 +376,42 @@ std::vector<std::string> shared_library_link_flags(
368376
return flags;
369377
}
370378

379+
} // namespace
380+
371381
// #249 consistency fix: expand include_dirs entries with the same
372382
// `expand_dir_glob` the dep path (prepare.cppm) uses, so a main-manifest
373383
// `include_dirs = ["*/include"]` glob works identically here. For a literal
374384
// (wildcard-free) entry expand_dir_glob only returns EXISTING directories,
375385
// whereas this helper historically joined unconditionally — keep the plain
376386
// join as a fallback so an -I for a dir created later (e.g. by a build
377387
// step) isn't silently dropped.
388+
//
389+
// Deliberately OUTSIDE the anonymous namespace: it is exported for its unit
390+
// test (like modgraph's glob_literal_prefix), and the two
391+
// local_include_dirs_*_for_manifest consumers below ride along so a single
392+
// namespace split serves the whole trio.
378393
std::vector<std::filesystem::path>
379394
expand_manifest_include_entry(const std::filesystem::path& root,
380395
const std::filesystem::path& inc)
381396
{
382-
if (inc.is_absolute()) return { inc };
397+
if (inc.is_absolute()) {
398+
// A TOML value like `C:/SDL2/include` keeps its `/` on MSVC — make
399+
// it native so the CDB's -I (via local_include_args) is uniform.
400+
auto n = inc;
401+
n.make_preferred();
402+
return { std::move(n) };
403+
}
383404
const auto glob = inc.generic_string();
384405
auto expanded = mcpp::modgraph::expand_dir_glob(root, glob);
385-
if (expanded.empty() && glob.find('*') == std::string::npos)
386-
expanded.push_back(root / inc);
406+
if (expanded.empty() && glob.find('*') == std::string::npos) {
407+
// Same native-spelling rule for the bare join (see above): `root / p`
408+
// with a multi-segment `generated/inc` is MIXED on MSVC, and this
409+
// fallback exists precisely for dirs like `generated/` that a later
410+
// build step creates — the #390 shape.
411+
auto joined = root / inc;
412+
joined.make_preferred();
413+
expanded.push_back(std::move(joined));
414+
}
387415
return expanded;
388416
}
389417

@@ -412,6 +440,8 @@ local_include_dirs_after_for_manifest(const std::filesystem::path& root,
412440
return dirs;
413441
}
414442

443+
namespace {
444+
415445
void append_unique_path(std::vector<std::filesystem::path>& out,
416446
std::filesystem::path path)
417447
{

src/build/prepare.cppm

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import mcpp.platform.axis;
1616
import mcpp.libs.json;
1717
import mcpp.log;
1818
import mcpp.manifest;
19+
import mcpp.modgraph.glob;
1920
import mcpp.modgraph.graph;
2021
import mcpp.modgraph.scanner;
2122
import mcpp.modgraph.validate;
@@ -2996,7 +2997,13 @@ prepare_build(bool print_fingerprint,
29962997
std::vector<std::filesystem::path> dirs;
29972998
for (auto const& inc : manifest.buildConfig.includeDirs) {
29982999
if (inc.is_absolute()) {
2999-
appendUniquePath(dirs, inc);
3000+
// Native spelling: a TOML `C:/SDL2/include` stays mixed on
3001+
// MSVC and leaks into the CDB's -I otherwise. Direct
3002+
// make_preferred — no generic_string round trip, which can
3003+
// throw for names the ANSI codepage cannot spell (mcpp#230).
3004+
auto n = inc;
3005+
n.make_preferred();
3006+
appendUniquePath(dirs, std::move(n));
30003007
continue;
30013008
}
30023009
for (auto& dir : mcpp::modgraph::expand_dir_glob(
@@ -3016,7 +3023,9 @@ prepare_build(bool print_fingerprint,
30163023
std::vector<std::filesystem::path> dirs;
30173024
for (auto const& inc : manifest.buildConfig.includeDirsAfter) {
30183025
if (inc.is_absolute()) {
3019-
appendUniquePath(dirs, inc);
3026+
auto n = inc;
3027+
n.make_preferred();
3028+
appendUniquePath(dirs, std::move(n));
30203029
continue;
30213030
}
30223031
for (auto& dir : mcpp::modgraph::expand_dir_glob(

src/modgraph/glob.cppm

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ import std;
1212

1313
export namespace mcpp::modgraph {
1414

15+
// Convert a manifest-style path or glob prefix (always spelled with the
16+
// generic `/` separator) to the platform's native spelling.
17+
//
18+
// MSVC's std::filesystem::path preserves the separators of the string it
19+
// was constructed from instead of normalizing them, so wrapping a raw
20+
// `generated/modules` in a path and joining it with `root / p` yields the
21+
// MIXED `C:\...\generated/modules` — and the directory-walk children built
22+
// on top of that stay mixed. `.string()` then carries the mixed form into
23+
// `compile_commands.json` (its `file` / `-c` fields), which CLion refuses
24+
// to parse. Ninja never notices because it renders everything via
25+
// generic_string(); the CDB is the first `.string()` consumer.
26+
//
27+
// POSIX is untouched (`make_preferred()` is a no-op there, and it is also
28+
// safe for already-native Windows input, which never contains `/`).
29+
std::filesystem::path native_path_from_generic(std::string_view s) {
30+
std::filesystem::path p(s);
31+
p.make_preferred();
32+
return p;
33+
}
34+
1535
// Does `candidate` match `glob`, interpreted relative to `root`?
1636
//
1737
// Supports "**" (any number of directory levels) and "*" (within one segment).

src/modgraph/scanner.cppm

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,12 @@ std::filesystem::path glob_literal_prefix(std::string_view glob) {
268268
? glob : glob.substr(0, wildcard);
269269
auto slash = literal.find_last_of('/');
270270
if (slash == std::string_view::npos) return {};
271-
return std::filesystem::path(literal.substr(0, slash));
271+
// Native separators, not the raw generic form: MSVC keeps the input's
272+
// `/` verbatim, and `root / p` plus the directory walk then propagate a
273+
// MIXED `root\generated/modules` into every downstream path — which is
274+
// what `compile_commands.json`'s `file` field showed on Windows for
275+
// multi-segment globs. See mcpp::modgraph::native_path_from_generic.
276+
return native_path_from_generic(literal.substr(0, slash));
272277
}
273278

274279
// mcpp#228: `{a,b}` alternation, recursively. Finds the first top-level `{`,
@@ -442,7 +447,9 @@ std::vector<std::filesystem::path> expand_dir_glob(const std::filesystem::path&
442447
// expand_glob) — include_dirs entries are meant to name one literal
443448
// directory each; a caller wanting alternatives lists multiple entries.
444449
if (glob.find('*') == std::string_view::npos) {
445-
auto p = root / std::filesystem::path(glob);
450+
// Native spelling (see native_path_from_generic — a raw `a/b` would
451+
// come back mixed from .string() on MSVC).
452+
auto p = root / native_path_from_generic(glob);
446453
if (std::filesystem::is_directory(p, ec)) out.push_back(p);
447454
return out;
448455
}
@@ -494,10 +501,24 @@ namespace {
494501

495502
// has_root_path: leave absolute AND root-relative ("/x" on Windows)
496503
// spellings alone — only genuinely root-less paths are project-relative.
504+
// Both branches normalize to NATIVE separators: a `-Ithird_party/inc` cxxflag
505+
// would otherwise come back as `C:\proj\third_party/inc` on MSVC (path keeps
506+
// the input `/` verbatim) and reach the CDB's arguments via packageCxxflags.
497507
std::string rewrite_rel_copy(const std::string& p, const std::filesystem::path& root) {
498508
std::filesystem::path fp(p);
499-
if (fp.has_root_path()) return p;
500-
return (root / fp).string();
509+
if (fp.has_root_path()) {
510+
// Nothing to re-spell → hand back the ORIGINAL bytes rather than
511+
// round-tripping them through path's narrow conversion, which throws
512+
// std::system_error for names the ANSI codepage cannot express
513+
// (mcpp#230 — see path_matches_glob). A rooted path with no '/' is
514+
// already native on both platform families.
515+
if (p.find('/') == std::string::npos) return p;
516+
fp.make_preferred();
517+
return fp.string();
518+
}
519+
auto joined = root / fp;
520+
joined.make_preferred();
521+
return joined.string();
501522
}
502523

503524
void rewrite_rel(std::string& p, const std::filesystem::path& root) {
@@ -682,7 +703,14 @@ local_include_dirs_for(const std::filesystem::path& root,
682703
std::vector<std::filesystem::path> dirs;
683704
for (auto const& inc : manifest.buildConfig.includeDirs) {
684705
if (inc.is_absolute()) {
685-
dirs.push_back(inc);
706+
// A TOML value like `C:/SDL2/include` keeps its `/` on MSVC —
707+
// normalize so the CDB's -I comes out native (mixed separators
708+
// break CLion). Direct make_preferred, no generic_string round
709+
// trip: the narrow conversion can throw for names the ANSI
710+
// codepage cannot spell (mcpp#230).
711+
auto n = inc;
712+
n.make_preferred();
713+
dirs.push_back(std::move(n));
686714
continue;
687715
}
688716
for (auto& d : expand_dir_glob(root, inc.generic_string())) {
@@ -701,7 +729,9 @@ local_include_dirs_after_for(const std::filesystem::path& root,
701729
std::vector<std::filesystem::path> dirs;
702730
for (auto const& inc : manifest.buildConfig.includeDirsAfter) {
703731
if (inc.is_absolute()) {
704-
dirs.push_back(inc);
732+
auto n = inc;
733+
n.make_preferred();
734+
dirs.push_back(std::move(n));
705735
continue;
706736
}
707737
for (auto& d : expand_dir_glob(root, inc.generic_string())) {
@@ -738,7 +768,10 @@ void scan_one_into(ScanResult& result,
738768
// Literal absolute entry — e.g. a dependency build.mcpp's OUT_DIR
739769
// generated source, which lives OUTSIDE the (possibly read-only)
740770
// package root. No glob expansion; taken as-is when it exists.
741-
if (std::filesystem::path gp(g); gp.is_absolute()) {
771+
// Native spelling: a raw `C:/abs/x.cppm` would stay mixed on MSVC
772+
// (see native_path_from_generic) and leak into the CDB.
773+
auto gp = native_path_from_generic(g);
774+
if (gp.is_absolute()) {
742775
std::error_code aec;
743776
if (std::filesystem::is_regular_file(gp, aec)) all_files.insert(gp);
744777
continue;

0 commit comments

Comments
 (0)