Skip to content

build: add a Windows/MSVC mcpp build alongside xmake - #3

Closed
Sunrisepeak wants to merge 29 commits into
masterfrom
feat/mcpp-windows
Closed

Sunrisepeak wants to merge 29 commits into
masterfrom
feat/mcpp-windows

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 12, 2026

Copy link
Copy Markdown
Owner

用 mcpp 为 XRGUI 增加 Windows/MSVC 构建,与现有 xmake 构建并存。只有 Windows + MSVC,Linux/GCC 不在本 PR 范围内。

基线是上游 master(6d63715),mcpp.toml 是对 xmake.lua转写而非第二套意见——两者若有差异,那是转写的 bug。

加了什么

文件 作用
mcpp.toml 工程清单,逐条对应 xmake.lua 的 target / 依赖 / flags
build.mcpp 资产 codegen,对应 xmake 的 media.svg_to_bin 规则 + before_build + after_build
mcpp/mo_yanxi_{utility,vulkan_wrapper,react_flow}/mcpp.toml 三个 submodule 各自的清单
mcpp/patches/mo_yanxi_vulkan_wrapper.patch 一处 import 提出条件块(6 行)
.github/workflows/mcpp-windows.yml Windows/MSVC CI:构建库、example,并构建并运行 tests
.agents/docs/2026-08-12-mcpp-windows.md 每处改动的理由,以及查证过程

清单放在 mcpp/ 而不是 submodule 里,因为那是别人的仓库;将来上游化时把文件挪进
submodule 根、去掉 ../../external/ 前缀即可,其余不变。

源码改动分两类,请分开审

一、mcpp 需要的(5 处)

