Skip to content

Commit 0e6c8e6

Browse files
committed
A kernel-abi provider infers the platform boundary, and the build cache
key now covers the realised environment A package providing mcpp:kernel-abi=<impl> is by definition the boundary where the platform's own interfaces are called, so it can never want the graph's presented [c-abi] environment instead of the triple's own. Rather than require every kernel-abi implementation to write c-environment = "platform" (and every already-released one to bump a version to add it), that value is now inferred from provides alone, in both manifest parsers (mcpp.toml and the xpkg index descriptor). The explicit key still wins when present. Measured without it: openkal-windows, compiled under the POSIX substitution like everything else in its graph, got a 32-bit wchar_t from -fno-short-wchar while the Win32 calls it makes hand back genuine 16-bit UTF-16, misreading its own results. Separately: the global build cache's key (~/.mcpp/build-cache/v1, mcpp.build.cache_key) read only a package's OWN declared cflags/cxxflags, never the engine's broadcast channel the realised environment (and __openkal__, and targetSideUsage) actually travels through. Two builds realising different environments for the identical package produced the identical key. Measured: upgrading mcpp in place, with the cache directory left alone, served objects compiled under the old realised environment into an image built under the new one. fill_package_config now folds in privateBuild.cflags/cxxflags/asmflags (the post-broadcast values) alongside the declared ones, exactly as it already did for include directories.
1 parent affcb48 commit 0e6c8e6

9 files changed

Lines changed: 362 additions & 26 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,30 @@
2525
采用 Cygwin 式语义,仅在编译行把 `--target=` 换成 `x86_64-pc-cygwin`;链接行保持图解析出
2626
的三元组不变,因为两个三元组生成的机器码实测完全一致(PE、Win64 调用约定、SEH)。
2727
无法满足的请求明确拒绝,点名目标、请求与缺什么。`[package] c-environment = "platform"`
28-
让一个包(如 openkal-windows)的自身单元退出这项实现,继续按三元组自身的默认环境编译。
28+
让一个包的自身单元退出这项实现,继续按三元组自身的默认环境编译。
2929
(`src/toolchain/cenv.cppm`,单测 `test_cenv.cpp`)
30+
- **`c-environment = "platform"` 对 `mcpp:kernel-abi=<impl>` 提供者是推导出来的,不需要
31+
自己声明,经 PR 进行中 openkal-musl 尖峰实验的修订。** 这样的包定义上就是说平台自身 ABI
32+
的边界,绝不会是想要图里呈现的 `[c-abi]` 环境的那个包——openkal-windows 在 POSIX 替换下
33+
编译,`-fno-short-wchar` 给了它 32 位 `wchar_t`,而它调的 Win32 接口回传真正的 16 位
34+
UTF-16,`wchar_t*` 循环于是把两个 UTF-16 码元读成一个码点,这是实测出来的失败,不是假设。
35+
推导让 openkal-windows 0.8.0、openkal-macos 0.10.0、openkal-linux 0.13.0 等每一个已发布
36+
实现都不需要新发版本、不需要跨仓库协调版本号就能把边界做对,这一类失败因此**无法被表达**。
37+
包自身清单里显式写的 `c-environment` 仍然优先于推导——该键仍是设计 §5.3 另一类情形(不是
38+
kernel-abi 边界、但自身确有平台绑定单元的普通包)唯一的表达手段。`mcpp.manifest.xpkg` 与
39+
`mcpp.toml` 两条清单解析路径都实现了同一条推导。(`modules/manifest/src/{toml,xpkg}.cppm`,
40+
单测 `test_manifest.cpp` 的 `CEnvironmentIsInferredForAKernelAbiProvider`(两条解析路径各一
41+
个),e2e `tests/e2e/741_...sh`)
42+
- **全局构建缓存(`~/.mcpp/build-cache/v1`)的键补上了解析出的环境,经 openkal-musl
43+
尖峰实验发现并修订。** `mcpp.build.cache_key::fill_package_config` 原先只读包自己清单里
44+
声明的 `cflags`/`cxxflags`(`manifest.buildConfig`),而解析出的 [c-abi] 环境是引擎的
45+
广播,只写入 `PackageRoot::privateBuild`,从不写回前者——两次解析出不同环境的构建因此
46+
拿到同一把键,原地升级 `mcpp` 时缓存目录若未清理,会把按旧环境编译的目标文件喂给按新
47+
环境构建的镜像,一个镜像混两种 C 环境且没有任何诊断,这正是本设计要防止的那个不变量本身。
48+
现在 `fill_package_config` 把 `privateBuild.cflags`/`cxxflags`/新增的 `asmflags`(广播后的
49+
值)与包自身声明的标志一起折进键里,和它原本处理 include 目录的方式一致。
50+
(`src/build/cache_key.cppm`,单测 `test_cache_key.cpp` 的
51+
`TwoDifferentRealisedCEnvironmentsDoNotShareASlot`)
3052
- **`__CYGWIN__`/`__CYGWIN32__` 保持定义,经 openkal-musl 尖峰实验修订。** 最初的实现
3153
取消定义它们(理由是图里没有真正的 Cygwin 用户态)。第三方可移植代码里需要知道**目标文件
3254
格式**——不是 C 环境,也不是平台 API——的那部分,没有别的名字能指代「PE 格式 + 呈现

