Skip to content

Commit d5b2aed

Browse files
committed
fix: remove module-boundary shapes that crash clang on the MSVC ABI; correct the hook environment
Windows CI (clang 20.1.7, x86_64-pc-windows-msvc) crashed compiling directives.cppm. The change introduced constructs of the kind src/pack/pack.cppm records crashing that toolchain: pointers to members of an imported type and a std::span over an exported constexpr array, plus optional-bearing types in an exported interface. They are replaced by plain code: `windows_choice_problem` states the accepted values once for both parsers, the directive check and apply are per field, `invocation_env` returns a plain struct, ScopedInvocationEnv keeps plain members, and the hook scope uses six ScopedEnv locals. tests/e2e/648 measured that a dependency's install hook runs before the toolchain is resolved, because prepare resolves it after the dependency graph. The hook therefore receives the build's target and empty toolchain names; the code comment, docs/22, docs/32, the CHANGELOG and the design record now state this, and 648 asserts it, including that inherited values do not leak through. Test fixes: 645 resolves its helper directory before changing directory and drives `mcpp run` through MCPP_TOOLCHAIN; 647 writes each manifest to its own package directory.
1 parent d515306 commit d5b2aed

15 files changed

Lines changed: 255 additions & 186 deletions

.agents/docs/2026-09-12-engine-gaps-after-the-sdk-batch.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,19 @@ code, and what was built instead.
546546
rule: the requested triple, or the host triple for a native build. The six
547547
values are computed by `install_hook_env`, from which the build-program
548548
environment also takes them, in their existing order, so no build program's
549-
re-run key changes.
549+
re-run key changes. The toolchain values are empty while a dependency
550+
installs, which §2.3 assumed otherwise: `tc` is resolved after the dependency
551+
graph, because a package in the graph may supply a target-side layer, so no
552+
compiler or standard library has been decided when a dependency's hook runs.
553+
Measured with tests/e2e/648, whose hook compiled in an empty `compiler=` and
554+
`stdlib=` beside the host's `os=linux`. Resolving the toolchain before
555+
installation is the reorder the engine deliberately does not make, and a
556+
guessed value would let a hook build the wrong variant, so the variables are
557+
emitted empty, which also keeps a value inherited from a parent process out
558+
of the hook. Neither §2.4 criterion holds as written: the refusal follows the
559+
hook (item 3), and a hook cannot print a resolved `stdlibId`. What holds is
560+
that the `c++-abi` refusal names both implementations before compilation, and
561+
that the hook sees the build's target and never an inherited toolchain value.
550562
5. **§3.1, absent and empty on Windows.** The CRT defines `_putenv_s(key, "")` as
551563
removal, so the Windows branch already produced an absent
552564
`XLINGS_PROJECT_DIR`, and the two platforms did not disagree about global

CHANGELOG.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,12 @@ Emscripten 链接产出的是首行为 `#!/usr/bin/env node` 的 JavaScript 启
8282
拒绝信息指出包与 feature,并给出满足它的表。依赖自己写的 `[target.<selector>.abi]` 被报告
8383
(`abi/dependency-table`)且不生效。
8484

85-
### 安装钩子收到已解析的工具链(#613)
85+
### 安装钩子的环境与 `c++-abi` 的做法(#613)
8686

87-
- 依赖包的安装钩子收到 `MCPP_COMPILER``MCPP_CXX_STDLIB``MCPP_TARGET``MCPP_TARGET_OS`
88-
`MCPP_TARGET_ARCH``MCPP_TARGET_ENV`,名称与取值同构建程序一致,由同一个函数计算。钩子可以据此拒绝
89-
或诊断,但不得把某种变体构建进名称未体现该变体的存储目录。
87+
- 依赖包的安装钩子收到 `MCPP_TARGET``MCPP_TARGET_OS``MCPP_TARGET_ARCH``MCPP_TARGET_ENV`,名称
88+
与规则同构建程序一致,由同一个函数计算。`MCPP_COMPILER``MCPP_CXX_STDLIB` 同样总是写出,但在依赖
89+
安装时为空:工具链在依赖图之后才解析,此时没有可陈述的编译器与标准库。每个变量都显式写出,钩子不会读到
90+
从父进程继承的值;钩子不得把某种变体构建进名称未体现该变体的存储目录。
9091
- 从源码构建静态库的包以 `requires = ["mcpp:c++-abi=<stdlib>"]` 声明它所针对的标准库;工具链解析出另一
9192
实现的工程被拒绝,拒绝信息指出两个实现。docs/06、docs/22 与 docs/32 记录这一做法。
9293

docs/22-target-side.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,12 @@ requires = ["mcpp:c++-abi=libstdc++"]
238238
```
239239

240240
A project whose toolchain resolves another `c++-abi` is then refused, naming
241-
both implementations, instead of failing at the link. The check follows the
242-
installation of the graph, so the install hook also receives the resolved values
243-
as `MCPP_CXX_STDLIB` and `MCPP_COMPILER` ([32 — Authoring a
244-
Payload](32-authoring-a-payload.md)) and can refuse before it compiles anything.
245-
It must not build a different variant into the same store directory, because
246-
the first consumer would then decide the variant for every later one.
241+
both implementations, instead of failing at the link. The check runs once the
242+
toolchain is resolved, which is after the dependency graph is installed, so the
243+
install hook has already run; the hook receives the build's target but no
244+
toolchain values ([32 — Authoring a Payload](32-authoring-a-payload.md)). It
245+
must not build a different variant into the same store directory, because the
246+
first consumer would then decide the variant for every later one.
247247

248248
### Standard Library Module Sources
249249

docs/32-authoring-a-payload.md

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,27 +72,30 @@ those two doing more.
7272
### The environment an install hook receives (mcpp 2026.9.12.2+)
7373

7474
When mcpp installs a package a project depends on, the package's `install()`
75-
runs with the resolved toolchain and target in its environment, under the names
76-
and with the values a build program receives ([build.mcpp](30-build-mcpp.md)):
75+
runs with the build's target in its environment, under the names and by the rule
76+
a build program uses ([build.mcpp](30-build-mcpp.md)): every variable is present,
77+
and empty when it has no value, so a hook never reads a value inherited from the
78+
process that started mcpp.
7779

78-
| Variable | Value |
80+
| Variable | Value while a dependency installs |
7981
|---|---|
80-
| `MCPP_COMPILER` | `gcc`, `clang` or `msvc` |
81-
| `MCPP_CXX_STDLIB` | `libstdc++`, `libc++` or `msvc-stl` |
8282
| `MCPP_TARGET` | the target triple the build was asked for, or the host triple for a native build |
8383
| `MCPP_TARGET_OS`, `MCPP_TARGET_ARCH`, `MCPP_TARGET_ENV` | the segments of that triple |
84+
| `MCPP_COMPILER`, `MCPP_CXX_STDLIB` | empty |
8485

85-
A value is empty when it does not apply. On Windows an empty variable is an
86-
absent one, and `os.getenv` answers `nil` for it. The hook of a toolchain payload
87-
receives none of these variables, because no toolchain has been resolved while it
88-
installs.
86+
The toolchain values are empty because the toolchain is resolved after the
87+
dependency graph: a package in the graph may supply a target-side layer, so no
88+
compiler or standard library has been decided when a dependency installs, and a
89+
hook that guessed one could build the wrong variant. On Windows an empty variable
90+
is an absent one, and `os.getenv` answers `nil` for it. The hook of a toolchain
91+
payload receives none of these variables.
8992

90-
A hook may use the values to refuse or to diagnose. It must not build a variant
93+
A hook may use the target to refuse or to diagnose. It must not build a variant
9194
into a store directory whose name does not state the variant: the store is keyed
9295
by package and version, so the first consumer would decide the variant for every
93-
later one. A package compiled against one C++ standard library declares that
94-
instead, with `requires = ["mcpp:c++-abi=libstdc++"]`
95-
([22 — The Target Side](22-target-side.md)).
96+
later one. A package compiled against one C++ standard library states that with
97+
`requires = ["mcpp:c++-abi=libstdc++"]`, which is checked once the toolchain is
98+
resolved ([22 — The Target Side](22-target-side.md)).
9699

97100
## The four things a descriptor must get right
98101

docs/zh/22-target-side.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,8 @@ requires = ["mcpp:c++-abi=libstdc++"]
202202
```
203203

