build: add a Windows/MSVC mcpp build alongside xmake - #3
Sunrisepeak wants to merge 29 commits into
Conversation
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.
…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.
~/.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.
… 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>
|
Superseded by #5, which merged first and carried this branch's full content plus the removal of the vswhere workaround. #5 branched from 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 Nothing was hidden; the wrong answer simply had to lose. |
用 mcpp 为 XRGUI 增加 Windows/MSVC 构建,与现有 xmake 构建并存。只有 Windows + MSVC,Linux/GCC 不在本 PR 范围内。
基线是上游
master(6d63715),mcpp.toml是对xmake.lua的转写而非第二套意见——两者若有差异,那是转写的 bug。加了什么
mcpp.tomlxmake.lua的 target / 依赖 / flagsbuild.mcppmedia.svg_to_bin规则 +before_build+after_buildmcpp/mo_yanxi_{utility,vulkan_wrapper,react_flow}/mcpp.tomlmcpp/patches/mo_yanxi_vulkan_wrapper.patch.github/workflows/mcpp-windows.yml.agents/docs/2026-08-12-mcpp-windows.md清单放在
mcpp/而不是 submodule 里,因为那是别人的仓库;将来上游化时把文件挪进submodule 根、去掉
../../external/前缀即可,其余不变。源码改动分两类,请分开审
一、mcpp 需要的(5 处)
前三处同一个原因:mcpp 的 M1 扫描器禁止条件预处理块里出现
import,也不支持头单元。XRGUI_FUCK_MSVC_INCLUDE_CPP_HEADER_IN_MODULE(16 个文件,全是删除)。这个宏在#include <x.h>和import <x.h>;之间二选一,mcpp 两种都不行,所以#include分支转正。
xmake.lua的注释本来就写着「msvc 新版好像没这问题了,哪天删了」。对 xmake 零行为变化——它走的本来就是这个 include。
stack_trace.cpp里一条 import 提到#if之外。全仓唯一一处。magic_enum.cppm把import std;包在#ifdef里)。src.backends/miniaudio/audio.cpp加XRGUI_MINIAUDIO_IMPL_PROVIDED守卫——compat.miniaudio会编上游的miniaudio.c,再实例化一次会让每个ma_*重复定义。xmake 不定义这个宏,其构建逐字节不变。
src/graphic/msdf.{cpp,ixx}的#define MSDFGEN_USE_CPP11加XRGUI_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 上单独验证过。
改动都只是把本来就在用的东西显式写出来:
typesetting.rich_text.argument.ixx补import mo_yanxi.math.vector2;(否则
error C2039: 'vec2' is not a member of 'mo_yanxi::math')typesetting.segmented_layout.ixx补#include <gch/small_vector.hpp>rich_text_fallback_style::operator==从= default改成写出函数体、按
data()/size()比较features;typesetting.ixx的feature_stack_ = { features.begin(), features.end() }改成assign(data(), data()+size())第 8 条最不直观,也最花时间:
layout_config的 defaultedoperator==逐成员走到一个gch::small_vector,而 gch 把容器的operator==和迭代器的operator-(a, b)都写成自由函数模板而非 hidden friend,于是它们不是 decl-reachable 的,被连同整个全局模块
片段一起丢出 BMI。而 defaulted 比较是在 odr-use 处合成的,所以每个比较
layout_config的 TU 都会炸,报错却指向ui.util.ixx:140(try_modify的定义处),和该改的文件不是一个。
判据是:gch 容器的迭代器只要出现在「会在别的模块里实例化」的代码里(类模板成员、
函数模板、BMI 里 inline 体调用的模板)就会中招。在导入方逐个补
#include是打地鼠(修好
text_edit.ixx就轮到label.ixx),所以改成走裸指针,从源头消掉这个依赖。本仓其余
small_vector都是非模板类的私有成员或.cpp里的局部变量,不受影响。CI
mcpp (Windows / MSVC)只跑 mcpp——xmake 那条腿已经在同一个 PR、同一个镜像上跑了。两条腿刻意保持只有构建系统不同,包括同一版 MSVC。
两条腿都是绿的(冷缓存从头构建验证),
xrgui_tests是真跑:有几件事是查证出来的,都写在 workflow 注释和
.agents/docs/里: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")一致)。vector2.ixx(67)的C1001,"IFC importdetected"),所以 14.52 是硬要求。
ldflags是逐字透传的,Windows 系统库必须用build.mcpp的mcpp::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 slangcexit=0),用它判断装没装会把一个能用的编译器判成缺失,着色器静默跳过。
error C3474: could not open output file '/interface'——因为/ifcOutput拿到了空值。详见下面第 3 条。
1. vswhere 屏蔽(
.github/workflows/mcpp-windows.yml)mcpp 不接受外部指定编译器:
src/toolchain/msvc.cppm里find_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.glfwldflags = { "-lgdi32" }compat.mimallocldflags = { "-lpsapi", "-lshell32", "-luser32", "-ladvapi32", "-lbcrypt" }compat.vulkanldflags = { "-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)这条查得最久,因为它伪装成偶发。症状:
实际命令行是
... /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。已知遗留
14.52这条线,具体 build 号漂移只打 warning——钉死等于把微软的发布节奏变成本仓的红 CI。
mcpp.lock仍在.gitignore里。直接依赖在mcpp.toml里都是精确版本,但要更强的可复现性可以把 lock 提交并加进缓存键。
[profile.release]暂不开 LTO(对应 xmake 的build.optimization.lto)——预览版 MSVC + C++ 模块 + LTO,在整个构建还没稳定绿之前不值得引入这个变量。
mcpp build --features tests),而不是 mcpp原生的
tests/**发现路径,因为本仓布局是src.tests/。它链接的是整个 xrgui 库(xmake 那个目标只链子集),是超集。