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
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,56 @@

## [Unreleased]

## [2026.9.18.3] - 2026-09-18

### Windows 主机 × freestanding 目标的 c-abi 校验探针两处真实缺陷被关掉

2026.9.18.1 发布的几小时内,openkal-llvm-runtime#24 的 CI 在 Windows 主机 × `riscv64-none-elf`
(freestanding) 一行红——探针报两条不匹配,都是真实存在的结构性缺陷,根因都在测量一侧而
不在声明一侧:

- `_WIN32` 声明 undefined,实测 defined。Windows 主机的 clang 即使带上 `--target=
riscv64-none-elf` 仍把 `_WIN32`(及 `__MINGW*__` 一族)注入预处理器的输出——hosted
三元组的 `--target=` 替换会改写主机宏,freestanding 不会。
- `__SIZEOF_WCHAR_T__` 声明 32,实测 16。`cenv::realise` 之前对 freestanding 的 wchar
默认值做了一个未经实测的假设:工具链默认就是 32 位,因此 `wchar = 32` 的声明在
freestanding 上不补 `-fno-short-wchar`。Linux/macOS 主机下这个假设恰好成立,Windows
主机下不成立——clang 用 MinGW 的 `<winnt.h>` 默认,即使 `--target=riscv64-none-elf`
也给出 16 位。

两条都是测量侧缺陷:探针应当读到的是「引擎根据声明生成的编译命令实际产生什么」,而不是
「引擎的编译命令加上主机泄漏之后产生什么」。这一版分别从两个角度修:

1. **引擎补 `-fno-short-wchar`(即实现逻辑的修正,不是 workaround)。** 既然「freestanding
默认 32 位」的假设是错的,那么正确的策略就是不论宿主是什么、freestanding 与否,
`wchar = 32` 一律加 `-fno-short-wchar`——这样编译产物确实就是 32 位 `wchar_t`,
探针再去量也是 32 位,声明成立。在 Linux/macOS 主机下加这个令牌是冗余的(它们本来就
是 32),但对的结果不变;在 Windows 主机下是必要的。原先的「freestanding 跳过 wchar
令牌」规则由此被取消,在 `mcpp.toolchain.cenv::realise` 中注释清楚。
2. **探针允许调用方剥离主机宏。** `cenv_probe::verify` 增加一个 `hostStripMacros`
参数——一组 `-U<name>` 令牌,在 `-E -dM` 之前前置。Windows 主机下,调用方把这四个
名(`_WIN32`、`_WIN64`、`__MINGW32__`、`__MINGW64__`)传给探针;Linux/macOS 主机下
传空集合。缓存键把这些令牌一起折进去,所以同编译器、同 argv、不同剥离集合不会共享
缓存槽。

新增 `test_cenv_probe.cpp` 中三个测试,分别钉住:剥离后宏不再出现在 dump 里、不同剥
离集合不共享缓存槽、Windows 主机的剥离集合恰好是那四个实测到的名字。
`test_cenv.cpp` 中新增两个测试,分别钉住:`wchar = 32` 在 freestanding 上也产出
`-fno-short-wchar`,`wchar = 16` 在 freestanding 上产出 `-fshort-wchar`——把「
freestanding 跳 wchar 令牌」这条曾经存在过的规则永久挡在回归测试之外。

包侧四处尝试均失败:CI 跳过(workaround,用户拒)、去掉 freestanding 上的 musl 依赖(破坏
libcxx 的 `<__mbstate_t.h>`)、per-target `[c-abi]` override(被解析但未生效——`[target.'cfg(..)'.build]`
的合法键集合是 BuildInputs 的成员,不接受 `c-abi` 块)。这一版把这两条结构性缺陷收进了
引擎里,而不是把它们推到包层。

(`src/toolchain/cenv.cppm`、`src/toolchain/cenv_probe.cppm`、`src/build/prepare.cppm`,
测试 `test_cenv.cpp`(`FreestandingWchar32AlwaysEmitsNoShortWchar`、
`FreestandingWchar16AlwaysEmitsShortWchar`)与 `test_cenv_probe.cpp`(
`AHostStrippedMacroIsAbsentFromTheDump`、
`AStripListDoesNotShareACacheSlotWithAnEmptyStrip`、
`TheWindowsHostStripListNamesExactlyTheFourMeasuredLeaks`)、`modules/versioning/src/version.cppm`、`mcpp.toml`)

