Skip to content

fix(ci): windows-2025 with the MSVC toolset pinned — both axes, not a trade - #388

Closed
Sunrisepeak wants to merge 4 commits into
mainfrom
fix/windows-image-and-toolset
Closed

fix(ci): windows-2025 with the MSVC toolset pinned — both axes, not a trade#388
Sunrisepeak wants to merge 4 commits into
mainfrom
fix/windows-image-and-toolset

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

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:

axis what depends on it measured
Vulkan loader (vulkan-1.dll) vulkan, eui-neo-vulkan, vulkan-hpp-module — they load it or fail at 0xC0000135 present on 2025 and latest, absent on 2022
MSVC STL clang compiles against huxerui.huxerui — 14.51 instantiates a vectorized std::find for a 24-byte type and static_asserts (mcpp-community/mcpp#609) 14.51 on 2025 and latest by default

windows-2022 fixed 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:

windows-2025:
  C:\...\Microsoft Visual Studio\18\Enterprise\VC\Tools\MSVC\14.29.30133
  C:\...\Microsoft Visual Studio\18\Enterprise\VC\Tools\MSVC\14.44.35207
  C:\...\Microsoft Visual Studio\18\Enterprise\VC\Tools\MSVC\14.51.36231   <- clang takes this

The image already carries a usable STL. Nothing but clang's newest-wins default was choosing the broken one.

Full matrix from that run:

image vulkan-1.dll huxerui-module vulkan
windows-latest present (1.3.300.0) fail pass
windows-2025 present (1.3.300.0) fail pass
windows-2022 absent pass fail
windows-2025 + VCToolsInstallDir=14.44 present pass

The change

windows-2025 for the loader; a step that points VCToolsInstallDir at 14.44.35207 for the STL. VCToolsInstallDir is what a developer prompt exports and what clang's MSVC detection reads.

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 falling back silently. A fallback to 14.51 would return as the same static_assert three 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.

… 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.
@Sunrisepeak

Copy link
Copy Markdown
Member Author

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 lever what it broke next
1 VCToolsInstallDir mcpp picks std.ixx by its own scan, not from the env — 14.51's std.ixx against 14.44's headers, 'flat_map' file not found
2 rename 14.51 in place mcpp enumerates every child of VC\Tools\MSVC; the renamed directory was still found and still sorted last
3 move 14.51 out clang's own MSVC detection goes through the VS installation's registration, found nothing, and emitted a compile with no include paths at all'assert.h' file not found
4 both levers together huxerui-module passed (154s ok) — and compat.openssl stopped building: its Windows path is Perl Configure + nmake, which reaches cl.exe through the same VS registration. windows build produced no libcrypto.lib / libssl.lib

Round 4 traded three Vulkan failures for one OpenSSL failure. The same shard is success on main at windows-2022, so that regression is this branch's.

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 cl.exe/nmake reads the registration. Moving a toolset out from under that is not a pin, it is damage.

The Vulkan problem has a better answer anyway

windows-2022 is fine for everything except the Vulkan loader, and the loader is the part that does not have to come from the image. vulkan-1.dll is Apache-2.0 and redistributable — shipping it is what the Vulkan Runtime redistributable is for, and what applications that bundle it already do. xim:vulkan-loader exists in xim-pkgindex today, linux-only.

Giving it a Windows branch takes the image out of the question entirely: windows-2022 keeps huxerui and OpenSSL working, and Vulkan stops depending on whether a runner happens to have a GPU driver. That is a change in the ecosystem rather than in the runner, which is the right place for it.

Follow-up: openxlings/xim-pkgindex — a Windows branch for vulkan-loader, then compat.vulkan's windows entry declares it.

Sunrisepeak added a commit to xlings-res/vulkan-loader that referenced this pull request Sep 11, 2026
…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.
Sunrisepeak added a commit to openxlings/xim-pkgindex that referenced this pull request Sep 11, 2026
… 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>
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.

1 participant