Commit eb02575
The C library layer declares the C environment it presents, the engine realises and checks it (#668)
* The C library layer declares the C environment it presents, the engine realises and checks it
Until now the compiler payload's target triple implied the environment a C
program compiles against. The moment a package supplies the C library
instead (mcpp:c-abi=<impl>), that stops being true: openkal-musl on
x86_64-windows-gnu generates PE/Win64 code while presenting a POSIX
environment, and every #ifdef _WIN32 above it is asking the wrong layer.
- [c-abi] manifest block: presents (posix/windows/none), data-model
(arch-default/lp64/llp64/ilp32), wchar (16/32), builtins (iso/platform).
Only a package that also provides mcpp:c-abi=<impl> may declare it.
presents/data-model/wchar carry no default; an unknown key or value is
always a parse error naming the key. Absent block changes nothing.
(modules/manifest/src/{targetside_model,toml,types}.cppm)
- Realisation (src/toolchain/cenv.cppm): a pure, table-driven mapping from
request to compiler tokens with no package names. The flagship case --
presents=posix on Windows/x86_64 -- realises as a Cygwin-flavoured
compile-only identity switch (--target=x86_64-pc-cygwin,
-U__CYGWIN__ -U__CYGWIN32__), leaving the link line on the graph's
resolved triple, because the two triples measure identical machine code.
Reaches C, C++, the dependency scan and the std module precompile.
A request this engine cannot realise is refused naming the target,
request and what is missing. [package] c-environment = "platform" opts a
package out of the realisation entirely.
- Verification, not trust (src/toolchain/cenv_probe.cppm): one -E -dM
predefined-macro dump, no codegen and no execution, checked against the
declaration and cached per configuration. Caught a real mapping bug
during development (data-model = "llp64" alongside presents = "posix" on
Windows silently read as already-satisfied); the mapping now refuses that
combination directly instead of relying on the probe to catch it late.
- __openkal__ defined for every target-side unit when the resolved
kernel-abi layer's interface is openkal, read from the layer's value.
- Closure visibility: provides = ["platform-sdk"] is a package's own
statement; the Target report gains a platform-deps line, and
[build] platform-dependencies = "refuse" fails the build when one is
present. (src/build/prepare.cppm)
- The realised environment and __openkal__ fold into the build fingerprint,
so LP64 and LLP64 builds of one source/manifest never share a directory.
The equivalent store-key gap for install-hook artifacts is designed but
not built here -- documented in docs/22 as a known gap.
Two pre-existing defects found while verifying this are filed separately,
not fixed here: #666 (a Clang-built mcpp binary SIGSEGVs
in its own ELF runtime inspector; GCC-built does not) and #667 (a GCC
self-host ICE importing mcpp.targetside from a new consumer under a
parallel build; -j1 avoids it).
Docs: docs/22 ([c-abi], verification, fingerprint), docs/21 (a declared
environment moves the compiled triple, not the linked one), docs/24 (the
three macro families, the __openkal__ rule, platform units), docs/06
(platform-sdk), and their zh mirrors. Tests: test_manifest.cpp,
test_cenv.cpp (14 cases covering the whole mapping table), e2e 741.
Design: mcpplibs/openkal .agents/docs/2026-09-18-openkal-c-environment-and-personalities-design.md
* Fix docs lint: zh heading parity and an interrogative table header
- docs/zh/21-the-target-triple.md was missing the zh mirror of the new
"a declared environment can move the compiled triple" note; heading
count parity with the English document now holds (25/25).
- docs/zh/24-openkal-cross.md's macro-family table header "由谁定义"
reads as a question (contains 谁); renamed to the noun phrase "定义者".
- Three cross-file anchors in the zh docs pointed at the ENGLISH heading
slug of their target section instead of the zh one
(#adaptation-to-the-resolved-target-side, #closure-visibility) — fixed
to the zh anchors check_docs_structure.sh actually validates against.
Caught by CI on #668 ("build + unit tests" job's doc-lint step); verified
locally afterwards with check_docs_style.sh, check_docs_structure.sh,
check_version_pins.sh, check_target_tiers.py and check_modules_wiring.sh.
* Keep __CYGWIN__/__CYGWIN32__ defined under the posix realisation
Design revision from the openkal-musl spike. The Cygwin-flavoured Windows
realisation no longer undefines __CYGWIN__/__CYGWIN32__ -- the -U tokens are
dropped, everything else (the --target= substitution, LP64, no _WIN32,
-fno-short-wchar for wchar=32) is unchanged.
The reason, stated once and correctly (an earlier private draft of this
change mis-attributed the cause to libunwind's own branch selection; that
was wrong on inspection of the vendored source and is not repeated here):
third-party portable code that needs to know the OBJECT FORMAT -- distinct
from the C environment and from the platform API -- has no other name for
"PE format with a POSIX-presenting C environment" than __CYGWIN__, and such
code cannot be patched the way this ecosystem's own packages can. This is a
trade-off for the 30-member measurement to settle: a library reaching for
__CYGWIN__ may also reach for a real Cygwin interface that does not exist
here, and if defining it produces more new failures than it fixes, the
answer flips.
Updated: the realisation table and its verification expectations
(src/toolchain/cenv.cppm), the unit test and e2e assertions (now checking
__CYGWIN__ stays DEFINED, the opposite of the first version), and the
English + zh docs/CHANGELOG prose.
* The realised [c-abi] environment reaches assembly units too
A .S unit's command line is assembled independently of a .c/.cpp unit's
(mcpp.build.flags::CompileFlags::as, not ::cc/::cxx), and only inherits the
-D/-U/-I subset of a package's C flags on purpose -- a -std= or -O token
meant for the C compiler is meaningless to GAS. The realised environment
broadcast landed only in privateBuild.cflags/cxxflags, so it never reached
that narrower channel: within one package a .c unit saw _WIN32 undefined
while a .S unit still saw it defined, because --target=/-fno-short-wchar
never reached the assembler's command line at all. Found by the openkal-musl
spike (okm_setjmp.S, and upstream libunwind's assembly.h, both select
register-save sets on that macro) -- a jmp_buf written by one save set and
sized by the other header is a silent mismatch, the exact failure class this
feature exists to remove.
Adds UsageRequirements::asmflags, a broadcast-only channel parallel to
cflags/cxxflags (no [build] asmflags manifest key backs it -- per-glob
`flags = [{ asmflags }]` remains the author-facing one), and routes the
realised environment tokens into it for GAS units only: NASM has no
--target= concept and accepts none of these flags, so it must never receive
them, and a unit test pins that split directly. Every token was checked
against clang's assembler-with-cpp front end first; none is rejected, so
nothing is filtered a second time.
e2e 741 gains a .S unit asserting it carries the same tokens main.cpp does.
* 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.
* Bump the build-cache epoch: a corrected key cannot tell a poisoned entry
from a trustworthy one
The previous commit made fill_package_config read the realised environment,
but a corrected derivation does not make an entry written under the old,
wrong one safe to keep. An entry is poisoned exactly when its recorded key
and its actual compiled inputs already disagreed at write time, and the
package most likely to still produce an unchanged key after the fix is the
one this same PR newly exempts from the realisation: a kernel-abi provider
inferred into c-environment = "platform" now has an empty
privateBuild.cflags, so its new key is computed from nothing -- matching
its old key, which was also computed from nothing, while the object on disk
was compiled WITH the substitution. No cheaper mechanism distinguishes a
pre-fix entry from a post-fix one, so kCacheEpoch moves (2 -> 3): every
existing ~/.mcpp/build-cache/v1 entry is orphaned unconditionally, one cold
rebuild for everyone, rather than trust a key equality that is wrong for
exactly the entries that matter most.
Also adds CacheKey.EveryPrivateBuildBroadcastFieldReachesTheKey, a durable
guard for the class of defect rather than only this one instance of it:
PackageRoot::privateBuild (UsageRequirements) is the engine's own broadcast
channel, and every one of its members -- today includeDirs, includeDirsAfter,
cflags, cxxflags, asmflags, ldflags, modules -- has to move the cache key,
because the key's only job is to describe what reaches the compiler. This
predates the c-abi wave (targetSideUsage's own broadcast, and __openkal__,
had the identical exposure before [c-abi] existed); the two fields nothing
broadcasts into yet (ldflags, modules) are asserted uncovered on purpose,
with an explicit instruction to flip that assertion in the same change that
adds their broadcast.
* Verification-probe test coverage, and the install-hook store's failure
mode written out plainly
The declaration probe (mcpp.toolchain.cenv_probe, design 2026-09-18 §3.2)
landed in an earlier commit and is wired into prepare.cppm, but had zero
direct test coverage: e2e 741 only exercises its success path (the build
would not otherwise complete) and its one refusal leg comes from
cenv::realise refusing an unrealisable request before the probe is ever
reached. Adds tests/unit/test_cenv_probe.cpp, calling cenv_probe::verify
directly against a real host compiler with deliberately wrong expectations
-- a declared long/wchar_t width the compiler disagrees with, a macro
declared defined that isn't and vice versa -- confirming each produces the
declared-vs-measured Mismatch entry prepare.cppm renders into its refusal
text, plus the cache's hit/miss behavior. Each test gets its own temp cache
directory: the probe's cache is keyed on (compiler, argv) alone, not on
what expectations are being checked, so two tests sharing a default cache
root and identical argv would have the second read the first's cached dump
rather than genuinely probe.
Separately, traced the install-hook store's code path (prepare.cppm,
install_hook_env) to answer directly: yes, an install hook can and does
compile target-side code into the shared store, and no, there is no way
today for it to know the realised [c-abi] environment, because the hook
runs before the toolchain resolves at all -- prepare.cppm resolves tc only
after the dependency graph installs, since resolving the target side can
itself depend on which package the graph supplies a layer from. This is not
a gap this PR could close with a local fix; docs/22 (+ zh mirror) now states
the failure mode plainly (objects sized for one wchar_t linked against
headers sized for another, with nothing checking it) rather than as a line
in a gap table, names what closing it for real would take, and says
explicitly that this is the same shape as the already-shipped c++-abi
store-key gap, not a new one this PR introduces.
* Self-review before merge: a fingerprint bug found, the fourth fact and
the inference precedence made findable, macOS CI pointed at #669
Found while verifying the P1 criterion (a package declaring nothing gets a
byte-identical command line): the c-abi fingerprint's cenv-platform loop
was unconditional, scanning every package for c-environment == "platform"
regardless of whether any [c-abi] realisation was active in this build at
all. Since c-environment = "platform" is now INFERRED for every
mcpp:kernel-abi=<impl> provider, this folded "cenv-platform:<name>" into
the fingerprint -- moving the output directory -- of every project using
one, even when nothing about the realised environment was ever active.
Confirmed live: the identical fakekernel-in-graph, no-[c-abi] scenario,
same project directory, same manifest, produced two different output-dir
hashes before and after gating the loop behind the same "cEnvTokens or
cEnvBuiltinsTokens non-empty" condition the rest of the block already uses
correctly. Fixed; a package using no c-abi feature at all now keeps the
directory it already had, whether or not its graph happens to include a
kernel-abi provider.
docs/24 gains the fourth fact this cross-repo doc's own table was missing:
object format (__CYGWIN__) is a property of the target triple, not a
fourth C-environment macro, and rides along with -- not folded into -- the
three-family table; and corrects its now-stale "platform units" paragraph,
which still described openkal-windows declaring c-environment explicitly,
to state the inferred/explicit split and precedence (both en and zh).
CHANGELOG's cache-epoch entry now leads with the user-facing consequence
(first build after upgrading is cold) rather than burying it inside the
technical explanation.
ci-macos.yml's SDK-selection comment is corrected per review: it claimed
mcpp's own SDK resolution is independent of and overrides the xlings LLVM
package's clang++.cfg sysroot, which does not survive checking (both
currently resolve to the same path; which wins on the command line is not
established). Replaced with the defensible statement plus a pointer to
#669, which both known-red xcode-27 jobs now name.
---------
Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>1 parent 2b42f74 commit eb02575
30 files changed
Lines changed: 3136 additions & 19 deletions
File tree
- .github/workflows
- docs
- zh
- modules
- manifest/src
- toolchain-model/src
- versioning/src
- src
- build
- modgraph
- toolchain
- tests
- e2e
- unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
27 | 31 | | |
28 | 32 | | |
29 | 33 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
8 | 108 | | |
9 | 109 | | |
10 | 110 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
523 | 523 | | |
524 | 524 | | |
525 | 525 | | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
526 | 552 | | |
527 | 553 | | |
528 | 554 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
468 | 468 | | |
469 | 469 | | |
470 | 470 | | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
471 | 486 | | |
472 | 487 | | |
473 | 488 | | |
| |||
0 commit comments