Skip to content

Latest commit

 

History

History
1097 lines (877 loc) · 64.1 KB

File metadata and controls

1097 lines (877 loc) · 64.1 KB

22 —— 目标侧

读者: 要用同一份 manifest 服务多个目标的作者。

本章回答的那一个问题: manifest 怎样说「只在那里」,以及哪些东西可以这样被 条件化。

不在这里: 目标名的词汇表,见 21 —— 目标三元组; 加速器这条轴在图之后才解析,属于 42 —— 异构硬件构建

一次构建在发出任何命令行之前,必须先回答一个问题:目标的编译器运行时、 平台接口、C 库与 C++ 运行时各自来自哪里。mcpp 在依赖图解析完成之后解析该问题 一次,其后的每一个阶段读取同一个已解析的值。

本文规定这一模型、约束它的规则、工程需要书写的内容,以及包需要声明的内容。

五个层

一次构建的目标侧由五个层构成。

内容 实现举例
compiler 执行编译的程序 llvmgccmsvc
compiler-runtime 编译器自身的运行时:整数与浮点 builtins、展开器 compiler-rtlibunwindlibgcc
kernel-abi 平台接口或其等价物 linuxwindowsdarwinopenkal
c-abi C 库 glibcmuslpicolibcucrtlibSystem
c++-abi C++ 库及其 ABI 运行时 libc++libc++abilibstdc++、MSVC STL

成为层的判据

一个部件成为层,需要三个条件同时成立:至少存在两个可互换的实现;它可以独立于 相邻层被替换;它与其下方的层之间存在一种确定的「曾为谁配置」关系。三者缺一, 该部件就属于相邻的层,而不是独立的一层。

compiler-runtime 独立于 c++-abi,因为 builtins 是一个 C 程序本身需要的东西。 把它算作 C++ 运行时的一部分,等于断言 C 程序不需要整数除法,而这一断言已经 产生过一次实测缺陷:一个交叉编译到 macOS 的 C 程序被问及是否存在 C++ 运行时, 答案是不存在,链接行因而保留了编译器载荷自带的 libc++

kernel-abi 在传统技术栈上没有名字——在那里,C 库直接发出系统调用, 或直接调用平台入口。给这条接缝命名,正是一份 C 库实现得以坐落在多个平台之上 的前提。

四种来源

每一层由四种来源之一供给。

来源 含义 可知时刻
payload 编译器载荷自带 依赖解析之前
prebuilt 一份被点名的预制载荷供给 依赖解析之前
graph 依赖图中的包供给 依赖解析之后
无人供给,且这是一句陈述

四种来源中,两种在依赖解析之前可知,两种只在其后可知。因此目标侧只在图存在的 那一刻解析一次。更早的任何推断,都是对一个尚不存在的事实所作的推断,而对同一 事实的多处独立推断不会互相一致。

缺席的层是一个答案,不是一处缺口。裸机目标没有内核;不依赖任何 C 库的工程 没有 C 库。

三条规则

每层恰好一个供给者

C 库、平台接口与 C++ 运行时是互斥的选择,不是可叠加的贡献。同一层出现两个 供给者是错误,在解析期报出,并同时指出这两个包,以及各自进入图的路径。

严格性的依据是失败的模式:选错供给者不会使链接失败,它产出的是一个能够运行、 却间歇性不能运行的程序。

为其下方的层配置过

一个实现只有在它曾被配置的那个层之上才可用。一份 libc++ 构建把这项配置 记录在自己的 __config_site 中;一份 libgcc 构建是为 GCC 配置的。这一关系 由声明得出,而非由推断得出——见下文的 requires

由此得出两条推论。编译器载荷的 C++ 运行时,只有在 C 库同样来自该载荷时才可用。 编译器运行时必须属于编译器自身所在的族,因为如果两者不一致,这次构建解析 __udivti3 的方式,会与同一程序中其它每一次链接不同。

跨来源接线

引擎只在两层来自不同来源时才为它们接线。

组合 关系的表达方 引擎
两层均来自 graph 包之间的普通依赖 不介入
两层均来自 payload 载荷自身一致 不介入
一层预制、一层来自图 只有引擎同时知道两边的地址 需要接线

因此,把一层从预制载荷移入依赖图,减少的是引擎的工作,而不是增加。这正是 一份源码能够在引擎不作任何改动的情况下到达多个平台的机制所在。

层名固定,实现不固定

五个层名是编译进引擎的一个闭集。填充它们的具体实现出现在包清单与索引中, 不出现在引擎的任何一行代码中。

层名可以固定,因为层由 C 与 C++ 的构建模型决定,并不增长。实现不能固定, 因为增长正是实现要做的事:一个生态的组合数是其实现数之积,而包数只是其和。

工程书写的内容

层名不出现在工程清单中。工程通过三个既有机制来表达自己的目标侧。

目标三元组

--target <三元组>,或 [build] target。OS 段选择平台接口;env 段陈述的是 对 C 库的一条请求——它是请求而非答案,解析出的值由构建自行报告。

省略该段即为不作陈述:x86_64-linux 请求「供给该层的任何实现」, x86_64-linux-musl 请求 musl。依赖图供给了另一个实现时,以图为准,构建会 报出该名字不准确,并给出应当使用的拼写。这条请求是被忽略,而不是被违反, 因此两种写法下产物相同。

工具链

mcpp toolchain default <族>@<版本>、清单中的 [toolchain],或针对单一目标的 [target.<三元组>].toolchain。它选择 compiler 层——五个层中唯一一个任何包都 不能供给的层。

目标表的一行可以携带一条约定:其载荷供给该目标的 C 库的工具链。这条约定在 两个条件同时成立时生效:清单对该目标未作陈述,依赖图中没有任何东西 供给该目标的系统。第二个条件只有在解析之后才可知,因此工具链在那之后解析, 而不在那之前。

依赖

其余每一层都通过依赖一个供给它的包来选择。一条依赖可以供给多个层,也可以 通过它自身的依赖带来更多供给者。

[dependencies]
openkal-llvm-runtime = "0.1"

构建报告的内容

构建打印它解析出的结果。清单中的一行陈述的是一个意图,该意图会在其下方的包 发生变化时过期;报告陈述的是结果,因而不会过期。

默认情况下,报告只列出编译器载荷未供给的那些层。一次零配置构建的五层全部 解析自同一份载荷,五行 (payload) 回答的是一个没有人提出的问题。

      Target x86_64-linux-gnu
      Target x86_64-windows-gnu → x86_64-w64-windows-gnu
             kernel-abi        openkal        (openkal-windows@0.1.3, graph)
             c-abi             musl           (openkal-musl@0.3.3, graph)
             c++-abi           libc++         (openkal-llvm-runtime@0.1.1, graph)

MCPP_VERBOSE=1 列出全部五层。诊断信息始终列出该判断所依据的每一层, 包括其中平凡的部分,因为一条省略了自身证据的错误信息,读者无法核实。

接口与实现是两列分开写的。openkal 是一个接口,openkal-windows 是它的一个 实现;把二者合并会掩盖一份源码为何能够到达多台机器。

闭包可见性

上面这份五层报告回答的是「每一层来自哪里」,不回答图中「还有哪些别的包」—— 一个绑定到某个平台 SDK 的依赖,对这份报告而言和普通依赖一样不可见。两种手段 补上这个缺口(设计 2026-09-18 §6)。

什么算平台依赖,精确定义。 一个包带来平台依赖,当且仅当它自己这样说—— provides = ["platform-sdk"],一个普通的、不带命名空间前缀的能力。这里的一切 都不是从头文件路径、链接 flag 或某个依赖的 visibility 推断出来的:推断 会带来与保留前缀 mcpp: 为五层所要避免的完全同一种「拼错即静默失效」的失败 模式,只是这次用在了这个引擎本来就无法直接观测的事实上。 06 —— 平台 SDK 依赖保持私有 是这样的包自己的清单所遵循的模式。

