Skip to content

Commit 0847b05

Browse files
committed
feat(windows): detection-first default + auto-repair an unusable MSVC choice
A bare Windows box has the UCRT runtime DLLs but neither the MSVC STL nor the Windows SDK — both arrive only with Visual Studio's 'Desktop development with C++' workload. mcpp's first-run default shared one pin with macOS (llvm@20.1.7), and on Windows the host triple is MSVC-ABI, so clang picked the MSVC STL and the build failed at compile time with no actionable message. The self-contained alternative (winlibs GCC targeting x86_64-windows-gnu) was already a verified target with Windows-native CI — just not the default. - msvc::has_usable_msvc(): STL *and* SDK, so a half-installed VS cannot pass - split kFirstRunMacWin into per-platform pins; the Windows no-VS case seeds only the target axis and lets the existing vocabulary pin derive the rest - TcOrigin: distinguish a default mcpp chose from one the user wrote down; only the former may be auto-repaired - fold the old 'VC tools but no SDK' check into one MSVC-ABI-usability gate that runs on every build, which is what repairs users already carrying a persisted llvm@20.1.7 — the first-run branch never fires again for them
1 parent 6437531 commit 0847b05

4 files changed

Lines changed: 366 additions & 24 deletions

File tree

‎src/build/prepare.cppm‎