‎docs/22-target-side.md‎

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -388,17 +388,54 @@ by deleting the only macro that names it. This is a **trade-off for the
388388
`cygwin_conv_path`) that does not exist here, and if defining it produces
389389
more new failures than it fixes, the answer flips.
390390

391-
**A package's own units can opt out.** A package that provides
392-
`mcpp:kernel-abi=openkal` (openkal-windows, say) has to see the platform's
393-
own environment — it includes platform declarations and `_WIN32` must be
394-
true for it. Such a package, or a platform shim, states:
391+
**A `kernel-abi` provider's own units are INFERRED onto the platform boundary
392+
— it never has to say so (mcpp 2026.9.18+, a mid-PR revision from the
393+
openkal-musl spike).** A package that provides `mcpp:kernel-abi=<impl>`
394+
(openkal-windows, say) has to see the platform's own environment — it
395+
includes platform declarations and `_WIN32` must be true for it — and it
396+
always will, by definition: such a package's whole job is to speak the
397+
platform's ABI, so it can never be the package that wants the graph's
398+
*presented* `[c-abi]` environment instead of the triple's own. mcpp does not
399+
wait to be told this. Any package whose `provides` names
400+
`mcpp:kernel-abi=<impl>` gets `c-environment = "platform"` as its default,
401+
with no key of its own:
395402

396403
```toml
397404
[package]
398405
provides = ["mcpp:kernel-abi=openkal"]
399-
c-environment = "platform" # this package's own units compile in the
400-
# triple's own default environment, whatever
401-
# the graph's c-abi declares
406+
# no [package] c-environment line — the boundary is inferred from `provides`
407+
```
408+
409+
**Why inference and not just the flag.** The flag alone works; what it
410+
cannot do is retroactively fix a package that has already shipped without
411+
it. openkal-windows 0.8.0, openkal-macos 0.10.0, openkal-linux 0.13.0, and
412+
every future kernel-abi implementation, get the boundary right — with no new
413+
release and no coordinated version bump across repositories — because
414+
`provides = ["mcpp:kernel-abi=<impl>"]` is the one fact they already state.
415+
The failure this closes was measured, not hypothetical: openkal-windows,
416+
compiled under the POSIX substitution like everything else in its graph,
417+
got a 32-bit `wchar_t` from `-fno-short-wchar` while the Win32 calls it
418+
makes hand back genuine 16-bit UTF-16 — a `wchar_t*` loop then read two
419+
UTF-16 units as one code point. Making the boundary a default rather than a
420+
manifest key a package must remember turns that failure class
421+
unrepresentable rather than merely documented.
422+
423+
**Precedence: an explicit `c-environment` in the package's own manifest
424+
always wins over the inference.** The inference only fills `cEnvironment`
425+
when the package wrote nothing — a package that, after all, needs the
426+
presented environment can still say so explicitly (there is no way today to
427+
write "not platform" back, because `"platform"` remains the only value this
428+
key accepts). The explicit key also stays the ONLY mechanism for §5.3's
429+
other category — an ordinary package that is not the kernel-abi boundary but
430+
still has platform-bound units of its own — where the author really is
431+
making a choice mcpp cannot infer:
432+
433+
```toml
434+
[package]
435+
# an ORDINARY package, not a kernel-abi provider — the engine cannot infer
436+
# this one; the author states it because platform-bound units are a real
437+
# minority of what this package builds (design §5.3)
438+
c-environment = "platform"
402439
```
403440

