From abe02e3a1768a89c10c1460208bcfff772393c44 Mon Sep 17 00:00:00 2001 From: Zhe Feng Date: Sun, 11 Oct 2026 23:27:35 +0800 Subject: [PATCH] fix: spell every ${mcpp.*} path token generically (#393) An action's `${mcpp.out_dir}`, `${mcpp.bin_dir}`, `${mcpp.compile_db}`, `${mcpp.self}` and `${mcpp.stage_dir}` were replaced with the path's native spelling, while `${mcpp.target_file:}` and `${mcpp.artifact:}` were already generic. A build program that wrote `${mcpp.out_dir}/gen/x.cppm` therefore got `C:\proj\target\...\gen/x.cppm` on Windows: one argv token carrying two separator conventions. The five sites use `generic_string()`, the spelling `escape_ninja_path` already gives every ninja node name and the tool paths (the compiler, `ar`, `mcpp` itself). POSIX output is byte-identical, so this changes no existing edge west of Windows. e2e 660, which runs on the Windows shard the `${mcpp.self}` token exists for, now asserts the action rule's argv carries no backslash. docs/30 and docs/zh/30 state the rule beside the vocabulary table. Closes #393. --- CHANGELOG.md | 3 ++ docs/30-build-mcpp.md | 8 +++++ docs/zh/30-build-mcpp.md | 6 ++++ src/build/prepare/plan.cpp | 20 +++++++++--- ...tion_names_the_engine_through_mcpp_self.sh | 31 +++++++++++++++++++ 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd156950..764840ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,9 @@ `[workspace.xlings.overrides]`. - Report a `build.mcpp` beside a workspace root without `[package]`, and the schema warnings of a workspace root when its members are built. +- Spell every `${mcpp.*}` path token with forward slashes on every platform, so + the `${mcpp.out_dir}/gen/x.cppm` a build program writes is one separator on + Windows too instead of a native prefix beside the author's `/` (#393). ### Deprecated diff --git a/docs/30-build-mcpp.md b/docs/30-build-mcpp.md index 06ac6a7b..6dd89988 100644 --- a/docs/30-build-mcpp.md +++ b/docs/30-build-mcpp.md @@ -1086,6 +1086,14 @@ none to rely on), and the only interpolations are a closed set: | `${mcpp.stage_dir}` *(2026.9.11.1+)* | the tree `mcpp pack` staged, absolute. `artifact` role only, and only under `mcpp pack --format ` | | `${mcpp.self}` *(2026.9.13.1+)* | the engine's own executable, absolute. An action's command is an argv with no shell, so a build program has no portable way to copy a file; the engine is present wherever a build runs, and `${mcpp.self} stage --verify content --output ` copies one file, creates the destination's parent, and writes only when the bytes differ. That argument shape is a contract from 2026.9.13.1 on; a build program that names it declares that release as its floor | +Every path a `${mcpp.*}` token expands to is spelled with **forward slashes**. +A token is spliced into the text the build program wrote, and that text spells +its own separators as `/`; a native spelling on Windows would put a `C:\…` +prefix beside that `/` inside one argv token. This is the spelling `mcpp` gives +the compiler, the archiver and every ninja node name, so +`${mcpp.out_dir}/gen/x.cppm` reads as one separator on Windows as well as on +POSIX. + The raw stdout protocol above remains the low-level substrate; `import mcpp;` is the typed layer over it. diff --git a/docs/zh/30-build-mcpp.md b/docs/zh/30-build-mcpp.md index e4582616..0e580bab 100644 --- a/docs/zh/30-build-mcpp.md +++ b/docs/zh/30-build-mcpp.md @@ -922,6 +922,12 @@ mcpp 会写出 `<暂存树>.stage-manifest` —— 一个兄弟文件,永不 | `${mcpp.stage_dir}` *(2026.9.11.1+)* | `mcpp pack` 暂存出的那棵树,绝对路径。仅 `artifact` role 可用,且仅在 `mcpp pack --format ` 下可用 | | `${mcpp.self}` *(2026.9.13.1+)* | 引擎自己的可执行文件,绝对路径。action 的命令是没有 shell 的 argv,构建程序因此没有可移植的拷贝手段;引擎在构建运行的每台机器上都在,`${mcpp.self} stage --verify content --output ` 拷贝一个文件、创建目标的父目录、只在字节不同时写入。这个参数形状自 2026.9.13.1 起是契约;写下它的构建程序即以该版本为下限 | +`${mcpp.*}` 展开出的每个路径都用**正斜杠**拼写。token 会被拼进构建程序写下的 +文本,而那段文本用自己的分隔符 `/`;在 Windows 上按原生拼写就会把 `C:\…` 前缀 +和那个 `/` 放进同一个 argv token 里。这与 mcpp 给编译器、归档器和每个 ninja +节点名的一贯拼写相同,因此 `${mcpp.out_dir}/gen/x.cppm` 在 Windows 上和在 POSIX +上读起来是同一种分隔符。 + 上面的裸 stdout 协议仍是底层基底;`import mcpp;` 是其上的类型化层。 ### 读取解析后的依赖图:`graph_file`(2026.9.16.1+) diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index 012bdc81..90cfcf07 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -981,9 +981,19 @@ static std::expected step13_build_graph_actions(PrepareState& for (std::size_t p; (p = s.find(what)) != std::string::npos; ) s.replace(p, what.size(), with); }; - rep("${mcpp.out_dir}", ctx.plan.outputDir.string()); - rep("${mcpp.bin_dir}", binDir.string()); - rep("${mcpp.compile_db}", ctx.plan.compileDbPath.string()); + // EVERY `${mcpp.*}` PATH TOKEN IS SPELLED GENERICALLY -- `\`-free on + // Windows too (mcpp#393). A token is spliced into text the build + // program wrote, and that text writes its own separators as `/` + // (`${mcpp.out_dir}/gen/x.cppm`); a native `C:\…` prefix beside + // that `/` is a mixed spelling, and the command line carries it to + // the tool as one string. Generic is also the spelling the engines + // around us already use: `escape_ninja_path` spells every ninja node + // name and every tool path this way (the compiler, `ar`, `mcpp` + // itself), and `${mcpp.target_file:}` below always has. POSIX output + // is byte-identical. + rep("${mcpp.out_dir}", ctx.plan.outputDir.generic_string()); + rep("${mcpp.bin_dir}", binDir.generic_string()); + rep("${mcpp.compile_db}", ctx.plan.compileDbPath.generic_string()); // The engine's own executable, absolute (2026.9.13.1+). An action // whose command is an argv with no shell has no portable way to // copy, touch or compare a file, and the engine is the one @@ -995,7 +1005,7 @@ static std::expected step13_build_graph_actions(PrepareState& // wrapper's: a version change regenerates build.ninja, and a // binary moved under an unchanged version leaves a stale path, // exactly as it would for the compiler. - rep("${mcpp.self}", mcpp::platform::fs::self_exe_path().string()); + rep("${mcpp.self}", mcpp::platform::fs::self_exe_path().generic_string()); // ABSOLUTE, unlike `${mcpp.target_file:}` and for the same reason // stated the other way round: the staged tree lives outside the // build directory and no ninja edge produces it, so there is no @@ -1011,7 +1021,7 @@ static std::expected step13_build_graph_actions(PrepareState& } else { thisActionUsesStageDir = true; } - rep("${mcpp.stage_dir}", stage ? stage->dir.string() : std::string{}); + rep("${mcpp.stage_dir}", stage ? stage->dir.generic_string() : std::string{}); } constexpr std::string_view kTf = "${mcpp.target_file:"; for (std::size_t p; (p = s.find(kTf)) != std::string::npos; ) { diff --git a/tests/e2e/660_an_action_names_the_engine_through_mcpp_self.sh b/tests/e2e/660_an_action_names_the_engine_through_mcpp_self.sh index 85d1d169..555f0c3a 100755 --- a/tests/e2e/660_an_action_names_the_engine_through_mcpp_self.sh +++ b/tests/e2e/660_an_action_names_the_engine_through_mcpp_self.sh @@ -71,6 +71,37 @@ if grep -q 'mcpp\.self' "$NINJA"; then echo "FAIL: \${mcpp.self} reached build.ninja unsubstituted"; exit 1 fi +# ── every `${mcpp.*}` path token expands to the GENERIC spelling ─────────── +# +# A token is spliced into text the build program wrote, and that text writes +# its own separators as `/` (`${mcpp.out_dir}/copied/deeper/app-copy`); a +# native `C:\…` prefix beside that `/` is a mixed spelling, and the command +# line reaches the tool as one string (mcpp#393). Every token this action names +# -- `${mcpp.self}`, `${mcpp.out_dir}` and `${mcpp.target_file:app}` -- is an +# engine path, so on Windows a backslash left in the argv IS the regression. +# On POSIX the two spellings are byte-identical, so this leg only bites on the +# shard the test exists for; the `# requires:`-free note at the top is why it +# is here and not in a unit test. +# +# The ARGV is read out of the rule's `command =` line, past the wrapper the +# engine adds when a check/stamp or an env/cwd is in play (`... -- `); a +# bare action has no wrapper, and then the whole line is the argv. The EDGE +# line cannot show the bug: `escape_ninja_path` generic-izes every node name +# whatever the token spelled. +CMD_LINES=$( + awk '/^rule mcpp_action_/{seen=1; next} + seen && /^ command = /{sub(/^ command = /, ""); print; seen=0}' "$NINJA" +) +[ -n "$CMD_LINES" ] || { echo "FAIL: no mcpp_action_ rule in build.ninja"; exit 1; } +while IFS= read -r line; do + argv="${line#* -- }" + case "$argv" in + *\\*) printf 'FAIL: a ${mcpp.*} path token expanded to a native spelling:\n %s\n' "$argv" + exit 1;; + esac +done <<< "$CMD_LINES" +echo "ok: \${mcpp.*} path tokens are spelled with forward slashes" + # The copy exists, in a directory the action never created itself, and is the # program byte for byte: the only way it got there is the engine's `stage`. COPY=$(find target -path '*/copied/deeper/app-copy' -print -quit)