Skip to content

Latest commit

 

History

History
164 lines (120 loc) · 9.59 KB

exit-codes.md

File metadata and controls

164 lines (120 loc) · 9.59 KB

SPEC-003:退出码契约

项 值
规范编号 SPEC-003
标题 mcpp 的进程退出码:分类、语义与稳定性承诺
状态 评审中 v1.2
版本 1.2
最后修改 2026-10-26
对应实现 mcpp >= 2026.9.1.1;5 与信号(§3.6、§3.7)mcpp >= 2026.10.26.1
相关设计文档 .agents/docs/2026-08-08-machine-readable-output-protocol-design.md §R4、.agents/docs/2026-08-31-issue540-seven-audit-findings.md §4、.agents/docs/2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md §5.4、§5.5(D23)
相关 issue #379、#540、#839

0. 这份规范存在的原因

机器可读输出协议的设计记录(§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 一共会返回哪些码。 这份规范是那 一处。

1. 适用范围

本规范约束 mcpp 可执行文件自身的进程退出码。

不约束:

  • 子进程(编译器、链接器、ninja、xlings、build.mcpp)的退出码。它们由 mcpp 解释, 不会直接透传;
  • mcpp test 所运行的测试二进制的退出码。测试失败在 mcpp 这一层是一次运行期失败, 按 §2 归入 1;
  • 库层 API 的返回值。status_severity() 这类返回 0..3 的函数是严重性排序, 与退出码无关,禁止被读作退出码。

2. 码表 已实现

码 类别 含义 通道
0 成功 命令完成了它承诺的事 正常输出走 stdout
1 运行期失败 命令跑了、请求合法、结果是失败 人类可读的原因走 stderr
2 用法错误 未知选项、不支持的选项值、缺少必需参数 stderr
4 环境未就绪 全局配置加载 / 首次初始化失败($MCPP_HOME 不可写、config.toml 损坏、引导 xlings 失败) stderr
5 副作用被拒绝 运行在调用方拒绝的副作用(--deny、MCPP_DENY_EFFECTS、--offline)之前停下;项目本身没有失败 stderr 写明副作用与原因
70 内部错误 未捕获异常。EX_SOFTWARE stderr
101 构建失败(仅 mcpp run) mcpp run 的规划或构建失败,程序没有启动。Cargo 对 cargo run 构建失败给出同一个值 stderr
127 未知命令 第一个位置参数不是一个子命令 stderr
128 + N 被信号结束(POSIX) SIGINT(130)、SIGTERM(143)、SIGHUP(129)结束了这次运行;状态由进程死于该信号得到,不是 mcpp 返回的值 结果记录写 cancelled(§3.7)

4 与 1 的分界是谁需要被修:4 说明 mcpp 自己的家还没有准备好,任何命令都会 撞上同一堵墙;1 说明这一次请求失败了,而 mcpp 是可用的。把二者合并会让「我的工程 有问题」和「我这台机器上的 mcpp 有问题」同读数。

70 与 1 的分界是这是不是一个缺陷:70 一律意味着 mcpp 有 bug,值得开 issue;1 通常不是。

101 只属于 mcpp run。run 透传程序自身的状态(docs/50-machine-output.md §6),所以它需要一个程序不常返回的值来表示「程序没有被构建出来」,使脚本能把一次 编译错误和一个返回 1 的程序区分开。mcpp build、mcpp test、mcpp pack 的状态 不变。自己返回 101 的程序,或返回 101 的 runner,读起来与构建失败相同;区分 它们靠 stderr,构建失败一定在那里写出原因。

3. 规则

3.1 分类必须稳定 已实现

一个已经发布的失败场景禁止在后续版本里改变它所属的类别。新增类别可以引入新 的码;把既有场景从 1 挪到 4(或反向)是破坏性变更。

3.2 用法错误必须先于副作用 已实现

返回 2 的路径禁止产生任何副作用。一个还不知道自己会被要求做什么的请求,不该 已经写过磁盘。

3.3 退出码禁止用于协议识别 已实现

客户端禁止用退出码判断「这个 mcpp 支不支持某项功能」。理由见 docs/50-machine-output.md §1:在该协议出现之前发布的每个版本上,未知选项本身就是 一次错误,而它当年走的是 stdout + 退出码 1。唯一跨版本成立的判据是解析 stdout。

3.4 1 可以与 stdout 上的信封同时出现 已实现

一次失败可以同时是一份文档。mcpp xpkg parse 对一份违反名字形态的描述符会把 判定作为 JSON 打到 stdout 并且退 1。

因此客户端禁止把非零退出当作「没有输出」而跳过解析。这与 §3.3 是同一条规则的两 个方向:退出码不携带「有没有输出」的信息。

3.5 非零退出必须在 stderr 上有原因 已实现

任何非零退出必须在 stderr 上留下至少一行说明。空 stderr 加非零退出是缺陷。

3.6 5:调用方的约束,不是项目的失败 已实现

调用方用 --deny <副作用,…>、MCPP_DENY_EFFECTS 或 --offline(等价于拒绝 network)约束一次运行可以产生的副作用。副作用的词汇与信封的 effects 相同 (docs/50-machine-output.md §4):init-mcpp-home、write-project、 write-global-cache、network、exec-build-script;read-project 不可拒绝。未知的 副作用名是用法错误,返回 2。

运行在第一个被拒绝的副作用之前停下,stderr 写明副作用与它要做的事(例如 network: install xim:gcc@16.1.0),退出码为 5。被拒绝的 write-global-cache 不 停下运行:构建改用工程本地的缓存,运行照常完成。

5 与 1 的分界是谁的决定挡住了运行:5 说明调用方的约束挡住了一件运行需要做 的事,去掉约束(或由调用方的宿主审批)后重跑即可;1 说明项目或环境本身失败。只有 shell 的 agent 依靠这一个数就能区分两者。

3.7 信号:状态由信号得到,记录由 mcpp 留下 已实现

POSIX 上,SIGINT、SIGTERM、SIGHUP 结束 mcpp 时,mcpp 先结束它启动的进程组(ninja、 钩子),再按信号的默认动作死去,因此调用方读到的状态是 128 + N:130、143、 129。这些值不是 mcpp 返回的,§3.1 的稳定性承诺对它们的含义是"被该信号结束"。

正在写构建目录的运行(从配置目录确定到构建结束)被信号结束时,结果记录 result.json 与 target/.last-result.json 的 status 为 cancelled (docs/50-machine-output.md §9)。记录的这一形态在运行开始时预先写好,由信号处理器用 异步信号安全的调用落盘。SIGKILL 不经过处理器:一份 status 仍为 running、而 pid 已不存在的记录,说明那次运行被强制结束。构建目录在两种情况下都保持一致,下一次构建 从中断处继续。

Windows 上,Ctrl-C 与 Ctrl-Break 由控制台处理器结束受管的作业、写下同样的 cancelled 记录,然后交给系统的默认处理;进程的状态是系统给出的 STATUS_CONTROL_C_EXIT(0xC000013A)。

4. 当前实现与本规范的差异

无。§2 的码是 src/ 中出现的全部进程退出码(2026-09-01 核对时是六个;1.1 起增加 101,见 execute.cppm 的 kRunBuildFailed;1.2 起增加 5,见 cli.cppm 末尾按 env::effect_refused() 的映射)。128 + N 不是 mcpp 返回的值,不在这张核对表中。

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 处不是退出码。

5. 变更记录

版本 日期 变更
1.2 2026-10-26 新增 5:运行在调用方拒绝的副作用之前停下(§3.6)。新增 128 + N:被信号结束,结果记录为 cancelled;Windows 为 STATUS_CONTROL_C_EXIT(§3.7)。
1.1 2026-10-01 新增 101:mcpp run 的规划或构建失败(此前为 1 或 2,与返回同一个值的程序无法区分)。
1.0 2026-09-01 首版。落地 2026-08-08 协议设计文档 §R4 指派而未写的契约;补上 1 与 4,并说明为什么 4 不属于 docs/11 那张按信封命令划定的表(#540)。