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
614 changes: 614 additions & 0 deletions .agents/docs/2026-08-26-resolved-but-not-consulted.md

Large diffs are not rendered by default.

54 changes: 53 additions & 1 deletion .github/workflows/ci-target-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ jobs:
command -v jq >/dev/null || { echo "::error::jq is missing on ${{ matrix.host }}"; exit 1; }
fail=0
for t in tests/e2e/295_*.sh tests/e2e/296_*.sh \
tests/e2e/297_*.sh tests/e2e/298_*.sh; do
tests/e2e/297_*.sh tests/e2e/298_*.sh \
tests/e2e/299_*.sh tests/e2e/300_*.sh \
tests/e2e/301_*.sh tests/e2e/302_*.sh \
tests/e2e/303_*.sh; do
echo "=== $t ==="
bash "$t" 2>&1 | tee "$(basename "$t").log" || true
rc=${PIPESTATUS[0]}
Expand Down Expand Up @@ -173,6 +176,55 @@ jobs:
"OK: a convention may be overridden, but not merely removed" \
"llvm is not installed here" || fail=1
fi

# ── 2026.8.26.2: an answer mcpp already had, now used ─────────────
#
# ⭐ 299/300/303 CARRY NO SKIP AT ALL, so they use `check` on every
# host. They read the vocabulary and the query's own document —
# neither depends on which payloads this machine happens to hold, and
# a version of them that skipped anywhere would be a version that
# could skip everywhere.
check 299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh \
"OK: a request that named no C library resolves to a row that exists" || fail=1
check 300_a_registered_family_is_not_reported_unknown.sh \
"OK: a registered family is not reported unknown" || fail=1
# ⚠️ 303's third half stacks a musl c-abi over this host's own target,
# and not every host stacks that — an MSVC-ABI host answers the
# layering question first, correctly, and there is then no
# two-answer document to check. Granted by reason; linux-x86_64 below
# is the denominator that must run the whole file.
if [ "${{ matrix.host }}" = linux-x86_64 ]; then
check 303_the_query_gives_one_answer_for_the_c_library.sh \
"OK: the query gives one answer for the C library" || fail=1
else
check_or_declared_skip 303_the_query_gives_one_answer_for_the_c_library.sh \
"OK: the query gives one answer for the C library" \
"refuses a musl c-abi over its own target" || fail=1
fi

# ⚠️ 301/302 NEED TWO COMPILER FAMILIES, AND THAT IS A PROPERTY OF THE
# MACHINE RATHER THAN OF THE CLAIM. "A requirement that DIFFERS from
# mcpp's own answer is applied" cannot be stated where only one family
# exists — macOS installs llvm only, and aarch64 Linux has no llvm
# payload at all (see 298's note).
#
# ⭐ AND THE DENOMINATOR IS linux-x86_64, WHICH HAS BOTH. Without a
# host required to actually run these, a reason accepted everywhere is
# a test that runs nowhere.
if [ "${{ matrix.host }}" = linux-x86_64 ]; then
check 301_the_graphs_compiler_is_taken_and_nothing_is_written.sh \
"OK: the graph's compiler is taken and nothing is written" || fail=1
check 302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh \
"OK: a stated compiler outranks the graph and two requirements do not stack" || fail=1
else
check_or_declared_skip 301_the_graphs_compiler_is_taken_and_nothing_is_written.sh \
"OK: the graph's compiler is taken and nothing is written" \
"no other family is" || fail=1
# 302's half two needs no second family and always runs; only half
# one is skipped, so the file still reaches its conclusion.
check 302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh \
"OK: a stated compiler outranks the graph and two requirements do not stack" || fail=1
fi
[ "$fail" = 0 ] || exit 1

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

## [2026.8.26.2] — 2026-08-26

已经解析出的答案,没有被用来做决定。完整分析见
[`.agents/docs/2026-08-26-resolved-but-not-consulted.md`](.agents/docs/2026-08-26-resolved-but-not-consulted.md)。

⭐ **这不是 2026.8.25.x 那个「谓词回答了比自己更窄的问题」的家族。** 那一族是判据
问错了;这一族里谓词问对了、答案也算对了,只是那个答案**没有接到决定上**。两条都
是「多存了一个字段而没有多接一根线」,因此读判据时看不出来 —— 只在用户问「你既然
已经知道了,为什么还要我说一遍」时暴露。

### 修复

- **⭐⭐ 依赖声明的编译器被检查,但从未被采纳。**

```
$ cat mcpp.toml
[dependencies]
openkal-llvm-runtime = "0.1.3" # requires = ["mcpp:compiler=llvm"]

$ mcpp build # 全局默认 gcc@16.1.0,而 llvm@22.1.8 已装
error: `openkal-llvm-runtime@0.1.3` requires the compiler to be `llvm`.
Select that compiler … mcpp toolchain default llvm
```