报告。 构建的 Target 报告新增一行,点名图中每一个声明了 platform-sdk 的包,没有则为空:

      Target              platform-deps     —
      Target              platform-deps     some.windows-headers@1.0.0

按与五层相同的可见性规则打印:只在有内容可报告时打印,或在 MCPP_VERBOSE 下总是打印。

拒绝开关。 [build] platform-dependencies = "refuse" 在图中存在这样的包 时直接让构建失败——这是「本次构建完全是基于其 kernel-abi 实现的闭包, 不多不少」这句话的机器可核验形式:

[build]
platform-dependencies = "refuse"

唯一接受的取值是 "refuse";不写(默认)则允许平台依赖,即今天的行为。

包声明的内容

provides

供给某一层的包,在保留前缀 mcpp: 之下声明这件事。

provides = ["mcpp:compiler-runtime=compiler-rt", "mcpp:c++-abi=libc++"]

语法为 mcpp:<层>[=<实现>]。层名对照闭集校验;拼写错误是一个错误,而不是一种 被静默禁用的行为。前缀之外的名字属于特性系统,原样透传。

mcpp:compiler 可以被 require,不能被 provide。编译器是这个引擎安装并驱动的 一份载荷,而族与族之间的差异——flag 拼写、模块模型、BMI 格式、驱动配置文件—— 是引擎必须持有的事实,不是包能够描述的数据。

requires

requires 是对称的另一半,也是在引擎中不出现任何实现名的前提下,执行分层 规则的机制。

requires = ["mcpp:compiler=llvm"]

libc++ 源码构建出的 C++ 运行时,其编译与其模块的编译都由 Clang 完成。 这一事实属于该包。引擎检查一条它能够一般性地陈述的关系——被命名的层必须 解析为被命名的实现——并通过同时指出二者来报出不匹配;一张编译进引擎的族表, 对它从未听说过的族做不到这一点。

这项检查在编译开始之前运行。它所拒绝的组合,若不加检查,会在该运行时自身的 头文件深处失败,报错信息命名的是一个读者从未打开过的文件,以及一个 mcpp 从未做出的决定。

同一条陈述也是这样一类包的做法:其安装钩子从源码编译出一个静态库。这样的库 是针对某一个 C++ 标准库编译的,无法链接进使用另一个标准库的程序;而它安装到 的那个存储目录只按包名与版本取键,并不区分这一选择。因此这类包声明它所针对的 实现:

requires = ["mcpp:c++-abi=libstdc++"]

工具链解析出另一个 c++-abi 的工程随后会被拒绝,拒绝信息同时指出两个实现, 而不是在链接时才失败。这项检查在工具链解析之后进行,而工具链在依赖图安装之后 才解析,因此检查发生时安装钩子已经运行过;钩子收到本次构建的目标,却收不到 工具链的取值(见 32 —— 编写载荷)。钩子不得把 另一种变体构建进同一个存储目录,否则第一个消费者就会替之后所有消费者决定 变体。

c-abi 包陈述它呈现的 C 环境(mcpp 2026.9.18+)

传统技术栈不需要陈述这件事:编译器载荷的目标三元组已经蕴含了 C 程序所编译 针对的环境。一旦某个包取代载荷供给 C 库,这一点就不再成立——openkal-musl 在 x86_64-windows-gnu 上生成 PE/Win64 代码,却向源码呈现 POSIX 环境,因为它是 musl 的一个移植版,它之上每一处 #ifdef _WIN32 问的都是错误的那一层。 [c-abi] 块是 C 库一次性陈述自己到底呈现了什么的方式——且只有供给该层的包 才可以陈述它。

# openkal-musl's manifest
[package]
provides = ["mcpp:c-abi=musl"]

[c-abi]
presents   = "posix"        # posix | windows | none
data-model = "arch-default"  # arch-default | lp64 | llp64 | ilp32
wchar      = 32               # 16 | 32
builtins   = "iso"            # iso | platform (default platform)

谁有资格声明。 写了 [c-abi]、却没有在 provides 里列出 mcpp:c-abi=<impl> 的包,是在陈述一件自己并不供给的层的事实——这总是错的, 而不只是不寻常,因此在清单解析阶段即被拒绝,并指出缺失的那条 provides 条目。

四个键,及各自封闭的取值集合。

取值 回答
presents posix / windows / none 源码看到哪一族环境身份宏(__unix__、还是 _WIN32、还是都不定义)
data-model arch-default / lp64 / llp64 / ilp32 long 有多宽
wchar 16 / 32 wchar_t 有多宽
builtins iso / platform(默认) 编译器是否可以假定平台 C 库自身的扩展在场

presentsdata-modelwchar 都没有默认值:块中漏写其中任何一个都会被 拒绝,并指出缺失的键——「没写」不等同于三个封闭取值中的任何一个。只有 builtins 有默认值 platform,即今天的行为。未知的键或未知的取值永远是 解析错误,并点名该键——绝不静默忽略。一个不声明 [c-abi] 块的包不改变 任何东西:解析出的目标侧、每一条编译命令、每一个缓存键,都与这项能力出现 之前逐字节相同。

presents 冻结在三个取值,而且它不回答任何关于能力的问题。 它陈述的是 源码看到哪些环境身份宏,不陈述任何接口、任何头文件、任何路径是否可用;由它 推断出这些内容的包,读到的是一个它没有被给予的事实——一个在 Windows 目标上 呈现 POSIX 的环境没有 fork、没有 /proc、没有 epoll,它陈述这件事的方式 是寻常的那一种:相应定义在链接期缺席。这个取值集不再增长,理由与 openkal 自己给出的、为它的核心集封闭所给的理由相同(SPEC 0.14 §3.2):一个描述环境 类别的名字,会被没有人想到过的那个环境证伪,而 openkal 已经把自己发布过 的唯一一个这样的名字(hosted)在一个发布周期之内撤回了。在这里再加第四个 取值,就是再造一次那个名字。一个包要知道某项能力在不在,答案要在它最早存在 的那个地方回答——依赖解析,由下文的接口枚举给出。

三件事互不蕴含,这是刻意分开的:presents 决定源码走哪条分支, data-model/wchar 决定 ABI。POSIX 不蕴含 LP64(32 位架构上是 ILP32), LP64 也不蕴含 POSIX。

实现(realisation)。 一旦 c-abi 层解析到声明了这个块的包,mcpp 就把 这条请求转换成编译器配置,施加到目标侧的每一个编译单元——C 库自己、C++ 运行时、编译器运行时的 builtins,以及图中所有普通包——覆盖 C、C++、汇编 编译,依赖扫描,以及 std 模块预编译。汇编(.S/.s)要做到这一点需要 自己单独的一条广播通道:.S 单元的命令行是独立组装的 (mcpp.build.flags::CompileFlags::as,不是 ::cc/::cxx),而且它有意 只从包的 C flags 里取出 -D/-U/-I 这一子集——对 C 编译器有意义的 -std=-O 之类 flag,对 GAS 毫无意义——所以实现出来的环境令牌(--target=-f[no-]short-wchar,以及 builtins = "iso" 添加的部分)要原样再广播一次, 进入这条更窄的通道(openkal-musl 尖峰实验发现并修好的缺口:同一个包里,.c 单元看到 _WIN32 未定义,.S 单元却仍看到它已定义——真实代码,比如 okm_setjmp.S 与上游 libunwind 的 assembly.h,正是按这个宏来挑选寄存器 保存集的)。mcpp 保存的是一份「请求到三元组与开关」的映射表,是一份不含任何 C 库名的通用知识:

