Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
391 changes: 391 additions & 0 deletions .agents/docs/2026-08-01-issue331-windows-msvc-triage.md

Large diffs are not rendered by default.

648 changes: 648 additions & 0 deletions .agents/docs/2026-08-02-windows-usability-design.md

Large diffs are not rendered by default.

1,033 changes: 1,033 additions & 0 deletions .agents/docs/2026-08-02-windows-usability-implementation-plan.md

Large diffs are not rendered by default.

107 changes: 104 additions & 3 deletions .github/workflows/ci-fresh-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -374,14 +374,26 @@ jobs:
mcpp run

# ──────────────────────────────────────────────────────────────────
# Windows: llvm@20.1.7 + MSVC STL
# Windows WITH Visual Studio: llvm@20.1.7 + MSVC STL
#
# Two images, because the OS version is a real variable for a tool that
# touches the UCRT, the Windows SDK and long paths. GitHub publishes no
# Windows 10/11 CLIENT image, so these Server builds are the closest
# available stand-ins: windows-2022 is the Win10 21H2 kernel generation,
# windows-2025 the Win11 24H2 one. What they cannot cover is genuinely
# client-only behaviour — UAC prompts, Defender real-time scanning, the
# long-path policy default — which needs a self-hosted runner.
# ──────────────────────────────────────────────────────────────────
windows-fresh:
needs: [wait-index]
name: Windows fresh install
name: Windows fresh install (${{ matrix.image }})
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: windows-latest
runs-on: ${{ matrix.image }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
image: [windows-2022, windows-2025]
env:
# The one derived value (see the header comment): every install job names
# the SAME version the index guard waited for, so the two cannot disagree.
Expand Down Expand Up @@ -437,3 +449,92 @@ jobs:
run: |
mcpp clean
mcpp run

# ──────────────────────────────────────────────────────────────────
# Windows WITHOUT Visual Studio — the shape of an ordinary user's machine
#
# A stock Windows install ships the UCRT runtime DLLs and nothing else: the
# MSVC STL and the Windows SDK arrive only with Visual Studio's "Desktop
# development with C++" workload. mcpp's Windows default targeted the MSVC
# ABI, so `mcpp new && mcpp build` failed on every such box — and no CI job
# could see it, because every GitHub Windows image ships Visual Studio.
#
# There is no VS-free runner, so the image is masked instead. The risk with
# masking is a false green: miss one of msvc.cppm's three discovery
# strategies (vswhere, environment, well-known paths) and mcpp still finds
# MSVC, takes the ordinary path, and the job passes while proving nothing.
# e2e 182 opens by asserting `mcpp toolchain default msvc` FAILS, which
# turns exactly that into a hard failure.
# ──────────────────────────────────────────────────────────────────
windows-nomsvc-fresh:
needs: [wait-index]
name: Windows fresh install (no Visual Studio)
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: windows-2025
timeout-minutes: 30
env:
MCPP_PIN: ${{ needs.wait-index.outputs.version }}
steps:
- uses: actions/checkout@v4

- name: Mask Visual Studio
shell: pwsh
run: |
$ErrorActionPreference = 'Continue'

# All three of msvc.cppm's discovery strategies converge on
# <vsRoot>\VC\Tools\MSVC, so mask the VC directory rather than the
# Visual Studio root: the root is held open on the runner and
# renaming it is denied, while VC one level down renames fine.
# The runner is disposable, so this is both safe and closer to
# "absent" than any env-only trick would be.
$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" }

Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC" `
-Directory -ErrorAction SilentlyContinue | ForEach-Object {
Rename-Item $_.FullName "$($_.Name).masked" -ErrorAction SilentlyContinue
}

foreach ($v in @('VSINSTALLDIR','VCINSTALLDIR','VCToolsInstallDir',
'VS170COMNTOOLS','VS160COMNTOOLS','VS150COMNTOOLS')) {
"$v=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
}

# Check the mask's own postcondition here, where the cause is
# obvious, rather than letting it surface later as a confusing pass.
$left = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" `
-Directory -ErrorAction SilentlyContinue
if ($left) {
Write-Host "FAIL: VC tools still present after masking:"
$left | ForEach-Object { Write-Host " $($_.FullName)" }
exit 1
}
Write-Host "Visual Studio masked: no VC\Tools\MSVC remains."

- name: Install xlings
shell: pwsh
env:
XLINGS_NON_INTERACTIVE: '1'
run: |
irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex

$xlingsbin = "$env:USERPROFILE\.xlings\subos\current\bin"
$env:PATH = "$xlingsbin;$env:PATH"
$xlingsbin | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8

- name: Install mcpp and config mirror
shell: pwsh
run: |
xlings update
xlings install "mcpp@$env:MCPP_PIN" -y -g --verbose
mcpp --version
mcpp self config --mirror GLOBAL

# The self-check, the fallback, persistence, a self-contained exe, and
# the refusal to overrule an explicit [toolchain] — all in e2e 182, so
# the assertions live with the tests rather than in workflow YAML.
- name: "No Visual Studio: fallback to winlibs GCC (e2e 182)"
shell: bash
run: |
MCPP="$(command -v mcpp)" bash tests/e2e/182_windows_no_msvc_fallback.sh
109 changes: 109 additions & 0 deletions .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,109 @@ jobs:
# Windows-specific behavioural regressions. Kept on one runner: each leg is
# seconds-to-2-minutes, so per-leg runners would cost more setup than they
# save.
# A Windows machine WITHOUT Visual Studio — the shape of an ordinary user's
# box, and the one shape no GitHub image provides. Every runner ships VS, so
# a bare-Windows regression was structurally invisible here; the fresh-install
# workflow now covers it too, but that one only runs post-release, which is
# far too late to learn that `mcpp new && mcpp build` no longer works on a
# stock machine.
#
# Order matters: mcpp is built while Visual Studio is still present (the
# self-host build uses llvm, which targets the MSVC ABI and needs it), and
# only then is VS masked. e2e 182 opens by asserting that MSVC detection
# FAILS, so an incomplete mask fails the job instead of quietly testing the
# ordinary path.
no-msvc-fallback:
name: "bare Windows: no Visual Studio (windows x64)"
# Takes the binary build-test already produced instead of building here.
# Building in this job means compiling with clang, which reads the MSVC
# STL — the open handles that leaves make the VS directories unrenamable,
# so the masking below silently did nothing.
needs: build-test
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Fetch the PR's mcpp.exe
uses: actions/download-artifact@v4
with:
name: mcpp-windows-x86_64
path: dist

- name: Unpack it
shell: bash
run: |
ZIP=$(ls dist/*.zip | head -1)
test -n "$ZIP" || { echo "FAIL: no zip artifact"; exit 1; }
unzip -q "$ZIP" -d unpacked
MCPP_SELF=$(find unpacked -name "mcpp.exe" | head -1)
test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe in $ZIP"; exit 1; }
MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF")
"$MCPP_SELF" --version
echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV"

# Masked before anything else touches Visual Studio, so no process of
# ours is holding a handle into it.
- name: Mask Visual Studio
shell: pwsh
run: |
$ErrorActionPreference = 'Continue'

# All three of msvc.cppm's discovery strategies converge on
# <vsRoot>\VC\Tools\MSVC — vswhere returns an installationPath that
# find_latest_msvc_tools then resolves through it, the env strategy
# checks it explicitly, and the well-known-path scan tests for it.
# So mask the VC directory rather than the Visual Studio root: the
# root is held open on the runner and renaming it is denied, while
# VC one level down renames fine. The runner is disposable, so this
# is both safe and closer to "absent" than any env-only trick.
$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" }

# -Path with a trailing wildcard segment lists the CONTENTS of the
# matches, not the matches themselves, so `…\*\*\VC` would hand back
# VC's children. Resolve-Path returns the directories themselves.
# Errors are reported, not swallowed: a silent failure here is how
# the first attempt "masked" nothing and still looked fine.
Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC" `
-ErrorAction SilentlyContinue | ForEach-Object {
$p = $_.Path
Write-Host "masking $p"
try { Rename-Item -LiteralPath $p -NewName "VC.masked" -ErrorAction Stop }
catch { Write-Host " rename failed: $($_.Exception.Message)" }
}

foreach ($v in @('VSINSTALLDIR','VCINSTALLDIR','VCToolsInstallDir',
'VS170COMNTOOLS','VS160COMNTOOLS','VS150COMNTOOLS')) {
"$v=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
}

# Check the mask's own postcondition here, where the cause is
# obvious, instead of letting it surface three steps later as a
# confusing pass.
$left = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" `
-Directory -ErrorAction SilentlyContinue
if ($left) {
Write-Host "FAIL: VC tools still present after masking:"
$left | ForEach-Object { Write-Host " $($_.FullName)" }
exit 1
}
Write-Host "Visual Studio masked: no VC\Tools\MSVC remains."

# After masking, so nothing this action does can be holding Visual
# Studio open. It installs xlings (which mcpp resolves the winlibs
# toolchain through) and a released mcpp; neither needs a C++ compiler,
# so a masked VS is irrelevant to it.
- uses: ./.github/actions/bootstrap-mcpp

- name: "No Visual Studio: fallback to winlibs GCC (e2e 182)"
shell: bash
env:
MCPP_VENDORED_XLINGS: ${{ env.XLINGS_BIN }}
run: |
MCPP="$MCPP_SELF" bash tests/e2e/182_windows_no_msvc_fallback.sh

toolchains:
name: "toolchains + regressions (windows x64)"
runs-on: windows-latest
Expand Down Expand Up @@ -292,6 +395,12 @@ jobs:
cd "$GITHUB_WORKSPACE"
MCPP="$MCPP_SELF" bash tests/e2e/99_msvc_native_build.sh

# build.mcpp under cl.exe. `MSVC x build.mcpp` was an empty cell in
# this matrix and the feature was correspondingly at zero — the ten
# build.mcpp e2e all run under clang, whose payload path has no
# spaces in it either. Both gaps closed here.
MCPP="$MCPP_SELF" bash tests/e2e/180_msvc_build_mcpp.sh

# restore the LLVM default for the remaining steps
"$MCPP_SELF" toolchain default llvm@20.1.7

Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,8 @@ the right toolchain payload is resolved and installed automatically.
| `x86_64-linux-gnu` | gcc *(Linux default)* or llvm | ✅ |
| `x86_64-linux-musl` | gcc 16, fully static | ✅ |
| `aarch64-linux-musl` | gcc 16, fully static — cross from x86_64 (qemu-verified) or native | ✅ |
| `x86_64-windows-gnu` | gcc 16 MinGW-w64 — native on Windows, cross from Linux (wine-verified) | ✅ |
| `x86_64-windows-msvc` | `msvc@system` (detected VS/BuildTools) or llvm ¹ *(Windows default)* | ✅ |
| `x86_64-windows-gnu` | gcc 16 MinGW-w64 — native on Windows, cross from Linux (wine-verified) *(Windows default without Visual Studio)* | ✅ |
| `x86_64-windows-msvc` | `msvc@system` (detected VS/BuildTools) or llvm ¹ *(Windows default with Visual Studio)* | ✅ |
| `aarch64-macos` | llvm *(macOS default)* | ✅ |
| `riscv64-linux-musl` | — | 🔄 |
| `aarch64-linux-gnu` | — | 🔄 |
Expand All @@ -308,10 +308,14 @@ the right toolchain payload is resolved and installed automatically.
> `musl-gcc@…` — stay permanently accepted as aliases and normalize to the
> canonical forms above.
>
> ¹ On Windows, llvm requires an existing **MSVC BuildTools or Visual Studio**
> (UCRT, Windows SDK, MSVC STL). The MinGW route (`--target x86_64-windows-gnu`,
> or `mcpp toolchain default gcc@16 --target x86_64-windows-gnu`) needs no
> Visual Studio at all.
> ¹ On Windows, llvm targets the MSVC ABI and therefore requires an existing
> **MSVC BuildTools or Visual Studio** (UCRT, Windows SDK, MSVC STL). You do
> not have to arrange this: on first run mcpp checks for a usable MSVC and,
> finding none, defaults to `x86_64-windows-gnu` (winlibs MinGW-w64) — fully
> self-contained, no Visual Studio, `import std` included. Nothing to install
> or configure; `mcpp new && mcpp build` just works on a stock Windows box.
> An explicit `[toolchain]` in `mcpp.toml` is always respected as written —
> mcpp revises its own default, never yours.

## Documentation

Expand Down
13 changes: 8 additions & 5 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,8 @@ mcpp 的身份模型是两条正交轴:**工具链** = `family@version`(family
| `x86_64-linux-gnu` | gcc(*Linux 默认*)或 llvm | ✅ |
| `x86_64-linux-musl` | gcc 16,全静态 | ✅ |
| `aarch64-linux-musl` | gcc 16,全静态——x86_64 交叉(qemu 实测)或原生 | ✅ |
| `x86_64-windows-gnu` | gcc 16 MinGW-w64——Windows 原生,Linux 交叉(wine 实测) | ✅ |
| `x86_64-windows-msvc` | `msvc@system`(探测 VS/BuildTools)或 llvm ¹(*Windows 默认*) | ✅ |
| `x86_64-windows-gnu` | gcc 16 MinGW-w64——Windows 原生,Linux 交叉(wine 实测)(*无 Visual Studio 时的 Windows 默认*) | ✅ |
| `x86_64-windows-msvc` | `msvc@system`(探测 VS/BuildTools)或 llvm ¹(*有 Visual Studio 时的 Windows 默认*) | ✅ |
| `aarch64-macos` | llvm(*macOS 默认*) | ✅ |
| `riscv64-linux-musl` | — | 🔄 |
| `aarch64-linux-gnu` | — | 🔄 |
Expand All @@ -304,9 +304,12 @@ mcpp 的身份模型是两条正交轴:**工具链** = `family@version`(family
> 旧拼写——`x86_64-w64-mingw32`、`gcc@16.1.0-musl`、`mingw-cross@…`、`musl-gcc@…`——
> 作为别名**永久接受**,归一到上表的 canonical 形式。
>
> ¹ Windows 上 llvm 依赖已安装的 **MSVC BuildTools 或 Visual Studio**(UCRT、Windows
> SDK、MSVC STL)。MinGW 路线(`--target x86_64-windows-gnu`,或
> `mcpp toolchain default gcc@16 --target x86_64-windows-gnu`)完全不需要 Visual Studio。
> ¹ Windows 上 llvm 打的是 MSVC ABI,因此依赖已安装的 **MSVC BuildTools 或
> Visual Studio**(UCRT、Windows SDK、MSVC STL)。这件事你不需要自己安排:mcpp 首跑会
> 探测是否有可用的 MSVC,探不到就默认走 `x86_64-windows-gnu`(winlibs MinGW-w64)——
> 完全自包含、不需要 Visual Studio、`import std` 可用。无需安装或配置,裸 Windows 上
> `mcpp new && mcpp build` 直接可用。而 `mcpp.toml` 里显式写的 `[toolchain]` 永远按你
> 写的执行——mcpp 只修正自己选的默认值,不改你的。

## 文档

Expand Down
28 changes: 26 additions & 2 deletions docs/03-toolchains.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ and `planned` targets that are registered but not yet shipped.

## Windows PE via MinGW-w64 (`x86_64-windows-gnu`, no Visual Studio required)

**This is the Windows default when no Visual Studio is present.** Windows ships
the UCRT runtime DLLs but not the MSVC STL or the Windows SDK — those come with
Visual Studio's "Desktop development with C++" workload. Since llvm on Windows
targets the MSVC ABI and needs both, mcpp checks for a usable MSVC (STL **and**
SDK — half of one is the trap) on first run and falls back here when it finds
none, persisting the choice so later builds are silent. Nothing to install or
configure.

The same check also repairs an existing setup: if a `[toolchain] default` mcpp
chose earlier can no longer work on this machine, it is revised in place, with
a line saying so. An explicit `[toolchain]` in `mcpp.toml` (or a
`[target.X].toolchain`) is never overruled — a project that needs the MSVC ABI
to link vcpkg-built `.lib` files gets an error naming the alternative, not a
silent ABI swap.

"MinGW" in mcpp is a **target**, not a toolchain name: `x86_64-windows-gnu`
— GCC producing Windows PE with the GNU CRT. The same identity works from
both hosts; which self-contained payload serves it is resolved automatically
Expand All @@ -126,7 +141,14 @@ mcpp toolchain default gcc@16 --target x86_64-windows-gnu
It uses the regular GCC module pipeline (`gcm.cache`, `import std` via
libstdc++'s `bits/std.cc`). The target's default linkage is **static** —
the produced `.exe` is fully self-contained (no `libstdc++-6.dll` to ship,
runs directly under wine); `[build] linkage = "dynamic"` opts out.
runs directly under wine). To opt out, set it on the target section —
`linkage` is exact-triple only (§2.7 of [mcpp.toml](05-mcpp-toml.md)), and a
`[build] linkage` key does not exist and is silently ignored:

```toml
[target.x86_64-windows-gnu]
linkage = "dynamic"
```

In a manifest:

Expand Down Expand Up @@ -180,7 +202,9 @@ INCLUDE/LIB environment from the detected VC tools + Windows SDK (no
`vcvarsall` involved), stages `std.ixx`/`std.compat.ixx` as `.ifc` BMIs,
compiles `.cppm` module units via `/interface /TP /ifcOutput`, scans with
`/scanDependencies`, and links with `link.exe`/`lib.exe` through response
files. `[build] linkage = "static"` selects the `/MT` CRT. A missing Windows
files. `[target.x86_64-windows-msvc] linkage = "static"` (or `mcpp build
--static`) selects the `/MT` CRT — not `[build] linkage`, which is not a key.
A missing Windows
SDK fails the build with installation guidance (`mcpp self doctor` reports
SDK status).

Expand Down
Loading
Loading