| 项 | 值 |
|---|---|
| 规范编号 | SPEC-003 |
| 标题 | mcpp 的进程退出码:分类、语义与稳定性承诺 |
| 状态 | 评审中 v1.0 |
| 版本 | 1.0 |
| 最后修改 | 2026-09-01 |
| 对应实现 | mcpp >= 2026.9.1.1 |
| 相关设计文档 | .agents/docs/2026-08-08-machine-readable-output-protocol-design.md §R4、.agents/docs/2026-08-31-issue540-seven-audit-findings.md §4 |
| 相关 issue | #379、#540 |
机器可读输出协议的设计记录(§R4)在实测四个退出码之后写下:
光接管 parse error 不够,还要把 usage / runtime / internal 的 rc 映射写成契约, 并覆盖异常边界 —— 否则客户端仍然要靠猜。这条现在是
docs/specs/的内容,不是 代码。
被指派的那份契约一直没有写。docs/50-machine-output.md 落地了其中的
usage / internal 一半(2、70、127),runtime 的一半 —— 也就是命令跑了并且
失败时返回的 1 —— 既不在那张表里,也不在别处。#540 由此把表读成「不完整」,并
提出补 4;而 4 恰恰是那几个带信封的命令给不出的码。
两件事都是同一个缺口的症状:没有一处说明 mcpp 一共会返回哪些码。 这份规范是那 一处。
本规范约束 mcpp 可执行文件自身的进程退出码。
不约束:
- 子进程(编译器、链接器、ninja、xlings、
build.mcpp)的退出码。它们由 mcpp 解释, 不会直接透传; mcpp test所运行的测试二进制的退出码。测试失败在 mcpp 这一层是一次运行期失败, 按 §2 归入1;- 库层 API 的返回值。
status_severity()这类返回0..3的函数是严重性排序, 与退出码无关,禁止被读作退出码。
| 码 | 类别 | 含义 | 通道 |
|---|---|---|---|
0 |
成功 | 命令完成了它承诺的事 | 正常输出走 stdout |
1 |
运行期失败 | 命令跑了、请求合法、结果是失败 | 人类可读的原因走 stderr |
2 |
用法错误 | 未知选项、不支持的选项值、缺少必需参数 | stderr |
4 |
环境未就绪 | 全局配置加载 / 首次初始化失败($MCPP_HOME 不可写、config.toml 损坏、引导 xlings 失败) |
stderr |
70 |
内部错误 | 未捕获异常。EX_SOFTWARE |
stderr |
127 |
未知命令 | 第一个位置参数不是一个子命令 | stderr |
4 与 1 的分界是谁需要被修:4 说明 mcpp 自己的家还没有准备好,任何命令都会
撞上同一堵墙;1 说明这一次请求失败了,而 mcpp 是可用的。把二者合并会让「我的工程
有问题」和「我这台机器上的 mcpp 有问题」同读数。
70 与 1 的分界是这是不是一个缺陷:70 一律意味着 mcpp 有 bug,值得开
issue;1 通常不是。
一个已经发布的失败场景禁止在后续版本里改变它所属的类别。新增类别可以引入新
的码;把既有场景从 1 挪到 4(或反向)是破坏性变更。
返回 2 的路径禁止产生任何副作用。一个还不知道自己会被要求做什么的请求,不该
已经写过磁盘。
客户端禁止用退出码判断「这个 mcpp 支不支持某项功能」。理由见
docs/50-machine-output.md §1:在该协议出现之前发布的每个版本上,未知选项本身就是
一次错误,而它当年走的是 stdout + 退出码 1。唯一跨版本成立的判据是解析 stdout。
一次失败可以同时是一份文档。mcpp xpkg parse 对一份违反名字形态的描述符会把
判定作为 JSON 打到 stdout 并且退 1。
因此客户端禁止把非零退出当作「没有输出」而跳过解析。这与 §3.3 是同一条规则的两 个方向:退出码不携带「有没有输出」的信息。
任何非零退出必须在 stderr 上留下至少一行说明。空 stderr 加非零退出是缺陷。
无。§2 的六个码是 src/ 中出现的全部进程退出码。
2026-09-01 穷举核对,记录方法与读数,因为「我数了一遍」不是判据:
$ grep -rhoE "return [0-9]+;" src/ --include=*.cppm --include=*.cpp | sort -u
return 0; return 1; return 2; return 3; return 4; return 5;
return 9; return 19; return 20; return 23; return 127; return 1024;
逐个归类:
| 数 | 属于退出码 | 出处 |
|---|---|---|
0 1 2 127 |
是 | 全仓;127 在 cli.cppm |
4 |
是,11 处,全部同一个原因 | config::load_or_init 失败:index_management.cppm×6、doctor.cppm×2、pack/pipeline.cppm、cli/cmd_toolchain.cppm、pm/commands.cppm 各 1 |
70 |
是 | main.cpp 的 rc = 70(EX_SOFTWARE),两处 |
3 |
否 | runtime_validation.cppm 的 status_severity() —— 严重性排序 |
4 5 9 19 20 1024 |
否 | runtime/elf.cppm 的 ELF 重定位类型表(R_RISCV_COPY / R_X86_64_COPY / …) |
23 |
否 | toolchain/msvc.cppm 的版本解析 |
4 同时出现在两栏,这正是为什么这张表按出处而不是按数字归类:同一个字面量在
一个文件里是退出码,在另一个文件里是 EM_RISCV 的重定位类型。仅凭 grep "return 4"
数出来的 13 处里,有 2 处不是退出码。
| 版本 | 日期 | 变更 |
|---|---|---|
| 1.0 | 2026-09-01 | 首版。落地 2026-08-08 协议设计文档 §R4 指派而未写的契约;补上 1 与 4,并说明为什么 4 不属于 docs/11 那张按信封命令划定的表(#540)。 |