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
968 changes: 968 additions & 0 deletions .agents/docs/2026-08-31-issue540-seven-audit-findings.md

Large diffs are not rendered by default.

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

## [2026.9.1.1] — 2026-09-01

#540 的七条审计,加上核验它们时挖出的四条没有人报过的。它们几乎全是同一族:
**mcpp 关于自己说了一句话,而 mcpp 不遵守它。**

完整核验、量化与设计见
[`.agents/docs/2026-08-31-issue540-seven-audit-findings.md`](.agents/docs/2026-08-31-issue540-seven-audit-findings.md)。

> **一条规则写在一处,却由它的一份手抄件来执行。** `kKnownBuildKeys`、
> `kKnownConditionalBuildKeys` 与 xpkg 的 `target_cfg` 列表,都是别处已有机器可读
> 形式(紧挨其上的 `doc->get_*()` 读取点、`BuildInputs` 的成员表)的转录,三份都
> 漂移了 —— 而 `kKnownBuildKeys` 漂移了**两次**,第二次就发生在描述第一次的注释上方
> 八行。代价不是少了一条警告,而是**一条假的警告**:`[build] has unsupported key
> 'std-module' (ignored)`,说的与实际发生的事情正相反。

### 修复

- **`[xlings] deps` 的供给从不检查自己是否成功。**(它的第一个真实受害者是本仓库自己的 e2e 88 —— 见 CHANGELOG 末尾) `xlings::call` 返回
`expected<CallResult, string>`,只要子进程跑起来就处于**值**态 —— 能力自身的状态
在 `CallResult` 里面,因为 xlings 讲完 NDJSON 协议后按设计退 0。调用点只测了
`if (!r)`,于是 xlings 能报出的每一种失败都被读成了成功:

```
$ mcpp build # deps = ["definitely-not-a-real-package"]
Provisioning [xlings] deps (definitely-not-a-real-package)
Finished dev [unoptimized + debuginfo] in 0.12s
```

记号随后把这次假成功变成**永久**的 —— 下一次构建连 `Provisioning` 都不再打印。
#531 自己的注释写着它修的缺陷是「声明看起来被接受了却什么都没做,这是一个配置键
能有的最坏形态」;没人读结果,它的修法重现了那个形态。正确写法就在同一个文件里
——依赖安装路径写的是 `if (r && r->exitCode != 0 && …)`。

- **该路径不认 `MCPP_OFFLINE`,也不认 `MCPP_NO_AUTO_INSTALL`。** 它自称与
`[toolchain]` 平权,而那条先例在任一开关下**硬错**并且报出触发的是哪一个。现在
两个都认,拦的是安装**动作**而不是整块 —— 已供给好的工程仍然离线构建得出来。

- **记号记录的是全局效果,却存在项目里。** 安装落在 registry(刻意如此,原注释说明
了理由),而 `<project>/.mcpp/.xlings-deps.stamp` 记着它。清掉或换掉 `MCPP_HOME`,
项目仍然声称已装;`mcpp clean` 只删 `target/`,也清不掉。改按依赖列表的哈希存进
registry,并且**只在成功时写**。

- **`[build] std-module` / `std-compat-module` / `std-module-flags` 被读取,却被报成
unsupported。** 三个键在 #494 被移入 `[build]` 正是为了让它们可条件化,而
`kKnownBuildKeys` 从未收录 —— 唯一一句关于它们的话说反了。

- **条件轴拒绝 `BuildInputs` 的两个成员。** `std-module-flags`(#494 就是为这条轴
才把它挪上来的)与 `private_include_dirs` —— 后者更严重:xpkg 描述符的
`target_cfg` 块,也就是**同一条轴的另一套语法**,是接受它的。两条列表的消息现在都
由列表本身生成。

- **`[features]` 是唯一一个完全没有 schema 检查的结构化段落。** 把 `include_dirs`
误写进 feature 里会零诊断地构建成功,而同样的错误写在 `[build]` 里会被报出来。

