Skip to content

Commit 3fb2d0c

Browse files
authored
fix: spell every ${mcpp.*} path token generically (#393) (#801)
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.
1 parent dd3ab3a commit 3fb2d0c

5 files changed

Lines changed: 63 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@
8181
`[workspace.xlings.overrides]`.
8282
- Report a `build.mcpp` beside a workspace root without `[package]`, and the
8383
schema warnings of a workspace root when its members are built.
84+
- Spell every `${mcpp.*}` path token with forward slashes on every platform, so
85+
the `${mcpp.out_dir}/gen/x.cppm` a build program writes is one separator on
86+
Windows too instead of a native prefix beside the author's `/` (#393).
8487

8588
### Deprecated
8689

‎docs/30-build-mcpp.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,14 @@ none to rely on), and the only interpolations are a closed set:
10861086
| `${mcpp.stage_dir}` *(2026.9.11.1+)* | the tree `mcpp pack` staged, absolute. `artifact` role only, and only under `mcpp pack --format <name>` |
10871087
| `${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 <dst> <src>` 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 |
10881088

1089+
Every path a `${mcpp.*}` token expands to is spelled with **forward slashes**.
1090+
A token is spliced into the text the build program wrote, and that text spells
1091+
its own separators as `/`; a native spelling on Windows would put a `C:\…`
1092+
prefix beside that `/` inside one argv token. This is the spelling `mcpp` gives
1093+
the compiler, the archiver and every ninja node name, so
1094+
`${mcpp.out_dir}/gen/x.cppm` reads as one separator on Windows as well as on
1095+
POSIX.
1096+
10891097
The raw stdout protocol above remains the low-level substrate; `import mcpp;`
10901098
is the typed layer over it.
10911099

‎docs/zh/30-build-mcpp.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,12 @@ mcpp 会写出 `<暂存树>.stage-manifest` —— 一个兄弟文件,永不
922922
| `${mcpp.stage_dir}` *(2026.9.11.1+)* | `mcpp pack` 暂存出的那棵树,绝对路径。仅 `artifact` role 可用,且仅在 `mcpp pack --format <name>` 下可用 |
923923
| `${mcpp.self}` *(2026.9.13.1+)* | 引擎自己的可执行文件,绝对路径。action 的命令是没有 shell 的 argv,构建程序因此没有可移植的拷贝手段;引擎在构建运行的每台机器上都在,`${mcpp.self} stage --verify content --output <dst> <src>` 拷贝一个文件、创建目标的父目录、只在字节不同时写入。这个参数形状自 2026.9.13.1 起是契约;写下它的构建程序即以该版本为下限 |
924924

925+
`${mcpp.*}` 展开出的每个路径都用**正斜杠**拼写。token 会被拼进构建程序写下的
926+
文本,而那段文本用自己的分隔符 `/`;在 Windows 上按原生拼写就会把 `C:\…` 前缀
927+
和那个 `/` 放进同一个 argv token 里。这与 mcpp 给编译器、归档器和每个 ninja
928+
节点名的一贯拼写相同,因此 `${mcpp.out_dir}/gen/x.cppm` 在 Windows 上和在 POSIX
929+
上读起来是同一种分隔符。
930+
925931
上面的裸 stdout 协议仍是底层基底;`import mcpp;` 是其上的类型化层。
926932

927933
### 读取解析后的依赖图:`graph_file`(2026.9.16.1+)

