Releases: mcpp-community/mcpp
Release list
v2026.9.26.2
编译数据库:一个配置一个数据库(#699 的报告;#397 C-1、#677 B1)
compile_commands.json 此前是一份跨命令、跨工具链合并的文件:删除后的下一次构建不再写回它,
另一个工具写入的条目与上一个工具链的条目都被保留,directory 写的是工程根目录而编译器在输出目录
中运行。现在:
- 每个配置一份数据库,位于
target/<triple>/<fingerprint>/compile_commands.json,只在本配置之内
合并,因此mcpp test之后的mcpp build保留测试单元的条目。工程根目录的文件是当前配置数据库
的副本,整体替换、从不合并,内容相同时不写;切换工具链或 profile 就切换整个文件,切回时恢复该
配置的条目。根目录的符号链接照旧按其指向写入。 - 根文件被删除后,下一次构建从配置数据库恢复它,快路径也是如此,不需要规划。
- 被替换的根文件含有 mcpp 没有写的条目时(条目的
output不在本工程的target/或 mcpp 主目录
之下),构建以一条警告说明其数量。 directory与 S1 的work-directory是编译器运行的输出目录,与 JSON Compilation Database 格式、
S1-8-2 以及 CMake、ninja 的写法一致;从directory重放 GCC 的条目不再在源码树中写出
gcm.cache/。- 提供模块的单元在
-c之前带上接口语言 flag(clang 为-x c++-module,GCC 为-x c++),
clangd 因此能处理.ixx接口;MSVC 的写法待 Windows 实测后再加。 - 导入
std的构建在compile_commands.json与emit --spec compile-commands中列出标准库单元
(S1-12-1),另一个版本的 clangd 因此能自己构建std。S1 文档中标准库单元的provides指向
共享 std 缓存中的 BMI,工具链带build-id,版本与构建身份完全一致的读者可以直接复用。 mcpp new写出的.gitignore包含compile_commands.json。
emit build-database 按成员规划(#699)
emit build-database --workspace 此前在第一个规划失败的成员处停止,已规划成员的集合全部丢失。
现在每个选中的成员独立规划:规划失败的成员不贡献集合,贡献一条 error 诊断,path 为它的
mcpp.toml;至少一个成员规划成功时文档带 data,watch 也列出失败成员的 mcpp.toml 与
build.mcpp;有任何错误时退出码为 1。在 emit 下,构建失败的宿主工具是警告
MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT,规划继续,构建程序得到该工具将被发布的路径;构建程序失败
的包不带该程序的指令地被描述,并得到一条 MCPP_BUILD_DATABASE_PROGRAM_FAILED 错误,path 为它的
build.mcpp。mcpp build 的行为不变。带错误诊断的 data 是 S2 0.3.0 的部分回答
(Sunrisepeak/mcpp-language-server#25)。
构建程序:runtime_search_dir、prepare 角色与 stamp 规则(#701、#702)
mcpp::runtime_search_dir(dir)(mcpp:runtime-search-dir=,协议 12)是[runtime] runtime_search_dirs的构建程序形态,并入同一个字段:ELF 与 Mach-O 的运行路径、mcpp run的加载
路径、mcpp pack的闭包搜索与运行时校验都照常读取它,依赖的声明到达消费方的可执行文件。目录在
构建程序运行时不必存在。保留字段[runtime] library_dirs不获得指令。- 新角色
prepare用于文件名在构建程序运行时未知的施工(安装一个前缀、解开一个 SDK):命令填充
用a.output_dir(dir)声明的目录,引擎写 stamp。命令成功而目录不存在或除 stamp 外不含文件时,
这条边失败并点名目录。声明包的每条编译边与计划中的每条链接边等待它,构建输出以PREPARE
标注。构建程序的重新运行依据落在某个prepare目录内时,构建给出警告。 - 角色以常量书写:
mcpp::roles::{source, check, object, artifact, prepare}。引擎拒绝这五个之外的
角色字符串并列出它们;此前未知的字符串被静默读作source。 check与prepare的命令成功后,引擎创建缺失的 stamp,并把已存在的 stamp 更新到当前时间;
此前已存在的 stamp 不被更新,一个输入改变一次之后,该 action 在此后每次构建中都会重新运行。
Windows 程序的 DLL 在链接后放到程序旁
PE 映像没有运行路径,运行时搜索目录中的 DLL 此前只服务于 mcpp run 与 mcpp pack,从构建目录直接
启动的程序找不到它。现在计划中带运行时搜索目录的 PE 程序在链接后多一条边 mcpp place-dlls:按
mcpp pack 的闭包求解与系统规则,把程序直接或间接导入、且在这些目录中解析到的 DLL 放到程序旁,
字节不同时才写;DLL 在其目录中被替换(包括被 prepare action 替换)后,同一次构建会再次放置它。
部署到程序旁的 DLL 从链接边的隐式输入改为 order-only 输入,DLL 变化不再使程序重新链接。
链接 flag 按词读取(#703)
[build] ldflags、mcpp::link_flag 与依赖传播的链接 flag 此前只为 ninja 转义,Linux 与 macOS 的
sh 展开其中的 $ORIGIN,程序的运行路径因此含有 /../lib,即宿主的 /lib。SPEC-004 §8 的词读法
现在同样适用于链接 flag:每个词原样到达链接器;依赖的链接 flag 按词传播;link_search、link_lib、
link_script 由路径构成的值是一个词,含空格的目录不再被拆开。为 shell 或 ninja 手工转义的写法
(\$ORIGIN、'$$ORIGIN')现在按写法读取,首次规划以 build/flag-words 指出这样的元素;
索引中没有这样的写法。
构建插件规范 SPEC-007
新增 docs/specs/build-plugins.md(草案 0.2):插件的配置、施工与校验各用一种机制,环境不完整时
警告而不失败,配置不依赖施工结果,运行时库以 runtime_search_dir 声明,构建期的网络访问在离线
构建中不发生。mcpp 只提供通用机制,某一个工具的知识只属于它的插件。
兼容性
- 构建程序缓存的 epoch 升到 3,升级后每个构建程序重新运行一次。
- 构建程序协议升到 12;使用新指令、新角色或角色常量的构建程序在旧引擎上编译失败并指出名字。
v2026.9.26.1
路径与文本统一为 UTF-8(#693)
mcpp 此前在 Windows 上以进程 ANSI 代码页持有路径,而它写出的 compile_commands.json 只能容纳
UTF-8。于是工程目录或 mcpp 主目录只要含非 ASCII 字符,每次构建都以
internal: unhandled exception: [json.exception.type_error.316] 失败,系统代码页能拼出的名字也
不例外(代码页 936 上的中文目录名、代码页 1252 上的 café);代码页拼不出的名字则以窄化异常失败。
Linux 上名字不是 UTF-8 的目录以同样的 JSON 异常失败。现在 mcpp 在所有平台上以 UTF-8 持有文本:
mcpp.exe以应用程序清单声明 UTF-8 代码页(res/mcpp.rc),在 Windows 10 1903 及以后的版本上
它与 Windows 交换的窄字符串都是 UTF-8。build.mcpp链接同一份清单;作为 host tool 构建的程序
默认也带它,除非其目标写windows_code_page = "legacy"或包自己嵌入了清单。- 新的目标键
windows_code_page("utf-8"或"legacy")为工程自己的可执行文件声明代码页;
不写时保持系统代码页。 - 路径没有 UTF-8 拼法的工程目录与
MCPP_HOME在构建之前被拒绝,消息以转义拼出该路径并给出
本机的原因;工程内部这样的文件名被跳过并按目录汇报一次;文本不是 UTF-8 的build.mcpp指令
按键名被拒绝。compile_commands.json的序列化失败报告为该文件的失败,不再成为内部异常。 build.ninja含非 ASCII 字节时,mcpp 以ninja -t wincodepage核对 Ninja 读取它的编码,
不是 UTF-8 就拒绝并点名该 Ninja。cl.exe、link.exe、lib.exe只在响应文件以字节顺序标记开头时
按 UTF-8 读取它(在 windows-latest 上实测),msvc 方言的响应文件因此以字节顺序标记开头。- 在忽略清单的 Windows(早于 1903)上,受影响路径的诊断会写出进程所在的 ANSI 代码页。
- Windows CI 新增回归任务:llvm、MSVC、MinGW 三行分别在 ASCII、
café与中文目录中构建并运行,
并核对build.ninja与compile_commands.json中该目录名的 UTF-8 字节。
报告中的静默 0xC0000409 来自 xlings 的 shim:它在代码页之外的工作目录中启动时抛出未捕获的异常。
xlings 2026.9.26.2 声明同样的 UTF-8 代码页并在 main 中设异常边界,mcpp 内置的 xlings 版本随之更新。
c_standard 作用于声明它的包(#695)
[build] c_standard 此前写在文件级 $cflags 上,图中每个 C 编译单元都读它:根包的值到达每个依赖,
依赖自己的声明被解析、被哈希进缓存键,却没有被施加。现在每个包的 C 单元以该包自己的标准编译,
未声明的包以 c11 编译,C++ 单元不接收 C 标准。cl.exe 以其默认模式编译 C,构建会用一行列出
声明未被施加的包。#690 设计记录 §3.1 中把 c_standard 列为「只读根包」的一行已附更正说明。
链接图提供的 C 库时不搜索宿主目录(#696)
C 库来自依赖图的链接此前没有 --sysroot,clang 会从宿主推导 /usr/lib 等库目录:
x86_64-linux-musl 上的 -lm 把宿主 glibc 的目标文件静默链接进 musl 镜像,aarch64-linux-musl
上则以一条不相干的报错失败。现在由 clang 链接的 ELF 目标带上指向构建目录内空目录的 --sysroot;
密封检查拒绝指向工具链存储、构建目录与图中各包之外目录的 -L([build] allow_host_libs = true
取消这一拒绝)。图中无人应答的 -l 以链接器自己的消息失败,mcpp 另附说明;缺失的名字全属于 musl 以
libc.a 应答的八个名字时,说明会点名带有这八个空归档的 openkal-musl 0.19.2。
musl 目标按声明的优化级别编译(#694)
引擎此前在 *-linux-musl 上把每个非零优化级别替换为 -Og(一个 musl-gcc 15.1.0 内部编译器错误的
变通,也作用于 clang 与 mcpp 自己的 Linux 发布二进制),而 Finished 一行按声明的级别报告
optimized。该分支已删除:编译与报告读取同一个值(realised_opt_level),空级别即 0。
原先的触发条件在任何可得的输入上都不再复现。
v2026.9.25.1
工作空间成员在每种位置上以相同方式编译(#690)
[workspace.build] 的继承此前在把清单固定进构建图的那一步执行,晚于 defines 的展开。
作为兄弟成员 path 依赖被编译的成员因此收不到工作空间的 defines(#690 报告的情形),
而被命令选中的根成员收到两份工作空间的 cflags、cxxflags 与 ldflags。现在成员在自己的
加载处继承,顺序与根包相同:先继承,再合并条件节,最后展开 defines;固定进构建图的一步只读取
结果,遇到未展开的 defines 时报告内部错误。
同一处还补上了两件同形的缺口:
- 作为依赖出现的成员解析自己的
x.workspace = true条目。此前该条目以既无版本也无路径的形式
进入解析,报出的是「no readable index entry」。 - 通过
git引用的、托管在 git 上的工作空间的成员,除[workspace.package]外,也继承所在仓库
的[workspace.build]与[workspace.dependencies],相对路径以仓库根为锚点。同一个提交在它
自己的检出中与在使用方的依赖图中以相同方式编译。 - 索引描述符以
mcpp = "<子目录>/mcpp.toml"指向归档内的成员时,该成员以同样方式取得归档中的
工作空间;在归档内查找工作空间根不越出该版本的安装根。本机已安装的 22 个带工作空间根的索引包
都没有声明可继承的表,这一变化不改变任何现有包。
[workspace.build] ios_deployment_target 此前被解析、被继承,却被已知键检查拒绝;可继承键现在
只在一张表里陈述,解析、已知键检查与报错文本都取自这张表。
defines 按宏名构成集合
defines 的条目按包接收它们的顺序合并:[workspace.build]、包自己的 [build]、命中的
[target.<selector>.build]。同一宏名的后一个条目在原位替换前一个,条目 !NAME 移除该宏名,
每个宏名只以一个 -D 词到达编译器。成员因此可以覆盖或移除继承来的宏,而不再产生重定义诊断
(-Werror 下是错误)。defines 条目同样取代同一个包的 cflags、cxxflags 中同名的 -D 词。
更早的 mcpp 会把 !NAME 作为 -D!NAME 交给编译器,使用它的包需要 mcpp 2026.9.25.1 或更新版本。
使用方的头文件目录不再进入依赖
根包的 include_dirs 与 include_dirs_after(包括 private_include_dirs)此前被写进
build.ninja 的文件级编译标志,图中每个编译单元都读到它们:根包目录中与系统头或依赖头同名的
文件会在依赖内部遮蔽它们。依赖缓存键不含这些目录,于是按一个工程的根包头文件编译出的依赖对象
会被另一个无关工程取用(实测:compat.cjson 的 cJSON_Compare(1.0, 1.2) 在未写任何头文件的
工程中答 1)。现在每个编译单元只收到自己的包的目录,以及它的依赖公开的目录;根包自己的编译单元
中每个目录只出现一次。
行为变化。 依赖若曾依靠使用方的头文件目录才能编译,现在报告缺少头文件;mcpp 在编译器报错
之后指出该头文件所在的使用方目录。依赖需要通过自己的 include_dirs 或它的某个依赖找到头文件。
依赖缓存的 epoch 由 3 升为 4,升级后首次构建重建一次依赖缓存,不需要任何操作。
作为 host tool 构建的依赖只合并一次条件节
依赖的 [target.<selector>.build] 条件节先按使用方的目标合并,host tool 子构建此前拿到的正是合并后的
清单,又按宿主合并一次,同时命中两者的条目因此到达工具两次。实测:条件节中的 -include once.h
(无 include guard)让单独构建正常的包作为工具时报重定义错误,2026.9.24.1 同样如此。清单现在记录
第一次合并之前的状态,子构建从该状态按宿主合并。
成员清单只有一种读法
mcpp publish、mcpp pack、mcpp emit xpkg、mcpp toolchain list、mcpp sbom、mcpp index list/update、
mcpp update 的索引刷新、快路径的身份判定与测试发现,此前直接读取成员目录中的原始清单:省略
version 的成员被拒绝,未写 [toolchain] 的成员在 toolchain list 中显示全局默认工具链,而同一
目录下的 mcpp build 用的是工作空间的工具链。现在它们与 prepare_build 经同一个函数读取继承后的
清单。
工作空间成员的发布形态自包含
在成员目录中执行 mcpp publish 或 mcpp emit xpkg 时,归档只包含成员自己的目录,此前其中的
mcpp.toml 原样照搬:继承的 version、[build] 标志不在其中,兄弟成员之间的 path 依赖原样保留
而使用方无法解析,描述符的 deps 静默略去这些依赖。现在发布的清单是归一化后的:继承来的值被写出,
x.workspace = true 条目取得解析后的来源,兄弟依赖以版本依赖发布,原清单以 mcpp.toml.orig 保留,
归一化后的文件同时写到 target/dist/<name>-<version>.mcpp.toml。归档由 git 对象按提交日期构建,
两次运行逐字节相同。不带 version 的兄弟 path 依赖、包外非成员的 path 依赖、位于成员目录之外的
继承头文件目录会被拒绝,报错给出应写的内容。不是工作空间成员的包与此前一样原样归档。
归一化后的清单只使用 2026.9.24.1 已接受的键,实测该版本的客户端可以构建它。
xlings 2026.9.20.1
内置的 xlings 由 2026.9.16.1 升为 2026.9.20.1(openxlings/xlings#610):项目 subos 中执行的命令
不再从项目的 subos 读取全局工作空间并据此删除全局 shim;下载失败的安装不再报告 installed。
测试基础设施
e2e 辅助脚本 _inherit_toolchain.sh 改为按版本链接开发者的载荷。此前它链接整个包目录,测试
新装的版本穿过链接写进开发者的 ~/.mcpp,链接脚本指向测试结束后即被删除的临时目录(#293 的
第二种形态;实测 31_transitive_deps.sh 写出了这样的 xim-x-glibc/2.44.3)。
v2026.9.24.1
MSVC ABI 上的 toolset:只选一次,可以指定,记录在案
clang 以 *-windows-msvc 为目标时,编译所针对的 MSVC 环境(STL、CRT、Windows SDK)由两个互不相关的
选择器决定:clang 驱动自己探测头文件与库(VCToolsInstallDir、PATH、最新实例的默认 toolset,
%INCLUDE% 存在时整体采用它),mcpp 另行按 VSINSTALLDIR、vswhere -latest、目录名最大者定位
std.ixx。机器上装有多个 toolset 时,两者可能指向不同的 toolset;项目无法指定用哪一个;选择结果
既不进缓存键,也不出现在任何记录里。
现在 MSVC toolset 是这一行的 sysroot,由 [target.<triple>].sysroot 指定,prepare 解析一次:
[toolchain]
windows = "[email protected]"
[target.x86_64-windows-msvc]
sysroot = "[email protected]" # 或 "msvc@system"(默认),或 "xim:[email protected]"结果以 -Xmicrosoft-visualc-tools-root、-Xmicrosoft-windows-sdk-root、
-Xmicrosoft-windows-sdk-version 传给编译、链接与 std 模块预编译(这几个是 clang-cl
/vctoolsdir 等选项的别名;带上它们后,clang 不再读 VCToolsInstallDir 与 %INCLUDE%),
std.ixx 取自同一个 toolset。构建打印一行 Resolved sysroot msvc@system → MSVC <版本> (...),
resolution.json 新增 msvc_toolset 与 windows_sdk,toolset 目录与 SDK 版本进入缓存键,
SDK 版本也成为这一行的运行时身份 ucrt@<版本>。
写法在 cl.exe 行与 clang 行上相同:
| 写法 | 含义 |
|---|---|
msvc@system |
本机默认:VCToolsInstallDir → VSINSTALLDIR 实例的默认 toolset → PATH 上的 cl.exe → 带 C++ 组件的最新实例的默认 toolset |
msvc@<toolset> |
本机已装的同版本 toolset 优先(所有实例中查找),没有时安装载荷;环境变量不参与,被忽略时打印说明 |
xim:msvc@<toolset> |
只用载荷,SDK 随载荷固定 |
xim: 是唯一的工具链命名空间。gcc、llvm 的工具链总来自载荷,xim:[email protected] 与 [email protected] 等价,
这一点与过去相同;过去任何 <ns>: 前缀都被静默剥掉,现在其他命名空间在读取处被拒绝。xim:msvc@system
不是一种写法,同样在读取处被拒绝(过去它在更晚的阶段失败,Linux 上报的是「只在 Windows 宿主可用」)。
行为变化。 msvc@<toolset> 过去一律使用载荷;现在机器上有同版本时直接用机器的那一份,SDK
随之取机器上的。需要载荷(连同它的 SDK)的项目改写为 xim:msvc@<toolset>。cl.exe 行的
msvc@system 改为取实例的默认 toolset(Microsoft.VCToolsVersion.default.txt)而不是目录名最大者,
并开始读取 VCToolsInstallDir;某个 toolset 缺少 std.ixx 时,不再借用另一个 toolset 的。
cl.exe 行上指向另一个 toolset 的 sysroot 被拒绝。mcpp toolchain list 在 Windows 上列出
本机已装的 toolset。
旧引擎读新写法。 在 Windows runner 上用 2026.9.21.3 实测:MSVC 行上的 sysroot = "msvc@system"
与 "msvc@<toolset>" 让整份清单被拒(「is not an xpkg reference」);"xim:msvc@<toolset>" 被接受
而不生效:不安装任何东西,clang 针对机器上的 toolset 编译,构建输出却把这个值列为 c-abi 层。
依赖所写 toolset 的项目应把 mcpp 固定在 2026.9.24.1 或更高。
macos_deployment_target 按目标生效,不再按宿主(#685)
在 Linux 或 Windows 宿主上 mcpp build --target aarch64-macos,产物的 LC_BUILD_VERSION minos 恒为
14.0:[build] macos_deployment_target 与 MACOSX_DEPLOYMENT_TARGET 都被忽略,改了值也不重建。
解析函数只在 #if defined(__APPLE__) 下读这两个输入,其他宿主上返回空,三元组于是回落到内置的
14.0;指纹也只在宿主是 macOS 时折入这个值;平台事实 macos.deployment-target 同样为空,包里
针对它的版本要求在这种构建里不被检查。反方向同理:macOS 宿主交叉到非 Apple 目标时,编译命令里
仍带 -mmacosx-version-min。
现在解析(env > manifest > 14.0)与宿主无关,是否适用由构建的目标决定:三元组、指纹、平台事实、
-mmacosx-version-min 与 std 模块预编译都按目标判定;build.mcpp 的宿主编译按它自己的目标(即宿主)
判定。macOS 宿主构建 macOS 目标时输出不变。判据是 tests/e2e/746_…:在 Linux 宿主上用
[target.aarch64-macos] toolchain = "llvm@…" 走 --configure-only,不需要 macOS SDK,断言
--target=arm64-apple-macos11.0、改值后指纹变化、env 优先于 manifest;把修复撤回,三条全红。
mcpp self doctor 报告 gcc 载荷里冻结的 fixincludes 头(#687)
gcc 13.3.0、15.1.0(以及 11.5.0)的 x86_64-linux-gnu 载荷在 include-fixed/ 里带着构建机上 glibc
头的 fixincludes 副本(pthread.h 等)。它们在搜索顺序上排在构建所用的 glibc 2.44 之前,于是
<mutex>、<memory> 编译失败。索引里本有一段安装时删除它们的清理代码,但单引号嵌套让 grep 搜索的是
当前目录而不是载荷,从未生效;配方的修正在 openxlings/xim-pkgindex#870,新安装会被清理。
已安装的载荷不会因为索引更新而重新清理。doctor 新增一项检查:列出每个 gcc 载荷里带
auto-edited by fixincludes 横幅的文件、横幅里的源路径,以及重装命令
(mcpp index update,然后 mcpp toolchain remove gcc@<v> 与 mcpp toolchain install gcc@<v>)。
这是警告,不是失败。
SPEC-006:工具链管理(草案)
docs/specs/toolchain-management.md:身份与写法、来源与选择、载荷契约、构建、验收与发布顺序,
每条标注实现状态。载荷契约与验收(载荷 lint、编译器 × C 库兼容矩阵、准入门进 CI)计划与下一批
LLVM 工具链一同实现。
v2026.9.21.3
mcpp test --no-run:为一个跑不了的目标构建测试,并把这当成答案
一个本机既不能执行、也没有 runner 可达的目标,会让每个测试停在 not run,命令退出 2。
这是对「这些测试通过吗」的正确回答——mcpp 没有查明。但 2 同样是 runner 坏掉时的
退出码,于是一个只想要「构建」的调用方无法区分这两者,只能退回去用 mcpp build。
而 mcpp build 构建的是包。对一个源码只在 tests/ 下的包,它一行都不编译。
实测 mcpp-index 的 archive 成员(源码是 tests/ 下两个文件):
$ mcpp build --target aarch64-macos # 退出 0
Compiling compat.lz4 / compat.xz / compat.zlib / compat.zstd ...
$ find target -name '*compression*' -o -name '*versions*'
(只有 musl 的 versionsort.o)
退出 0,编译了这个成员的依赖,而该成员自己的代码一行都没有编过。一个读这个
退出码的兼容性测量,会把它记成「这个成员在 macOS 上构建通过」。
--no-run 让那个更窄的断言有了自己的答案:被选中的每个测试都为该目标编译并链接,
没有任何一个被执行,结果也这么写:
test result ok. 0 passed; 0 failed; 2 built, not run
built 与 not_run 分开计数,机器接口也一样(docs/50):not_run 是「试过而做不到,
问题悬着,退出 2」,built 是「被要求不要执行,构建就是问题的全部,退出 0」。编译不过
的测试仍然是失败。--no-run 与 --no-runner 同时给出会被拒绝——两个名字只差一个字符
而含义相反,不存在应当优先的那一个读法。
判据是 tests/e2e/745_no_run_builds_the_tests_and_says_so.sh,四条腿。其中 runner 用的是
一个不存在的程序名:用一个执行不了的目标会让这个测试需要交叉工具链和特定宿主,而
一个找不到的 runner 在每台宿主上、对本机目标、不装任何东西,就能造出同一个局面。
builtins = "iso" 发的那个 token 是静默空操作,已换成 -fno-builtin
[c-abi] builtins = "iso" 声明 C 库只提供 ISO 函数、没有厂商扩展。Apple 目标上
clang 的循环惯用法识别会把常量模式填充改写成 memset_pattern16 调用——那是一个
Apple libc 扩展,这样的库没有它。此前这里发的是 -fno-builtin-memset_pattern16。
在一份真实的 build.ninja 编译命令上做 A/B,只改这一个 flag:
| flag | memset_pattern16 引用数 |
|---|---|
| 照原样(flag 在) | 1 |
| flag 删掉 | 1 |
-fno-builtin |
0 |
-mllvm -disable-loop-idiom-memset |
0 |
而它报不出自己什么都没做。 clang 静默接受 -fno-builtin-totally_not_a_function:
-fno-builtin-X 这一族按 clang 的 builtin 表校验,而 memset_pattern16 是 LLVM
TargetLibraryInfo 的 libfunc,不在那张表里;发出调用的是 LoopIdiomRecognize,它查
TLI,按函数名的属性到不了它。
不选 -mllvm 的理由是它传的是 LLVM 内部选项,不是受支持的接口,改名或删除之后这套
机制会再次静默失效——那正是这次要修的缺陷本身。代价是量出来的:在暴露此事的那个翻译
单元(libarchive 的 7zip reader,-O2,aarch64-macos)上,目标文件从 38200 涨到
38888 字节,1.8%,因为 -fno-builtin 同时撤走了 C 库确实提供的那些 ISO 函数。这比
builtins = "iso" 声明的范围宽,而它宽在安全的方向:代码生成器不合成的调用不会变成
链接错误。
这个空操作能活下来,是因为它没有判据。 cenv 的探针用 -dM dump 校验自己发的
token,而代码生成阶段的性质在预处理器 dump 里不可见。判据现在在
.github/workflows/openkal-cross.yml,三条腿,跑在三台宿主上:
| 腿 | 内容 | 判据 |
|---|---|---|
| 1 | 不带任何 flag 编译探针 | 符号必须出现——否则探针已经触发不了惯用法,腿 2、3 什么都没测 |
| 2 | -fno-builtin-memset_pattern16 |
符号仍必须出现(钉住这个缺陷;若哪天红了,说明 clang 认了窄拼法,cenv 可以改回去把这 1.8% 拿回来) |
| 3 | mcpp 为 aarch64-macos 走 openkal 栈构建同一份源码 |
目标文件里零引用 |
腿 3 的对照:换回旧 token,同一个工程链接失败于
ld64.lld: error: undefined symbol: memset_pattern16。
更正:那个「四个成员」是二,而分组用错了依据
2026.9.21.2 的条目、cenv.cppm 与 predefines.cppm 的注释、docs/21 与 docs/22
都写着「借来的 __CYGWIN__ 代价是四个成员:archive、sqlite3、mimalloc、c-ares」。
撤销之后的重测把这个数目否掉了。
| 成员 | 记的真因 | 实测真因 | 撤销后 |
|---|---|---|---|
archive(经 xz) |
__CYGWIN__ |
对 | 清了 |
sqlite3 |
__CYGWIN__ |
对 | 清了 |
c-ares |
__CYGWIN__ |
错:#ifdef HAVE_WINDOWS_H,而那个宏由 mcpp-index 自己的配方在 windows 分支 #define |
仍红 |
mimalloc |
__CYGWIN__ |
错:已经不走到任何头文件——error in backend: Target OS doesn't support __builtin_thread_pointer() yet |
仍红 |
四个是按「诊断」分的组,不是按「真因」。 四个都停在 windows.h(mimalloc 当时如此),
于是被记成同一类。按诊断分组不是按真因分组,这样数出来的数目会高估一次撤销能修掉多少。
这条更正本身来自判据:重测把总失败从 10 降到 5、新增失败为零,而「windows.h 组 4→0」
这半条没有达成——是 4→2。一个达成了一半的判据,比一个没写的判据更有价值:它指出了
分母是错的。
mimalloc 暴露出替身三元组的一个代价,与宏无关
fatal error: error in backend: Target OS doesn't support __builtin_thread_pointer() yet.
presents = "posix" 在 Windows 上实现成 --target=x86_64-pc-cygwin。LLVM 没有为那个
OS 实现 __builtin_thread_pointer(),而 mimalloc 用它取线程局部堆指针。这是替身
三元组的第一个被测量到的、超出宏名之外的代价;先前关于这次替换的记录只讨论了预处理器
看到什么。
v2026.9.21.2
引擎拥有的宏改为全大写
__mcpp_target_<os>__ 成为 __MCPP_TARGET_<OS>__,__openkal__ 成为 __OPENKAL__。
拼法仍取自三元组自己的 os 字段,只是转为大写,引擎依旧不认识任何操作系统名。
业界的约定是按「名字是什么」分的,不是按谁写的分。 厂商名与产品名用大写
(__APPLE__、_WIN32、__MINGW32__、__GNUC__),系统种类名用小写(__linux__、
__unix__、__gnu_linux__)。mcpp 拥有的每一行都属于第一类:它命名的是 mcpp,或者
是 openkal。「是哪一种系统」由 __linux__ 那一族回答,而那一族是 mcpp 供给而非拥有
的,所以它们保持小写。
小写拼法在 2026.9.21.1 发布过一版,产生它的推理是:这些名字在真实守卫里与 __linux__
并排出现,跟它一致看起来就像一致性。那是把「相邻」当成了「同类」。 __APPLE__ 出现
在同样那些守卫里却是大写,因为它属于某个人。
__MCPP_ 是前缀而不是规则的全部。 __OPENKAL__ 同样是 mcpp 拥有的,但它命名的是
openkal 而不是 mcpp。test_predefines.cpp 里那条断言因此改为检查拼法约定(大写、
__ 包裹),而不再检查前缀——按前缀断言会把 __OPENKAL__ 判成违规,而它不是。
撤销一个宏的判据是一次数读者的测量
这张表里的一条是已发布的接口,而撤销一条是静默的:一个 #if 选了另一条分支,
照常编译。构建工具手里没有任何机制能让它变响。所以撤销不由读来决定,而由一次枚举
读者的测量来决定,数出多少就决定要走几步。
| 撤销的名字 | 数出的读者 | 步数 |
|---|---|---|
__CYGWIN__ |
四个第三方成员,外加本生态安装出去的两个头里的六处 | 三步 |
__mcpp_target_<os>__、__openkal__ |
零 | 一步 |
第二行的分母是全生态每一个仓库,逐文件类型扫过:没有一个源文件、没有一份清单读它们,
出现的地方只有引擎自己的发出处、它的测试与散文。两个名字都是在这里发明的,所以不可能有
上游代码握着它们;暴露窗口 __openkal__ 是三天,目标宏是一个发布。两次之间规则没变,
变的是数目。
新增判据 EveryTargetInTheRegistryYieldsAValidIdentifier:分母取自目标注册表本身而不是
测试旁边写死的一张名单,逐行解析并断言发出的宏是合法标识符——拼法既然取自 os 字段,一个
带点或带版本后缀的 os 就会产出编不过的宏,而失败会落在用户的构建里而不是这里。
__CYGWIN__ 撤销完成(三仓序列的最后一步)
Windows 上 presents = "posix" 的实现现在在编译行加 -U__CYGWIN__ / -U__CYGWIN32__。
--target=x86_64-pc-cygwin 本身保留——它供给 __unix__ 并压掉 _WIN32,那正是「呈现
POSIX」的含义;不需要的只是那个借来的名字。
30 成员测量判定这个名字的代价是四个成员(archive、sqlite3、mimalloc、c-ares 各自
停在 #include <windows.h>,经由 #if defined(_WIN32) || defined(__CYGWIN__))。上游用
它表达「Win32 可用」,mimalloc 把这句话写在守卫自己的注释里。
序列按仓库排序,而这个次序就是安全性论证本身:
- 2026.9.21.1 在借来的名字旁边加上 mcpp 自己的名字;2026.9.21.2 趁它还没有消费者时改成
大写。每一步都是纯增量。 [email protected]与[email protected]改读新名字,并保留
|| defined(__CYGWIN__),于是在本次改动两侧的引擎上都能构建。先于第三步发布。- 本版停止定义借来的名字。
先做第三步——本分支的一个更早修订就这么做过——会让已发布的那两个安装头全部落进各自的
#else。libunwind 的 static_assert 会响亮地红;setjmp.h 那一处不会,它自己的注释写着
「a mismatch nothing reports until the record overruns」。
残余窗口被点名而不是被说没有。 把 openkal-musl 精确钉在 0.18.0 或更早、同时把
引擎升过本版的工程,会拿到那个静默的 #else。在本版之前把索引的 latest 移到 0.19.0,
是把窗口压到「精确钉」的办法;这里没有任何机制能把它关掉,因为引擎无从知道一个包安装出去
的头读了哪些宏。
一条没人回答的 requirement 会说出来
一共三种情形、其中两种能构建:提供方陈述了 provides-interfaces 且包含该需求(构建);
陈述了但不包含(拒绝);什么都没陈述(构建,而在这行提示之前是静默的)。第三种是有意的
——provides-interfaces 晚于那些实现出现,一个还没采纳它的图必须照常构建——但它让
「是」与「没问成」同读数。
note kernel-abi interfaces: [email protected] states none, 2 requirements unchecked
提示点名解析出的那个实现:一个只被告知「有东西没检查」的读者无法据此行动。提供方确实
陈述了列表时这行不出现——tests/e2e/743 双向断言,新增的第四条腿正是为此:没有它,第三条腿
在一个无条件打印这行的引擎上同样会绿,那样就什么都没测到。
自举钉
.xlings.json 的 workspace pin 从 2026.9.20.1 走到 2026.9.21.1。
v2026.9.21.1
引擎定义的宏成为一份规范,而规范就是那个模块
src/toolchain/predefines.cppm 同时是契约与实现:契约是模块里的数据(kContract),
发出是它旁边的函数(define_tokens),tests/unit/test_predefines.cpp 双向断言两者
一致——发出去却不在表里、或在表里却没人发,都让构建变红。规范与实现分处两地就会漂移,
这一点本轮已经在理由令牌表上付过一次学费。
__mcpp_target_<os>__,覆盖所有平台而不只是 Windows。 拼法取自三元组自己的 os
字段,引擎不认识任何操作系统名——三元组解析器新增一个目标,它的宏随之存在。实测:
x86_64-linux-gnu → __mcpp_target_linux__;x86_64-windows-gnu →
__mcpp_target_windows__;riscv64-none-elf → __mcpp_target_none__。
总是定义,不只在有东西被压掉时。 条件式发出会让缺席含义不唯一:「不是 Windows」与
「是 Windows 但没有东西压掉它的宏」会读成同一件事。
命名:小写,__mcpp_ 前缀。 业界并存两套约定——厂商与产品名大写(__APPLE__、
_WIN32),系统种类名小写(__linux__、__unix__)——这些命名的是目标种类,在真实守卫里
与第二族并排。__mcpp_ 前缀承重:一个 mcpp 拥有的名字,语义由 mcpp 自己定。
__openkal__ 收进同一份契约;__unix__ 列在表里但标注「供给而非拥有」,保持标准拼法。
mcpp 为「这个目标是 Windows」给出自己的名字
presents = "posix" 在 Windows 上实现成 Cygwin 形状的目标,有意压掉 _WIN32——那正是
「呈现 POSIX」的含义。但 ABI 并没有跟着环境一起变:调用约定仍是 Win64,寄存器保存区
仍按它的大小。生态里有两个已安装的头按这个事实定尺寸——openkal-musl 的
bits/setjmp.h 定 jmp_buf,openkal-llvm-runtime 的 __libunwind_config.h 定
unw_context_t。已安装的头会被应用程序自己的编译读到,所以两者都用不了包私有的 define。
本版发 -D__mcpp_target_windows__。它是 mcpp 自己的名字,语义由 mcpp 自己定;只在这次
替换下发出,普通 Windows 构建仍有 _WIN64。
__CYGWIN__ 仍然定义着,这是次序不是结论。 30 成员测量已判定这个借来的名字代价是
四个成员(archive、sqlite3、mimalloc、c-ares 各自停在 #include <windows.h>,
经由 #if defined(_WIN32) || defined(__CYGWIN__));上游用它表达「Win32 可用」,
mimalloc 把这句话写在守卫自己的注释里。一个借来的名字,语义由借出方的历史决定。
撤掉它试过了,被跨仓库交叉验证挡下。 上面那两个头正是因为没有别的 target-wide 名字
才读它;撤掉后 libunwind 的 static_assert 响亮地红了,而 setjmp.h 那一处不会响——
它自己的注释写着「a mismatch nothing reports until the record overruns」。那次测量数的是
第三方读者,没数我们自己的。
于是撤销分三步,每个中间状态都能构建:本版增加新名字;两个包改读新名字并保留
|| defined(__CYGWIN__);之后的版本再停止定义借来的那个。先做第三步会让已发布的那
两个头全部落进 #else——错误的记录尺寸,没有任何东西报告。
v2026.9.20.1
校验探针量的是构建宿主,而不是它要核对的那个目标
2026.9.18.3 的 c-abi 校验探针在每一次 freestanding 构建上都没有选中目标。
Toolchain::crossTargetFlag 只为 hosted 目标设置(它自己的注释写明了原因:freestanding
目标的 --target 必须与随行的 ISA 标志一起给出,放两处就是同一个决定写两遍),而那另一处
是 mcpp.freestanding.linkline 的编译前缀,探针从不问它;cenv::realise 对 freestanding
也不产 --target。于是探针的命令行是 -D__unix__ -fno-short-wchar -ffreestanding -x c++ -E -dM -——没有任何目标选择,clang 回答的是它自己所在的那台机器。
在 Linux 宿主上这台机器恰好满足 __unix__ 已定义、_WIN32 未定义、wchar_t 32 位,
于是检查以错误的理由通过;在 Windows 宿主上它报 _WIN32 已定义、wchar_t 16 位,
两条不匹配同时出现。2026.9.18.3 把这两条读成"Windows 宿主的 clang 即使带上 --target=
仍注入宿主预定义",并据此加了 hostStripMacros。该归因不成立:clang 的预定义跟随目标
而不是宿主。本机实测——clang --target=riscv64-none-elf -dM 在 Linux 上 __linux__ 计数
为 0、__SIZEOF_WCHAR_T__ 为 4,而 --target=x86_64-w64-windows-gnu 在同一台 Linux 上
定义 _WIN32——如果那个 --target 真在命令行上,clang 会答 4 而不是 2。观察到 2,说明
它不在。
这一版从根因修:
- 探针拿到目标。 freestanding 目标的
--target与 ISA 标志由
mcpp.freestanding.linkline的编译前缀提供,现在进入探针 argv。 hostStripMacros删除,而删除本身是要点。-U_WIN32 -U_WIN64 -U__MINGW32__ -U__MINGW64__抹掉的正是"探针量错了机器"这件事的唯一证据。将来若真有宿主泄漏,它必须
到达不匹配报告,而不是在能被看见之前就被 undefine 掉。- 装配处拒绝这次遗漏,而不是调用点记得不要犯。 新增
cenv_probe::assemble_argv:各个部件在某些配置下都合法地为空,所以没有任何单独一个
能承载这条不变量。freestanding 目标的 argv 没有选中目标即拒绝并点名目标;宿主本地
构建接受没有目标选择的 argv——那里宿主就是目标,缺席是那个决定本身。
单测 test_cenv_probe.cpp 的三个 strip 测试被替换:它们测的是"-U 有没有到达命令行"
这个机制,而不是"探针量的是不是正确的机器"这条性质。新的五个 CenvProbeArgv 测试不需要
交叉工具链,直接对装配函数断言;另加两个带 clang 的端到端探针测试,在没有 clang 的宿主
上跳过而不谎报。
builtins 的 Windows 行:结论不变,写在旁边的机制是错的
cenv.cppm 称 clang 自带 intrin.h / mm_malloc.h 的问题"已由既有的 -nostdlibinc
隔离关闭"。实测不成立:-nostdlibinc 移除的是标准系统头目录,clang 自己的 resource
目录仍在(那是 -nobuiltininc 移除的),带着该标志仍复现 intrin.h:12:15——与 mcpp-index
为 fmtlib.fmt 记录的诊断逐字符相同。真正关掉这两者的是 Cygwin 式实现:mm_malloc.h:42
在 __MINGW32__ 上选 __mingw_aligned_malloc,没有它则落到 posix_memalign;而去取
<intrin.h> 的源码是在 _WIN32 之后才这么做的。调查结论不变(Windows 上没有可关的循环
惯用法内建),改的是写在它旁边的那句机制。
一个包可以陈述它需要该层的哪些接口,解析期回答
能力的"在不在"过去无处可问,于是全被挤到预处理期,而那比答案存在得更早——mcpp#674 的
全部压力来自这一格空着。openkal SPEC 0.14 §6.2 列出三个时刻并规定每个都是该信息最早
能存在的时刻;§3.3 撤回了它给接口集合起过的唯一一个名字(hosted),理由是"一个描述
环境类别的名字会被没有人想到过的那个环境证伪",替代做法是由消费者逐条列举。
新增 [kernel-abi] 表:
# 实现方(只有提供该层的包可以写 provides-interfaces)
[kernel-abi]
provides-interfaces = ["openkal.abort", "openkal.stream", "openkal.memory"]
# 消费方(任何包都可以写 requires-interfaces)
[kernel-abi]
requires-interfaces = ["openkal.fs", "openkal.net"]引擎不认识这两个集合的任何一个成员:对它们做的唯一操作是集合差
(targetside::interfaces_not_provided),因此某个规范新增一个接口不需要 mcpp 发版。
不满足即在编译任何东西之前拒绝,并同时点名缺的接口、要它的包、以及没提供它的实现——
只报"缺"会让读者自己去猜该改哪一边。
一个什么都没陈述的提供者,不是一个什么都不提供的提供者:实现方没有写
provides-interfaces 的图照常构建,链接仍以它一贯的词汇报告缺席。什么都不写的清单,
产出的命令行与这项能力存在之前逐字节相同。
拒绝记录的 reason 为 interface-not-provided,并且这个令牌也印在拒绝消息里,用方括号
包着,与 E0006 同一个约定——mcpp-index 的兼容性测量靠它把「这个图不供给这个成员所要的」
与「这个成员没能构建」分开,而一条只有人能认出的拒绝会逼迫那个消费者去匹配散文。
docs/50 的 reason 令牌表同时补上了 2026.9.18.1 起一直在发却从未列出的三个:
c-env-unrealisable、c-env-verification-mismatch、platform-dependency。
理由令牌表与引擎不再靠人读对齐
docs/50 的 reason 令牌表是机器接口:mcpp-index 的兼容性测量就是从拒绝里读一个
令牌,来区分「这个图没有提供该成员要的东西」与「该成员没构建成功」,而这条区分决定一个
会被发布的数字。引擎能发而表里没有的令牌,是一条没有人能依赖的承诺。
2026.9.18.1 那轮往这张表里补过「缺的那四个」;本轮枚举发现另外四个一直缺着——
apple-sdk-absent、lld-required-absent、host-tool-toolchain、std-module-precompile。
靠读来比较的集合,比较的是样本。
四条补齐,并新增 .github/tools/check_reason_tokens.sh:它双向比对
refusal.cppm 能发出的令牌与表里的行,并要求简体中文镜像携带同一个集合。
两个方向各去掉一条都会红。
| \reason` | |——第一格里一个反引号名字,形状与下面每一行 完全相同。按「行首反引号名字」匹配会把 reason` 当成一个令牌。行与列头的区别在第二格
非空,所以判据按性质挑对象,不按语法挑。
[c-abi-absent]:枚举例外,不枚举规则
一个 C 库供给的名字集合在清单里不可枚举(POSIX 约一千二百个),枚举它正是 §3.3 记录下
撤回的那个错误。例外是可枚举的——openkal-musl 的 README 列了六项,而那段散文没有任何
东西在执行它,并且已经被推翻过一次(0.16.0 之前 SIG_IGN 对每个信号都被接受却一个都没
安装)。
[c-abi-absent]
fork = { form = "link" }
mprotect = { form = "enosys", note = "openkal 没有作用于映射保护属性的操作" }
tcsetattr = { form = "accepted-no-effect", note = "openkal 不命名的那些字段不被施加" }form 必填且封闭。link 是 openkal 自己的能力模型对实现所要求的形状(§6.1 把运行期
报告不支持称为缺陷);另外两个是对它的偏离,给它们命名是为了让一次偏离成为可以被数出来
的东西。链接点到 link 形状里的某一项时,mcpp 把清单读回来:undefined reference to 'fork' 因此带着那句说明它是缺陷还是环境限制的话一起到达。
同一形状的第二处:拒绝消息里那行标签可以被读成它所否认的那句话。 缺失的接口列在
上面,紧接着一行 provided by fakekernel (2 interfaces)——读起来正是
「openkal.space 由 fakekernel 提供」,而这条拒绝存在的理由恰恰是它没有提供。改成
「the resolved implementation is fakekernel (2 interfaces), and none of those listed
above is among them」。e2e 743 此前的每一条断言都只匹配标识符,而标识符在两种措辞下
都在正确的位置;现在它断言整句,并显式拒绝旧措辞。
这条说明此前少一个右括号,而九个单测都没看见。 渲染出来是
the C library in this graph (musl declares that it does not supply ...——C 库的名字由
两个各自独立的条件插进去:一个左括号,然后是名字,右括号从来没有被发出过。单测断言的
是 find("musl"),那句话里同样有 "musl"。判据瞄准一句话的子串,就看不见这句话。
修法是整个括号部分只做一次替换;新增的 e2e 744 真跑一次链接失败并断言整句,把右括号去掉
它就红。
它是顶层表,而这是量出来的。 先写成 [c-abi].absent——更顺——之后拿真正发布的
2026.9.18.3 归档(当时的索引 floor)跑 openkal-musl 0.17.0 将要发布的那份清单:嵌套
写法让每个旧 mcpp 在每个目标上拒绝整份清单,报 [c-abi] has no member 'absent'。
[c-abi] 枚举自己的成员并拒绝其余,而这条严格性是对的——拼错的 presents 不该静默
关掉一条声明。同一次测量里,未知的顶层表被忽略,构建照常完成。
这张表做的每件事都是诊断性的:给一次已经失败的链接加一句话,没有任何 flag、链接行
或产物依赖它。于是忽略它的引擎产出的正是它今天产出的那条链接错误;而拒绝它的引擎会把
索引 floor 逼到本版本——为了一句他们无非是收不到的说明,夺走停在其下的每个客户端手里的
整个索引。改成顶层表后,openkal-musl 0.17.0 不要求任何 floor 变动。
把 absent 写在 [c-abi] 里面会被拒绝,拒绝消息点名顶层的那个拼法。
汇编器先问 PATH 再看沙箱,而其余每一个工具都反过来
find_usable_nasm 先 which("nasm"),沙箱里那份钉住的只作兜底。于是装了汇编器的
机器用它自己的那一份,没装的下载钉住的那一份——三台机器可以从同一棵源码树产出三份
不同的目标文件,而两次构建里都没有一行说它用的是哪一个。引擎里其余每一个工具都不是
这个方向:编译器与链接器是载荷,C 库与 C++ 运行时是包,ninja 与 patchelf 走 xlings,
ar / strip / objcopy 由解析出的工具链自己的目录派生且从不是裸名。
顺序反过来:先沙箱,后宿主。宿主那一份保留,因为一台离线而本来就装了可用汇编器的
机器仍然应当能构建——但被用到时构建会点名它:
degraded: the assembler for this build is the host's ('/usr/bin/nasm'), not the one this engine pins
docs/20 新增一节列全 mcpp 在宿主上取的每一项与各自的理由。一个宿主工具到达构建
本身不是缺陷,静默地到达才是——所以那是一张表而不是一条禁令。
presents 的取值集冻结
docs/22 写明:presents 回答的是源码看到哪些环境身份宏,不回答任何能力是否存在。
一个包不得由它推断某个接口、某个头或某个路径是否可用。取值集不增长,理由与 openkal 为
自己的核心集封闭所给的相同——一个描述环境类别的名字会被没有人想到过的那个环境证伪,
而 openkal 把自己发过的唯一一个这样的名字在一个发布周期之内撤回了。
(src/toolchain/cenv.cppm、src/toolchain/cenv_probe.cppm、src/build/prepare.cppm、
src/build/ninja_backend.cppm、src/build/refusal.cppm、
modules/manifest/src/{targetside_model,toml,types}.cppm,
单测 test_cenv_probe.cpp、test_manifest.cpp、test_targetside.cpp、
test_build_flags.cpp,e2e tests/e2e/743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh,
docs/22 及其 zh 镜像,modules/versioning/src/version.cppm、mcpp.toml)
v2026.9.18.3
Windows 主机 × freestanding 目标的 c-abi 校验探针两处真实缺陷被关掉
2026.9.18.1 发布的几小时内,openkal-llvm-runtime#24 的 CI 在 Windows 主机 × riscv64-none-elf
(freestanding) 一行红——探针报两条不匹配,都是真实存在的结构性缺陷,根因都在测量一侧而
不在声明一侧:
_WIN32声明 undefined,实测 defined。Windows 主机的 clang 即使带上--target= riscv64-none-elf仍把_WIN32(及__MINGW*__一族)注入预处理器的输出——hosted
三元组的--target=替换会改写主机宏,freestanding 不会。__SIZEOF_WCHAR_T__声明 32,实测 16。cenv::realise之前对 freestanding 的 wchar
默认值做了一个未经实测的假设:工具链默认就是 32 位,因此wchar = 32的声明在
freestanding 上不补-fno-short-wchar。Linux/macOS 主机下这个假设恰好成立,Windows
主机下不成立——clang 用 MinGW 的<winnt.h>默认,即使--target=riscv64-none-elf
也给出 16 位。
两条都是测量侧缺陷:探针应当读到的是「引擎根据声明生成的编译命令实际产生什么」,而不是
「引擎的编译命令加上主机泄漏之后产生什么」。这一版分别从两个角度修:
- 引擎补
-fno-short-wchar(即实现逻辑的修正,不是 workaround)。 既然「freestanding
默认 32 位」的假设是错的,那么正确的策略就是不论宿主是什么、freestanding 与否,
wchar = 32一律加-fno-short-wchar——这样编译产物确实就是 32 位wchar_t,
探针再去量也是 32 位,声明成立。在 Linux/macOS 主机下加这个令牌是冗余的(它们本来就
是 32),但对的结果不变;在 Windows 主机下是必要的。原先的「freestanding 跳过 wchar
令牌」规则由此被取消,在mcpp.toolchain.cenv::realise中注释清楚。 - 探针允许调用方剥离主机宏。
cenv_probe::verify增加一个hostStripMacros
参数——一组-U<name>令牌,在-E -dM之前前置。Windows 主机下,调用方把这四个
名(_WIN32、_WIN64、__MINGW32__、__MINGW64__)传给探针;Linux/macOS 主机下
传空集合。缓存键把这些令牌一起折进去,所以同编译器、同 argv、不同剥离集合不会共享
缓存槽。
新增 test_cenv_probe.cpp 中三个测试,分别钉住:剥离后宏不再出现在 dump 里、不同剥
离集合不共享缓存槽、Windows 主机的剥离集合恰好是那四个实测到的名字。
test_cenv.cpp 中新增两个测试,分别钉住:wchar = 32 在 freestanding 上也产出
-fno-short-wchar,wchar = 16 在 freestanding 上产出 -fshort-wchar——把「
freestanding 跳 wchar 令牌」这条曾经存在过的规则永久挡在回归测试之外。
包侧四处尝试均失败:CI 跳过(workaround,用户拒)、去掉 freestanding 上的 musl 依赖(破坏
libcxx 的 <__mbstate_t.h>)、per-target [c-abi] override(被解析但未生效——[target.'cfg(..)'.build]
的合法键集合是 BuildInputs 的成员,不接受 c-abi 块)。这一版把这两条结构性缺陷收进了
引擎里,而不是把它们推到包层。
(src/toolchain/cenv.cppm、src/toolchain/cenv_probe.cppm、src/build/prepare.cppm,
测试 test_cenv.cpp(FreestandingWchar32AlwaysEmitsNoShortWchar、
FreestandingWchar16AlwaysEmitsShortWchar)与 test_cenv_probe.cpp(
AHostStrippedMacroIsAbsentFromTheDump、
AStripListDoesNotShareACacheSlotWithAnEmptyStrip、
TheWindowsHostStripListNamesExactlyTheFourMeasuredLeaks)、modules/versioning/src/version.cppm、mcpp.toml)
v2026.9.18.2
2026.9.18.1 发布几小时内,三个下游仓库的 CI 揭出的三处同形缺陷:声明满足与做不到被当成了一回事
三处缺陷,同一个形状:引擎把「目标默认已经满足这条声明」和「这条声明做不到」当成了同一种
情况来处理,结果要么拒绝了一个根本不需要拒绝的编译器,要么拒绝了一个其实能实现的目标。
- GCC 在 Linux 上被拒绝,仅仅因为声明了
[c-abi],即便实现出来的东西是空的。
openkal-musl 0.15.0 在 Linux 上的声明——presents = "posix", data-model = "arch-default", wchar = 32——正是 x86_64 Linux 的默认三元组本来就是的样子,实现出来
不需要任何令牌。旧的检查顺序是先问「这是不是 Clang」,再算实现——所以只要图里出现
[c-abi]就先把非 Clang 编译器拒了,不管实现到底需不需要它做什么。每一个用 GCC 在
Linux 上构建 openkal-musl 的用户,因此白白丢了这个包。现在cenv::realise先跑(它是
声明与目标的纯函数,与编译器无关),只有在结果令牌非空时才检查编译器族——Windows 上的
Cygwin 式替换仍然是非空的,所以那条路径的拒绝没有变。 - macOS 默认并不满足
presents = "posix",校验探针当场抓到了这一点——这正是它存在的
意义。 探针报告:声明__unix__已定义,实测未定义。Apple 的 clang 默认预定义的是
__APPLE__/__MACH__,从来不是__unix__;最初设计文本假设 macOS 的默认三元组像
Linux 一样已经满足,这个假设是错的。 - 裸机(freestanding)目标对
presents = "posix"一律拒绝,openkal-llvm-runtime 因此
在riscv64-none-elf上还没碰到别的目标就先被挡下来了。
三处修成一条规则,也是比它取代的那条更简单的表述:实现 presents = "posix" 在每个目标上
陈述的是同一件可观察的事实——__unix__ 已定义,_WIN32 未定义——目标之间的差别只在于
实现它要花多大代价。Linux:不花代价。macOS 与裸机:定义 __unix__,一个令牌。Windows:
Cygwin 式替换。已知的权衡,留给 mcpp-index 30 个成员的实测去称量:写成
#ifdef __unix__ ... #elif defined(__APPLE__) 的可移植代码,现在在 macOS 上也会走 Unix
分支。
(src/toolchain/cenv.cppm、src/build/prepare.cppm,单测 test_cenv.cpp(
MacosPosixArchDefaultDefinesUnix、FreestandingPosixDefinesUnix、
FreestandingWindowsIsStillRefused)与 test_cenv_probe.cpp(令牌与探针互相印证的一例),
e2e tests/e2e/742_gcc_accepted_when_the_realisation_is_empty.sh,docs/22 及其 zh 镜像)