目标 请求 实现
Linux posix / arch-default 默认三元组已经满足
macOS posix / arch-default 一个令牌,-D__unix__——Apple 的 clang 在默认三元组上预定义的是 __APPLE__/__MACH__,从来不是 __unix__
freestanding(裸机) posix / arch-default 同样一个令牌,-D__unix__,理由相同:这里同样没有任何东西定义它
Windows posix / arch-default 采用 Cygwin 式语义:只在编译行加 --target=x86_64-pc-cygwin;再加 -D__MCPP_TARGET_WINDOWS__(见下方说明);data-model 变为 LP64 是三元组切换的结果,不是另一个开关
任意目标 builtins = "iso" 关闭代码生成阶段假定平台 C 库在场的惯用法识别——在 Apple 目标上发出 -fno-builtin,因为按函数名逐个开关的那种拼法,对唯一重要的那个惯用法实测是静默空操作memset_pattern16 是 LLVM TargetLibraryInfo 的 libfunc,不是 clang 的 builtin);A/B 对照与实测代价见 src/toolchain/cenv.cppm
其余情况 明确拒绝,点名目标、请求与缺什么——绝不静默降级

macOS 与 freestanding 这两行是一次修正,不是设计原文(协调者修订, 2026.9.18.1 发布不到一天就被真实构建抓到)。 实现 presents = "posix" 所遵循的规则是:在每一个目标上陈述的都是同一件可观察的事实——__unix__ 已 定义,_WIN32 未定义——目标之间的差别只在于实现它要花多大代价。原文假设 macOS 的默认三元组已经像 Linux 一样呈现了它;真实构建上的校验探针把这个假设 当场抓包为假——声明已定义,实测未定义——这正是那个探针存在的意义所在, 不是它的漏洞。原文还把 freestanding 目标对 presents = "posix" 的任何请求 一律拒绝,这挡住了整整一个目标(riscv64-none-elf,openkal-llvm-runtime), 而不是去实现这个引擎确实能够给出的事实:freestanding 目标一开始两个宏都没有 定义,和 macOS 默认三元组一模一样,所以这处修复付出的是同一个令牌的代价。 已知的成本,留给 mcpp-index 三十个成员的实测去权衡:写成 #ifdef __unix__ ... #elif defined(__APPLE__) 的可移植代码,现在在 macOS 上 也会走 Unix 分支,只有那条分支本身在 macOS 上也写对了才是对的。

编译器只在实现真的需要一个它拿不出来的令牌时才被拒绝——不是仅仅因为声明了 [c-abi] cenv::realise(上文)先运行,是声明与目标的纯函数;只有 Clang 才行的这条要求第二个才检查,且只在结果令牌非空时才触发。openkal-musl 自己在 Linux 上的声明——presents = "posix", data-model = "arch-default", wchar = 32——实现出来的是空(上表第一行),所以 GCC 在那里被接受, 校验探针照常核实,和任何其他空实现一样。Windows 那一情形没有变:Cygwin 式 替换是非空的,所以一个非 Clang 编译器在那里仍然被拒绝,并被点名它做不到什么。

Windows 那一行是旗舰情形:x86_64-w64-windows-gnux86_64-pc-cygwin 产出的机器码完全一致——同样的 PE 格式、同样的 Win64 调用约定、同样的 SEH—— 差别只在预处理器看到什么,以及 long 有多宽。因此实现只触及编译行; 链接行保持图解析出的那个三元组,因为目标文件格式没有变化。

__MCPP_TARGET_WINDOWS__ 在这里定义,而 __CYGWIN__ 仍然定义着 (2026.9.21.1;这个名字在那一版是小写拼法,2026.9.21.2 在它还没有消费者时 改成了这个拼法)。

这次替换有意压掉 _WIN32——那正是「呈现 POSIX」的含义。但ABI 并没有跟着 环境一起变:调用约定仍是 Win64,寄存器保存区仍按它原来的大小。这个生态里 有两个已安装的头按这一事实定尺寸:

头文件 它定尺寸的记录
openkal-musl port/include/bits/setjmp.h jmp_buf
openkal-llvm-runtime __libunwind_config.h unw_context_tunw_cursor_t

一个已安装的头会被应用程序自己的编译读到,所以两者都用不了包私有的 define。__MCPP_TARGET_WINDOWS__ 是 mcpp 为它们所问的那个问题给出的自己 的名字——这个目标是不是 Windows,无论它之上呈现的是什么 C 环境——既然是 自己的名字,它的语义就不由别人的历史决定。它只在这次替换下发出;一次普通的 Windows 构建仍然有 _WIN64

__CYGWIN__ 仍然定义着,这是一个先后次序的结果,不是要留下它的决定。 三十成员测量已经判定,这个借来的名字的代价是若干成员:它们都停在 #include <windows.h>,经由 #if defined(_WIN32) || defined(__CYGWIN__) 到达。上游用它表达的意思是「Win32 可用」——sqlite3 把它列在 SQLITE_OS_WIN 之下。一个借来的名字,其含义由出借方的历史决定, 不由借用方的意图决定。

这个数字曾经是四,现在是二(2026-09-21 更正)。archive(经由 xz)与 sqlite3 读这个名字,都在撤回它的那次发布上被清理掉了。c-aresmimalloc 被归入同一组,是因为四者都停在 #include <windows.h>,而两者 都不是这个名字造成的:c-ares 是经由 #ifdef HAVE_WINDOWS_H 到达那个头的, 这个宏由 mcpp-index 自己的配方在它的 Windows 分支里定义;mimalloc 则根本 不再到达任何头——它在代码生成器里失败,报错是「目标 OS 还不支持 __builtin_thread_pointer()」,这是替代三元组本身的性质,而不是任何宏的 性质。按诊断信息分组,不等于按成因分组。

撤掉它试过了,结果打断了上面那两个头——它们读它,正是因为没有别的 target-wide 名字可用。libunwind 的 static_assert 响亮地报了错;setjmp.h 那一处不会——它自己的注释写着「不会有任何东西报告这处不一致,直到记录溢出 为止」。所以撤销分三步,每个中间状态都能构建:

  1. 2026.9.21.1 增加 __MCPP_TARGET_WINDOWS__——纯增量;
  2. 那两个包改读新名字,保留 || defined(__CYGWIN__),以便在两种引擎上都能 构建;
  3. 之后的版本再停止定义借来的那一个。

先做第三步,会让已发布的那两个头全部落进各自的 #else:记录尺寸是错的, 而没有任何东西报告这件事。

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

[package]
provides = ["mcpp:kernel-abi=openkal"]
# no [package] c-environment line — the boundary is inferred from `provides`

为什么要推导,而不只是提供这个开关。 开关本身没有问题;它做不到的是 追溯性地修好一个已经发布、却没写这个开关的包。openkal-windows 0.8.0、 openkal-macos 0.10.0、openkal-linux 0.13.0,以及未来任何一个 kernel-abi 实现, 都能因此把边界做对——不需要新发版本,也不需要跨仓库协调版本号——因为 provides = ["mcpp:kernel-abi=<impl>"] 正是它们本就已经声明的那一个事实。 这里堵住的失败是实测出来的,不是假设:openkal-windows 和图里其余部分一样在 POSIX 替换下编译,-fno-short-wchar 给了它 32 位的 wchar_t,而它调用的 Win32 接口回传的却是真正的 16 位 UTF-16——于是一个 wchar_t* 循环把两个 UTF-16 码元读成了一个码点。把这条边界变成默认值,而不是一个包必须记得去写 的清单键,让这一类失败变得无法被表达,而不只是被记录在文档里。