404441
This is a boundary rule, documented rather than enforced by the engine
@@ -422,6 +459,26 @@ declares `lp64` and `llp64` compile the same source into objects whose
422459
`long` disagrees in width, so they never share an output directory, and
423460
neither can reuse a cached object the other produced.
424461

462+
**The global build cache's key also covers it (mcpp 2026.9.18+, a mid-PR
463+
fix, not the design's original text).** `~/.mcpp/build-cache/v1` — the
464+
cache an ordinary dependency compile reuses across projects and across an
465+
`mcpp` upgrade — is a SEPARATE mechanism from the build fingerprint above,
466+
keyed per package from exactly the axes that reach that package's own
467+
compile command line (`mcpp.build.cache_key`). The realised environment
468+
reaches a package's command line entirely through an engine BROADCAST (the
469+
same channel `targetSideUsage` and `-D__openkal__` use, never the package's
470+
own declared `[build] cflags`/`cxxflags`), so the key's own derivation had
471+
to be told to read the broadcast, not only the declaration — found exactly
472+
that way (coordinator report, openkal-musl spike): upgrading `mcpp` in
473+
place, with the cache directory left in place, served objects compiled
474+
under the OLD realised environment into an image built under the new one,
475+
two C environments in one image, with no diagnosis at all. `fill_package_
476+
config` now folds in `PackageRoot::privateBuild.cflags`/`cxxflags`/
477+
`asmflags` — the post-broadcast values — alongside the package's own
478+
declared flags, exactly as it already did for include directories.
479+
`--cache=off`, or clearing the cache directory, was never a sign the key
480+
was RIGHT; both routes bypass it entirely.
481+
425482
**Store key — not yet closed.** A package whose *install hook* compiles a
426483
static library from source into the shared store is keyed by package and
427484
version, not by which environment it was built against — the same gap

‎docs/zh/22-target-side.md‎

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -317,15 +317,44 @@ Windows 一行是旗舰情形:`x86_64-w64-windows-gnu` 与 `x86_64-pc-cygwin`
317317
去够一个这里并不存在的真正 Cygwin 接口(`sys/cygwin.h`、`cygwin_conv_path`)——如果定义它
318318
带来的新失败比修好的还多,结论就会翻过来。
319319

320-
**包可以让自己的单元退出。** 提供 `mcpp:kernel-abi=openkal` 的包(比如 openkal-windows)必须
321-
看到平台自身的环境——它要 include 平台声明,`_WIN32` 对它必须为真。这样的包,或者一个平台
322-
shim,声明:
320+
**`kernel-abi` 提供者的自身单元被推导落到平台边界上——它不必自己说出来**
321+
(mcpp 2026.9.18+,PR 进行中根据 openkal-musl 尖峰实验做的修订)。提供
322+
`mcpp:kernel-abi=<impl>` 的包(比如 openkal-windows)必须看到平台自身的
323+
环境——它要 include 平台声明,`_WIN32` 对它必须为真——而且永远如此,这是
324+
由定义决定的:这样的包的全部工作就是说平台自己的 ABI,所以它绝不可能是那个
325+
想要图里「呈现」的 `[c-abi]` 环境、而不是三元组自身环境的包。mcpp 不等着被
326+
告知这一点。任何 `provides` 里点名 `mcpp:kernel-abi=<impl>` 的包,默认就得到
327+
`c-environment = "platform"`,不需要自己声明:
323328