204204
工具链解析出另一个 `c++-abi` 的工程随后会被拒绝,拒绝信息同时指出两个实现,而不是在链接时失败。这项检查
205-
在依赖图安装之后进行,因此安装钩子还会通过 `MCPP_CXX_STDLIB``MCPP_COMPILER` 收到解析结果
206-
([32 —— 编写载荷](32-authoring-a-payload.md)),可以在编译任何东西之前拒绝。钩子不得把另一种变体构建进
205+
在工具链解析之后进行,而工具链在依赖图安装之后才解析,因此检查时安装钩子已经运行过;钩子收到本次构建的
206+
目标,但收不到工具链的取值([32 —— 编写载荷](32-authoring-a-payload.md))。钩子不得把另一种变体构建进
207207
同一个存储目录,否则第一个消费者就会替之后所有消费者决定变体。
208208

209209
### 标准库模块源

docs/zh/32-authoring-a-payload.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,24 @@ end
6666

6767
### 安装钩子收到的环境(mcpp 2026.9.12.2+)
6868

69-
mcpp 安装工程所依赖的包时,该包的 `install()` 在环境中收到已解析的工具链与目标,变量名与取值同构建程序
70-
收到的一致([build.mcpp](30-build-mcpp.md)):
69+
mcpp 安装工程所依赖的包时,该包的 `install()` 在环境中收到本次构建的目标,变量名与规则同构建程序一致
70+
([build.mcpp](30-build-mcpp.md)):每个变量都显式写出,没有取值时为空,因此钩子不会读到从启动 mcpp 的
71+
进程继承来的值。
7172

72-
| 变量 | 取值 |
73+
| 变量 | 依赖安装时的取值 |
7374
|---|---|
74-
| `MCPP_COMPILER` | `gcc``clang``msvc` |
75-
| `MCPP_CXX_STDLIB` | `libstdc++``libc++``msvc-stl` |
7675
| `MCPP_TARGET` | 本次构建所请求的目标三元组;原生构建时为宿主三元组 |
7776
| `MCPP_TARGET_OS``MCPP_TARGET_ARCH``MCPP_TARGET_ENV` | 该三元组的各段 |
77+
| `MCPP_COMPILER``MCPP_CXX_STDLIB` ||
7878

79-
不适用的取值为空。在 Windows 上空变量即不存在的变量,`os.getenv` 对它返回 `nil`。工具链载荷的钩子不会
80-
收到这些变量,因为它安装时尚未解析出任何工具链。
79+
工具链的取值为空,因为工具链在依赖图之后才解析:依赖图中的包可能提供目标侧的层,所以依赖安装时编译器与
80+
标准库都尚未确定,而猜测一个取值的钩子可能构建出错误的变体。在 Windows 上空变量即不存在的变量,
81+
`os.getenv` 对它返回 `nil`。工具链载荷的钩子不会收到这些变量。
8182

82-
钩子可以用这些值来拒绝或给出诊断,但不得把某种变体构建进名称未体现该变体的存储目录:存储目录按包名与版本
83-
区分,否则第一个消费者就会替之后所有消费者决定变体。针对某一个 C++ 标准库编译的包应改为声明这一点,即
84-
`requires = ["mcpp:c++-abi=libstdc++"]`([22 —— 目标侧](22-target-side.md))。
83+
钩子可以用目标来拒绝或给出诊断,但不得把某种变体构建进名称未体现该变体的存储目录:存储目录按包名与版本
84+
区分,否则第一个消费者就会替之后所有消费者决定变体。针对某一个 C++ 标准库编译的包应以
85+
`requires = ["mcpp:c++-abi=libstdc++"]` 陈述这一点,该需求在工具链解析之后检查
86+
([22 —— 目标侧](22-target-side.md))。
8587

8688
## 描述符必须做对的四件事
8789

modules/buildmcpp/src/directives.cppm

