项目可以声明自己在哪个环境里构建。这一条声明决定项目链接哪个 C 库、以及它的
构建程序找到哪些工具——于是同一份 mcpp.toml 在开发机和 CI 上是同一个构建,
不论这两台机器上还装了别的什么。
[xlings]
subos = "tools"
[xlings.workspace]
"xim:qemu-riscv" = "9.2.4-1"可运行的工程:examples/07-project-subos/。
SubOS 是一个目录,里面是一份用户态:它自己的 bin、自己的库视图、自己那套已
装包版本,以及一个自述用的 subos_info 块。mcpp 把它当作「这个项目对着什么
构建」的答案,而且是回答这个问题的唯一机制——不是编译器所在路径,不是
XLINGS_ACTIVE_SUBOS,也不是当前 shell。
存在两种,区别在于目录落在哪里:
| 声明 | 目录 | 与谁共享 |
|---|---|---|
| 未声明 | mcpp 初始化的 subos/default |
机器上的每个项目 |
subos = "default" |
同一个目录,只是被显式点名 | 机器上的每个项目 |
subos = "<name>" |
<project>/.mcpp/.xlings/subos/<name>/ |
不共享 |
第三行是隔离的那种。它属于该项目,就放在清单旁边,删掉项目它也随之消失。
C 库。 payload-first 的构建链接的是某一个确定的 glibc,而「哪一个」是项目 的性质而非机器的性质。第 8 章讲绑定本身、降级规则,以及一个不自述的 SubOS 会 让这些规则变成什么。
构建程序看见哪些工具(mcpp 2026.8.25.1+)。被声明环境的 bin 放在
build.mcpp 运行时 PATH 的最前面:
PATH=<被声明环境的 bin>:<mcpp 自己启动时的 PATH>
因此构建程序里把 qemu-system-riscv64 写成裸名,拿到的就是那个环境里的副本。
这条通道的契约见第 7 章。
只对声明了的项目生效。 没有 [xlings].subos 的项目拿到的是 mcpp 启动时
的 PATH,逐字节不变。把一个共享目录放到每个项目前面,会让「构建看见什么」
取决于这台机器上还装过什么——同一台机器上的两个项目彼此一致,而同一个项目在
两台机器上不一致。是声明本身把它放到了前面。
前置而非替换。 构建程序理应会调 git、python3 或 shell,这些都不在
SubOS 里。前置让被声明的环境成为默认答案;其余的仍在它后面可达。
指名一个环境,同时改变了工具的版本从哪来。工程自己 [xlings.workspace] 里的条目
总是胜出;不同的是它们叠在什么之上:
| 工程声明了 | 它没点名的工具,版本来自 |
|---|---|
[xlings.workspace],无 subos |
机器的环境 |
[xlings.workspace] 与 subos = "<名>" |
那个环境自己的 workspace;机器的不适用 |
第二行就是隔离的含义。指名的环境有自己的已安装集合,把机器的版本带进去会指向那里 不存在的版本 —— 所以原本依赖「机器上装了就能用」的工程,一旦指名环境,就必须把用到 的都声明出来。
在工程内执行的 xlings use 压过这两者,直到 mcpp 重写环境为止:它是最后合并的那
一层,而人做出的动作应当压过一份文件。
[xlings.workspace] 声明的是「环境里要有哪些包」,而每个包的载荷目录另有通道交付,
即 MCPP_XPKG_<NAME>_DIR。这与 PATH 是两个问题,答案也保持分开:需要某个包
的数据文件(比如 protoc 自带的 well-known .proto)的构建程序问目录,需要
运行某个程序的构建程序问 PATH。
工作区成员的声明不是工作区的声明。 工作区构建中由工作区根持有这个选择;成员的
[xlings] 只在该成员作为独立根被构建时生效。
依赖的声明是另一回事,而且它被采纳([xlings] deps 自 2026.9.5.4,下面那条版本
规则自 2026.9.6.6)。板级支持包知道哪个模拟器够得到它那台机器,规则包知道它驱动哪个
工具包;要消费者把这些再写一遍,正是这类包存在的意义所反对的重复。依赖声明的东西会被
装上,而 MCPP_XPKG_<NAME>_DIR 在那个依赖自己的构建程序里为它作答。
工程与依赖命名同一个包时,只装它的一个版本:身份是 (namespace, name),版本是这个
包上的约束。离产物更近的声明赢,并且覆盖会被报出来;不满足对方所陈述之要求的钉会被拒绝
并点出两侧。见 05 — mcpp.toml 的「一个包一个版本」。
mcpp 解析被声明的名字,并读取它找到的东西。解析不到的名字是硬失败:
error: selected SubOS 'tools' does not exist at …/.mcpp/.xlings/subos/tools;
create/bootstrap that environment instead of falling back to active/default
回退到 default 或回退到当前活跃的那个,等于用另一个环境顶替清单点名的那个,而
这恰恰会让一份 mcpp.toml 意味着两个不同的构建。创建并填充 SubOS 属于 xlings
这一层——xlings subos new——mcpp 去管理 SubOS 状态则是把分层倒置。
一个存在但不携带 subos_info 块的环境是降级而非失败:运行时绑定报
inconclusive,没有 payload-first 绑定可用,打印一条提示,构建继续。完整规则见
第 8 章。
- 产物取决于版本的代码生成器。
protoc、flatc、着色器编译器:它的输出是 下游一切的输入,所以项目钉住生产者,而不是指望机器上那个恰好兼容。 - 构建程序要运行的模拟器。 若干裸机包把「在 QEMU 里启动产物」作为验证的一 部分;是哪个 QEMU 属于「验证了什么」的一部分。
- CI 与开发机不一致的项目,两边都没错,而构建不该察觉到差异。
- 同一台机器上两个项目需要同一工具的不同版本。 共享目录意味着必有一方落 败;私有环境让这个问题不成立。
代价一侧:隔离环境是一个必须被创建并填充的目录,而这笔账由首次构建来付。
2026.8.29 起 mcpp 会做这件事 —— 声明在 [xlings.workspace] 里的包在首次使用时被供给,
一个尚不存在的具名 [xlings] subos 会被创建而不是被拒绝 —— 但代价是实打实的:
干净机器上的第一次构建会先下载安装,然后才编译。工具很普通、版本也无所谓的项目,
不声明、直接继承机器的那份更划算。
在 --offline / MCPP_OFFLINE 或 MCPP_NO_AUTO_INSTALL 下,mcpp 转为拒绝而不是
安装,并列出包名以便手动供给 —— 与 [toolchain] 遵守的是同样两个开关,理由也相同:
一次没被要求的下载,不该由构建替工程决定。
这份声明在每台构建本工程的宿主上都会供给,宿主装不了的包是错误,不是被跳过的条目。
只存在于某一个宿主平台的工具因此按平台声明(2026.9.2.1):
deps = [{ linux = "qemu-user-aarch64" }] 在 Linux 上声明这个模拟器,在别处什么都不声明。
键与解析规则见第 5 章 §2.13。
哪些命令会安装它。 一条条目可以带档位 —— { version = "0.24.0", when = "run" } ——
[feature-xlings.<feature>] 则把工具挂在某个 feature 上。用不到的工具因此不会被下载:
见第 5 章 §2.13。不写档位就是从前的行为。
runner。 [xlings.workspace] 下的程序也是 [target.<triple>].runner 查找其第一个元素
的首选位置,在 PATH 之前(第 5 章 §2.7.3)。两个键合起来,在 CI 宿主上供给用户态模拟器,
并通过它执行交叉构建的产物,而清单不必写出载荷的路径。
| 需求 | 写在哪里 |
|---|---|
| 程序链接的库 | [dependencies] |
| 编译器 | [toolchain],第 3 章 |
| 依赖产出的宿主工具 | tools = [...],第 7 章 |
| 环境里要有的工具 | [xlings.workspace] |
| 只有某个命令或某个 feature 需要的工具 | when = "run"、[feature-xlings.<f>] |
| 用哪个环境 | [xlings] subos |
- 7 - build.mcpp —— 构建程序收到的契约,含它运行时的
PATH。 - 8 - 工具链内部 —— 运行时选择、
RuntimeBinding快照与降级规则。 - 5 - mcpp.toml —— 全部清单键,含
[xlings]。