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