Skip to content

Releases: mcpp-community/mcpp

v2026.8.18.2

Choose a tag to compare

@github-actions github-actions released this 18 Aug 00:36
33e4b34

新增

  • 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

Choose a tag to compare

@github-actions github-actions released this 17 Aug 20:12
df7a443

新增

  • 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.mdexamples/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,而那些符号明明在对象里。产出这个比拒绝更糟。
  • --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; 共用一个拼写,却是两种不同的声明,而扫描器把
    它们当成了一种:前者被记成**「requires M: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

Choose a tag to compare

@github-actions github-actions released this 16 Aug 21:57
dc6f903

(no CHANGELOG entry found for 2026.8.17.1)

v2026.8.16.3

Choose a tag to compare

@github-actions github-actions released this 16 Aug 14:00
e4f6c1e

修复

  • 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-sdk payload 少了带 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

Choose a tag to compare

@github-actions github-actions released this 16 Aug 08:13
987b783

修复

  • 装好的 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

Choose a tag to compare

@github-actions github-actions released this 16 Aug 07:00
26b26ec

工具链

  • 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-sdk payload
    → 原来的绝对路径(降为回退)。

    第二条不需要任何配置:编译器自己的路径就说明了它来自哪个 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

Choose a tag to compare

@github-actions github-actions released this 15 Aug 14:51
19cd960

(no CHANGELOG entry found for 2026.8.15.3)

v2026.8.15.2

Choose a tag to compare

@github-actions github-actions released this 15 Aug 14:03
f622f02

(no CHANGELOG entry found for 2026.8.15.2)

v2026.8.15.1

Choose a tag to compare

@github-actions github-actions released this 15 Aug 08:58
c459cf2

(no CHANGELOG entry found for 2026.8.15.1)

v2026.8.11.3

Choose a tag to compare

@github-actions github-actions released this 11 Aug 15:21
a749e9f

修复

  • ⚠️ 回归:产物加载的库不是它链接的那一份 —— $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.so 9.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 都不产生,整条断言链是空转的。