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
1,749 changes: 1,749 additions & 0 deletions .agents/docs/2026-08-30-issues-527-529-535-537-analysis-and-design.md

Large diffs are not rendered by default.

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

## [2026.8.30.2] — 2026-08-30

六处缺陷,来自 #527 / #529 的分析,外加一处在实现 review 时挖出来、没有人报过的。
它们分属两族:**记录存在而做决定的代码不读它**,以及**运行期搜索路径把「声明的」
和「这台机器碰巧装了的」混在一起**。

完整分析、量化与设计见
[`.agents/docs/2026-08-30-issues-527-529-535-537-analysis-and-design.md`](.agents/docs/2026-08-30-issues-527-529-535-537-analysis-and-design.md)。

> **host 依赖的规则按轴分,而这个分叉是刻意的。** **工具链属于 mcpp 的契约**:
> `import std` 可用、闭包可计算、同一份构建在别的机器和 CI 上一致,都是关于
> 「一个 mcpp 解析得出、叫得出名字的编译器」的陈述,所以 `[toolchain] = "system"`
> 被**明确拒绝**(`msvc` 是唯一例外)。**而程序链接哪些库是程序自己的事**:
> 工程可以链 host 的库或自己的 `.so`,mcpp 说明代价并指出 mcpp-index 那条路,但不拒绝。

### 修复

