fix(ci): windows-2025 with the MSVC toolset pinned — both axes, not a trade - #388
fix(ci): windows-2025 with the MSVC toolset pinned — both axes, not a trade#388Sunrisepeak wants to merge 4 commits into
Conversation
… trade #385 pinned the windows leg to `windows-2022` to get past MSVC STL 14.51 rejecting `huxerui.huxerui`, and that pin cost three members: `vulkan`, `eui-neo-vulkan` and `vulkan-hpp-module` began failing at 0xC0000135. It was a trade, and it was avoidable. THE MISTAKE WAS TREATING ONE LABEL AS ONE AXIS A windows job depends on the image for two unrelated things, and both were being chosen with a single word: * the Vulkan LOADER — `vulkan-1.dll` is not a Windows component, it arrives with a GPU driver or the SDK. Measured: present on 2025 and on latest, ABSENT on 2022. * the MSVC STL clang compiles against — 14.51 instantiates a vectorized `std::find` for a 24-byte type and static_asserts (mcpp#609). Choosing an old image fixed the STL and lost the loader. They are separate axes and only looked joined because both were read off the label. The probe that settled it printed the image inventory before building anything, and the inventory is the finding: `windows-2025` carries THREE toolsets — 14.29.30133, 14.44.35207, 14.51.36231 — and clang simply takes the newest. SO: NEW IMAGE, OLD TOOLSET `windows-2025` for the loader, `VCToolsInstallDir` naming 14.44 for the STL. Measured on exactly that combination: mcpp test -p huxerui-module test result ok. 1 passed MSVC\14.44.35207 in the compile 6 occurrences MSVC\14.51.36231 0 The last two lines are the point. A pass alone would not distinguish "compiled against 14.44" from "compiled against 14.51 and got lucky", so the probe was built to report which STL the build actually reached. Both halves are pinned deliberately: the image because `latest` moves what is installed, the toolset because the image ships more than one and the newest is the broken one. The step throws when 14.44 is absent rather than letting a silent fallback to 14.51 return as the same static_assert three shards later, attributed to whichever descriptor changed that week — which is the failure mode the rolling label already produced here once.
The first attempt set `VCToolsInstallDir`. It was measured working — on a
job that built one member from a clean home. On the real leg it made
things worse: all five windows shards failed where before only huxerui did.
WHY IT FAILED, AND IT IS THE INTERESTING PART
`VCToolsInstallDir` steers clang's header search and nothing else. mcpp
finds MSVC STL's `std.ixx` through its own SYSTEM detection, which takes
the newest toolset on the machine. So clang compiled against 14.44's
headers while mcpp handed it 14.51's `std.ixx`, and every member using
`import std` died on:
std.ixx:126:10: fatal error: 'flat_map' file not found
`flat_map` is in 14.51's STL and not in 14.44's. The probe never saw this
because it built `huxerui-module` alone; the failure needs a member that
precompiles the std module, which is most of them.
CHANGE THE MACHINE, WHICH IS WHAT mcpp SAYS THE LEVER IS
src/toolchain/msvc.cppm, on the SYSTEM origin: "everything about which
toolset this picks is a property of the machine, not of the caller." There
is no env override — `MCPP_TOOLCHAIN` selects a toolchain, not an MSVC
toolset — and the caller-side pin, `msvc@<toolset>`, lives in a manifest.
Per-package is the wrong granularity here: this has to hold for every
member on the leg, and no member should have to know that an STL on the
runner is broken.
So 14.51 is renamed aside. mcpp's detection and clang's own then both land
on 14.44 and cannot disagree — one lever instead of two that only looked
equivalent.
The runner is ephemeral, so the change dies with the job. The step throws
when 14.44 is missing rather than letting a silent fallback to 14.51 come
back as the static_assert three shards later.
Renaming in place did not hide it. mcpp enumerates every child of
`VC\Tools\MSVC`, so `14.51.36231.disabled-see-mcpp-609` was still a
candidate — and still sorted last, so still chosen. The log named it
outright:
...\14.51.36231.disabled-see-mcpp-609\modules\std.ixx:12:10:
fatal error: 'assert.h' file not found
which is the same header/std.ixx split as before, just with a longer path.
The rename changed what the directory is called and not what it is.
`Move-Item` to RUNNER_TEMP instead. What made the first attempt wrong is
worth keeping: "newest wins" is a scan of that directory, so the only way
to take a toolset out of the running is to take it out of the directory.
…d each other
Moving 14.51 out settled mcpp and broke clang. Each lever was measured
alone, and each failed in its own half:
* `VCToolsInstallDir` alone — clang used 14.44's headers, mcpp kept
scanning and handed it 14.51's std.ixx:
std.ixx:126: fatal error: 'flat_map' file not found
* moving 14.51 out alone — mcpp picked 14.44, and clang's own MSVC
detection (which goes through the VS installation's registration, and
that still names 14.51) found nothing. The compile came out with NO
include paths at all:
std.ixx:12: fatal error: 'assert.h' file not found
`assert.h` is a UCRT header, and the command line carried neither
/imsvc nor -I — clang was not looking in the wrong place, it had no
place to look.
mcpp SCANS the toolset directory; clang READS the environment. Neither
reads the other's answer, so there is one lever per consumer and both are
needed. Setting both is not redundancy.
That took three rounds to land and the intermediate states are in the
step's comment, because each of them looks like a reasonable thing to try
and each is wrong in a way only the next failure shows.
|
Closing: the approach does not converge, and the measurements say why. Four rounds, each fixing the previous one and each exposing a consumer that resolves MSVC a different way:
Round 4 traded three Vulkan failures for one OpenSSL failure. The same shard is The lesson is in round 4: an image's VS installation has more consumers than the two I was steering. mcpp scans, clang reads the environment, and everything that shells out to The Vulkan problem has a better answer anyway
Giving it a Windows branch takes the image out of the question entirely: Follow-up: openxlings/xim-pkgindex — a Windows branch for |
…dows box The linux payload was built by hand in an xlings subos, so that it links our glibc rather than the host's. Windows has neither that problem nor that machine: there is nothing to seal against, and no Windows box to build on. A runner is the honest way to produce it. WHY A WINDOWS PAYLOAD IS WANTED AT ALL `vulkan-1.dll` is not an OS component. It arrives with a GPU driver, with LunarG's Vulkan Runtime redistributable, or bundled beside an application — it is Apache-2.0 and redistributing it is the ordinary arrangement, which is what that redistributable exists for. So a machine with no GPU driver has no loader, and anything linking `vulkan-1.lib` dies at process start with 0xC0000135 before `main`. That is how `vulkan`, `eui-neo-vulkan` and `vulkan-hpp-module` fail on a `windows-2022` runner in mcpp-index while passing on images that happen to carry a driver. Four rounds of pinning images and MSVC toolsets were spent trying to find a runner where both that and everything else worked (mcpplibs/mcpp-index#388, closed). Shipping the loader takes the image out of the question instead. The loader still finds ICDs through the registry, so a driverless machine enumerates no devices. That is correct and it is enough: loader-level calls are answered by the loader alone, and that is the whole surface the consuming test asserts. WHAT THE JOB CHECKS BEFORE PUBLISHING Upstream's `update_deps.py` pins Vulkan-Headers, because a loader built against whatever headers are lying around advertises a version its trampolines may not implement. Staging then LOADS the DLL it just built and resolves `vkEnumerateInstanceVersion`, `vkCreateInstance` and `vkGetInstanceProcAddr`. An artifact that exists is not an artifact that works, and a driverless runner can still answer that much — no instance required. Layout is bin/vulkan-1.dll + lib/vulkan-1.lib: each platform's own convention, not the linux one repeated.
… on the image (#818) `vulkan-1.dll` is not an OS component. It arrives with a GPU driver, with LunarG's Vulkan Runtime redistributable, or bundled beside an application — it is Apache-2.0 and redistributing it is the ordinary arrangement, which is what that redistributable is for. So a machine with no driver has no loader at all, and anything linking `vulkan-1.lib` dies at process start with 0xC0000135 before `main`. Measured rather than assumed: `vulkan`, `eui-neo-vulkan` and `vulkan-hpp-module` fail exactly that way on a `windows-2022` runner in mcpp-index, and pass on images that happen to carry a driver. Four rounds of pinning images and MSVC toolsets went into hunting for a runner where that and everything else worked (mcpplibs/mcpp-index#388, closed). This takes the image out of the question instead. BUILT ON A RUNNER, AND CHECKED BEFORE PUBLISHING The linux payload is hand-built in an xlings subos so it links OUR glibc. Windows has neither that problem nor that machine, so the artifact comes from xlings-res/vulkan-loader's `build-windows.yml`: upstream's own `update_deps.py` pins Vulkan-Headers (a loader built against stray headers advertises a version its trampolines may not implement), and staging LOADS the DLL it just built and resolves vkEnumerateInstanceVersion, vkCreateInstance and vkGetInstanceProcAddr. An artifact that exists is not an artifact that works. THREE THINGS THE WINDOWS PATH DOES DIFFERENTLY, EACH FOR A REASON * No `selfcontain.seal`: it rewrites ELF RPATH and a PE has no such field. The loader imports only kernel32/advapi32/cfgmgr32, which ARE OS components — unlike itself. * No `sysroot.declare_libs` / `exports.runtime.libdirs`: the first builds a linker view for ELF, the second is read by xlings's elfpatch, which has nothing to patch. A PE resolves imports by NAME against the exe's directory and the search path. * So: PATH. `${pkgdir}/bin` via `subos.env`, with `prepend` rather than `set` — PATH is a list and more than one provider is entitled to be on it, and a `set` beats every other provider's prepend. A placeholder rather than an absolute path, as the spec requires. WHAT IT DOES NOT DO It ships no ICD. The loader finds drivers through the registry, so on a machine with no GPU driver `vkEnumerateInstanceVersion` answers and device enumeration comes back empty. That is the honest state of such a machine, and it is what a headless runner can be held to — which is also the whole surface the consuming test asserts. Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
Fixes the three Vulkan failures #385 introduced, without giving back the huxerui fix it bought.
The mistake was treating one label as one axis
A windows job depends on the image for two unrelated things, and both were being chosen with a single word:
vulkan-1.dll)vulkan,eui-neo-vulkan,vulkan-hpp-module— they load it or fail at0xC0000135huxerui.huxerui— 14.51 instantiates a vectorizedstd::findfor a 24-byte type andstatic_asserts (mcpp-community/mcpp#609)windows-2022fixed the STL and lost the loader. That is a trade, and #385 merged it knowingly — but the axes only looked joined because both were read off the label.What the probe found (#387)
The probe printed the image inventory before building anything, and the inventory is the finding:
The image already carries a usable STL. Nothing but clang's newest-wins default was choosing the broken one.
Full matrix from that run:
vulkan-1.dllwindows-latestwindows-2025windows-2022windows-2025+VCToolsInstallDir=14.44The change
windows-2025for the loader; a step that pointsVCToolsInstallDirat 14.44.35207 for the STL.VCToolsInstallDiris what a developer prompt exports and what clang's MSVC detection reads.Measured on exactly that combination:
The last two lines are the point: a pass alone would not distinguish "compiled against 14.44" from "compiled against 14.51 and got lucky", so the probe was built to report which STL the build actually reached.
Both halves are pinned deliberately
The image because
latestmoves what is installed. The toolset because the image ships more than one and the newest is the broken one.The step throws when 14.44 is absent rather than falling back silently. A fallback to 14.51 would return as the same
static_assertthree shards later, attributed to whichever descriptor changed that week — which is precisely the failure mode the rolling label produced here once already.Follow-up
tmp/windows-image-probe(#387) can be closed and the branch deleted once this lands; it exists only to produce the table above.