Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions docs/30-build-mcpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` |
| `${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 |

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.

Expand Down
6 changes: 6 additions & 0 deletions docs/zh/30-build-mcpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,12 @@ mcpp 会写出 `<暂存树>.stage-manifest` —— 一个兄弟文件,永不
| `${mcpp.stage_dir}` *(2026.9.11.1+)* | `mcpp pack` 暂存出的那棵树,绝对路径。仅 `artifact` role 可用,且仅在 `mcpp pack --format <name>` 下可用 |
| `${mcpp.self}` *(2026.9.13.1+)* | 引擎自己的可执行文件,绝对路径。action 的命令是没有 shell 的 argv,构建程序因此没有可移植的拷贝手段;引擎在构建运行的每台机器上都在,`${mcpp.self} stage --verify content --output <dst> <src>` 拷贝一个文件、创建目标的父目录、只在字节不同时写入。这个参数形状自 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+)
Expand Down
20 changes: 15 additions & 5 deletions src/build/prepare/plan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -981,9 +981,19 @@ static std::expected<void, std::string> 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
Expand All @@ -995,7 +1005,7 @@ static std::expected<void, std::string> 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
Expand All @@ -1011,7 +1021,7 @@ static std::expected<void, std::string> 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; ) {
Expand Down
31 changes: 31 additions & 0 deletions tests/e2e/660_an_action_names_the_engine_through_mcpp_self.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`... -- <argv>`); 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)
Expand Down
Loading