Skip to content

Commit a1f4429

Browse files
authored
feat(toolchain): MSVC system-toolchain detection & selection (msvc@system) — 0.0.88 (#208)
* docs(agents): MSVC system-toolchain detection design + implementation plan * feat(toolchain): MSVC system-toolchain detection & selection (msvc@system) (0.0.88) MSVC joins as the first *system* toolchain: mcpp locates and identifies an installed Visual Studio / Build Tools (vswhere → env → well-known paths; cl.exe banner → compiler version/arch, localization-tolerant) but never installs or removes MSVC itself. - toolchain default msvc: detect + report (VS product, VC tools, cl version, std.ixx availability), persist stable 'msvc@system'; absent → install guidance (VS Installer C++ workload / winget BuildTools), exit non-zero. msvc@<prefix> acts as pin-verify against the detected install. - toolchain list: Windows 'System:' section; install msvc reports what's there or guides; remove msvc refuses (system component). - self doctor: Windows 'msvc (system)' check section. - manifest [toolchain] windows = "msvc@system" now actually works; msvc specs on non-Windows hosts error clearly. - build: native cl.exe (.ifc) pipeline is gated off with one owned message pointing at llvm@20.1.7 (MSVC-ABI Clang) until it lands. - detect(): cl.exe classification path (filename short-circuit, banner enrich); bmi_traits gains the MSVC .ifc branch. - tests: unit (banner/guidance/spec/traits) + e2e 95 (Windows, requires msvc cap) / 96 (non-Windows rejection); run_all.sh msvc capability. - design: .agents/docs/2026-07-13-msvc-system-toolchain-detection-design.md * fix(e2e/ci): run msvc detection assertions from a neutral cwd toolchain list stars the *effective* default; the repo root's mcpp.toml [toolchain] windows=llvm@20.1.7 would shadow the freshly-set global msvc@system when run_all.sh executes from the workspace. * ci: run MSVC detection step before the LLVM self-host rebuild The rebuild cleans target/, invalidating the job's $MCPP_SELF fingerprint path — the MSVC step then failed with exit 127 (binary gone). * fix(msvc): well-known-paths fallback covers major-version VS directories windows-latest now installs VS under 'Microsoft Visual Studio\18\Enterprise' (major version, not year branding) — vswhere finds it, but the strategy-3 path scan should too. * fix(msvc): const ref when iterating path segments — libc++ yields temporaries GCC/libstdc++ and Clang/MSVC-STL accepted 'auto&' here; Clang/libc++ (macOS job) correctly rejects binding a non-const lvalue ref to the temporary.
1 parent 558d286 commit a1f4429

18 files changed

Lines changed: 1086 additions & 14 deletions
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
# MSVC System-Toolchain Detection — Design
2+
3+
Date: 2026-07-13
4+
Status: approved for implementation (target release: 0.0.88)
5+
6+
## 1. Problem & scope
7+
8+
mcpp on Windows today builds exclusively with MSVC-ABI Clang (`llvm@20.1.7`),
9+
which *silently borrows* the MSVC STL (`std.ixx`) and Windows SDK from an
10+
installed Visual Studio via `mcpp.toolchain.msvc` discovery. But MSVC itself
11+
is not a selectable toolchain: `mcpp toolchain default msvc` falls into the
12+
xim-package path and fails with a confusing index error, and a machine
13+
*without* VS fails deep inside the Clang std-module build with no actionable
14+
message.
15+
16+
**Goal of this change (detection-first):** make MSVC a first-class *system*
17+
toolchain that a developer can switch to / configure, where mcpp
18+
19+
1. checks whether the system has a usable MSVC installation,
20+
2. auto-locates it (vswhere → env vars → well-known paths) and identifies its
21+
version (VS product, VC tools version, `cl.exe` compiler version),
22+
3. and, when MSVC is absent, prints clear installation guidance —
23+
**mcpp never installs MSVC itself.**
24+
25+
**Explicit non-goal (phase 2, separate PR):** building with `cl.exe`
26+
(`/std:c++23`, `.ifc` modules, `link.exe`/`lib.exe`, `/scanDependencies`).
27+
Selecting MSVC and then running `mcpp build` must fail with a precise
28+
"native cl.exe builds not yet supported" message that names the detected
29+
version and points at `llvm@20.1.7` as the working alternative — never with
30+
an incidental downstream error.
31+
32+
## 2. Existing scaffold (what we build on)
33+
34+
| Layer | Symbol | Status |
35+
|---|---|---|
36+
| model | `CompilerId::MSVC`, `is_msvc_target()` (`src/toolchain/model.cppm`) | exists |
37+
| discovery | `mcpp.toolchain.msvc`: `find_vs_install_path()` (vswhere/env/paths), `find_msvc_tools_dir()`, `find_cl()`, `find_std_module_source()` (`src/toolchain/msvc.cppm`) | exists, used only by clang.cppm for std.ixx |
38+
| capabilities | `capabilities_for()` MSVC case: `msvc-stl` / `lib.exe` (`src/toolchain/provider.cppm`) | stub exists |
39+
| abi | msvcrt / msvc-stl / msvc mappings (`src/toolchain/abi.cppm`) | exists |
40+
| spec | `frontend_candidates_for("msvc") → {"cl.exe"}` (`src/toolchain/registry.cppm`) | exists |
41+
| manifest | `[toolchain] windows = "msvc@system"` documented in `src/manifest/types.cppm` comment | schema ready, unimplemented |
42+
| escape hatch | `tcSpec == "system"` skips xim resolution (`src/build/prepare.cppm:685`) | precedent for system toolchains |
43+
44+
Missing: `detect()` cannot classify `cl.exe`; no cl version identification;
45+
no lifecycle (`default`/`list`/`install`/`remove`) branches; no
46+
prepare-time resolution of `msvc@system`; no doctor reporting; no
47+
absent-MSVC guidance anywhere.
48+
49+
## 3. Design
50+
51+
### 3.1 The `msvc@system` spec
52+
53+
MSVC is a **system toolchain**: mcpp locates it, never installs/removes it.
54+
Canonical spec string is `msvc@system`. Accepted user inputs (CLI and
55+
manifest) and their normalization:
56+
57+
- `msvc`, `msvc@system``msvc@system`
58+
- `msvc@<ver-prefix>` (e.g. `msvc@19.44`) → detect, then require the detected
59+
`cl.exe` version to start with `<ver-prefix>`; mismatch is an error that
60+
prints the detected version. (Pin-verify, not select-among-many: we always
61+
use the newest VC tools of the newest VS, same policy as vswhere `-latest`.)
62+
63+
The persisted global default (`~/.mcpp/config.toml [toolchain] default`) and
64+
the manifest value are always the *stable* form `msvc@system` — never a
65+
concrete version — so config survives VS updates. Concrete versions are
66+
displayed, not stored.
67+
68+
Registry additions (`registry.cppm`):
69+
70+
- `bool is_system_toolchain(const ToolchainSpec&)` — true for `msvc` today
71+
(the concept is deliberately general; `system` (PATH compiler) stays as-is).
72+
- `parse_toolchain_spec` unchanged; msvc branching happens in callers via
73+
`is_system_toolchain` (keeps parse pure).
74+
75+
### 3.2 Discovery + identification (`msvc.cppm` extension)
76+
77+
New exported surface:
78+
79+
```cpp
80+
struct MsvcInstallation {
81+
std::filesystem::path vsRoot; // C:\Program Files\Microsoft Visual Studio\2022\BuildTools
82+
std::string vsProduct; // "2022 BuildTools" (derived from path segments)
83+
std::string toolsVersion; // "14.44.35207" (VC\Tools\MSVC\<dir>)
84+
std::filesystem::path clPath; // ...\bin\Hostx64\x64\cl.exe
85+
std::string clVersion; // "19.44.35211" (banner; falls back to toolsVersion-derived)
86+
std::string arch; // "x64" | "x86" | "arm64" (host-native target dir)
87+
bool hasStdModules; // modules\std.ixx present
88+
};
89+
90+
// Locate the best (newest) usable installation. nullopt = MSVC absent.
91+
std::optional<MsvcInstallation> detect_installation();
92+
93+
// Pure, cross-platform, unit-testable:
94+
// parse "Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35211 for x64"
95+
// (also localized banners: match a 19.xx.xxxxx token + arch token anywhere).
96+
std::optional<std::pair<std::string,std::string>> parse_cl_banner(std::string_view);
97+
98+
// Multi-line installation guidance used everywhere MSVC is required but absent.
99+
std::string install_guidance();
100+
```
101+
102+
Implementation notes:
103+
104+
- `detect_installation()` composes the existing `find_vs_install_path()` +
105+
`find_latest_msvc_tools()`; adds cl banner capture. `cl.exe` is executed
106+
bare with stderr merged; the banner prints regardless of the "usage" exit
107+
status, so **ignore the exit code** and parse the output
108+
(`platform::process::capture`, not probe's `run_capture` which errors on
109+
non-zero exit). If execution or parsing fails, fall back to
110+
`clVersion = ""` and report `toolsVersion` — never fail detection just
111+
because the banner didn't parse.
112+
- `parse_cl_banner` must not assume the English banner: scan for a
113+
`\d+\.\d+\.\d+`-shaped token (first match) and an arch token among
114+
`x64|x86|arm64|ARM64` (last match). Lives outside `#ifdef _WIN32` so Linux
115+
unit tests cover it.
116+
- `install_guidance()` (also outside `#ifdef`) says, in this order: what was
117+
searched (vswhere / VSINSTALLDIR / standard paths), that mcpp does not
118+
install MSVC, and how to get it:
119+
- Visual Studio Installer → workload **Desktop development with C++**
120+
(component `Microsoft.VisualStudio.Component.VC.Tools.x86.x64`)
121+
- or Build Tools only: `winget install Microsoft.VisualStudio.2022.BuildTools`
122+
then add the C++ workload in the installer
123+
- then re-run `mcpp toolchain default msvc`.
124+
- `arch`: host-native pair only (`Hostx64\x64` on x64, `Hostarm64\arm64` on
125+
ARM64 hosts, falling back across the two). Cross target dirs are phase 2.
126+
127+
### 3.3 Lifecycle commands (`lifecycle.cppm`)
128+
129+
All four subcommands gain an msvc branch **before** the xim-package path,
130+
gated on `is_system_toolchain(spec)`:
131+
132+
- `mcpp toolchain default msvc[@…]`
133+
- non-Windows → error: `the msvc toolchain is only available on Windows hosts`.
134+
- Windows, detected → verify optional version pin, then
135+
`write_default_toolchain(cfg, "msvc@system")` and print e.g.
136+
```
137+
Detected msvc 19.44.35211 (VS 2022 BuildTools, VC tools 14.44.35207)
138+
cl: C:\...\bin\Hostx64\x64\cl.exe
139+
import std: available (std.ixx)
140+
Default set to msvc@system (was: llvm@20.1.7)
141+
note: `mcpp build` with native MSVC (cl.exe) is not yet supported — coming in a later release.
142+
```
143+
- Windows, absent → error + `install_guidance()`, exit 1.
144+
- `mcpp toolchain install msvc` — never installs. If detected: print the
145+
detection summary + hint `mcpp toolchain default msvc`, exit 0. If absent:
146+
`install_guidance()`, exit 1. Non-Windows: same error as `default`.
147+
- `mcpp toolchain list` — on Windows, after the xim "Installed" rows, run
148+
detection and append a `System:` section:
149+
```
150+
System:
151+
msvc 19.44.35211 (VS 2022 BuildTools) C:\...\cl.exe
152+
```
153+
with `*` when the effective default is `msvc@system`
154+
(`matches_default_toolchain` gains: `configuredDefault == "msvc@system"`
155+
matches compiler `msvc`, any version). Absent MSVC adds one hint line:
156+
`(msvc: not detected — run 'mcpp toolchain default msvc' for setup guidance)`.
157+
Non-Windows: section omitted entirely.
158+
- `mcpp toolchain remove msvc` — error: system toolchain, remove via the
159+
Visual Studio Installer.
160+
161+
### 3.4 Build-time resolution (`prepare.cppm`)
162+
163+
In the toolchain-resolution chain (before the xim `resolve_xpkg_path`
164+
branch): if `tcSpec` parses to an msvc system spec →
165+
166+
- non-Windows: error `toolchain 'msvc@system' is only available on Windows`.
167+
- Windows: `msvc::detect_installation()`; absent → error + guidance;
168+
found → `explicit_compiler = clPath` (no xim resolution, no post-install
169+
fixup), `ui::info("Resolved", "msvc@system → <cl path>")`.
170+
171+
After `detect()` returns (ctx.tc populated), a single gate:
172+
`tc.compiler == CompilerId::MSVC` → return error:
173+
174+
```
175+
native MSVC (cl.exe) builds are not yet supported by mcpp.
176+
detected: msvc 19.44.35211 at C:\...\cl.exe (selection & detection work today)
177+
for building on Windows use the MSVC-ABI Clang toolchain instead:
178+
mcpp toolchain default llvm@20.1.7
179+
```
180+
181+
This keeps every command that only *resolves/inspects* the toolchain
182+
(`toolchain …`, `doctor`, `self env`) fully working while `build`/`run`/
183+
`test` fail early with one owned message.
184+
185+
### 3.5 Detection classification (`detect.cppm`)
186+
187+
`detect()` classifies by driver **filename first** for MSVC: stem `cl`
188+
(case-insensitive) short-circuits before the `--version` probe (cl.exe has no
189+
`--version`; running it with GNU flags produces garbage). The MSVC path:
190+
191+
- run bare `cl.exe` (stderr merged), `parse_cl_banner` → `tc.version`, arch.
192+
- `tc.compiler = CompilerId::MSVC`; `tc.targetTriple` = `x86_64-pc-windows-msvc`
193+
/ `i686-…` / `aarch64-…` from the banner arch (fallback: path segment
194+
`Hostx64\x64`, then host arch).
195+
- `tc.driverIdent` = normalized banner (fingerprint input — MSVC patch
196+
updates change the banner, invalidating BMI caches correctly).
197+
- `tc.stdModuleSource` = `msvc::find_std_module_source()`;
198+
`tc.hasImportStd` accordingly. Skip `-dumpmachine` / `-print-sysroot` /
199+
payload probing (meaningless for cl.exe).
200+
- `bmi_traits()` gains the MSVC branch (`ifc.cache` / `.ifc` /
201+
`needsExplicitModuleOutput=true`) so phase 2 doesn't silently reuse GCC
202+
defaults; unreachable in builds this release because of the 3.4 gate.
203+
204+
### 3.6 Doctor (`doctor.cppm`)
205+
206+
Windows-only section "Checking msvc (system)": detected → print product /
207+
tools version / cl version / std.ixx presence as ok-lines; absent → a
208+
warning (not a failure) with the one-line hint to run
209+
`mcpp toolchain default msvc` for guidance. When the *effective default* is
210+
`msvc@system`, doctor's existing toolchain check must treat the missing
211+
std-module BMI as expected (build support pending), not an error.
212+
213+
### 3.7 User docs
214+
215+
`docs/03-toolchains.md`: new "MSVC (system toolchain, Windows)" section —
216+
what detection does, the not-installed guidance, the build-support status,
217+
`[toolchain] windows = "msvc@system"` manifest example (finally matching the
218+
types.cppm comment).
219+
220+
## 4. Testing
221+
222+
Unit (`tests/unit/test_toolchain_msvc.cpp`, cross-platform):
223+
224+
- `parse_cl_banner`: English banner, localized banner (arch token + version
225+
token only), garbage → nullopt, arm64 variant.
226+
- `install_guidance()` non-empty, mentions `winget` and does not claim mcpp
227+
installs MSVC.
228+
- spec normalization: `msvc` / `msvc@system` / `msvc@19.44` →
229+
`is_system_toolchain` true; `gcc@16.1.0` → false.
230+
- `matches_default_toolchain("msvc@system", "msvc", <any>)` true.
231+
- `bmi_traits` MSVC branch.
232+
233+
E2E:
234+
235+
- `95_msvc_system_toolchain.sh` — `# requires: msvc` (new capability in
236+
`run_all.sh`, set on Windows when vswhere+VC tools present):
237+
`toolchain default msvc` succeeds & prints `Detected` + `msvc@system`;
238+
`toolchain list` shows the System section with `*`; `mcpp build` on a
239+
scratch project fails with `not yet supported` + detected version;
240+
`toolchain remove msvc` fails with the system-toolchain message;
241+
`toolchain install msvc` exits 0 with the already-installed summary.
242+
- `96_msvc_unavailable_nonwindows.sh` — `# requires: unix-shell`:
243+
`toolchain default msvc` fails with `only available on Windows`;
244+
`toolchain list` shows no System section.
245+
246+
CI: `ci-windows.yml` gains one step "Toolchain: MSVC — detection & selection"
247+
running the e2e positive flow against `$MCPP_SELF` (windows-latest ships VS
248+
2022 Enterprise, so detection must succeed; treat failure as regression).
249+
250+
## 5. Rollout plan
251+
252+
1. Temp branch `tmp/msvc-windows-ci`: implementation + **all workflows except
253+
`ci-windows.yml` deleted** (CI-time economy while iterating), draft PR,
254+
iterate to green.
255+
2. Real branch `feat/msvc-system-toolchain` from the green tree with
256+
workflows restored, version bumped to **0.0.88**
257+
(`src/toolchain/fingerprint.cppm` `MCPP_VERSION` + `mcpp.toml` version +
258+
`CHANGELOG.md`), real PR, full CI green, merge.
259+
3. Tag `v0.0.88` → `release.yml` → mirror assets to xlings-res (gh + gtc
260+
both ends) → xim-pkgindex mcpp package update (gitee auto-mirrors) →
261+
`xlings install mcpp` verification → bump bootstrap pin.
262+
263+
## 6. Risks
264+
265+
- **cl.exe bare-run exit code/output shape varies by VS version** — mitigated:
266+
ignore exit code, tolerate parse failure (fall back to tools-dir version).
267+
- **windows-latest runner image changes VS layout** — mitigated: detection has
268+
three strategies; CI step asserts detection so a silent regression surfaces.
269+
- **Non-English banners** — mitigated: token-based parsing, unit-tested.
270+
- **Users interpret `msvc` selection as build support** — mitigated: the
271+
explicit note at selection time + the owned build-time error.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# MSVC System-Toolchain Detection — Implementation Plan
2+
3+
Companion to `2026-07-13-msvc-system-toolchain-detection-design.md`.
4+
Target release: 0.0.88. Each step compiles + `mcpp test` passes on its own.
5+
6+
## Step 1 — discovery & identification core (`src/toolchain/msvc.cppm`)
7+
8+
- Add `MsvcInstallation`, `detect_installation()`, `parse_cl_banner()`,
9+
`install_guidance()` per design §3.2.
10+
- `parse_cl_banner` + `install_guidance` live outside `#ifdef _WIN32`.
11+
- `vsProduct` derived from the two path segments after
12+
`Microsoft Visual Studio` (`"2022 BuildTools"`); empty if layout unusual.
13+
- cl banner capture: `platform::process::capture` on bare cl.exe, merged
14+
stderr, exit code ignored.
15+
16+
## Step 2 — spec layer (`src/toolchain/registry.cppm`)
17+
18+
- `is_system_toolchain(const ToolchainSpec&)` (compiler == "msvc").
19+
- `matches_default_toolchain`: `"msvc@system"` matches (`"msvc"`, any ver).
20+
- `display_label("msvc", …)` unchanged (generic path already fine).
21+
22+
## Step 3 — model traits (`src/toolchain/model.cppm`)
23+
24+
- `bmi_traits`: MSVC branch → `{ifc.cache, .ifc, ifc, true, false, false}`.
25+
26+
## Step 4 — detection (`src/toolchain/detect.cppm`)
27+
28+
- Filename short-circuit (stem `cl`, case-insensitive) before `--version`
29+
probe → `enrich` MSVC path per design §3.5 (banner → version/arch/triple,
30+
driverIdent, std.ixx, skip dumpmachine/sysroot/payloads).
31+
32+
## Step 5 — lifecycle (`src/toolchain/lifecycle.cppm`)
33+
34+
- `toolchain_set_default` / `toolchain_install` / `toolchain_remove` /
35+
`toolchain_list`: msvc branches per design §3.3 (non-Windows error;
36+
detect-report-persist; guidance on absence; System section in list).
37+
38+
## Step 6 — build-time resolution + gate (`src/build/prepare.cppm`)
39+
40+
- msvc spec branch before xim resolution (design §3.4).
41+
- post-detect gate on `CompilerId::MSVC` with the owned
42+
"not yet supported" error.
43+
44+
## Step 7 — doctor (`src/doctor.cppm`)
45+
46+
- Windows "Checking msvc (system)" section; msvc-default-aware std-BMI check.
47+
48+
## Step 8 — tests
49+
50+
- `tests/unit/test_toolchain_msvc.cpp` (banner/guidance/spec/traits, §4).
51+
- `tests/e2e/95_msvc_system_toolchain.sh` (`# requires: msvc`),
52+
`tests/e2e/96_msvc_unavailable_nonwindows.sh` (`# requires: unix-shell`).
53+
- `tests/e2e/run_all.sh`: `msvc` capability (Windows + vswhere + VC tools).
54+
55+
## Step 9 — docs
56+
57+
- `docs/03-toolchains.md` MSVC section; CHANGELOG entry.
58+
59+
## Step 10 — CI
60+
61+
- `ci-windows.yml`: step "Toolchain: MSVC — detection & selection" driving
62+
the 95 e2e flow via `$MCPP_SELF`.
63+
64+
## Rollout (design §5)
65+
66+
1. `tmp/msvc-windows-ci` branch: steps 1-10 + delete all workflows except
67+
`ci-windows.yml`; draft PR; iterate until green.
68+
2. `feat/msvc-system-toolchain`: green tree + workflows restored + version
69+
bump (fingerprint.cppm, mcpp.toml, CHANGELOG) → real PR → full CI → merge.
70+
3. Release v0.0.88 → xlings-res mirror (gh + gtc) → xim-pkgindex →
71+
`xlings install mcpp` verify → bootstrap pin bump.
72+
73+
## Verification per step
74+
75+
- Local: `mcpp build && mcpp test` (Linux) after every step; unit tests for
76+
the pure functions run on Linux.
77+
- Windows behavior only verifiable in CI — keep Windows-only logic thin and
78+
push the parseable/testable parts into pure functions.

.github/workflows/ci-windows.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,43 @@ jobs:
220220
cd hello_win
221221
"$MCPP_SELF" run
222222
223+
# windows-latest ships VS 2022 Enterprise with the VC workload, so
224+
# msvc@system detection MUST succeed here — a failure is a regression
225+
# in the discovery/identification chain (vswhere → env → paths).
226+
# Runs BEFORE the LLVM self-host rebuild: that step cleans + rebuilds
227+
# target/, invalidating this job's $MCPP_SELF fingerprint path.
228+
- name: "Toolchain: MSVC — detection & selection (msvc@system)"
229+
shell: bash
230+
run: |
231+
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
232+
233+
# Neutral cwd: the repo root's mcpp.toml [toolchain] would shadow
234+
# the global default in `toolchain list` / doctor output.
235+
TMP=$(mktemp -d); cd "$TMP"
236+
237+
out=$("$MCPP_SELF" toolchain default msvc); echo "$out"
238+
grep -q "Detected" <<<"$out"
239+
grep -q "msvc@system" <<<"$out"
240+
241+
"$MCPP_SELF" toolchain list | tee tc-list.txt
242+
grep -E '\*\s*msvc' tc-list.txt
243+
244+
"$MCPP_SELF" self doctor 2>&1 | tee doctor.txt || true
245+
grep -qi "msvc" doctor.txt
246+
247+
# native cl.exe builds are gated with one owned message
248+
"$MCPP_SELF" new hello_msvc && cd hello_msvc
249+
set +e
250+
bout=$("$MCPP_SELF" build 2>&1); brc=$?
251+
set -e
252+
echo "$bout"
253+
test $brc -ne 0
254+
grep -q "not yet supported" <<<"$bout"
255+
256+
# restore the LLVM default for the remaining steps
257+
cd "$GITHUB_WORKSPACE"
258+
"$MCPP_SELF" toolchain default llvm@20.1.7
259+
223260
- name: "Toolchain: LLVM — build mcpp (self-host)"
224261
shell: bash
225262
run: |

0 commit comments

Comments
 (0)