Skip to content

Commit 4d81d06

Browse files
authored
2026.10.1.3: a source for every tool a build uses, declared, programmable and observable (#755)
Every tool a build uses now has a source that can be declared, decided by a build program, and read back. A project that writes none of the new keys builds exactly as before, and its output is unchanged. - `[xlings.overrides]` states where a declared payload comes from: the root or workspace manifest (also under `[target.'cfg(..)']`), `MCPP_XLINGS_OVERRIDE_<NS>_<NAME>`, or `~/.mcpp/config.toml`. An overridden payload is not provisioned and does not reach the offline gate. - `provision = "on-request"` installs a payload when a build program asks for it; every request of one invocation is installed together and only the programs that asked run again. - A toolchain named by path, a `bootstrap` toolchain, and a toolchain phase in which the root build program states the toolchain it builds with. - A build reports its sources: a status line per explicit source, a summary on `Finished`, a record in `resolution.json`, and `mcpp why sources | tool <name> | payload <ns:name>`; `--managed-only` refuses a build whose sources are not the ecosystem's. - Protocol 15: `xpkg_source`, `xpkg_program`, `xpkg_request`, `xpkg_pending`, `phase`, `decision`, `toolchain`. Also fixed, each found by a platform or a measurement: a build program's compile command that outgrew the Windows shell limit (through a response file written in each driver's grammar), a stated linker that reached the link on Linux only, a manifest key that said nothing about the engine floor, `which()` missing a name that is also a shell builtin, and an executable suffix a host appends itself. Closes #755
1 parent 68e4998 commit 4d81d06

69 files changed

Lines changed: 4519 additions & 165 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
subject: design
3+
status: landed
4+
---
5+
6+
# 工具与工具链的来源:声明、编程决定、可观察
7+
8+
日期:2026-10-01。状态:已落地(mcpp 2026.10.1.3,mcpp#755)。
9+
10+
生态侧的设计记录在 mcpp-plugins `.agents/docs/2026-10-01-ecosystem-build-plugin-framework-design.md`(v3)。本记录只写引擎这一侧:为什么是这些机制,以及每个决定的理由。
11+
12+
## 1. 问题
13+
14+
一次构建用到三类东西,它们的来源此前各有一套规则,而其中两类根本无法由使用者陈述。
15+
16+
**插件声明的载荷与是否使用无关,都在构建程序运行前下载。** 这是时序的后果:
17+
`[feature-xlings.<f>]` 的条目在特性打开、target 选择器为真时由 prepare 供给
18+
(features.cpp 的 `step6_xlings_workspace_from_graph`),而构建程序在那之后才运行。于是
19+
`build.mcpp` 里写 `o.cmake = "/usr/bin/cmake"` 也照样下载 `xim:cmake`;离线时整个构建被拒,
20+
构建程序根本没有运行的机会。实测记录在 mcpp-plugins 的
21+
`.agents/docs/2026-10-01-payload-source-verify.sh`:一个替身插件在 `MCPP_NO_AUTO_INSTALL=1`
22+
下三种配置的读数。
23+
24+
**下载由「声明了」触发,而不是「用到了」。** `dist-apk` 的 `bundletool` 只在
25+
`--format aab` 时用到,却每次 Android 构建都装;`dist-appimage` 的工具每次 Linux 构建都装。
26+
插件清单自己记下了这件事的代价:「provisioning runs before the build program learns
27+
`--format`」。
28+
29+
**主工具链只能是托管载荷。** PATH 上的编译器被拒,理由是无法识别、无法复现(docs/20),
30+
而这条理由对「一棵被命名、被识别、被记录的树」并不成立——`msvc@system` 就是反例,它定位
31+
机器上的 VS 并照常驱动。自建 trunk、厂商交叉工具链因此只能写 xim 配方,还要改核心的
32+
`to_xim_package`。
33+
34+
**输出不区分来源。** 一次构建说不出「这次用的 cmake 是谁的」,出错时也无从归因。
35+
36+
## 2. 决定与理由
37+
38+
### 2.1 一个「来源」概念,五个类
39+
40+
主体是 `toolchain.build`、`toolchain.bootstrap`、`payload:<ns>:<name>`、
41+
`tool:<module>:<name>`;类是 `managed`、`pinned`、`custom`、`program`、`host`。
42+
43+
类划出的线只有一条:**是生态选的,还是人或机器选的**。`managed` 与 `pinned` 都是生态的,
44+
所以它们合起来是「默认」,而默认的输出必须与本机制存在之前逐字相同——这是无感升级的判据,
45+
e2e 与 framework-lab 的 golden 都按它断言。
46+
47+
`host` 单独成类,不与 `custom` 合并:它把产物与机器状态绑在一起,而其他几类不会。判据是
48+
「版本有没有被陈述」,不是「路径像不像系统目录」——后者是猜测。
49+
50+
### 2.2 记录只有一份
51+
52+
`SourceDecision` 一个主体一条,写进已有的 `resolution.json`(新增 `sources` 键,没有读者
53+
按 `schema_version` 分支),并由输出、`mcpp why`、机器输出、`--managed-only` 共同读取。
54+
55+
**理由是这个代码库已经付过的代价。** 同一个答案被两处分别推导,失败形态是「装了 A、答了
56+
B,而且什么都没说」——`mcpp.xlings.address_set` 的文件头记着这件事。所以 `fillXpkgDirs`
57+
与供给集合共用一次统一的结果,来源记录也只有一处写入。
58+
59+
### 2.3 覆盖:环境变量 > 清单 > 全局配置
60+
61+
与 `[indices]` 的「项目 > 全局」方向一致,但环境变量排在清单之前。理由是 CI 与发行版打包
62+
要在不改清单的前提下换工具,而 `[tools.overrides]` 的文档已经把环境变量定位成这件事的出口。
63+
风险(环境意外覆盖项目的明确决定)由「每条覆盖都显示并记录」抵掉:
64+
`Using xim:cmake ← … [custom · env MCPP_XLINGS_OVERRIDE_XIM_CMAKE]`。
65+
66+
覆盖只认根。依赖替消费方决定来源,就是替使用者做决定;`[tools.overrides]` 已经立下同一条
67+
规矩。
68+
69+
**覆盖参与版本校验,但不参与裁决。** 它陈述的是「从哪里来」,不是「要哪一版」,所以键不带
70+
版本;写了 `version` 时按 `addrset::override_violation` 与每条落败的要求比较,没写时记一条
71+
note 点出未被校验的要求。引擎**不**运行任意程序去问版本:每个工具的 `--version` 格式不同,
72+
那是插件的知识。
73+
74+
### 2.4 按需供给:请求 + 重跑,而不是「构建程序之后再供给」
75+
76+
`rules-cuda` 读工具包头文件里的版本,`rules-qt` 读 SDK 文件,`dist-apk` 从 platform 目录读
77+
API level,`dist-wix` 检查 payload 里的文件是否存在——这些成员在**规划时**就需要载荷。把
78+
供给整体移到构建程序之后会让它们规划失败。
79+
80+
所以是:程序请求 → 引擎批量安装 → 只重跑请求过的程序。请求的那次运行被**丢弃**
81+
(`run_build_program` 在 `dirs::apply` 与 `write_cache` 之前返回),所以没有要撤销的状态,
82+
也没有半应用的指令集。最多三轮,第三轮仍有新请求就报错并点名——终止条件不依赖被调用方
83+
改变状态,这是 `resolve_target_toolchain` 的递归曾经付过的代价。
84+
85+
稳态零开销:下一次构建载荷已安装,第一轮就能拿到答案,`contract_hash` 因此与第二轮相同,
86+
缓存命中。
87+
88+
### 2.5 工具链:bootstrap 与 build 两段,`path:` 进入 spec
89+
90+
这两个角色**本来就存在**:交叉构建时 `build.mcpp` 由一个宿主工具链编译
91+
(`xlings.cpp` 的 G3 分支)。本次只是给它们命名,并让 build 这一侧可以独立配置。
92+
93+
`path:<dir>` 做成 `ToolchainSpec` 的一种拼法,而不是第二条解析路径。理由是
94+
`parse_toolchain_spec` 有十几个调用点,第二条路径意味着十几处都要学会它;做成一种拼法后,
95+
不认识它的调用点自然报错而不是静默走错。族由 `<root>/bin` 里有哪个驱动决定——驱动是事实,
96+
清单里的 `family` 只用于核对。
97+
98+
**不写入那棵树。** 托管载荷的 `clang++.cfg` 是这套机制的**产物**(docs/91 §5.1),写给直接
99+
调用 clang 的人;mcpp 自己的调用绕过它。一棵不属于 mcpp 的树因此不该被写入,
100+
`resolve_clang_driver` 改为在 `localRoot` 非空时按「驱动旁有 libc++」开启模型,而不是按
101+
cfg 文件是否存在。
102+
103+
**身份按内容,不按版本。** 一个 trunk 驱动可以在版本不变时被重建,所以驱动与每个
104+
`tools` 程序的路径、大小、修改时间进入 `driverIdent`(于是进入指纹),并写入
105+
`local-toolchain.stamp` 供三条快速路径比较。不读字节:一个驱动几百 MB,而每次 prepare 都要
106+
读它。
107+
108+
**`mcpp.lock` 不记工具链**,本次也没有加。lock 的内容是依赖解析的结果,而工具链不是被解析
109+
的依赖;一台没有这棵树的机器在读到声明处就被拒绝,这已经是「不可移植」要的那句话。设计稿
110+
曾写成「lock 中记为 `local`」,那是没有实现的断言,文档与规范按实际行为更正。
111+
112+
### 2.6 工具链阶段:两遍 prepare,第一遍不说话
113+
114+
构建程序需要它的宿主模块,宿主模块来自依赖图;而依赖图的解析需要工具链
115+
(`cfg(compiler = ...)`、`requires`)。这是一个真实的环,所以它被切成两遍:
116+
第一遍用 bootstrap 走到宿主模块注册,运行工具链阶段,然后以一个内部信号结束;
117+
第二遍从头用陈述的工具链。
118+
119+
第一遍**不叙述**:它要说的每一句,第二遍都会就真正发生的那次构建再说一次。实现是
120+
`driver.cpp` 的 `QuietPass`,而不是在每个输出点加条件——后者是会漏的那种。
121+
122+
工具链阶段**只能**陈述工具链:它运行在依赖图之前,那里的一条 flag、一个源文件或一个
123+
action 描述的是一次还不存在的构建;一个载荷请求也无法回答,因为声明它的图还没读。引擎
124+
按名拒绝,而不是静默丢弃。
125+
126+
它有自己的 `artifactsDir`(`target/.build-mcpp/toolchain-phase`):与构建阶段共用一份缓存
127+
记录,会让两者每次构建互相失效。
128+
129+
## 3. 验证
130+
131+
- 单元:`tests/unit/test_sources.cpp`(15 例)——清单键的解析与拒绝、协议 15 的指令、
132+
覆盖的版本校验、`path:` 的族判定。
133+
- e2e:873(覆盖)、874(按需)、875(按路径命名的工具链)、876(工具链阶段)、
134+
877(`mcpp why` 与机器输出)。判据统一是 `MCPP_NO_AUTO_INSTALL=1`:构建成功即「没有要求
135+
下载」,而拒绝会点名它本要装的东西。
136+
- 既有 e2e:以关键词选出的 62 个用例通过;219 在**已发布的 2026.10.1.2** 上同样失败(本机
137+
glibc 顺序),658 需要连接 Android 设备。
138+
- 生态:mcpp-plugins 0.19.0 的 11 个 consumer fixture 与 27 例 `plugin-logic` 通过;
139+
`tests/cmake-consumer` 在 `MCPP_NO_AUTO_INSTALL=1` 下,由 `build.mcpp` 点名 cmake 即可
140+
构建——这正是本次要做成的那件事。
141+
142+
## 4. 已知边界
143+
144+
- `mcpp build` 没有 `--format json`,所以来源的机器形态走 `mcpp why sources`,而不是构建
145+
事件流。docs/50 §3 把 `ndjson` 记为保留,本次不动它。
146+
- 覆盖与按需供给都只作用于 xlings 载荷。依赖包 `kind = "bin"` 的宿主工具仍走
147+
`[tools.overrides]`:两者的键空间与语义不同(一个是 `<pkg>:<tool>` 的程序,一个是
148+
`ns:name` 的目录),合并会让一张表有两种键。
149+
- 构建程序的编译命令在超过预算时走响应文件。这条路径在本仓库的 CI 里**到不了**:Linux 与 macOS
150+
的预算是 128 KiB,而只有 Windows 上 `capture_exec` 所经的 shell 是 8191 字节。覆盖它的是
151+
mcpp-plugins 的 `all-rules-compile`(导入十五个宿主模块)与两个单测——每种 tokenize 语法
152+
一个,因为 clang 与 GCC 把反斜杠当转义,cl 与 clang-cl 不当。
153+
- `tools = { ld = ... }` 只对 clang 驱动成立(`--ld-path`);gcc 按 `-B` 目录里的名字 `ld`
154+
选链接器,所以一个别名程序无法那样被选中,声明处直接拒绝而不是在链接时忽略。这条起初写成了
155+
Linux clang 分支里的一行,于是 macOS 的 Apple 链接形状拿不到它——被陈述的链接器进了指纹
156+
(改动 wrapper 会让快速路径失效),却不参与链接,而且什么都不说。现在它在所有形状之后追加
157+
一次。覆盖它的是 toolchain-lab:e2e 875 在没有装 llvm 载荷的宿主上 SKIP,而 macOS CI 正是
158+
这样的宿主。
159+
- 工具链描述数据化(核心读描述文件、不再硬编码 `to_xim_package`)不在本次范围;
160+
`[toolchain] { path }` 的字段已经与那份描述同形,所以它是后续的第三种载体,而不是改写。

‎.agents/docs/README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
1818
---
1919
```
2020

21-
322 records.
21+
323 records.
2222

2323
## By subject
2424

@@ -30,6 +30,7 @@ Records that declare one. Everything else is listed by date below.
3030

3131
### design
3232

33+
- [工具与工具链的来源:声明、编程决定、可观察](2026-10-01-tool-and-toolchain-sources-design.md) — landed
3334
- [A pack's build reported as a build, and a unit's compile independent of the member selection: triage and design (#753, #751)](2026-10-01-pack-drive-and-selection-independent-compile-design.md) — landed
3435
- [Member selection, build programs prepared once, a pack over several members, and the output streams of `mcpp run`: the plan for the release after 2026.9.30.2 (#748, #749, #750)](2026-09-30-member-selection-and-build-program-cost-plan.md) — landed
3536
- [The build's wall time, its progress count, a hang after the build, and #732 and #744: measurements and a remediation plan](2026-09-30-build-wall-time-progress-count-and-hang-plan.md) — landed
@@ -113,6 +114,7 @@ Records that declare one. Everything else is listed by date below.
113114

114115
### 2026-10
115116

117+
- [工具与工具链的来源:声明、编程决定、可观察](2026-10-01-tool-and-toolchain-sources-design.md) — landed
116118
- [A pack's build reported as a build, and a unit's compile independent of the member selection: triage and design (#753, #751)](2026-10-01-pack-drive-and-selection-independent-compile-design.md) — landed
117119
### 2026-09
118120

‎CHANGELOG.md‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,81 @@
44
> Each `## [<version>]` section is that release's notes. Entries are written in English
55
> from 2026.9.28.3 on; earlier entries remain as written.
66

7+
## [2026.10.1.3] - 2026-10-01
8+
9+
This release gives every tool a build uses a source that can be declared,
10+
decided by a build program, and read back (mcpp#755;
11+
`.agents/docs/2026-10-01-tool-and-toolchain-sources-design.md`). A project that
12+
writes none of the new keys builds exactly as before, and its output is
13+
unchanged.
14+
15+
### Added
16+
17+
- **`[xlings.overrides]` states where a declared payload comes from**, in the
18+
root manifest (also under `[target.'cfg(..)']`), as
19+
`MCPP_XLINGS_OVERRIDE_<NS>_<NAME>`, or in `~/.mcpp/config.toml`. An
20+
overridden payload is not provisioned and does not reach the offline gate;
21+
`mcpp::xpkg_dir` answers the root it implies, and the new
22+
`mcpp::xpkg_program` and `mcpp::xpkg_source` answer the program it named and
23+
`override`. A stated `version` is checked against every requirement a package
24+
of the graph made. A dependency that writes the table is refused: which
25+
payloads a package needs is its own statement, where they come from is the
26+
project's.
27+
- **`provision = "on-request"` installs a payload when a build program asks for
28+
it**, with `mcpp::xpkg_request`. Every request of one invocation is installed
29+
together and only the programs that asked run again, so a build whose program
30+
names its own tool downloads nothing. `mcpp emit build-database` installs
31+
nothing and records `MCPP_BUILD_DATABASE_PAYLOAD_DEFERRED`.
32+
- **A toolchain named by path**: `[toolchain] <key> = { path = "<dir>", prefix,
33+
sysroot, family, launcher, tools }`, or `MCPP_TOOLCHAIN=path:<dir>`. mcpp
34+
probes the drivers in the tree, identifies them, drives them with its own
35+
link model, and writes nothing into the tree. The driver and each stated tool
36+
enter the fingerprint by content, and a build records them beside its output,
37+
so the fast paths decline once one of them changed.
38+
- **`[toolchain] bootstrap`** names the toolchain that compiles and runs build
39+
programs when it should not be the one building the project.
40+
- **`[toolchain] <key> = { configure = "build.mcpp" }`** hands the build
41+
toolchain to the root build program: it runs once in a toolchain phase, where
42+
`mcpp::phase()` is `"toolchain"`, and states the toolchain with
43+
`mcpp::toolchain(key, value)`. That phase may state nothing else.
44+
- **A build reports its sources.** A source that is not the ecosystem's gets a
45+
line of its own (`Using … [custom · mcpp.toml:22]`, `Bootstrap …`), the
46+
`Finished` line summarises them, and the record is written to
47+
`resolution.json`. `mcpp why sources`, `mcpp why tool <name>` and
48+
`mcpp why payload <ns:name>` report it, including as `mcpp.why.sources` under
49+
`--format json`.
50+
- **`--managed-only` / `MCPP_MANAGED_ONLY=1`** refuses a build whose toolchain,
51+
payload or plugin tool came from anywhere but the ecosystem, naming each.
52+
- **Protocol 15** for build programs: `xpkg_source`, `xpkg_program`,
53+
`xpkg_request`, `xpkg_pending`, `phase`, `decision` and `toolchain`.
54+
55+
### Changed
56+
57+
- `mcpp why toolchain` states the source of the toolchain and the origin the
58+
resolution recorded, in place of a sentence listing every way one can be
59+
chosen.
60+
61+
### Fixed
62+
63+
- **A build program's compile command goes through a response file when it
64+
outgrows the channel it travels.** The command carries one
65+
`-fmodule-file=<name>=<path>` per host module the program imports, with
66+
absolute paths, and on Windows it reaches a shell that tolerates 8191 bytes: a
67+
program importing fifteen modules reported only `The command line is too
68+
long.`, naming neither the length nor the cause. The file is written in the
69+
grammar its driver reads -- single quotes for clang and GCC, which treat a
70+
backslash as an escape, Windows quoting for cl and clang-cl -- and stays beside
71+
the program for a failed compile to show.
72+
- **A manifest key this engine does not know says which engine the package
73+
needs.** A package written for a newer mcpp was refused with `unknown key
74+
'<key>'` and nothing about the version, because the engine floor is checked on
75+
the document that very parse failed to produce. The refusal now names the
76+
floor, this engine and the upgrade, which is what a reader meets first after a
77+
plugin collection raises it.
78+
- **`which()` resolves a name that is also a shell builtin.** `command -v true`
79+
prints `true`, not a path, so a bare-name payload override of such a name was
80+
refused as not found on a machine carrying `/usr/bin/true`.
81+
782
## [2026.10.1.2] - 2026-10-01
883

984
This release implements the design for a pack's build and a compile that does

‎docs/09-commands-by-scenario.md‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,21 @@ recorded build is replayed only for the toolchain request that recorded it.
131131
$ mcpp why toolchain
132132
toolchain: gcc 16.1.0 (x86_64-linux-gnu)
133133
abi(libc)=glibc cxxstdlib=libstdc++ arch=x86_64 os=linux triple=x86_64-linux-gnu
134-
reason: [toolchain] in mcpp.toml if set, else platform-native default
134+
source: pinned · [toolchain]
135+
reason: [toolchain] in mcpp.toml
136+
```
137+
138+
`mcpp why sources` reports where each tool a build uses came from
139+
(2026.10.1.3+): the toolchain, every payload, and every tool a plugin runs, with
140+
what was consulted for it. `mcpp why tool <name>` and
141+
`mcpp why payload <ns:name>` narrow it to one:
142+
143+
```
144+
$ mcpp why payload cmake
145+
sources:
146+
payload:xim:cmake /usr/bin/cmake
147+
custom · mcpp.toml:22 for mcpp:plugins
148+
considered: payload xim:cmake@>=3.31 (not installed: overridden)
135149
```
136150

137151
`mcpp why deps` lists the resolved dependency graph before the lines of
@@ -633,8 +647,9 @@ mcpp is involved and its documentation will not help.
633647

634648
## Current limitations
635649

636-
- `mcpp why --format json` is defined for the `toolchain` topic only. The other
637-
topics report `'<topic>' has no machine-readable shape yet` and exit non-zero.
650+
- `mcpp why --format json` is defined for the `toolchain`, `sources`, `tool` and
651+
`payload` topics. The other topics report `'<topic>' has no machine-readable
652+
shape yet` and exit non-zero.
638653
- `mcpp search` matches a substring; there is no field selector, and no way to
639654
restrict a search to one namespace.
640655
- `mcpp clean --stale` reads `target/.build_cache`, which holds a bounded number

0 commit comments

Comments
 (0)