324329
```toml
325330
[package]
326331
provides = ["mcpp:kernel-abi=openkal"]
327-
c-environment = "platform" # 本包自身的单元按三元组自身的默认环境编译,
328-
# 不论图里的 c-abi 声明了什么
332+
# 没有 [package] c-environment 这一行——边界是从 provides 推导出来的
333+
```
334+
335+
**为什么要推导,而不只是提供这个开关。** 开关本身没问题;它做不到的是追溯性地
336+
修好一个已经发布、却没写这个开关的包。openkal-windows 0.8.0、openkal-macos
337+
0.10.0、openkal-linux 0.13.0,以及未来任何 kernel-abi 实现,都能因此把边界
338+
做对——不需要新发版本,也不需要跨仓库协调版本号——因为 `provides =
339+
["mcpp:kernel-abi=<impl>"]` 正是它们本就已经声明的那一个事实。这里堵住的
340+
失败是实测出来的,不是假设:openkal-windows 和图里其余部分一样,在 POSIX
341+
替换下编译,`-fno-short-wchar` 给了它 32 位的 `wchar_t`,而它调用的 Win32
342+
接口回传的却是真正的 16 位 UTF-16——于是一个 `wchar_t*` 循环把两个 UTF-16
343+
码元读成了一个码点。把这条边界变成默认值,而不是一个包必须记得去写的清单
344+
键,让这一类失败变得**无法被表达**,而不只是被记录在文档里。
345+
346+
**优先级:包自己清单里显式写的 `c-environment` 永远赢过推导。**
347+
推导只在包什么都没写的时候才填 `cEnvironment`——如果一个包终究还是需要
348+
呈现的环境,它仍然可以显式这样声明(今天还没有办法反着写「不是
349+
platform」,因为 `"platform"` 仍是这个键唯一接受的取值)。这个显式键也仍然
350+
是 §5.3 另一类情形——不是 kernel-abi 边界、但自身确实有平台绑定单元的普通
351+
包——唯一的手段,那种情形下作者做出的确实是 mcpp 无法推导出来的选择:
352+
353+
```toml
354+
[package]
355+
# 一个普通包,不是 kernel-abi 提供者——这一个引擎推导不出来;作者要自己
356+
# 声明它,因为平台绑定单元只是这个包所构建内容里真正的少数(设计 §5.3)
357+
c-environment = "platform"
329358
```
330359

331360
这是一条边界规则,由这一个开关记录下来,而不是引擎强制执行:这样的包对图其余部分暴露的
@@ -342,6 +371,19 @@ c-environment = "platform" # 本包自身的单元按三元组自身的默认
342371
`llp64` 的两次构建,从同一份源码编译出 `long` 宽度不同的目标文件,因此二者绝不共享输出目录,
343372
也不会复用对方产出的目标文件缓存。
344373