- **`mcpp build --help` / `mcpp test --help` 说默认档位是 release,而它是 dev。**
六处说得对(解析器、它的注释、docs/05、一条 e2e、mcpp 自己的 mcpp.toml、
`mcpp pack --help`),两处说错。`prepare.cppm` 里那条字段注释是没被报告的第三处。

- **`mcpp index update <name>` 承诺按索引筛选,而它只筛项目级索引。** 限制此前只写在
一条注释里 —— 一个只有实现者看得到的地方,从外面看与「这功能坏了」无从区分。

### 新增

- **`[target.'cfg(<层> = "…")'.build]` —— 按已解析目标侧条件化(#494 / #540)。**
docs/14 用一整节记载了这个能力,包括为什么它不能用 feature 选择代替;而
`cfgpred::Ctx` 只由三元组构造,所以每一个这样的段落都被**静默**丢弃,包成功构建
在错误的 C 库配置上。五个层名 `compiler` / `compiler-runtime` / `kernel-abi` /
`c-abi` / `c++-abi` 现在是谓词的键,可与三元组键在 `all`/`any`/`not` 下组合。

⚠️ 层谓词**不能**选择依赖 —— 层是从依赖图解析出来的 —— 这种段落会被报出并忽略,
而不是被静默丢弃。

- **mcpp 不认识的 cfg 键会被报出来。** 求值器过去对未知键返回假,而那与「这一段本就
不该匹配」读数完全相同。词汇表从求值器**导出**而不是被转录 —— 否则这条诊断自己就
会成为本次发布正在修的那第五份手抄件。

- **[`docs/spec/exit-codes.md`](docs/spec/exit-codes.md)(SPEC-003)。** 2026-08-08
的协议设计文档 §R4 把这份契约指派给了 `docs/spec/`,它一直没有写。`docs/11` 那张表
落地的是 usage/internal 的一半;命令**跑了并且失败**时返回的 `1` 既不在表里也不在
别处 —— 而按信封命令划定的那张表**给不出** `4`。

### 变更

- **`c-abi` 层报的是库名,不再是三元组的 env 段。** 二者在 `musl` 上重合,在 `gnu`
上分叉:Linux 上它请求的是 glibc,Windows 上它命名的是工具链的 MinGW 形态,而后者
的 C 运行时是 UCRT。docs/14 一直把实现列作 `glibc`/`musl`/`picolibc`,e2e 296 的
文件头也把它期望的报告写作 `c-abi glibc (payload)`。在这个值只被打印的年代这只是
措辞不一致;它现在是用户书写的谓词值。请求那一侧保留三元组的拼写(规范 §3.4),
两者经 `c_abi_request_satisfied` 比较而非按相等。

- **`mcpp::target_libc()` 的文档改为它实际回答的问题** —— 供给 sysroot 的那个**载荷**
包,而这个值是目标侧解析的一项**输入**。要按已解析的层分支,用层谓词。

## [2026.8.30.2] — 2026-08-30

六处缺陷,来自 #527 / #529 的分析,外加一处在实现 review 时挖出来、没有人报过的。
Expand Down
48 changes: 40 additions & 8 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -1114,16 +1114,31 @@ for arch/env conditions and combinators.
`build` with `cflags` / `cxxflags` / `ldflags` / `sources` (mcpp 0.0.95+ —
conditional source globs, e.g. gating `src/x86/**/*.asm` behind
`cfg(arch = "x86_64")`; `!`-exclusion globs work here too), plus `flags` and
`include_dirs` / `include_dirs_after` (mcpp 0.0.102+).
`include_dirs` / `include_dirs_after` (mcpp 0.0.102+), plus
`private_include_dirs` and `std-module-flags` (mcpp 2026.9.1.1+).
- **What `build` accepts is exactly the set of *additive build inputs*** — the
things that combine by appending and are consumed after the predicate is
evaluated. `linkage`, `target`, and the profile knobs are deliberately not
among them: they are *inputs to* target selection (conditioning `target` on
a predicate evaluated against `target` is circular), or they need
override-rather-than-append semantics.
evaluated, which is the member list of `BuildInputs`. `linkage`, `target`,
and the profile knobs are deliberately not among them: they are *inputs to*
target selection (conditioning `target` on a predicate evaluated against
`target` is circular), or they need override-rather-than-append semantics.
A key outside the set is reported and ignored; the message lists the set it
checked against, so it cannot drift from the check.
- **Evaluated against the resolved target** — the `--target` triple for a cross
build, otherwise the host. So a native Linux build never even *downloads* a
`[target.windows]` dependency.
- **Predicate keys**: `os`, `arch`, `family`, `env` — the triple's coordinates —
and, from mcpp 2026.9.1.1, the five target-side layer names `compiler`,
`compiler-runtime`, `kernel-abi`, `c-abi`, `c++-abi`
([14 — The Target Side](14-target-side.md)). Barewords `linux` / `macos` /
`windows` / `unix` are sugar for the matching `os` / `family` test. A key
outside this set is reported as a schema warning and the section does not
apply — it used to answer false in silence, which is indistinguishable from
a section that correctly did not match.
- **A layer predicate cannot select dependencies.** A layer is resolved *from*
the dependency graph, so a dependency chosen by one would decide the answer
it is asking for. `[target.'cfg(c-abi = "musl")'.dependencies]` is reported
and ignored; the `build` inputs under the same predicate do apply.
- **Precedence**: an exact-triple table wins over a `cfg`/alias table; multiple
matching predicate tables have their flags concatenated. Conditional entries
are appended **after** the unconditional `[build]` ones, so under GNU
Expand Down Expand Up @@ -1178,11 +1193,21 @@ The value is an xpkg reference or the empty string; a bare name is rejected when
the manifest is parsed, because accepting it would install nothing and then fail
much later naming a missing libc.

A build program can ask which C library was resolved: `mcpp::target_libc()`
returns its package name and `mcpp::target_libc_profile()` the sub-directory for
the target's ISA profile. Both are empty on the zero-libc tier. See
A build program can ask which C library **payload** supplies the sysroot:
`mcpp::target_libc()` returns that package's name and
`mcpp::target_libc_profile()` the sub-directory for the target's ISA profile.
Both are empty on the zero-libc tier. See
[13 — Bare-Metal and Freestanding Targets](13-baremetal.md).

⚠️ **That is not the same question as "which C library did the target side
resolve to".** `target_libc()` names the payload mcpp installed, and that value
is an *input* to target-side resolution — a package in the dependency graph can
supply the C library instead, in which case the resolved `c-abi` is not what
this returns. To branch on the resolved layer, use a layer predicate:
`[target.'cfg(c-abi = "musl")'.build]` ([14 — The Target
Side](14-target-side.md)). This paragraph said "which C library was resolved"
until 2026.9.1.1, which was the wrong one of the two.

### 2.7.2 Bare metal (`os = none`) — freestanding targets

`riscv64-none-elf` and `riscv32-none-elf` are targets with no operating system
Expand Down Expand Up @@ -1304,6 +1329,13 @@ simd = { sources = ["src/simd/**"], flags = [
{ glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"] } ] }
```

- **The table form accepts exactly** `implies`, `forward`, `defines`, `sources`,
`flags`, `requires`, `provides`. Anything else is reported as a schema warning
and ignored (mcpp 2026.9.1.1+); `deps` is reported separately as reserved and
points at `[feature-deps.<name>]`. Before that release `[features]` was
the one structured section with no schema check at all, so a misplaced
`include_dirs` inside a feature built successfully with no diagnostic while
the identical mistake in `[build]` was reported.
- `defines` are **bare** macro names (no `-D`); each desugars to `-D<x>` on the
package's own compile when the feature is active — exactly like `[targets.*]
defines`. They are restricted by convention to the package's **own** namespaced
Expand Down
23 changes: 22 additions & 1 deletion docs/11-machine-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,26 @@ A request that does not yet know what it will be given must not write into the
channel the protocol owns. Combined with §1, a client's rule is complete: no
JSON on stdout means "not supported", whatever the reason.

Exit codes:
Exit codes **of the enveloped commands** — the kinds `--protocol-version`
advertises. This table is scoped to them on purpose; a code another command
returns is not in it, and adding one would document something these commands
cannot produce. The full mapping across all of mcpp is
[the exit-code contract](spec/exit-codes.md).

| code | meaning |
|---|---|
| 0 | success |
| 1 | the command ran and failed — see stderr, and `diagnostics` when stdout carries an envelope |
| 2 | usage error — unknown option, unsupported value |
| 70 | internal error (uncaught exception) |
| 127 | unknown command |

⚠️ **`1` can arrive with an envelope on stdout.** `mcpp xpkg parse` reports a
descriptor that violates the name form as JSON *and* exits 1: the document is
the answer, and the exit code says the answer is a rejection. §1 still holds —
parse stdout, do not branch on the code — but a client that treats any non-zero
exit as "no output" will discard a document it was given.

## 4. Effects — what a command does before it prints

An IDE with an untrusted-workspace gate has to decide **before** running.
Expand Down Expand Up @@ -309,6 +320,16 @@ A field was added rather than `cLibrary` renamed or `mode` widened, because §6
promises that fields are added and never removed and that a field's meaning
never changes.

⚠️ **`layers[].interface` changed VALUE for a payload-supplied glibc in
2026.9.1.1** — from `gnu` to `glibc`, and on Windows from `gnu` to `ucrt`. The
field's meaning is unchanged (it still names the implementation), so §6 holds;
what changed is that it stopped reporting the triple's env segment, which is a
request rather than an implementation and is not the name of any C library. The
values are now the ones [14 — The Target Side](14-target-side.md) has always
listed, and a package may compare against them in a `cfg(c-abi = …)` predicate.
A client keying on the literal `gnu` needs updating; `musl`, `picolibc` and
`libSystem` are unaffected.

⭐ **`reason` is a token, not a sentence.** The refusal's message is still
written for a person and still names the target, the rule and the way out — but
a program classifying the outcome reads `reason`:
Expand Down
22 changes: 16 additions & 6 deletions docs/13-baremetal.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,12 +555,22 @@ A board-support package is an ordinary mcpp package. It declares the emulator
it needs under `[xlings] deps`, exports one C++ module for consumers, and emits
its board facts from `build.mcpp`.

⚠️ **A declaration under `[xlings] deps` is not an install trigger.** It is what
lets `mcpp::xpkg_dir` answer *"where did that package land"*; it installs
nothing. A board package listed in the index descriptor's platform `deps` gets
its emulator installed with it, and never meets this. A project that declares
its own — because no board package serves its machines — will find `xpkg_dir`
empty on a clean machine and must say so:
**A declaration under `[xlings] deps` provisions the package on the first
build** (since 2026.8.29). It is also what lets `mcpp::xpkg_dir` answer *"where
did that package land"*. Both halves matter: the same declaration installs the
emulator and tells the build program where it went.

The provisioning is the contract `[toolchain]` has always had — declare it,
mcpp installs it on first use — and it obeys the same two knobs: under
`--offline` / `MCPP_OFFLINE` or `MCPP_NO_AUTO_INSTALL` mcpp refuses instead,
naming the packages so they can be installed out of band.

⚠️ **A build program still must not assume the directory exists.** Provisioning
runs for the package that DECLARES the deps; a build program can be reached
through paths where that has not happened — a dependency of a project that
declares nothing, an environment where the knobs above refused — so
`xpkg_dir` may still come back empty and the program must say so rather than
emitting a broken runner:

```cpp
if (const char* dir = mcpp::xpkg_dir("xim", "qemu-riscv"); dir && *dir) {
Expand Down
34 changes: 31 additions & 3 deletions docs/14-target-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The target side of a build consists of five layers.
| `compiler` | the program that compiles | `llvm`, `gcc`, `msvc` |
| `compiler-runtime` | the compiler's own runtime: integer and floating-point builtins, the unwinder | `compiler-rt` with `libunwind`, `libgcc` |
| `kernel-abi` | the platform interface, or its equivalent | `linux`, `windows`, `darwin`, `openkal` |
| `c-abi` | the C library | `glibc`, `musl`, `picolibc` |
| `c-abi` | the C library | `glibc`, `musl`, `picolibc`, `ucrt`, `libSystem` |
| `c++-abi` | the C++ library and its ABI runtime | `libc++` with `libc++abi`, `libstdc++`, MSVC STL |

### Membership Criteria
Expand Down Expand Up @@ -263,10 +263,38 @@ Requiring a feature selection for this would oblige a project to restate what
the target triple or its dependency graph has already established, and permit
the two statements to disagree.

The predicate keys are the five layer names, and their values are the interface
names in the table at the top of this chapter — the same strings the `Target`
report prints. They combine with the triple keys under `all`/`any`/`not`:

```toml
[target.'cfg(all(linux, c-abi = "musl"))'.build]
cxxflags = ["-D_GNU_SOURCE"]
```

⚠️ **A layer names the library, not the triple's env segment.** They coincide
for `musl` and diverge for `gnu`: on Linux that segment asks for glibc, and on
Windows it names the MinGW flavour of the toolchain, whose C runtime is the same
UCRT the MSVC flavour links. The spelling is `c-abi = "glibc"`, never
`c-abi = "gnu"`; the request, as opposed to the answer, is `env = "gnu"` — a
different question (`docs/spec/target-side.md` §3.4).

⚠️ **`env` and `c-abi` are not interchangeable.** `env` is what the triple
*asked* for; `c-abi` is what the graph and the payload *answered*. An
`openkal-musl` in the dependency graph supplies musl under an `x86_64-linux-gnu`
triple, and only `c-abi` sees that.

These predicates are available in `[build]` sections only. The target side is
resolved after dependency resolution, so a dependency selected by one would
form a cycle. A package whose C libraries require different dependencies is
split per C library, or depends on the union and selects sources in `[build]`.
form a cycle; `[target.'cfg(<layer> = …)'.dependencies]` is reported and
ignored rather than silently dropped. A package whose C libraries require
different dependencies is split per C library, or depends on the union and
selects sources in `[build]`.

A key mcpp does not know — a typo, or a predicate from a newer mcpp — is
reported as a schema warning and the section does not apply. It used to
evaluate to false in silence, which reads exactly like a section that correctly
did not match.

## Diagnostics

Expand Down
15 changes: 12 additions & 3 deletions docs/17-the-project-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,18 @@ gives the full rule.
question does not arise.

Against that: an isolated environment is a directory that has to be created and
populated before the first build, and mcpp will not do it. A project whose
tools are ordinary and whose versions do not matter is better off declaring
nothing and inheriting the machine's.
populated, and the first build pays for it. Since 2026.8.29 mcpp does that
work — a declared `[xlings] deps` is provisioned on first use, and a named
`[xlings] subos` that does not exist yet is created rather than refused — but
the cost is real: the first build on a clean machine downloads and installs
before it compiles anything. A project whose tools are ordinary and whose
versions do not matter is better off declaring nothing and inheriting the
machine's.

Under `--offline` / `MCPP_OFFLINE` or `MCPP_NO_AUTO_INSTALL`, mcpp refuses
instead of installing, and names the packages so they can be provisioned
out of band — the same two knobs `[toolchain]` honours, for the same reason: an
unasked-for download is not something a build decides on a project's behalf.

## 6. What belongs somewhere else

Expand Down
1 change: 1 addition & 0 deletions docs/spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
|---|---|---|---|---|
| [SPEC-001](package-identity.md) | 包身份(`package.namespace` / `package.name`)、`[dependencies]` 选择器与匹配机制 | 评审中 v1.1 | 2026-08-03 | mcpp >= 0.0.106 |
| [SPEC-002](target-side.md) | 目标侧模型与能力声明(`mcpp:` 保留命名空间、五层、三条规则) | 评审中 v1.0 | 2026-08-24 | mcpp >= 2026.8.24.2 |
| [SPEC-003](exit-codes.md) | 退出码契约(分类、语义、稳定性承诺) | 评审中 v1.0 | 2026-09-01 | mcpp >= 2026.9.1.1 |

## 文档约定

Expand Down
Loading
Loading