优先级:包自己清单里显式写的 c-environment 永远赢过推导。 推导只在包 什么都没写时才填 cEnvironment——如果一个包终究还是需要呈现的那个环境, 它仍然可以显式这样声明(今天还没有办法反着写「不是 platform」,因为 "platform" 仍是这个键唯一接受的取值)。这个显式键也仍然是 §5.3 另一类 情形——不是 kernel-abi 边界、但自身确实有平台绑定单元的普通包——唯一的手段, 那种情形下作者做出的确实是 mcpp 无法推导出来的选择:

[package]
# an ORDINARY package, not a kernel-abi provider — the engine cannot infer
# this one; the author states it because platform-bound units are a real
# minority of what this package builds (design §5.3)
c-environment = "platform"

这是一条边界规则,由这一个开关记录下来,而不是由引擎强制执行的:这样的包对 图其余部分暴露的接口,仍然只能用定宽类型跨越(SPEC §5.4)。

声明被校验,而不是被信任。 一条声明要经过核对,绝不直接被信任——这与 openkal 核对自身一致性声明所用的做法相同。目标侧解析出上述开关之后,mcpp 用它们编译一个纯预处理探针(-E -dM,把预定义宏全部打印出来——足够便宜, 且不需要执行,这一点很重要,因为解析出的环境常常是一个交叉目标),读回 __SIZEOF_LONG____SIZEOF_WCHAR_T__,以及哪些环境身份宏被定义,与声明 核对。不符即让构建失败,并同时打印声明值与实测值。结果按配置(编译器二进制 身份加最终参数)缓存,同一配置解析两次只编译一次探针。

探针的命令行必须选中目标,而在 freestanding 上它一度没有做到(mcpp 2026.9.20.1)。 clang 是一个二进制,它能产出自己被编译进去的每一个目标, 因此一条不点名目标的命令行回答的是它自己所在的那台机器。宿主交叉目标经由 --target= 到达探针;freestanding 目标的目标选择与必须随行的 ISA flag 走的是同一条路——freestanding 编译前缀——而探针从未拿到过它。于是每一次 freestanding 构建实际上是拿宿主去核对声明:在 Linux 宿主上,那台宿主恰好 满足 __unix__ 已定义、_WIN32 未定义、wchar_t 32 位,于是检查以错误的 理由通过;在 Windows 宿主上它报 _WIN32 已定义、wchar_t 16 位,构建失败。

2026.9.18.3 把那次 Windows 失败读成「--target= 替换没能剥掉宿主预定义」, 于是加了一个 hostStripMacros 列表,由调用方在 -dM 之前前置一组 -U<name>。clang 的预定义跟随的是目标而不是宿主——在 Linux 宿主上 --target=riscv64-none-elf__riscv、不报 __linux____SIZEOF_WCHAR_T__ 为 4——所以根本没有一次替换失败过。那个列表已被删除, 而删除本身就是要点,不是顺手整理:它抹掉的正是「探针量错了机器」这件事的 唯一证据。

装配处拒绝这次遗漏,而不是让每个调用点各自记得不要犯它。 cenv_probe::assemble_argv 在一个 freestanding 目标的 argv 没有选中目标时 返回拒绝;而对一次宿主本地构建接受没有目标选择的 argv——那里宿主就是 目标,缺席是那个决定本身,不是它的遗漏。

__SIZEOF_WCHAR_T__ 那一处配套缺陷仍然关闭在实现侧,并且与上述无关: cenv::realisedecl.wcharBits = 32 一律发出 -fno-short-wchar,不论 某个工具链的默认值是什么,于是编译产出的宽度就是声明所述的那个,而不是继承 来的。

一个包提供该层的哪些接口,又需要哪些(mcpp 2026.9.20.1)。 一项能力要么 在场要么不在场,而一个包需要能够在被构建之前陈述自己需要什么。openkal 自己 的规范(0.14 §3.3)撤回了它为接口集合起过的唯一一个名字(hosted), 理由是:一个描述环境类别的名字,会被没有人想到过的那个环境证伪;替代做法是 由消费者在自己的包里逐条列举。mcpp 为 kernel-abi 层承载这份列举:

# the implementation
[package]
provides = ["mcpp:kernel-abi=openkal"]

[kernel-abi]
provides-interfaces = ["openkal.abort", "openkal.stream", "openkal.memory",
                       "openkal.env", "openkal.time", "openkal.fs"]

# a consumer
[kernel-abi]
requires-interfaces = ["openkal.fs", "openkal.net"]

provides-interfaces 只有供给该层的包可以陈述,这是 [c-abi] 遵循的同一条 规则,理由也相同。requires-interfaces 没有这个限制:它是一句关于陈述它的 那个包自身的话。

引擎不认识这两个集合中的任何一个成员。 对它们施加的唯一操作是集合差, 因此某个规范新增一个接口不需要 mcpp 发新版;而一次拼写错误会产出一条点名 该字符串的拒绝,而不是一次被静默关掉的检查——一个没有被提供的名字就是缺的, 不论它是否真的存在。

这个问题在依赖解析时回答,因为那是它最早能被回答的时刻。 openkal SPEC 0.14 §6.2 列出能力信息出现的三个时刻,并规定每一个都是该信息最早能存在的 那一刻:解析回答「这个程序可不可以针对这个实现构建」,链接回答「有没有用到 实现没有提供的接口」,一个能力字回答「在它提供的接口内,它如何表现」。源码用 #ifdef 问的是同一个问题,却问在预处理期——比任何答案存在得更早。

一个什么都没陈述的提供者,不是一个什么都不提供的提供者。 一份实现方没有 写 provides-interfaces 的图照常构建;这个键晚于那些包出现,而链接仍以它 一贯的词汇报告缺席。

但它会说出来(mcpp 2026.9.21.2)。 一共三种情形,其中两种能构建—— 提供方陈述了一份列表且列表包含该需求;陈述了列表但不包含(拒绝);什么都没 陈述——而在这条提示出现之前,第一种与第三种产生一模一样的输出:

        note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked

一条没人回答的需求,读起来和一条被确认过的需求一模一样,这正是它要关掉 的那个缺口:一个看着绿色构建的消费者,分不清「问过并且一致」与「从来没问成」。 提示点名解析出的那个实现,因为一个只被告知「有东西没检查」的读者,无法据此 行动。提供方确实陈述了列表时,这一行不出现,tests/e2e/743 双向断言—— 没有第二条腿,第一条腿在一个无条件打印这一行的引擎上同样会绿。

一个 C 库不供给什么([c-abi-absent],mcpp 2026.9.20.1)。 一个 C 库供给 的名字集合在清单里不可枚举——POSIX 约有一千二百个——枚举它正是 §3.3 记录下 撤回的那个错误。例外是可枚举的:

[c-abi-absent]
fork      = { form = "link" }
mprotect  = { form = "enosys", note = "openkal has no operation upon a mapping's protection" }
tcsetattr = { form = "accepted-no-effect", note = "the fields openkal does not name are not applied" }

form 必填且取值封闭。link 是 openkal 自己的能力模型对一个实现所要求的 形状(§6.1 把运行期报告不支持称为一处缺陷);另外两个是对它的偏离,给它们 命名是为了让一次偏离成为可以被数出来的东西。当链接命中 link 形状里的 某一项时,mcpp 把这份清单读回来,于是 undefined reference to 'fork' 会带着 那句说明它是缺陷还是这个程序所构建环境之限制的话一起到达。

早于这些键的引擎,以及两张表都写成顶层表的理由。 旧 mcpp 忽略它不认识的 顶层表,而拒绝它认识的表里的未知成员——后者正是让拼错的 presents 不会静默关掉一条声明的机制。因此这两张新表都写成顶层表;对 [c-abi-absent] 而言,这个位置是量出来的,不是偏好。