Lines changed: 227 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,65 @@ bool graph_or_targets_import_std(const mcpp::modgraph::Graph& graph,
599599
// `--no-cache` used to be the only switch and it meant "clear the build dir",
600600
// which says nothing about a cache (and its help text claimed all of target/);
601601
// it stays as a deprecated alias for Off.
602+
// Where the resolved toolchain spec came from.
603+
//
604+
// This exists so mcpp can tell its own guesses apart from the user's
605+
// instructions. When a resolved toolchain turns out to be unusable on this
606+
// machine (the motivating case: a Windows default that targets the MSVC ABI
607+
// on a box with no Visual Studio), mcpp may quietly revise a default it
608+
// picked itself — but a spec the user wrote into mcpp.toml must produce an
609+
// error instead. A project that needs the MSVC ABI to link vcpkg-built .lib
610+
// files is worse off with a silent ABI swap than with a failed build.
611+
//
612+
// Deliberately derived from the two config layers that already exist rather
613+
// than persisted: no new field, nothing to keep in sync on disk.
614+
export enum class TcOrigin {
615+
None, // nothing resolved yet
616+
ManifestToolchain, // mcpp.toml [toolchain] — user explicit
617+
TargetSection, // mcpp.toml [target.X].toolchain — user explicit
618+
GlobalDefault, // config.toml [toolchain] default — mcpp's own default
619+
TargetPin, // triple.cppm vocabulary convention
620+
FirstRun, // chosen and persisted by this very invocation
621+
};
622+
623+
export inline bool tc_origin_is_user_explicit(TcOrigin o) {
624+
return o == TcOrigin::ManifestToolchain || o == TcOrigin::TargetSection;
625+
}
626+
627+
// What to tell a user whose build targets the MSVC ABI on a machine that
628+
// cannot serve it. Two shapes, because the two states need different fixes:
629+
//
630+
// • cl.exe was found but the Windows SDK was not — a half-installed VS.
631+
// Point at the missing SDK component; switching toolchains would be an
632+
// over-correction for someone who clearly wants MSVC.
633+
// • nothing usable at all — the bare-Windows case. Lead with the MinGW-w64
634+
// route, which needs no Visual Studio and is already a verified target,
635+
// and keep the "install the C++ workload" option second.
636+
export std::string msvc_unavailable_guidance(const mcpp::toolchain::Toolchain& tc) {
637+
namespace pins = mcpp::toolchain::triple::pins;
638+
const bool haveVcTools = tc.compiler == mcpp::toolchain::CompilerId::MSVC;
639+
if (haveVcTools && mcpp::toolchain::msvc::find_msvc_tools_dir()) {
640+
return std::format(
641+
"msvc {} was detected at {}, but no Windows SDK was found —\n"
642+
" cl.exe cannot compile without the UCRT/SDK headers.\n"
643+
" Install the 'Windows 11 SDK' component via the Visual Studio\n"
644+
" Installer (it is part of the Desktop development with C++\n"
645+
" workload), then retry.",
646+
tc.version, tc.binaryPath.string());
647+
}
648+
return std::format(
649+
"this build targets the MSVC ABI, which needs Visual Studio /\n"
650+
" Build Tools (MSVC STL + Windows SDK) — neither was found.\n"
651+
"\n"
652+
" No Visual Studio? Use the self-contained MinGW-w64 toolchain\n"
653+
" (no Visual Studio required, `import std` works):\n"
654+
" mcpp toolchain default {} --target {}\n"
655+
"\n"
656+
" Have Visual Studio? Install the 'Desktop development with C++'\n"
657+
" workload — it provides the MSVC STL and the Windows SDK.",
658+
pins::kSuggestGccMingw, pins::kFirstRunWinGnuTarget);
659+
}
660+
602661
export enum class CacheMode { Global, Local, Off };
603662

604663
export std::optional<CacheMode> parse_cache_mode(std::string_view v) {
@@ -945,10 +1004,42 @@ prepare_build(bool print_fingerprint,
9451004
}
9461005

9471006
auto tcSpec = m->toolchain.for_platform(kCurrentPlatform);
1007+
// Where the spec came from decides whether mcpp may later revise it.
1008+
// See TcOrigin: mcpp can rewrite a default it chose itself, but must not
1009+
// silently overrule one the user wrote down.
1010+
auto tcOrigin = tcSpec.has_value() ? TcOrigin::ManifestToolchain
1011+
: TcOrigin::None;
9481012
if (!tcSpec.has_value()) {
9491013
auto cfg = get_cfg();
9501014
if (cfg && !(*cfg)->defaultToolchain.empty()) {
951-
tcSpec = (*cfg)->defaultToolchain;
1015+
tcSpec = (*cfg)->defaultToolchain;
1016+
tcOrigin = TcOrigin::GlobalDefault;
1017+
}
1018+
}
1019+
1020+
// ─── Windows first run without Visual Studio ────────────────────────
1021+
// The host triple on Windows is MSVC-ABI, so the historical default
1022+
// (llvm) resolves to clang targeting MSVC — which uses the MSVC STL and
1023+
// the Windows SDK. Neither ships with Windows; both arrive only with
1024+
// Visual Studio's "Desktop development with C++" workload. On a bare box
1025+
// that default installs fine and then fails at compile time with no
1026+
// actionable message.
1027+
//
1028+
// Seed only the TARGET axis and let the block right below derive the
1029+
// rest: the vocabulary table already maps x86_64-windows-gnu to its pin
1030+
// (winlibs GCC) and to static linkage, so the toolchain answer stays a
1031+
// single derivation instead of being spelled out a second time here.
1032+
bool windowsGnuFirstRun = false;
1033+
if constexpr (mcpp::platform::is_windows) {
1034+
if (!tcSpec.has_value() && overrides.target_triple.empty()
1035+
&& m->buildConfig.target.empty()
1036+
&& !mcpp::toolchain::msvc::has_usable_msvc()) {
1037+
auto cfgW = get_cfg();
1038+
if (!cfgW || (*cfgW)->defaultTarget.empty()) {
1039+
overrides.target_triple =
1040+
std::string(mcpp::toolchain::triple::pins::kFirstRunWinGnuTarget);
1041+
windowsGnuFirstRun = true;
1042+
}
9521043
}
9531044
}
9541045

@@ -1012,7 +1103,10 @@ prepare_build(bool print_fingerprint,
10121103
if (parsed) overrides.target_triple = parsed->str();
10131104

10141105
if (hasExplicitSection) {
1015-
if (!it->second.toolchain.empty()) tcSpec = it->second.toolchain;
1106+
if (!it->second.toolchain.empty()) {
1107+
tcSpec = it->second.toolchain;
1108+
tcOrigin = TcOrigin::TargetSection;
1109+
}
10161110
if (!it->second.linkage.empty()) m->buildConfig.linkage = it->second.linkage;
10171111
}
10181112
// Convention from the vocabulary table (triple.cppm): the target's
@@ -1021,8 +1115,13 @@ prepare_build(bool print_fingerprint,
10211115
// mapping, not here) and its default linkage. GCC 16 pin rationale:
10221116
// GCC 15 drops module template instantiations at link (remediation
10231117
// doc A2; packages shipped 2026-07-08/09, GitHub+GitCode).
1024-
if (known && !hasToolchainOverride && !known->pin.empty())
1118+
if (known && !hasToolchainOverride && !known->pin.empty()) {
10251119
tcSpec = std::string(known->pin);
1120+
// A convention, not an instruction: on the Windows-GNU first-run
1121+
// path this is what turns the seeded target into `gcc@16.1.0`.
1122+
if (!tc_origin_is_user_explicit(tcOrigin))
1123+
tcOrigin = TcOrigin::TargetPin;
1124+
}
10261125
if (known && known->defaultStatic && m->buildConfig.linkage.empty())
10271126
m->buildConfig.linkage = "static";
10281127
}
@@ -1164,14 +1263,29 @@ prepare_build(bool print_fingerprint,
11641263
std::string_view release = mcpp::platform::env::offline_mode()
11651264
? "or drop --offline / unset MCPP_OFFLINE to let mcpp auto-install."
11661265
: "or unset MCPP_NO_AUTO_INSTALL to let mcpp auto-install.";
1266+
// Windows without a usable MSVC must not be told to install llvm:
1267+
// that default resolves to clang targeting the MSVC ABI, which is
1268+
// exactly what this machine cannot build. Name the toolchain that
1269+
// will actually work there instead.
1270+
if (mcpp::platform::is_windows
1271+
&& !mcpp::toolchain::msvc::has_usable_msvc()) {
1272+
return std::unexpected(std::format(
1273+
"no toolchain configured (and no Visual Studio found).\n"
1274+
" run one of:\n"
1275+
" mcpp toolchain install {} --target {}\n"
1276+
" mcpp toolchain default {} --target {}\n"
1277+
" {}",
1278+
pins::kSuggestGccMingw, pins::kFirstRunWinGnuTarget,
1279+
pins::kFirstRunWinGnu, pins::kFirstRunWinGnuTarget, release));
1280+
}
11671281
if constexpr (mcpp::platform::is_macos || mcpp::platform::is_windows) {
11681282
return std::unexpected(std::format(
11691283
"no toolchain configured.\n"
11701284
" run one of:\n"
11711285
" mcpp toolchain install {}\n"
11721286
" mcpp toolchain default {}\n"
11731287
" {}",
1174-
pins::kSuggestLlvm, pins::kFirstRunMacWin, release));
1288+
pins::kSuggestLlvm, pins::kFirstRunMac, release));
11751289
} else {
11761290
return std::unexpected(std::format(
11771291
"no toolchain configured.\n"
@@ -1206,8 +1320,13 @@ prepare_build(bool print_fingerprint,
12061320
// toolchain, addable later for native-ABI aarch64 builds.
12071321
namespace pins = mcpp::toolchain::triple::pins;
12081322
std::string defaultSpec;
1209-
if constexpr (mcpp::platform::is_macos || mcpp::platform::is_windows) {
1210-
defaultSpec = std::string(pins::kFirstRunMacWin);
1323+
if constexpr (mcpp::platform::is_macos) {
1324+
defaultSpec = std::string(pins::kFirstRunMac);
1325+
} else if constexpr (mcpp::platform::is_windows) {
1326+
// Reaching here means has_usable_msvc() was true — the seed above
1327+
// diverts the no-Visual-Studio case onto the windows-gnu target
1328+
// before the target block runs, so it never gets this far.
1329+
defaultSpec = std::string(pins::kFirstRunWinMsvc);
12111330
} else if (mcpp::platform::host_arch == std::string_view("x86_64")) {
12121331
defaultSpec = std::string(pins::kFirstRunLinuxX86_64);
12131332
} else {
@@ -1271,24 +1390,113 @@ prepare_build(bool print_fingerprint,
12711390
mcpp::ui::status("Default", std::format("set to {}", defaultSpec));
12721391
} // best-effort: a failed config write only loses the persistence,
12731392
// not the running build.
1274-
tcSpec = defaultSpec;
1393+
tcSpec = defaultSpec;
1394+
tcOrigin = TcOrigin::FirstRun;
1395+
}
1396+
1397+
// Windows first run that got diverted to winlibs GCC: announce it and
1398+
// persist BOTH axes, so the next invocation is silent and
1399+
// `mcpp toolchain list` shows the same pair the build actually used.
1400+
// Persisting only the target would leave the toolchain axis implicit
1401+
// (derived from the vocabulary pin) and the two views would disagree.
1402+
if (windowsGnuFirstRun && tcSpec.has_value()) {
1403+
mcpp::ui::info("First run",
1404+
std::format("no toolchain configured and no Visual Studio found — "
1405+
"installing {} for {} (MinGW-w64, self-contained)",
1406+
*tcSpec, overrides.target_triple));
1407+
if (auto cfgW = get_cfg(); cfgW) {
1408+
if (mcpp::config::write_default_toolchain(**cfgW, *tcSpec))
1409+
(*cfgW)->defaultToolchain = *tcSpec;
1410+
if (mcpp::config::write_default_target(**cfgW, overrides.target_triple))
1411+
(*cfgW)->defaultTarget = overrides.target_triple;
1412+
mcpp::ui::status("Default",
1413+
std::format("set to {} → {}", *tcSpec, overrides.target_triple));
1414+
}
1415+
tcOrigin = TcOrigin::FirstRun;
12751416
}
12761417

12771418
auto tc = mcpp::toolchain::detect(explicit_compiler);
12781419
if (!tc) return std::unexpected(tc.error().message);
12791420

1280-
// Native MSVC builds need the synthesized INCLUDE/LIB env — absent when
1281-
// detection found VC tools but no Windows SDK. Fail here with guidance
1282-
// instead of cl.exe's later "cannot open include file: 'corecrt.h'".
1283-
if (tc->compiler == mcpp::toolchain::CompilerId::MSVC
1284-
&& tc->envOverrides.empty()) {
1285-
return std::unexpected(std::format(
1286-
"msvc {} was detected at {}, but no Windows SDK was found —\n"
1287-
" cl.exe cannot compile without the UCRT/SDK headers.\n"
1288-
" Install the 'Windows 11 SDK' component via the Visual Studio\n"
1289-
" Installer (it is part of the Desktop development with C++\n"
1290-
" workload), then retry.",
1291-
tc->version, tc->binaryPath.string()));
1421+
// ── Targeting the MSVC ABI without a usable MSVC ─────────────────────
1422+
//
1423+
// One judgement, one place. This used to be two separate concerns and
1424+
// only one of them was implemented: `msvc@system` with no Windows SDK
1425+
// was caught here, while clang-targeting-MSVC on a machine with no
1426+
// Visual Studio at all — the default on every bare Windows box — fell
1427+
// straight through to clang's own "'vector' file not found", from which
1428+
// no user could infer that a working alternative was one flag away.
1429+
// Deriving the same judgement in two places is how the second case went
1430+
// unnoticed, so they are now one condition with two outcomes.
1431+
const bool targetsMsvcAbi =
1432+
tc->compiler == mcpp::toolchain::CompilerId::MSVC
1433+
|| mcpp::toolchain::is_msvc_target(*tc);
1434+
if (targetsMsvcAbi && !mcpp::toolchain::msvc::has_usable_msvc()) {
1435+
const bool mayRepair =
1436+
!tc_origin_is_user_explicit(tcOrigin)
1437+
&& !mcpp::platform::env::offline_mode()
1438+
&& !mcpp::platform::env::no_auto_install()
1439+
&& mcpp::platform::is_windows;
1440+
if (!mayRepair) {
1441+
return std::unexpected(msvc_unavailable_guidance(*tc));
1442+
}
1443+
// mcpp chose this default itself and it cannot work on this machine.
1444+
// Revise it — including for users who already have `llvm@20.1.7`
1445+
// persisted by an older mcpp: the first-run branch never fires again
1446+
// for them, so this gate (which runs on EVERY build) is what repairs
1447+
// them without a single manual command.
1448+
namespace pins = mcpp::toolchain::triple::pins;
1449+
mcpp::ui::info("Toolchain",
1450+
std::format("{} targets the MSVC ABI but no Visual Studio "
1451+
"(MSVC STL + Windows SDK) was found — switching to {} → {}",
1452+
tcSpec.value_or("the configured default"),
1453+
pins::kFirstRunWinGnu, pins::kFirstRunWinGnuTarget));
1454+
1455+
overrides.target_triple = std::string(pins::kFirstRunWinGnuTarget);
1456+
// The x86_64-windows-gnu row is defaultStatic; the target block that
1457+
// normally applies that already ran, so mirror just this one field.
1458+
if (m->buildConfig.linkage.empty()) m->buildConfig.linkage = "static";
1459+
1460+
auto gnuSpec = mcpp::toolchain::parse_toolchain_spec(
1461+
std::string(pins::kFirstRunWinGnu));
1462+
if (!gnuSpec) return std::unexpected(gnuSpec.error());
1463+
if (auto t = mcpp::toolchain::triple::parse(overrides.target_triple))
1464+
gnuSpec->target = *t;
1465+
auto gnuPkg = mcpp::toolchain::to_xim_package(*gnuSpec);
1466+
1467+
auto cfgR = get_cfg();
1468+
if (!cfgR) return std::unexpected(cfgR.error());
1469+
mcpp::fetcher::Fetcher fetcherR(**cfgR);
1470+
mcpp::fetcher::InstallProgressHandler progressR;
1471+
auto payloadR = fetcherR.resolve_xpkg_path(gnuPkg.target(),
1472+
/*autoInstall=*/true, &progressR);
1473+
if (!payloadR) {
1474+
return std::unexpected(std::format(
1475+
"switching to the MinGW-w64 toolchain ({}) failed: {}\n"
1476+
" install it manually with:\n"
1477+
" mcpp toolchain install {} --target {}",
1478+
pins::kFirstRunWinGnu, payloadR.error().message,
1479+
pins::kSuggestGccMingw, pins::kFirstRunWinGnuTarget));
1480+
}
1481+
explicit_compiler =
1482+
mcpp::toolchain::toolchain_frontend(payloadR->binDir, gnuPkg);
1483+
if (!std::filesystem::exists(explicit_compiler)) {
1484+
return std::unexpected(std::format(
1485+
"MinGW-w64 payload {} has no known C++ frontend in {}",
1486+
gnuPkg.target(), payloadR->binDir.string()));
1487+
}
1488+
mcpp::toolchain::ensure_post_install_fixup(**cfgR, payloadR->root, gnuPkg);
1489+
1490+
// Persist both axes so the repair happens once, not on every build.
1491+
if (mcpp::config::write_default_toolchain(**cfgR, pins::kFirstRunWinGnu))
1492+
(*cfgR)->defaultToolchain = std::string(pins::kFirstRunWinGnu);
1493+
if (mcpp::config::write_default_target(**cfgR, overrides.target_triple))
1494+
(*cfgR)->defaultTarget = overrides.target_triple;
1495+
1496+
tcSpec = std::string(pins::kFirstRunWinGnu);
1497+
tcOrigin = TcOrigin::FirstRun;
1498+
tc = mcpp::toolchain::detect(explicit_compiler);
1499+
if (!tc) return std::unexpected(tc.error().message);
12921500
}
12931501

12941502
// For musl-gcc the toolchain is fully self-contained

‎src/toolchain/msvc.cppm‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,19 @@ struct WindowsSdk {
101101
// Locate the Windows 10/11 SDK (highest version with ucrt headers).
102102
std::optional<WindowsSdk> find_windows_sdk();
103103

104+
// True only when BOTH halves of a usable MSVC C++ setup are present: the
105+
// STL's std module source AND the Windows SDK.
106+
//
107+
// Either half alone is a half-installed state — Visual Studio with only the
108+
// .NET workload, or VC tools without the SDK — that a cheaper
109+
// `find_vs_install_path()` probe would happily call "MSVC is here", only for
110+
// the build to fail later inside the compiler. Selecting a toolchain on a
111+
// weaker signal than the one the build actually needs is the bug this
112+
// predicate exists to prevent, so it deliberately asks for both.
113+
//
114+
// Always false off Windows: the whole discovery chain is Win32-only.
115+
bool has_usable_msvc();
116+
104117
// Synthesize the environment cl.exe/link.exe need — what vcvars would set,
105118
// derived directly from the located VC tools + SDK (no vcvarsall.bat run):
106119
// INCLUDE = <tools>\include; <sdk>\Include\<v>\{ucrt,um,shared,winrt}
@@ -434,6 +447,17 @@ std::optional<WindowsSdk> find_windows_sdk() {
434447
return std::nullopt;
435448
}
436449

450+
bool has_usable_msvc() {
451+
#if defined(_WIN32)
452+
// Both, deliberately — see the declaration for why either half alone is
453+
// a trap. Order matters only for cost: the STL probe short-circuits the
454+
// SDK directory scan on machines with no Visual Studio at all.
455+
return find_std_module_source().has_value() && find_windows_sdk().has_value();
456+
#else
457+
return false;
458+
#endif
459+
}
460+
437461
std::vector<EnvVar> build_env_for_cl(const std::filesystem::path& clPath,
438462
std::string_view arch,
439463
const WindowsSdk& sdk) {

0 commit comments

Comments
 (0)