Skip to content

Commit 103780b

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 e2f0aa5 commit 103780b

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

605664
export std::optional<CacheMode> parse_cache_mode(std::string_view v) {
@@ -1016,10 +1075,42 @@ prepare_build(bool print_fingerprint,
10161075
}
10171076

10181077
auto tcSpec = m->toolchain.for_platform(kCurrentPlatform);
1078+
// Where the spec came from decides whether mcpp may later revise it.
1079+
// See TcOrigin: mcpp can rewrite a default it chose itself, but must not
1080+
// silently overrule one the user wrote down.
1081+
auto tcOrigin = tcSpec.has_value() ? TcOrigin::ManifestToolchain
1082+
: TcOrigin::None;
10191083
if (!tcSpec.has_value()) {
10201084
auto cfg = get_cfg();
10211085
if (cfg && !(*cfg)->defaultToolchain.empty()) {
1022-
tcSpec = (*cfg)->defaultToolchain;
1086+
tcSpec = (*cfg)->defaultToolchain;
1087+
tcOrigin = TcOrigin::GlobalDefault;
1088+
}
1089+
}
1090+
1091+
// ─── Windows first run without Visual Studio ────────────────────────
1092+
// The host triple on Windows is MSVC-ABI, so the historical default
1093+
// (llvm) resolves to clang targeting MSVC — which uses the MSVC STL and
1094+
// the Windows SDK. Neither ships with Windows; both arrive only with
1095+
// Visual Studio's "Desktop development with C++" workload. On a bare box
1096+
// that default installs fine and then fails at compile time with no
1097+
// actionable message.
1098+
//
1099+
// Seed only the TARGET axis and let the block right below derive the
1100+
// rest: the vocabulary table already maps x86_64-windows-gnu to its pin
1101+
// (winlibs GCC) and to static linkage, so the toolchain answer stays a
1102+
// single derivation instead of being spelled out a second time here.
1103+
bool windowsGnuFirstRun = false;
1104+
if constexpr (mcpp::platform::is_windows) {
1105+
if (!tcSpec.has_value() && overrides.target_triple.empty()
1106+
&& m->buildConfig.target.empty()
1107+
&& !mcpp::toolchain::msvc::has_usable_msvc()) {
1108+
auto cfgW = get_cfg();
1109+
if (!cfgW || (*cfgW)->defaultTarget.empty()) {
1110+
overrides.target_triple =
1111+
std::string(mcpp::toolchain::triple::pins::kFirstRunWinGnuTarget);
1112+
windowsGnuFirstRun = true;
1113+
}
10231114
}
10241115
}
10251116

@@ -1083,7 +1174,10 @@ prepare_build(bool print_fingerprint,
10831174
if (parsed) overrides.target_triple = parsed->str();
10841175

10851176
if (hasExplicitSection) {
1086-
if (!it->second.toolchain.empty()) tcSpec = it->second.toolchain;
1177+
if (!it->second.toolchain.empty()) {
1178+
tcSpec = it->second.toolchain;
1179+
tcOrigin = TcOrigin::TargetSection;
1180+
}
10871181
if (!it->second.linkage.empty()) m->buildConfig.linkage = it->second.linkage;
10881182
}
10891183
// Convention from the vocabulary table (triple.cppm): the target's
@@ -1092,8 +1186,13 @@ prepare_build(bool print_fingerprint,
10921186
// mapping, not here) and its default linkage. GCC 16 pin rationale:
10931187
// GCC 15 drops module template instantiations at link (remediation
10941188
// doc A2; packages shipped 2026-07-08/09, GitHub+GitCode).
1095-
if (known && !hasToolchainOverride && !known->pin.empty())
1189+
if (known && !hasToolchainOverride && !known->pin.empty()) {
10961190
tcSpec = std::string(known->pin);
1191+
// A convention, not an instruction: on the Windows-GNU first-run
1192+
// path this is what turns the seeded target into `gcc@16.1.0`.
1193+
if (!tc_origin_is_user_explicit(tcOrigin))
1194+
tcOrigin = TcOrigin::TargetPin;
1195+
}
10971196
if (known && known->defaultStatic && m->buildConfig.linkage.empty())
10981197
m->buildConfig.linkage = "static";
10991198
}
@@ -1235,14 +1334,29 @@ prepare_build(bool print_fingerprint,
12351334
std::string_view release = mcpp::platform::env::offline_mode()
12361335
? "or drop --offline / unset MCPP_OFFLINE to let mcpp auto-install."
12371336
: "or unset MCPP_NO_AUTO_INSTALL to let mcpp auto-install.";
1337+
// Windows without a usable MSVC must not be told to install llvm:
1338+
// that default resolves to clang targeting the MSVC ABI, which is
1339+
// exactly what this machine cannot build. Name the toolchain that
1340+
// will actually work there instead.
1341+
if (mcpp::platform::is_windows
1342+
&& !mcpp::toolchain::msvc::has_usable_msvc()) {
1343+
return std::unexpected(std::format(
1344+
"no toolchain configured (and no Visual Studio found).\n"
1345+
" run one of:\n"
1346+
" mcpp toolchain install {} --target {}\n"
1347+
" mcpp toolchain default {} --target {}\n"
1348+
" {}",
1349+
pins::kSuggestGccMingw, pins::kFirstRunWinGnuTarget,
1350+
pins::kFirstRunWinGnu, pins::kFirstRunWinGnuTarget, release));
1351+
}
12381352
if constexpr (mcpp::platform::is_macos || mcpp::platform::is_windows) {
12391353
return std::unexpected(std::format(
12401354
"no toolchain configured.\n"
12411355
" run one of:\n"
12421356
" mcpp toolchain install {}\n"
12431357
" mcpp toolchain default {}\n"
12441358
" {}",
1245-
pins::kSuggestLlvm, pins::kFirstRunMacWin, release));
1359+
pins::kSuggestLlvm, pins::kFirstRunMac, release));
12461360
} else {
12471361
return std::unexpected(std::format(
12481362
"no toolchain configured.\n"
@@ -1277,8 +1391,13 @@ prepare_build(bool print_fingerprint,
12771391
// toolchain, addable later for native-ABI aarch64 builds.
12781392
namespace pins = mcpp::toolchain::triple::pins;
12791393
std::string defaultSpec;
1280-
if constexpr (mcpp::platform::is_macos || mcpp::platform::is_windows) {
1281-
defaultSpec = std::string(pins::kFirstRunMacWin);
1394+
if constexpr (mcpp::platform::is_macos) {
1395+
defaultSpec = std::string(pins::kFirstRunMac);
1396+
} else if constexpr (mcpp::platform::is_windows) {
1397+
// Reaching here means has_usable_msvc() was true — the seed above
1398+
// diverts the no-Visual-Studio case onto the windows-gnu target
1399+
// before the target block runs, so it never gets this far.
1400+
defaultSpec = std::string(pins::kFirstRunWinMsvc);
12821401
} else if (mcpp::platform::host_arch == std::string_view("x86_64")) {
12831402
defaultSpec = std::string(pins::kFirstRunLinuxX86_64);
12841403
} else {
@@ -1342,24 +1461,113 @@ prepare_build(bool print_fingerprint,
13421461
mcpp::ui::status("Default", std::format("set to {}", defaultSpec));
13431462
} // best-effort: a failed config write only loses the persistence,
13441463
// not the running build.
1345-
tcSpec = defaultSpec;
1464+
tcSpec = defaultSpec;
1465+
tcOrigin = TcOrigin::FirstRun;
1466+
}
1467+
1468+
// Windows first run that got diverted to winlibs GCC: announce it and
1469+
// persist BOTH axes, so the next invocation is silent and
1470+
// `mcpp toolchain list` shows the same pair the build actually used.
1471+
// Persisting only the target would leave the toolchain axis implicit
1472+
// (derived from the vocabulary pin) and the two views would disagree.
1473+
if (windowsGnuFirstRun && tcSpec.has_value()) {
1474+
mcpp::ui::info("First run",
1475+
std::format("no toolchain configured and no Visual Studio found — "
1476+
"installing {} for {} (MinGW-w64, self-contained)",
1477+
*tcSpec, overrides.target_triple));
1478+
if (auto cfgW = get_cfg(); cfgW) {
1479+
if (mcpp::config::write_default_toolchain(**cfgW, *tcSpec))
1480+
(*cfgW)->defaultToolchain = *tcSpec;
1481+
if (mcpp::config::write_default_target(**cfgW, overrides.target_triple))
1482+
(*cfgW)->defaultTarget = overrides.target_triple;
1483+
mcpp::ui::status("Default",
1484+
std::format("set to {} → {}", *tcSpec, overrides.target_triple));
1485+
}
1486+
tcOrigin = TcOrigin::FirstRun;
13461487
}
13471488

13481489
auto tc = mcpp::toolchain::detect(explicit_compiler);
13491490
if (!tc) return std::unexpected(tc.error().message);
13501491

1351-
// Native MSVC builds need the synthesized INCLUDE/LIB env — absent when
1352-
// detection found VC tools but no Windows SDK. Fail here with guidance
1353-
// instead of cl.exe's later "cannot open include file: 'corecrt.h'".
1354-
if (tc->compiler == mcpp::toolchain::CompilerId::MSVC
1355-
&& tc->envOverrides.empty()) {
1356-
return std::unexpected(std::format(
1357-
"msvc {} was detected at {}, but no Windows SDK was found —\n"
1358-
" cl.exe cannot compile without the UCRT/SDK headers.\n"
1359-
" Install the 'Windows 11 SDK' component via the Visual Studio\n"
1360-
" Installer (it is part of the Desktop development with C++\n"
1361-
" workload), then retry.",
1362-
tc->version, tc->binaryPath.string()));
1492+
// ── Targeting the MSVC ABI without a usable MSVC ─────────────────────
1493+
//
1494+
// One judgement, one place. This used to be two separate concerns and
1495+
// only one of them was implemented: `msvc@system` with no Windows SDK
1496+
// was caught here, while clang-targeting-MSVC on a machine with no
1497+
// Visual Studio at all — the default on every bare Windows box — fell
1498+
// straight through to clang's own "'vector' file not found", from which
1499+
// no user could infer that a working alternative was one flag away.
1500+
// Deriving the same judgement in two places is how the second case went
1501+
// unnoticed, so they are now one condition with two outcomes.
1502+
const bool targetsMsvcAbi =
1503+
tc->compiler == mcpp::toolchain::CompilerId::MSVC
1504+
|| mcpp::toolchain::is_msvc_target(*tc);
1505+
if (targetsMsvcAbi && !mcpp::toolchain::msvc::has_usable_msvc()) {
1506+
const bool mayRepair =
1507+
!tc_origin_is_user_explicit(tcOrigin)
1508+
&& !mcpp::platform::env::offline_mode()
1509+
&& !mcpp::platform::env::no_auto_install()
1510+
&& mcpp::platform::is_windows;
1511+
if (!mayRepair) {
1512+
return std::unexpected(msvc_unavailable_guidance(*tc));
1513+
}
1514+
// mcpp chose this default itself and it cannot work on this machine.
1515+
// Revise it — including for users who already have `llvm@20.1.7`
1516+
// persisted by an older mcpp: the first-run branch never fires again
1517+
// for them, so this gate (which runs on EVERY build) is what repairs
1518+
// them without a single manual command.
1519+
namespace pins = mcpp::toolchain::triple::pins;
1520+
mcpp::ui::info("Toolchain",
1521+
std::format("{} targets the MSVC ABI but no Visual Studio "
1522+
"(MSVC STL + Windows SDK) was found — switching to {} → {}",
1523+
tcSpec.value_or("the configured default"),
1524+
pins::kFirstRunWinGnu, pins::kFirstRunWinGnuTarget));
1525+
1526+
overrides.target_triple = std::string(pins::kFirstRunWinGnuTarget);
1527+
// The x86_64-windows-gnu row is defaultStatic; the target block that
1528+
// normally applies that already ran, so mirror just this one field.
1529+
if (m->buildConfig.linkage.empty()) m->buildConfig.linkage = "static";
1530+
1531+
auto gnuSpec = mcpp::toolchain::parse_toolchain_spec(
1532+
std::string(pins::kFirstRunWinGnu));
1533+
if (!gnuSpec) return std::unexpected(gnuSpec.error());
1534+
if (auto t = mcpp::toolchain::triple::parse(overrides.target_triple))
1535+
gnuSpec->target = *t;
1536+
auto gnuPkg = mcpp::toolchain::to_xim_package(*gnuSpec);
1537+
1538+
auto cfgR = get_cfg();
1539+
if (!cfgR) return std::unexpected(cfgR.error());
1540+
mcpp::fetcher::Fetcher fetcherR(**cfgR);
1541+
mcpp::fetcher::InstallProgressHandler progressR;
1542+
auto payloadR = fetcherR.resolve_xpkg_path(gnuPkg.target(),
1543+
/*autoInstall=*/true, &progressR);
1544+
if (!payloadR) {
1545+
return std::unexpected(std::format(
1546+
"switching to the MinGW-w64 toolchain ({}) failed: {}\n"
1547+
" install it manually with:\n"
1548+
" mcpp toolchain install {} --target {}",
1549+
pins::kFirstRunWinGnu, payloadR.error().message,
1550+
pins::kSuggestGccMingw, pins::kFirstRunWinGnuTarget));
1551+
}
1552+
explicit_compiler =
1553+
mcpp::toolchain::toolchain_frontend(payloadR->binDir, gnuPkg);
1554+
if (!std::filesystem::exists(explicit_compiler)) {
1555+
return std::unexpected(std::format(
1556+
"MinGW-w64 payload {} has no known C++ frontend in {}",
1557+
gnuPkg.target(), payloadR->binDir.string()));
1558+
}
1559+
mcpp::toolchain::ensure_post_install_fixup(**cfgR, payloadR->root, gnuPkg);
1560+
1561+
// Persist both axes so the repair happens once, not on every build.
1562+
if (mcpp::config::write_default_toolchain(**cfgR, pins::kFirstRunWinGnu))
1563+
(*cfgR)->defaultToolchain = std::string(pins::kFirstRunWinGnu);
1564+
if (mcpp::config::write_default_target(**cfgR, overrides.target_triple))
1565+
(*cfgR)->defaultTarget = overrides.target_triple;
1566+
1567+
tcSpec = std::string(pins::kFirstRunWinGnu);
1568+
tcOrigin = TcOrigin::FirstRun;
1569+
tc = mcpp::toolchain::detect(explicit_compiler);
1570+
if (!tc) return std::unexpected(tc.error().message);
13631571
}
13641572

13651573
// 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)