写成 [c-abi].absent 更顺,但那样一来,每个早于 2026.9.20.1 的 mcpp 都会 在每一个目标上拒绝整份清单,报 [c-abi] has no member 'absent'。判据取自 真正发布的 2026.9.18.3 归档——当时的索引 floor——跑在 openkal-musl 0.17.0 即将发布的那份清单上。这张表做的每一件事都是诊断性的:它给一次已经失败 的链接加上一句话,没有任何 flag、链接行或产物依赖它。于是忽略它的引擎产出的 正是它今天产出的那条链接错误,而拒绝它的引擎会让整个包不可用,并且会把索引 floor 逼到这个版本——为了一句他们本来就收不到的说明,夺走停在其下的每一个 客户端手里的整个索引

对一个包的后果:采用这两张表中的任何一张,都不对索引 floor 提出要求,旧引擎 解析出的图照常构建,失去的只是那项检查和那句说明,而那正是那个版本本来就有 的行为。把 absent 写在 [c-abi] 里面会被拒绝,拒绝消息点名顶层的那个拼法。

指纹。 解析出的环境参与构建的指纹(compileFlags,§92 第 7 项):C 库 声明 lp64llp64 的两次构建,从同一份源码编译出 long 宽度不同的 目标文件,因此二者绝不共享输出目录,也不会复用对方产出的目标文件缓存。

全局构建缓存的键也覆盖了这一点(mcpp 2026.9.18+,一次 PR 中期修订,不是 设计原文)。 ~/.mcpp/build-cache/v1——一次普通依赖编译跨工程、也跨 mcpp 升级复用的缓存——是与上面构建指纹分开的另一套机制,按包逐一取键, 只取真正到达该包自身编译命令行的那些轴(mcpp.build.cache_key)。解析出的 环境到达一个包的命令行,完全是通过引擎的广播(与 targetSideUsage-D__OPENKAL__ 同一条通道),从来不经过包自己声明的 [build] cflags/cxxflags——所以键的推导本身也必须被告知去读广播之后的 值,而不只是声明的值。这个缺口正是这样被发现的(协调者反馈,openkal-musl 尖峰实验):原地升级 mcpp、且缓存目录未清理时,给按新环境构建的镜像喂了 按解析环境编译出的目标文件——一个镜像里混了两种 C 环境,且没有任何 诊断。fill_package_config 现在把 PackageRoot::privateBuild.cflags/ cxxflags/asmflags——广播之后的值——和包自己声明的 flag 一起折进键里, 做法与它原本处理 include 目录的方式完全一致。--cache=off,或者干脆清空 缓存目录,从来都不是「键本身是对的」这件事的信号——这两条路径都只是绕开了 这个键,而不是证明了它。

这次发布还把缓存的 epoch 提了一版,让已有条目全部作废——升级后第一次 构建会是冷构建。 键改对了,不代表用旧的、错误推导方式写下的条目就可以留着 继续信:一个条目被污染,恰恰是因为它记录的键和它实际编译时的输入从一开始就 对不上——而修好之后最可能仍然拿到不变的键的那个包,正好是这次修订里新推导 进 c-environment = "platform" 的那一类(上文的 kernel-abi 提供者):它的 privateBuild.cflags 现在是空的,新键因此是从空内容算出来的,跟旧键(同样是 从空内容算出来的)一样;而磁盘上那份目标文件,却是带着替换令牌编译出来的。 没有更便宜的办法能把修复前写下的条目和修复后写下的条目分开,所以 mcpp.build.cache_key::kCacheEpoch 往上提了一版(2 → 3),让整个缓存无条件 作废,而不是去相信一个恰恰在最要紧的那些条目上靠不住的键相等判断。

存储键——尚未补上,这里把它到底意味着什么讲清楚(对照真正写入 store 的 那条代码路径核实过,mcpp 2026.9.18+)。 一个包的安装钩子能够、也确实 会编译目标侧代码——目标文件、静态库——而它装进的共享 store 只按包名与版本 取键,这与 requires 已经记录的 C++ 运行时选择缺口同形。它与上面 构建缓存的键不同、也不是这一个 PR 能用同样方式补上,是因为:安装钩子运行在 工具链解析之前,这是真实的顺序约束,不是遗漏。install_hook_env 在正常 路径上工具链相关的字段一律为空——prepare.cppm 要等依赖图装完之后才解析 tc,因为解析目标侧本身可能要依赖图最终供给了哪个包的哪一层(c-abi 提供者正是这个图里的一个成员)。钩子没有办法去问这次构建解析出了什么环境, 因为在它运行的那一刻,压根还没有算出来——不是 mcpp 忘了传给它,是那时候 真的没有可传的东西。

失败模式,直说,不当成缺口表里的一行: 一个编译了对环境敏感的 C 代码的 钩子(凡是正确性依赖 wchar_t 宽度、数据模型、或哪些环境身份宏被定义的 代码)没有办法问这次构建实际解析出什么,所以它只能按一种假设编译,然后指望 每一个消费者都跟它假设的一样。一个图里声明的 [c-abi] 与之不符的工程, 会拿到按一种 wchar_t 宽度编译的目标文件,去链接按另一种宽度写的头文件—— 没有任何东西会去核对这件事:store 里不记录它装的是按哪种环境编译的, 所以根本没有可以核对的对象,只有一次悄无声息的错误链接。这与上面 C++ 运行时 那条 requires 检查已经接受下来的、同一种形状的已知限制相同,不是这一个 PR 新引入的——[c-abi] 继承它,是因为它继承了同一个 store。

要真正补上它需要什么: 要么 (a) 改成两阶段安装——把安装钩子做的任何 目标侧编译推迟到目标侧解析完成之后,解析出环境后再重新调用一次钩子(或者 一个更晚的第二个钩子),这会改动整个代码库目前当作固定不变的安装/解析顺序; 要么 (b) 把 c++-abi 那条 requires 形状检查的做法,推广到 c-abi/c-environment——包声明它的 store 产物是按哪种环境构建的,工具链 解析完之后核对,不符就拒绝——这个方案本节已经设计好,但这个 PR 里没有实现。 在其中一个真正落地之前,过渡期的纪律和 C++ 运行时那条现有要求一样:这类包的 安装钩子不得把一种以上的环境变体构建进同一个 store 目录。

标准库模块源

作为标准库的包陈述它的 std 模块源在何处,以及该源需要什么。

[build]
std-module        = "llvm-generated/std.cppm"
std-compat-module = "llvm-generated/std.compat.cppm"
std-module-flags  = ["--no-default-config", "-nostdinc", "-nostdinc++"]

这些键属于 [build],因为模块源是该包的一个翻译单元:它以该包的 include 目录与定义被编译。属于 [build] 同时使这些 flag 可以条件化,而一个在多种 C 库之上供给同一 C++ 运行时的包需要这一点。

[target.'cfg(c-abi = "musl")'.build]
std-module-flags = ["-D_GNU_SOURCE"]

声明 std-module 而没有相应的 provides 条目是一个错误:该包描述了一个它 并不供给的库。

这三个键的 [package] 写法仍被接受,且不可条件化。

标准库自身的语言级别(mcpp 2026.9.15.2+)

一张模块图以同一个标准编译,即根包的标准(07 —— 工作空间 §4.2)。供给 C++ 层的包(hosted-standard-librarymcpp:c++-abi=<impl>) 是唯一的例外:它陈述了 [package] standard 时,它的每一个既不提供也不导入 模块的 C++ 翻译单元都恰好以该级别编译,与图的级别无关。