这次拒绝没有换来任何信息:它要求的东西已经在本机,版本也已确定
(`MCPP_TOOLCHAIN=llvm@22.1.8 mcpp build` 用时 1.02s)。付出的代价是让用户改一
处**全局**状态去满足**一个**工程的一条依赖。

真因是位置:`prepare.cppm` 有一个标题写着「the toolchain, resolved now that
the graph exists」的接缝,它扫 `pkg.manifest.provides` 来决定编译器,**却不扫
`requires_`** —— 后者在一千行之后才被收集,只用来否决这个决定。

⭐⭐ **修好之后不写任何东西,而这是位置带来的,不是额外加的开关。**
`resolve_target_toolchain` 只有两个调用点,整个函数体(含首次运行的
安装并持久化分支与全部三处 `write_default_toolchain`)都在图之后。把图的要求
写进 `tcSpec` 的时机早于首次运行分支被求值,于是:

| | 之前 | 现在 |
|---|---|---|
| 已有 gcc 默认的机器 | 拒绝,要求改全局默认 | 装/用 llvm,`config.toml` 不动 |
| 什么都没装的机器 | 装 gcc → 持久化 gcc → 再拒绝 | 首次运行分支根本不进,直接装 llvm |

⭐ 状态行点名是哪个包要求的、顶掉了什么;`why toolchain --format json` 新增
`compiler.chosenBy = {origin, requiredBy, replaced}`,让「为什么是 llvm」不必去
解析那行提示 —— 那正是机器接口存在的理由所要消除的字符串匹配。

拒绝只剩一种局面:工程自己在 `[toolchain]` 或 `[target.X]` 写下了相反的编译器。
⚠️ 那种局面里全局默认与本次构建无关,因此原来那条 `mcpp toolchain default llvm`
的建议**连问题都解决不了**,已改为指向那条陈述本身。两个包要求不同的族则是错误
而不是一次挑选,并同时点名两个包。

- **⭐⭐ tier 闸问的是补全后的身份,而不是请求。**

```
$ mcpp build --target aarch64-linux
error: target 'aarch64-linux-gnu' is registered but not yet supported (planned)
$ mcpp build --target aarch64-linux-musl
Finished dev [unoptimized + debuginfo] in 0.99s
```

`parse` 把缺失的 env 段按词法填成 `gnu`,那是为了让**身份**完整(输出目录、缓存
键),`envExplicit` 就是为记住这个区别而存在的 —— 而 tier 闸不看它。被问的是
「aarch64 的 Linux」,被回答的是「aarch64-linux-**gnu**」,报错还引用了一个用户
从没打过的字符串。

省略了 env 段的请求现在对着词表补全,规则 1(词法默认受支持就用它)排在最前,
因此 `x86_64-linux` 一动不动,而这件事能自己退休。⚠️ **`parse()` 未改**:身份
必须保持词法、全量、与宿主无关。

- **⚠️ `unknown target 'riscv64-linux'` 说的是假话。**

`riscv64-linux-musl` 就在词表里(`planned`)。词法填充产生了一个**完全不存在**
的行,于是一个已登记的目标族被报成未知。而且这条路径没有 `refusal::record`,
`--format json` 把它报成 `reason: "other"`。现在它给出 planned 的诊断并点名
`riscv64-linux-musl`;真正的拼写错误仍报 unknown,但带上了新的
`unknown-target` 记号。

- **⚠️ `why toolchain --format json` 的两个字段互相矛盾。**

`cLibrary` 说 glibc/payload,`layers[].c-abi` 说 musl/graph。产物给出裁决 ——
静态、无解释器、无 `DT_NEEDED`、11 个 openkal 符号 —— glibc 不在里面。两者各自
准确,回答的却是不同的问题,而消费方无从判断该信哪个。新增
`cLibrary.suppliesTarget`;⭐ 是**增字段**而非改名或给 `mode` 加取值,因为
docs/11 §6 承诺字段只增不删、含义永不改变。

- **⚠️⚠️ 而「不写任何东西」需要一个有名字的规则,不只是一个位置。**

`write_default_toolchain` 有三个调用点。首次运行那个的条件是
`!tcSpec.has_value()`,方案确实让它进不去;另外两个 —— Windows 首次运行改道、
MSVC 不可用时的修复 —— 条件不是它,**都可达**。一台没装工具链的 Windows 机器
构建一个要求 llvm 的工程,会把 llvm 写成这台机器的默认值。

⭐ 修法是给规则一个名字:`tc_origin_may_persist(TcOrigin)`,两处都调用,一条单
测陈述它。⚠️ 这条缺陷是**读出来的**:它需要一台没有工具链的 Windows 机器,而
`config.toml` 的 sha256 判据跑在已配好的环境里,两条分支一条都到不了。

### 兼容性