‎src/build/prepare/plan.cpp‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -981,9 +981,19 @@ static std::expected<void, std::string> step13_build_graph_actions(PrepareState&
981981
for (std::size_t p; (p = s.find(what)) != std::string::npos; )
982982
s.replace(p, what.size(), with);
983983
};
984-
rep("${mcpp.out_dir}", ctx.plan.outputDir.string());
985-
rep("${mcpp.bin_dir}", binDir.string());
986-
rep("${mcpp.compile_db}", ctx.plan.compileDbPath.string());
984+
// EVERY `${mcpp.*}` PATH TOKEN IS SPELLED GENERICALLY -- `\`-free on
985+
// Windows too (mcpp#393). A token is spliced into text the build
986+
// program wrote, and that text writes its own separators as `/`
987+
// (`${mcpp.out_dir}/gen/x.cppm`); a native `C:\…` prefix beside
988+
// that `/` is a mixed spelling, and the command line carries it to
989+
// the tool as one string. Generic is also the spelling the engines
990+
// around us already use: `escape_ninja_path` spells every ninja node
991+
// name and every tool path this way (the compiler, `ar`, `mcpp`
992+
// itself), and `${mcpp.target_file:}` below always has. POSIX output
993+
// is byte-identical.
994+
rep("${mcpp.out_dir}", ctx.plan.outputDir.generic_string());
995+
rep("${mcpp.bin_dir}", binDir.generic_string());
996+
rep("${mcpp.compile_db}", ctx.plan.compileDbPath.generic_string());
987997
// The engine's own executable, absolute (2026.9.13.1+). An action
988998
// whose command is an argv with no shell has no portable way to
989999
// copy, touch or compare a file, and the engine is the one
@@ -995,7 +1005,7 @@ static std::expected<void, std::string> step13_build_graph_actions(PrepareState&
9951005
// wrapper's: a version change regenerates build.ninja, and a
9961006
// binary moved under an unchanged version leaves a stale path,
9971007
// exactly as it would for the compiler.
998-
rep("${mcpp.self}", mcpp::platform::fs::self_exe_path().string());
1008+
rep("${mcpp.self}", mcpp::platform::fs::self_exe_path().generic_string());
9991009
// ABSOLUTE, unlike `${mcpp.target_file:}` and for the same reason
10001010
// stated the other way round: the staged tree lives outside the
10011011
// build directory and no ninja edge produces it, so there is no
@@ -1011,7 +1021,7 @@ static std::expected<void, std::string> step13_build_graph_actions(PrepareState&
10111021
} else {
10121022
thisActionUsesStageDir = true;
10131023
}
1014-
rep("${mcpp.stage_dir}", stage ? stage->dir.string() : std::string{});
1024+
rep("${mcpp.stage_dir}", stage ? stage->dir.generic_string() : std::string{});
10151025
}
10161026
constexpr std::string_view kTf = "${mcpp.target_file:";
10171027
for (std::size_t p; (p = s.find(kTf)) != std::string::npos; ) {

‎tests/e2e/660_an_action_names_the_engine_through_mcpp_self.sh‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,37 @@ if grep -q 'mcpp\.self' "$NINJA"; then
7171
echo "FAIL: \${mcpp.self} reached build.ninja unsubstituted"; exit 1
7272
fi
7373

74+
# ── every `${mcpp.*}` path token expands to the GENERIC spelling ───────────
75+
#
76+
# A token is spliced into text the build program wrote, and that text writes
77+
# its own separators as `/` (`${mcpp.out_dir}/copied/deeper/app-copy`); a
78+
# native `C:\…` prefix beside that `/` is a mixed spelling, and the command
79+
# line reaches the tool as one string (mcpp#393). Every token this action names
80+
# -- `${mcpp.self}`, `${mcpp.out_dir}` and `${mcpp.target_file:app}` -- is an
81+
# engine path, so on Windows a backslash left in the argv IS the regression.
82+
# On POSIX the two spellings are byte-identical, so this leg only bites on the
83+
# shard the test exists for; the `# requires:`-free note at the top is why it
84+
# is here and not in a unit test.
85+
#
86+
# The ARGV is read out of the rule's `command =` line, past the wrapper the
87+
# engine adds when a check/stamp or an env/cwd is in play (`... -- <argv>`); a
88+
# bare action has no wrapper, and then the whole line is the argv. The EDGE
89+
# line cannot show the bug: `escape_ninja_path` generic-izes every node name
90+
# whatever the token spelled.
91+
CMD_LINES=$(
92+
awk '/^rule mcpp_action_/{seen=1; next}
93+
seen && /^ command = /{sub(/^ command = /, ""); print; seen=0}' "$NINJA"
94+
)
95+
[ -n "$CMD_LINES" ] || { echo "FAIL: no mcpp_action_ rule in build.ninja"; exit 1; }
96+
while IFS= read -r line; do
97+
argv="${line#* -- }"
98+
case "$argv" in
99+
*\\*) printf 'FAIL: a ${mcpp.*} path token expanded to a native spelling:\n %s\n' "$argv"
100+
exit 1;;
101+
esac
102+
done <<< "$CMD_LINES"
103+
echo "ok: \${mcpp.*} path tokens are spelled with forward slashes"
104+
74105
# The copy exists, in a directory the action never created itself, and is the
75106
# program byte for byte: the only way it got there is the engine's `stage`.
76107
COPY=$(find target -path '*/copied/deeper/app-copy' -print -quit)

0 commit comments

Comments
 (0)