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
387 changes: 387 additions & 0 deletions .agents/docs/2026-08-02-issue336-pr142-analysis.md

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [2026.8.3.1] — 2026-08-03

### 修复

- **`[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++,而文档一直在承诺它可以。

- **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++ 上用户根本写不出来。

修法是让静态链接恢复动态链接本来就有的保证:macOS + 自包含时,mcpp 生成一个极小的 C 翻译单元并把它的对象排在链接行**最前**,由它先把流顶上去。它对 `ios_base::Init::Init()` 的引用是 **weak** 的 —— 换一份不这么拼这个 ABI 符号的工具链,链接与今天完全一样,shim 退化为空操作。

### 新增

- **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`)。

这个字段替换的旧字段名描述的是**手段**("静态链接 stdlib"),而它承载的其实是**意图**(产物能在哪些机器上跑)—— 这正是同一个 `true` 在四种配置上展开成四种不同结果的原因,其中一种是**静默空转**:Linux + clang/libc++ 工具链上 `static_stdlib = true` 一个 flag 都不发,交付的是工具链耦合的产物,而 manifest、文档和 `--version` 都说它是自包含的。

- **Linux + libc++ 工具链现在真的能自包含**:显式链入 `libc++.a` / `libc++abi.a` / `libunwind.a`(缺 libunwind.a 时产物仍会拉 `libunwind.so.1`,所以它是机制的一部分而不是可选项)。实测 `NEEDED` 只剩 libc / libm / loader。

- **兑现不了的契约一定会被报出来。** 工具链不带 `libc++.a`、macOS 没有 deployment floor、MSVC 运行时没有 `/MT` 机制、macOS 上没有可用的 `toolchain-coupled` 形态(LLVM 的 libc++abi/libunwind dylib 向上链 `/usr/lib/libc++`,会把第二份 libc++ 载进进程)—— 这些格子现在都会打印实际退到了哪一档。

### 变更

- **五处独立推导收敛成一处。** "这个产物自带 C++ 运行时吗"过去在 `flags.cppm` 的 `ldStdlibDefault` / `ldStdlibTest` / `-static-libstdc++` / MinGW `-static` 四处,加上 `ninja_backend.cppm` 里那个按 `LinkUnit::TestBinary` 的二分派,各推一遍 —— 这正是新语义只落到其中一处的成因。现在是 `src/build/distribution.cppm` 里的三层模型:角色(由链接单元内在决定)→ 契约(按角色取默认,可覆盖)→ 机制(唯一放 flag 的地方,且是**总函数**)。

- 相应地,C++ 运行时相关的链接 flag 从全局 `ldflags` 移到了**每个链接单元**的 `unit_ldflags` —— 两个角色在同一次构建里可以持有不同契约,这一点全局通道表达不了。它们都是驱动级 flag,相对库的位置无意义,Linux/Windows 的链接语义不变。

## [2026.8.1.2] — 2026-08-01

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

**Static runtime by default (portable by default)**: when `static_stdlib = true`
(the default), macOS linking statically links in LLVM's bundled libc++/libc++abi —
the system libc++ would otherwise pin the actual runnable version to the build
machine's OS (older systems lack newer symbols, e.g. the support symbols behind
`std::print`), and only static linking can truly deliver the floor. As a result,
the default build's artifacts work out of the box on any macOS ≥ 14. Set
`static_stdlib = false` to fall back to the dynamic system libc++ (the artifact is
then only guaranteed to run on the build machine's version and above). A lower
floor (11–13) requires a self-built libc++ archive (already verified to work, a
data-level switch, available on request).
### The C++ runtime contract (`cxx_runtime`)

`cxx_runtime` states what the produced artifact promises about the machine that
runs it. It is a **distribution** property, not a build one — it describes the
runtime dependency set, and the flags that deliver it differ per platform.

```toml
[build]
cxx_runtime = "self-contained" # applies to every target (the default)

# or, per role:
[build.cxx_runtime]
default = "self-contained" # binaries and shared libraries
tests = "host-coupled" # test binaries never leave this machine

# or, per target triple — beside `linkage`, which is the same axis:
[target.x86_64-linux-gnu]
cxx_runtime = "host-coupled" # e.g. this build is for a distro package
```

| value | the artifact needs, at run time | typical use |
|---|---|---|
| `self-contained` (default) | no C++ runtime outside itself | shipping a binary |
| `toolchain-coupled` | the C++ runtime of the toolchain mcpp installed | local iteration |
| `host-coupled` | whatever the driver resolves by default (the system runtime) | distro packaging, `dlopen` plugins that must share a runtime with their host |

**Self-contained by default (portable by default)**: on macOS this statically
links LLVM's bundled libc++/libc++abi — the system libc++ would otherwise pin the
runnable version to the build machine's OS (older systems lack newer symbols, e.g.
the support symbols behind `std::print`), and only static linking can truly deliver
the `macos_deployment_target` floor. On Linux/MinGW it is `-static-libstdc++` (GCC)
or the whole-link `-static` (MinGW); on a Linux clang/libc++ toolchain it links
libc++.a/libc++abi.a/libunwind.a explicitly. A lower macOS floor (11–13) requires a
self-built libc++ archive (already verified to work, a data-level switch, available
on request).

