Skip to content

Commit 23974c5

Browse files
committed
a build program under a target row's pin resolves the host's compiler, not the row's (#622)
1 parent 94f1413 commit 23974c5

2 files changed

Lines changed: 112 additions & 3 deletions

File tree

src/build/prepare.cppm

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,6 +1712,19 @@ prepare_build(bool print_fingerprint,
17121712
// learned by experiment — writing the same value a second time in
17131713
// `[target.<triple>]` and observing that it works.
17141714
std::string pinReplacedDefault;
1715+
// THE HOST SPEC AS IT STOOD BEFORE A TARGET ROW'S CONVENTION REPLACED IT,
1716+
// whatever its origin. `build.mcpp` is compiled and run on this machine,
1717+
// so its compiler is a host fact; the row's pin is a target fact. Before
1718+
// this snapshot existed, `host_tc_for_build_program` read `tcSpec` after
1719+
// the row had overwritten it and resolved the row's payload "for the
1720+
// host" -- which works by accident for a payload whose compiler can also
1721+
// target the host (an NDK clang) and cannot work for one that cannot:
1722+
// `em++` produces WebAssembly under every invocation, and every project
1723+
// 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.
1727+
std::optional<std::string> hostSpecBeforeRowPin;
17151728
// THE PACKAGE WHOSE `requires` CHOSE THE COMPILER, AND WHAT IT ASKED FOR.
17161729
//
17171730
// Non-empty only when the graph's requirement actually changed the answer.
@@ -4114,10 +4127,19 @@ prepare_build(bool print_fingerprint,
41144127
"build.mcpp under a cross --target needs a resolvable host "
41154128
"toolchain — set one via [toolchain] or `mcpp toolchain default`"));
41164129
}
4117-
auto spec = mcpp::toolchain::parse_toolchain_spec(*tcSpec);
4130+
// THE ROW'S CONVENTION IS NOT THE HOST'S COMPILER. When the target
4131+
// row's pin replaced a spec the user or the machine had chosen, the
4132+
// 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.
4135+
const std::string hostSpecText =
4136+
(tcOrigin == TcOrigin::TargetPin && hostSpecBeforeRowPin.has_value()
4137+
&& !hostSpecBeforeRowPin->empty() && *hostSpecBeforeRowPin != "system")
4138+
? *hostSpecBeforeRowPin : *tcSpec;
4139+
auto spec = mcpp::toolchain::parse_toolchain_spec(hostSpecText);
41184140
if (!spec || spec->version.empty()) {
41194141
return std::unexpected(std::format(
4120-
"toolchain spec '{}' is invalid for the build.mcpp host resolve", *tcSpec));
4142+
"toolchain spec '{}' is invalid for the build.mcpp host resolve", hostSpecText));
41214143
}
41224144
// Deliberately NO target injection: the spec resolves for the host.
41234145
auto pkg = mcpp::toolchain::to_xim_package(*spec);
@@ -4128,7 +4150,7 @@ prepare_build(bool print_fingerprint,
41284150
auto payload = fetcher.resolve_xpkg_path(pkg.target(), /*autoInstall=*/true, &progress);
41294151
if (!payload) {
41304152
return std::unexpected(std::format(
4131-
"host toolchain for build.mcpp ('{}'): {}", *tcSpec,
4153+
"host toolchain for build.mcpp ('{}'): {}", hostSpecText,
41324154
payload.error().message));
41334155
}
41344156
auto frontendR = mcpp::toolchain::payload_frontend(payload->root, pkg);
@@ -7240,6 +7262,12 @@ prepare_build(bool print_fingerprint,
72407262
if (tcOrigin == TcOrigin::GlobalDefault && tcSpec.has_value()
72417263
&& *tcSpec != targetPinCandidate)
72427264
pinReplacedDefault = *tcSpec;
7265+
// Kept for the build program's host resolution; see the
7266+
// declaration. Taken from every origin, not only the global
7267+
// default, because a `[toolchain]` the manifest named is just as
7268+
// much the host's compiler as a remembered default is.
7269+
if (tcSpec.has_value() && *tcSpec != targetPinCandidate)
7270+
hostSpecBeforeRowPin = *tcSpec;
72437271
tcSpec = targetPinCandidate;
72447272
tcOrigin = TcOrigin::TargetPin;
72457273
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env bash
2+
# requires: elf
3+
# 657 -- a project with a build.mcpp builds for wasm32-emscripten (#622).
4+
#
5+
# `build.mcpp` is compiled and run on this machine. Under a cross target the
6+
# engine resolves a host toolchain for it from the toolchain spec, and the
7+
# spec was read AFTER the target row's convention had replaced it: for the
8+
# Web row that is `emsdk@6.0.9`, whose `em++` produces WebAssembly under every
9+
# invocation. Every project with a build program therefore failed under
10+
# `--target wasm32-emscripten`, inside emcc.py (`AssertionError ...
11+
# phase_compile_inputs`), measured 2026-09-12 by the first dist-web build. An
12+
# NDK clang can also target the host, which is why the Android rows never
13+
# showed it.
14+
#
15+
# The engine now keeps the spec as it stood before the row's pin replaced it
16+
# and resolves the build program's compiler from that. Asserted here as the
17+
# only criterion that distinguishes the two: the build succeeds, the program
18+
# runs under node, and the build program's `MCPP_HOST` equals the machine's
19+
# triple while `MCPP_TARGET` is the Web row. Skips honestly where no emsdk
20+
# payload is installed, as 650 does.
21+
set -e
22+
23+
have_emsdk=0
24+
for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripten \
25+
"$HOME"/.xlings/data/xpkgs/xim-x-emsdk/*/emscripten; do
26+
[[ -x "$d/em++" ]] && have_emsdk=1
27+
done
28+
if [ "$have_emsdk" -ne 1 ]; then
29+
echo "SKIP: 657 -- no emsdk payload installed (looked under xim-x-emsdk/*/emscripten)"
30+
exit 0
31+
fi
32+
33+
TMP=$(mktemp -d)
34+
trap "rm -rf $TMP" EXIT
35+
fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; }
36+
37+
mkdir -p "$TMP/p/src"
38+
cat > "$TMP/p/mcpp.toml" <<'TOML'
39+
[package]
40+
name = "bp"
41+
version = "0.1.0"
42+
43+
[targets.bp]
44+
kind = "bin"
45+
main = "src/main.cpp"
46+
47+
[target.wasm32-emscripten]
48+
runner = ["node"]
49+
TOML
50+
cat > "$TMP/p/src/main.cpp" <<'CPP'
51+
#include <cstdio>
52+
int main() { std::puts("1-2-3"); return 0; }
53+
CPP
54+
# The build program records the two triples it was told, so the test can say
55+
# which machine it was compiled for without reading a compiler's argv.
56+
cat > "$TMP/p/build.mcpp" <<'CPP'
57+
import mcpp;
58+
#include <cstdio>
59+
int main() {
60+
std::FILE* f = std::fopen("bp-env.txt", "w");
61+
if (!f) return 1;
62+
std::fprintf(f, "host=%s\ntarget=%s\n", mcpp::host(), mcpp::target());
63+
std::fclose(f);
64+
return 0;
65+
}
66+
CPP
67+
68+
cd "$TMP/p"
69+
"$MCPP" build --target wasm32-emscripten > build.log 2>&1 \
70+
|| fail "a project with a build.mcpp does not build for wasm32-emscripten (the host compiler for build.mcpp was the row's em++)" build.log
71+
grep -q 'AssertionError' build.log && fail "emcc.py's assertion is in the log" build.log
72+
[ -f bp-env.txt ] || fail "the build program did not run" build.log
73+
host_triple=$(grep '^host=' bp-env.txt | cut -d= -f2)
74+
target_triple=$(grep '^target=' bp-env.txt | cut -d= -f2)
75+
[ "$target_triple" = "wasm32-emscripten" ] || fail "MCPP_TARGET was '$target_triple'" bp-env.txt
76+
case "$host_triple" in wasm32*|"") fail "MCPP_HOST was '$host_triple'" bp-env.txt ;; esac
77+
[ -n "$(find target/wasm32-emscripten -path '*/bin/bp.js' | head -1)" ] || fail "no bin/bp.js" build.log
78+
"$MCPP" run --target wasm32-emscripten > run.log 2>&1 || fail "mcpp run failed" run.log
79+
grep -qx '1-2-3' run.log || fail "the program did not print 1-2-3" run.log
80+
81+
echo "PASS: 657_a_build_program_under_the_web_row_uses_the_host_compiler"

0 commit comments

Comments
 (0)