[package]
standard = "c++23"
provides = ["hosted-standard-library", "mcpp:c++-abi=libc++"]

标准库以自己的级别构建,并在其他任何级别下被使用:上游以 C++23 编译 libc++,而 libc++ 22 的源码在 C++20 下无法编译。这一例外是安全的,因为它 覆盖的单元既不读也不写 BMI;该包的模块单元,包括 stdstd.compat 模块,仍按图的级别编译,因此没有任何模块被分裂。该级别被追加到每个被覆盖 单元自己的 flag 中,因此同样到达编译命令、依赖扫描、compile_commands.jsonmcpp emit build-database。不陈述 standard 的供给者按图的级别编译。

这一例外不推广到其他包。一个头文件若声明依赖于 __cplusplus,会使普通库的 目标文件与其消费者的目标文件在没有诊断的情况下互不一致,而标准库正是其接口 被设计为在另一级别下使用的那一类包。

对已解析目标侧的适配

供给某一层的包经常支持其下方层的多个实现。它查询已解析的目标侧,而不是 被告知。

[target.'cfg(c-abi = "musl")'.build]
include_dirs = ["config/musl"]

[target.'cfg(c-abi = "picolibc")'.build]
include_dirs = ["config/picolibc"]

若此处要求一次 feature 选择,将迫使工程重述目标三元组或其依赖图已经确立的 事实,并让两处陈述有可能互相矛盾。

当某一层由依赖图供给时,编译器不再搜索该层的宿主位置。 mcpp.toolchain.hostflags 读取每一层的来源,并关闭对应的编译器隐式搜索: 图供给 c-abi 时去掉编译器自带的 C 库搜索路径(-nostdlibinc),图供给 c++-abi 时去掉它的 C++ 搜索路径(-nostdinc++)——两者各自独立判断, 不取决于另一层是否也来自图。在此之前,一个只因为宿主头文件恰好补上了某个 缺口才能编译的包,会在一台机器上构建成功、在另一台机器上以不同方式失败; 现在确定的结果只有两种——「在图里找到」与「没找到」,不再有「用了这台机器上 恰好装着的那份 SDK」。包应当通过下面的层谓词来适配,而不是依赖宿主机器 恰好装了什么。

谓词的键就是五个层名,值就是本章开头那张表里的接口名——与 Target 报告 打印的是同一批字符串。它们可以与三元组键在 all/any/not 下组合:

[target.'cfg(all(linux, c-abi = "musl"))'.build]
cxxflags = ["-D_GNU_SOURCE"]

层名指的是库,不是三元组的 env 段。 二者在 musl 上重合,在 gnu 上 分叉:在 Linux 上该段请求的是 glibc,在 Windows 上它命名的是工具链的 MinGW 形态,而后者的 C 运行时与 MSVC 形态链接的是同一个 UCRT。写法是 c-abi = "glibc",而非 c-abi = "gnu";与答案相对的那个「请求」是 env = "gnu"——这是另一个问题(docs/specs/target-side.md §3.4)。

envc-abi 不可互换。 env 是三元组请求的东西;c-abi 是图 与载荷回答的东西。依赖图里的 openkal-musl 会在 x86_64-linux-gnu 三元组下供给 musl,而只有 c-abi 看得见这件事。

这些谓词只在 [build] 段中可用。目标侧在依赖解析之后才被解析,因此由它 选择的依赖会构成一个环;[target.'cfg(<层> = …)'.dependencies] 会被报出并 忽略,而不是被静默丢弃。一个在不同 C 库下需要不同依赖的包,要么按 C 库拆分, 要么依赖其并集,再在 [build] 中选择源码。

mcpp 不认识的键——拼错的字,或来自更新版本 mcpp 的谓词——会被报成一条 schema 警告,并且该段不生效。它过去静默地求值为假,而那与「这一段本就 不该匹配」读起来完全相同。

诊断

四种情形由引擎而非由编译器报出。

情形 报出内容
被要求的实现不是解析出的那个 同时指出二者,以及选择它的命令
两个包供给同一层 同时指出二者,以及各自进入图的路径
某一层无人供给 指出该层,以及应当依赖的能力
载荷的 C++ 运行时位于一个外来 C 库之上 同时指出二者,以及两条出路

一条来自编译器或链接器、关于某个目标侧组合的消息,表明缺少一条诊断。在任何 命令行被发出之前,引擎已经知道该组合不成立。

兼容性

三条规定保全既有清单与既有构建。

能力名 hosted-standard-library 继续表示 C++ 层。同时携带两种拼写的包是 一个供给者,其中命名了接口的那一条是被报告的一条。

工具链族拼写 openkal-llvm 归一为 llvm。它命名的是同一份载荷,并携带一条 关于目标侧的事实,而上述模型从包的声明中解析该事实。

保留前缀内的未知名字,在根工程自己的清单中是一个错误,在依赖的清单中是一条 警告。前者是作者正看着的一处拼写错误;后者是一份针对更新引擎写成的清单, 拒绝它将意味着层名词表永远不能被一个已发布的包扩展。清单中其它位置的未知键 被忽略。

这条规定只约束此后的引擎。一个包若声明某个层名,它的使用者仍须运行不早于 该层名被引入的那个版本。

[target.*] —— 平台条件依赖与 flag

[target.<sel>] 表把依赖与构建 flag 限定到某个平台。选择器 <sel> 有 三种形式:

