Releases: mcpp-community/mcpp
Release list
v2026.8.18.2
新增
-
kind = "shared"在 MSVC ABI 上可用了 —— mcpp 自己生成.def。MSVC 在没有
__declspec(dllexport)、也没有.def时,DLL 什么都不导出;
导入库为空,消费者拿到一堆 unresolved externals,而符号明明就在对象里。
拒绝的理由成立,结论不成立 —— CMake 的WINDOWS_EXPORT_ALL_SYMBOLS自 3.4
起就是这么做的,而且它的bindexplib直接读 COFF、不依赖 dumpbin。这一点是
决定性的:dumpbin只在 Visual Studio 开发者环境里,而 mcpp 在 Windows 的默认
工具链是 clang,mcpp build根本不在那个环境里。mcpp.build.coff_exports是那个读取器,写成对字节的纯函数,于是它能在任何
平台上被测试 —— 16 条单测逐字节构造对象(那是唯一能按需改变存储类的办法),
外加一个真实的 mingw-cross 对象:只喂自己测试输出的读取器,只会与自己一致。
超过 65535 个可导出符号时拒绝而不截断:被截断的导出表能干净链完,
然后在「恰好需要那个掉出去的符号」的消费者那里失败。标注优先。 对象里已带
/EXPORT:指令(即__declspec(dllexport)的产物)时,
mcpp 让开、不生成任何东西 —— 再加一份列表会把同名符号导出两次(LNK4197),
更糟的是把其余所有符号也导出,用「全部」替换掉作者选定的公开面。
这件事靠检测而不是配置:为「我标注过了」加一个 manifest 键,就是给对象已经
说过的事再加一个说法,而两者可以不一致。仍有两条限制是工具消不掉的(与 CMake 记录的同两条):导出的数据在消费端仍需
__declspec(dllimport);vtable 被引用的类要整类标注。两者都写进了 docs/12。 -
打包库可以被原生
cl.exe消费。 生成的 manifest 现在同时带方言中立的
[target.<pred>.runtime](link_library_dirs/libraries),mcpp 会按 target
渲染成/LIBPATH:+<n>.lib或-L+-l<n>。两种拼写都带:旧版 mcpp
只读ldflags并静默忽略新段,去掉它会让旧客户端一个 flag 都拿不到;
而新版读到中立形式时忽略同腿的ldflags而不是叠加 —— 叠加会把-L
送回cl的命令行。
修复
-
mcpp pack现在跟着工程的module_extensions走,不需要再配一次。实测:接口是
.ixx的库打出来的包是静默错的 ——两半都错且都不出声:没有接口,消费者
import不了;而空的发布集合正是打包器
判定「C 表面」的依据,于是这个包同时不再约束 C++ ABI,兼容性闸门也不再检查
编译器与标准库。真因是 lib root 约定把.cppm写死了。现在按已声明的每个扩展名
各给一个候选并取存在的那个;生成的 manifest 也会自己声明module_extensions
—— 从发布的文件算出来,因此不可能与sources不一致。 -
sources命中却分类不出角色的文件,现在当场拒绝。未声明的
.ixx过去会编译出一个没人链接的对象,报错是
undefined reference to mk::answer@mathkit()—— 既不点名扩展名也不点名那条键。
根因是「这是不是模块接口」有两个答题者:扫描器读到export module记下
provides(所以边上挂了bmi_out),而分类器说Other,链接集合只读后者。⚠️ 这是行为变化:sources里混进.md/.txt的工程会开始报错。
v2026.8.18.1
新增
-
mcpp pack <target>可以把一个库打成「接口 + 预编译二进制」的包(#433)。闭源库、离线环境、以及「构建农场已经编过一遍了」这三种场景,过去都只能自己
写脚本收集产物。现在:mcpp pack mathkit # 静态库包 mcpp pack mathkit --target x86_64-linux-gnu \ --target aarch64-linux-gnu # 一个包,两条腿
产出的是一个普通的 mcpp 包 —— 一份正常的
mcpp.toml,走 mcpp 早就有的
「载荷自带 manifest」通路。新增 manifest 段 0 个、键 0 个:打什么由
[targets.<n>].kind决定(所以没有--lib、没有--artifact),发布哪些接口
由[lib]约定 + 模块图决定,公开头是[build].include_dirs全量,
每条腿的 ABI tag 与 digest 记在既有的[[runtime.artifacts]]上。
一个老版本 mcpp 照样能构建这种包 —— 它只是不执行下面那两道闸门。一个包可以同时带两种接口:
include/(文本,#include,不编译)与
interface/(模块,消费者编译它)。实测同一个包被「只 #include」/「只 import」/
「两者都用」三种方式消费,静态与动态两种形态,六格全过。发布哪些
.cppm是算出来的 —— lib root 的模块闭包,不是「所有.m.o」。
实现分区(module M:secret;)照样产.m.o,按扩展名挑会泄露闭源源码;
同一个闭包反过来决定归档里要删哪些对象,按.m.o删则会删掉真代码、
三个平台全部链接失败。两条清单都会打印出来。详见
docs/12-binary-distribution.md、examples/05-lib-distribution。 -
kind = "shared"不再只有 Linux:PE/MinGW 与 Mach-O 都能产、能打包、能跑。过去这条路只在 ELF 上验证过,其余一律拒绝。这是能力的增加,不是修一个洞 ——
⚠️ 早前的提交信息把那道守卫描述成「在原生构建上失效」,那是错的:
tc.targetTriple由编译器的-dumpmachine填,原生构建上非空
(实测resolution.json记的是x86_64-linux-gnu),所以原生 macOS / 原生 Windows
本来就被它拦住。真正缺的东西各不相同,而且都不是 flag 拼写:
- PE 缺导入库。 一个 PE 共享库是两个文件:加载器打开的
.dll,以及
链接器消费的桩归档。mcpp 只写了前者,消费者直接链.dll—— mingw 的 ld 容忍这个,
别的链接器都不容忍,于是能用的那种情况把坏掉的那种遮住了。现在链接边把导入库
作为隐式输出声明出来,包里两个都带,生成的 manifest 指向导入库。
另外 PE 可执行文件带-static,而-static会让 ld 进入纯静态模式并拒绝导入库,
报的是have you installed the static version of the mathkit library?——
既没点 DLL 也没点-static;所以那条-l之前要先-Wl,-Bdynamic。 - Mach-O 缺 install name。
.dylib记录的是链接时的路径,而原先只在声明了
soname时才发-install_name—— 一旦放开 macOS,每个没写 soname 的.dylib
都会把构建目录烙进去:在打包机上完好,换个地方就image not found。
现在无条件发@rpath/<file>。这个选择原先还是用宿主的#if defined(__APPLE__)
做的(在旧的拒绝之下不可达,但放开之后就会发错),现在按 target 决定,
和target_output早就做的一样。 - PE/MSVC 仍然拒绝,但换了个理由,而且是真理由。 不是链接器 ——
link /DLL /IMPLIB:一直都在规则表里。是符号导出:没有__declspec(dllexport)
或.def,MSVC 的 DLL 什么都不导出 ⇒ 导入库是空的 ⇒ 消费者拿到一堆
unresolved externals,而那些符号明明在对象里。产出这个比拒绝更糟。
- PE 缺导入库。 一个 PE 共享库是两个文件:加载器打开的
-
--target不能服务时直接拒绝,而不是悄悄按宿主构建。实测(Linux):
mcpp build --target x86_64-windows-msvc解析到原生 g++、
写进target/x86_64-linux-gnu/、报告成功 —— 一个 ELF 被当成 Windows 构建交付。
词表的 tier 说的是「mcpp 支持这个 target」,从来没说「这台机器能产出它」;
后者是host_can_serve的问题,现在prepare.cppm会问它,并把
这台宿主能构建的清单列进错误信息。逃生口保留:显式
[target.X] toolchain = "…"表示交叉链是你自己提供的,mcpp 的载荷矩阵无权否决。 -
[package] platforms会与实际打出的腿对账(设计里承诺过、实现里没有)。四种比较只有两种值得打印:打了却没声明永远可行动;声明了却没打,
且这台宿主本来能构建它才可行动。正常发布流程是 CI 上每平台各跑一次mcpp pack,
所以 Linux runner 不产 macOS 腿不是遗漏、是每一次 —— 永远触发的告警会把真正该看的
那条盖掉。所以判据用的是host_can_serve,与--target是同一个函数。 -
消费预编译包时的两道闸门。 都是不检查就会静默出错的:
接口与二进制是否仍然配对。 这条闸门存在是因为另一种结果被实测过:把随包
接口里一个结构体的两个int成员互换 —— Itanium ABI 不 mangle 字段顺序 ——
消费者编译过、链接过、运行过、打印出交换后的错数据,任何工具都没有一句诊断。二进制是否为这套工具链所编。 失配时诊断会列出包里确实有哪些 tag ——
一句「找不到」会让人去找一个就在自己硬盘上的包。另外,在解开的分发包目录里直接
mcpp build会被拒绝:那儿的interface/
是声明,定义在旁边的归档里,构建会产出一个几乎空的库然后报告成功。
修复
-
「谁也没判定过」这个状态过去被拼成了「是接口」,于是闭源实现分区会静默发布出去。
一个分区的源码能不能发布,取决于一个关键字:
export module M:api;可以走,
module M:impl;不能。而三条建图路径里有两条读不到它:
[scan_overrides."<glob>"]声明了文件提供哪些模块,没有地方能说它是否 export;
P1689 的is-interface是可选键,mcpp 把它解析进了一个从来没人读的字段。两者都以
providesInterface = true到达,而字段自己的注释把这叫「保守方向」,
理由是「这个标志只会产生一条警告」。这恰好说反了:true正是那个不产生
任何警告的值 —— 于是用[scan_overrides]声明的实现分区被一声不响地发布。现在它是三态的,每条路径只说自己真的知道的事:文本扫描器读关键字并显式写 true/false;
P1689 读取器把编译器的答案(包括它的沉默)原样带过来;scan_overrides
留空,因为 schema 表达不了。未知会告警,而且和已知那条说的是不同的话。 -
实现分区(
module M:part;)在 Windows 上构建不了,而根因在扫描器里。module M:part;与module M;共用一个拼写,却是两种不同的声明,而扫描器把
它们当成了一种:前者被记成**「requiresM:part、provides 空」** ——
一个文件 requires 自己的名字。于是图里没有从「import 分区的单元」到
「定义分区的单元」的边,构建顺序无约束:GCC 与 macOS clang 靠各自的依赖扫描
兜住了,Windows clang 以failed to read compiled module失败。同一处还有第二半:
import :part;的解析读的是u.provides,而实现单元
(module M;)没有 provides ⇒ 它里面的import :secret;停留在字面的
:secret,没有任何单元提供。两个平台都会刷的那条
module 'M:part' imported but not provided in this build就是这两件事的
合并症状 —— 它读起来像一条提示,其实是病因。实现分区在此之前mcpp 里任何地方都没有测试覆盖,是库分发的 e2e 第一次
用到它才暴露出来。现在扫描器记provides = M:part并标providesInterface = false;import :part;按 TU 自己所属的模块名解析。⚠️ 两处行为变化:①两个文件声明同一个分区(module m:p;× 2)现在会被
拒绝并点名两个文件,此前是静默接受 —— 那种程序本来就 ill-formed,
但它是一条新的失败路径;②sources = []从「等于不写」变成「什么都不编」,
一个真写了sources = []又依赖默认 glob 的工程会发现产物变空(此前无法表达
「什么都不编」,所以这种写法只可能是误解)。 -
[target.'<三元组>'.build]在没有--target时从不命中。同一个语句的两种拼写互相矛盾:
cfg(linux)在原生构建上命中,
[target.'x86_64-linux-gnu'.build]不命中。根因是matches()拿着原始的
--target字符串(原生构建下是空的)短路返回 false,而同一文件的
context_for()对cfg(...)回落到宿主三元组 —— 一个决定两处推导。
manifest/types.cppm的注释从写下起承诺的就是回落那一种。形状是最坏的那种:CI 传
--target是绿的,开发者本机的mcpp build
静默丢掉那一段,失败在链接期出现、点的是符号而不是谓词。修法是删掉第二个答题者:解析后的三元组进
cfgpred::Ctx,matches()
只有一个来源。 -
sources = []与不写sources逐字节等价。解析器在向量为空时一律填默认 glob,于是作者没有任何写法能表达
「什么都不要编」。二进制分发需要这个:一个纯头文件的包不编译任何东西,
而src/下任何遗留文件都会被扫进消费者的构建,并可能与预编译库里的符号
重复定义。改成记录键是否出现(BuildConfig::sourcesDeclared),
与XlingsConfig::subosDeclared同一个模式。 -
卸载后的清扫会波及别的版本**,而那可能正在被另一个进程解压。**
sweep_parked_payloads原来把整个 family 目录扫一遍,把任何没有文件的
版本目录删掉。但安装是逐步往版本目录里写文件的(所以 package_fetcher
用标记文件而不是"目录在"来判断装完没有),于是另一个正在解压的版本在那
短暂窗口里和"残骨架"长得一模一样 —— 共用一个MCPP_HOME的机器上
(自托管 runner、共享开发机)两个 mcpp 进程同时跑是常态。.trash-*照旧全扫(那个名字只有这段代码会写);"没有文件的骨架"这一条
收窄成只扫这条命令点名的那一个版本。 -
msvc_available_here()每次构建都要为每个已装 payload 起一次 cl.exe。它只想知道"这儿有没有能用的 toolset",却走了完整的
installation_at(),
那里面会跑一次 cl 拿 banner 来定版本。而这个判据在每次构建的 MSVC ABI
门上都会被问到 —— 正好是装了多个 toolset 的机器最慢。installation_at(..., identifyVersion=false)跳过 banner。布局仍然只有
一份实现 —— 不是再抄一遍路径拼接。
v2026.8.17.1
(no CHANGELOG entry found for 2026.8.17.1)
v2026.8.16.3
修复
-
macOS 上任何带
build.mcpp的工程都构建不了(#437)。同一个工具链、同一个环境,主构建能过,host helper 不能过 ——
这才是它是 bug 而不是"macOS 环境问题"的地方。主构建在 macOS 上刻意用
-fuse-ld=lld,flags.cppm的注释原文就写着
"Xcode 15.4's ld aborting at launch on macos-14 CI when its libc++
resolution was diverted"。而 host helper 走hostflags.cppm的 trust-cfg
分支,返回空 link token,于是用 Xcode 的/usr/bin/ld—— 那个 ld 自己
就是链 libc++ 的 Mach-O,跑在 payload 工具链设好的DYLD_*里,dyld 把它的
libc++ 解析到 payload 那份,缺__ZdaPv,还没开始链接就 abort。cfg 选的是 runtime,从来没选过链接器。所以"信任 cfg"是对的但不完整:
继续信任它,同时在 macOS 上把链接器点名。lld 随做编译的那套工具链一起发布,
不可能被解析到它没链过的 libc++ 上。根因链记在
.agents/docs/2026-08-13-build-optimization-status.md§9a-3
—— 那份文档原来停在"没有继续猜,本机复现不了"。 -
Windows 上
mcpp toolchain remove <msvc toolset>报 "Access is denied"。remove_all直接上,不清只读位、不重试、报错也不说是哪个文件。
payload 是从 .vsix/.msi 解出来的,归档条目带只读属性;POSIX 只要目录
可写就能 unlink 子项,所以这个问题在 Linux/macOS 上根本不出现。
另外/Zi构建之后 mspdbsrv.exe 还会在 payload 里活几秒。两个原因表现完全一样(都是 "Access is denied"),所以两个都处理:
先清可写位重试,再给活着的进程一个有上限的等待窗口(10 × 300ms)。
报错现在会指出卡在哪个文件 —— 光一句 "Access is denied" 没法处理。这个诊断当场就派上用场了:它点名的是
bin\Hostx64\x64\Microsoft.VisualStudio.Telemetry.dll—— 既不是只读位
也不是 mspdbsrv,而是 cl.exe 拉起的后台遥测进程vctip.exe占着它。
占用者不是 mcpp 能删掉的东西,真正的修复在 payload 那边
(openxlings/xim-pkgindex#637 不再安装 vctip.exe);这边留下的是通用兜底
和那句能读懂的报错。报错同时会说清楚:失败的 remove 不是空操作 ——
remove_all会先删掉
能删的,所以剩下的是一个有洞的工具链,得重来一次而不是接着用。修完 vctip 之后占用者换成了
mspdbcore.dll——/Zi构建拉起的
mspdbsrv.exe,它构建完还活几十秒,而且就住在正要被删的 payload 里。
等它不现实(等多久都是猜),所以改成挪走。⚠️ 第一版挪错了东西:去重命名 payload 目录。Windows 允许重命名一个
打开着的文件(更新器就是这么替换运行中的 .exe),但不允许重命名
一个含有打开文件的目录 —— CI 当场否掉了这个前提。正确的形状是反过来的:把文件逐个挪到旁边的
.trash-*,再删掉此时
只剩目录的那棵树。要是某个文件连挪都挪不动,就把.trash-*清掉、整体报失败
—— 挪了一半的 payload 比原封不动更糟。最后一层:文件都挪走了,空目录骨架仍可能删不掉(sharing violation ——
某个进程把它当工作目录,mspdbsrv 正是在 payload 里被拉起来的)。判据因此定为
「没有文件残留 = 已卸载」:一个没有文件的工具链就是没装,这正是remove
承诺的事;报失败等于报了相反的事实。骨架在下一条生命周期命令里清扫。顺带修掉对称的那半句谎:
toolchain default原来只查目录存在,
会把骨架当成装好的工具链交给构建。现在它问payload_frontend要一个
能解析出来的编译器 —— 和installed()、find_windows_sdk()同一条规则:
装好了 = 能用,不是"在那儿"。 -
半装的 Windows SDK 被当成装好的,链接到最后才炸。
find_windows_sdk()认一个根的条件是Include\<v>\ucrt\corecrt.h
存在 —— 只看头文件。而 SDK 是头文件和导入库两半。托管的
xim:windows-sdkpayload 少了带kernel32.lib的那个 MSI 时,
这个根照样"找到了",而且因为版本号更高,排在机器自己那套完整 SDK 前面。
于是每个 TU 都编过了,一直到最后一步:LINK : fatal error LNK1104: cannot open file 'kernel32.lib'日志里没有任何一行提到 SDK。
现在两半都要:
Include\<v>\ucrt\corecrt.h和
Lib\<v>\um\<arch>\kernel32.lib。半装的根被跳过,搜索落到下一个,
本来就能用的构建就能用了 —— 和has_usable_msvc()坚持"两半都要"是同一条
理由,只是这次轮到 SDK 自己。 -
装好的 msvc toolset 在
mcpp doctor里看不见,而toolchain list看得见。doctor 还在用 #436 修掉的那个写法 ——
toolchain_frontend(<root>/bin, ...)。
cl.exe 在VC/Tools/MSVC/<ver>/bin/Host<h>/<arch>/,深四层,bin/下什么都没有
→continue→ 整个 toolset 被丢掉。#436 把 list 换成了payload_frontend,
doctor 没跟着换,于是同一台机器上两个命令互相矛盾,而且都"成功"。这是同一条布局规则的第四份拷贝。
-
has_usable_msvc()把"这台机器有 VS"当成了"这里能用 MSVC"。它只探测机器,却门控三处对受管 toolset 同样适用的决策(首次运行是否转向
mingw、离线文案、MSVC ABI 修复门)。于是一台钉了msvc@14.44.35207、
没装 Visual Studio 的机器,判据回答false—— 工具链明明就在 payload 里躺着,
却被转去 mingw。新增
msvc_available_here(pkgsDir):两条来源都算。原来那个保留,
它回答的是另一个问题。 -
默认的
/MD构建在只有受管 toolset 的干净 Windows 上跑不起来。产物链
vcruntime140.dll/msvcp140.dll,这两个不是 Windows 自带的
(自带的是ucrtbase.dll)。而全仓库搜不到一处Redist/vcruntime140/
msvcp140—— 我们把CRT.Redist.X64.base.vsix下载下来,一个字节都没用。CI 看不到,因为 runner 上装着 Visual Studio。
新增
vc_redist_dir(),把 toolset 自带的那份 CRT 放进linkRuntimeDirs
—— Windows 上它就是 PATH,和 gcc 把私有 libstdc++ 放进LD_LIBRARY_PATH
是同一件事。5 个测试:能从编译器路径单独找到、redist 版本号不等于 tools
版本号(14.44.35112 vs 14.44.35207,推导会找不到)、取最新、
debug_nonredist永不返回(不可再分发)、没有 redist 不算错误。
v2026.8.16.2
修复
-
装好的 msvc toolset 在
mcpp toolchain list里不出现。枚举问的是
toolchain_frontend(root / "bin", pkg),而 cl.exe 在
VC/Tools/MSVC/<ver>/bin/Host<h>/<arch>/—— 深四层。拿不到就continue,
于是每一个 msvc payload 都装得好好的、然后看不见。这个布局有三个地方需要知道:安装知道、构建知道、列表不知道 ——
一条规则被内联抄了第三份时的典型结果。三者现在共用
payload_frontend(payloadRoot, pkg, family)。2026.8.16.1带着这条(tag 打在修复之前),其余部分不受影响 ——
install / build / default / remove 都是好的,只有 list 少一行。
v2026.8.16.1
工具链
-
MSVC 不再是「唯一版本无法声明」的工具链。
gcc / llvm 由 mcpp 自己安装、按声明解析;MSVC 是体系里唯一的例外——每一个
msvc spec 都是系统 spec,manifest 写了版本也会被丢掉。后果不是不够优雅,是
同一份源码在两台机器上会被不同的编译器编译,而且不报错。xrgui#3 实测:同一轮 CI 里 mcpp 用 14.51、xmake 用 14.52,直到 14.51 触发
ICE 才暴露。导出完整 vcvars 环境无效,最后只能在 CI 里把vswhere.exe
挪开,逼 mcpp 落到VSINSTALLDIR那一步。现在由 spec 的版本轴决定来源,两条来源并存:
spec 来源 用哪个编译器 msvc@system(或裸msvc)机器自己的 Visual Studio 这台机器上装的那个 msvc@<toolset>(如msvc@14.44.35207)mcpp 安装的 xlings payload 声明的那个,每台机器都是 msvc@<toolset>与gcc@16.1.0在每个方面都同构:多版本共存、
toolchain remove msvc@<toolset>可卸载、manifest 里写了就自动安装。
payload 自带编译器、STL,并通过xim:windows-sdk依赖带上 ucrt/um 头与库,
机器上什么都不必预装。实现上,「获取」与「解析」被拆成两条正交的轴:获取与 gcc 共用一条
(xim 安装),解析与msvc@system共用一条(installation_from_tools_dir)。
所以受管 toolset 不是第二条代码路径,也就不会长出自己的 bug。 -
⚠️ 破坏性变更:msvc@19.44不再是 pin-verify。它过去表示「用系统 MSVC,并校验 banner 前缀」——而且只有
mcpp toolchain default会校验,构建路径完全忽略它。版本轴现在到处都表示
toolset。写成19.x时,mcpp 会用这台机器自己的 cl 版本说清楚,并给出两个
替代写法(msvc@system或该机器实际的 toolset 版本)。 -
VSINSTALLDIR现在优先于 vswhere 探测。vswhere 在几乎每台开发机上都能返回点什么,于是
VSINSTALLDIR事实上不可达:
一次已经导出了完整 vcvars 环境的构建,仍然用 vswhere 排第一的那个编译器。
猜测不该压过答案。 顺带给 vswhere 加了-prerelease——没有它,只装了
Insiders 的机器会被报告成「没有 MSVC」,而磁盘上明明有一个可用的 cl.exe。VS*COMNTOOLS仍排在 vswhere 之后:那是机器全局的残留(2017 的
VS150COMNTOOLS不该压过当前安装),而VSINSTALLDIR是有人为这个 shell
设的。 -
Windows SDK 不再只认两个写死的绝对路径。
顺序改为:
WindowsSdkDir(+WindowsSdkVersion,vcvars 本来就导出这两个)
→ 受管 toolset 在 mcpp 自己 store 里的xim:windows-sdkpayload
→ 原来的绝对路径(降为回退)。第二条不需要任何配置:编译器自己的路径就说明了它来自哪个 store,
SDK 是它在那里的邻居。所以 mcpp 里没有任何地方写死 SDK 版本。
接口一致性
-
cxx_runtime = "self-contained"在 MSVC 上真的生效了。过去两个旋钮只有一个管用:
linkage = "static"确实发/MT,而
cxx_runtime = "self-contained"报「未实现」——对着同一个物理开关。
两处注释也互相矛盾(flags.cppm:605说发了/MT,distribution.cppm:202
说「根本没有 /MT」)。在 MSVC ABI 上这两条不是可以二选一的旋钮:
/MT把 C 运行时和 C++ 运行时
从同一个库里链进来,它们本来就是一个开关。现在两种写法都选中它,由
msvc_wants_static_crt()统一推导——项目的 TU 与 std 模块问的是同一个函数,
不再各写各的表达式(#422 正是这样分叉的)。默认仍是
/MD:判据取的是manifest 里写下的字面值,不是解析后的
contract——后者对多数 role 默认就是 self-contained,拿它做判据会把每一个
Windows 构建都翻成/MT。MSVC 的 CRT 模型是整个项目的属性(一个项目只编一份 std 模块,cl 把
_MSVC_MT/_MSVC_MD烤进去),所以按 role 覆盖会被明确拒绝并说明原因,
而不是在 ucrt 头文件里炸出 C5050/C2375。
测试
- msvc 的发现逻辑第一次可以在 Windows 之外测试:
installation_at()接受目录
而不是去探测机器,find_windows_sdk()接受 root 列表。6 个新单测在 Linux CI
上跑真实的 fixture 目录树,包括「两个 toolset 都在,要老的那个」这条——
「取最新」的实现会在这里失败。 - 新增 e2e
239_msvc_managed_toolset.sh。它的每一条断言都写成
系统编译器来应答就会失败:cl.exe 必须在 mcpp 的 store 里、toolset 目录
必须是 spec 声明的那个、同一台机器上换回msvc@system必须仍然解析到系统
的 cl(两条来源互不污染)。
v2026.8.15.3
(no CHANGELOG entry found for 2026.8.15.3)
v2026.8.15.2
(no CHANGELOG entry found for 2026.8.15.2)
v2026.8.15.1
(no CHANGELOG entry found for 2026.8.15.1)
v2026.8.11.3
修复
-
⚠️ 回归:产物加载的库不是它链接的那一份 ——$ORIGIN被 SubOS 库视图遮蔽。2026.8.11.2(PR #413)首次把 SubOS 库视图(farm)写进产物的DT_RPATH,但它
落在$ORIGIN之前。于是 imgui/GLFW 应用链接的是 mcpp 从compat.x11源码
构建、部署到产物目录的libX11.so,运行期加载的却是 farm 里 xlings 装的
xim:libX11—— 链接期用 A,运行期加载 B,程序在 main 之前就死:undefined symbol: _ZNKSt13runtime_error4whatEv真因不是「放错了位置」,而是一条链接命令行的顺序由两个互不知情的生产者用
+=决定:flags.cppm把 farm 拼进全局 ldflags(并注释「so it is LAST」),
plan.cppm把$ORIGIN拼进 per-unit,而每条链接规则渲染的是
$ldflags $unit_ldflags。三处各自都对,合起来是错的。新增
mcpp.build.link_line:把 per-unit 尾部声明成具名槽位,相对顺序写在
类型里、由单测钉死。新增一个生产者必须先选一个槽 —— 而"选"正是"在产物自己的
目录之前还是之后"这个问题被提出来的地方。 -
⚠️ 共享库不再把自己的 C++ 运行时导出给别人(ELF)。SharedLibrary此前与可执行文件共用Distributable角色,于是拿到同一份
self-contained 契约:-static-libstdc++。在 ELF 上这不是"私有一份" —— 只有一个
全局符号命名空间,共享对象会导出它定义的每一个全局符号。一个纯 C 的 compat 包
因此导出了 777 个 GLOBAL 标准库符号(libXau.so:39KB 的 Xau + 9.5MB 的 libstdc++)。可执行文件链接时
-lX11排在驱动的-lstdc++之前,ld 就用它满足了
std::runtime_error::what(),归档成员从不拉入 —— 可执行文件的
-static-libstdc++变成空操作,它的 C++ 运行时事实上是那个.so。上一条的
库替换之所以致命,根源在这里。共享库默认契约改为按目标格式分档:ELF
toolchain-coupled,
Mach-O / PE 维持self-contained(两者都没有这个危害 —— Mach-O 的机制本就是
-load_hidden,PE 没有全局命名空间)。显式cxx_runtime = { shared = "…" }
仍可选回自包含,此时自动补-Wl,--exclude-libs,让内嵌的运行时留在动态符号表之外。实测(helloegui,imgui + GLFW + X11):
libXau.so9.5MB → 39KB,
libX11.so导出 std 符号 2931 → 0,GUI 正常启动。
内部
-
dist::default_contract从没有任何生产调用方变成唯一真源:角色→契约的策略
此前在flags.cppm被第二次推导,而这正是distribution.cppm开篇声讨的那类债
(「used to be derived independently in five places」)换个位置复发。 -
e2e 219 的断言由「farm 是最后一个绝对路径条目」收紧为「字面最后一项」,
并补一条行为不变量(LD_DEBUG=libs实测同名 SONAME 解析到$ORIGIN)。
旧断言把$ORIGIN过滤掉了,对坏顺序与好顺序给出同一个结论 —— 它在一个 farm
并非最后的二进制上报告「farm is last」。测试工程也从int main()换成消费依赖
共享库,否则它连$ORIGIN都不产生,整条断言链是空转的。