374+
**全局构建缓存的键也覆盖了这一点(mcpp 2026.9.18+,PR 进行中的修订,不是设计原文)。**
375+
`~/.mcpp/build-cache/v1`——一次普通依赖编译跨项目、也跨 `mcpp` 升级复用的缓存——是与上面
376+
构建指纹分开的另一套机制,按包逐一取键,只取真正到达该包自身编译命令行的那些轴
377+
(`mcpp.build.cache_key`)。解析出的环境到达一个包的命令行,完全是通过引擎的**广播**
378+
(与 `targetSideUsage`、`-D__openkal__` 同一条通道),从来不经过包自己声明的
379+
`[build] cflags`/`cxxflags`——所以键的推导本身也得被告知去读广播后的值,而不只是声明的值。
380+
这个缺口正是这样被发现的(协调者反馈,openkal-musl 尖峰实验):原地升级 `mcpp`、缓存目录
381+
未清理时,给按新环境构建的镜像喂了按**旧**解析环境编译出的目标文件——一个镜像里混了两种
382+
C 环境,而且没有任何诊断。`fill_package_config` 现在把 `PackageRoot::privateBuild.cflags`/
383+
`cxxflags`/`asmflags`——广播之后的值——和包自己声明的标志一起折进键里,做法与它原本处理
384+
include 目录的方式完全一致。`--cache=off`,或者干脆清空缓存目录,从来都不是键本身正确
385+
的信号——这两条路径都是绕开了这个键,而不是证明了它。
386+
345387
**存储键——尚未补上。** 安装钩子把从源码编译的静态库装进共享 store 时,键只含包名与版本,
346388
不含针对哪种环境编译——这与 [requires](#requires) 已经记录的 C++ 运行时选择缺口同形。用同样
347389
的方式补上(一条 `requires` 形状的环境声明,在解析时核对、不符即拒绝)已经设计好,但尚未

‎modules/manifest/src/toml.cppm‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,35 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
11571157
"what the graph's C library declares.", *v)));
11581158
m.cEnvironment = *v;
11591159
}
1160+
// `c-environment = "platform"` is INFERRED, not merely allowed, for any
1161+
// package that PROVIDES `mcpp:kernel-abi=<impl>` — design revision from
1162+
// the openkal-musl spike (openkal-windows called Win32 with a
1163+
// POSIX-substituted `wchar_t` width and silently misread its own UTF-16
1164+
// return values). A `kernel-abi` provider is BY DEFINITION the boundary
1165+
// where the platform's own interfaces are called; its whole job is to
1166+
// speak the platform's ABI, so it can never be the package that wants
1167+
// the graph's presented [c-abi] environment instead of the triple's own.
1168+
// Making this the DEFAULT rather than a manifest key every kernel-abi
1169+
// package must remember means every already-released implementation
1170+
// (openkal-windows, -macos, -linux, and whatever comes next) gets the
1171+
// boundary right with no new release and no coordinated version bump,
1172+
// and the failure class becomes unrepresentable rather than merely
1173+
// documented.
1174+
//
1175+
// The explicit key above still wins: this only fills `cEnvironment` when
1176+
// nothing was written, so a kernel-abi package that, after all, needs
1177+
// the presented environment can still say so — there is just no way to
1178+
// say "not platform" today, because `"platform"` is the only value this
1179+
// key accepts (see the parse error above). Precedence is written down
1180+
// here and in docs/22 so it is discovered by reading, not by surprise.
1181+
if (m.cEnvironment.empty()) {
1182+
bool providesKernelAbi = std::ranges::any_of(m.provides, [](auto const& e) {
1183+
auto cap = mcpp::targetside::parse_capability(e);
1184+
return cap && *cap
1185+
&& (*cap)->layer == mcpp::targetside::CapLayer::KernelAbi;
1186+
});
1187+
if (providesKernelAbi) m.cEnvironment = "platform";
1188+
}
11601189
// [package] exclusive — capabilities this package claims sole provision of.
11611190
//
11621191
// Not validated against the reserved prefix: exclusivity is a property of

‎modules/manifest/src/xpkg.cppm‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import mcpp.pm.dep_spec;
99
import mcpp.pm.dependency_selector;
1010
import mcpp.platform;
1111
import mcpp.platform.axis;
12+
import mcpp.targetside;
1213
import mcpp.version_req;
1314

1415
export namespace mcpp::manifest {
@@ -2343,6 +2344,24 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
23432344
m.package.standard = m.cppStandard.canonical;
23442345
m.language.standard = m.cppStandard.canonical;
23452346

2347+
// `c-environment = "platform"` is INFERRED for a `mcpp:kernel-abi=<impl>`
2348+
// provider — see the identical block, and its full reasoning, in
2349+
// `toml.cppm`'s `[package] c-environment` handling. Repeated here rather
2350+
// than only there because this is a SEPARATE parser (an xpkg descriptor
2351+
// never touches toml.cppm), and this function does not parse an explicit
2352+
// `c-environment` key at all yet (`docs/22`, known gap) — so `m
2353+
// .cEnvironment` is always empty on entry here, and the inference is
2354+
// unconditional rather than "if empty". An already-released
2355+
// implementation (openkal-windows, -macos, -linux, …), installed from
2356+
// the index as an xpkg descriptor, is exactly the case this exists to
2357+
// cover without a new release.
2358+
if (std::ranges::any_of(m.provides, [](auto const& e) {
2359+
auto cap = mcpp::targetside::parse_capability(e);
2360+
return cap && *cap
2361+
&& (*cap)->layer == mcpp::targetside::CapLayer::KernelAbi;
2362+
}))
2363+
m.cEnvironment = "platform";
2364+
23462365
return m;
23472366
}
23482367

0 commit comments

Comments
 (0)