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
447 changes: 447 additions & 0 deletions .agents/docs/2026-08-16-toolchain-architecture-review.md

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions .github/workflows/ci-windows-msvc-xlings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: ci-windows-msvc-xlings

# mcpp driving the xlings-MANAGED MSVC toolset (`msvc@<toolset>`) — the whole
# chain, end to end: index → payload → unpack → resolve → build → run →
# remove.
#
# WHY THIS IS NOT IN ci-windows-e2e.yml, which also runs MSVC tests:
#
# Different subject. Everything MSVC in the main suite (95_msvc_system,
# 99_msvc_native_build, 177, 180, 182) tests mcpp against the machine's own
# Visual Studio. That is mcpp's code and nothing else's. This job tests
# mcpp against the xlings ECOSYSTEM — a package index, a mirror, a payload
# set, an unpack recipe — most of which lives in another repository and
# moves on its own schedule.
#
# Different failure meaning. When this job goes red it usually means the
# index moved, not that the pull request broke something. Mixed into the
# main suite that reads as "your change broke Windows", and the honest
# signal (100+ fast tests, all about mcpp) gets buried under one slow test
# about somebody else's package. Keeping them apart keeps both readable.
#
# Different cost. ~380 MB of downloads (xim:msvc + xim:windows-sdk) and a
# real toolchain install, against a suite whose other tests are seconds
# each.
#
# The split is enforced by a capability, not by a file list: the tests carry
# `# requires: xlings-msvc`, granted only by MCPP_E2E_XLINGS_MSVC=1 below. So
# the main suite skips them by construction, and a new test joins this job by
# declaring the capability — there is no second list to keep in sync.
#
# Paired workflows: ci-windows.yml, ci-windows-e2e.yml.

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
xlings-msvc:
name: xlings-managed msvc toolset (windows x64, self-host)
runs-on: windows-latest
timeout-minutes: 45
env:
MCPP_HOME: C:\Users\runneradmin\.mcpp
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp

- name: Build mcpp from source (self-host)
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP" build
# Newest mcpp.exe, not an arbitrary one — `target/` is restored from
# cache and keeps a directory per build fingerprint. Same reasoning
# as ci-windows-e2e.yml, where picking wrong ran the previous
# release's binary.
MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \
| sort -rn | head -1 | cut -d" " -f2-)
test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; }
MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF")
"$MCPP_SELF" --version
echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV"

- name: xlings-managed msvc e2e
shell: bash
timeout-minutes: 30
env:
# Grants the `xlings-msvc` capability. Without it these tests skip
# everywhere, which is exactly what the main suite wants.
MCPP_E2E_XLINGS_MSVC: '1'
# Name what this job runs, so the job title and its contents cannot
# drift apart. Widen the glob when a second test joins.
E2E_ONLY: '239_*.sh'
run: |
export MCPP="$MCPP_SELF"
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL
"$MCPP_SELF" self config --mirror GLOBAL
bash tests/e2e/run_all.sh

# A run that matched nothing is a green tick for having done nothing,
# and it looks exactly like a run that passed. E2E_ONLY is a glob typed
# by hand; if it stops matching, say so here rather than in a report
# nobody reads.
- name: Fail if the filter selected no tests
if: always()
shell: bash
run: |
n=$(ls tests/e2e/239_*.sh 2>/dev/null | wc -l)
test "$n" -gt 0 || { echo "FAIL: E2E_ONLY matched no tests"; exit 1; }
echo "selected $n test(s)"
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,55 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [Unreleased]

### 修复

- **Windows 上 `mcpp toolchain remove <msvc toolset>` 报 "Access is denied"。**

`remove_all` 直接上,不清只读位、不重试、报错也不说是哪个文件。
payload 是从 .vsix/.msi 解出来的,归档条目带只读属性;POSIX 只要**目录**
可写就能 unlink 子项,所以这个问题在 Linux/macOS 上根本不出现。
另外 `/Zi` 构建之后 mspdbsrv.exe 还会在 payload 里活几秒。

两个原因表现完全一样(都是 "Access is denied"),所以两个都处理:
先清可写位重试,再给活着的进程一个**有上限**的等待窗口(10 × 300ms)。
报错现在会指出卡在哪个文件 —— 光一句 "Access is denied" 没法处理。