## [2026.9.18.2] - 2026-09-18

### 2026.9.18.1 发布几小时内,三个下游仓库的 CI 揭出的三处同形缺陷:声明满足与做不到被当成了一回事
Expand Down
20 changes: 20 additions & 0 deletions docs/22-target-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,26 @@ the build and prints both the declared and the measured values. The result
is cached per configuration (compiler binary identity + exact flags), so a
build that resolves the same configuration twice pays for the probe once.

**Host contamination (mcpp 2026.9.18.3+).** The probe runs on the BUILD
host's clang, not a target-native one, and on a Windows host the driver's
predefines (`_WIN32`, `_WIN64`, `__MINGW32__`, `__MINGW64__`) leak through
`--target=` substitution for a freestanding target the same way the
Cygwin-substituted Windows row's `__CYGWIN__` does NOT — a structural
difference between the hosted and freestanding substitutions that the
verification step's measurement has to compensate for. The probe accepts
a `hostStripMacros` list of `-U<name>` tokens it prepends to its `-dM`
command, and the caller (this layer's `prepare`) supplies exactly the four
Windows-host names on Windows, nothing on Linux or macOS. The matching
defect on `__SIZEOF_WCHAR_T__` (Windows host × freestanding measures 2
where a `wchar = 32` declaration asks for 4) is closed on the realisation
side, not the probe side: `cenv::realise` now ALWAYS emits
`-fno-short-wchar` for `decl.wcharBits = 32`, regardless of what the
host's toolchain would default to, so the probe measures the state the
engine actually produced (32 bits, with the flag) rather than the host's
leak. The "freestanding skips the wchar flag" rule the wave's earlier
versions carried was an unverified assumption about the toolchain default,
and the wave's measurement is what verified it wrong.

**Fingerprint.** The realised environment participates in the build's
fingerprint (`compileFlags`, §92's field 7): two builds whose C library
declares `lp64` and `llp64` compile the same source into objects whose
Expand Down
13 changes: 13 additions & 0 deletions docs/zh/22-target-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,19 @@ c-environment = "platform"
同时打印声明值与实测值。结果按配置(编译器二进制身份 + 最终参数)缓存,同一配置解析两次只
编译一次探针。

**主机污染(mcpp 2026.9.18.3+)。** 探针跑在**构建主机**的 clang 上,而不是目标本地的;
Windows 主机的驱动会预先定义 `_WIN32`、`_WIN64`、`__MINGW32__`、`__MINGW64__`,这些定义会
穿透 `--target=` 替换到达 freestanding 目标——而 hosted 三元组的 `--target=` 替换是不会
让 `_WIN32`/`_WIN64` 漏出来的(那是 Cygwin 那一行已经处理过的事情)。这是 hosted 与
freestanding 替换之间一个结构性差异,核对步骤的测量必须为此做补偿。探针接受一个
`hostStripMacros` 参数,即一组 `-U<name>` 令牌,它在 `-dM` 之前前置;调用方(本层的
`prepare`)在 Windows 主机上恰好传那四个名字,Linux/macOS 主机上传空集合。`__SIZEOF_WCHAR_T__`
那一半(Windows 主机 × freestanding 测出 2,而 `wchar = 32` 的声明要求 4)是在实现侧关的,
不是探针侧:`cenv::realise` 现在对 `decl.wcharBits = 32` **一律**发 `-fno-short-wchar`——
不论宿主工具链的默认值是什么——这样探针测量到的就是引擎实际产出的状态(32 位、令牌在),
而不是主机的泄漏。早先版本里「freestanding 跳过 wchar 令牌」那条规则是一个未实测的关于
工具链默认值的假设,本轮的测量把它证伪了。

**指纹。** 解析出的环境参与构建指纹(`compileFlags`,§92 的第 7 项):C 库声明 `lp64` 与
`llp64` 的两次构建,从同一份源码编译出 `long` 宽度不同的目标文件,因此二者绝不共享输出目录,
也不会复用对方产出的目标文件缓存。
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.18.2"
version = "2026.9.18.3"
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.18.2";
inline constexpr std::string_view MCPP_VERSION = "2026.9.18.3";

} // namespace mcpp
67 changes: 66 additions & 1 deletion src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -10852,6 +10852,68 @@ prepare_build(bool print_fingerprint,
// paths and library search flags do not, and leaving them out
// is what makes this probe cheap AND cacheable across every
// package that shares this build's target side.
//
// `hostStripMacros` — Windows-host leak through `--target=`
// substitution. A freestanding cross compile (`--target=
// riscv64-none-elf`) on a Windows host still sees `_WIN32` (and
// the `__MINGW*__` family) in the preprocessor output: those
// are the HOST driver's predefines, and unlike the `__APPLE__` /
// `__linux__` family on Linux/macOS, the `--target=` substitution
// does NOT strip them for the freestanding target. Without the
// strip, the probe's `-dM` dump records `_WIN32` defined
// regardless of what the realised `[c-abi] presents = "posix"`
// asked for, and the probe fails with a mismatch that is a
// property of the host's driver, not of the declaration being
// checked. The strip is added ONLY when the host is Windows;
// Linux and macOS drivers' defaults do not contaminate
// `--target=` substitutions in the same way, and adding `-U`
// tokens there would have to be defended as harmless rather
// than measured (the wave's measurement caught exactly four
// host-side leaks — listed below — and adding to that set is
// the right way to extend the strip; speculatively unstripping
// everywhere is not).
//
// `hostStripFlags` — host-side wchar leakage on Windows ×
// freestanding. The probe runs `-E -dM -x c++` with no source
// unit and no include path; the `__SIZEOF_WCHAR_T__` it reads
// comes from the toolchain's own defaults, which clang on a
// Windows host sets from `<winnt.h>` (16 bits) even with
// `--target=riscv64-none-elf`. The realisation closes this by
// ALWAYS emitting `-fno-short-wchar` for `decl.wcharBits = 32`
// (`mcpp.toolchain.cenv`'s wchar branch, just rewritten — the
// old "freestanding skips the flag" rule was an unverified
// assumption that the wave's measurement caught as false). So
// when the host is Windows AND the target is freestanding, the
// host-strip set above AND `-ffreestanding` are both needed —
// the former for the preprocessor predefines, the latter so the
// driver does not pick up the Windows CRT's `<wchar.h>` even
// when the build's own include path doesn't carry one. Outside
// that combination the strip is unnecessary: Linux/macOS hosts
// do not leak `_WIN32` through `--target=`, and the wchar fix
// lives in the realisation, not the probe.
std::vector<std::string> hostStripMacros;
std::vector<std::string> hostStripFlags;
if (mcpp::platform::is_windows) {
// The four names measured as leaking through `--target=` on
// a Windows host (2026-09-18, openkal-llvm-runtime#24,
// windows-host × riscv64-none-elf). Adding to this set is
// a measurement-driven change, not a guess; pin new entries
// here with the failing build that named them.
hostStripMacros = {
"-U_WIN32",
"-U_WIN64",
"-U__MINGW32__",
"-U__MINGW64__",
};
if (tt && tt->is_freestanding())
// Windows host's driver would otherwise pull in
// MinGW's `<wchar.h>` even with no source unit, so the
// wchar probe sees 2 instead of the declared 32.
// `-ffreestanding` is what every real compile on a
// freestanding target already adds (`openkal-musl`'s
// `cflags`, mcpp's own freestanding handling).
hostStripFlags.push_back("-ffreestanding");
}
if (tc->cEnvExpectWcharBits != 0 || tc->cEnvExpectLongBytes != 0
|| !tc->cEnvExpectDefined.empty()
|| !tc->cEnvExpectUndefined.empty()) {
Expand All @@ -10860,10 +10922,13 @@ prepare_build(bool print_fingerprint,
probeArgv.push_back(tc->crossTargetFlag);
for (auto& t : tc->cEnvTokens) probeArgv.push_back(t);
for (auto& t : tc->cEnvBuiltinsTokens) probeArgv.push_back(t);
for (auto& t : hostStripFlags) probeArgv.push_back(t);
auto probe = mcpp::toolchain::cenv_probe::verify(
tc->binaryPath, probeArgv,
tc->cEnvExpectWcharBits, tc->cEnvExpectLongBytes,
tc->cEnvExpectDefined, tc->cEnvExpectUndefined);
tc->cEnvExpectDefined, tc->cEnvExpectUndefined,
mcpp::home::cache_root(),
hostStripMacros);
if (!probe) {
refusal::record(refusal::Code::CEnvUnrealisable);
return std::unexpected(probe.error());
Expand Down
59 changes: 51 additions & 8 deletions src/toolchain/cenv.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,19 @@ inline mcpp::targetside::CAbiDataModel triple_native_data_model(
// The `wchar_t` width a target's default triple already has. Measured
// (design §1.4): 16 on Windows (MinGW and Cygwin alike, absent
// `-fno-short-wchar`), 32 everywhere else clang targets (Linux, macOS,
// freestanding ELF).
// hosted ELF/Mach-O).
//
// WHAT IS NOT IN THIS FUNCTION. The freestanding case used to be answered
// "32", on the reasoning that `riscv64-none-elf` etc. measure as 32 bits on
// Linux and macOS hosts. The wave's Windows-host × riscv64-none-elf
// measurement showed that assumption to be wrong: clang on a Windows host
// still uses MinGW's `<winnt.h>` defaults even with `--target=riscv64-none-elf`,
// and `__SIZEOF_WCHAR_T__` measures 2 (16 bits) there. The probe correctly
// flagged this as a mismatch with `decl.wcharBits = 32`. The fix is in the
// `wchar` realisation below — always add `-fno-short-wchar` when the
// declaration asks for 32, regardless of what the toolchain would have
// defaulted to — so the probe then measures the state the engine actually
// produced, not the host's leak.
inline int native_wchar_bits(std::string_view os) {
return os == "windows" ? 16 : 32;
}
Expand Down Expand Up @@ -362,14 +374,45 @@ inline std::expected<Realisation, std::string> realise(
}

// ── `wchar` ──────────────────────────────────────────────────────────────
//
// The freestanding case used to be "assumed native, no token added".
// The wave's Windows-host × riscv64-none-elf measurement (openkal-
// llvm-runtime#24, 2026-09-18) caught that assumption as wrong:
// clang on a Windows host uses MinGW's `<winnt.h>` defaults even with
// `--target=riscv64-none-elf`, so the toolchain's own default for
// `wchar_t` is 16 bits there — and a `decl.wcharBits = 32` (musl's
// declaration) would compile to a 16-bit `wchar_t` because no token
// was added. The probe caught this; the right fix is here, not in the
// probe: a freestanding target ALWAYS gets `-fno-short-wchar` for
// `wchar=32`, so the compiler produces 32-bit `wchar_t` regardless of
// what its host-contaminated default would have been. Symmetric for
// `wchar=16`: a freestanding target's toolchain default on Linux/macOS
// is 32, so `-fshort-wchar` is always needed too. The hosted cases
// are unchanged — `native_wchar_bits(os)` is correct for every hosted
// target, because the host's toolchain default is what the real
// compile actually sees (Windows host has `-fshort-wchar` baked in
// via MinGW headers; Linux/macOS hosts have 32 bits).
//
// The probe below then measures what this engine actually produced
// (32 bits on a freestanding target, with the flag) — not the host's
// leak — and the declaration holds. There is no measurement-side
// workaround here: the previous version had a "freestanding target
// skips the wchar flag" rule that was wrong on Windows hosts, and
// adding it costs nothing on Linux/macOS hosts (they default to 32,
// so the flag is redundant but harmless on the hosted freestanding
// builds that don't exist; for hosted Linux/macOS, no flag is added).
if (decl.hasWchar) {
const int native = freestanding ? 32 : native_wchar_bits(os);
if (decl.wcharBits != native) {
// One clang pair, relative to whatever the triple in force
// (possibly already substituted above) would otherwise give:
// `-fshort-wchar` for 16, `-fno-short-wchar` for 32. Measured on
// the Cygwin triple (design §1.4): default 16, `-fno-short-wchar`
// gives 32.
if (freestanding) {
// Freestanding: the toolchain's host-contaminated default is
// not something this engine can trust. Always emit the
// token that makes the compile match the declaration.
r.tokens.push_back(decl.wcharBits == 32 ? "-fno-short-wchar"
: "-fshort-wchar");
} else if (decl.wcharBits != native_wchar_bits(os)) {
// Hosted: the target's own default is what the compile sees,
// and `native_wchar_bits(os)` correctly captures it
// (16 on Windows because MinGW headers bake in
// `-fshort-wchar`; 32 on Linux and macOS).
r.tokens.push_back(decl.wcharBits == 32 ? "-fno-short-wchar"
: "-fshort-wchar");
}
Expand Down
Loading
Loading