Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<row>]` 均不存在,没有"替换之前"的宿主 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 <glibc>/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 <name>` 与 `mcpp run --format <name>` 报告的产物改为请求引入的
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion modules/versioning/src/version.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions src/build/hostprogram.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -685,6 +686,18 @@ build_mcpp_module(const fs::path& bdir, const fs::path& compiler,

auto run = [&](std::vector<std::string> argv, const char* what)
-> std::expected<void, std::string> {
// 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{}",
Expand Down
125 changes: 104 additions & 21 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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.<row>] 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<std::string> hostSpecBeforeRowPin;
// THE PACKAGE WHOSE `requires` CHOSE THE COMPILER, AND WHAT IT ASKED FOR.
//
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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, <host>-linux-musl),
// so the resolver finds the `<host_arch>-linux-musl-g++` frontend
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()));
Expand Down
Loading
Loading