⭐ **没有任何一次原本成功的构建换了行为。** 图声明编译器的情形里,原来的结局是
`check_requirements` 拒绝 —— 也就是说那些构建本来就不成功;`--target aarch64-linux`
与 `riscv64-linux` 原来是拒绝;`x86_64-linux` / `x86_64-windows` / `riscv64-none` /
`aarch64-macos` 的补全结果一字未变(单测逐行遍历整张词表守住这一条)。机器接口只
增字段。`[toolchain] default = "system"`(PATH 编译器这条逃生口)不被替换。

### 判据

- e2e `299`–`303`,全部走 `--format json` 分类而非字符串搜索,并接入
`target matrix` 的第一层 —— **四台构建机各跑一遍**。
- ⭐ `301` 的判据是 `~/.mcpp/config.toml` 的 **sha256**,不是「构建成功」:构建成功
与配置被改写可以同时为真,而那正是这次要消除的行为。
- ⭐ `299` 的第二半是对照 —— `x86_64-linux` 必须仍是 gnu。只测 aarch64 会让「把
linux 的默认整个换成 musl」这种过头实现看起来是对的。
- 单测 `TripleRequest.*` 七条,含一条遍历整张词表的「每个受支持的行都能从它自己的
拼写到达」。

## [2026.8.26.1] — 2026-08-26

写出 `--target` 这个动作,曾被当成「这个构建的系统来自依赖图」。完整分析见
Expand Down
52 changes: 52 additions & 0 deletions docs/03-toolchains.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,58 @@ The pair persists as `[toolchain] default = "gcc@16.1.0"` +
configs with combined spellings like `default = "gcc@15.1.0-musl"` keep
working unchanged.)

### What decides a build's compiler

Five things can name it. They are ranked, and the rank is what makes the two
statements a project can write outrank everything mcpp keeps on its own:

| | source | may mcpp revise it |
|---|---|---|
| 1 | `[target.<triple>] toolchain` in `mcpp.toml` | no |
| 2 | `[toolchain] default` in `mcpp.toml` (or `MCPP_TOOLCHAIN`) | no |
| 3 | `requires = ["mcpp:compiler=<family>"]` from a dependency | — |
| 4 | the target row's pin, when the row's payload supplies the target side | yes |
| 5 | `mcpp toolchain default`, then mcpp's first-run default | yes |

**A dependency may require a compiler family.** A C++ runtime is configured for
one family and records that configuration in the headers it ships, so a package
supplying one states which compiler it was built for. When that requirement
differs from a value at rank 4 or 5 — answers mcpp derived itself — mcpp takes
the required family for that build:

```
$ mcpp build
Resolving toolchain
Resolved llvm@22.1.8 → …/xim-x-llvm/22.1.8/bin/clang++
required by openkal-llvm-runtime@0.1.3 (`requires = ["mcpp:compiler=llvm"]`),
not your gcc@16.1.0 — this project only
```

⭐ **Nothing is written.** Not `~/.mcpp/config.toml`, not the project's
`mcpp.toml`. The requirement is a property of this build, so it applies to this
build; the machine's default stays whatever it was, for every other project.
The version comes from what is already installed — the same resolution
`mcpp toolchain default <family>` performs — and only from the ecosystem's own
pin when nothing of that family is present.

**A compiler the project states is not revised.** At ranks 1–2 the project has
said what it builds with, and a dependency disagreeing is a real contradiction:

```
error: `openkal-llvm-runtime@0.1.3` requires the compiler to be `llvm`.
compiler gcc (16.1.0, payload)
required llvm (required by openkal-llvm-runtime@0.1.3)
This build's compiler is stated in [toolchain] in mcpp.toml, and a compiler
the project states outranks one its dependencies ask for.
Change it to `llvm`, or remove it — with nothing stated, mcpp takes the
compiler the graph requires and changes no configuration to do it.
```

**Two dependencies requiring different families is an error, not a pick.** One
build has one compiler; resolving by graph-traversal order would decide it by an
order the author neither writes nor can predict, and would satisfy one package
while failing the other inside a header.

## Inspecting Toolchain Status

