…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
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.exe aside, and that workaround is still in xrgui's workflow.
The version axis decides the origin
msvc@14.44.35207 is isomorphic to gcc@16.1.0: coexisting versions, toolchain remove msvc@<toolset>, auto-install from a manifest. The payload brings the compiler, the STL, and — via its xim:windows-sdk dependency — 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.
VSINSTALLDIR now outranks vswhere
vswhere returns something on nearly every developer machine, which made VSINSTALLDIR effectively 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*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.
The Windows SDK stops being two absolute paths
WindowsSdkDir (+ WindowsSdkVersion) → the xim:windows-sdk payload 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 MSVC
Two knobs, one working: linkage = "static" really emitted /MT while cxx_runtime = "self-contained" reported "not implemented" — for the same physical switch, with two comments contradicting each other (flags.cppm:605 vs distribution.cppm:202).
On the MSVC ABI these are not alternatives: /MT links the C and C++ runtimes out of the same library. Both spellings select it through one msvc_wants_static_crt(), asked by both the project's TUs and the std module — rather than each spelling out linkage == "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_MD baked in), so a per-role override is refused with a message saying why, instead of failing later inside the ucrt headers.
Test plan
Depends on openxlings/xim-pkgindex#629 being published before 239 can do more than SKIP.
Closes #432