这个诊断当场就派上用场了:它点名的是
`bin\Hostx64\x64\Microsoft.VisualStudio.Telemetry.dll` —— 既不是只读位
也不是 mspdbsrv,而是 cl.exe 拉起的后台遥测进程 `vctip.exe` 占着它。
**占用者不是 mcpp 能删掉的东西**,真正的修复在 payload 那边
(openxlings/xim-pkgindex#637 不再安装 vctip.exe);这边留下的是通用兜底
和那句能读懂的报错。

报错同时会说清楚:**失败的 remove 不是空操作** —— `remove_all` 会先删掉
能删的,所以剩下的是一个有洞的工具链,得重来一次而不是接着用。

修完 vctip 之后占用者换成了 `mspdbcore.dll` —— `/Zi` 构建拉起的
**mspdbsrv.exe**,它构建完还活几十秒,而且就住在正要被删的 payload 里。
等它不现实(等多久都是猜),所以改成**挪走**:Windows 拒绝删除含打开文件的
目录,但允许**重命名**。`remove` 的承诺是"这个工具链不再装着",改名之后
它确实不再装着;剩下的字节在下一条生命周期命令里清扫(那时占用者早退了)。

- **半装的 Windows SDK 被当成装好的,链接到最后才炸。**

`find_windows_sdk()` 认一个根的条件是 `Include\<v>\ucrt\corecrt.h`
存在 —— 只看头文件。而 SDK 是头文件**和**导入库两半。

托管的 `xim:windows-sdk` payload 少了带 `kernel32.lib` 的那个 MSI 时,
这个根照样"找到了",而且因为版本号更高,**排在机器自己那套完整 SDK 前面**。
于是每个 TU 都编过了,一直到最后一步:

LINK : fatal error LNK1104: cannot open file 'kernel32.lib'

日志里没有任何一行提到 SDK。

现在两半都要:`Include\<v>\ucrt\corecrt.h` 和
`Lib\<v>\um\<arch>\kernel32.lib`。半装的根被跳过,搜索落到下一个,
本来就能用的构建就能用了 —— 和 `has_usable_msvc()` 坚持"两半都要"是同一条
理由,只是这次轮到 SDK 自己。

## [2026.8.16.2] — 2026-08-16

### 修复
Expand Down
6 changes: 6 additions & 0 deletions docs/03-toolchains.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,12 @@ pinned toolset in mcpp's store, then `C:\Program Files (x86)\Windows Kits\10`.
A missing SDK fails the build with guidance (`mcpp self doctor` reports SDK
status).

A root only counts as an SDK when it has **both** halves — `Include\<v>\ucrt\
corecrt.h` *and* `Lib\<v>\um\<arch>\kernel32.lib`. A root with headers and no
import libraries is skipped rather than selected, so a partially unpacked
payload cannot outrank the machine's complete SDK and turn into
`LNK1104: cannot open file 'kernel32.lib'` at the very end of a build.

**CRT model.** `/MD` (host-coupled) by default; `/MT` when either

```toml
Expand Down
30 changes: 24 additions & 6 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -1090,11 +1090,22 @@ prepare_build(bool print_fingerprint,
// rest: the vocabulary table already maps x86_64-windows-gnu to its pin
// (winlibs GCC) and to static linkage, so the toolchain answer stays a
// single derivation instead of being spelled out a second time here.
// "Is MSVC usable here" — either origin. Asking `has_usable_msvc()` (which
// probes the machine) would answer "no" on a box that has a pinned
// msvc@<toolset> payload and no Visual Studio, and every decision below
// would then divert a perfectly good toolchain to mingw.
auto msvc_usable_either_origin = [&]() -> bool {
auto c = get_cfg();
if (!c) return mcpp::toolchain::msvc::has_usable_msvc();
return mcpp::toolchain::msvc::msvc_available_here(
(*c)->xlingsHome() / "data" / "xpkgs");
};

bool windowsGnuFirstRun = false;
if constexpr (mcpp::platform::is_windows) {
if (!tcSpec.has_value() && overrides.target_triple.empty()
&& m->buildConfig.target.empty()
&& !mcpp::toolchain::msvc::has_usable_msvc()) {
&& !msvc_usable_either_origin()) {
auto cfgW = get_cfg();
if (!cfgW || (*cfgW)->defaultTarget.empty()) {
overrides.target_triple =
Expand Down Expand Up @@ -1320,12 +1331,19 @@ prepare_build(bool print_fingerprint,
// (cl.exe is four levels deeper) and the ELF post-install fixup
// (there is nothing to patchelf on a PE toolchain).
if (spec->family == mcpp::toolchain::Family::Msvc) {
// Not `payload->root`: that field is the fetcher's guess at where
// the useful tree starts, and it descends into a lone
// subdirectory when the version dir has no bin/ include/ lib/.
// An msvc payload's only entry is `VC/`, so the guess lands one
// level too deep. (store, name, version) is known — use it.
auto verDir = mcpp::xlings::paths::xim_tool(
mcpp::config::make_xlings_env(**cfg), pkg.ximName, pkg.ximVersion);
auto inst = mcpp::toolchain::msvc::installation_at(
payload->root, pkg.ximVersion);
verDir, pkg.ximVersion);
if (!inst) {
return std::unexpected(std::format(
"msvc payload at '{}' has no cl.exe under VC/Tools/MSVC/{}",
payload->root.string(), pkg.ximVersion));
verDir.string(), pkg.ximVersion));
}
explicit_compiler = inst->clPath;
mcpp::ui::info("Resolved", std::format(
Expand Down Expand Up @@ -1379,7 +1397,7 @@ prepare_build(bool print_fingerprint,
// exactly what this machine cannot build. Name the toolchain that
// will actually work there instead.
if (mcpp::platform::is_windows
&& !mcpp::toolchain::msvc::has_usable_msvc()) {
&& !msvc_usable_either_origin()) {
return std::unexpected(std::format(
"no toolchain configured (and no Visual Studio found).\n"
" run one of:\n"
Expand Down Expand Up @@ -1434,7 +1452,7 @@ prepare_build(bool print_fingerprint,
if constexpr (mcpp::platform::is_macos) {
defaultSpec = std::string(pins::kFirstRunMac);
} else if constexpr (mcpp::platform::is_windows) {
// Reaching here means has_usable_msvc() was true — the seed above
// Reaching here means msvc_usable_either_origin() was true — the seed above
// diverts the no-Visual-Studio case onto the windows-gnu target
// before the target block runs, so it never gets this far.
defaultSpec = std::string(pins::kFirstRunWinMsvc);
Expand Down Expand Up @@ -1548,7 +1566,7 @@ prepare_build(bool print_fingerprint,
const bool targetsMsvcAbi =
tc->compiler == mcpp::toolchain::CompilerId::MSVC
|| mcpp::toolchain::is_msvc_target(*tc);
if (targetsMsvcAbi && !mcpp::toolchain::msvc::has_usable_msvc()) {
if (targetsMsvcAbi && !msvc_usable_either_origin()) {
// Native cl.exe is ALWAYS a deliberate choice: mcpp never selects
// msvc@system on its own — it cannot install one — so the only way it
// reaches config.toml is a user typing `mcpp toolchain default msvc`.
Expand Down
11 changes: 9 additions & 2 deletions src/doctor.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,15 @@ export int doctor_report() {
s.family = id->family;
s.version = vEntry.path().filename().string();
s.target = id->target;
auto bin = mcpp::toolchain::toolchain_frontend(
vEntry.path() / "bin", mcpp::toolchain::to_xim_package(s));
// payload_frontend, not toolchain_frontend(root/"bin"):
// cl.exe is four levels down at
// VC/Tools/MSVC/<ver>/bin/Host<h>/<arch>/, so the `bin/`
// lookup finds nothing and `continue` drops every
// installed msvc toolset. That is the defect #436 fixed
// in `toolchain list`; doctor kept its own copy, so the
// two commands disagreed about the same machine.
auto bin = mcpp::toolchain::payload_frontend(
vEntry.path(), mcpp::toolchain::to_xim_package(s), s.family);
if (bin.empty()) continue;
sawAny = true;

Expand Down
Loading
Loading