选择器 含义 示例
裸 OS 别名 单个 OS / 族——简洁且常用的形式 [target.windows][target.unix]
cfg(...) 谓词 复合条件(arch / env / 组合子) [target.'cfg(all(linux, not(arch = "aarch64")))']
精确三元组 某个具体目标(同时承载 toolchain / linkage / sysroot / runner / min_api_level,见 04 §2.7.3 [target.x86_64-linux-musl]

一个选择器可以承载平台条件的依赖构建 flag

# Concise bare-alias form — pull OpenBLAS and link it only on Windows.
[target.windows.dependencies.compat]
openblas = "0.3.33"
[target.windows.build]
ldflags = ["-Llib", "-llibopenblas"]

# cfg(...) for compound predicates (grammar: all/any/not over os/arch/family/env,
# plus the bare aliases windows/unix/linux/macos).
[target.'cfg(all(linux, not(arch = "aarch64")))'.build]
cxxflags = ["-march=x86-64-v2"]

[target.windows][target.'cfg(windows)'] 完全等价——裸别名 windows / linux / macos / unix 都不是合法的目标三元组,因此不存在 歧义。单个 OS/族用裸形式,arch/env 条件与组合子用 cfg(...)

  • 可用键dependencies / dev-dependencies / build-dependencies / feature-deps.<feature>(mcpp 2026.8.6.2+——见 30 —— 构建程序:build.mcpp;feature 本身无条件注册, 只有它的依赖集合受限定),以及带 cflags / cxxflags / ldflags / sourcesbuild(mcpp 0.0.95+——条件源码 glob,例如把 src/x86/**/*.asm 收在 cfg(arch = "x86_64") 之后;! 排除 glob 在此 同样有效),再加 flagsinclude_dirs / include_dirs_after (mcpp 0.0.102+),以及 private_include_dirsstd-module-flags (mcpp 2026.9.1.1+),还有带 frameworks / libraries / link_library_dirsruntime(mcpp 2026.8.29.1+;frameworks 自 2026.9.12.3 起),以及带 kindtargets.<name>(mcpp 2026.9.14.2+; 见 targets.<name> kind)。

  • mcpp 不读取的子表会被报出(mcpp 2026.9.14.2+):拼错的 [target.<sel>.dependecies] 是一条警告,列出 [target.<sel>] 表拥有的 各个段,--strict 下成为错误。

  • 条件依赖声明替换无条件声明(mcpp 2026.9.14.2+)。在选择器命中的行上, [target.<sel>.dependencies] 中某个身份的声明就是该身份的声明,无条件 声明在该行上不生效。某一行上以不同形态链接的依赖写两次,每次都带来源:

    [dependencies]
    huxerui.huxerui = { version = "0.3.0" }
    
    [target.'cfg(env = "android")'.dependencies]
    huxerui.huxerui = { version = "0.3.0", linkage = "shared" }

    同一规则适用于 dev-dependenciesbuild-dependenciesfeature-deps.<feature>;多个命中的段按清单顺序生效,最后一个为准。 mcpp why deps 给出每条请求来自哪张表(09 —— 按场景的命令)。 只写选项、不写来源的表 huxerui.huxerui = { linkage = "shared" } 声明的是名为 huxerui.huxerui.linkage 的包:mcpp 报出这一行并给出补全 来源后的声明,解析随即失败。2026.9.14.2 之前的引擎保留无条件声明。

  • runtime 是链接行中与方言无关的那一半。 build.ldflags 按 GNU 拼法 书写,而原生 cl.exe 不接受 -L。这些键表达同一件事而不承诺拼法:mcpp 把 libraries / link_library_dirs 渲染成 -L<dir> + -l<name>/LIBPATH:<dir> + <name>.lib,把 frameworks 在 Mach-O 各行渲染成 -framework <name>,其余各行不产生任何 flag。它们就是顶层 [runtime] (见 04 —— mcpp.toml §2.11)已有的同几个键,此处只是 让它们按目标生效,并未引入新词汇。此处的一条追加在顶层列表之后, 不是替换——manifest 借此把 UIKit 挡在 macOS 链接之外、把 AppKit 挡在 iOS 链接之外,同时在顶层共享两者都要的那些 framework。[runtime] 的其余键在这里会被报出并忽略,因为它们不是按目标区分的。

    # Linked only on Windows, and spelled correctly for whichever compiler builds it.
    [target.windows.runtime]
    libraries = ["user32", "gdi32"]

    一个谓词下只写这一张表时,它与其他情形一样生效。在 mcpp 2026.9.9.1 之前并非如此:除非同一谓词下还写了别的东西,该块会被解析后丢弃。

  • build.ldflags 中的相对搜索路径属于写下它的包。 -L<dir>-Wl,-rpath,<dir>——无论写在这里、写在顶层 [build] ldflags,还是来自 mcpp::link_flag——以该包目录下的绝对路径进入链接,依赖的 flag 传给 消费者时也是如此。以加载器展开的记号开头的项按原样传递:$ORIGIN 及其他任何 $ 记号,以及 @executable_path@loader_path@rpath (mcpp 2026.9.14.2+)。

  • build 接受的恰好是可叠加的构建输入集合——那些以追加方式合并、 并在谓词求值之后被消费的东西,也就是 BuildInputs 的成员表。linkagetarget 与档案开关刻意不在其中:它们是目标选择的输入(用一个针对 target 求值的谓词去条件化 target 是循环的),或者需要覆盖而非追加的 语义。集合之外的键会被报出并忽略;消息里列出的正是它比对用的那份集合, 因此不会与检查本身漂移。

  • 按解析后的目标求值——交叉构建取 --target 三元组,否则取宿主。因此 原生 Linux 构建根本不会下载 [target.windows] 依赖。

  • 谓词的键osarchfamilyenv——三元组的坐标——以及自 mcpp 2026.9.1.1 起的五个目标侧层名 compilercompiler-runtimekernel-abic-abic++-abi(见 22 —— 目标侧)。 accelerator 同样是这里的键,由本次构建自己的 accel--accel[build] accel 里的后端名)回答,因此它是对一个集合的成员判定; accelerator = "none" 是一种写法,用来说明「本次构建没有命名任何后端」, 而不必枚举它不是的那些后端。裸词 linux / macos / windows / unix 是对应 os / family 判定的糖。集合之外的键会被报成一条 schema 警告, 且该段不生效——它过去静默地求值为假,而那与「这一段本就不该匹配」 读起来完全相同。

  • 被解析的层的谓词不能选择依赖。 层是依赖图解析出来的,因此由它 选出的依赖会决定它正在询问的那个答案。 [target.'cfg(c-abi = "musl")'.dependencies] 会被报出并忽略;同一谓词下 的 build 输入照常生效。accelerator 不在此列(mcpp 2026.9.6.5):它是 构建的输入而不是图给出的答案,所以 [target.'cfg(accelerator = "cuda")'.dependencies] 生效。

  • 优先级:精确三元组表胜过 cfg/别名表;多个命中的谓词表,其 flag 按顺序拼接,其依赖声明按清单顺序生效。条件项追加在无条件 [build]之后,因此在 GNU「最后一个 flag 生效」的规则下,条件规则会覆盖 更宽的无条件规则。这正是让按 OS 移除成为可表达的原因:

    [build]
    flags = [{ glob = "third_party/zlib/**", defines = ["HAVE_UNISTD_H=1"] }]
    
    # clang-MSVC has no <unistd.h>: undo the base define, add the windows one.
    [target.'cfg(windows)'.build]
    flags = [{ glob = "third_party/zlib/**",
               defines = ["NO_FSEEKO"], cflags = ["-UHAVE_UNISTD_H"] }]
  • 未命中当前目标的条件 flags 条目根本不存在,因此它不会产生「glob 未匹配到任何源文件」的警告。于是一份 manifest 可以同时携带三个 OS 的 flag 表,而不会在另外两个上制造噪声——与未启用 feature 的条目根本不存在 是同一个道理。无条件表里的零命中 glob 仍然告警,因为那里它是真实 缺陷。

  • toolchain / linkage / sysroot 仅限精确三元组——它们描述某一个 具体的交叉目标,因此写在 [target.<triple>] 下(见上),而不是裸别名或 cfg(...) 下。

sysroot —— 目标的 C 库

sysroot(mcpp 2026.8.20.2+)覆盖目标表为某个三元组绑定的 C 库,与 toolchain 覆盖编译器 pin 处于同一条轴上:一个指名目标所解析的编译器, 另一个指名它的 C 库,在工程有理由与之分歧之前,两者都只由引擎决定。

[target.riscv64-none-elf]
sysroot = "xim:newlib-riscv@4.4"     # a different C library
[target.riscv64-none-elf]
sysroot = ""                          # no C library at all

键缺席与键为空是两个不同的答案。 缺席继承目标表的 C 库。存在且为空是 零 libc 档:不解析任何 C 库,不加入头文件与库目录,链接行上只有工程 与其依赖提供的内容,#include <stdio.h> 不再解析。内核与 bootloader 要的正是这一档,而把两种情形合并会让这类工程静默地把目标的 C 库拿回去。

取值是一个 xpkg 引用或空字符串;裸名在解析清单时即被拒绝,因为接受它会 导致什么都不安装,然后在很晚的时候以「缺少 libc」失败。

在 MSVC ABI 行(*-windows-msvc)上,sysroot 是 MSVC toolset,取值是 msvc 的 写法(2026.9.24.1+):msvc@system(键缺席时的默认)、msvc@<toolset>(已安装的 同版本 toolset,没有时为载荷)或 xim:msvc@<toolset>(只要载荷)。这类行上的 其他取值在解析清单时即被拒绝。见 20 —— 工具链管理 中 「MSVC ABI 上的 clang:toolset 就是 sysroot」一节。

构建程序可以询问供给 sysroot 的是哪个 C 库载荷mcpp::target_libc() 返回该包的名字,mcpp::target_libc_profile() 返回目标 ISA 档位对应的 子目录。零 libc 档上两者均为空。见 40 —— 裸机与 freestanding 目标

