diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d08ba0d..df8becb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -173,6 +173,46 @@ Emscripten 自己的 CMake 工具链(`CMAKE_EXECUTABLE_SUFFIX ".js"`)与 Rust 的宿主 spec(用户写的 `[toolchain]` 或机器默认),构建程序按它解析;行 pin 没有替换任何 东西时行为不变。`tests/e2e/657`。 +### 修复:交叉目标下,构建程序的宿主工具链带上宿主自己的 C 库(#622 续) + +- **第一处口子:行 pin 没有替换任何东西时,回落的仍然是行自己的 spec。** 上一条修复 + 留了一个口子,注释里也写明了:"行 pin 没有替换任何东西时行为不变"——而"不变"的 + 行为正是缺陷本身。在 xlings 沙箱里对已发布的 2026.9.12.3 复现:全新 `$HOME` + (`~/.mcpp` registry 为空)第一次调用就是 `mcpp pack --target wasm32-emscripten`, + 这时 `[toolchain]`、全局默认、`[target.]` 均不存在,没有"替换之前"的宿主 spec + 可以保留,于是回落分支又落回了行 pin 本身——宿主编译器被解析成 `emsdk@6.0.9` + (`em++`),`build.mcpp` 在 emcc.py 的 `phase_compile_inputs` 里得到与最初报告相同的 + `AssertionError`。开发机上同一份工程能过,只是因为此前的原生构建已经把机器默认工具链 + 写进了 `~/.mcpp/config.toml`。"没有可保留的宿主 spec"不等于"没有宿主可言":引擎现在 + 在这种情况下解析平台自身的原生默认工具链(`native_first_run_spec()`,与首次运行安装器 + 共用同一份平台/架构判定,而不是第二份手抄的判定),即一次普通 `mcpp build`(不带 + `--target`)在同一台机器上会安装的那一个,而不是目标行的约定。该函数只返回 spec + 字符串,不做安装或持久化——安装、fixup、探测复用 `host_tc_for_build_program` 原有的 + 通用流水线。 +- **第二处口子,在同一个 lambda 里,第一处修好之后才现形:宿主编译器选对了,却没带上 + 它自己的 C 库。** 在 xlings 沙箱里对同一个已发布版本复现(全新 registry,真实的 + gcc@16.1.0 + glibc@2.44.2 载荷,没有任何会掩盖问题的机器状态):pack 日志正确地说 + "Resolved host toolchain for build.mcpp: gcc 16.1.0 (x86_64-linux-gnu)",随即在 + 编译内建 `mcpp` module 时报 `features.h: No such file or directory`;沙箱里 + `echo | g++ -x c++ -E -v -` 只列出该 gcc 载荷自己的 `c++/16.1.0`、`include`、 + `include-fixed`,没有任何 C 库目录。真因:`host_tc_for_build_program` 的交叉分支 + 调用 `detect(frontend)` 时只传编译器路径,没有像原生路径那样一并传入 + `runtimePayload` 与宿主的运行时绑定契约哈希——`detect()` 正是靠第二个参数才能探测出 + 这个宿主工具链具体对应哪一份 glibc/linux-headers 载荷(`Toolchain::payloadPaths`), + 漏传这两个参数,该字段就始终为空,无论是显式的 `-isystem`/`-idirafter /include` + 还是依赖它才会触发的 sysroot 补全符号链接都不会发生。修法是把原生调用点已经在用、也 + 已经确认与 `--target` 无关的那两个参数原样传给这次 `detect()` 调用——宿主的 C 运行时 + 绑定(`runtimeBindingSnapshot`)从不随目标切换,这一点在两处修复里都成立。开发机上 + 这处口子不会稳定复现:mcpp 自带的一次性沙箱布局初始化会以另一条路径把一份可用的 + sysroot 铺好,与这处缺陷完全无关——和这个代码库里其他"机器上的既有状态掩盖了一个 + 载荷缺口"的记录是同一个形状,真正可信的判据只有沙箱。 +- 判据:`tests/e2e/657` 新增第二、三阶段——在同一次调用里新建一个全新 `$MCPP_HOME`, + 确认交叉构建下 `MCPP_HOST` 落在真实宿主三元组而非 `wasm32-*`、日志中不出现 + `AssertionError` 或 `features.h` 缺失,随后在**同一个**全新 home 里再跑一次原生构建, + 确认修复没有反过来影响原生路径;`features.h` 那一条断言是可移植的(不依赖某台机器 + 具体走 `-isystem` 载荷路径还是 sysroot 符号链接),第二处口子本身的权威判据是 xlings + 沙箱里的实测,记在这次提交的说明里。 + ### 修复:发布物是终端产物;库形态的应用也带上运行期文件(#622) - `mcpp pack --format ` 与 `mcpp run --format ` 报告的产物改为请求引入的 diff --git a/mcpp.toml b/mcpp.toml index 1ecddb93..d2b09b4e 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.12.3" +version = "2026.9.12.4" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 6e251f35..9b20a4d6 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.9.12.3"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.12.4"; } // namespace mcpp diff --git a/src/build/hostprogram.cppm b/src/build/hostprogram.cppm index debcd4e5..6ac6c67c 100644 --- a/src/build/hostprogram.cppm +++ b/src/build/hostprogram.cppm @@ -14,6 +14,7 @@ export module mcpp.build.hostprogram; import std; import mcpp.build.directives; // kProtocolVersion — the announced value has ONE source +import mcpp.log; import mcpp.platform; import mcpp.platform.process; import mcpp.toolchain.dialect; @@ -685,6 +686,18 @@ build_mcpp_module(const fs::path& bdir, const fs::path& compiler, auto run = [&](std::vector argv, const char* what) -> std::expected { + // THE ONLY PLACE THIS ARGV IS EVER OBSERVABLE ON A SUCCESSFUL RUN. + // A failing compile shows its own command implicitly (the compiler's + // diagnostics name the headers it looked for and did not find); a + // passing one otherwise leaves no trace of which `-isystem` rows it + // carried — which is exactly the fact that distinguishes a host + // toolchain whose C library was attached from one whose wasn't (#622, + // `host_tc_for_build_program`'s cross branch). `verbose()` always logs + // it; MCPP_VERBOSE=1 additionally echoes it to stderr. + std::string joined; + for (auto const& a : argv) { if (!joined.empty()) joined += ' '; joined += a; } + mcpp::log::verbose("buildmcpp-host", + std::format("mcpp module {}: {}", what, joined)); auto r = mcpp::platform::process::capture_exec(argv, env, bdir.string()); if (r.exit_code != 0) return std::unexpected(std::format("mcpp module {} failed (exit {}):\n{}", diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 77a2dc44..9b20fa89 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -1721,9 +1721,18 @@ prepare_build(bool print_fingerprint, // target the host (an NDK clang) and cannot work for one that cannot: // `em++` produces WebAssembly under every invocation, and every project // with a build program failed under `--target wasm32-emscripten` inside - // `emcc.py` (#622, measured by the dist-web member's first build). Empty - // when the row replaced nothing, in which case the row's pin remains the - // only spec there is and the previous behaviour is kept. + // `emcc.py` (#622, measured by the dist-web member's first build). + // + // Empty when the row replaced nothing — no [toolchain], no global + // default, no [target.] entry existed before the row's pin applied. + // THIS IS NOT "the row's pin remains the only spec there is": on a + // fresh $HOME whose first-ever invocation names a hosted `--target` + // (nothing to be "before"), that reading resolved the SAME payload the + // row just picked — `em++` again — as the host compiler, which is the + // exact defect this field exists to close, just with no prior value to + // restore. `host_tc_for_build_program` resolves the platform's own + // native default in that case instead (`native_first_run_spec()`), the + // same one a plain `mcpp build` would have installed. std::optional hostSpecBeforeRowPin; // THE PACKAGE WHOSE `requires` CHOSE THE COMPILER, AND WHAT IT ASKED FOR. // @@ -2374,6 +2383,31 @@ prepare_build(bool print_fingerprint, (*c)->xlingsHome() / "data" / "xpkgs"); }; + // THE PLATFORM'S CANONICAL NATIVE DEFAULT — a spec string only; no + // install, no persistence. Two places need "what would a native + // `mcpp build` pick here, with no --target": the first-run installer + // further below (which goes on to install and persist it), and + // `host_tc_for_build_program`'s cross branch (which needs a genuine HOST + // compiler when nothing was ever recorded as one — see its own comment + // for why #622 happened). One derivation, called from both, so they + // cannot drift the way a hand-copied second copy would. + auto native_first_run_spec = [&]() -> std::string { + namespace pins = mcpp::toolchain::triple::pins; + if constexpr (mcpp::platform::is_macos) { + return std::string(pins::kFirstRunMac); + } else if constexpr (mcpp::platform::is_windows) { + // A machine with no usable MSVC gets the GNU pin, not an + // MSVC-ABI clang it cannot use — mirrors the windows-gnu seed + // below, which this function's other caller runs after. + return std::string(msvc_usable_either_origin() + ? pins::kFirstRunWinMsvc : pins::kFirstRunWinGnu); + } else if (mcpp::platform::host_arch == std::string_view("x86_64")) { + return std::string(pins::kFirstRunLinuxX86_64); + } else { + return std::string(pins::kFirstRunLinuxOther); + } + }; + bool windowsGnuFirstRun = false; if constexpr (mcpp::platform::is_windows) { if (!tcSpec.has_value() && overrides.target_triple.empty() @@ -3332,20 +3366,11 @@ prepare_build(bool print_fingerprint, // static binaries (ideal for aarch64 / Termux, no bionic dep). // glibc-world linking (X11/GL) needs an explicit glibc // toolchain, addable later for native-ABI aarch64 builds. - namespace pins = mcpp::toolchain::triple::pins; - std::string defaultSpec; - if constexpr (mcpp::platform::is_macos) { - defaultSpec = std::string(pins::kFirstRunMac); - } else if constexpr (mcpp::platform::is_windows) { - // Reaching here means msvc_usable_either_origin() was true — the seed above - // diverts the no-Visual-Studio case onto the windows-gnu target - // before the target block runs, so it never gets this far. - defaultSpec = std::string(pins::kFirstRunWinMsvc); - } else if (mcpp::platform::host_arch == std::string_view("x86_64")) { - defaultSpec = std::string(pins::kFirstRunLinuxX86_64); - } else { - defaultSpec = std::string(pins::kFirstRunLinuxOther); - } + // `native_first_run_spec()` (declared above) is this exact selection + // — on Windows it re-checks `msvc_usable_either_origin()`, which here + // is redundant (the seed above already diverted the unusable case + // onto the windows-gnu target before this block runs) but harmless. + std::string defaultSpec = native_first_run_spec(); auto defaultParsed = mcpp::toolchain::parse_toolchain_spec(defaultSpec); // The legacy "-musl" spelling normalizes to (gcc, -linux-musl), // so the resolver finds the `-linux-musl-g++` frontend @@ -4130,12 +4155,36 @@ prepare_build(bool print_fingerprint, // THE ROW'S CONVENTION IS NOT THE HOST'S COMPILER. When the target // row's pin replaced a spec the user or the machine had chosen, the // build program resolves the replaced one: it is what a native build - // on this machine would use, and it is what the user wrote. A pin - // that replaced nothing is resolved as before. + // on this machine would use, and it is what the user wrote. + // + // A PIN THAT REPLACED NOTHING IS NOT "RESOLVED AS BEFORE" ANY MORE + // (#622). "Before" meant falling through to `*tcSpec`, which at this + // point (`tcOrigin == TargetPin`) IS the row's own pin — a TARGET + // answer. For a row whose payload can only ever emit its target + // (`emscripten@…` → em++, WebAssembly under every invocation) that + // resolved a cross compiler as the HOST toolchain for build.mcpp, + // which is compiled AND RUN on this machine: the compile itself + // "succeeds" (clang accepts the syntax) and the failure surfaces one + // step later, inside the payload's own driver, trying to produce a + // program this machine can execute (measured: emcc.py's + // `phase_compile_inputs` hits `assert os.path.exists(output_file)` + // and raises, on the very first `mcpp build --target + // wasm32-emscripten` in a fresh $HOME, before any [toolchain] default + // has ever been resolved or persisted). A row whose payload happens + // to double as a host compiler (an NDK clang) hid the same defect by + // accident. + // + // "Nothing to fall back on" must mean "resolve the platform's native + // default now", exactly as a plain `mcpp build` would on a virgin + // machine — not "reuse the target's answer". `native_first_run_spec()` + // is that exact selection (declared once, above, and used by the + // first-run installer itself), reused rather than re-derived so the + // two cannot silently drift apart. const std::string hostSpecText = (tcOrigin == TcOrigin::TargetPin && hostSpecBeforeRowPin.has_value() && !hostSpecBeforeRowPin->empty() && *hostSpecBeforeRowPin != "system") - ? *hostSpecBeforeRowPin : *tcSpec; + ? *hostSpecBeforeRowPin + : (tcOrigin == TcOrigin::TargetPin ? native_first_run_spec() : *tcSpec); auto spec = mcpp::toolchain::parse_toolchain_spec(hostSpecText); if (!spec || spec->version.empty()) { return std::unexpected(std::format( @@ -4168,7 +4217,41 @@ prepare_build(bool print_fingerprint, return std::unexpected(std::format( "host toolchain post-install fixup: {}", fixed.error())); else report_fixup(*fixed, payload->root); - auto htc = mcpp::toolchain::detect(frontend); + // SAME THREE ARGUMENTS THE NATIVE CALL USES (line ~3550), not the + // one-argument form. `detect()` probes `payloadPaths` — the + // fine-grained glibc/linux-headers xpkg directories `resolve_link_model` + // attaches as explicit `-isystem` rows — from the SECOND argument, and + // does so only when it is given; passing only `frontend` leaves + // `tc.payloadPaths` unset, so `host_base_flags`/`host_compile_tokens` + // fell back to `tc.sysroot` alone (from the payload's own + // `*sysroot_spec: --sysroot=%R`, `%R` being wherever the fixup pointed + // it — nothing, on a sandbox with no leaked subos sysroot to fill it + // in by accident). + // + // Measured in the xlings sandbox against the released 2026.9.12.3, on + // a fresh registry (a real, non-symlinked gcc@16.1.0 payload, no + // ambient /usr/include, no subos state to leak): "Resolved host + // toolchain for build.mcpp: gcc 16.1.0 (x86_64-linux-gnu)" — the right + // FAMILY, since #622's first fix already keeps the pre-row spec — and + // then the `mcpp` module compile failed with `features.h: No such + // file or directory`, because that gcc's specs alone name no C + // library. `echo | g++ -x c++ -E -v -` there lists only the payload's + // own `c++/16.1.0`, `include`, `include-fixed` — no glibc directory. + // On a development machine the same probe happens to pass, but for a + // reason that has nothing to do with this code path: the shared-store + // gcc's search list there ends with a SUBOS's `usr/include`, leaked + // into `%R` by machine state the payload never declared (the same + // shape as "host /usr/include silently completes a payload + // toolchain") — which is exactly the kind of thing a fresh sandbox + // does not have lying around to hide the gap. + // + // `runtimePayload` and `runtimeBindingSnapshot` (declared once, near + // the top of this function) are the HOST's C-library identity — never + // re-derived from `--target`, see their own declarations — so passing + // them here is not a parallel derivation; it is the one this function + // already had in scope and the native call already trusts. + auto htc = mcpp::toolchain::detect( + frontend, runtimePayload, runtimeBindingSnapshot.contractHash); if (!htc) return std::unexpected(htc.error().message); mcpp::ui::info("Resolved", std::format( "host toolchain for build.mcpp: {}", htc->label())); diff --git a/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh b/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh index 2a395e95..dc8b1b31 100755 --- a/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh +++ b/tests/e2e/657_a_build_program_under_the_web_row_uses_the_host_compiler.sh @@ -18,6 +18,55 @@ # runs under node, and the build program's `MCPP_HOST` equals the machine's # triple while `MCPP_TARGET` is the Web row. Skips honestly where no emsdk # payload is installed, as 650 does. +# +# PHASE 2, below, closes the gap the first fix left open: "the spec as it +# stood before the row's pin" only exists when SOMETHING was there to stand — +# a [toolchain], a global default, a [target.] entry. On a fresh +# $MCPP_HOME whose very first invocation names `--target wasm32-emscripten` +# (no prior default has ever been resolved or persisted), there is nothing to +# restore, and the engine fell through to the row's OWN pin again — the exact +# defect this file exists to catch, just with no earlier value standing in +# the way. Measured against the released 2026.9.12.3 binary: the build +# resolved "host toolchain for build.mcpp: clang 6.0.9 +# (wasm32-unknown-emscripten)" and then failed inside emcc.py's +# `phase_compile_inputs` with the same `AssertionError` as the original +# report — reusing a project that already imports `mcpp` reproduces it +# without needing `mcpp:plugins`. The fix resolves the platform's own native +# default in that case (the same one a plain `mcpp build` would install), +# not the row's convention. Phase 2 holds a fresh $MCPP_HOME so this is the +# condition actually exercised, and phase 3 checks the fix did not cost the +# native path anything in that same fresh home. +# +# PHASE 2 ALSO CATCHES A SECOND, NARROWER DEFECT IN THE SAME LAMBDA, found +# after the row-pin fix above: even once the host toolchain is correctly +# resolved as the platform's gcc, `host_tc_for_build_program`'s cross branch +# called `detect(frontend)` with ONLY the compiler path -- not the two extra +# arguments (`runtimePayload`, the host's runtime-binding contract hash) the +# native call site always passes. `detect()` uses the second argument to +# probe which glibc/linux-headers xpkg payloads belong to this exact host +# toolchain (`Toolchain::payloadPaths`); without it, that field stays unset, +# so neither the explicit `-isystem`/`-idirafter /include` rows nor +# the sysroot-completion symlinking that depends on it ever run. Measured in +# the xlings sandbox (a fresh registry, real gcc@16.1.0 + glibc@2.44.2 +# payloads, no leaked host state): the log said "Resolved host toolchain for +# build.mcpp: gcc 16.1.0 (x86_64-linux-gnu)" -- the RIGHT compiler -- and +# then `mcpp module compile failed`: +# .../xim-x-gcc/16.1.0/include/c++/16.1.0/x86_64-linux-gnu/bits/os_defines.h: +# fatal error: features.h: No such file or directory +# `echo | g++ -x c++ -E -v -` in that sandbox listed only the payload's own +# `c++/16.1.0`, `include`, `include-fixed` -- no C library directory at all. +# The fix passes the SAME `runtimePayload`/host runtime-binding the native +# call already trusts (declared once, near the top of prepare_build) rather +# than deriving anything new -- the value is the host's, so it does not vary +# with `--target`. NOT RELIABLY REPRODUCIBLE OUTSIDE A REAL SANDBOX: on an +# interactive dev machine the same fresh-$MCPP_HOME recipe did not fail -- +# mcpp's own bundled "Initialize mcpp sandbox layout" step populates a usable +# sysroot there by some path this defect never went near, the same shape as +# every other "ambient machine state hides a payload gap" note in this +# codebase's history. The assertion below is therefore the portable, +# environment-independent half (the exact error string from the report must +# never appear); the authoritative before/after check is the sandbox run +# recorded in this commit's message. set -e have_emsdk=0 @@ -34,6 +83,8 @@ TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } +# ── PHASE 1: the ambient $MCPP_HOME (a global default toolchain typically +# already exists here — "the spec as it stood before the row's pin") ─────── mkdir -p "$TMP/p/src" cat > "$TMP/p/mcpp.toml" <<'TOML' [package] @@ -78,4 +129,82 @@ case "$host_triple" in wasm32*|"") fail "MCPP_HOST was '$host_triple'" bp-env.tx "$MCPP" run --target wasm32-emscripten > run.log 2>&1 || fail "mcpp run failed" run.log grep -qx '1-2-3' run.log || fail "the program did not print 1-2-3" run.log +# ── PHASE 2: a FRESH $MCPP_HOME whose first-ever invocation is the cross +# build — no [toolchain], no global default, nothing for `hostSpecBeforeRowPin` +# to carry forward. This is the condition the original fix's own comment +# named and left open ("Empty when the row replaced nothing, in which case +# the row's pin remains the only spec there is") and is exactly what a fresh +# sandbox $HOME holds on its first `mcpp build --target wasm32-emscripten` — +# see .agents/docs/2026-09-12-622-verify.sh section J, measured against the +# released 2026.9.12.3 binary. +FRESH_HOME="$TMP/fresh-mcpp-home" +mkdir -p "$TMP/q/src" +cat > "$TMP/q/mcpp.toml" <<'TOML' +[package] +name = "bq" +version = "0.1.0" + +[targets.bq] +kind = "bin" +main = "src/main.cpp" + +[target.wasm32-emscripten] +runner = ["node"] +TOML +cat > "$TMP/q/src/main.cpp" <<'CPP' +#include +int main() { std::puts("1-2-3"); return 0; } +CPP +cat > "$TMP/q/build.mcpp" <<'CPP' +import mcpp; +#include +int main() { + std::FILE* f = std::fopen("bp-env.txt", "w"); + if (!f) return 1; + std::fprintf(f, "host=%s\ntarget=%s\n", mcpp::host(), mcpp::target()); + std::fclose(f); + return 0; +} +CPP + +cd "$TMP/q" +# MCPP_VERBOSE=1 makes the mcpp-module compile command itself observable in +# the log (mcpp.build.hostprogram's `run` lambda logs it under the +# "buildmcpp-host" tag) — the only place a SUCCESSFUL compile's `-isystem`/ +# `-idirafter /include` rows are visible at all; a failing one shows +# them implicitly, through the headers the compiler says it could not find. +MCPP_VERBOSE=1 MCPP_HOME="$FRESH_HOME" "$MCPP" build --target wasm32-emscripten > fresh-build.log 2>&1 \ + || fail "a fresh \$MCPP_HOME's first build does not build for wasm32-emscripten (row-pin fallback picked the row's own compiler as the host toolchain again)" fresh-build.log +grep -q 'AssertionError' fresh-build.log && fail "emcc.py's assertion is in the fresh-home log" fresh-build.log +grep -q 'host toolchain for build.mcpp: clang' fresh-build.log \ + && fail "the fresh-home host toolchain for build.mcpp was the Web row's own clang" fresh-build.log +# THE SECOND DEFECT'S EXACT SIGNATURE (see the header comment): the host +# toolchain can be correctly gcc and still have no path to its own C +# library. This string appears only when a C library header search +# actually failed, so it is portable across whatever the local sysroot +# happens to look like (payload -isystem rows vs. a symlinked bundled +# sysroot) — unlike asserting a specific flag on the compile line. +grep -qi 'features\.h' fresh-build.log \ + && fail "features.h was reported missing while compiling the mcpp module (host toolchain has no C library attached)" fresh-build.log +[ -f bp-env.txt ] || fail "the build program did not run in the fresh home" fresh-build.log +fresh_host_triple=$(grep '^host=' bp-env.txt | cut -d= -f2) +fresh_target_triple=$(grep '^target=' bp-env.txt | cut -d= -f2) +[ "$fresh_target_triple" = "wasm32-emscripten" ] || fail "fresh-home MCPP_TARGET was '$fresh_target_triple'" bp-env.txt +case "$fresh_host_triple" in wasm32*|"") fail "fresh-home MCPP_HOST was '$fresh_host_triple'" bp-env.txt ;; esac +[ -n "$(find target/wasm32-emscripten -path '*/bin/bq.js' | head -1)" ] || fail "no bin/bq.js in the fresh home" fresh-build.log +MCPP_HOME="$FRESH_HOME" "$MCPP" run --target wasm32-emscripten > fresh-run.log 2>&1 \ + || fail "mcpp run failed in the fresh home" fresh-run.log +grep -qx '1-2-3' fresh-run.log || fail "the program did not print 1-2-3 in the fresh home" fresh-run.log + +# ── PHASE 3: the SAME fresh home's native path must still work — a wrong +# fallback (e.g. always overriding the host spec, rather than only when the +# row replaced nothing) would show up here as the native build picking up +# something wasm-shaped instead of a real host compiler. ─────────────────── +rm -rf target +MCPP_HOME="$FRESH_HOME" "$MCPP" build > fresh-native-build.log 2>&1 \ + || fail "the native build in the same fresh home regressed" fresh-native-build.log +native_bin=$(find target -path '*/bin/bq' | head -1) +[ -n "$native_bin" ] || fail "no native bin/bq in the same fresh home" fresh-native-build.log +[ "$("$native_bin")" = "1-2-3" ] || fail "the native bin/bq in the same fresh home did not print 1-2-3" fresh-native-build.log + echo "PASS: 657_a_build_program_under_the_web_row_uses_the_host_compiler"