`static_stdlib` is the older spelling and still works: `true` means
`self-contained`, `false` means `host-coupled`. An explicit `cxx_runtime` wins.

**A contract that cannot be honored is reported, never silently downgraded.** If a
toolchain ships no `libc++.a`, or a contract has no mechanism on that platform
(`self-contained` under the MSVC runtime would need `/MT`, which mcpp does not emit
yet), the build prints what it fell back to instead of quietly producing a
different artifact than the manifest asked for.

**Scope.** The contract governs the C++ runtime only. Static **libc** is a separate
axis (`linkage = "static"` / `--static`, e.g. a musl target), and the deployment
floor is a third (`macos_deployment_target`). Also, `host-coupled` means mcpp adds
nothing to embed a C++ runtime; it does not strip the toolchain rpath the link
carries for other reasons, so on ELF such an artifact may still find the
toolchain's libraries first.

> **macOS + `self-contained` and static initialization order.** Mach-O has no
> priority-ordered initializer section and libc++'s `<iostream>` carries no
> `ios_base::Init` guard of its own (unlike libstdc++ and the MSVC STL), so a
> stream initializer pulled out of `libc++.a` would otherwise run *after* the
> program's own global constructors — a global whose constructor touches
> `std::cout` would read an unconstructed stream and crash at process start. mcpp
> links a tiny generated object first to force the streams up; nothing is required
> of your code. See mcpp-community/mcpp#336.

`defines` takes **bare** macro names (no `-D`) and desugars each entry to `-D<x>` on
both the C and C++ compile channels. It reaches every TU in the package — module
Expand Down
62 changes: 54 additions & 8 deletions docs/zh/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ cflags = ["-DFOO=1"] # 额外 C 编译参数
cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...)
ldflags = ["-lfoo"] # 额外链接参数
defines = ["BIZ=1", "QUX"] # 作用于每个 TU 的预处理宏(脱糖为 -D;会进入模块扫描)
static_stdlib = true # 静态链接 libstdc++(默认 true)
cxx_runtime = "self-contained" # C++ 运行时契约(见下节);static_stdlib 是旧拼写
macos_deployment_target = "14.0" # macOS 产物的最低支持系统版本(仅 macOS 生效)
cache = "global" # 依赖的全局构建缓存:global(默认)| local | off(见 §2.10)
```
Expand All @@ -174,13 +174,59 @@ cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftP
14.0 即 LLVM 官方静态库自身的下限)。该值会进入 BMI 指纹——切换 target
会自动重建模块缓存。

**默认即静态运行时(portable by default)**:`static_stdlib = true`
(默认)时,macOS 链接会静态链入 LLVM 自带的 libc++/libc++abi ——
系统 libc++ 会把实际可运行版本钉死在构建机的 OS(老系统缺新符号,
如 `std::print` 的支撑符号),静态化才能真正兑现 floor。因此默认构建的
产物在任何 macOS ≥ 14 上开箱即用。设 `static_stdlib = false` 退回动态
系统 libc++(产物只保证在构建机同版本及以上运行)。更低 floor(11–13)
需自建 libc++ 归档(已验证可行,数据级切换,按需提供)。
### C++ 运行时契约(`cxx_runtime`)

`cxx_runtime` 声明的是**产物对运行它的机器做出的承诺**。它是**分发**属性而非
构建属性 —— 它描述的是运行期依赖集,而兑现它的 flag 逐平台不同。

```toml
[build]
cxx_runtime = "self-contained" # 作用于所有目标(默认值)

