Skip to content

Commit c92b4cb

Browse files
committed
fix(build): make the C++ runtime contract one decision (#336)
`[build] static_stdlib = false` was silently ignored for test binaries from 0.0.86 to 2026.8.2.2 while docs/05-mcpp-toml.md kept documenting the opt-out. The cause is structural: "does this artifact carry its own C++ runtime" was derived independently in five places — ldStdlibDefault, ldStdlibTest, the -static-libstdc++ string, the MinGW -static branch, and the LinkUnit::TestBinary two-way switch in the ninja emitter — and #202's new semantics landed in some of them and not the others. Replaced by a three-layer model in src/build/distribution.cppm: Role intrinsic to the link unit (test binaries run here and are discarded; archives embed no runtime at all) Contract what the artifact promises about the machine that runs it Mechanism (contract x stdlib x binary format) -> flags, as a TOTAL function Totality is the property that matters: every cell answers, and a cell that cannot honor what was asked returns `degraded` plus a diagnostic the backend must print. That turns three silent downgrades into reported ones — including Linux + clang/libc++, where `static_stdlib = true` emitted no flag at all and shipped a toolchain-coupled artifact while the manifest, the docs and the build output all called it self-contained. It now links libc++.a/libc++abi.a/ libunwind.a for real (NEEDED drops to libc/libm/loader). Also fixes the crash that made the gap visible. On macOS a global object whose constructor touches std::cout SIGSEGVs at process start under the default contract: Mach-O runs __init_offsets in link order and has no priority-ordered init section, so the stream initializer pulled out of libc++.a lands last, and libc++'s <iostream> has no ios_base::Init guard of its own (libstdc++ and the MSVC STL do, which is why only macOS breaks). Nor could package code work around it — std::ios_base::Init is only forward-declared in libc++'s headers, so the standard's own remedy is unavailable there. mcpp now links a generated C object first whose constructor calls ios_base::Init::Init(); the reference is weak, so a toolchain spelling that symbol differently links exactly as before. New surface: [build] cxx_runtime = "self-contained" | "toolchain-coupled" | "host-coupled", per role via { default, tests } and per triple via [target.<triple>].cxx_runtime — beside `linkage`, which is the same axis. static_stdlib stays a faithful alias. Analysis: .agents/docs/2026-08-02-issue336-pr142-analysis.md Unblocks: mcpplibs/mcpp-index#142
1 parent 76152d9 commit c92b4cb

14 files changed

Lines changed: 1711 additions & 142 deletions

.agents/docs/2026-08-02-issue336-pr142-analysis.md

Lines changed: 357 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.8.3.1] — 2026-08-03
7+
8+
### 修复
9+
10+
- **`[build] static_stdlib = false` 对测试二进制静默失效(#336)。** #124 在 0.0.52 明确写下这个 opt-out,`docs/05-mcpp-toml.md` 至今也还这么写;但 #202(0.0.86)把测试二进制改成与分发目标相同的静态 `-load_hidden` libc++ 时,新增的那条推导**没有带上 `staticStdlib`**。结果是约一年时间里 macOS 上的 `mcpp test` 没有任何办法回到动态 libc++,而文档一直在承诺它可以。
11+
12+
- **macOS 上全局对象在静态初始化期访问 `std::cout` 必崩(#336)。** Mach-O 没有按优先级排序的初始化段(`init_priority` 只在单个 TU 内有效),归档成员的初始化器按链接顺序排在最后;而 libc++ 的 `<iostream>` 不像 libstdc++ / MSVC STL 那样自带 `ios_base::Init` 守卫,流的构造只存在于库内对象里。两件事叠起来的后果是:默认配置下,任何在构造函数里碰 `std::cout` 的全局对象都会读到 vptr 为零的流,进程启动即 SIGSEGV —— 而且**包侧无法修复**,因为 `std::ios_base::Init` 在 libc++ 的头文件里只有前向声明(`ios:70`),标准为静态初始化次序提供的官方解药在 libc++ 上用户根本写不出来。
13+
14+
修法是让静态链接恢复动态链接本来就有的保证:macOS + 自包含时,mcpp 生成一个极小的 C 翻译单元并把它的对象排在链接行**最前**,由它先把流顶上去。它对 `ios_base::Init::Init()` 的引用是 **weak** 的 —— 换一份不这么拼这个 ABI 符号的工具链,链接与今天完全一样,shim 退化为空操作。
15+
16+
### 新增
17+
18+
- **C++ 运行时分发契约 `[build] cxx_runtime`** 三档:`self-contained`(默认)/ `toolchain-coupled` / `host-coupled`,可按角色(`{ default = ..., tests = ... }`)也可按目标三元组(`[target.<triple>] cxx_runtime`,与 `linkage` 并列 —— 它们本就是同一根轴)。`static_stdlib` 保留为忠实别名(`true``self-contained`,`false``host-coupled`)。
19+
20+
这个字段替换的旧字段名描述的是**手段**("静态链接 stdlib"),而它承载的其实是**意图**(产物能在哪些机器上跑)—— 这正是同一个 `true` 在四种配置上展开成四种不同结果的原因,其中一种是**静默空转**:Linux + clang/libc++ 工具链上 `static_stdlib = true` 一个 flag 都不发,交付的是工具链耦合的产物,而 manifest、文档和 `--version` 都说它是自包含的。
21+
22+
- **Linux + libc++ 工具链现在真的能自包含**:显式链入 `libc++.a` / `libc++abi.a` / `libunwind.a`(缺 libunwind.a 时产物仍会拉 `libunwind.so.1`,所以它是机制的一部分而不是可选项)。实测 `NEEDED` 只剩 libc / libm / loader。
23+
24+
- **兑现不了的契约一定会被报出来。** 工具链不带 `libc++.a`、macOS 没有 deployment floor、MSVC 运行时没有 `/MT` 机制、macOS 上没有可用的 `toolchain-coupled` 形态(LLVM 的 libc++abi/libunwind dylib 向上链 `/usr/lib/libc++`,会把第二份 libc++ 载进进程)—— 这些格子现在都会打印实际退到了哪一档。
25+
26+
### 变更
27+
28+
- **五处独立推导收敛成一处。** "这个产物自带 C++ 运行时吗"过去在 `flags.cppm``ldStdlibDefault` / `ldStdlibTest` / `-static-libstdc++` / MinGW `-static` 四处,加上 `ninja_backend.cppm` 里那个按 `LinkUnit::TestBinary` 的二分派,各推一遍 —— 这正是新语义只落到其中一处的成因。现在是 `src/build/distribution.cppm` 里的三层模型:角色(由链接单元内在决定)→ 契约(按角色取默认,可覆盖)→ 机制(唯一放 flag 的地方,且是**总函数**)。
29+
30+
- 相应地,C++ 运行时相关的链接 flag 从全局 `ldflags` 移到了**每个链接单元**`unit_ldflags` —— 两个角色在同一次构建里可以持有不同契约,这一点全局通道表达不了。它们都是驱动级 flag,相对库的位置无意义,Linux/Windows 的链接语义不变。
31+
632
## [2026.8.1.2] — 2026-08-01
733

834
### 新增

docs/05-mcpp-toml.md

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ cflags = ["-DFOO=1"] # Extra C compile flags
157157
cxxflags = ["-DBAR=2"] # Extra C++ compile flags (do not put -std=... here)
158158
ldflags = ["-lfoo"] # Extra link flags
159159
defines = ["BIZ=1", "QUX"] # Preprocessor macros for every TU (desugars to -D; reaches module scans)
160-
static_stdlib = true # Statically link libstdc++ (default true)
160+
cxx_runtime = "self-contained" # C++ runtime contract (§ below); static_stdlib is the old spelling
161161
target = "x86_64-linux-musl" # Default build target when no --target is passed
162162
# (≙ cargo build.target; e.g. "ship fully-static")
163163
macos_deployment_target = "14.0" # Minimum supported OS version for macOS artifacts (macOS only)
@@ -188,16 +188,66 @@ baseline, and 14.0 is the floor of LLVM's official static libraries themselves).
188188
This value enters the BMI fingerprint, so switching targets automatically rebuilds
189189
the module cache.
190190

191-
**Static runtime by default (portable by default)**: when `static_stdlib = true`
192-
(the default), macOS linking statically links in LLVM's bundled libc++/libc++abi —
193-
the system libc++ would otherwise pin the actual runnable version to the build
194-
machine's OS (older systems lack newer symbols, e.g. the support symbols behind
195-
`std::print`), and only static linking can truly deliver the floor. As a result,
196-
the default build's artifacts work out of the box on any macOS ≥ 14. Set
197-
`static_stdlib = false` to fall back to the dynamic system libc++ (the artifact is
198-
then only guaranteed to run on the build machine's version and above). A lower
199-
floor (11–13) requires a self-built libc++ archive (already verified to work, a
200-
data-level switch, available on request).
191+
### The C++ runtime contract (`cxx_runtime`)
192+
193+
`cxx_runtime` states what the produced artifact promises about the machine that
194+
runs it. It is a **distribution** property, not a build one — it describes the
195+
runtime dependency set, and the flags that deliver it differ per platform.
196+
197+
```toml
198+
[build]
199+
cxx_runtime = "self-contained" # applies to every target (the default)
200+
201+
# or, per role:
202+
[build.cxx_runtime]
203+
default = "self-contained" # binaries and shared libraries
204+
tests = "host-coupled" # test binaries never leave this machine
205+
206+
# or, per target triple — beside `linkage`, which is the same axis:
207+
[target.x86_64-linux-gnu]
208+
cxx_runtime = "host-coupled" # e.g. this build is for a distro package
209+
```
210+
211+
| value | the artifact needs, at run time | typical use |
212+
|---|---|---|
213+
| `self-contained` (default) | no C++ runtime outside itself | shipping a binary |
214+
| `toolchain-coupled` | the C++ runtime of the toolchain mcpp installed | local iteration |
215+
| `host-coupled` | whatever the driver resolves by default (the system runtime) | distro packaging, `dlopen` plugins that must share a runtime with their host |
216+
217+
**Self-contained by default (portable by default)**: on macOS this statically
218+
links LLVM's bundled libc++/libc++abi — the system libc++ would otherwise pin the
219+
runnable version to the build machine's OS (older systems lack newer symbols, e.g.
220+
the support symbols behind `std::print`), and only static linking can truly deliver
221+
the `macos_deployment_target` floor. On Linux/MinGW it is `-static-libstdc++` (GCC)
222+
or the whole-link `-static` (MinGW); on a Linux clang/libc++ toolchain it links
223+
libc++.a/libc++abi.a/libunwind.a explicitly. A lower macOS floor (11–13) requires a
224+
self-built libc++ archive (already verified to work, a data-level switch, available
225+
on request).
226+
227+
`static_stdlib` is the older spelling and still works: `true` means
228+
`self-contained`, `false` means `host-coupled`. An explicit `cxx_runtime` wins.
229+
230+
**A contract that cannot be honored is reported, never silently downgraded.** If a
231+
toolchain ships no `libc++.a`, or a contract has no mechanism on that platform
232+
(`self-contained` under the MSVC runtime would need `/MT`, which mcpp does not emit
233+
yet), the build prints what it fell back to instead of quietly producing a
234+
different artifact than the manifest asked for.
235+
236+
**Scope.** The contract governs the C++ runtime only. Static **libc** is a separate
237+
axis (`linkage = "static"` / `--static`, e.g. a musl target), and the deployment
238+
floor is a third (`macos_deployment_target`). Also, `host-coupled` means mcpp adds
239+
nothing to embed a C++ runtime; it does not strip the toolchain rpath the link
240+
carries for other reasons, so on ELF such an artifact may still find the
241+
toolchain's libraries first.
242+
243+
> **macOS + `self-contained` and static initialization order.** Mach-O has no
244+
> priority-ordered initializer section and libc++'s `<iostream>` carries no
245+
> `ios_base::Init` guard of its own (unlike libstdc++ and the MSVC STL), so a
246+
> stream initializer pulled out of `libc++.a` would otherwise run *after* the
247+
> program's own global constructors — a global whose constructor touches
248+
> `std::cout` would read an unconstructed stream and crash at process start. mcpp
249+
> links a tiny generated object first to force the streams up; nothing is required
250+
> of your code. See mcpp-community/mcpp#336.
201251
202252
`defines` takes **bare** macro names (no `-D`) and desugars each entry to `-D<x>` on
203253
both the C and C++ compile channels. It reaches every TU in the package — module

docs/zh/05-mcpp-toml.md

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ cflags = ["-DFOO=1"] # 额外 C 编译参数
151151
cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...)
152152
ldflags = ["-lfoo"] # 额外链接参数
153153
defines = ["BIZ=1", "QUX"] # 作用于每个 TU 的预处理宏(脱糖为 -D;会进入模块扫描)
154-
static_stdlib = true # 静态链接 libstdc++(默认 true)
154+
cxx_runtime = "self-contained" # C++ 运行时契约(见下节);static_stdlib 是旧拼写
155155
macos_deployment_target = "14.0" # macOS 产物的最低支持系统版本(仅 macOS 生效)
156156
cache = "global" # 依赖的全局构建缓存:global(默认)| local | off(见 §2.10)
157157
```
@@ -174,13 +174,59 @@ cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftP
174174
14.0 即 LLVM 官方静态库自身的下限)。该值会进入 BMI 指纹——切换 target
175175
会自动重建模块缓存。
176176

177-
**默认即静态运行时(portable by default)**:`static_stdlib = true`
178-
(默认)时,macOS 链接会静态链入 LLVM 自带的 libc++/libc++abi ——
179-
系统 libc++ 会把实际可运行版本钉死在构建机的 OS(老系统缺新符号,
180-
`std::print` 的支撑符号),静态化才能真正兑现 floor。因此默认构建的
181-
产物在任何 macOS ≥ 14 上开箱即用。设 `static_stdlib = false` 退回动态
182-
系统 libc++(产物只保证在构建机同版本及以上运行)。更低 floor(11–13)
183-
需自建 libc++ 归档(已验证可行,数据级切换,按需提供)。
177+
### C++ 运行时契约(`cxx_runtime`)
178+
179+
`cxx_runtime` 声明的是**产物对运行它的机器做出的承诺**。它是**分发**属性而非
180+
构建属性 —— 它描述的是运行期依赖集,而兑现它的 flag 逐平台不同。
181+
182+
```toml
183+
[build]
184+
cxx_runtime = "self-contained" # 作用于所有目标(默认值)
185+
186+
# 或者按角色分别指定:
187+
[build.cxx_runtime]
188+
default = "self-contained" # 可执行文件与共享库
189+
tests = "host-coupled" # 测试二进制从不离开本机
190+
191+
# 或者按目标三元组 —— 与 `linkage` 并列,因为它们是同一根轴:
192+
[target.x86_64-linux-gnu]
193+
cxx_runtime = "host-coupled" # 例如这次构建是为发行版打包
194+
```
195+
196+
| 取值 | 产物运行时需要 | 典型场景 |
197+
|---|---|---|
198+
| `self-contained`(默认) | 自身之外不需要任何 C++ 运行时 | 分发二进制 |
199+
| `toolchain-coupled` | mcpp 装的那份工具链的 C++ 运行时 | 本地迭代 |
200+
| `host-coupled` | 驱动默认解析到的那份(通常是系统运行时) | 发行版打包;必须与宿主共用同一份运行时的 `dlopen` 插件 |
201+
202+
**默认即自包含(portable by default)**:macOS 上这会静态链入 LLVM 自带的
203+
libc++/libc++abi —— 系统 libc++ 会把实际可运行版本钉死在构建机的 OS(老系统
204+
缺新符号,如 `std::print` 的支撑符号),只有静态化才能真正兑现
205+
`macos_deployment_target` 的 floor。Linux/MinGW 上它是 `-static-libstdc++`
206+
(GCC)或整条链的 `-static`(MinGW);Linux 上的 clang/libc++ 工具链则显式链入
207+
libc++.a/libc++abi.a/libunwind.a。更低的 macOS floor(11–13)需自建 libc++
208+
归档(已验证可行,数据级切换,按需提供)。
209+
210+
`static_stdlib` 是旧拼写,仍然有效:`true` 等价于 `self-contained`,`false`
211+
等价于 `host-coupled`。显式写了 `cxx_runtime` 时以后者为准。
212+
213+
**兑现不了的契约会被报出来,绝不静默降级。** 若工具链不带 `libc++.a`,或某个
214+
契约在该平台上没有对应机制(MSVC 运行时的 `self-contained` 需要 `/MT`,mcpp
215+
目前不发射),构建会打印实际退到了哪一档,而不是悄悄交付一个与 manifest 所述
216+
不同的产物。
217+
218+
**边界。** 该契约只管 C++ 运行时。静态 **libc** 是另一根轴(`linkage = "static"`
219+
/ `--static`,如 musl 目标),部署下限是第三根轴(`macos_deployment_target`)。
220+
另外,`host-coupled` 只承诺 mcpp 不做任何"把 C++ 运行时打进产物"的动作,它不会
221+
去掉链接因其它原因已经携带的工具链 rpath —— 所以在 ELF 上这类产物仍可能优先
222+
找到工具链的库。
223+
224+
> **macOS + `self-contained` 与静态初始化次序。** Mach-O 没有按优先级排序的
225+
> 初始化段,而 libc++ 的 `<iostream>` 也不像 libstdc++ / MSVC STL 那样自带
226+
> `ios_base::Init` 守卫 —— 于是从 `libc++.a` 里拉出来的流初始化器本来会排在
227+
> 程序自己的全局构造函数**之后**:一个在构造函数里碰 `std::cout` 的全局对象会
228+
> 读到尚未构造的流,进程启动即崩。mcpp 会把一个极小的生成对象排在链接最前面
229+
> 把流顶上去,你的代码不需要做任何事。详见 mcpp-community/mcpp#336
184230
185231
`defines` 接受****宏名(不带 `-D`),把每个条目脱糖为 `-D<x>`,同时作用于 C 和
186232
C++ 编译通道。它覆盖包内每个 TU(含模块接口单元),因此也会进入 P1689 模块扫描

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.8.2.2"
3+
version = "2026.8.3.1"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

0 commit comments

Comments
 (0)