Skip to content

Commit e9230df

Browse files
2026.9.21.2: the macros mcpp owns are spelt in upper case, and the borrowed name is withdrawn (#681)
* 2026.9.21.2: the macros mcpp owns are spelt in upper case, and the borrowed name is withdrawn `__mcpp_target_<os>__` becomes `__MCPP_TARGET_<OS>__` and `__openkal__` becomes `__OPENKAL__`. The spelling is still the triple's own `os` field, now upper-cased; the engine still learns no operating-system name. THE CONVENTION SPLITS BY WHAT A NAME IS, NOT BY WHO WRITES IT. A vendor or product name is upper --- `__APPLE__`, `_WIN32`, `__MINGW32__`, `__GNUC__`. A kind-of-system name is lower --- `__linux__`, `__unix__`, `__gnu_linux__`. Every row this engine OWNS is of the first kind: it names mcpp, or it names openkal. The kind-of-system question is answered by `__linux__` and its family, which mcpp SUPPLIES rather than owns and which therefore keep their lower-case spelling, for that exact reason. The lower-case spelling shipped in 2026.9.21.1 on the reverse reading: that these names sit beside `__linux__` in real guards, so matching it was consistency. THAT CONFUSES ADJACENCY WITH KIND. `__APPLE__` sits in those same guards and is upper, because it belongs to somebody. `__MCPP_` is a prefix, not the whole rule: `__OPENKAL__` is owned and names *openkal* rather than mcpp. The assertion in `test_predefines.cpp` therefore checks the SPELLING CONVENTION --- upper case, `__`-wrapped --- instead of the prefix, which would have judged `__OPENKAL__` a violation when it is not. WITHDRAWING A MACRO IS DECIDED BY A COUNT, NOT BY READING An entry in this contract is a published interface, and withdrawing one is SILENT: a `#if` selects the other branch and compiles. No mechanism available to a build tool makes that loud. So a withdrawal is decided by a measurement that ENUMERATES READERS, and what it finds sets how many steps the withdrawal takes. The two this project has performed came out differently: withdrawn readers found steps __CYGWIN__ 4 third-party members, 3 + 6 sites in the two headers we INSTALL __mcpp_target_<os>__, __openkal__ none 1 The second row's denominator is every repository of this ecosystem, swept by file type: no source file and no manifest reads either name, and the only occurrences are this engine's emitter, its tests, and prose. Both names were invented here, so no upstream code can hold one; exposure was three days for `__openkal__` and a single release for the target macro. The rule did not change between the two rows. The count did. A new assertion takes its denominator from the target registry rather than from a list written beside the test: every canonical triple is parsed and its emitted macro checked to be a valid identifier. The spelling comes from the `os` field, so an `os` carrying a dot or a version suffix would produce a macro no compiler accepts, and the failure would land in a user's build. __CYGWIN__ IS WITHDRAWN --- THE LAST STEP OF A THREE-REPOSITORY SEQUENCE The Windows `presents = "posix"` realisation now adds `-U__CYGWIN__` and `-U__CYGWIN32__` to the compile line. `--target=x86_64-pc-cygwin` stays: it is what supplies `__unix__` and suppresses `_WIN32`, which is what presenting POSIX means. Only the borrowed NAME was unwanted. 1. 2026.9.21.1 defined mcpp's own name beside the borrowed one; this release re-spells it, while it still has no consumer. Purely additive. 2. `openkal-musl@0.19.0` and `openkal-llvm-runtime@0.14.0` read the new name and keep `|| defined(__CYGWIN__)`, so they build on an engine from either side of this change. PUBLISHED BEFORE THIS RELEASE. 3. this release stops defining the borrowed name. THE ORDERING IS MEASURED, NOT ASSERTED. It holds between repositories, so no test in this one can check it. Building an openkal program for `x86_64-windows-gnu` against the PUBLISHED `openkal-llvm-runtime@0.13.0` on this engine fails on libunwind's two `static_assert`s --- `x86_64 registers do not fit into unw_context_t`, `UnwindCursor<> does not fit in unw_cursor_t`. That is step three taken first, and it is why step two ships first. `setjmp.h` is the silent half: its own comment says a mismatch is reported by nothing until the record overruns. THE RESIDUAL WINDOW IS NAMED RATHER THAN CLAIMED AWAY. A project pinning `openkal-musl` at 0.18.0 or earlier EXACTLY, and upgrading past this release, gets that silent `#else`. Moving the index's `latest` onto 0.19.0 first keeps the window to exact pins; nothing available here closes it, because the engine cannot know which macros a package's installed headers read. Bootstrap pin advances from 2026.9.20.1 to 2026.9.21.1. * an unanswered requirement says so, and the openkal branch this engine is verified against is overridable E3: A REQUIREMENT NOBODY ANSWERED READ EXACTLY LIKE A CONFIRMED ONE. Three situations exist and two of them build: the resolved implementation states a `provides-interfaces` list containing the requirement (build), states a list without it (refused), or states nothing at all (build). The third is deliberate --- the key postdates the implementations, and a graph that has not adopted it must keep building --- but until now it produced output identical to the first, so a consumer inspecting a green build could not tell "checked and agreed" from "never asked". note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked The note names the implementation taken from the RESOLVED layer: a reader told only that something went unchecked cannot act on it. `tests/e2e/743` gains two legs rather than one. Leg C asserts the note is present, leg D that it is absent when the provider does state its list. WITHOUT LEG D, LEG C PASSES AGAINST AN ENGINE THAT PRINTS THE LINE UNCONDITIONALLY, which measures nothing. Leg C also asserts the COUNT, the one part of the message the fixture determines: a note reporting "1" or "0" would satisfy every assertion that matches only an identifier. THE OPENKAL BRANCH IS OVERRIDABLE FOR ONE RUN This repository already has one half of a cross-verification protocol: the ecosystem repositories build against an mcpp PR branch through `MCPP_SOURCE_REF`, so an engine change is measured against them before it merges. The reverse was hard-coded to `main`, which makes a change that REQUIRES a coordinated ecosystem commit unverifiable until after that commit lands --- and unmergeable until then, since `openkal-cross` is the job that fails. `__CYGWIN__`'s withdrawal is the case that showed it. `openkal-cross` builds `openkal-llvm-runtime@main`, whose installed header read only the borrowed name, so this engine's own CI reproduced the ordering constraint as a red cell. The cell is CORRECT --- the packages must publish first --- but verifying the engine before that publish needs the input. Left empty, nothing changes. E2 (the link-time set difference) is recorded in the plan as withdrawn from this release, with its blocker corrected: it is not the workload the plan claimed but a design decision. `SURFACE.txt` is not installed by any package, and docs/22 states that the engine knows no member of either set --- so where an interface-to-symbol map lives, and whether the engine can read one without learning this ecosystem's vocabulary, has to be answered before the feature has a shape. * plan: C2 is two different things, and only one of them is C2 Reading the measured diagnostics and the recipes apart: curl lib/setopt.c:31: 'linux/tcp.h' file not found cmp-module asio/detail/config.hpp:899: 'linux/version.h' file not found CURL IS A RECIPE DEFECT, the same shape as C3/expat. `compat.curl.lua` generates `#define HAVE_LINUX_TCP_H 1` inside `#if defined(__linux__)`. openkal runs on the Linux kernel, so `__linux__` is CORRECT; what is wrong is the recipe reading it as "glibc's whole Linux userspace is installed". The same block asserts `HAVE_GLIBC_STRERROR_R` --- openkal-musl is musl, so that one is actively false --- along with `HAVE_SYS_EVENTFD_H`, `HAVE_FSETXATTR` and a hard-coded host path in `CURL_CA_BUNDLE`. The honest test is `__has_include(<linux/tcp.h>)`: standard C, and it asks the question being asked rather than inferring which headers exist from which kernel. CMP-MODULE IS THE REAL C2. asio's `#include <linux/version.h>` sits outside every `ASIO_DISABLE_*` guard, so no configuration macro prevents it, and no manifest key reaches inside a third-party header --- which is justification one in `predefines.cppm`. `members.toml` already has an `[excluded]` table whose stated semantics are exactly "cannot be built in any openkal graph". What is missing is a second reason category, not a second table. E2's blocker is corrected in the same pass: not the workload the plan claimed but a design decision, since SURFACE.txt is installed by nothing and docs/22 states the engine knows no member of either set. * plan: the cross-verification protocol has a structural hole, found while using it Both ecosystem packages went green against the mcpp PR branch, AND NEITHER GREEN TOUCHED `__MCPP_TARGET_WINDOWS__`: openkal-musl linux/gcc, linux/llvm, macos/llvm; the cross-link row is Linux<->macOS openkal-llvm-runtime x86_64-linux-gnu and riscv64-none-elf No package's CI in this ecosystem builds `x86_64-windows-gnu`, and the whole subject of this change is a macro that exists only on Windows targets. The greens are real and they measure something else --- a criterion that ran, passed, and whose object was not present. The only job that builds that target is mcpp's own `openkal-cross`, and it had the ecosystem branch hard-coded to `main`, so an engine change requiring a coordinated ecosystem commit was unverifiable until after that commit landed. `openkal_ref` closes it; empty means today's behaviour. Consequence for ordering: the example in that job reaches the runtime through `path = "../.."` and the runtime's manifest pins `openkal-musl = "0.19.0"` from the INDEX, so musl 0.19.0 has to be registered before the input can be used. * plan: correct the Windows-coverage finding --- the runtime does have that leg Read job by job instead of from the first job of a run: openkal-llvm-runtime `host-dimension` has a Windows-host row that builds every target, including x86_64-windows-gnu, and it resolved openkal-musl@0.19.0 on the PR engine. The Windows leg of E1 WAS verified. openkal-musl linux/gcc, linux/llvm, macos/llvm, a Linux<->macOS cross-link, and a start-on-the-other-system job. No Windows cell at all. I concluded from the first job alone that no package in the ecosystem builds Windows-over-openkal. That was wrong and the conclusion it cast doubt on stands. The real gap is smaller and still a gap: the package whose change this round is `bits/setjmp.h` --- an installed header whose only branch is about Windows --- has no Windows cell of its own. It is verified today only transitively, because the runtime's CI pulls musl in as a path dependency and compiles it along the way. * the second layer of __cxa_thread_atexit is located, and the first hypothesis was right Following the criterion the finding itself wrote down --- instrument `run_dtors` --- with `&dtors` printed at both sites: registered dtor, dtors=0x7ffffe994680, &dtors=0x7ffffe9946a8 run_dtors called, dtors=0, &dtors=0x7ffffe9946c8 `run_dtors` IS called, which retires one branch. `&dtors` DIFFERS three times in one thread, which gives the other: `__thread` is emutls here, emutls keeps its per-thread blocks behind a pthread key of its own, that key's destructor had already released this thread's block, and every read afterwards allocates a fresh zeroed one. HYPOTHESIS ONE IN SECTION 4 WAS CORRECT AND WAS RECORDED AS REFUTED. What was wrong was the probe: musl runs key destructors in creation order, and that probe created its own key BEFORE first touching a thread-local, so emutls outlived it. The real ordering is the reverse. A probe cannot report an ordering it was constructed to avoid --- the predicate was right and the object's construction excluded the condition under test. Fixed in openkal-llvm-runtime 0.15.0 by keeping the list in the TLS key's own value, which no other key's teardown can reach. Two conditions asserted, in examples/cxx, on both targets: the thread_local is constructed, and its destructor runs. * plan: C1 is closed --- the only true unknown in the list Both layers located and fixed, shipping in openkal-llvm-runtime 0.15.0. The second layer: `__thread` is emutls under `-femulated-tls`, emutls keeps its per-thread blocks behind a pthread key of its own, and that key's destructor releases this thread's block before libc++abi's runs. Criterion: `&dtors` differs three times in one thread. It was closed by the criterion the finding document itself wrote down. And the hypothesis that document recorded as refuted was correct --- the probe created its own key before first touching a thread-local, and musl runs key destructors in creation order, so emutls outlived it. The predicate was right; the object's construction excluded the condition under test. * the finding's status word is one the checker knows `status: resolved` is not in the vocabulary the structure check accepts (active | landed | superseded | abandoned), and the agents index had not been regenerated. Both caught by `check_docs_structure.sh`, which is the check that exists so a front-matter word nobody reads does not drift. * plan: why the missing Windows cell is not a one-line matrix row Measured rather than assumed. A minimal criterion --- a setjmp/longjmp program with `_Static_assert(sizeof(jmp_buf) >= 32 * sizeof(unsigned long long))`, declaring `openkal-musl` alone, built for x86_64-windows-gnu: compiles which is the layer that matters, since a short jmp_buf is silent and a compile-time assertion is the only thing that makes it loud does not link cpow.o and others leave references unresolved; openkal-musl alone is not a complete link, because the openkal implementation and compiler-rt are assembled by openkal-llvm-runtime So a Windows cell there is either compile-only (which does catch this defect, and is cheap) or pulls the runtime in as a path dependency --- the mirror of what the runtime's own CI already does. Either is new work. * a sandbox verification for this wave, with its control reading Six sections. B and C are CHANGE and must fail on the previous release; E and F are GUARD and must pass on both. Measured, on the host, against the genuine published archive of 2026.9.21.1: 2026.9.21.1 fails=2 B: no upper-case target macro C: an unanswered requirement produced no note 2026.9.21.2 fails=0 C'\''s second leg passes on both by design: it asserts the note is ABSENT when the provider does state its list. Without it, the first leg would pass against an engine that printed the line unconditionally --- a negative control is not a hole in a CHANGE section. B carries both directions in ONE translation unit, because either alone passes for the wrong reason: an engine defining NEITHER spelling satisfies "the lower-case one is gone", and one defining BOTH satisfies "the upper-case one is here". Its second leg asks a freestanding target for its own macro, which is what says the spelling is DERIVED from the triple rather than enumerated. D and F need openkal-llvm-runtime 0.14.0 from the index and reported NOT RUN rather than passing when it was not yet registered. * plan: the criteria table carries status, and two rows split E1b and E1c did not exist when the table was written; C2 turned out to be two items with two different owners; E2 left this round with its blocker corrected. Each row now says what state it is in rather than only what would count as passing, because a table of criteria with no readings is a list of intentions. Recorded readings: the verification script's control run (2026.9.21.1 fails=2, 2026.9.21.2 fails=0), the nine green cells of openkal-cross, and the two directions the A3 check was measured in. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 24711c7 commit e9230df

25 files changed

Lines changed: 1166 additions & 221 deletions

‎.agents/docs/2026-09-20-cxa-thread-atexit-finding.md‎

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
---
22
subject: review
3-
status: active
3+
status: landed
44
---
55

6-
# `__cxa_thread_atexit` 在 openkal-Windows 上:定位到一层,第二层未定位
6+
# `__cxa_thread_atexit` 在 openkal-Windows 上:两层都已定位并修复
7+
8+
> **2026-09-21 收尾。** 第二层已定位,修法已实测,发在
9+
> `openkal-llvm-runtime@0.15.0`。下文 §1–§5 保留当时的记录(包括两个被否掉的假设),
10+
> §7 是结论。**§4 的第一个假设当时被判为「否」,而它其实是对的——错的是那次探针的
11+
> 构造,见 §7。**
712
813
- 日期:2026-09-20
914
- 来源:mcpp-index 的 30-member 重测,doctest 与 spdlog 两个成员停在
@@ -117,3 +122,60 @@ Windows : __cxa_thread_atexit_impl = 0 -> fallback branch
117122
- 被调用而链表为空 ⇒ 注册那一侧的问题
118123
- 没被调用 ⇒ `__libcpp_tls_create` / key 注册那一侧的问题
119124
2. 无论结论如何,修法必须让「析构会跑」与「链接会过」同时成立,或者两者都不成立。
125+
126+
127+
---
128+
129+
## 7. 第二层:已定位(2026-09-21)
130+
131+
### 读数
132+
133+
按 §6 写下的第一条判据做——在 fallback 的 `run_dtors` 里打一行,并同时打印
134+
`&dtors`:
135+
136+
```
137+
[probe] DtorsManager ctor: creating key
138+
[probe] registered dtor, dtors=0x7ffffe994680, key=0x2, &dtors=0x7ffffe9946a8
139+
[prog] in thread, v=7
140+
[probe] run_dtors called, dtors=0, alive=0, &dtors=0x7ffffe9946c8
141+
[prog] after join, ran=0 (expect 7)
142+
[probe] run_dtors called, dtors=0, alive=0, &dtors=0x7ffffe994708
143+
```
144+
145+
`run_dtors` **被调用了**——§6 的第二支排除。而 **`&dtors` 三次都不同**,在同一个线程里。
146+
147+
### 真因
148+
149+
`__thread DtorList* dtors` 在本包为 PE 采用的 `-femulated-tls` 下由 emutls 提供。emutls
150+
把每线程的块挂在它**自己的**一个 pthread key 后面,而那个 key 的析构已经先释放了本线程
151+
的块;之后每次读都新分配一个**清零**的块,所以地址每次都不一样。`run_dtors` 走的是空链表。
152+
153+
### §4 假设一其实是对的,错的是那次探针的构造
154+
155+
当时写的是:「PE 上 `thread_local` 走 emutls,它自己的 pthread key 先于 libc++abi 的 key
156+
被析构,于是 `run_dtors` 读到的链表已经空了」——**这就是真因**。
157+
158+
那次探针之所以读到 42,是因为 musl **按 key 的创建顺序**逐个调析构,而探针自己
159+
`pthread_key_create` 在第一次访问 `thread_local` **之前**,于是 emutls 的 key 排在它后面、
160+
析构也在它之后。真实情形里 libc++abi 的 `dtors_key`(实测 `key=0x2`)排在 emutls 之后。
161+
162+
**一个探针报不出它被构造成不会发生的那个顺序。** 判据落在了一个正确的谓词上,而对象的
163+
构造恰好排除了被测的那个条件——这与 [[a-check-that-picks-its-object-by-convention]] 同族。
164+
165+
### 修法
166+
167+
链表存进 **key 自己的值**。key 的析构函数本来就被交给这个值,而任何别的 key 的拆除都碰
168+
不到它。`dtors_alive` 随之不需要:值非空就是「链表在」。零新机制。
169+
170+
### 判据(两条,缺一不可)
171+
172+
`examples/cxx`,两个目标:
173+
174+
```
175+
ok: a thread_local is constructed in a spawned thread
176+
ok: and its destructor runs when that thread ends
177+
```
178+
179+
`x86_64-linux-gnu` 与 `x86_64-windows-gnu`(wine)均 `failures: 0`。只断言「链接通过」
180+
或只断言「构造发生」的判据会同时放过两层——这正是 §5 决定不发第一层补丁的那个理由,
181+
现在它变成了判据本身的形状。
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
#!/usr/bin/env bash
2+
# Ecosystem verification for the 2026.9.21.2 wave against the PUBLISHED mcpp
3+
# and index, run inside a SubOS sandbox with CN mirrors for xlings and mcpp.
4+
#
5+
# B64=$(base64 -w0 .agents/docs/2026-09-21-macros-and-withdrawal-verify.sh)
6+
# xlings subos use v920 --sandbox --cmd \
7+
# "echo $B64 | base64 -d > /tmp/v.sh && MCPP_VERIFY_VERSION=2026.9.21.2 bash /tmp/v.sh"
8+
#
9+
# RUN IT AGAINST THE PREVIOUS RELEASE FIRST (MCPP_VERIFY_VERSION=2026.9.21.1):
10+
# every CHANGE section must FAIL there and pass here, and every GUARD section
11+
# must pass on both. A CHANGE section green on both measured nothing.
12+
#
13+
# The sandbox's $HOME persists between runs of one SubOS, so each section
14+
# clears its own directory. A section that cannot run says so and is listed
15+
# again at the end: a run reporting only failures cannot be told from one that
16+
# examined nothing.
17+
#
18+
# TWO RUNS AND THE READING FROM EACH (host dry run, 2026-09-21, the older one
19+
# against the genuine published archive rather than a local build):
20+
#
21+
# mcpp 2026.9.21.1 (published) fails=2
22+
# B the engine does not define the upper-case target macro
23+
# C an unanswered requirement produced no note
24+
# mcpp 2026.9.21.2 fails=0
25+
#
26+
# C's SECOND LEG PASSES ON BOTH, and that is the point of having it: it asserts
27+
# the note is ABSENT when the provider does state its list, so without it the
28+
# first leg would pass against an engine that printed the line unconditionally.
29+
# A negative control is not a hole in a CHANGE section.
30+
#
31+
# D and F needed `openkal-llvm-runtime@0.14.0`, registered after that dry run,
32+
# and both reported NOT RUN rather than passing.
33+
set -u
34+
35+
VER="${MCPP_VERIFY_VERSION:?set MCPP_VERIFY_VERSION}"
36+
STORE="${MCPP_VERIFY_BIN:-$HOME/.xlings/data/xpkgs/xim-x-mcpp/$VER/bin/mcpp}"
37+
38+
fails=0
39+
skipped=""
40+
fail() { printf 'ASSERT-FAIL: %s\n' "$1"; fails=$((fails + 1)); }
41+
ok() { printf 'ok: %s\n' "$1"; }
42+
section() { printf '\n== %s ==\n' "$1"; }
43+
skip() { printf 'NOT RUN: %s\n' "$1"; skipped="$skipped
44+
- $1"; }
45+
unset XLINGS_ACTIVE_SUBOS
46+
47+
root="$HOME/verify-9212"
48+
rm -rf "$root"; mkdir -p "$root"
49+
50+
section "A. identity and mirror"
51+
if [ ! -x "$STORE" ]; then
52+
skip "mcpp $VER is not in the store at $STORE"
53+
printf '\n-- summary --\nfails=%d\nnot run:%s\n' "$fails" "${skipped:- (none)}"
54+
exit 1
55+
fi
56+
got="$("$STORE" --version 2>&1 | head -1)"
57+
case "$got" in
58+
*"$VER"*) ok "mcpp $VER from $STORE" ;;
59+
*) fail "the binary at $STORE reports '$got'" ;;
60+
esac
61+
"$STORE" self config --mirror CN >/dev/null 2>&1 \
62+
&& ok "mcpp mirror set to CN" || fail "mcpp self config --mirror CN"
63+
64+
# ── CHANGE 1. The owned macros are spelt in upper case ──────────────────────
65+
#
66+
# BOTH DIRECTIONS IN ONE TRANSLATION UNIT, because either alone passes for the
67+
# wrong reason: an engine defining NEITHER spelling satisfies "the lower-case
68+
# one is gone", and one defining BOTH satisfies "the upper-case one is here".
69+
section "B. __MCPP_TARGET_<OS>__ replaces __mcpp_target_<os>__ (CHANGE)"
70+
b="$root/b"; rm -rf "$b"; mkdir -p "$b/src"
71+
cat > "$b/src/main.c" <<'EOF'
72+
#if !defined(__MCPP_TARGET_LINUX__)
73+
#error "__MCPP_TARGET_LINUX__ is not defined"
74+
#endif
75+
#if defined(__mcpp_target_linux__)
76+
#error "the lower-case spelling is still defined"
77+
#endif
78+
int main(void) { return 0; }
79+
EOF
80+
cat > "$b/mcpp.toml" <<'EOF'
81+
[package]
82+
name = "macro-probe"
83+
version = "0.1.0"
84+
85+
[targets.macro-probe]
86+
kind = "bin"
87+
main = "src/main.c"
88+
EOF
89+
if (cd "$b" && "$STORE" build >/dev/null 2>&1); then
90+
ok "the upper-case target macro is defined and the lower-case one is not"
91+
else
92+
out=$(cd "$b" && "$STORE" build 2>&1)
93+
case "$out" in
94+
*"__MCPP_TARGET_LINUX__ is not defined"*)
95+
fail "the engine does not define the upper-case target macro" ;;
96+
*"lower-case spelling is still defined"*)
97+
fail "the engine still defines the lower-case target macro" ;;
98+
*) fail "the probe did not build, and for neither of the two reasons" ;;
99+
esac
100+
fi
101+
102+
# THE SPELLING IS DERIVED, NOT ENUMERATED. A second target with a different
103+
# `os` says the engine reads the triple rather than carrying a table of names.
104+
b2="$root/b2"; rm -rf "$b2"; mkdir -p "$b2/src"
105+
cat > "$b2/src/main.c" <<'EOF'
106+
#if !defined(__MCPP_TARGET_NONE__)
107+
#error "__MCPP_TARGET_NONE__ is not defined for a freestanding target"
108+
#endif
109+
void _start(void) {}
110+
EOF
111+
cat > "$b2/mcpp.toml" <<'EOF'
112+
[package]
113+
name = "macro-probe-bare"
114+
version = "0.1.0"
115+
116+
[targets.macro-probe-bare]
117+
kind = "bin"
118+
main = "src/main.c"
119+
120+
[build]
121+
ldflags = ["-nostdlib", "-nostartfiles", "-static"]
122+
EOF
123+
if (cd "$b2" && "$STORE" build --target riscv64-none-elf >/dev/null 2>&1); then
124+
ok "a freestanding target spells its own macro from the triple"
125+
else
126+
skip "the freestanding toolchain did not resolve in this sandbox"
127+
fi
128+
129+
# ── CHANGE 2. A requirement nobody answered is named ────────────────────────
130+
#
131+
# Three situations exist and two build. Without this line the first and the
132+
# third produce identical output, so a consumer cannot tell "checked and
133+
# agreed" from "never asked". Both legs, because a note printed
134+
# unconditionally would satisfy the first one alone.
135+
section "C. an unanswered requirement is named (CHANGE)"
136+
c="$root/c"; rm -rf "$c"; mkdir -p "$c/impl/src" "$c/src"
137+
printf 'int fake_kernel_marker(void){return 0;}\n' > "$c/impl/src/lib.c"
138+
printf 'int main(void){return 0;}\n' > "$c/src/main.c"
139+
mk_impl() { # $1 = provides-interfaces body, or empty
140+
if [ -z "$1" ]; then
141+
cat > "$c/impl/mcpp.toml" <<'EOF'
142+
[package]
143+
name = "fakekernel"
144+
version = "0.1.0"
145+
provides = ["mcpp:kernel-abi=openkal"]
146+
147+
[targets.fakekernel]
148+
kind = "lib"
149+
sources = ["src/*.c"]
150+
EOF
151+
else
152+
cat > "$c/impl/mcpp.toml" <<EOF
153+
[package]
154+
name = "fakekernel"
155+
version = "0.1.0"
156+
provides = ["mcpp:kernel-abi=openkal"]
157+
158+
[targets.fakekernel]
159+
kind = "lib"
160+
sources = ["src/*.c"]
161+
162+
[kernel-abi]
163+
provides-interfaces = [$1]
164+
EOF
165+
fi
166+
}
167+
cat > "$c/mcpp.toml" <<'EOF'
168+
[package]
169+
name = "iface-probe"
170+
version = "0.1.0"
171+
172+
[dependencies]
173+
fakekernel = { path = "impl" }
174+
175+
[build]
176+
allow_host_libs = true
177+
178+
[kernel-abi]
179+
requires-interfaces = ["openkal.fs", "openkal.net"]
180+
EOF
181+
mk_impl ''
182+
out=$(cd "$c" && "$STORE" build 2>&1)
183+
if [ $? -ne 0 ]; then
184+
fail "a provider that states nothing must not be refused"
185+
else
186+
case "$out" in
187+
*"kernel-abi interfaces"*"states none"*"2 requirements unchecked"*)
188+
ok "the note names the implementation and how many went unchecked" ;;
189+
*"kernel-abi interfaces"*)
190+
fail "the note is printed but does not carry the count" ;;
191+
*) fail "an unanswered requirement produced no note" ;;
192+
esac
193+
fi
194+
rm -rf "$c/target"
195+
mk_impl '"openkal.fs", "openkal.net", "openkal.abort"'
196+
out=$(cd "$c" && "$STORE" build 2>&1)
197+
if [ $? -ne 0 ]; then
198+
fail "a graph whose provider states every requirement must build"
199+
else
200+
case "$out" in
201+
*"kernel-abi interfaces"*)
202+
fail "the note appeared for a graph in which everything WAS checked" ;;
203+
*) ok "a provider that states its list draws no note" ;;
204+
esac
205+
fi
206+
207+
# ── CHANGE 3. The borrowed name is withdrawn ────────────────────────────────
208+
#
209+
# `__CYGWIN__` was left defined so that code needing "PE object format with a
210+
# POSIX-presenting C environment" had a name. A 30-member measurement found
211+
# four members reading it as "Win32 is available" and reaching windows.h.
212+
# This needs a real openkal graph, so it skips rather than failing when the
213+
# sandbox cannot reach one.
214+
section "D. __CYGWIN__ is withdrawn on a Windows target presenting POSIX (CHANGE)"
215+
d="$root/d"; rm -rf "$d"; mkdir -p "$d/src"
216+
cat > "$d/src/main.cpp" <<'EOF'
217+
#if defined(__CYGWIN__) || defined(__CYGWIN32__)
218+
#error "the borrowed name is still defined"
219+
#endif
220+
#if !defined(__MCPP_TARGET_WINDOWS__)
221+
#error "mcpp's own name for the target is missing"
222+
#endif
223+
#if defined(_WIN32)
224+
#error "presents = posix must suppress _WIN32"
225+
#endif
226+
#if !defined(__OPENKAL__)
227+
#error "__OPENKAL__ is not defined over a resolved openkal layer"
228+
#endif
229+
#if defined(__openkal__)
230+
#error "the lower-case spelling is still defined"
231+
#endif
232+
int main() { return 0; }
233+
EOF
234+
cat > "$d/mcpp.toml" <<'EOF'
235+
[package]
236+
name = "withdrawal-probe"
237+
version = "0.1.0"
238+
239+
[dependencies]
240+
openkal-llvm-runtime = "0.14.0"
241+
EOF
242+
out=$(cd "$d" && "$STORE" build --target x86_64-windows-gnu 2>&1)
243+
rc=$?
244+
case "$out" in
245+
*"openkal-llvm-runtime"*"not found"*|*"did not resolve"*)
246+
skip "openkal-llvm-runtime 0.14.0 did not resolve from the index" ;;
247+
*)
248+
if [ $rc -eq 0 ]; then
249+
ok "the borrowed name is gone, mcpp's own name is there, over openkal"
250+
else
251+
case "$out" in
252+
*"borrowed name is still defined"*) fail "__CYGWIN__ is still defined" ;;
253+
*"own name for the target is missing"*) fail "__MCPP_TARGET_WINDOWS__ is missing" ;;
254+
*"__OPENKAL__ is not defined"*) fail "__OPENKAL__ is missing over openkal" ;;
255+
*"lower-case spelling is still defined"*) fail "__openkal__ is still defined" ;;
256+
*"presents = posix"*) fail "_WIN32 survived the substitution" ;;
257+
*) skip "the openkal Windows graph did not build in this sandbox" ;;
258+
esac
259+
fi ;;
260+
esac
261+
262+
# ── GUARD. A package declaring nothing is untouched ─────────────────────────
263+
section "E. a package declaring neither key builds and runs (GUARD)"
264+
e="$root/e"; rm -rf "$e"; mkdir -p "$e/src"
265+
printf '#include <cstdio>\nint main(){std::puts("plain ok");return 0;}\n' > "$e/src/main.cpp"
266+
cat > "$e/mcpp.toml" <<'EOF'
267+
[package]
268+
name = "plain"
269+
version = "0.1.0"
270+
EOF
271+
if (cd "$e" && "$STORE" build >/dev/null 2>&1) \
272+
&& "$e"/target/*/*/bin/plain 2>/dev/null | grep -q "plain ok"; then
273+
ok "a package that declares nothing builds and runs"
274+
else
275+
fail "a package that declares nothing must be untouched by this release"
276+
fi
277+
278+
# ── GUARD. An openkal program from the published index ──────────────────────
279+
section "F. an openkal program from the published index (GUARD)"
280+
f="$root/f"; rm -rf "$f"; mkdir -p "$f/src"
281+
printf '#include <cstdio>\nint main(){std::puts("openkal ok");return 0;}\n' > "$f/src/main.cpp"
282+
cat > "$f/mcpp.toml" <<'EOF'
283+
[package]
284+
name = "openkal-hello"
285+
version = "0.1.0"
286+
287+
[dependencies]
288+
openkal-llvm-runtime = "0.14.0"
289+
EOF
290+
if (cd "$f" && "$STORE" build >/dev/null 2>&1); then
291+
if "$f"/target/*/*/bin/openkal-hello 2>/dev/null | grep -q "openkal ok"; then
292+
ok "an openkal program builds and runs from the published index"
293+
else
294+
fail "the openkal program built and did not run"
295+
fi
296+
else
297+
skip "openkal-llvm-runtime 0.14.0 did not resolve from the index"
298+
fi
299+
300+
printf '\n-- summary --\nfails=%d\nnot run:%s\n' "$fails" "${skipped:-
301+
(none)}"
302+
[ "$fails" -eq 0 ]

0 commit comments

Comments
 (0)