From 41aaa0b7d6ebe98ca63a3855d631ef679d641256 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 06:43:39 +0800 Subject: [PATCH 1/6] docs(agents): MSVC system-toolchain detection design + implementation plan --- ...-msvc-system-toolchain-detection-design.md | 271 ++++++++++++++++++ ...vc-system-toolchain-implementation-plan.md | 78 +++++ 2 files changed, 349 insertions(+) create mode 100644 .agents/docs/2026-07-13-msvc-system-toolchain-detection-design.md create mode 100644 .agents/docs/2026-07-13-msvc-system-toolchain-implementation-plan.md diff --git a/.agents/docs/2026-07-13-msvc-system-toolchain-detection-design.md b/.agents/docs/2026-07-13-msvc-system-toolchain-detection-design.md new file mode 100644 index 0000000..9411497 --- /dev/null +++ b/.agents/docs/2026-07-13-msvc-system-toolchain-detection-design.md @@ -0,0 +1,271 @@ +# MSVC System-Toolchain Detection — Design + +Date: 2026-07-13 +Status: approved for implementation (target release: 0.0.88) + +## 1. Problem & scope + +mcpp on Windows today builds exclusively with MSVC-ABI Clang (`llvm@20.1.7`), +which *silently borrows* the MSVC STL (`std.ixx`) and Windows SDK from an +installed Visual Studio via `mcpp.toolchain.msvc` discovery. But MSVC itself +is not a selectable toolchain: `mcpp toolchain default msvc` falls into the +xim-package path and fails with a confusing index error, and a machine +*without* VS fails deep inside the Clang std-module build with no actionable +message. + +**Goal of this change (detection-first):** make MSVC a first-class *system* +toolchain that a developer can switch to / configure, where mcpp + +1. checks whether the system has a usable MSVC installation, +2. auto-locates it (vswhere → env vars → well-known paths) and identifies its + version (VS product, VC tools version, `cl.exe` compiler version), +3. and, when MSVC is absent, prints clear installation guidance — + **mcpp never installs MSVC itself.** + +**Explicit non-goal (phase 2, separate PR):** building with `cl.exe` +(`/std:c++23`, `.ifc` modules, `link.exe`/`lib.exe`, `/scanDependencies`). +Selecting MSVC and then running `mcpp build` must fail with a precise +"native cl.exe builds not yet supported" message that names the detected +version and points at `llvm@20.1.7` as the working alternative — never with +an incidental downstream error. + +## 2. Existing scaffold (what we build on) + +| Layer | Symbol | Status | +|---|---|---| +| model | `CompilerId::MSVC`, `is_msvc_target()` (`src/toolchain/model.cppm`) | exists | +| 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 | +| capabilities | `capabilities_for()` MSVC case: `msvc-stl` / `lib.exe` (`src/toolchain/provider.cppm`) | stub exists | +| abi | msvcrt / msvc-stl / msvc mappings (`src/toolchain/abi.cppm`) | exists | +| spec | `frontend_candidates_for("msvc") → {"cl.exe"}` (`src/toolchain/registry.cppm`) | exists | +| manifest | `[toolchain] windows = "msvc@system"` documented in `src/manifest/types.cppm` comment | schema ready, unimplemented | +| escape hatch | `tcSpec == "system"` skips xim resolution (`src/build/prepare.cppm:685`) | precedent for system toolchains | + +Missing: `detect()` cannot classify `cl.exe`; no cl version identification; +no lifecycle (`default`/`list`/`install`/`remove`) branches; no +prepare-time resolution of `msvc@system`; no doctor reporting; no +absent-MSVC guidance anywhere. + +## 3. Design + +### 3.1 The `msvc@system` spec + +MSVC is a **system toolchain**: mcpp locates it, never installs/removes it. +Canonical spec string is `msvc@system`. Accepted user inputs (CLI and +manifest) and their normalization: + +- `msvc`, `msvc@system` → `msvc@system` +- `msvc@` (e.g. `msvc@19.44`) → detect, then require the detected + `cl.exe` version to start with ``; mismatch is an error that + prints the detected version. (Pin-verify, not select-among-many: we always + use the newest VC tools of the newest VS, same policy as vswhere `-latest`.) + +The persisted global default (`~/.mcpp/config.toml [toolchain] default`) and +the manifest value are always the *stable* form `msvc@system` — never a +concrete version — so config survives VS updates. Concrete versions are +displayed, not stored. + +Registry additions (`registry.cppm`): + +- `bool is_system_toolchain(const ToolchainSpec&)` — true for `msvc` today + (the concept is deliberately general; `system` (PATH compiler) stays as-is). +- `parse_toolchain_spec` unchanged; msvc branching happens in callers via + `is_system_toolchain` (keeps parse pure). + +### 3.2 Discovery + identification (`msvc.cppm` extension) + +New exported surface: + +```cpp +struct MsvcInstallation { + std::filesystem::path vsRoot; // C:\Program Files\Microsoft Visual Studio\2022\BuildTools + std::string vsProduct; // "2022 BuildTools" (derived from path segments) + std::string toolsVersion; // "14.44.35207" (VC\Tools\MSVC\) + std::filesystem::path clPath; // ...\bin\Hostx64\x64\cl.exe + std::string clVersion; // "19.44.35211" (banner; falls back to toolsVersion-derived) + std::string arch; // "x64" | "x86" | "arm64" (host-native target dir) + bool hasStdModules; // modules\std.ixx present +}; + +// Locate the best (newest) usable installation. nullopt = MSVC absent. +std::optional detect_installation(); + +// Pure, cross-platform, unit-testable: +// parse "Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35211 for x64" +// (also localized banners: match a 19.xx.xxxxx token + arch token anywhere). +std::optional> parse_cl_banner(std::string_view); + +// Multi-line installation guidance used everywhere MSVC is required but absent. +std::string install_guidance(); +``` + +Implementation notes: + +- `detect_installation()` composes the existing `find_vs_install_path()` + + `find_latest_msvc_tools()`; adds cl banner capture. `cl.exe` is executed + bare with stderr merged; the banner prints regardless of the "usage" exit + status, so **ignore the exit code** and parse the output + (`platform::process::capture`, not probe's `run_capture` which errors on + non-zero exit). If execution or parsing fails, fall back to + `clVersion = ""` and report `toolsVersion` — never fail detection just + because the banner didn't parse. +- `parse_cl_banner` must not assume the English banner: scan for a + `\d+\.\d+\.\d+`-shaped token (first match) and an arch token among + `x64|x86|arm64|ARM64` (last match). Lives outside `#ifdef _WIN32` so Linux + unit tests cover it. +- `install_guidance()` (also outside `#ifdef`) says, in this order: what was + searched (vswhere / VSINSTALLDIR / standard paths), that mcpp does not + install MSVC, and how to get it: + - Visual Studio Installer → workload **Desktop development with C++** + (component `Microsoft.VisualStudio.Component.VC.Tools.x86.x64`) + - or Build Tools only: `winget install Microsoft.VisualStudio.2022.BuildTools` + then add the C++ workload in the installer + - then re-run `mcpp toolchain default msvc`. +- `arch`: host-native pair only (`Hostx64\x64` on x64, `Hostarm64\arm64` on + ARM64 hosts, falling back across the two). Cross target dirs are phase 2. + +### 3.3 Lifecycle commands (`lifecycle.cppm`) + +All four subcommands gain an msvc branch **before** the xim-package path, +gated on `is_system_toolchain(spec)`: + +- `mcpp toolchain default msvc[@…]` + - non-Windows → error: `the msvc toolchain is only available on Windows hosts`. + - Windows, detected → verify optional version pin, then + `write_default_toolchain(cfg, "msvc@system")` and print e.g. + ``` + Detected msvc 19.44.35211 (VS 2022 BuildTools, VC tools 14.44.35207) + cl: C:\...\bin\Hostx64\x64\cl.exe + import std: available (std.ixx) + Default set to msvc@system (was: llvm@20.1.7) + note: `mcpp build` with native MSVC (cl.exe) is not yet supported — coming in a later release. + ``` + - Windows, absent → error + `install_guidance()`, exit 1. +- `mcpp toolchain install msvc` — never installs. If detected: print the + detection summary + hint `mcpp toolchain default msvc`, exit 0. If absent: + `install_guidance()`, exit 1. Non-Windows: same error as `default`. +- `mcpp toolchain list` — on Windows, after the xim "Installed" rows, run + detection and append a `System:` section: + ``` + System: + msvc 19.44.35211 (VS 2022 BuildTools) C:\...\cl.exe + ``` + with `*` when the effective default is `msvc@system` + (`matches_default_toolchain` gains: `configuredDefault == "msvc@system"` + matches compiler `msvc`, any version). Absent MSVC adds one hint line: + `(msvc: not detected — run 'mcpp toolchain default msvc' for setup guidance)`. + Non-Windows: section omitted entirely. +- `mcpp toolchain remove msvc` — error: system toolchain, remove via the + Visual Studio Installer. + +### 3.4 Build-time resolution (`prepare.cppm`) + +In the toolchain-resolution chain (before the xim `resolve_xpkg_path` +branch): if `tcSpec` parses to an msvc system spec → + +- non-Windows: error `toolchain 'msvc@system' is only available on Windows`. +- Windows: `msvc::detect_installation()`; absent → error + guidance; + found → `explicit_compiler = clPath` (no xim resolution, no post-install + fixup), `ui::info("Resolved", "msvc@system → ")`. + +After `detect()` returns (ctx.tc populated), a single gate: +`tc.compiler == CompilerId::MSVC` → return error: + +``` +native MSVC (cl.exe) builds are not yet supported by mcpp. + detected: msvc 19.44.35211 at C:\...\cl.exe (selection & detection work today) + for building on Windows use the MSVC-ABI Clang toolchain instead: + mcpp toolchain default llvm@20.1.7 +``` + +This keeps every command that only *resolves/inspects* the toolchain +(`toolchain …`, `doctor`, `self env`) fully working while `build`/`run`/ +`test` fail early with one owned message. + +### 3.5 Detection classification (`detect.cppm`) + +`detect()` classifies by driver **filename first** for MSVC: stem `cl` +(case-insensitive) short-circuits before the `--version` probe (cl.exe has no +`--version`; running it with GNU flags produces garbage). The MSVC path: + +- run bare `cl.exe` (stderr merged), `parse_cl_banner` → `tc.version`, arch. +- `tc.compiler = CompilerId::MSVC`; `tc.targetTriple` = `x86_64-pc-windows-msvc` + / `i686-…` / `aarch64-…` from the banner arch (fallback: path segment + `Hostx64\x64`, then host arch). +- `tc.driverIdent` = normalized banner (fingerprint input — MSVC patch + updates change the banner, invalidating BMI caches correctly). +- `tc.stdModuleSource` = `msvc::find_std_module_source()`; + `tc.hasImportStd` accordingly. Skip `-dumpmachine` / `-print-sysroot` / + payload probing (meaningless for cl.exe). +- `bmi_traits()` gains the MSVC branch (`ifc.cache` / `.ifc` / + `needsExplicitModuleOutput=true`) so phase 2 doesn't silently reuse GCC + defaults; unreachable in builds this release because of the 3.4 gate. + +### 3.6 Doctor (`doctor.cppm`) + +Windows-only section "Checking msvc (system)": detected → print product / +tools version / cl version / std.ixx presence as ok-lines; absent → a +warning (not a failure) with the one-line hint to run +`mcpp toolchain default msvc` for guidance. When the *effective default* is +`msvc@system`, doctor's existing toolchain check must treat the missing +std-module BMI as expected (build support pending), not an error. + +### 3.7 User docs + +`docs/03-toolchains.md`: new "MSVC (system toolchain, Windows)" section — +what detection does, the not-installed guidance, the build-support status, +`[toolchain] windows = "msvc@system"` manifest example (finally matching the +types.cppm comment). + +## 4. Testing + +Unit (`tests/unit/test_toolchain_msvc.cpp`, cross-platform): + +- `parse_cl_banner`: English banner, localized banner (arch token + version + token only), garbage → nullopt, arm64 variant. +- `install_guidance()` non-empty, mentions `winget` and does not claim mcpp + installs MSVC. +- spec normalization: `msvc` / `msvc@system` / `msvc@19.44` → + `is_system_toolchain` true; `gcc@16.1.0` → false. +- `matches_default_toolchain("msvc@system", "msvc", )` true. +- `bmi_traits` MSVC branch. + +E2E: + +- `95_msvc_system_toolchain.sh` — `# requires: msvc` (new capability in + `run_all.sh`, set on Windows when vswhere+VC tools present): + `toolchain default msvc` succeeds & prints `Detected` + `msvc@system`; + `toolchain list` shows the System section with `*`; `mcpp build` on a + scratch project fails with `not yet supported` + detected version; + `toolchain remove msvc` fails with the system-toolchain message; + `toolchain install msvc` exits 0 with the already-installed summary. +- `96_msvc_unavailable_nonwindows.sh` — `# requires: unix-shell`: + `toolchain default msvc` fails with `only available on Windows`; + `toolchain list` shows no System section. + +CI: `ci-windows.yml` gains one step "Toolchain: MSVC — detection & selection" +running the e2e positive flow against `$MCPP_SELF` (windows-latest ships VS +2022 Enterprise, so detection must succeed; treat failure as regression). + +## 5. Rollout plan + +1. Temp branch `tmp/msvc-windows-ci`: implementation + **all workflows except + `ci-windows.yml` deleted** (CI-time economy while iterating), draft PR, + iterate to green. +2. Real branch `feat/msvc-system-toolchain` from the green tree with + workflows restored, version bumped to **0.0.88** + (`src/toolchain/fingerprint.cppm` `MCPP_VERSION` + `mcpp.toml` version + + `CHANGELOG.md`), real PR, full CI green, merge. +3. Tag `v0.0.88` → `release.yml` → mirror assets to xlings-res (gh + gtc + both ends) → xim-pkgindex mcpp package update (gitee auto-mirrors) → + `xlings install mcpp` verification → bump bootstrap pin. + +## 6. Risks + +- **cl.exe bare-run exit code/output shape varies by VS version** — mitigated: + ignore exit code, tolerate parse failure (fall back to tools-dir version). +- **windows-latest runner image changes VS layout** — mitigated: detection has + three strategies; CI step asserts detection so a silent regression surfaces. +- **Non-English banners** — mitigated: token-based parsing, unit-tested. +- **Users interpret `msvc` selection as build support** — mitigated: the + explicit note at selection time + the owned build-time error. diff --git a/.agents/docs/2026-07-13-msvc-system-toolchain-implementation-plan.md b/.agents/docs/2026-07-13-msvc-system-toolchain-implementation-plan.md new file mode 100644 index 0000000..22c8c9d --- /dev/null +++ b/.agents/docs/2026-07-13-msvc-system-toolchain-implementation-plan.md @@ -0,0 +1,78 @@ +# MSVC System-Toolchain Detection — Implementation Plan + +Companion to `2026-07-13-msvc-system-toolchain-detection-design.md`. +Target release: 0.0.88. Each step compiles + `mcpp test` passes on its own. + +## Step 1 — discovery & identification core (`src/toolchain/msvc.cppm`) + +- Add `MsvcInstallation`, `detect_installation()`, `parse_cl_banner()`, + `install_guidance()` per design §3.2. +- `parse_cl_banner` + `install_guidance` live outside `#ifdef _WIN32`. +- `vsProduct` derived from the two path segments after + `Microsoft Visual Studio` (`"2022 BuildTools"`); empty if layout unusual. +- cl banner capture: `platform::process::capture` on bare cl.exe, merged + stderr, exit code ignored. + +## Step 2 — spec layer (`src/toolchain/registry.cppm`) + +- `is_system_toolchain(const ToolchainSpec&)` (compiler == "msvc"). +- `matches_default_toolchain`: `"msvc@system"` matches (`"msvc"`, any ver). +- `display_label("msvc", …)` unchanged (generic path already fine). + +## Step 3 — model traits (`src/toolchain/model.cppm`) + +- `bmi_traits`: MSVC branch → `{ifc.cache, .ifc, ifc, true, false, false}`. + +## Step 4 — detection (`src/toolchain/detect.cppm`) + +- Filename short-circuit (stem `cl`, case-insensitive) before `--version` + probe → `enrich` MSVC path per design §3.5 (banner → version/arch/triple, + driverIdent, std.ixx, skip dumpmachine/sysroot/payloads). + +## Step 5 — lifecycle (`src/toolchain/lifecycle.cppm`) + +- `toolchain_set_default` / `toolchain_install` / `toolchain_remove` / + `toolchain_list`: msvc branches per design §3.3 (non-Windows error; + detect-report-persist; guidance on absence; System section in list). + +## Step 6 — build-time resolution + gate (`src/build/prepare.cppm`) + +- msvc spec branch before xim resolution (design §3.4). +- post-detect gate on `CompilerId::MSVC` with the owned + "not yet supported" error. + +## Step 7 — doctor (`src/doctor.cppm`) + +- Windows "Checking msvc (system)" section; msvc-default-aware std-BMI check. + +## Step 8 — tests + +- `tests/unit/test_toolchain_msvc.cpp` (banner/guidance/spec/traits, §4). +- `tests/e2e/95_msvc_system_toolchain.sh` (`# requires: msvc`), + `tests/e2e/96_msvc_unavailable_nonwindows.sh` (`# requires: unix-shell`). +- `tests/e2e/run_all.sh`: `msvc` capability (Windows + vswhere + VC tools). + +## Step 9 — docs + +- `docs/03-toolchains.md` MSVC section; CHANGELOG entry. + +## Step 10 — CI + +- `ci-windows.yml`: step "Toolchain: MSVC — detection & selection" driving + the 95 e2e flow via `$MCPP_SELF`. + +## Rollout (design §5) + +1. `tmp/msvc-windows-ci` branch: steps 1-10 + delete all workflows except + `ci-windows.yml`; draft PR; iterate until green. +2. `feat/msvc-system-toolchain`: green tree + workflows restored + version + bump (fingerprint.cppm, mcpp.toml, CHANGELOG) → real PR → full CI → merge. +3. Release v0.0.88 → xlings-res mirror (gh + gtc) → xim-pkgindex → + `xlings install mcpp` verify → bootstrap pin bump. + +## Verification per step + +- Local: `mcpp build && mcpp test` (Linux) after every step; unit tests for + the pure functions run on Linux. +- Windows behavior only verifiable in CI — keep Windows-only logic thin and + push the parseable/testable parts into pure functions. From aff3cb38632ce8f2596712825a609d68b0b971d2 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 06:56:46 +0800 Subject: [PATCH 2/6] feat(toolchain): MSVC system-toolchain detection & selection (msvc@system) (0.0.88) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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@ 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 --- .github/workflows/ci-windows.yml | 32 ++++ CHANGELOG.md | 24 +++ docs/03-toolchains.md | 46 +++++ mcpp.toml | 2 +- src/build/prepare.cppm | 37 +++- src/doctor.cppm | 26 +++ src/toolchain/detect.cppm | 12 ++ src/toolchain/fingerprint.cppm | 2 +- src/toolchain/lifecycle.cppm | 118 +++++++++++- src/toolchain/model.cppm | 12 ++ src/toolchain/msvc.cppm | 200 ++++++++++++++++++++ src/toolchain/registry.cppm | 12 ++ tests/e2e/95_msvc_system_toolchain.sh | 65 +++++++ tests/e2e/96_msvc_unavailable_nonwindows.sh | 43 +++++ tests/e2e/run_all.sh | 9 + tests/unit/test_toolchain_msvc.cpp | 96 ++++++++++ 16 files changed, 723 insertions(+), 13 deletions(-) create mode 100755 tests/e2e/95_msvc_system_toolchain.sh create mode 100755 tests/e2e/96_msvc_unavailable_nonwindows.sh create mode 100644 tests/unit/test_toolchain_msvc.cpp diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 5318e5a..f5b9571 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -230,6 +230,38 @@ jobs: "$MCPP" clean --bmi-cache "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved llvm@20.1.7" build.log + # windows-latest ships VS 2022 Enterprise with the VC workload, so + # msvc@system detection MUST succeed here — a failure is a regression + # in the discovery/identification chain (vswhere → env → paths). + - name: "Toolchain: MSVC — detection & selection (msvc@system)" + shell: bash + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + + out=$("$MCPP_SELF" toolchain default msvc); echo "$out" + grep -q "Detected" <<<"$out" + grep -q "msvc@system" <<<"$out" + + "$MCPP_SELF" toolchain list | tee tc-list.txt + grep -E '\*\s*msvc' tc-list.txt + + "$MCPP_SELF" self doctor 2>&1 | tee doctor.txt || true + grep -qi "msvc" doctor.txt + + # native cl.exe builds are gated with one owned message + TMP=$(mktemp -d); cd "$TMP" + "$MCPP_SELF" new hello_msvc && cd hello_msvc + set +e + bout=$("$MCPP_SELF" build 2>&1); brc=$? + set -e + echo "$bout" + test $brc -ne 0 + grep -q "not yet supported" <<<"$bout" + + # restore the LLVM default for the remaining steps + cd "$GITHUB_WORKSPACE" + "$MCPP_SELF" toolchain default llvm@20.1.7 + - name: Package Windows release zip id: package shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index e7dc169..d45949b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,30 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.88] — 2026-07-13 + +### 新增 + +- **MSVC 系统工具链支持(`msvc@system`,detection-first)**。MSVC 作为首个 + "系统工具链"接入:mcpp 负责**定位与识别**(vswhere → `VSINSTALLDIR`/ + `VS*COMNTOOLS` → 标准安装路径三级发现;cl.exe banner 解析出编译器版本/架构, + 容错本地化 banner),**从不安装/卸载** MSVC 本体。 + - `mcpp toolchain default msvc`:检测系统 MSVC,打印 VS 产品/VC tools/ + cl 版本与 `std.ixx`(import std)可用性,持久化稳定 spec `msvc@system` + (不落具体版本,VS 升级后配置依然有效);未安装时输出安装指引 + (VS Installer C++ 工作负载 / `winget install …BuildTools`)并退出非零。 + `msvc@19.44` 形式为 pin 校验(仍取最新 VC tools,前缀不符则报错)。 + - `mcpp toolchain list`:Windows 上新增 `System:` 段展示检测到的 MSVC; + `install msvc` 报告已装现状或给指引;`remove msvc` 明确拒绝(系统组件)。 + - `mcpp self doctor`:Windows 上新增 "msvc (system)" 检查段。 + - manifest 支持 `[toolchain] windows = "msvc@system"`(types.cppm 注释中的 + 既有 schema 首次落地);非 Windows 主机使用 msvc spec 时给出明确报错。 + - `mcpp build`:原生 cl.exe 构建(.ifc 管线)**本版暂不支持**,在工具链解析 + 后以单一 owned 错误信息拦截,并提示可用的 `llvm@20.1.7`(MSVC-ABI Clang)。 + - detect() 新增 cl.exe 分类路径(文件名短路,banner → 版本/triple), + `bmi_traits` 预置 MSVC `.ifc` 分支;e2e 95/96 + 单测覆盖。 + - 设计文档:`.agents/docs/2026-07-13-msvc-system-toolchain-detection-design.md`。 + ## [0.0.87] — 2026-07-09 ### 修复 diff --git a/docs/03-toolchains.md b/docs/03-toolchains.md index b7e1422..1ed8ef9 100644 --- a/docs/03-toolchains.md +++ b/docs/03-toolchains.md @@ -69,6 +69,52 @@ Available (run `mcpp toolchain install `): The entry marked with `*` is the current default toolchain. `@mcpp/...` is shorthand for `~/.mcpp/...`, used to keep the output narrower. +## MSVC (System Toolchain, Windows) + +MSVC is different from every other toolchain mcpp manages: it is a **system +toolchain**. mcpp locates and identifies an installed Visual Studio / Build +Tools — it never installs, updates, or removes MSVC itself. + +```bash +mcpp toolchain default msvc +``` + +On a machine with MSVC installed, mcpp auto-locates it (via `vswhere.exe`, +then `VSINSTALLDIR`/`VS*COMNTOOLS`, then the standard install paths), +identifies the versions involved, and persists the stable spec `msvc@system`: + +``` +Detected msvc 19.44.35211 (VS 2022 BuildTools) (VC tools 14.44.35207) + cl: C:\Program Files\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.44.35207\bin\Hostx64\x64\cl.exe + import std: available (std.ixx) +Default set to msvc@system (was: llvm@20.1.7) +``` + +If MSVC is **not** installed, mcpp prints installation guidance instead +(Visual Studio Installer with the *Desktop development with C++* workload, or +`winget install Microsoft.VisualStudio.2022.BuildTools`) and exits non-zero — +install it yourself, then re-run the command. + +`mcpp toolchain list` shows the detected MSVC in a separate `System:` section, +and `mcpp self doctor` reports its status on Windows. In a manifest you can +pin it per-platform: + +```toml +[toolchain] +windows = "msvc@system" +``` + +`msvc@` (e.g. `msvc@19.44`) acts as a pin-verify: mcpp still uses the +newest installed VC tools, but errors if the detected version doesn't match +the prefix. + +> [!NOTE] +> Selection and detection are supported today; **building with native MSVC +> (cl.exe) is not yet supported** — `mcpp build` fails with a clear message +> naming the detected version. For building on Windows use the MSVC-ABI Clang +> toolchain: `mcpp toolchain default llvm@20.1.7` (it borrows the MSVC STL +> from the same detected installation). + ## Project-Level Version Pinning If a project needs to pin a specific version rather than rely on the global default, declare it in the project's `mcpp.toml`: diff --git a/mcpp.toml b/mcpp.toml index 130318b..e75db21 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.87" +version = "0.0.88" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 02216dd..3157fe3 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -18,6 +18,7 @@ import mcpp.modgraph.validate; import mcpp.toolchain.clang; import mcpp.toolchain.detect; import mcpp.toolchain.fingerprint; +import mcpp.toolchain.msvc; import mcpp.toolchain.registry; import mcpp.toolchain.stdmod; import mcpp.toolchain.post_install; @@ -682,7 +683,28 @@ prepare_build(bool print_fingerprint, } } - if (tcSpec.has_value() && *tcSpec != "system") { + // msvc@system: a *system* toolchain — located on the machine, never + // resolved through xim packages. mcpp does not install MSVC. + bool tcSpecIsMsvc = false; + if (tcSpec.has_value()) { + if (auto s = mcpp::toolchain::parse_toolchain_spec(*tcSpec); + s && mcpp::toolchain::is_system_toolchain(*s)) + tcSpecIsMsvc = true; + } + if (tcSpecIsMsvc) { + if (!mcpp::platform::is_windows) { + return std::unexpected(std::format( + "toolchain '{}' is only available on Windows hosts", *tcSpec)); + } + auto inst = mcpp::toolchain::msvc::detect_installation(); + if (!inst) { + return std::unexpected(mcpp::toolchain::msvc::install_guidance()); + } + explicit_compiler = inst->clPath; + mcpp::ui::info("Resolved", std::format( + "msvc@system → msvc {} ({})", + inst->display_version(), inst->clPath.string())); + } else if (tcSpec.has_value() && *tcSpec != "system") { auto spec = mcpp::toolchain::parse_toolchain_spec(*tcSpec); if (!spec || spec->version.empty()) { return std::unexpected(std::format( @@ -841,6 +863,19 @@ prepare_build(bool print_fingerprint, auto tc = mcpp::toolchain::detect(explicit_compiler); if (!tc) return std::unexpected(tc.error().message); + // Native MSVC builds are gated off until the cl.exe/.ifc pipeline lands. + // Selection & detection (`mcpp toolchain default msvc`, `list`, `doctor`) + // work; the build must fail here with one owned message rather than an + // incidental error deep in the GCC/Clang-shaped flag machinery. + if (tc->compiler == mcpp::toolchain::CompilerId::MSVC) { + return std::unexpected(std::format( + "native MSVC (cl.exe) builds are not yet supported by mcpp.\n" + " detected: msvc {} at {} (selection & detection work today)\n" + " for building on Windows use the MSVC-ABI Clang toolchain instead:\n" + " mcpp toolchain default llvm@20.1.7", + tc->version, tc->binaryPath.string())); + } + // For musl-gcc the toolchain is fully self-contained // (`/x86_64-linux-musl/{include,lib}` is its own sysroot). // musl-gcc's `-dumpmachine` reports `x86_64-linux-musl`. diff --git a/src/doctor.cppm b/src/doctor.cppm index 1c67e8e..60e168a 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -16,8 +16,10 @@ import mcpp.build.plan; import mcpp.config; import mcpp.fallback.install_integrity; import mcpp.fetcher.progress; +import mcpp.platform; import mcpp.platform.process; import mcpp.toolchain.detect; +import mcpp.toolchain.msvc; import mcpp.toolchain.registry; import mcpp.toolchain.stdmod; import mcpp.toolchain.abi; @@ -96,6 +98,30 @@ export int doctor_report() { ok(std::format("{} at {}", tc->label(), tc->binaryPath.string())); } + // Windows: report the system MSVC (msvc@system). Absence is a warning, + // not an error — mcpp works with LLVM/Clang without it, and mcpp never + // installs MSVC itself. + if (mcpp::platform::is_windows) { + mcpp::ui::status("Checking", "msvc (system)"); + if (auto inst = mcpp::toolchain::msvc::detect_installation()) { + ok(std::format("msvc {}{} (VC tools {})", + inst->display_version(), + inst->vsProduct.empty() + ? std::string{} + : std::format(" (VS {})", inst->vsProduct), + inst->toolsVersion)); + ok(std::format("cl at {}", inst->clPath.string())); + if (inst->hasStdModules) { + ok("import std: std.ixx available"); + } else { + warn("MSVC STL std.ixx missing (VC tools too old for import std?)"); + } + } else { + warn("msvc not detected — run `mcpp toolchain default msvc` for " + "setup guidance (mcpp does not install MSVC)"); + } + } + mcpp::ui::status("Checking", "std module"); if (tc) { auto cacheRoot = mcpp::toolchain::default_cache_root(); diff --git a/src/toolchain/detect.cppm b/src/toolchain/detect.cppm index f0d9c47..c3079e0 100644 --- a/src/toolchain/detect.cppm +++ b/src/toolchain/detect.cppm @@ -8,6 +8,7 @@ export import mcpp.toolchain.probe; import std; import mcpp.toolchain.clang; import mcpp.toolchain.gcc; +import mcpp.toolchain.msvc; import mcpp.xlings; export namespace mcpp::toolchain { @@ -38,6 +39,17 @@ detect(const std::filesystem::path& explicit_compiler) { Toolchain tc; tc.binaryPath = *bin_r; + + // MSVC cl.exe has no --version / -dumpmachine / -print-sysroot; classify + // it by filename and take a dedicated enrich path (banner → version, + // arch → triple, std.ixx lookup). No runtime dirs / sysroot / payloads — + // those concepts are GCC/Clang-shaped. + if (auto stem = lower_copy(tc.binaryPath.stem().string()); stem == "cl") { + if (auto r = mcpp::toolchain::msvc::enrich_toolchain_from_cl(tc); !r) + return std::unexpected(r.error()); + return tc; + } + tc.compilerRuntimeDirs = discover_compiler_runtime_dirs(tc.binaryPath); auto envPrefix = compiler_env_prefix(tc); diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 7b30e0f..cb12886 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.87"; +inline constexpr std::string_view MCPP_VERSION = "0.0.88"; struct FingerprintInputs { Toolchain toolchain; diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index 83405fb..220ab1d 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -15,6 +15,7 @@ import mcpp.fetcher.progress; import mcpp.manifest; import mcpp.platform; import mcpp.toolchain.detect; +import mcpp.toolchain.msvc; import mcpp.toolchain.registry; import mcpp.toolchain.post_install; import mcpp.ui; @@ -164,6 +165,27 @@ struct EffectiveDefault { bool fromProject = false; }; +// ─── msvc@system: system-toolchain helpers ────────────────────────────── +// +// MSVC is located and identified, never installed/removed by mcpp. All four +// subcommands branch here before the xim-package path. + +int msvc_wrong_host() { + mcpp::ui::error("the msvc toolchain is only available on Windows hosts"); + return 1; +} + +void msvc_print_detected(const mcpp::toolchain::msvc::MsvcInstallation& inst) { + mcpp::ui::status("Detected", std::format( + "msvc {}{} (VC tools {})", + inst.display_version(), + inst.vsProduct.empty() ? "" : std::format(" (VS {})", inst.vsProduct), + inst.toolsVersion)); + std::println(" cl: {}", inst.clPath.string()); + std::println(" import std: {}", + inst.hasStdModules ? "available (std.ixx)" : "not available"); +} + EffectiveDefault effective_default_toolchain(const mcpp::config::GlobalConfig& cfg) { std::error_code ec; auto mpath = std::filesystem::current_path(ec) / "mcpp.toml"; @@ -234,6 +256,26 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) { } } + // ─── System section (Windows: detected MSVC) ──────────────────── + // MSVC is never in xpkgs — it's located on the machine. Show it so + // `toolchain list` reflects everything `toolchain default` accepts. + if (mcpp::platform::is_windows) { + if (auto inst = mcpp::toolchain::msvc::detect_installation()) { + bool isDefault = mcpp::toolchain::matches_default_toolchain( + effective.spec, "msvc", inst->display_version()); + std::println(""); + std::println("System:"); + std::println(" {:<3}{:<22} {}", + isDefault ? "*" : "", + mcpp::toolchain::display_label("msvc", inst->display_version()), + inst->clPath.string()); + } else { + std::println(""); + std::println(" (msvc: not detected — run `mcpp toolchain default msvc` " + "for setup guidance)"); + } + } + // ─── Available section ────────────────────────────────────────── // List xim:gcc + xim:musl-gcc versions known to the local index // that aren't already installed. Helpful to discover what users @@ -280,6 +322,34 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) { // `mcpp toolchain install ` — install + post-install fixups. export int toolchain_install(const mcpp::config::GlobalConfig& cfg, const std::string& pos0, const std::string& pos1) { + // Accept three input shapes — they all collapse to (compiler, version): + // mcpp toolchain install gcc 16.1.0 → ("gcc", "16.1.0") + // mcpp toolchain install gcc@16.1.0 → ("gcc", "16.1.0") + // mcpp toolchain install gcc 15 → ("gcc", "15") partial + // (parsed before the bootstrap check: system toolchains need no + // bootstrap, and an invalid spec should not report a bootstrap error) + auto spec = mcpp::toolchain::parse_toolchain_spec( + pos0, pos1); + if (!spec || spec->compiler.empty()) { + mcpp::ui::error("missing compiler name; e.g. `mcpp toolchain install gcc 16.1.0`"); + return 2; + } + + // msvc@system: mcpp never installs MSVC — report what's there, or + // print installation guidance. + if (mcpp::toolchain::is_system_toolchain(*spec)) { + if (!mcpp::platform::is_windows) return msvc_wrong_host(); + if (auto inst = mcpp::toolchain::msvc::detect_installation()) { + msvc_print_detected(*inst); + std::println(""); + std::println("MSVC is already installed — mcpp does not manage it."); + std::println("Tip: `mcpp toolchain default msvc` to make it the default."); + return 0; + } + mcpp::ui::error(mcpp::toolchain::msvc::install_guidance()); + return 1; + } + // Toolchain install needs patchelf (ELF fixup) and ninja (build). // Fail early if bootstrap is incomplete rather than producing a // broken toolchain with missing fixups. @@ -291,16 +361,6 @@ export int toolchain_install(const mcpp::config::GlobalConfig& cfg, return 1; } - // Accept three input shapes — they all collapse to (compiler, version): - // mcpp toolchain install gcc 16.1.0 → ("gcc", "16.1.0") - // mcpp toolchain install gcc@16.1.0 → ("gcc", "16.1.0") - // mcpp toolchain install gcc 15 → ("gcc", "15") partial - auto spec = mcpp::toolchain::parse_toolchain_spec( - pos0, pos1); - if (!spec || spec->compiler.empty()) { - mcpp::ui::error("missing compiler name; e.g. `mcpp toolchain install gcc 16.1.0`"); - return 2; - } auto pkg = mcpp::toolchain::to_xim_package(*spec); // Partial-version resolution: `gcc 15` → highest available 15.x.y in @@ -385,6 +445,39 @@ export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, mcpp::ui::error("missing spec; e.g. `mcpp toolchain default gcc@16.1.0`"); return 2; } + + // msvc@system: locate + identify the system MSVC, persist the stable + // spec (never a concrete version — config survives VS updates). + if (mcpp::toolchain::is_system_toolchain(*spec)) { + if (!mcpp::platform::is_windows) return msvc_wrong_host(); + auto inst = mcpp::toolchain::msvc::detect_installation(); + if (!inst) { + mcpp::ui::error(mcpp::toolchain::msvc::install_guidance()); + return 1; + } + // `msvc@19.44` is a pin-verify against the detected install, not + // a selection among many — mcpp always uses the newest VC tools. + if (!spec->version.empty() && spec->version != "system" + && !inst->display_version().starts_with(spec->version)) { + mcpp::ui::error(std::format( + "msvc@{} requested, but the system MSVC is {} (VC tools {})", + spec->version, inst->display_version(), inst->toolsVersion)); + return 1; + } + msvc_print_detected(*inst); + auto wr = mcpp::config::write_default_toolchain(cfg, "msvc@system"); + if (!wr) { + mcpp::ui::error(wr.error().message); + return 1; + } + mcpp::ui::status("Default", std::format( + "set to msvc@system (was: {})", + cfg.defaultToolchain.empty() ? "" : cfg.defaultToolchain)); + std::println("note: `mcpp build` with native MSVC (cl.exe) is not yet " + "supported — coming in a later release."); + return 0; + } + auto pkg = mcpp::toolchain::to_xim_package(*spec); // Partial-version resolution against installed payloads. @@ -421,6 +514,11 @@ export int toolchain_remove(const mcpp::config::GlobalConfig& cfg, const std::string& pos0) { auto xlEnv = mcpp::config::make_xlings_env(cfg); auto parsedSpec = mcpp::toolchain::parse_toolchain_spec(pos0); + if (parsedSpec && mcpp::toolchain::is_system_toolchain(*parsedSpec)) { + mcpp::ui::error("msvc is a system toolchain managed by the Visual " + "Studio Installer — mcpp cannot remove it"); + return 1; + } if (!parsedSpec || parsedSpec->version.empty()) { mcpp::ui::error(std::format("invalid spec '{}'", pos0)); return 2; diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index 58ac208..288c8dc 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -85,6 +85,18 @@ bool is_msvc_target(const Toolchain& tc) { } BmiTraits bmi_traits(const Toolchain& tc) { + if (tc.compiler == CompilerId::MSVC) { + // Native cl.exe builds are gated off until the .ifc pipeline lands; + // these traits exist so nothing silently reuses the GCC defaults. + return { + .bmiDir = "ifc.cache", + .bmiExt = ".ifc", + .manifestPrefix = "ifc", + .needsExplicitModuleOutput = true, + .needsPrebuiltModulePath = false, + .scanNeedsFModules = false, + }; + } if (is_clang(tc)) { return { .bmiDir = "pcm.cache", diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index ffe786c..b2109aa 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -18,6 +18,8 @@ export module mcpp.toolchain.msvc; import std; import mcpp.platform; +import mcpp.toolchain.model; +import mcpp.toolchain.probe; export namespace mcpp::toolchain::msvc { @@ -33,6 +35,46 @@ std::optional find_std_module_source(); // Find cl.exe (for future MSVC toolchain support). std::optional find_cl(); +// ─── System-toolchain detection (msvc@system) ──────────────────────────── +// +// mcpp treats MSVC as a *system* toolchain: it locates and identifies an +// installed Visual Studio / Build Tools, but never installs or removes one. + +struct MsvcInstallation { + std::filesystem::path vsRoot; // …\Microsoft Visual Studio\2022\BuildTools + std::string vsProduct; // "2022 BuildTools" (path-derived; may be empty) + std::string toolsVersion; // "14.44.35207" (VC\Tools\MSVC\) + std::filesystem::path clPath; // …\bin\Hostx64\x64\cl.exe + std::string clVersion; // "19.44.35211" (banner; empty if unparseable) + std::string arch; // "x64" | "x86" | "arm64" + bool hasStdModules = false; // modules\std.ixx present + + // Preferred user-facing version: compiler version, else tools version. + std::string display_version() const { + return clVersion.empty() ? toolsVersion : clVersion; + } +}; + +// Locate the best (newest) usable installation. nullopt = MSVC absent. +std::optional detect_installation(); + +// Parse a cl.exe banner into (version, arch). Token-based so localized +// banners work: first "d.d.d[.d]" run is the version, arch is the arm64/x64/ +// x86 token. Pure and cross-platform for unit testing. +std::optional> +parse_cl_banner(std::string_view banner); + +// Map a cl banner arch token to the canonical windows-msvc triple. +std::string triple_for_arch(std::string_view arch); + +// Multi-line guidance shown wherever MSVC is required but absent. +// States what was searched and how to install (mcpp does not install MSVC). +std::string install_guidance(); + +// Classify + enrich an already-probed cl.exe binary for detect(): +// version/arch from the banner, targetTriple, driverIdent, std.ixx lookup. +std::expected enrich_toolchain_from_cl(Toolchain& tc); + } // namespace mcpp::toolchain::msvc namespace mcpp::toolchain::msvc { @@ -185,4 +227,162 @@ std::optional find_cl() { return std::nullopt; } +// ─── System-toolchain detection ────────────────────────────────────────── + +std::optional> +parse_cl_banner(std::string_view banner) { + // Version: first digit/dot run with at least two dots ("19.44.35211", + // possibly four components). Never anchored to the English word + // "Version" — localized banners reorder the sentence. + std::string version; + { + std::string run; + int dots = 0; + auto flush = [&] { + if (version.empty() && dots >= 2 && run.back() != '.') + version = run; + run.clear(); + dots = 0; + }; + for (char c : banner) { + if (c >= '0' && c <= '9') { run += c; } + else if (c == '.' && !run.empty()) { run += c; ++dots; } + else if (!run.empty()) { flush(); } + if (!version.empty()) break; + } + if (!run.empty()) flush(); + } + if (version.empty()) return std::nullopt; + + auto lower = mcpp::toolchain::lower_copy(banner); + std::string arch; + if (lower.find("arm64") != std::string::npos) arch = "arm64"; + else if (lower.find("x64") != std::string::npos) arch = "x64"; + else if (lower.find("x86") != std::string::npos) arch = "x86"; + + return std::pair{version, arch}; +} + +std::string triple_for_arch(std::string_view arch) { + if (arch == "arm64") return "aarch64-pc-windows-msvc"; + if (arch == "x86") return "i686-pc-windows-msvc"; + return "x86_64-pc-windows-msvc"; +} + +std::string install_guidance() { + return + "MSVC was not found on this system.\n" + " searched: vswhere.exe, VSINSTALLDIR / VS*COMNTOOLS, and the standard\n" + " 'Program Files\\Microsoft Visual Studio\\\\' paths\n" + " mcpp does not install MSVC — install it yourself, then retry:\n" + " - Visual Studio Installer: add the 'Desktop development with C++' workload\n" + " (component: Microsoft.VisualStudio.Component.VC.Tools.x86.x64)\n" + " - or Build Tools only: winget install Microsoft.VisualStudio.2022.BuildTools\n" + " then add the C++ workload in the installer\n" + " afterwards run: mcpp toolchain default msvc"; +} + +namespace { + +// "…\Microsoft Visual Studio\2022\BuildTools" → "2022 BuildTools". +[[maybe_unused]] std::string product_from_vs_root(const std::filesystem::path& vsRoot) { + std::vector parts; + for (auto& seg : vsRoot) parts.push_back(seg.string()); + for (std::size_t i = 0; i + 2 < parts.size(); ++i) { + if (parts[i] == "Microsoft Visual Studio") + return parts[i + 1] + " " + parts[i + 2]; + } + return {}; +} + +// Capture cl.exe's banner. cl prints it (plus a usage complaint) when run +// bare; the exit status is irrelevant — parse whatever came out. +std::string capture_cl_banner(const std::filesystem::path& cl) { + auto r = mcpp::platform::process::capture( + "\"" + cl.string() + "\" 2>&1"); + return r.output; +} + +} // namespace + +std::optional detect_installation() { +#if defined(_WIN32) + auto vs = find_vs_install_path(); + if (!vs) return std::nullopt; + auto tools = find_latest_msvc_tools(*vs); + if (!tools) return std::nullopt; + + MsvcInstallation inst; + inst.vsRoot = *vs; + inst.vsProduct = product_from_vs_root(*vs); + inst.toolsVersion = tools->filename().string(); + + // Host-native bin dir first (arm64 hosts run arm64 cl; everything else + // x64), with the remaining pairs as fallback. + std::vector> pairs; + if (mcpp::platform::host_arch == std::string_view("aarch64") + || mcpp::platform::host_arch == std::string_view("arm64")) { + pairs = {{"Hostarm64", "arm64"}, {"Hostx64", "x64"}, {"Hostx86", "x86"}}; + } else { + pairs = {{"Hostx64", "x64"}, {"Hostarm64", "arm64"}, {"Hostx86", "x86"}}; + } + for (auto [host, target] : pairs) { + auto cl = *tools / "bin" / host / target / "cl.exe"; + std::error_code ec; + if (std::filesystem::exists(cl, ec)) { + inst.clPath = cl; + inst.arch = std::string(target); + break; + } + } + if (inst.clPath.empty()) return std::nullopt; + + std::error_code ec; + inst.hasStdModules = + std::filesystem::exists(*tools / "modules" / "std.ixx", ec); + + // Version identification: banner is authoritative; tolerate failure + // (clVersion stays empty and display_version() falls back to the + // tools-dir version). + if (auto parsed = parse_cl_banner(capture_cl_banner(inst.clPath))) { + inst.clVersion = parsed->first; + if (!parsed->second.empty()) inst.arch = parsed->second; + } + return inst; +#else + return std::nullopt; +#endif +} + +std::expected enrich_toolchain_from_cl(Toolchain& tc) { + auto banner = capture_cl_banner(tc.binaryPath); + auto parsed = parse_cl_banner(banner); + if (!parsed) { + return std::unexpected(DetectError{std::format( + "'{}' looks like MSVC cl but produced no recognizable banner:\n{}", + tc.binaryPath.string(), banner)}); + } + tc.compiler = CompilerId::MSVC; + tc.version = parsed->first; + tc.targetTriple = triple_for_arch(parsed->second); + tc.driverIdent = mcpp::toolchain::normalize_driver_output(banner); + + // MSVC STL ships std.ixx next to the tools dir the cl binary lives in: + // \bin\Host*\*\cl.exe → \modules\std.ixx. + auto toolsDir = tc.binaryPath.parent_path() // x64 + .parent_path() // Hostx64 + .parent_path() // bin + .parent_path(); // + std::error_code ec; + if (auto ixx = toolsDir / "modules" / "std.ixx"; + std::filesystem::exists(ixx, ec)) { + tc.stdModuleSource = ixx; + tc.hasImportStd = true; + } else if (auto found = find_std_module_source()) { + tc.stdModuleSource = *found; + tc.hasImportStd = true; + } + return {}; +} + } // namespace mcpp::toolchain::msvc diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index a92688b..d01a71a 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -58,6 +58,11 @@ bool matches_default_toolchain(std::string_view configuredDefault, std::string_view compiler, std::string_view version); +// System toolchains are located on the machine, never installed/removed by +// mcpp. Today that's MSVC (`msvc@system`); the PATH-compiler escape hatch +// (`[toolchain] … = "system"`) is a separate, older mechanism. +bool is_system_toolchain(const ToolchainSpec& spec); + std::vector> available_toolchain_indexes(); std::filesystem::path derive_c_compiler(const Toolchain& tc); @@ -227,9 +232,16 @@ bool matches_default_toolchain(std::string_view configuredDefault, && configuredDefault == std::format("gcc@{}-musl", version)) { return true; } + // The persisted msvc default is always the stable "msvc@system" (never a + // concrete version), so it matches whatever version detection reports. + if (compiler == "msvc" && configuredDefault == "msvc@system") return true; return false; } +bool is_system_toolchain(const ToolchainSpec& spec) { + return spec.compiler == "msvc"; +} + std::vector> available_toolchain_indexes() { return { {"gcc", "gcc"}, diff --git a/tests/e2e/95_msvc_system_toolchain.sh b/tests/e2e/95_msvc_system_toolchain.sh new file mode 100755 index 0000000..3853c12 --- /dev/null +++ b/tests/e2e/95_msvc_system_toolchain.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# requires: msvc +# 95_msvc_system_toolchain.sh — msvc@system detection & selection (Windows): +# - `toolchain default msvc` locates + identifies the system MSVC and +# persists the stable spec msvc@system +# - `toolchain list` shows the detected MSVC in a System section, starred +# - version pin-verify: `msvc@99` mismatches the detected install +# - `toolchain remove/install msvc`: mcpp never manages MSVC itself +# - `mcpp build` fails with the owned "not yet supported" gate message +set -e + +# This test flips the global default toolchain; save + restore it so later +# tests / CI steps keep their configured toolchain. +CONF="${MCPP_HOME:-$HOME/.mcpp}/config.toml" +ORIG_DEFAULT="" +if [[ -f "$CONF" ]]; then + ORIG_DEFAULT=$(sed -n '/^\[toolchain\]/,/^\[/p' "$CONF" \ + | grep '^default' | head -1 | cut -d'"' -f2 || true) +fi +TMP=$(mktemp -d) +restore() { + if [[ -n "$ORIG_DEFAULT" ]]; then + "$MCPP" toolchain default "$ORIG_DEFAULT" >/dev/null 2>&1 || true + fi + rm -rf "$TMP" +} +trap restore EXIT + +# 1) switch to msvc: detect + identify + persist +out=$("$MCPP" toolchain default msvc 2>&1) || { echo "FAIL: default msvc: $out"; exit 1; } +[[ "$out" == *"Detected"* ]] || { echo "FAIL: no Detected line: $out"; exit 1; } +[[ "$out" == *"msvc "* ]] || { echo "FAIL: no msvc version: $out"; exit 1; } +[[ "$out" == *"msvc@system"* ]] || { echo "FAIL: default not msvc@system: $out"; exit 1; } +[[ "$out" == *"cl:"* ]] || { echo "FAIL: no cl path: $out"; exit 1; } + +# 2) list shows the System section with the effective-default star +out=$("$MCPP" toolchain list 2>&1) +[[ "$out" == *"System:"* ]] || { echo "FAIL: no System section: $out"; exit 1; } +echo "$out" | grep -E '\*\s*msvc' >/dev/null \ + || { echo "FAIL: msvc row not starred as default: $out"; exit 1; } + +# 3) version pin-verify: an impossible major must mismatch +rc=0; out=$("$MCPP" toolchain default msvc@99 2>&1) || rc=$? +[[ $rc -ne 0 ]] || { echo "FAIL: msvc@99 should mismatch"; exit 1; } +[[ "$out" == *"requested"* ]] || { echo "FAIL: mismatch message: $out"; exit 1; } + +# 4) mcpp never manages the MSVC installation +rc=0; out=$("$MCPP" toolchain remove msvc 2>&1) || rc=$? +[[ $rc -ne 0 && "$out" == *"system toolchain"* ]] \ + || { echo "FAIL: remove msvc: rc=$rc out=$out"; exit 1; } +out=$("$MCPP" toolchain install msvc 2>&1) \ + || { echo "FAIL: install msvc (present) should exit 0: $out"; exit 1; } +[[ "$out" == *"already installed"* ]] \ + || { echo "FAIL: install msvc message: $out"; exit 1; } + +# 5) native cl.exe builds are gated with one owned message +cd "$TMP" +"$MCPP" new hello_msvc >/dev/null 2>&1 +cd hello_msvc +rc=0; out=$("$MCPP" build 2>&1) || rc=$? +[[ $rc -ne 0 ]] || { echo "FAIL: build should be gated"; exit 1; } +[[ "$out" == *"not yet supported"* ]] || { echo "FAIL: gate message: $out"; exit 1; } +[[ "$out" == *"llvm@20.1.7"* ]] || { echo "FAIL: no alternative hint: $out"; exit 1; } + +echo "PASS: msvc@system detection, selection, guidance, and build gate" diff --git a/tests/e2e/96_msvc_unavailable_nonwindows.sh b/tests/e2e/96_msvc_unavailable_nonwindows.sh new file mode 100755 index 0000000..41e07d0 --- /dev/null +++ b/tests/e2e/96_msvc_unavailable_nonwindows.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# requires: unix-shell +# 96_msvc_unavailable_nonwindows.sh — msvc@system is Windows-only: +# - `toolchain default/install msvc` fail with a host error (no network, +# no sandbox mutation) +# - `toolchain list` has no System section off-Windows +# - a manifest pinned to msvc@system fails the same way at build time +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +rc=0; out=$("$MCPP" toolchain default msvc 2>&1) || rc=$? +[[ $rc -ne 0 && "$out" == *"only available on Windows"* ]] \ + || { echo "FAIL: default msvc: rc=$rc out=$out"; exit 1; } + +rc=0; out=$("$MCPP" toolchain install msvc 2>&1) || rc=$? +[[ $rc -ne 0 && "$out" == *"only available on Windows"* ]] \ + || { echo "FAIL: install msvc: rc=$rc out=$out"; exit 1; } + +out=$("$MCPP" toolchain list 2>&1) || true +[[ "$out" != *"System:"* ]] || { echo "FAIL: System section off-Windows: $out"; exit 1; } + +# Manifest pinned to msvc@system errors out at resolution, not mid-build. +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" +cat > mcpp.toml < src/main.cpp <<'EOF' +import std; +int main() { std::println("never built"); return 0; } +EOF +rc=0; out=$("$MCPP" build 2>&1) || rc=$? +[[ $rc -ne 0 && "$out" == *"only available on Windows"* ]] \ + || { echo "FAIL: build with msvc@system manifest: rc=$rc out=$out"; exit 1; } + +echo "PASS: msvc@system correctly rejected off-Windows" diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index 6fff57a..dade949 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -67,6 +67,15 @@ case "$OS" in if [[ "${MSYS:-}" == *winsymlinks* ]] || cmd.exe /c "mklink /?" &>/dev/null 2>&1; then CAPS+=(symlink) fi + # msvc: a system Visual Studio / Build Tools with the VC workload + # (what `mcpp toolchain default msvc` must be able to detect). + VSWHERE="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" + if [[ -f "$VSWHERE" ]] \ + && "$VSWHERE" -latest -products '*' \ + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 \ + -property installationPath 2>/dev/null | grep -q .; then + CAPS+=(msvc) + fi # NOTE: Windows runners may have g++.exe (MinGW/Strawberry) in PATH # but it's not a proper mcpp-compatible GCC. Don't add gcc capability. # fresh-sandbox: not yet reliable on Windows — xlings LLVM auto-install diff --git a/tests/unit/test_toolchain_msvc.cpp b/tests/unit/test_toolchain_msvc.cpp new file mode 100644 index 0000000..a504834 --- /dev/null +++ b/tests/unit/test_toolchain_msvc.cpp @@ -0,0 +1,96 @@ +#include + +import std; +import mcpp.toolchain.model; +import mcpp.toolchain.msvc; +import mcpp.toolchain.registry; + +using namespace mcpp::toolchain; + +// ─── parse_cl_banner ───────────────────────────────────────────────────── + +TEST(MsvcBanner, ParsesEnglishBanner) { + auto r = msvc::parse_cl_banner( + "Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35211 for x64\n" + "Copyright (C) Microsoft Corporation. All rights reserved.\n" + "\n" + "usage: cl [ option... ] filename... [ /link linkoption... ]\n"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->first, "19.44.35211"); + EXPECT_EQ(r->second, "x64"); +} + +TEST(MsvcBanner, ParsesLocalizedBannerByTokens) { + // Chinese VS reorders the sentence; only the tokens are stable. + auto r = msvc::parse_cl_banner( + "用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.44.35211 版\n" + "版权所有(C) Microsoft Corporation。保留所有权利。\n"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->first, "19.44.35211"); + EXPECT_EQ(r->second, "x64"); +} + +TEST(MsvcBanner, ParsesArm64AndFourComponentVersions) { + auto r = msvc::parse_cl_banner( + "Microsoft (R) C/C++ Optimizing Compiler Version 19.29.30133.0 for ARM64\n"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->first, "19.29.30133.0"); + EXPECT_EQ(r->second, "arm64"); +} + +TEST(MsvcBanner, RejectsGarbage) { + EXPECT_FALSE(msvc::parse_cl_banner("").has_value()); + EXPECT_FALSE(msvc::parse_cl_banner("bash: cl: command not found").has_value()); + // A bare two-component number is not a cl version. + EXPECT_FALSE(msvc::parse_cl_banner("something 1.2 else").has_value()); +} + +TEST(MsvcBanner, TripleForArch) { + EXPECT_EQ(msvc::triple_for_arch("x64"), "x86_64-pc-windows-msvc"); + EXPECT_EQ(msvc::triple_for_arch("x86"), "i686-pc-windows-msvc"); + EXPECT_EQ(msvc::triple_for_arch("arm64"), "aarch64-pc-windows-msvc"); + // Unknown/empty arch falls back to the x64 triple. + EXPECT_EQ(msvc::triple_for_arch(""), "x86_64-pc-windows-msvc"); +} + +// ─── install guidance ──────────────────────────────────────────────────── + +TEST(MsvcGuidance, MentionsInstallRoutesAndOwnership) { + auto g = msvc::install_guidance(); + ASSERT_FALSE(g.empty()); + EXPECT_NE(g.find("winget"), std::string::npos); + EXPECT_NE(g.find("does not install"), std::string::npos); + EXPECT_NE(g.find("mcpp toolchain default msvc"), std::string::npos); +} + +// ─── spec layer ────────────────────────────────────────────────────────── + +TEST(MsvcSpec, SystemToolchainClassification) { + for (auto s : {"msvc", "msvc@system", "msvc@19.44"}) { + auto spec = parse_toolchain_spec(s); + ASSERT_TRUE(spec.has_value()) << s; + EXPECT_TRUE(is_system_toolchain(*spec)) << s; + } + auto gcc = parse_toolchain_spec("gcc@16.1.0"); + ASSERT_TRUE(gcc.has_value()); + EXPECT_FALSE(is_system_toolchain(*gcc)); +} + +TEST(MsvcSpec, StableDefaultMatchesAnyDetectedVersion) { + EXPECT_TRUE(matches_default_toolchain("msvc@system", "msvc", "19.44.35211")); + EXPECT_TRUE(matches_default_toolchain("msvc@system", "msvc", "19.29.30133")); + EXPECT_FALSE(matches_default_toolchain("msvc@system", "gcc", "16.1.0")); + EXPECT_FALSE(matches_default_toolchain("gcc@16.1.0", "msvc", "19.44.35211")); +} + +// ─── model traits ──────────────────────────────────────────────────────── + +TEST(MsvcModel, BmiTraitsUseIfc) { + Toolchain tc; + tc.compiler = CompilerId::MSVC; + auto t = bmi_traits(tc); + EXPECT_EQ(t.bmiDir, "ifc.cache"); + EXPECT_EQ(t.bmiExt, ".ifc"); + EXPECT_TRUE(t.needsExplicitModuleOutput); + EXPECT_FALSE(t.scanNeedsFModules); +} From 4cb42116965c8e316d0e6fb336705bec326772d2 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 07:00:29 +0800 Subject: [PATCH 3/6] 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. --- .github/workflows/ci-windows.yml | 5 ++++- tests/e2e/95_msvc_system_toolchain.sh | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index f5b9571..e6d0fb2 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -238,6 +238,10 @@ jobs: run: | export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + # Neutral cwd: the repo root's mcpp.toml [toolchain] would shadow + # the global default in `toolchain list` / doctor output. + TMP=$(mktemp -d); cd "$TMP" + out=$("$MCPP_SELF" toolchain default msvc); echo "$out" grep -q "Detected" <<<"$out" grep -q "msvc@system" <<<"$out" @@ -249,7 +253,6 @@ jobs: grep -qi "msvc" doctor.txt # native cl.exe builds are gated with one owned message - TMP=$(mktemp -d); cd "$TMP" "$MCPP_SELF" new hello_msvc && cd hello_msvc set +e bout=$("$MCPP_SELF" build 2>&1); brc=$? diff --git a/tests/e2e/95_msvc_system_toolchain.sh b/tests/e2e/95_msvc_system_toolchain.sh index 3853c12..2cdd9f8 100755 --- a/tests/e2e/95_msvc_system_toolchain.sh +++ b/tests/e2e/95_msvc_system_toolchain.sh @@ -26,6 +26,11 @@ restore() { } trap restore EXIT +# Neutral cwd: `toolchain list` stars the *effective* default, and a project +# mcpp.toml [toolchain] in the cwd (e.g. the mcpp repo root, where run_all.sh +# executes) would shadow the global default we're about to set. +cd "$TMP" + # 1) switch to msvc: detect + identify + persist out=$("$MCPP" toolchain default msvc 2>&1) || { echo "FAIL: default msvc: $out"; exit 1; } [[ "$out" == *"Detected"* ]] || { echo "FAIL: no Detected line: $out"; exit 1; } @@ -54,7 +59,6 @@ out=$("$MCPP" toolchain install msvc 2>&1) \ || { echo "FAIL: install msvc message: $out"; exit 1; } # 5) native cl.exe builds are gated with one owned message -cd "$TMP" "$MCPP" new hello_msvc >/dev/null 2>&1 cd hello_msvc rc=0; out=$("$MCPP" build 2>&1) || rc=$? From 7d7538b71c5d9b946bc31aeb9546ebba17ecaa19 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 07:40:09 +0800 Subject: [PATCH 4/6] ci: run MSVC detection step before the LLVM self-host rebuild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rebuild cleans target/, invalidating the job's $MCPP_SELF fingerprint path — the MSVC step then failed with exit 127 (binary gone). --- .github/workflows/ci-windows.yml | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index e6d0fb2..2325f70 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -220,19 +220,11 @@ jobs: cd hello_win "$MCPP_SELF" run - - name: "Toolchain: LLVM — build mcpp (self-host)" - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - cp "$MCPP_SELF" /tmp/mcpp-fresh.exe - MCPP=/tmp/mcpp-fresh.exe - "$MCPP" toolchain default llvm@20.1.7 - "$MCPP" clean --bmi-cache - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved llvm@20.1.7" build.log - # windows-latest ships VS 2022 Enterprise with the VC workload, so # msvc@system detection MUST succeed here — a failure is a regression # in the discovery/identification chain (vswhere → env → paths). + # Runs BEFORE the LLVM self-host rebuild: that step cleans + rebuilds + # target/, invalidating this job's $MCPP_SELF fingerprint path. - name: "Toolchain: MSVC — detection & selection (msvc@system)" shell: bash run: | @@ -265,6 +257,16 @@ jobs: cd "$GITHUB_WORKSPACE" "$MCPP_SELF" toolchain default llvm@20.1.7 + - name: "Toolchain: LLVM — build mcpp (self-host)" + shell: bash + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + cp "$MCPP_SELF" /tmp/mcpp-fresh.exe + MCPP=/tmp/mcpp-fresh.exe + "$MCPP" toolchain default llvm@20.1.7 + "$MCPP" clean --bmi-cache + "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved llvm@20.1.7" build.log + - name: Package Windows release zip id: package shell: bash From d0bf7bbbd45004aafbd86cbd88a5ba7e0650f562 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 08:02:39 +0800 Subject: [PATCH 5/6] fix(msvc): well-known-paths fallback covers major-version VS directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/toolchain/msvc.cppm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index b2109aa..7e6c05f 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -141,7 +141,10 @@ std::optional find_vs_via_paths() { "C:\\Program Files\\Microsoft Visual Studio", "C:\\Program Files (x86)\\Microsoft Visual Studio", }; - static constexpr std::string_view years[] = {"2025", "2022", "2019", "2017"}; + // Newer VS installs use the major version as the directory ("18", seen + // on windows-latest 2026-07: …\Microsoft Visual Studio\18\Enterprise), + // older ones the year branding. + static constexpr std::string_view years[] = {"19", "18", "2025", "2022", "2019", "2017"}; static constexpr std::string_view editions[] = { "Enterprise", "Professional", "Community", "BuildTools", "Preview" }; From 4765d19432ca390b1c1a7e2f824eb52f5e1b8518 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 08:15:37 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix(msvc):=20const=20ref=20when=20iterating?= =?UTF-8?q?=20path=20segments=20=E2=80=94=20libc++=20yields=20temporaries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GCC/libstdc++ and Clang/MSVC-STL accepted 'auto&' here; Clang/libc++ (macOS job) correctly rejects binding a non-const lvalue ref to the temporary. --- src/toolchain/msvc.cppm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index 7e6c05f..30526e2 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -290,7 +290,8 @@ namespace { // "…\Microsoft Visual Studio\2022\BuildTools" → "2022 BuildTools". [[maybe_unused]] std::string product_from_vs_root(const std::filesystem::path& vsRoot) { std::vector parts; - for (auto& seg : vsRoot) parts.push_back(seg.string()); + // path iterators yield temporaries under libc++ — const ref only. + for (const auto& seg : vsRoot) parts.push_back(seg.string()); for (std::size_t i = 0; i + 2 < parts.size(); ++i) { if (parts[i] == "Microsoft Visual Studio") return parts[i + 1] + " " + parts[i + 2];