这与「目标侧解析出的 C 库是哪一个」不是同一个问题。 target_libc() 命名的是 mcpp 装上的那个载荷,而这个值是目标侧解析的一项输入——依赖图 里的包可以改为供给 C 库,那时解析出的 c-abi 就不是这里返回的东西。要按 已解析的层分支,请用层谓词:[target.'cfg(c-abi = "musl")'.build] (见 22 —— 目标侧)。这一段在 2026.9.1.1 之前写的是 「解析到的是哪份 C 库」,那是两者里错的那一个。

abi —— 整个产物共享的开关(mcpp 2026.9.12.2+)

[target.'cfg(os = "emscripten")'.abi]
threads    = true
exceptions = true

目标的某些性质不是单个翻译单元可以自行选择的 flag。线程支持即是一例:在 WebAssembly 上,每个目标文件、预编译的标准库模块与链接必须在共享内存与 原子操作上保持一致,只要有一个翻译单元没有启用它们,链接就会失败,或者 模块拒绝加载。异常是同一种形状:clang 把异常模型记进 BMI,并拒绝一个与之 不一致的导入者。这类性质写作 [target.<selector>.abi] 的有类型成员, 而不是 cxxflags 中的一个 flag,引擎因此能把它施加到每一个必须一致的 单元上,并把它与包的需求相比较。

成员 类型 渲染为 到达
threads 布尔 在既非 PE 也非 freestanding 的目标上为 -pthread;在 PE 与 freestanding 目标上不产生任何 flag 标准库模块的预构建、依赖扫描、所有包的每个 C 与 C++ 翻译单元,以及链接
exceptions (mcpp 2026.9.12.3+) 布尔 -fexceptions,只在 os = "emscripten" 上,经方言 flag 进入编译行,也进入链接行;在其余每个目标上什么都不产生,因为那里异常本来就是默认开启的 threads 相同的那一套

两个成员都经由方言 flag 进入依赖缓存键,因此没有启用某个成员的构建,不会 被启用它的构建复用。未知成员,以及不是布尔值的成员,都会被拒绝,并同时 列出 threadsexceptions

没有 exceptions,观察到的失败在运行时,不在链接时。 一个跨 import std 边界抛出异常的 Web 程序能正常编译并链接——Emscripten 的 编译期异常支持不依赖这个 flag——只在 throw 真正执行时中止:

Aborted(Assertion failed: Exception thrown, but exception catching is not
enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or
-sEXCEPTION_CATCHING_ALLOWED=[..] to catch.)

只有根 manifest 能做决定。 这个开关属于产物,而根包是唯一构建产物的 包。依赖写下的 [target.<selector>.abi] 会被报告 (abi/dependency-table),且不改变任何东西。依赖改为声明自己的需求:

[package]
requires_abi = { threads = true }          # the package needs threads

[features]
mt = { requires_abi = { threads = true } } # only this feature needs them

根包未满足的需求在任何编译开始之前就被拒绝,拒绝信息指出包名以及提出 需求的对象:

error: `wasmrt` requires the artefact's ABI to have threads (feature `mt`), and this build does not state it.
       Add to the root manifest, for the targets that need it:

           [target.'cfg(os = "<os>")'.abi]
           threads = true

若没有这项拒绝,不匹配会表现为一个预编译模块的配置错误,而该错误既不指出 包,也不指出开关。

requires_abi 在 target 轴上(mcpp 2026.9.12.3+)

[package] requires_abi[features.<f>] requires_abi 是无条件的: 它们要求本包构建的每一个目标都打开某个开关。一个需求局限于某个平台的 依赖——比如每个 hosted 行都要线程、Web 上一个都不要——直接写在已经承载 它自己 sources 的选择器上:

[target.'cfg(linux)']
requires_abi = { threads = true }

# the per-feature form, named after feature-deps and feature-xlings
[target.'cfg(linux)'.feature-requires-abi]
mt = { threads = true }

需求集合是 [package] requires_abi、活跃 feature 各自的表、以及每一个 命中的选择器各自表的并集——不止一个选择器可以要求同一个成员,而它们各自 都是真话。它只对命中已解析目标的选择器生效:[target.'cfg(windows)'] 下的需求对一次 Linux 构建不施加任何东西,双向皆然——有它、没它,那次构建 都不受影响。根包未满足的需求在任何编译开始之前就被拒绝,拒绝信息按原样 点名那个选择器:

error: `wasmrt` requires the artefact's ABI to have threads ([target.'cfg(linux)']), and this build does not state it.
       Add to the root manifest, for the targets that need it:

           [target.'cfg(os = "<os>")'.abi]
           threads = true

早于 2026.9.12.3 的引擎静默读过这个键——既不警告,也不报错。 [target.<sel>] requires_abi 是目标选择器下一个取值为表的键,而旧引擎的 schema 清扫会跳过每一个取值为表的键,理由是它假定表就是条件通道; requires_abi 在这里恰好是一个内联表,与那个假定同一种 TOML 形状,于是 不受任何报告地漏过同一次清扫。一个依赖这份拒绝来保护一次无条件线程构建的 包,因此要自己声明引擎下限 ([build-dependencies.mcpp] version = ">= 2026.9.12.3"),而不能指望 旧客户端自己发现这个缺口。

--no-entry(Emscripten 里没有 main 的模块用的 flag)不是 mcpp 解释的开关;它是一条普通的 [target.'cfg(os = "emscripten")'.build] ldflags 条目,main 照样只是 指出一个翻译单元——见 21 —— 目标三元组

targets.<name> kind —— 一个库在某一行上的形态(mcpp 2026.9.14.2+)

[targets.huxerui]
kind = "lib"

[target.'cfg(env = "android")'.targets.huxerui]
kind = "shared"

[targets.<name>] kind 的按行形式(04 —— mcpp.toml §2.2)。在桌面各行上链接进应用、在 Android 上必须是唯一一份共享库的框架, 在它自己的清单里声明一次;每个消费者都只保留一行无条件依赖。

  • 一行陈述 kind,在两种库形态 libshared 之间选择;或者 (2026.9.15.2+) 陈述 linkage,给出该库在这些行上的默认形态而不约束 它。同一行同时写两者会被拒绝,后命中的陈述替换先前的陈述,包括无条件 表中的陈述。既不是该包库目标(声明的或推断的)、也不是程序目标、 也不属于其他形态的名字都会被拒绝。
  • 在命中的行上,该包被约束为共享形态,与 [targets.<name>] kind = "shared" 的约束完全相同(04 —— mcpp.toml 中的 dependency_linkage):不写 linkage 的消费者得到共享库,写 linkage = "static" 的消费者得到一条点名这一行的警告,--strict 下 成为错误。mcpp why deps 以原因 row-kind 报告该形态。某行的 linkage = "shared" 让不写 linkage 的消费者得到共享库,原因为 package-default;对消费者的 linkage = "static" 予以遵从,给出信息行 而不是警告。
  • 点名目标侧层的选择器不能承载这张表;该表被报出并忽略,因为库的形态是 在解析出回答该层的那张图时决定的。
  • 2026.9.14.2 之前的引擎不读取这张表,也不报告。依赖它的包要写明这一 引擎下限。

当前边界

  • 依赖不能以加速器为条件。 加速器这一层是从依赖图解析出来的,因此由它 选择的依赖会决定它自己正在问的那个答案。mcpp 会报告该谓词并忽略它; 包要么无条件,要么以平台为条件,而由加速器选择的是 [build] sources
  • 一般而言,层也不能选择依赖,理由相同:任何早于图的推导,都是在推断一个 尚不存在的事实。