Skip to content

feat(toolchain): MSVC gets a version axis — msvc@<toolset> alongside msvc@system - #434

Merged
Sunrisepeak merged 4 commits into
mainfrom
feat/msvc-managed-toolchain
Aug 16, 2026
Merged

feat(toolchain): MSVC gets a version axis — msvc@<toolset> alongside msvc@system#434
Sunrisepeak merged 4 commits into
mainfrom
feat/msvc-managed-toolchain

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

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

Spec Origin Which compiler
msvc@system (or bare msvc) the machine's own Visual Studio unchanged
msvc@<toolset> e.g. msvc@14.44.35207 an xlings payload mcpp installs the one named, on every machine

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.

⚠️ Breaking: msvc@19.44 was a pin-verify against the system 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 naming 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 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

  • 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, 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.
  • New e2e 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 to msvc@system must resolve to the system cl again.
  • mcpp test — 83/83 targets
  • e2e 95 / 96 / 103 / 183 on Linux
  • ci-windows / ci-windows-e2e — the only place the managed install can actually run

Depends on openxlings/xim-pkgindex#629 being published before 239 can do more than SKIP.

Closes #432

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

MSVC: 无法指定编译器 / SDK 路径写死 / cxx_runtime 与 linkage 说法不一

2 participants