@@ -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+
603662export enum class CacheMode { Global, Local, Off };
604663
605664export 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
0 commit comments