feat(toolchain): MSVC gets a version axis — msvc@<toolset> alongside msvc@system - #434
Merged
Conversation
…de `msvc@system` (2026.8.16.1) gcc and llvm are installed by mcpp and resolved from what the manifest declares. MSVC was the one exception: EVERY msvc spec was a system spec, so a manifest could name a toolset and have that name discarded. The consequence is not inelegance. It is that the same source compiles with different compilers on different machines, and nothing says so. Measured on xrgui#3: in ONE CI run mcpp used 14.51 and xmake used 14.52, and it stayed invisible until 14.51 hit an ICE. Exporting a complete vcvars environment did not help -- the only way out was to move `vswhere.exe` aside so mcpp would fall through to VSINSTALLDIR, and that workaround is still in xrgui's workflow. ## The version axis decides the origin msvc@system (or bare msvc) the machine's own Visual Studio -- UNCHANGED msvc@<toolset> an xlings payload mcpp installs and pins `msvc@14.44.35207` is isomorphic to `gcc@16.1.0` in every respect: coexisting versions, `toolchain remove msvc@<toolset>`, auto-install from a manifest. The payload brings the compiler, the STL, and -- through its `xim:windows-sdk` dependency -- the ucrt/um headers and libs, so nothing has to be preinstalled. Structurally, ACQUISITION and RESOLUTION are separated: acquisition is shared with gcc (the xim install), resolution is shared with `msvc@system` (`installation_from_tools_dir`). A pinned toolset is therefore not a second code path, and cannot grow its own bugs. What it does not share is the bin/-shaped frontend lookup (cl.exe is four levels deeper) and the ELF post-install fixup (there is nothing to patchelf on a PE toolchain). BREAKING: `msvc@19.44` was a pin-verify against the system install's cl banner -- checked by `toolchain default` and silently ignored by builds. The version axis now names a toolset everywhere. A `19.x` spelling errors out with this machine's actual cl version and both replacements. ## VSINSTALLDIR now outranks vswhere vswhere returns something on nearly every developer machine, which made VSINSTALLDIR effectively unreachable -- a build that had exported a complete vcvars environment still compiled with whatever vswhere ranked first. A guess must not silently override an answer. vswhere also gains `-prerelease`: without it a machine with only an Insiders VS is reported as "MSVC not found" while a perfectly good cl.exe sits on disk. VS*COMNTOOLS stays BELOW vswhere. Those are machine-wide leftovers -- a 2017 VS150COMNTOOLS must not outrank a current install -- whereas VSINSTALLDIR is someone setting it for this shell. The old `find_vs_via_env()` conflated the two, so promoting it would have promoted the leftovers too. ## The Windows SDK stops being two absolute paths Order: WindowsSdkDir (+ WindowsSdkVersion, both exported by vcvars) -> the `xim:windows-sdk` payload beside a pinned toolset in mcpp's own store -> the hardcoded roots, now a fallback. The second source needs no configuration and hardcodes no version: the COMPILER'S OWN PATH says which store it came from, and the SDK is its neighbour there. `sibling_sdk_roots()` returns empty for a system cl, so the two origins stay separate. ## cxx_runtime = "self-contained" now actually does something on MSVC Two knobs, one working: `linkage = "static"` really emitted /MT while `cxx_runtime = "self-contained"` reported "not implemented" -- for the same physical switch. Two comments contradicted each other about it (flags.cppm:605 vs distribution.cppm:202). On the MSVC ABI these are not alternatives: /MT links the C runtime and the C++ runtime out of the same library. Both spellings now select it through one `msvc_wants_static_crt()`, which the project's TUs and the std module both ask -- rather than each spelling out `linkage == "static"`, which is exactly how they diverged in #422. The default stays /MD: the predicate reads the WRITTEN manifest scalar, not the resolved contract, because most roles default to self-contained and keying off that would flip every Windows build to /MT. The CRT model is a whole-PROJECT property (one std module per project, and cl bakes _MSVC_MT/_MSVC_MD into it), so a per-role override is now refused with a message that says why, instead of failing later inside the ucrt headers. ## Tests MSVC discovery is testable off Windows for the first time: `installation_at()` takes a directory instead of probing, `find_windows_sdk()` takes a list of roots, and neither is behind a platform macro. Six new unit tests drive real fixture trees on Linux CI -- including "both toolsets present, ask for the OLDER one", which a latest-wins implementation fails and a real machine might pass by accident. New e2e `239_msvc_managed_toolset.sh`, written so that the SYSTEM compiler answering would FAIL rather than pass quietly: cl.exe must be inside mcpp's store, the toolset directory must be the one named, and switching the same project back to `msvc@system` must resolve to the system cl again. 83/83 unit targets pass; 95/96/103/183 e2e pass on Linux. Closes #432
`toolchain list --available` is not a thing -- `toolchain list` already prints an "Available toolchains" section. Caught by ci-windows on the first run, which is the right place for it: this test only ever executes there. Also reordered: the install now runs BEFORE the discoverability check, so a runner that cannot reach the index skips cleanly instead of failing an assertion about a list the index would have filled in. And the check became stronger than the one it replaces -- it asserts the INSTALLED toolset shows up, not merely that some msvc row exists, because a toolset that installs and then never appears is indistinguishable from one that did not install.
…can self-certify Four repos, five changes, ONE real dependency edge: the packages must be published before mcpp can install them and before xrgui can use them. Everything else is parallel, and stringing it into a line is the usual waste in work shaped like this. The part worth reading is §4: which acceptance criteria a person could satisfy by adjusting a test, and which they could not. The mirror is the example that earned the distinction -- the criterion is not "the upload succeeded" but "the bytes came back with Microsoft's sha256", and that caught a real failure where the tool reported 16 files as failed while the release listing showed them present and they were in fact absent.
Self-review catch. `installation_at()` succeeding means cl.exe is where the declared version says it should be -- it says nothing about the ucrt/um headers, which arrive as a separate package dependency and can therefore fail on their own. The install printed "Installed", and the build died inside the ucrt headers much later. That is the half-installed state `has_usable_msvc()` was written for; this applies the same judgement to the managed origin, and names the dependency that must have failed rather than leaving the reader to work it out. Also: `msvc_print_detected` now takes the label. "Detected" is a claim about probing the machine, and printing it after unpacking a payload the caller NAMED describes the wrong thing -- quietly, and in exactly the direction this whole change is about.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
gcc and llvm are installed by mcpp and resolved from what the manifest declares. MSVC was the one exception: every msvc spec was a system spec, so a manifest could name a toolset and have that name discarded.
The consequence is not inelegance. It is that the same source compiles with different compilers on different machines, and nothing says so. Measured on xrgui#3: in one CI run mcpp used 14.51 and xmake used 14.52, invisible until 14.51 hit an ICE. Exporting a complete vcvars environment did not help — the only way out was to move
vswhere.exeaside, and that workaround is still in xrgui's workflow.The version axis decides the origin
msvc@system(or baremsvc)msvc@<toolset>e.g.msvc@14.44.35207msvc@14.44.35207is isomorphic togcc@16.1.0: coexisting versions,toolchain remove msvc@<toolset>, auto-install from a manifest. The payload brings the compiler, the STL, and — via itsxim:windows-sdkdependency — the ucrt/um headers and libs.Structurally, acquisition and resolution are separated: acquisition is shared with gcc (the xim install), resolution is shared with
msvc@system(installation_from_tools_dir). A pinned toolset is therefore not a second code path and cannot grow its own bugs.VSINSTALLDIRnow outranks vswherevswhere returns something on nearly every developer machine, which made
VSINSTALLDIReffectively unreachable. A guess must not silently override an answer. vswhere also gains-prerelease, so an Insiders-only machine stops being reported as "MSVC not found" while a working cl.exe sits on disk.VS*COMNTOOLSstays below vswhere — those are machine-wide leftovers (a 2017VS150COMNTOOLSmust not outrank a current install), whereasVSINSTALLDIRis someone setting it for this shell. The oldfind_vs_via_env()conflated the two.The Windows SDK stops being two absolute paths
WindowsSdkDir(+WindowsSdkVersion) → thexim:windows-sdkpayload beside a pinned toolset → the hardcoded roots, now a fallback.The second source needs no configuration and hardcodes no version: the compiler's own path says which store it came from, and the SDK is its neighbour there.
sibling_sdk_roots()returns empty for a system cl, so the origins stay separate.cxx_runtime = "self-contained"now does something on MSVCTwo knobs, one working:
linkage = "static"really emitted/MTwhilecxx_runtime = "self-contained"reported "not implemented" — for the same physical switch, with two comments contradicting each other (flags.cppm:605vsdistribution.cppm:202).On the MSVC ABI these are not alternatives:
/MTlinks the C and C++ runtimes out of the same library. Both spellings select it through onemsvc_wants_static_crt(), asked by both the project's TUs and the std module — rather than each spelling outlinkage == "static", which is how they diverged in #422.The default stays
/MD: the predicate reads the written manifest scalar, not the resolved contract, because most roles default to self-contained and keying off that would flip every Windows build to/MT.The CRT model is a whole-project property (one std module per project,
_MSVC_MT/_MSVC_MDbaked in), so a per-role override is refused with a message saying why, instead of failing later inside the ucrt headers.Test plan
installation_at()takes a directory instead of probing,find_windows_sdk()takes a list of roots, neither behind a platform macro. 6 new unit tests drive real fixture trees on Linux CI, including "both toolsets present, ask for the older one" — which a latest-wins implementation fails and a real machine might pass by accident.239_msvc_managed_toolset.sh, written so the system compiler answering would FAIL rather than pass quietly: cl.exe must be inside mcpp's store, the toolset dir must be the one named, and switching the same project back tomsvc@systemmust resolve to the system cl again.mcpp test— 83/83 targetsDepends on openxlings/xim-pkgindex#629 being published before
239can do more than SKIP.Closes #432