- **`mcpp test` 每次调用都在重算一份没有变化的答案(#529)。** 两个 post-link ELF
pass 都写了读回优化(stat 没变 ⇒ 复用上次判定),而 `prepare_build` 每次调用都用
一个**全新的 json 对象**重写 `resolution.json` —— 那里面没有这两条记录。于是每次
调用一开始,就把自己后端待会儿要找的备忘录删掉了。

记录改存进 `.mcpp-runtime-verdicts.json`(它本来就活得过 `prepare_build`),
`resolution.json` 继续发布一份副本 —— 这正是 `sync_resolution_verdict` 已有的
形态。同时:
- 剪枝的判据从「不在本次 plan 里」改为「产物已不在磁盘上」。`mcpp build` 与
`mcpp test` 共用一个输出目录而 link unit 集合不同,前一个判据让两条命令互删
对方的记录;
- 记录的失效键纳入 SubOS farm 的 `.xlings.json` 时间戳与 `MCPP_ALLOW_HOST_LIBS`
—— 让备忘录持久化,就产生了一条以前不存在的正确性义务。

实测(10 个 link unit,每个 11.5MB,全热):

| | 之前 | 之后 |
|---|---|---|
| `mcpp build -p <member>` | 0.70s | 0.32s |
| `mcpp test -p <member>`(紧接 build) | 3.15s | **0.36s** |
| `mcpp test -p <member>`(连续) | 1.95s | 0.40s |

- **`path` 依赖里新增一个源文件,fast path 看不见。** 陈旧性扫描只覆盖被构建的那个
工程,于是 `mcpp build` 报 `Finished dev in 0.00s`,而那个模块从没编译过。内容改动
之所以还能被抓到,靠的不是扫描,是 ninja 重链后的事后放弃。这是 #359 那条形态
("glob 输入变了而现存文件的 mtime 一个没动")在它当年没有覆盖到的目录里。
workspace 成员之间就是 `path` 依赖,所以这不是边角情况。

- **`[toolchain] system` 现在被明确拒绝,而不再崩溃(#527 Bug 1)。**
它此前配合 `build.mcpp` 会死在 `posix_spawnp('') failed (error 2)` —— 一条以崩溃形式
出现的"拒绝"不是政策,是穿着政策外衣的 bug。

**mcpp 只用它自己管理的工具链构建。** `PATH` 上的编译器无法被识别、无法被复现,于是
`import std` 可用性、运行期闭包、"同一份构建在另一台机器上"全都不再是 mcpp 能承诺的
东西。拒绝消息给出该写什么、去哪看可选项,并点明 `msvc` 是**唯一例外**
(它点名的是一个族,mcpp 定位其安装),同时说明**host 库是另一条轴,不在拒绝之列**。

- **`standard = 26`(不带引号)被静默忽略。** 键被文档写成字符串,而 `get_string` 对
裸整数返回空,于是工程按默认档位编译、零诊断。#527 自己的三处示例就是这么写的。
两种拼法现在都接受。

### 新增

- **`[workspace.package]` 与 `[workspace.build]`(#527 Bug 2 / RFC 3)。** workspace 根
的 `[build]` 此前完全没有传给成员;现在标量按「成员**声明过**就成员优先」继承,
向量按 workspace 在前追加。

「声明过」是**解析时记录的事实**,不是与默认值比较得出的推断 —— 成员在
`standard = 26` 的 workspace 下刻意写 `standard = "c++23"` 必须保住,而那与默认值
同为一串字节。这正是 cpp20 设计文档 §9-Q3 记下的前置条件。

`allow_host_libs` 明确不可继承:它关掉的是某个具体产物的检查,workspace 根设一次
就等于替所有后来加入的成员也关掉了。`[workspace.package]` / `[workspace.build]` 里
不认识的键会被**拒绝**而不是忽略。

没有 `[workspace.target.<triple>]`:根里普通的 `[target.<triple>]` 本来就按 triple
被成员继承,为同一能力再加一种拼法只增加接口面。

- **依赖声明了高于当前图的标准时会说出来。** C++ 模块图只有一个标准,依赖自己的
`standard` 不生效 —— 这是对的;缺的是它一直不说。degraded 级别(`--strict` 提升),
且**只对工程作者自己拥有的 manifest 生效**:索引里带 mcpp 段的描述符 782 个全都声明了
`language`,其中 756/774 是 `import_std = false` 的 C 库带的样板值,信任「声明过」会
让 c++20 的根工程对着整个索引报警。

- **方言标志没进 `import std` 预编译时,在编译前拒绝。** `[build] cxxflags` 里的
`-fno-exceptions` / `-fno-rtti` 会到达每个 TU 却到不了 std BMI 预编译,于是每个
importer 都在 mcpp 生成的文件里失败,而报错只讲机制不讲那个键。现在提前拒绝并指出
`dialect_cxxflags`。读的是**生效后**的标志集合(`[build]` / `[profile.*]` /
`[target.…]` 都算),并且在图中没有 `import std` 时不触发。

这两个标志仍然**不自动提升**:依赖可以合法地不同意,消费者无权替它决定。

## [2026.8.29.1] — 2026-08-29

构建规则以普通包分发的机制自 2026.8.5.1 就能用,而**规范**一直没有:一个规则包
Expand Down
49 changes: 49 additions & 0 deletions docs/03-toolchains.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,55 @@ Pinned toolsets coexist with each other and with a system Visual Studio.
> that may have been intended. (The family-less `[toolchain] … = "system"` — the PATH
> compiler — is a separate and deliberate escape hatch, and is unaffected.)

### `[toolchain] … = "system"` — refused

**mcpp builds only with toolchains it manages.** A compiler taken from `PATH` is
not supported, and the configuration is refused rather than warned about:

```
error: [toolchain] linux = "system" is not supported: mcpp builds only with
toolchains it manages.
A compiler taken from PATH cannot be identified or reproduced, so
`import std` availability, the runtime closure and "the same build on
another machine" all stop being things mcpp can promise.
Name one instead — mcpp installs it on first use:

[toolchain]
linux = "gcc@16.1.0"

or set a machine default with `mcpp toolchain default gcc@16.1.0`, and
see `mcpp toolchain list` for what is available.
```

`msvc@system` is **the one exception** and is a different spelling: it names a
*family* whose installation mcpp locates and identifies, on the one platform
where the compiler cannot be redistributed. See the section above.

#### Why the toolchain and the libraries get different answers

mcpp's rule about host dependence is not uniform across axes, and the split is
deliberate:

- **mcpp itself, and everything the mcpp ecosystem publishes, depends on no
host.** Toolchains and payloads come through xlings — the xim index or
mcpp-index. This is what makes a build reproducible across machines and Linux
distributions.
- **The toolchain is part of that contract, so it is not the project's to take
from the host.** Everything mcpp promises — `import std` availability, a
computable runtime closure, the same build on a teammate's machine and in CI
— is a statement about a compiler mcpp resolved and can name. A `PATH`
compiler makes all of it unverifiable, which is why this one is a refusal.
- **The libraries a program links are the program's own business.** A project
may link a host library or its own `.so`. mcpp says what that costs and names
the supported route — declare the provider so it resolves from mcpp-index, and
if the index does not carry it yet, contributing the package is the path — but
it does not refuse, as long as the result builds and runs. The developer owns
the artifact and guarantees it.

A build that provably *cannot* run stays an error on either axis: a runtime
closure that cannot be satisfied is refused, because the artifact will not
start. See [binary distribution](12-binary-distribution.md).

### `msvc@system` — the machine's own Visual Studio

mcpp locates and identifies an installed Visual Studio / Build Tools; it never
Expand Down
41 changes: 41 additions & 0 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,47 @@ Two properties worth knowing:
If the sources `import std;` at a level the resolved toolchain does not provide the `std`
module for, mcpp fails before compiling and names both the toolchain and the project level.

Both spellings of the value are accepted: `standard = "c++26"` and `standard = 26`.

When a **dependency declares a level above the graph's**, mcpp says so before compiling
rather than letting it fail somewhere inside that dependency's sources. See
[workspace §4.2](06-workspace.md).

#### Dialect flags and the `import std` BMI

Some flags change what the standard library's headers declare, so the precompiled `import std`
BMI has to be built with them too. That is what `[build] dialect_cxxflags` is for: it is
applied to the std BMI prebuild, the module scan **and** every translation unit in the graph,
including dependencies.

```toml
[build]
dialect_cxxflags = ["-fno-exceptions"]
```

mcpp promotes a few flags into that channel automatically when it finds them in `cxxflags`
(`-freflection`, `-fchar8_t`, `-D_GLIBCXX_USE_CXX11_ABI=…`) — a graph that mixes those is
ill-formed anyway, so no dependency can hold a different opinion about them.

`-fno-exceptions` and `-fno-rtti` are **not** promoted, because a dependency can legitimately
disagree: they remove a language facility the dependency may use, and the consumer cannot make
that choice on its behalf. Left in `cxxflags` they reach every TU and not the prebuild, so the
build cannot succeed — mcpp refuses it before compiling and names the key:

```
error: `-fno-exceptions` changes the language dialect, but the `import std` BMI is
precompiled without it, so every importing translation unit will fail with
"language dialect differs".
Declare it as a dialect flag instead:

[build]
dialect_cxxflags = ["-fno-exceptions"]
```

The check reads the **effective** flags, so it fires for the same flag written in
`[profile.<name>] cxxflags` or in a `[target.…]` block. It does not fire when nothing in the
graph imports `std`, where the flag is an ordinary per-unit option that works.

### 2.2 `[targets.<name>]` — Build Targets

```toml
Expand Down
96 changes: 96 additions & 0 deletions docs/06-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,102 @@ linkage = "static"
default = "llvm@20.1.7"
```

### 4.1 `[workspace.package]` and `[workspace.build]`

Package metadata and build flags shared by every member are declared once at the
workspace root:

```toml
[workspace]
members = ["libs/core", "libs/http", "apps/server"]

[workspace.package]
standard = 26 # or "c++26"; both spellings are accepted
version = "0.4.2"
license = "Apache-2.0"
authors = ["example"]

[workspace.build]
cxxflags = ["-Wall", "-Wextra"]
dialect_cxxflags = ["-fno-exceptions"]
```

A member then declares only what is its own:

```toml
[package]
name = "core"
# standard, version, license and authors are inherited;
# [workspace.build] cxxflags are inherited
```

**The merge rule.**

| kind | rule |
|---|---|
| scalars (`standard`, `version`, `license`, `c_standard`, `linkage`, …) | the member wins **when it declared the key**; otherwise the workspace value applies |
| vectors (`cxxflags`, `ldflags`, `defines`, `dialect_cxxflags`, `include_dirs`, …) | append, **workspace first** — so a member's own flag comes later on the command line, where it wins |
| `[workspace.dependencies]` | explicit opt-in per dependency, `x.workspace = true` (§3) |

"Declared" means the key was written, not that its value differs from the
default. A member that deliberately pins `standard = "c++23"` under a
`[workspace.package] standard = 26` keeps c++23; a member that says nothing gets
c++26. Those two are the same value and opposite intents, which is why the
distinction is recorded rather than inferred.

Scalars and vectors are inherited **implicitly**, without a per-key opt-in. The
drift a workspace exists to prevent is a member that forgot to opt in, so
inheritance is the default and overriding is what has to be stated.
Dependencies keep their explicit opt-in because a dependency is an edge in the
resolution graph: inheriting one implicitly would change what a member resolves
without its own manifest naming it.

**`version` may be omitted by a member** when `[workspace.package]` supplies it.
It remains required overall — a member with neither is refused, naming both the
member and the workspace key that would have supplied it.

**Not everything is inheritable.** `[workspace.build] allow_host_libs` is
refused. It disables the hermetic-link check for a specific artifact, and a
workspace root able to set it once would disable that check for members added
later by someone who never read the root manifest. Keys that describe *how to
build* are inheritable; keys that describe *which safety check not to run* stay
with the package whose artifact it is. Any other unknown key in
`[workspace.package]` / `[workspace.build]` is refused too, rather than ignored:
a key that is silently dropped from a table whose whole purpose is propagation
produces a workspace that looks configured and is not.

**There is no `[workspace.target.<triple>]`.** A plain `[target.<triple>]` block
in the workspace root is already inherited by every member, per triple, with the
member winning. A second spelling for the same capability would be surface with
no function.

### 4.2 One standard for the whole module graph

A C++ module graph has exactly one standard: BMIs are not compatible across
levels, so the root package's `standard` is applied to every package in the
graph, including dependencies. A dependency's own `standard` is not applied.

When a dependency **declares** a level higher than the graph is built at, mcpp
reports it before compiling:

```
warning: dependency `render` declares standard = "c++26", and this graph is
built at c++23
impact: a C++ module graph has one standard, so the dependency's declaration
is not applied and its sources are compiled at the graph's level
hint: raise the consumer's standard to "c++26", or declare it once for
every member:

[workspace.package]
standard = "c++26"
```

This is a warning rather than an error — such a build usually succeeds, and it
is promoted to an error by `--strict`. It is reported only for manifests the
project author controls (the root package, workspace members, and `path`
dependencies): a package resolved from an index carries a `standard` written by
a descriptor generator rather than by the person reading the message.

## 5. Build Commands

### 5.1 Building & testing from the Workspace Root
Expand Down
41 changes: 41 additions & 0 deletions docs/zh/03-toolchains.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,47 @@ pinned toolset 之间、以及与系统 Visual Studio 之间都可以共存。
> `[toolchain] … = "system"` —— 即 PATH 上的编译器 —— 是另一套、也是有意保留的
> 逃生口,不受影响。)

### `[toolchain] … = "system"` —— 拒绝

**mcpp 只用它自己管理的工具链构建。** `PATH` 上现成的编译器不受支持,该配置会被**拒绝**,
而不是提示:

```
error: [toolchain] linux = "system" is not supported: mcpp builds only with
toolchains it manages.
A compiler taken from PATH cannot be identified or reproduced, so
`import std` availability, the runtime closure and "the same build on
another machine" all stop being things mcpp can promise.
Name one instead — mcpp installs it on first use:

[toolchain]
linux = "gcc@16.1.0"

or set a machine default with `mcpp toolchain default gcc@16.1.0`, and
see `mcpp toolchain list` for what is available.
```

`msvc@system` 是**唯一的例外**,而且是另一种拼法:它点名的是一个**族**,mcpp 负责定位并识别
其安装 —— 那是唯一一个编译器不能被重新分发的平台。见上一节。

#### 为什么工具链与库得到的答案不同

mcpp 对 host 依赖的规则并不是各条轴统一的,这个分叉是刻意的:

- **mcpp 自身、以及 mcpp 生态发布的一切,都不依赖任何 host。** 工具链与 payload 都经由
xlings 获得 —— xim 索引或 mcpp-index。这正是构建能跨机器、跨 Linux 发行版复现的原因。
- **工具链属于这份契约,所以它不是工程可以从 host 拿的东西。** mcpp 承诺的每一件事 ——
`import std` 可用、运行期闭包可计算、同一份构建在同事机器上和 CI 里一致 —— 都是关于
**一个 mcpp 解析出来、叫得出名字的编译器**的陈述。`PATH` 上的编译器让这些全部无法核验,
这就是这一条是拒绝的原因。
- **程序链接哪些库,是程序自己的事。** 工程可以链 host 的库,也可以链自己的 `.so`。mcpp 会
说明这样做的代价,并指出受支持的路径 —— 声明该 provider 让它从 mcpp-index 解析;索引尚未
收录时,**把包贡献进 mcpp-index** 就是那条路 —— 但只要结果能构建、能运行,就不强行拒绝。
产物是开发者的,由他保证。

而"证明跑不起来"的构建在两条轴上都仍然是错误:运行期闭包不可满足时会被拒绝,因为产物根本
起不来。见[二进制分发](12-binary-distribution.md)。

### `msvc@system` —— 机器自己的 Visual Studio

mcpp 只负责定位并识别已安装的 Visual Studio / Build Tools,**从不**安装、
Expand Down
Loading
Loading