# 或者按角色分别指定:
[build.cxx_runtime]
default = "self-contained" # 可执行文件与共享库
tests = "host-coupled" # 测试二进制从不离开本机

# 或者按目标三元组 —— 与 `linkage` 并列,因为它们是同一根轴:
[target.x86_64-linux-gnu]
cxx_runtime = "host-coupled" # 例如这次构建是为发行版打包
```

| 取值 | 产物运行时需要 | 典型场景 |
|---|---|---|
| `self-contained`(默认) | 自身之外不需要任何 C++ 运行时 | 分发二进制 |
| `toolchain-coupled` | mcpp 装的那份工具链的 C++ 运行时 | 本地迭代 |
| `host-coupled` | 驱动默认解析到的那份(通常是系统运行时) | 发行版打包;必须与宿主共用同一份运行时的 `dlopen` 插件 |

**默认即自包含(portable by default)**:macOS 上这会静态链入 LLVM 自带的
libc++/libc++abi —— 系统 libc++ 会把实际可运行版本钉死在构建机的 OS(老系统
缺新符号,如 `std::print` 的支撑符号),只有静态化才能真正兑现
`macos_deployment_target` 的 floor。Linux/MinGW 上它是 `-static-libstdc++`
(GCC)或整条链的 `-static`(MinGW);Linux 上的 clang/libc++ 工具链则显式链入
libc++.a/libc++abi.a/libunwind.a。更低的 macOS floor(11–13)需自建 libc++
归档(已验证可行,数据级切换,按需提供)。

`static_stdlib` 是旧拼写,仍然有效:`true` 等价于 `self-contained`,`false`
等价于 `host-coupled`。显式写了 `cxx_runtime` 时以后者为准。

**兑现不了的契约会被报出来,绝不静默降级。** 若工具链不带 `libc++.a`,或某个
契约在该平台上没有对应机制(MSVC 运行时的 `self-contained` 需要 `/MT`,mcpp
目前不发射),构建会打印实际退到了哪一档,而不是悄悄交付一个与 manifest 所述
不同的产物。

**边界。** 该契约只管 C++ 运行时。静态 **libc** 是另一根轴(`linkage = "static"`
/ `--static`,如 musl 目标),部署下限是第三根轴(`macos_deployment_target`)。
另外,`host-coupled` 只承诺 mcpp 不做任何"把 C++ 运行时打进产物"的动作,它不会
去掉链接因其它原因已经携带的工具链 rpath —— 所以在 ELF 上这类产物仍可能优先
找到工具链的库。

> **macOS + `self-contained` 与静态初始化次序。** Mach-O 没有按优先级排序的
> 初始化段,而 libc++ 的 `<iostream>` 也不像 libstdc++ / MSVC STL 那样自带
> `ios_base::Init` 守卫 —— 于是从 `libc++.a` 里拉出来的流初始化器本来会排在
> 程序自己的全局构造函数**之后**:一个在构造函数里碰 `std::cout` 的全局对象会
> 读到尚未构造的流,进程启动即崩。mcpp 会把一个极小的生成对象排在链接最前面
> 把流顶上去,你的代码不需要做任何事。详见 mcpp-community/mcpp#336。

`defines` 接受**裸**宏名(不带 `-D`),把每个条目脱糖为 `-D<x>`,同时作用于 C 和
C++ 编译通道。它覆盖包内每个 TU(含模块接口单元),因此也会进入 P1689 模块扫描
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.8.2.2"
version = "2026.8.3.1"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
Loading
Loading