@@ -1721,9 +1721,18 @@ prepare_build(bool print_fingerprint,
17211721 // target the host (an NDK clang) and cannot work for one that cannot:
17221722 // `em++` produces WebAssembly under every invocation, and every project
17231723 // with a build program failed under `--target wasm32-emscripten` inside
1724- // `emcc.py` (#622, measured by the dist-web member's first build). Empty
1725- // when the row replaced nothing, in which case the row's pin remains the
1726- // only spec there is and the previous behaviour is kept.
1724+ // `emcc.py` (#622, measured by the dist-web member's first build).
1725+ //
1726+ // Empty when the row replaced nothing — no [toolchain], no global
1727+ // default, no [target.<row>] entry existed before the row's pin applied.
1728+ // THIS IS NOT "the row's pin remains the only spec there is": on a
1729+ // fresh $HOME whose first-ever invocation names a hosted `--target`
1730+ // (nothing to be "before"), that reading resolved the SAME payload the
1731+ // row just picked — `em++` again — as the host compiler, which is the
1732+ // exact defect this field exists to close, just with no prior value to
1733+ // restore. `host_tc_for_build_program` resolves the platform's own
1734+ // native default in that case instead (`native_first_run_spec()`), the
1735+ // same one a plain `mcpp build` would have installed.
17271736 std::optional<std::string> hostSpecBeforeRowPin;
17281737 // THE PACKAGE WHOSE `requires` CHOSE THE COMPILER, AND WHAT IT ASKED FOR.
17291738 //
@@ -2374,6 +2383,31 @@ prepare_build(bool print_fingerprint,
23742383 (*c)->xlingsHome() / "data" / "xpkgs");
23752384 };
23762385
2386+ // THE PLATFORM'S CANONICAL NATIVE DEFAULT — a spec string only; no
2387+ // install, no persistence. Two places need "what would a native
2388+ // `mcpp build` pick here, with no --target": the first-run installer
2389+ // further below (which goes on to install and persist it), and
2390+ // `host_tc_for_build_program`'s cross branch (which needs a genuine HOST
2391+ // compiler when nothing was ever recorded as one — see its own comment
2392+ // for why #622 happened). One derivation, called from both, so they
2393+ // cannot drift the way a hand-copied second copy would.
2394+ auto native_first_run_spec = [&]() -> std::string {
2395+ namespace pins = mcpp::toolchain::triple::pins;
2396+ if constexpr (mcpp::platform::is_macos) {
2397+ return std::string(pins::kFirstRunMac);
2398+ } else if constexpr (mcpp::platform::is_windows) {
2399+ // A machine with no usable MSVC gets the GNU pin, not an
2400+ // MSVC-ABI clang it cannot use — mirrors the windows-gnu seed
2401+ // below, which this function's other caller runs after.
2402+ return std::string(msvc_usable_either_origin()
2403+ ? pins::kFirstRunWinMsvc : pins::kFirstRunWinGnu);
2404+ } else if (mcpp::platform::host_arch == std::string_view("x86_64")) {
2405+ return std::string(pins::kFirstRunLinuxX86_64);
2406+ } else {
2407+ return std::string(pins::kFirstRunLinuxOther);
2408+ }
2409+ };
2410+
23772411 bool windowsGnuFirstRun = false;
23782412 if constexpr (mcpp::platform::is_windows) {
23792413 if (!tcSpec.has_value() && overrides.target_triple.empty()
@@ -3332,20 +3366,11 @@ prepare_build(bool print_fingerprint,
33323366 // static binaries (ideal for aarch64 / Termux, no bionic dep).
33333367 // glibc-world linking (X11/GL) needs an explicit glibc
33343368 // toolchain, addable later for native-ABI aarch64 builds.
3335- namespace pins = mcpp::toolchain::triple::pins;
3336- std::string defaultSpec;
3337- if constexpr (mcpp::platform::is_macos) {
3338- defaultSpec = std::string(pins::kFirstRunMac);
3339- } else if constexpr (mcpp::platform::is_windows) {
3340- // Reaching here means msvc_usable_either_origin() was true — the seed above
3341- // diverts the no-Visual-Studio case onto the windows-gnu target
3342- // before the target block runs, so it never gets this far.
3343- defaultSpec = std::string(pins::kFirstRunWinMsvc);
3344- } else if (mcpp::platform::host_arch == std::string_view("x86_64")) {
3345- defaultSpec = std::string(pins::kFirstRunLinuxX86_64);
3346- } else {
3347- defaultSpec = std::string(pins::kFirstRunLinuxOther);
3348- }
3369+ // `native_first_run_spec()` (declared above) is this exact selection
3370+ // — on Windows it re-checks `msvc_usable_either_origin()`, which here
3371+ // is redundant (the seed above already diverted the unusable case
3372+ // onto the windows-gnu target before this block runs) but harmless.
3373+ std::string defaultSpec = native_first_run_spec();
33493374 auto defaultParsed = mcpp::toolchain::parse_toolchain_spec(defaultSpec);
33503375 // The legacy "-musl" spelling normalizes to (gcc, <host>-linux-musl),
33513376 // so the resolver finds the `<host_arch>-linux-musl-g++` frontend
@@ -4130,12 +4155,36 @@ prepare_build(bool print_fingerprint,
41304155 // THE ROW'S CONVENTION IS NOT THE HOST'S COMPILER. When the target
41314156 // row's pin replaced a spec the user or the machine had chosen, the
41324157 // build program resolves the replaced one: it is what a native build
4133- // on this machine would use, and it is what the user wrote. A pin
4134- // that replaced nothing is resolved as before.
4158+ // on this machine would use, and it is what the user wrote.
4159+ //
4160+ // A PIN THAT REPLACED NOTHING IS NOT "RESOLVED AS BEFORE" ANY MORE
4161+ // (#622). "Before" meant falling through to `*tcSpec`, which at this
4162+ // point (`tcOrigin == TargetPin`) IS the row's own pin — a TARGET
4163+ // answer. For a row whose payload can only ever emit its target
4164+ // (`emscripten@…` → em++, WebAssembly under every invocation) that
4165+ // resolved a cross compiler as the HOST toolchain for build.mcpp,
4166+ // which is compiled AND RUN on this machine: the compile itself
4167+ // "succeeds" (clang accepts the syntax) and the failure surfaces one
4168+ // step later, inside the payload's own driver, trying to produce a
4169+ // program this machine can execute (measured: emcc.py's
4170+ // `phase_compile_inputs` hits `assert os.path.exists(output_file)`
4171+ // and raises, on the very first `mcpp build --target
4172+ // wasm32-emscripten` in a fresh $HOME, before any [toolchain] default
4173+ // has ever been resolved or persisted). A row whose payload happens
4174+ // to double as a host compiler (an NDK clang) hid the same defect by
4175+ // accident.
4176+ //
4177+ // "Nothing to fall back on" must mean "resolve the platform's native
4178+ // default now", exactly as a plain `mcpp build` would on a virgin
4179+ // machine — not "reuse the target's answer". `native_first_run_spec()`
4180+ // is that exact selection (declared once, above, and used by the
4181+ // first-run installer itself), reused rather than re-derived so the
4182+ // two cannot silently drift apart.
41354183 const std::string hostSpecText =
41364184 (tcOrigin == TcOrigin::TargetPin && hostSpecBeforeRowPin.has_value()
41374185 && !hostSpecBeforeRowPin->empty() && *hostSpecBeforeRowPin != "system")
4138- ? *hostSpecBeforeRowPin : *tcSpec;
4186+ ? *hostSpecBeforeRowPin
4187+ : (tcOrigin == TcOrigin::TargetPin ? native_first_run_spec() : *tcSpec);
41394188 auto spec = mcpp::toolchain::parse_toolchain_spec(hostSpecText);
41404189 if (!spec || spec->version.empty()) {
41414190 return std::unexpected(std::format(
@@ -4168,7 +4217,41 @@ prepare_build(bool print_fingerprint,
41684217 return std::unexpected(std::format(
41694218 "host toolchain post-install fixup: {}", fixed.error()));
41704219 else report_fixup(*fixed, payload->root);
4171- auto htc = mcpp::toolchain::detect(frontend);
4220+ // SAME THREE ARGUMENTS THE NATIVE CALL USES (line ~3550), not the
4221+ // one-argument form. `detect()` probes `payloadPaths` — the
4222+ // fine-grained glibc/linux-headers xpkg directories `resolve_link_model`
4223+ // attaches as explicit `-isystem` rows — from the SECOND argument, and
4224+ // does so only when it is given; passing only `frontend` leaves
4225+ // `tc.payloadPaths` unset, so `host_base_flags`/`host_compile_tokens`
4226+ // fell back to `tc.sysroot` alone (from the payload's own
4227+ // `*sysroot_spec: --sysroot=%R`, `%R` being wherever the fixup pointed
4228+ // it — nothing, on a sandbox with no leaked subos sysroot to fill it
4229+ // in by accident).
4230+ //
4231+ // Measured in the xlings sandbox against the released 2026.9.12.3, on
4232+ // a fresh registry (a real, non-symlinked gcc@16.1.0 payload, no
4233+ // ambient /usr/include, no subos state to leak): "Resolved host
4234+ // toolchain for build.mcpp: gcc 16.1.0 (x86_64-linux-gnu)" — the right
4235+ // FAMILY, since #622's first fix already keeps the pre-row spec — and
4236+ // then the `mcpp` module compile failed with `features.h: No such
4237+ // file or directory`, because that gcc's specs alone name no C
4238+ // library. `echo | g++ -x c++ -E -v -` there lists only the payload's
4239+ // own `c++/16.1.0`, `include`, `include-fixed` — no glibc directory.
4240+ // On a development machine the same probe happens to pass, but for a
4241+ // reason that has nothing to do with this code path: the shared-store
4242+ // gcc's search list there ends with a SUBOS's `usr/include`, leaked
4243+ // into `%R` by machine state the payload never declared (the same
4244+ // shape as "host /usr/include silently completes a payload
4245+ // toolchain") — which is exactly the kind of thing a fresh sandbox
4246+ // does not have lying around to hide the gap.
4247+ //
4248+ // `runtimePayload` and `runtimeBindingSnapshot` (declared once, near
4249+ // the top of this function) are the HOST's C-library identity — never
4250+ // re-derived from `--target`, see their own declarations — so passing
4251+ // them here is not a parallel derivation; it is the one this function
4252+ // already had in scope and the native call already trusts.
4253+ auto htc = mcpp::toolchain::detect(
4254+ frontend, runtimePayload, runtimeBindingSnapshot.contractHash);
41724255 if (!htc) return std::unexpected(htc.error().message);
41734256 mcpp::ui::info("Resolved", std::format(
41744257 "host toolchain for build.mcpp: {}", htc->label()));
0 commit comments