Lines changed: 74 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -879,17 +879,21 @@ void apply(mcpp::manifest::Manifest& m, const Directives& d) {
879879
// A named executable's subsystem and entry. `target_directive_error` has
880880
// refused every value that names no executable, so the conditions below
881881
// only keep this function total.
882-
for (auto [slot, member] : {
883-
std::pair{Slot::WindowsSubsystem, &mcpp::manifest::Target::windowsSubsystem},
884-
std::pair{Slot::WindowsEntry, &mcpp::manifest::Target::windowsEntry}}) {
885-
for (auto const& entry : d.at(slot)) {
886-
auto sep = entry.rfind(':');
887-
if (sep == std::string::npos) continue;
888-
const auto name = entry.substr(0, sep);
889-
for (auto& t : m.targets)
890-
if (t.name == name && t.kind == mcpp::manifest::Target::Binary)
891-
t.*member = entry.substr(sep + 1);
892-
}
882+
for (auto const& entry : d.at(Slot::WindowsSubsystem)) {
883+
const auto sep = entry.rfind(':');
884+
if (sep == std::string::npos) continue;
885+
const auto name = entry.substr(0, sep);
886+
for (auto& t : m.targets)
887+
if (t.name == name && t.kind == mcpp::manifest::Target::Binary)
888+
t.windowsSubsystem = entry.substr(sep + 1);
889+
}
890+
for (auto const& entry : d.at(Slot::WindowsEntry)) {
891+
const auto sep = entry.rfind(':');
892+
if (sep == std::string::npos) continue;
893+
const auto name = entry.substr(0, sep);
894+
for (auto& t : m.targets)
895+
if (t.name == name && t.kind == mcpp::manifest::Target::Binary)
896+
t.windowsEntry = entry.substr(sep + 1);
893897
}
894898

895899
// Build-graph nodes. Decoded here rather than at parse time so the cache
@@ -931,72 +935,71 @@ std::optional<mcpp::manifest::BuildAction> decode_action(std::string_view payloa
931935
}
932936
}
933937

934-
std::string target_directive_error(const mcpp::manifest::Manifest& m, const Directives& d) {
935-
struct Field {
936-
Slot slot;
937-
std::string_view wire;
938-
std::string_view key;
939-
std::span<const std::string_view> accepted;
940-
std::string mcpp::manifest::Target::* member;
941-
};
942-
const Field fields[] = {
943-
{Slot::WindowsSubsystem, "windows-subsystem", "windows_subsystem",
944-
mcpp::manifest::kWindowsSubsystems, &mcpp::manifest::Target::windowsSubsystem},
945-
{Slot::WindowsEntry, "windows-entry", "windows_entry",
946-
mcpp::manifest::kWindowsEntries, &mcpp::manifest::Target::windowsEntry},
947-
};
948-
for (auto const& f : fields) {
949-
std::map<std::string, std::string> stated; // target name -> value
950-
for (auto const& entry : d.at(f.slot)) {
951-
auto sep = entry.rfind(':');
952-
if (sep == std::string::npos || sep == 0 || sep + 1 == entry.size())
953-
return std::format(
954-
"build.mcpp emitted `mcpp:{}={}`, which is not `<target>:<value>`.",
955-
f.wire, entry);
956-
const std::string name = entry.substr(0, sep);
957-
const std::string value = entry.substr(sep + 1);
958-
if (std::ranges::find(f.accepted, std::string_view(value)) == f.accepted.end()) {
959-
std::string list;
960-
for (auto a : f.accepted)
961-
list += (list.empty() ? "" : ", ") + std::format("\"{}\"", a);
962-
return std::format(
963-
"build.mcpp emitted `mcpp:{}={}`, and \"{}\" is not one of {}.",
964-
f.wire, entry, value, list);
965-
}
966-
const mcpp::manifest::Target* target = nullptr;
938+
// One of the two named-target directives; `subsystem` selects the field.
939+
static std::string named_target_error(const mcpp::manifest::Manifest& m,
940+
const std::vector<std::string>& entries,
941+
std::string_view wire, std::string_view key,
942+
bool subsystem) {
943+
std::map<std::string, std::string> stated; // target name -> value
944+
for (auto const& entry : entries) {
945+
const auto sep = entry.rfind(':');
946+
if (sep == std::string::npos || sep == 0 || sep + 1 == entry.size())
947+
return std::format(
948+
"build.mcpp emitted `mcpp:{}={}`, which is not `<target>:<value>`.",
949+
wire, entry);
950+
const std::string name = entry.substr(0, sep);
951+
const std::string value = entry.substr(sep + 1);
952+
if (auto list = mcpp::manifest::windows_choice_problem(subsystem, value);
953+
!list.empty())
954+
return std::format(
955+
"build.mcpp emitted `mcpp:{}={}`, and \"{}\" is not one of {}.",
956+
wire, entry, value, list);
957+
const mcpp::manifest::Target* target = nullptr;
958+
for (auto const& t : m.targets)
959+
if (t.name == name) { target = &t; break; }
960+
if (target == nullptr) {
961+
std::string names;
967962
for (auto const& t : m.targets)
968-
if (t.name == name) { target = &t; break; }
969-
if (!target) {
970-
std::string names;
971-
for (auto const& t : m.targets)
972-
names += (names.empty() ? "" : ", ") + t.name;
973-
return std::format(
974-
"build.mcpp emitted `mcpp:{}={}`, and package `{}` declares no "
975-
"target named `{}` (its targets: {}).",
976-
f.wire, entry, m.package.name, name, names.empty() ? "none" : names);
977-
}
978-
if (target->kind != mcpp::manifest::Target::Binary)
979-
return std::format(
980-
"build.mcpp emitted `mcpp:{}={}`, and `{}` applies to an executable "
981-
"(`kind = \"bin\"`); target `{}` is not one.",
982-
f.wire, entry, f.key, name);
983-
if (const std::string& declared = target->*f.member;
984-
!declared.empty() && declared != value)
985-
return std::format(
986-
"build.mcpp emitted `mcpp:{}={}`, and mcpp.toml declares "
987-
"`[targets.{}] {} = \"{}\"`. One of the two has to change.",
988-
f.wire, entry, name, f.key, declared);
989-
if (auto [it, fresh] = stated.try_emplace(name, value);
990-
!fresh && it->second != value)
991-
return std::format(
992-
"build.mcpp emitted `mcpp:{}` twice for target `{}`, as \"{}\" and "
993-
"as \"{}\".",
994-
f.wire, name, it->second, value);
963+
names += (names.empty() ? "" : ", ") + t.name;
964+
return std::format(
965+
"build.mcpp emitted `mcpp:{}={}`, and package `{}` declares no "
966+
"target named `{}` (its targets: {}).",
967+
wire, entry, m.package.name, name,
968+
names.empty() ? std::string("none") : names);
995969
}
970+
if (target->kind != mcpp::manifest::Target::Binary)
971+
return std::format(
972+
"build.mcpp emitted `mcpp:{}={}`, and `{}` applies to an executable "
973+
"(`kind = \"bin\"`); target `{}` is not one.",
974+
wire, entry, key, name);
975+
const std::string& declared =
976+
subsystem ? target->windowsSubsystem : target->windowsEntry;
977+
if (!declared.empty() && declared != value)
978+
return std::format(
979+
"build.mcpp emitted `mcpp:{}={}`, and mcpp.toml declares "
980+
"`[targets.{}] {} = \"{}\"`. One of the two has to change.",
981+
wire, entry, name, key, declared);
982+
auto found = stated.find(name);
983+
if (found == stated.end())
984+
stated.emplace(name, value);
985+
else if (found->second != value)
986+
return std::format(
987+
"build.mcpp emitted `mcpp:{}` twice for target `{}`, as \"{}\" and "
988+
"as \"{}\".",
989+
wire, name, found->second, value);
996990
}
997991
return {};
998992
}
999993

994+
std::string target_directive_error(const mcpp::manifest::Manifest& m, const Directives& d) {
995+
if (auto e = named_target_error(m, d.at(Slot::WindowsSubsystem),
996+
"windows-subsystem", "windows_subsystem", true);
997+
!e.empty())
998+
return e;
999+
return named_target_error(m, d.at(Slot::WindowsEntry),
1000+
"windows-entry", "windows_entry", false);
1001+
}
1002+
10001003
std::string action_error(const Directives& d) {
10011004
for (auto const& payload : d.at(Slot::Actions)) {
10021005
// The typed API sets this when an argv did not fit its fixed buffer.

0 commit comments

Comments
 (0)