```bash
Expand Down
42 changes: 40 additions & 2 deletions docs/11-machine-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,17 +262,55 @@ It resolves and reports; it does not build. `data` is:
| `requested` | `{target, toolchain}` — what was asked for |
| `status` | `ok` or `refused` |
| `reason` | a refusal token, or `none` |
| `compiler` | `{family, version, driver}` — the driver that would run |
| `compiler` | `{family, version, driver, chosenBy}` — the driver that would run, and why |
| `triple` | `{requested, toolchain, llvm}` |
| `cLibrary` | `{mode, path, origin}` — `mode` is `sysroot` / `payload-first` / `none`; `origin` is `payload` / `subos` / `host` / `none` |
| `cLibrary` | `{mode, path, origin, suppliesTarget}` — `mode` is `sysroot` / `payload-first` / `none`; `origin` is `payload` / `subos` / `host` / `none` |
| `layers[]` | the five target-side layers: `{layer, interface, impl, origin, subset}` |

⭐ **`compiler.chosenBy` answers "why this one".** `{origin, requiredBy,
replaced}` — `origin` is the same phrase the build's status line uses
(`[toolchain] in mcpp.toml`, `your default`, `target default`,
`required by the dependency graph`, `first-run default`). `requiredBy` names the
package when a `requires = ["mcpp:compiler=…"]` decided it, and `replaced` names
the spec that was displaced; both are empty when nothing was.

```jsonc
"compiler": { "family": "clang", "version": "22.1.8", "driver": "…/clang++",
"chosenBy": { "origin": "required by the dependency graph",
"requiredBy": "openkal-llvm-runtime@0.1.3",
"replaced": "gcc@16.1.0" } }
```

Without it a consumer asking *why* would have to parse the status line — the
substring matching this document exists to remove.

⚠️ **`cLibrary` and `layers[].c-abi` answer two questions, and `suppliesTarget`
says which one governs.** `cLibrary` describes the *payload's* link model — the
search paths a payload-supplied C library would use. `layers[].c-abi` describes
the *build*. When a dependency supplies the C library the two diverge, and
before `suppliesTarget` existed the document reported both with no way to tell
them apart:

```jsonc
"cLibrary": { "origin": "payload", "path": "…/xim-x-glibc/2.44/lib64",
"suppliesTarget": false }, // ← added; the payload is not in the artifact
"layers": [ { "layer": "c-abi", "interface": "musl",
"impl": "openkal-musl@0.3.5", "origin": "graph" } ]
```

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.

⭐ **`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`:

| `reason` | |
|---|---|
| `unknown-target` | the spelling names no row, and no `(arch, os)` group either |
| `ambiguous-request` | several rows serve this `(arch, os)` and none is the default |
| `compiler-requirement-conflict` | the graph's required compiler cannot be used here |
| `tier-planned` | the row exists in the vocabulary; nothing is wired yet |
| `host-cannot-serve` | no payload here, and no dependency supplied the system |
| `capability-pin` | the row's toolchain is a capability, not a preference |
Expand Down
40 changes: 40 additions & 0 deletions docs/16-the-target-triple.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ cannot rename it.
mcpp build --target x86_64-linux # = x86_64-linux-gnu
mcpp build --target x86_64-windows # = x86_64-windows-gnu
mcpp build --target riscv64-none # = riscv64-none-elf
mcpp build --target aarch64-linux # = aarch64-linux-musl
mcpp build --target aarch64-macos # macOS has no segment to decline
```

Expand All @@ -80,6 +81,45 @@ identity, which must be total, and as a request, which must be able to say
nothing; mcpp keeps both, filling the segment for the identity while recording
that the fill was a fill.

### The completion is chosen from the vocabulary, not from a fixed word

The fourth line above is why the two roles have to stay separate. Filling
`aarch64-linux` lexically gives `aarch64-linux-gnu`, and that row is `planned`
— while `aarch64-linux-musl` is `verified`. Before 2026.8.26.2 the tier gate
asked about the filled value, so:

```
$ mcpp build --target aarch64-linux
error: target 'aarch64-linux-gnu' is registered but not yet supported (planned)
$ mcpp build --target aarch64-linux-musl
Finished dev [unoptimized + debuginfo] in 0.99s
```

The question asked was *aarch64, Linux*. The question answered was
*aarch64-linux-**gnu***, and the message quotes a triple that appears nowhere in
the command. `riscv64-linux` was worse: the fill named a row outside the
vocabulary entirely, so a registered family was reported as `unknown target`.

A request that declined the segment is completed against the known-target table,
in this order:

1. the lexical default names a supported row — take it (`x86_64-linux` → `gnu`);
2. exactly one row for this `(arch, os)` is supported — take it
(`aarch64-linux` → `musl`);
3. nothing is supported — keep the lexical form, and diagnose against the rows
that *do* exist (`riscv64-linux` → "planned; registered rows for this system:
`riscv64-linux-musl`");
4. several are supported and the lexical default is none of them — refuse and
list them. No `(arch, os)` has this shape today.

Rule 1 comes first so this retires itself: the day `aarch64-linux-gnu` graduates
from `planned`, the lexical answer wins again with nothing to edit.

**Writing the segment opts out.** A written segment is a request, not a gap, so
`--target aarch64-linux-gnu` still reaches the `planned` row's refusal — which
is the escape hatch for opting into a row early with an explicit
`[target.<triple>] toolchain`.

### Which Spelling To Use

**Under the build-time system, decline it.** The graph supplies the C library
Expand Down
Loading
Loading