前三处同一个原因:mcpp 的 M1 扫描器禁止条件预处理块里出现 import,也不支持头单元。

  1. 删除 XRGUI_FUCK_MSVC_INCLUDE_CPP_HEADER_IN_MODULE(16 个文件,全是删除)。这个宏在
    #include <x.h>import <x.h>; 之间二选一,mcpp 两种都不行,所以 #include
    分支转正。xmake.lua 的注释本来就写着「msvc 新版好像没这问题了,哪天删了」。
    对 xmake 零行为变化——它走的本来就是这个 include。
  2. submodule 补丁:stack_trace.cpp 里一条 import 提到 #if 之外。全仓唯一一处。
  3. magic_enum 改走索引包(它的 magic_enum.cppmimport std; 包在 #ifdef 里)。
  4. src.backends/miniaudio/audio.cppXRGUI_MINIAUDIO_IMPL_PROVIDED 守卫——
    compat.miniaudio 会编上游的 miniaudio.c,再实例化一次会让每个 ma_* 重复定义。
    xmake 不定义这个宏,其构建逐字节不变。
  5. src/graphic/msdf.{cpp,ixx}#define MSDFGEN_USE_CPP11XRGUI_MSDFGEN_NO_CPP11
    守卫。compat.msdfgen 按该宏关闭构建,并通过每个公开头都包含的
    msdfgen-config.h 告知消费者——那正是这个包让两边对「哪些声明存在」达成一致的机制。
    自己 define 就单方面破坏了它:重载在我们的 TU 里被声明、库里却没有,链接期才以
    unresolved external symbol Contour::addEdge(EdgeHolder&&) 暴露。xrepo 的 msdfgen
    是开着这个宏构建的,所以 xmake 保持原样、逐字节不变。

二、和 mcpp 无关的(3 处):上游 master 在今天的 MSVC 上编不过

这三处两条腿都需要。上游那次绿的 run(2026-06-24)用的是 MSVC 14.52.36510
今天 aka.ms/vs/18/insiders 给的是 14.52.36629,后者收紧了模块边界的名字泄漏。
这一点用一个只含一个 markdown 文件的探针分支(#4)在未改动的 master 上单独验证过。
改动都只是把本来就在用的东西显式写出来:

  1. typesetting.rich_text.argument.ixximport mo_yanxi.math.vector2;
    (否则 error C2039: 'vec2' is not a member of 'mo_yanxi::math'
  2. typesetting.segmented_layout.ixx#include <gch/small_vector.hpp>
  3. 两处绕开 gch 迭代器rich_text_fallback_style::operator=== default 改成
    写出函数体、按 data()/size() 比较 featurestypesetting.ixx
    feature_stack_ = { features.begin(), features.end() } 改成 assign(data(), data()+size())

第 8 条最不直观,也最花时间:layout_config 的 defaulted operator== 逐成员走到一个
gch::small_vector,而 gch 把容器的 operator== 和迭代器的 operator-(a, b) 都写成
自由函数模板而非 hidden friend,于是它们不是 decl-reachable 的,被连同整个全局模块
片段一起丢出 BMI。而 defaulted 比较是在 odr-use 处合成的,所以每个比较
layout_config 的 TU 都会炸,报错却指向 ui.util.ixx:140try_modify 的定义处),
和该改的文件不是一个。

判据是:gch 容器的迭代器只要出现在「会在别的模块里实例化」的代码里(类模板成员、
函数模板、BMI 里 inline 体调用的模板)就会中招。在导入方逐个补 #include 是打地鼠
(修好 text_edit.ixx 就轮到 label.ixx),所以改成走裸指针,从源头消掉这个依赖。
本仓其余 small_vector 都是非模板类的私有成员或 .cpp 里的局部变量,不受影响。

CI

mcpp (Windows / MSVC) 只跑 mcpp——xmake 那条腿已经在同一个 PR、同一个镜像上跑了。
两条腿刻意保持只有构建系统不同,包括同一版 MSVC。

两条腿都是绿的(冷缓存从头构建验证),xrgui_tests真跑

[==========] Running 54 tests from 16 test suites.
[==========] 54 tests from 16 test suites ran. (378 ms total)
[  PASSED  ] 54 tests.

normalized icons: 40   compiled shaders: 17

有几件事是查证出来的,都写在 workflow 注释和 .agents/docs/ 里:

  • mcpp 版本必须钉死,下限 2026.8.15.1。 之前那串 C5050 _MSVC_MT +
    C2375 'free': redefinition; different linkage 不是缓存问题:mcpp 编 std 模块时
    一个 runtime flag 都没传,cl 于是默认 /MT,而工程 TU 是 /MD(mcpp#422)。
    而不带版本的 xlings install mcpp 装的是 2026.8.11.3,正好差一个版本。
  • cxx_runtime = "self-contained" 在 MSVC 上是空操作,mcpp 自己会说
    "not implemented for the MSVC runtime yet; using host-coupled"。已改成显式
    host-coupled(即 /MD,与 xmake 的 set_runtimes("MD") 一致)。
  • 镜像自带的 14.51 根本编不了本仓vector2.ixx(67)C1001,"IFC import
    detected"),所以 14.52 是硬要求。
  • ldflags 是逐字透传的,Windows 系统库必须用 build.mcppmcpp::link_lib()
    写成 GNU 的 -luser32 送到 MSVC 的 link.exe 只换来
    LNK4044: unrecognized option '/luser32'; ignored——它不报错、继续跑,直到最后
    235 个 unresolved external,现场完全看不出链接标志被丢了。
    link_lib/link_search 走工具链方言,user32 会变成 user32.lib-luser32
  • 探测工具要按名字解析,别问它版本。 slangc 根本没有 --version
    unknown command-line option '--version', exit=1;而 where slangc exit=0),
    用它判断装没装会把一个能用的编译器判成缺失,着色器静默跳过。
  • submodule 里的 UTF-8 BOM 会让 mcpp 认不出模块声明,报出来的却是
    error C3474: could not open output file '/interface'——因为 /ifcOutput 拿到了空值。
    详见下面第 3 条。

⚠️ 三处是替 mcpp / 索引包做事,合并前请看一眼

1. vswhere 屏蔽(.github/workflows/mcpp-windows.yml

mcpp 不接受外部指定编译器:src/toolchain/msvc.cppmfind_vs_via_vswhere()
vswhere 没带 -prerelease,Insider 实例对它不可见;而 VSINSTALLDIR 那条回退
只在 vswhere 找不到时才执行。所以光导出 vcvars 完全无效(实测:每轮白花 7 分钟,
日志里的 cl 一个字没变)。

当前 workflow 的做法是把 vswhere.exe 挪开,逼 mcpp 落到 VSINSTALLDIR
mcpp 那边改一行find_vs_via_vswhere()-prerelease,或让显式 VSINSTALLDIR
优先)就能把这一步整个删掉。构建前有断言:mcpp 若没解析到 14.52 立刻失败,
而不是十几分钟后以 C1001 告终。

2. 替索引包重新声明 Windows 链接(build.mcpp

这三个包都用 GNU 拼法的 ldflags 声明 Windows 链接,在 MSVC 下全部无效

声明
compat.glfw ldflags = { "-lgdi32" }
compat.mimalloc ldflags = { "-lpsapi", "-lshell32", "-luser32", "-ladvapi32", "-lbcrypt" }
compat.vulkan ldflags = { "-Llib", "-lvulkan-1" }

所以 build.mcpp 里用 link_lib/link_search 把它们重新声明了一遍
(vulkan 的目录在包的 lib/$VULKAN_SDK/{Lib,lib} 里挑真正存在 vulkan-1.lib
那个,不写死路径)。这是在替包做事,等索引包改成两种链接器都能消费的形式后就该删。

3. 剥掉 submodule 的 UTF-8 BOM(.github/workflows/mcpp-windows.yml

这条查得最久,因为它伪装成偶发。症状:

array_queue.ixx(191): error C3474: could not open output file '/interface'

实际命令行是 ... /ifcOutput /interface /TP ...——/ifcOutput 后面是空的
cl 于是把下一个 token 当成了输出文件名。

链条:mcpp 逐行读模块声明,而它的源码里没有任何地方处理 BOM。文件若以
EF BB BF 紧跟 export module X; 开头,扫描器就认为它不提供模块,编译边拿不到
bmi_out;但 MSVC 的 ninja 规则仍无条件发 /ifcOutput $bmi_out

「偶发」的错觉来自 BOM 分两类:后面跟 module;(GMF 行)的没事,因为
export module 在后面的行上;后面直接跟 export module X; 的才中招。
external/ 下 8 个带 BOM 的文件里只有 2 个属于后者(array_queue.ixx
array_stack.ixx),又分布在不同的 feature 构建里,所以每轮挂的步骤都不一样。

CI 现在在打完 patch 后统一剥掉。没做成 patch 文件——内容只有三个不可见字节的
patch 没法审。mcpp 侧该修的是:扫描器跳过 BOM;退一步,bmi_out 为空时不该还发
/ifcOutput

已知遗留

  • 两条腿的 MSVC build 号跟随 Insider 漂移。 只断言 14.52 这条线,具体 build 号
    漂移只打 warning——钉死等于把微软的发布节奏变成本仓的红 CI。
  • mcpp.lock 仍在 .gitignore 里。直接依赖在 mcpp.toml 里都是精确版本,
    但要更强的可复现性可以把 lock 提交并加进缓存键。
  • [profile.release] 暂不开 LTO(对应 xmake 的 build.optimization.lto)——
    预览版 MSVC + C++ 模块 + LTO,在整个构建还没稳定绿之前不值得引入这个变量。
  • tests 做成 feature 门控的普通 bin(mcpp build --features tests),而不是 mcpp
    原生的 tests/** 发现路径,因为本仓布局是 src.tests/。它链接的是整个 xrgui 库
    (xmake 那个目标只链子集),是超集。

Describes this project for mcpp on Windows only. xmake stays the reference: its
MSVC CI is green, so mcpp.toml is a transcription of xmake.lua rather than a
second opinion, and the new workflow runs both build systems in the same job so
a failure can only mean the transcription is wrong.

WHAT IS DESCRIBED, NOT CHANGED

  mcpp.toml                      the xrgui target: sources, include dirs,
                                 defines, the three bin targets, and the
                                 add_requires() list as pinned packages
  mcpp/mo_yanxi_utility/         one manifest per submodule, transcribed from
  mcpp/mo_yanxi_vulkan_wrapper/  each repo's own xmake.lua. They live here
  mcpp/mo_yanxi_react_flow/      rather than inside the submodules because those
                                 are separate upstream repositories; the globs
                                 reach back out of tree so upstreaming later is
                                 just a move
  build.mcpp                     the counterpart of xmake's media.svg_to_bin
                                 rule and its before_build hook: bin2c per SVG
                                 plus the aggregated assets_summary.h

THE FOUR SOURCE CHANGES, AND WHY EACH IS UNAVOIDABLE

1. XRGUI_FUCK_MSVC_INCLUDE_CPP_HEADER_IN_MODULE is gone (16 files, all
   deletions). The macro selected between `#include <plf_hive.h>` and
   `import <plf_hive.h>;`. mcpp's M1 scanner rejects an import inside a
   conditional preprocessor block even when the branch is inactive, and it
   cannot build header units at all -- so the include branch had to become
   unconditional. xmake.lua's own comment already said the macro was probably
   obsolete on current MSVC ("msvc 新版好像没这问题了,哪天删了"), so the define
   is dropped there too. Nothing else changes for xmake: the same include is
   what its build was already taking.

2. src.backends/miniaudio/audio.cpp guards its MINIAUDIO_IMPLEMENTATION behind
   XRGUI_MINIAUDIO_IMPL_PROVIDED. compat.miniaudio compiles upstream's own
   miniaudio.c, so instantiating the implementation again would define every
   ma_* symbol twice. xmake does not define the guard macro, so its build is
   byte-for-byte what it was.

3. magic_enum comes from the index package rather than the submodule's
   module/magic_enum.cppm, because that file also wraps `import std;` in an
   #ifdef. The package carries a scan_override for exactly this, which is
   cheaper than patching upstream.

4. mcpp/patches/mo_yanxi_vulkan_wrapper.patch hoists one import above an
   `#if defined(__cpp_lib_stacktrace)` in stack_trace.cpp -- same scanner rule
   as (1), and the only site in a submodule. Six lines, applied by CI.

TWO INTERFACE DETAILS

nanosvg: this repo writes <nanosvg/nanosvg.h> because xrepo installs the headers
one directory deeper than upstream keeps them. compat.nanosvg follows upstream,
so [generated_files] emits two forwarding headers instead of touching every call
site.

VMA: taken from the checked-out submodule, not compat.vulkan-memory-allocator,
because vk_wrap/util/vma.cpp already defines VMA_IMPLEMENTATION.

CI

.github/workflows/mcpp-windows.yml reuses the upstream workflow's environment
verbatim -- VS 2026 Insider with MSVC 14.52 preview, Vulkan SDK, slang, and the
same asset-generation tasks -- then builds with xmake first and mcpp second, and
runs xrgui_tests.
…explicitly

xmake.lua lists ./external/stb and ./external/VulkanMemoryAllocator/include at
the repo root; neither directory exists there. The stb headers actually live in
external/include, and VMA's are inside the vulkan_wrapper submodule -- which is
where <vk_mem_alloc.h> resolves from today, through that target's public
includedir. Naming VMA's real path here as well keeps the root build independent
of how a path dependency propagates include dirs.
compat.mimalloc and friends declare their Windows link libraries as -lpsapi
rather than psapi.lib, and those descriptors pass mcpp-index's Windows CI. Use
the proven spelling instead of assuming ldflags reach link.exe verbatim.
Every run installs VS 2026 Insider and the Vulkan SDK before it compiles
anything, so a stale run left alive after a new push burns a long slot for a
result that is already obsolete.
build_and_dispatch.yml already builds this PR with xmake, on the same runner
image and the same MSVC. Repeating that here cost another VS 2026 Insider
install per run and told us nothing the other workflow was not already saying.

xmake is not installed at all now. Its xrgui.gen_slang / xrgui.gen_icon tasks
are thin wrappers around properties/build_util/slang_builder.py and
properties/assets_raw/svg_normalize.py, so the job calls those directly and the
generated assets are identical.
All four are in xim-pkgindex, and mcpp already reads that registry to resolve
its own toolchains and packages -- so the CI was hand-rolling downloads for
tools the ecosystem already publishes. One `xlings install` replaces two
setup-* actions and two Invoke-WebRequest blocks, and the versions now come from
the same index the build itself uses.

The asset scripts are invoked with `python` rather than `py`: the Windows
launcher comes with actions/setup-python, not with the xlings package. The
install step checks each tool resolves on PATH so a miss fails there instead of
three steps later.

Vulkan SDK and MSVC stay as they were -- neither is in the index.
The installer edits the user profile and prints 'restart terminal to refresh
PATH'. Appending to GITHUB_PATH only reaches later steps, so calling xlings in
the same step failed with 'The term xlings is not recognized'. Set $env:PATH
too, and locate the directory by finding xlings.exe rather than guessing --
~/.xlings has both bin/ and subos/default/bin/.
With the feature branch listed under `push` and a PR open, each push started
two runs -- one for refs/heads/feat/mcpp-windows and one for refs/pull/N/merge.
They did not cancel each other because the concurrency group keyed on
github.ref, which differs between the two.

Now push only covers master and everything else arrives through pull_request,
and the group keys on head_ref when there is a PR.
xmake's xrgui.gen_icon and xrgui.gen_slang are thin wrappers around
properties/assets_raw/svg_normalize.py and properties/build_util/slang_builder.py.
build.mcpp now calls both, so `mcpp build` is self-contained and a local
checkout gets the same assets CI does instead of needing a separate step.

Both are skipped, with a message, when their tools are missing -- python for the
icons, python and slangc for the shaders. That is deliberate: the tree still has
to build without them, which is what the empty assets_summary.h is for.

FAILURE IS A WARNING, NOT AN ABORT

Their exit status cannot be trusted. svg_normalize.py returns 0 even when the
npx child crashes -- observed locally, where Node fails with ERR_DLOPEN_FAILED
and the script still exits 0. Aborting on a non-zero status would therefore be
both useless (it does not fire) and harmful (a machine with a broken Node could
not build at all). So build.mcpp warns and continues, and CI gains an
"Assert generated assets exist" step that checks the OUTPUTS -- the only signal
here that means anything.

CI DROPS TWO STEPS

The separate asset-generation step is gone, and so is `pip install tomli`:
slang_builder.py uses Python 3.11+'s built-in tomllib and only falls back to
tomli on older interpreters. xlings installs 3.13. That line was inherited from
the upstream workflow, whose own comment says it was an assumption to adjust.

For the record, since it is easy to assume otherwise: mcpp itself needs neither
Python nor Node. It is a self-contained binary and the C++ build needs only the
toolchain. Both are purely for asset generation, and Node's single use is the
oslllo-svg-fixer npm package, which runs a browser engine to flatten SVG strokes
into fills so msdfgen can consume them.
Sunrisepeak added a commit to mcpplibs/mcpp-index that referenced this pull request Aug 12, 2026
…MSVC (#208)

Both declared compiler flags that only GCC and Clang understand in their COMMON
cflags, so MSVC received them verbatim. Found while building XRGUI
(Sunrisepeak/xrgui#3) with mcpp on windows-latest with MSVC 14.5x:

    cl : Command line error D8021 : invalid numeric argument
                                    '/Wno-implicit-function-declaration'

That is compat.freetype, and it is fatal -- the package cannot build on Windows
at all. Every consumer goes down with it: harfbuzz's FreeType bridge, msdfgen's
ext/import-font, and anything drawing text.

compat.zlib's is the quieter and worse-behaved half:

    cl : Command line warning D9002 : ignoring unknown option '-include'
    cl : Command line warning D9024 : unrecognized source file type
                                      'mcpp_zlib_config.h', object file assumed

cl does not reject `-include`; it warns, drops the flag, and builds. So
mcpp_zlib_config.h was never included and the package compiled with a silently
different configuration than the recipe describes. On Windows that header is
empty by construction (everything in it is behind `#if !defined(_WIN32)`), so
nothing was actually miscompiled this time -- but the mechanism would not have
told us if it had been.

THE FIX

Both descriptors already had per-OS sections; the platform-specific flags now
live in them.

  compat.freetype  common cflags keep only the FT_* defines. cl accepts -D, so
                   those reach MSVC unchanged.
                   -Wno-implicit-function-declaration -> linux + macosx
                   -D_DARWIN_C_SOURCE -> macosx. It was on every platform,
                   which was wrong on Linux too, just harmlessly so.

  compat.zlib      -D_GNU_SOURCE -> linux; -include mcpp_zlib_config.h ->
                   linux + macosx. Windows needs neither. Also switched to the
                   two-element {"-include", "file"} form the rest of the index
                   uses, rather than one string with an embedded space.

VERIFIED

Linux: tests/examples/freetype, msdfgen and harfbuzz all still pass (1 passed,
0 failed each) -- msdfgen and harfbuzz because they are freetype's consumers and
a fix that only satisfies Windows would be no fix at all.

Windows: the point of the change; this repo's windows workspace leg builds
tests/examples/freetype, and it only rebuilds members a PR touches -- which is
why the defect survived until a project outside this repo pulled freetype on
MSVC.

THE SAME DEFECT, NOT TOUCHED HERE

Sweeping every descriptor for platform-specific flags in common cflags turns up
four more, all with a windows xpm section, so all reachable on MSVC today:

  compat.lua               -include mcpp_lua_platform_config.h
  compat.godot-cpp         -include cstdlib
  compat.redis-plus-plus   -include cstdint
  compat.eui-neo           -include mcpp_eui_backends.h, -fno-char8_t

Left alone deliberately: each needs its own judgement about what the MSVC
equivalent should be (/FI, /Zc:char8_t-) or whether the flag is needed there at
all, and I have no evidence about those packages on Windows the way I do for
these two. Flagging rather than blind-editing.

(The X11 packages also carry -D_GNU_SOURCE in common cflags. cl accepts -D and
those packages are Linux-only, so it is untidy rather than broken.)
…ild does

xmake copies properties/assets and properties/vk_layer_settings.txt into the
target dir once the build finishes, so the image can find its data. Without it
the CI artifact is an executable with nothing to load.

build.mcpp cannot do this itself. It runs during prepare, and the binary
directory is not in the environment contract -- ${mcpp.bin_dir} exists only as
an action interpolation. So this declares two `role = "artifact"` actions, whose
inputs are link outputs and which therefore run after the link.

One command copies the whole tree, but every output has to be named: mcpp fixes
the file set during prepare. That is workable here because the set is five files
and because the generated shaders are already on disk by this point -- the
generators run earlier in the same build.mcpp.

Verified on Linux: both edges appear in build.ninja as ARTIFACT nodes, all five
outputs register as default targets, and running them puts assets/ and
vk_layer_settings.txt in bin/.

Two more lines from the same xmake block:

  add_syslinks(...)                       already in [target.windows.build] ldflags
  set_policy("build.optimization.lto")    [profile.release] lto = true, NOT enabled

lto is supported -- confirmed by watching it reach the compiler -- but a preview
MSVC plus C++ modules plus LTO is three variables at once on a build that has
not gone green yet. It goes on after, where a regression can be attributed.
…act means

Two actions were one too many. A single copy command takes several sources, so
there is no reason to split a directory and a plain file across two edges. On
POSIX that is `cp -r <assets> <vk_layer_settings.txt> <bin>/`; on Windows it is
Copy-Item, which handles a directory and a file in the same call -- `copy` and
`xcopy` each only manage half of that, which is what led to the split.

Also corrects the comment. `role = "artifact"` does NOT by itself order the edge
after the link; the docs' "runs after the link" holds because such an action's
inputs are typically link outputs. These inputs are source files, and checking
build.ninja confirms the edge depends on no link output at all, so ninja may run
the copy whenever it likes. That is fine -- runtime data does not depend on the
binaries -- but the comment claimed an ordering the graph does not provide.

Forcing the ordering would mean naming a specific target as an input, and which
targets exist depends on the active features, so it would be brittle for no
gain.

Verified on Linux: one ARTIFACT edge, five declared outputs, and running it
puts assets/ and vk_layer_settings.txt in bin/.
xmake.lua has set_runtimes(is_mode("debug") and "MDd" or "MD"). I missed it,
so the build ran on mcpp's default cxx_runtime -- self-contained, which is /MT
on MSVC.

That is not just a difference from xmake; it was internally inconsistent. The
std module interface came out built for the static CRT while the project's own
TUs compiled against the dynamic one:

    warning C5050: Possible incompatible environment while importing module
                   'std': _MSVC_MT is defined in module command line and not
                   in current command line
    corecrt_malloc.h(89): error C2375: 'free': redefinition; different linkage

cxx_runtime = "host-coupled" is the /MD side of that contract, which puts
std.ifc and every TU on the same runtime and matches what xmake ships.
speak-agent and others added 14 commits August 12, 2026 19:26
~/.mcpp holds compiled artifacts whose validity depends on the compile flags,
and `restore-keys: mcpp-win-` let a changed manifest still hit an older entry.
That is how the cxx_runtime fix appeared not to work: the run restored a std.ifc
built for the static CRT, so cl still reported

    warning C5050: _MSVC_MT is defined in module command line and not in
                   current command line

against TUs that were now /MD, and failed in corecrt_malloc.h exactly as before.

The key now covers build.mcpp as well, and a miss is a cold cache rather than a
near-miss.
…an MSVC nothing used

TWO SEPARATE THINGS, BOTH FOUND BY READING THE FAILING RUN.

1. src/font/typesetting.rich_text.argument.ixx uses math::vec2 without
   importing the module that declares it.

   Following its entire import closure:

     mo_yanxi.font.manager    -> export import mo_yanxi.font -> nothing
     mo_yanxi.static_string   -> nothing
     mo_yanxi.typesetting.util-> nothing
     mo_yanxi.heterogeneous   -> nothing
     mo_yanxi.graphic.color   -> export import mo_yanxi.math.vector4
                                 -> vector4 only plain-imports mo_yanxi.math

   Nothing re-exports mo_yanxi.math.vector2, which is where vec2 lives. The
   file compiled on older MSVC because names leaked across the module boundary;
   14.52 does not leak them, and it fails with

     error C2039: 'vec2': is not a member of 'mo_yanxi::math'

   This is not environment drift -- a probe branch carrying unmodified upstream
   master (#4) fails identically, on the same commit whose
   upstream run went green in June. The drift only exposed it.

   (I had seen this same error under GCC earlier and written it off as a stale
   BMI. That was wrong: GCC was reporting the same real defect.)

2. The workflow no longer installs VS 2026 Insider, and pins the runner.

   It spent roughly half an hour per run installing MSVC 14.52 preview and
   exporting its vcvars -- and then every cl invocation used the preinstalled
   14.51:

     exported by vcvars : VCToolsVersion 14.52.36629  (VS2026Insider)
     used by every cl   : .../Visual Studio/18/Enterprise/.../14.51.36231

   mcpp's `msvc@system` discovers Visual Studio itself and does not read the
   exported environment. Installing a toolchain nothing reads is not a pin, it
   is latency. The runner image (windows-2025-vs2026) already ships VS 2026
   Enterprise, so `runs-on: windows-2025` -- pinned rather than `latest` -- is
   what actually holds MSVC still.

   19 steps at the start, 11 now.
cxx_runtime = "host-coupled" (/MD, what xmake uses) does not work: mcpp builds
the std module interface with the static runtime whatever this is set to, so the
project TUs came out /MD against a /MT std.ifc and cl failed in corecrt_malloc.h
with C2375 'free': redefinition; different linkage.

Verified against a cold cache -- the previous attempt restored a stale artifact,
this one did not, and the log carries no command building std.ifc at all. It
comes from mcpp's global std cache, built /MT.

So follow std rather than fight it. self-contained is /MT and is mcpp's own
default, which puts the packages, the std module and this project on one
runtime. The difference from xmake is what the artifact promises about the
machine that runs it, not semantics.

Worth reporting upstream: cxx_runtime should reach the std module build.
This workflow installed VS 2026 Insider from aka.ms/vs/18/insiders on every run
and asked vcvars for 14.52. That URL always serves the newest build, so the
toolchain moved underneath the build: the same commit that went green upstream
on 2026-06-24 (run 28114032617) fails today under 14.52.36629, and a probe
branch carrying unmodified upstream master fails the same way
(#4). A floating preview is not a pin.

The runner image (windows-2025-vs2026) already ships VS 2026 Enterprise with
MSVC 14.51.36231, so:

  - the Insider install step is gone, and with it ~30 minutes per run
  - vswhere no longer passes -prerelease, which is what selected the Insider
    instance over the preinstalled one
  - MSVC_VER is 14.51, which --vs_toolset already interpolates
  - runs-on is windows-2025 rather than windows-latest: the label is the pin,
    and `latest` would move the toolchain again

Also adds a concurrency group. Each run installs the Vulkan SDK and compiles the
whole tree, so leaving superseded runs alive costs a long slot for an answer
nobody reads.
…xact toolset

TWO THINGS, THE SECOND CORRECTING MY PREVIOUS COMMIT.

1. src/font/typesetting.segmented_layout.ixx now includes gch/small_vector.hpp
   itself.

   It instantiates algorithms over gch::small_vector_iterator but never named
   the header -- it inherited the declarations from mo_yanxi.typesetting.rich_text.
   That worked while rich_text reached the header through `import <...>;`,
   because a header unit exports what it declares. Moving that to a global
   module fragment include (which mcpp requires, since it supports neither
   header units nor imports inside conditional blocks) changed the rule: a GMF
   declaration the purview never names is discarded rather than written into
   the BMI, and operator-(it, it) is a namespace-scope template found only by
   ADL at instantiation time. So the importer stopped finding it:

     xutility(1865): error C2679: binary '-': no operator found which takes a
                     right-hand operand of type '_InIt'
                     [_InIt=gch::small_vector_iterator<const hb_feature_t*, __int64>]

   This is a regression from my header-unit removal, not drift. Including the
   header where the types are used is what modules ask for anyway.

2. The MSVC pin is by exact toolset, and 14.51 is not usable.

   My previous commit pinned to the runner's preinstalled 14.51 to stop chasing
   a floating preview. That was wrong: 14.51 does not compile this codebase at
   all --

     font.manager.ixx
     vector2.ixx(67): fatal error C1001: Internal compiler error.
     (compiler file '...\Compiler\CxxFE\sl\p1\c\template.cpp', line 26415)
     note: IFC import detected.

   -- which is presumably why the workflow reached for a preview to begin with.
   So the Insider install is back, and the pin is what it can actually be: the
   job asks vcvars for 14.52, then checks VCToolsVersion is exactly 14.52.36629
   and fails with a clear message if it is not. aka.ms/vs/18/insiders will keep
   moving; this makes it move visibly instead of silently changing the compiler
   under a green checkmark.

   runs-on stays pinned at windows-2025, and a concurrency group cancels
   superseded runs -- each one installs VS 2026 Insider and the Vulkan SDK
   before compiling anything.
The two legs were not comparing the same thing. mcpp resolves `msvc@system`
by searching vswhere, then VSINSTALLDIR / VS*COMNTOOLS, then the standard
Program Files paths -- so with no vcvars exported it found the runner image's
preinstalled VS 2026 Enterprise and built with 14.51.36231, while xmake built
with the 14.52 preview. A previous revision removed the Insider install from
this job on the belief that nothing read it; the upstream green run disproves
that plainly, every cl in it being

  C:\VS2026Insider\VC\Tools\MSVC\14.52.36510\bin\HostX64\x64\cl.exe

and 14.51 does not survive this codebase anyway (C1001 at vector2.ixx:67).

So install and export it, exactly as build_and_dispatch.yml does, and fail the
job if mcpp still reports 14.51. VCToolsVersion joins the cache key for the
same reason: ~/.mcpp holds .ifc files, and an .ifc and its consumer must come
from one compiler -- restoring a std module built by another cl is what
produced the C5050/C2375 pair this job kept dying on.

cxx_runtime follows from that. "self-contained" was chosen to match a std
module cl reported as /MT, but mcpp does not implement it for the MSVC runtime
and says so, silently using host-coupled; the setting was a no-op and the
mismatch was the cross-compiler cache. Written out as host-coupled now, which
is /MD -- what xmake's set_runtimes("MD") asks for.

Both legs now assert the 14.52 line rather than an exact build number.
aka.ms/vs/18/insiders always serves the newest Insider, so pinning the build
number turns Microsoft's release schedule into a red CI on commits that
changed nothing; the drift is worth a warning, not a failure.

Also: text_edit.ixx compares two layout_configs, whose defaulted operator==
reaches rich_text_fallback_style::features, a gch::small_vector. gch declares
that container's operator== as a free function template rather than a hidden
friend, so it is not decl-reachable from the exported class and its module
discards it along with the rest of the global module fragment. Overload
resolution then reached for small_vector's private allocator base, which 14.52
reports as C2243 against ui.util.ixx, where the template is defined. Same
shape, and same fix, as typesetting.segmented_layout.ixx.
Four source changes are for mcpp's scanner and one duplicate symbol; three more
are neither, and would be needed by the xmake leg alone -- unmodified master no
longer compiles on the Insider toolset that has replaced the one its last green
run used. Recording which is which, and the toolchain findings that cost the
most to establish: how mcpp resolves msvc@system, why VCToolsVersion has to be
in the cache key, and that cxx_runtime has exactly one reachable value on MSVC.
… the comparison at its source

Two separate things, both diagnosed from mcpp's own source rather than guessed
at from the outside.

mcpp version. The C5050/C2375 pair this job kept dying on was not a stale
cache: mcpp compiled the std module with no runtime flag at all, so cl defaulted
it to /MT while the project TUs got /MD (mcpp#422, fixed in 2026.8.15.1 by
msvc_crt_flag in src/toolchain/dialect.cppm). `xlings install mcpp` served
2026.8.11.3, one release short, and reproduced it on a provably cold cache. So
ask for the version by name, make it active, and check what ended up on PATH --
mcpp is the thing under test here and should not be whatever the index served
that hour.

MSVC. The previous commit added a VS 2026 Insider install and a vcvars export to
this job so it would compile with the same toolset as the xmake leg. It does
not work, and cannot: mcpp's discovery asks vswhere without -prerelease, so an
Insider instance is invisible to it, and the VSINSTALLDIR fallback that a
developer command prompt would set is only consulted when vswhere finds nothing
(src/toolchain/msvc.cppm). The build log named Enterprise 14.51.36231 either
way -- the install was seven minutes a run for no change in what compiled. It is
gone again, this time with the measurement written down, and the divergence
between the two legs recorded where someone will find it rather than papered
over. Closing it is a one-line change in mcpp, not here.

The cache key now carries the *detected* toolset instead of a literal, so a
runner image that moves MSVC gets a cold cache rather than a std module built by
a compiler that is no longer installed.

Source: rich_text_fallback_style::operator== is written out instead of
defaulted. A defaulted comparison is synthesized wherever it is odr-used, which
is every importer that compares two layout_configs -- text_edit.ixx, label.ixx,
and one more each time somebody adds a setter. None of them can see gch's
operator== for small_vector (a free function template, so not decl-reachable
from the class that holds one), and each resolves the comparison to
small_vector's private allocator base instead: C2243, reported against
ui.util.ixx where try_modify is defined rather than against the file that would
have to change. Spelling the body binds those names once, in the module whose
global module fragment includes the header. The include added to text_edit.ixx
in the previous commit is reverted -- it fixed one instantiation site out of N.
…t build this at all

The previous commit settled for the toolset mcpp picks on its own, on the
grounds that nothing here could change it. The first half of that was wrong in
a way only a run could show: the image's 14.51.36231 does not merely differ from
the xmake leg's compiler, it cannot compile this codebase.

  mo_yanxi_utility/src/utility/math/basic/vector2.ixx(67):
    fatal error C1001: Internal compiler error.
    (compiler file '...\CxxFE\sl\p1\c\template.cpp', line 26415)
    note: IFC import detected.

instantiating math::vector2<float>::ceil. That is the same ICE the xmake
workflow's comments claimed without a citation; it now has one.

The second half was right: mcpp cannot be told which MSVC to use. It probes
vswhere with no -prerelease, so an Insider instance is invisible to it, and the
VSINSTALLDIR fallback -- the only lever a caller has -- is reached only when
that probe comes up empty. So this installs the preview to a path of our
choosing, exports vcvars straight from it, and moves vswhere.exe aside so the
fallback runs. It is a workaround for mcpp and is labelled as one: a one-line
change there (pass -prerelease, or let an explicit VSINSTALLDIR win) deletes the
whole step.

The toolchain check that follows is now an assertion rather than a note, because
the job can finally decide the answer. Resolving 14.51 does not produce a
slightly worse build, it produces C1001 some minutes later, and this says so at
the point where it is still cheap to read.

Same run confirmed the other half of the previous commit: with mcpp 2026.8.15.3
the only C5050 left is _UTF8 (the std module is not built with this project's
/utf-8, which is harmless). _MSVC_MT, _MSVC_MD and _DLL are all gone, and with
them C2375 in corecrt_malloc.h.
Writing the operator out was necessary but not sufficient. `lhs.features ==
rhs.features` calls gch::operator==, which is itself a function template and so
instantiates where it is called -- in the importer -- carrying std::equal and
its need for gch's operator-(a, b) along with it. That operator is another free
function template, equally absent from the BMI, so the failure came back wearing
a different hat: a wall of C2794 / C3376 / C2062 inside <xutility> instead of
one C2243.

const hb_feature_t* has none of this: no BMI lookup, no gch templates, same
meaning. Both legs reached the identical error before this, which at least
confirms they are now compiling the same code with the same compiler.
…terators

Second site of the rule the previous commit established, and the last error left
on either leg. layout_context_impl::reset builds its std::vector<hb_feature_t>
from a pair of gch::small_vector iterators; vector's range constructor needs
operator-(a, b) to size the allocation, and that is a free function template in
gch, discarded from the BMI along with the rest of the global module fragment.
The member belongs to a class template, so it instantiates in whichever module
imports it -- label.ixx and segmented_layout.ixx here -- and none of them can see
gch's header.

The rule, for whoever hits this next: a gch iterator is fine inside the TU that
includes the header, and breaks anywhere that instantiates in another module.
Everything else in this tree holds its small_vectors as private members of
non-template classes or as locals in .cpp files, which is why only these two
turned up.
Everything compiled; nothing linked. 235 unresolved externals, and the reason
was one line above them in the log:

    LNK4044: unrecognized option '/luser32'; ignored

mcpp's ldflags reach the linker verbatim -- plan.cppm calls it "a flat string in
the link command" -- so a GNU spelling arrives at MSVC's link.exe unchanged, and
link.exe warns and carries on rather than failing. Every link flag in the
command was dropped that way. link_lib and link_search go through the toolchain
dialect instead (Transform::LibFlag / LibSearchPath), which spells `user32` as
user32.lib here and -luser32 under a GNU linker, so build.mcpp is where this
belongs.

Not all of it is ours. compat.glfw declares `-lgdi32`, compat.mimalloc
`-lpsapi -lshell32 -luser32 -ladvapi32 -lbcrypt`, and compat.vulkan
`-Llib -lvulkan-1` -- all as ldflags, all dropped for the same reason, which is
why the missing symbols ran from __imp_DestroyIcon through vkCreateDevice. Until
those packages say it in a form both linkers can consume, this repeats them,
with compat.vulkan's import library located through dep_dir rather than a
guessed path. That block is compensating for package metadata and is marked as
such.
Down to two causes from 116 unresolved externals.

vulkan-1 was never named: dep_dir("compat.vulkan") came back empty, and the
branch that handled that only printed a warning -- to a stdout mcpp reads as
directives and does not echo, so it said nothing at all. Now the search dir is
whichever of the package's lib/ or $VULKAN_SDK/{Lib,lib} actually holds
vulkan-1.lib (the SDK being what mo_yanxi_vulkan_wrapper's xmake.lua links
against), and link_lib is called either way. Naming a library that cannot be
found fails with "cannot open input file 'vulkan-1.lib'", which points at the
problem; the silent path produced a hundred vk* symbols that did not.

msdfgen was one symbol, Contour::addEdge(EdgeHolder&&). src/graphic/msdf.{cpp,ixx}
`#define MSDFGEN_USE_CPP11` before including the headers, but compat.msdfgen is
built with it off and publishes that through the msdfgen-config.h every public
header includes -- which is precisely the mechanism that package uses to keep
both sides agreeing on which declarations exist. Defining it ourselves broke the
agreement in one direction: declared for our TUs, absent from the library.
XRGUI_MSDFGEN_NO_CPP11 lets the package's answer stand under mcpp. xrepo's
msdfgen does build with it, so the xmake build keeps the define and is unchanged.
The library and xrgui_hello link now. What failed was the assertion after them:
40 normalized icons, 0 compiled shaders. build.mcpp gates the shader generator
on `have_tool("slangc")`, which ran `slangc --version` and took a non-zero exit
to mean "not installed" -- while the workflow's own `Get-Command slangc` had
succeeded two steps earlier. Whether slangc spells that flag differently or
exits non-zero after printing it, the two questions are not the same one, and
only the second is what the gate wants to ask. `where` / `command -v` asks it.

The workflow now records both answers next to each other at install time, so if
this is not the whole story the next run says so in one line instead of in a
missing directory.

Diagnostics move to stderr, since stdout is the directive stream. That does not
make them visible -- mcpp captures both, merged, and echoes them only when the
program fails -- and the comment says so rather than implying otherwise. The
real gate stays where it was: CI asserting on the outputs, which is exactly what
caught this.
Both legs are green. Replacing the guess in build.mcpp's comment with the
measurement that settled it: slangc has no --version at all, so probing with
one called an installed compiler missing and skipped the shader generator --
"normalized icons: 40   compiled shaders: 0". Now 40 and 17.

Also notes, where the next reader will look for it, that build.mcpp's output is
captured and echoed only on failure, so a diagnostic there is not a substitute
for CI asserting on the outputs.
The intermittent failure was not intermittent. mcpp reads module declarations
line by line and nothing in its source touches a byte-order mark, so a file
opening with EF BB BF immediately followed by `export module X;` provides no
module as far as the scanner is concerned. Its compile edge then gets no
$bmi_out while the MSVC rule still emits `/ifcOutput $bmi_out`, and cl reads the
next token as the filename:

    array_queue.ixx(191): error C3474: could not open output file '/interface'

Eight files under external/ carry a BOM, but only two open with the module
declaration -- array_queue.ixx and array_stack.ixx. The rest open with `module;`
and put `export module` on a later line, where the BOM cannot reach it. Those
two land in different feature builds, which is why consecutive runs failed at
different steps and one run passed outright.

Stripped rather than patched: a patch whose entire content is three invisible
bytes is not reviewable. Safe, because /utf-8 is on the compile line and nothing
is inferring the encoding from a mark.
Sunrisepeak added a commit to openxlings/xim-pkgindex that referenced this pull request Aug 16, 2026
… the curl dep (#628)

* feat(msvc): add 14.52.36629, and qualify the curl dependency now that it is published

Two things, and the first one matters more than it looks.

**14.52.36629 — the toolset xrgui actually needs.** The package shipped with
14.44.35207 only, and that does not build Sunrisepeak/xrgui#3: that tree needs
a 14.52 preview, and the toolset one step below it (14.51) does not merely
differ, it ICEs --

    mo_yanxi_utility/src/utility/math/basic/vector2.ixx(67):
      fatal error C1001: Internal compiler error.  note: IFC import detected.

so 14.44 was never going to serve it. 14.52 comes from the Insiders channel
(aka.ms/vs/18/insiders/channel), a different manifest with differently spelled
package ids -- Microsoft.VC.14.52.* rather than Microsoft.VC.14.44.17.14.*.

It carries a risk the release entry does not: Microsoft rotates Insiders
payloads. When 14.52.36629 goes, the URLs 404 -- loudly, and with the sha256
still pinned, so a rotated build cannot quietly take its place. That is the
failure mode worth having, and it is why `latest` stays on the release channel:
asking for a preview toolset should be a choice.

The payload table is now keyed by toolset, which is also what makes the
multi-version claim real rather than theoretical -- two toolsets, side by side,
`xlings use msvc <ver>` between them. Each one's directory version was read off
a real payload, not assumed: 14.44's payloads carry 35228 / 35220 / 35226 and
all unpack into 14.44.35207, while 14.52's payloads and directory agree at
36629.

**curl.** It is published, so the two bare dependencies become
`xim:curl@8.21.0` and the EXEMPT entries come out of check-dep-namespace.lua --
which is exactly what that list's own comment asks for: "REMOVE it in the
follow-up that lands after publication. An entry left behind is the bug this
check exists to catch, wearing a permit."

* fix(msvc): define payloads() after TOOLSETS, not before

    msvc.lua:116: attempt to index a nil value (global 'TOOLSETS')

A function that closes over a local has to be written below that local. Placed
above it, the name it captures is a global -- which is nil, and says so only
when the hook runs.

Nothing a syntax check can catch, and the local gates cannot either: they read
the recipe, they do not install it. windows-test does.

---------

Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
@Sunrisepeak

Copy link
Copy Markdown
Owner Author

Superseded by #5, which merged first and carried this branch's full content plus the removal of the vswhere workaround.

#5 branched from feat/mcpp-windows, so everything reviewed here — the mcpp.toml/build.mcpp transcription, the submodule manifests, the patch, the workflow — landed in master with it, and did so without the Move-Item vswhere.exe step this PR needed.

That difference is the point rather than a detail. This PR's own notes said the shadowing step was "a patch for mcpp, not something this repo should own"; mcpp#434 made an explicitly-set VSINSTALLDIR outrank the vswhere probe, and #5 verified the consequence with vswhere still present and the preinstalled Enterprise 14.51 still installed:

Resolved msvc@system → msvc 19.52.36629
  (C:\VS2026Insider\VC\Tools\MSVC\14.52.36629\bin\Hostx64\x64\cl.exe)

Nothing was hidden; the wrong answer simply had to lose. xrgui_tests 54/54, both legs green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants