Skip to content

Commit 13f5a8a

Browse files
The build program's host toolchain under a cross target carries the host's C library (#622, 2026.9.12.4) (#627)
* the build program's host toolchain under a cross target carries the host's C library (#622) * the build program's host toolchain resolves its own C-library payload paths (#622) host_tc_for_build_program's cross branch called detect(frontend) with only the compiler path, unlike the native call site's three-argument form. detect() uses the second/third arguments (the host's runtime-binding identity) to probe which glibc/linux-headers xpkg payloads belong to this exact toolchain (Toolchain::payloadPaths); without them the field stays unset, so neither the explicit -isystem/-idirafter <glibc>/include rows nor the sysroot-completion symlinking that depends on it ever run. Measured in the xlings sandbox (fresh registry, real gcc@16.1.0 payload) with the released 2026.9.12.3: "Resolved host toolchain for build.mcpp: gcc 16.1.0" (the row-pin fix already gets the right compiler), then "mcpp module compile failed": features.h: No such file or directory. Fixed by passing the same runtimePayload/runtimeBindingSnapshot the native path already trusts -- the value is the host's, so it does not vary with --target. Not reliably reproducible outside a real sandbox (this dev machine's own bundled sandbox layout init fills in a usable sysroot through an unrelated path). Also adds a small observability hook (mcpp.build.hostprogram): the mcpp module's compile argv is now logged under MCPP_VERBOSE=1, which is the only way to see it on a run that succeeds. * chore: bump version to 2026.9.12.4 --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent fe3a008 commit 13f5a8a

6 files changed

Lines changed: 288 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,46 @@ Emscripten 自己的 CMake 工具链(`CMAKE_EXECUTABLE_SUFFIX ".js"`)与 Rust
173173
的宿主 spec(用户写的 `[toolchain]` 或机器默认),构建程序按它解析;行 pin 没有替换任何
174174
东西时行为不变。`tests/e2e/657`。
175175

176+
### 修复:交叉目标下,构建程序的宿主工具链带上宿主自己的 C 库(#622 续)
177+
178+
- **第一处口子:行 pin 没有替换任何东西时,回落的仍然是行自己的 spec。** 上一条修复
179+
留了一个口子,注释里也写明了:"行 pin 没有替换任何东西时行为不变"——而"不变"的
180+
行为正是缺陷本身。在 xlings 沙箱里对已发布的 2026.9.12.3 复现:全新 `$HOME`
181+
(`~/.mcpp` registry 为空)第一次调用就是 `mcpp pack --target wasm32-emscripten`,
182+
这时 `[toolchain]`、全局默认、`[target.<row>]` 均不存在,没有"替换之前"的宿主 spec
183+
可以保留,于是回落分支又落回了行 pin 本身——宿主编译器被解析成 `emsdk@6.0.9`
184+
(`em++`),`build.mcpp` 在 emcc.py 的 `phase_compile_inputs` 里得到与最初报告相同的
185+
`AssertionError`。开发机上同一份工程能过,只是因为此前的原生构建已经把机器默认工具链
186+
写进了 `~/.mcpp/config.toml`。"没有可保留的宿主 spec"不等于"没有宿主可言":引擎现在
187+
在这种情况下解析平台自身的原生默认工具链(`native_first_run_spec()`,与首次运行安装器
188+
共用同一份平台/架构判定,而不是第二份手抄的判定),即一次普通 `mcpp build`(不带
189+
`--target`)在同一台机器上会安装的那一个,而不是目标行的约定。该函数只返回 spec
190+
字符串,不做安装或持久化——安装、fixup、探测复用 `host_tc_for_build_program` 原有的
191+
通用流水线。
192+
- **第二处口子,在同一个 lambda 里,第一处修好之后才现形:宿主编译器选对了,却没带上
193+
它自己的 C 库。** 在 xlings 沙箱里对同一个已发布版本复现(全新 registry,真实的
194+
gcc@16.1.0 + glibc@2.44.2 载荷,没有任何会掩盖问题的机器状态):pack 日志正确地说
195+
"Resolved host toolchain for build.mcpp: gcc 16.1.0 (x86_64-linux-gnu)",随即在
196+
编译内建 `mcpp` module 时报 `features.h: No such file or directory`;沙箱里
197+
`echo | g++ -x c++ -E -v -` 只列出该 gcc 载荷自己的 `c++/16.1.0`、`include`、
198+
`include-fixed`,没有任何 C 库目录。真因:`host_tc_for_build_program` 的交叉分支
199+
调用 `detect(frontend)` 时只传编译器路径,没有像原生路径那样一并传入
200+
`runtimePayload` 与宿主的运行时绑定契约哈希——`detect()` 正是靠第二个参数才能探测出
201+
这个宿主工具链具体对应哪一份 glibc/linux-headers 载荷(`Toolchain::payloadPaths`),
202+
漏传这两个参数,该字段就始终为空,无论是显式的 `-isystem`/`-idirafter <glibc>/include`
203+
还是依赖它才会触发的 sysroot 补全符号链接都不会发生。修法是把原生调用点已经在用、也
204+
已经确认与 `--target` 无关的那两个参数原样传给这次 `detect()` 调用——宿主的 C 运行时
205+
绑定(`runtimeBindingSnapshot`)从不随目标切换,这一点在两处修复里都成立。开发机上
206+
这处口子不会稳定复现:mcpp 自带的一次性沙箱布局初始化会以另一条路径把一份可用的
207+
sysroot 铺好,与这处缺陷完全无关——和这个代码库里其他"机器上的既有状态掩盖了一个
208+
载荷缺口"的记录是同一个形状,真正可信的判据只有沙箱。
209+
- 判据:`tests/e2e/657` 新增第二、三阶段——在同一次调用里新建一个全新 `$MCPP_HOME`,
210+
确认交叉构建下 `MCPP_HOST` 落在真实宿主三元组而非 `wasm32-*`、日志中不出现
211+
`AssertionError` 或 `features.h` 缺失,随后在**同一个**全新 home 里再跑一次原生构建,
212+
确认修复没有反过来影响原生路径;`features.h` 那一条断言是可移植的(不依赖某台机器
213+
具体走 `-isystem` 载荷路径还是 sysroot 符号链接),第二处口子本身的权威判据是 xlings
214+
沙箱里的实测,记在这次提交的说明里。
215+
176216
### 修复:发布物是终端产物;库形态的应用也带上运行期文件(#622)
177217

178218
- `mcpp pack --format <name>` 与 `mcpp run --format <name>` 报告的产物改为请求引入的

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.9.12.3"
3+
version = "2026.9.12.4"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

modules/versioning/src/version.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ import std;
3131

3232
export namespace mcpp {
3333

34-
inline constexpr std::string_view MCPP_VERSION = "2026.9.12.3";
34+
inline constexpr std::string_view MCPP_VERSION = "2026.9.12.4";
3535

3636
} // namespace mcpp

src/build/hostprogram.cppm

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export module mcpp.build.hostprogram;
1414

1515
import std;
1616
import mcpp.build.directives; // kProtocolVersion — the announced value has ONE source
17+
import mcpp.log;
1718
import mcpp.platform;
1819
import mcpp.platform.process;
1920
import mcpp.toolchain.dialect;
@@ -685,6 +686,18 @@ build_mcpp_module(const fs::path& bdir, const fs::path& compiler,
685686

686687
auto run = [&](std::vector<std::string> argv, const char* what)
687688
-> std::expected<void, std::string> {
689+
// THE ONLY PLACE THIS ARGV IS EVER OBSERVABLE ON A SUCCESSFUL RUN.
690+
// A failing compile shows its own command implicitly (the compiler's
691+
// diagnostics name the headers it looked for and did not find); a
692+
// passing one otherwise leaves no trace of which `-isystem` rows it
693+
// carried — which is exactly the fact that distinguishes a host
694+
// toolchain whose C library was attached from one whose wasn't (#622,
695+
// `host_tc_for_build_program`'s cross branch). `verbose()` always logs
696+
// it; MCPP_VERBOSE=1 additionally echoes it to stderr.
697+
std::string joined;
698+
for (auto const& a : argv) { if (!joined.empty()) joined += ' '; joined += a; }
699+
mcpp::log::verbose("buildmcpp-host",
700+
std::format("mcpp module {}: {}", what, joined));
688701
auto r = mcpp::platform::process::capture_exec(argv, env, bdir.string());
689702
if (r.exit_code != 0)
690703
return std::unexpected(std::format("mcpp module {} failed (exit {}):\n{}",

src/build/prepare.cppm

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)