读者: 要把一个程序交付到没有 mcpp 的机器上的人。
本章回答的那一个问题: 一次构建怎样变成另一台机器能运行的东西,以及每一种 打包模式各自携带什么。
不在这里: 交付一个供其它包在构建时使用的库,那是 12 —— 分发预构建库。在此之后: 11 —— 发布一个库到 mcpp-index。
本页讲的是打包一个程序。若要以「接口 + 预构建二进制」的形式发布一个库, 见 12 —— 分发预构建库。
mcpp pack具体做哪一种,由 target 的kind决定,不由某个 flag 决定:mcpp pack <name>打包[targets.<name>],一个bin会变成一个 bundle, 而一个lib/shared会变成一个库包。不给名字时,mcpp 选择唯一可打包的 target。
mcpp build默认产出的动态链接二进制,其 loader 与 RUNPATH 都指向构建 沙盒。它是开发产物,不是交付物。有三条路径能把它变成交付物 —— 没有一条会 用到宿主的 C 库。
下面每一种路径产出的产物,其 C 运行时都来自生态,从不来自 /lib64。这是有意
安排的:mcpp 之所以针对私有 glibc 构建,正是为了让产物的行为不取决于底层是
哪一个发行版;如果分发的最后一步又伸手去用宿主的 libc,前面这一步做的事就白
费了。
| 路径 | 命令 | C 运行时的来源 | 适用条件 | |
|---|---|---|---|---|
| A | 走生态 | mcpp emit xpkg → xlings install <pkg> |
目标机自己的 xlings 载荷 | 目标机已装 xlings |
| B | 单个静态文件 | mcpp build --target x86_64-linux-musl |
不来自任何地方 —— 已链接进去 | 需要一个没有任何运行时依赖的单文件 |
| C | 自带运行时 | mcpp pack --mode self-contained |
随 bundle 一起分发 | 任意 Linux,含比构建机更旧的版本 |
关于路径 A。 刚构建出的二进制中记录的 PT_INTERP 指向构建机的载荷,因此
把该文件手工复制到另一台机器上不会运行:那条路径在目标机上不存在。这是「手工
复制」这个动作的性质,不是产物本身的性质 —— 经 xlings 安装时,包内的 ELF 会
在安装期被重新指向目标机自己的载荷。记录的路径是构建机的细节,不是分发格式的
一部分。需要在机器间手工搬运二进制时,能存活下来的是路径 B 与 C。
关于路径 B。 --target …-musl 隐含静态链接,因此没有 loader、没有
RUNPATH,运行期不需要查找任何东西。它的结果最小也最可移植,是程序不需要
glibc 专有行为(NSS 查询、dlopen 宿主插件)时的首选。
关于路径 C。 bundle 内携带这套工具链的 glibc 及其 loader,因此能在比构建机
更旧的发行版上运行 —— 这是路径 B 覆盖不到、而又确实需要 glibc 时的那一格。选
它之前先读下面 /proc/self/exe 一节:经由内置 loader 启动会改变程序对「自己
在哪」的认知。
一次打包由三个相互独立的选择与一组构建腿决定(2026.10.27.1+):
| 维度 | 选项 | 回答的问题 | 取值 |
|---|---|---|---|
| 链接 | --linkage |
库在程序里,还是在程序旁边 | static、shared |
| 闭包 | --mode |
运行该包的机器提供什么 | system、vendored(默认)、self-contained |
| 容器 | --pkg-format |
交付成什么 | tar、zip、dir、single-file、polyglot、某个包提供的格式,或一棵树的容器列表(tar,okb);未给出时 Windows 程序为 zip,其余为 tar |
| 腿 | --target,可重复 |
打包覆盖哪些目标 | 目标与目标集合(x86_64-openkal、openkal、linux-musl) |
| 系统 | --systems |
自带的运行器或单文件为哪些系统而做 | linux、linux-aarch64、all |
-
链接是程序的属性。
--linkage static|shared(或[pack] linkage)是这次打包的构建的依赖链接, 优先于[build] dependency_linkage与构建配置中的值;未给出时每个库按它自己的声明链接。Linux 上--linkage static选择<arch>-linux-musl行(-linux-gnu目标换成对应的 musl 行),全静态程序 不论--mode为何都整体打包;--target …-linux-musl隐含它。ok 映像上它是一个.oke、没有.okso。 -
闭包是包的属性:共享库闭包中有多少随程序携带。
--mode static是--linkage static --mode self-contained的简写。 -
容器是交付的形状,见下文。
-
腿是命令命名的目标(2026.10.30.1+):每个
--target,未给出时为[pack] targets,再未给出时为构建的 目标。目标集合(openkal、linux-musl)为目标表中该系统、所选平台服务的行。平台不增加腿。多条腿按目标的种类 合并:目标 多条腿 lib、shared一个库包(12) 声明运行器的平台(openkal)的程序 一棵以平台命名的树, <name>-<version>-openkalAndroid 应用 一个 APK 树,每条腿一个 lib/<abi>/其他程序 每条腿一个包,各以三元组命名 写出多于一个文件的打包要求
-o为目录:以/结尾的路径或已存在的目录,每个文件以缺省名写在其下。一条 openkal 腿的打包在Packed之后写出:only x86_64; --target openkal or [pack] targets = ["openkal"] packs every architecture。
| Mode | 宿主必须提供 | 体积 | 使用场景 |
|---|---|---|---|
system |
每一个 .so(含第三方) |
最小 | .deb/.rpm,同发行版机群(由包管理器声明依赖) |
vendored(默认) |
libc / libstdc++ / loader | +几 MB | 主流发行版(Ubuntu 22+、Debian 12+、RHEL 9+) |
self-contained |
无 | +30–50 MB | 任意 Linux,含较旧的 glibc;携带闭包与 run.sh wrapper |
static(--linkage static --mode self-contained) |
无(单文件) | +5–10 MB | musl;架构匹配的 Linux x86_64 或 aarch64,Docker scratch,Alpine |
如何选择:
- 分发格式打包(
.deb/.rpm)或同发行版内部部署 →system - 面向主流发行版的桌面 / 服务端发布 →
vendored(默认) - 跨发行版,或目标是较旧的 glibc(旧版 CentOS、麒麟等) →
self-contained - 单个便携文件、不依赖宿主 →
static
没有任何 mode 会携带构建机的路径。 开发构建有意寻址的是本机:它的
DT_RPATH 命名工具链的载荷目录与 SubOS 的库视图(<subos>/lib),它的
PT_INTERP 命名一个私有 loader。每一种 mode 都改写这两者 —— vendored 与
self-contained 改写为相对 $ORIGIN 的路径,system 则整个清空搜索路径并
恢复平台标准的 interpreter。system 不是「原样保留构建时的样子」,而是「目标
机会提供一切」这一断言 —— 这是关于目标机的陈述,不可能用本机的绝对路径拼出
来。e2e 215 会扫描 bundle 里每一个 ELF,查找任何落在 $MCPP_HOME 之下的路径,
命中即失败。
不能成立的组合在任何编译之前被拒绝,消息写出两个选择:
| 组合 | 出路 |
|---|---|
--pkg-format single-file 与 --mode system |
--mode vendored 或 self-contained:单文件携带它所运行的东西 |
--compress 与 --pkg-format dir 或提供者的格式 |
--pkg-format tar、zip 或 single-file:只有容器被压缩 |
--compress zstd 或 xz 与 --pkg-format zip |
--compress gzip(DEFLATE)或 none,或 --pkg-format tar |
--compress gzip 或 xz 与 --pkg-format single-file |
--compress zstd[:<level>] 或 none:启动器解压 zstd |
自带运行器的映像树与映像的单文件之外使用 --systems |
--mode self-contained,或 --pkg-format single-file;不相容的 [pack] systems 写出提示后搁置 |
映像的单文件,--systems 中写出的系统的行本 mcpp 不构建程序(x86_64-macos、aarch64-windows-gnu) |
只列出会构建的组合的 --systems;--systems all 取这些并写出其余 |
结果多于一个文件(每腿一个包、多个单文件)而 -o 命名一个文件 |
-o <目录>/ |
未声明 runner 的平台的多条映像腿,且不是单文件 |
每个目标打一次,或 --pkg-format single-file |
平台目标之外的 --target |
平台服务的目标 |
| 展开为空的目标集合,或其行选到不同平台 | 一个目标,或另一个集合 |
--pkg-format 列表含 dir、single-file 或 polyglot;列表与命名一个文件的 -o;mcpp run 的列表 |
一个格式,或 -o <目录>/ |
一个系统的程序(原生目标)使用 --pkg-format polyglot |
--pkg-format single-file,或 --target openkal |
mcpp run --pkg-format tar、zip 或 dir |
--pkg-format single-file 或 polyglot,或某个包提供的格式 |
映像的 --mode self-contained 而格式只有 okb |
--pkg-format tar,okb:okb 只含映像,运行器会被丢弃 |
在任何地方都不能运行的树(runs_on 为空) |
--mode self-contained --systems <本 mcpp 构建的系统> |
对 lib 或 shared 目标使用被分派的格式 |
库的路线,12 |
对 lib 或 shared 目标使用 --pkg-format single-file |
tar、zip 或 dir |
与容器不相容的 [pack] compress(目录,或 zip 的 zstd)在这次打包中被搁置;只有选项被拒绝。
「自包含」有一个下限。有些库只能来自目标机器:图形驱动的用户态部分与正在
运行的内核模块版本绑定,而对专有栈而言,再分发是不被允许的。这类依赖应声明为
运行期能力需求(docs/04-mcpp-toml.md §2.11),模式表因此多出一列:
| Mode | 需要宿主提供能力的程序 |
|---|---|
system |
yes |
vendored(默认) |
对这类程序而言正确的默认值 |
self-contained |
打包期拒绝 |
static |
打包期拒绝 |
两处拒绝出自同一个事实:自带 libc 的 bundle 无法消费宿主提供的库。 那个
.so 带着它对目标机 libc 的要求到达,而该进程没有那份 libc —— 双向实测记录
于 mcpp#392 / mcpp#401:私有 glibc 遇上宿主加载的对象,会在重定位阶段、
main 之前崩溃。此前这两种 mode 的行为要么是链接通过、启动时失败,要么是
静默降级(就图形栈而言表现为软件渲染,且没有任何提示)。
vendored 会打包这类程序,并在 bundle 根目录写出一个 HOST-REQUIREMENTS
文件,说明目标机必须提供什么:
capability=opengl.glx.driver discovery=rpath-of-dispatch
discovery 是可操作的那一半 —— 各机制彼此独立,满足其一不代表满足另一个。该
文件只在确有内容需要说明时才写出:一个空文件等于声称什么都不需要。
针对一个被陈述的目标基座构建、且加载目标自身 C 库的包(vendored、system)还陈述它能运行的最低
C 库(2026.10.24.1+):程序与
随包库所需的最高 GLIBC_x.y,打印为 Requires glibc >= 2.38 …,并写为
capability=c-abi.glibc>=2.38 discovery=PT_INTERP。程序构建时所针对的基座即成为这一下限;
self-contained 则随包带上基座自己的 loader 与 C 库。mcpp pack 使用的 release 与 dist 档
会裁剪产物携带的源码路径并定义 NDEBUG(见 04 —— [profile])。
上表给出规范名称。旧名称仍是永久性的兼容别名:bundle-project =
vendored,bundle-all = self-contained。tarball 的文件名后缀是被冻结的
wire 格式(由 install.sh 消费),不跟随改名:vendored 无后缀,
self-contained 是 -bundle-all,static 是 -static,system 是
-system。
mcpp pack # vendored by default
mcpp pack --mode system
mcpp pack --mode static
mcpp pack --mode self-contained # alias: --mode bundle-all
mcpp pack --target x86_64-linux-musl # equivalent to --mode static
mcpp pack --target aarch64-linux-musl # ARM64 equivalent
mcpp pack --pkg-format dir # output as a directory, no tarball
mcpp pack --pkg-format appimage # a format a package in the graph provides
mcpp pack -o myapp.tar.gz # filename only: lands at target/dist/myapp.tar.gz
mcpp pack -o /abs/path/myapp.tar.gz # includes a directory: output to the literal path
mcpp pack --profile dev # build with a different profile (default: release)
mcpp pack --dev # the same, as `build` and `run` spell it; --profile wins over it
mcpp pack --message-format json # one mcpp.pack envelope on stdout (mcpp 2026.9.16.1+)
mcpp pack --no-strip # ship the artifacts as built
mcpp pack -p app --pkg-format release # a workspace member, as if run in its directory
mcpp pack --workspace --pkg-format release # every member with a program, planned and built once
mcpp pack -p cli -p gui -o dist/ # two members, each archive below dist/
mcpp pack --debug-symbols dbg/ # write the separated *.debug files under dbg/
mcpp pack --pkg-format msi --features installer # activate root-package features for the pack
mcpp pack --linkage static # the libraries inside the program
mcpp pack --pkg-format single-file # one executable file
mcpp pack --pkg-format tar --compress zstd # .tar.zst
mcpp pack --target x86_64-openkal # one leg: the image's tree for x86_64
mcpp pack --target openkal # every architecture of the platform, one tree
mcpp pack --target openkal --mode self-contained --systems all --pkg-format tar,okb -o dist/
mcpp pack --target linux-musl -o dist/ # a static package per architecture
mcpp pack --merge a.zip --merge b.zip -o all.zip # one package from packages built on several machines
mcpp emit xpkg --package all.zip --url https://example.org/v1.0.0 # its xim-pkgindex entrypack 与 run 接受 --format 作为 --pkg-format 的旧写法,并打印一条警告(2026.10.27.1+)。
其他命令上的 --format 是输出编码(--format json)。
--features <LIST>(mcpp 2026.9.15.2+)为打包所执行的每一次构建启用根包
feature:每一条 --target 腿,以及被分派格式的两遍构建。它接受的取值与
mcpp build --features 相同,因此只为某一种发布才需要的宿主工具,可以声明在
带 tools = [...] 的 [feature-deps.<f>] 下,只由点名 <f> 的那次打包构建
出来。mcpp run --pkg-format <name> --features <LIST> 把同样的 feature 交给它
执行的那次打包。
-p <member>(mcpp 2026.9.28.3+)在工作区根目录打包某个成员:成员的解析方式与其他 -p
相同,打包在该成员目录中进行,因此结果与在该目录执行 mcpp pack 相同。相对路径的 -o 仍以
执行命令时所在的目录为基准。
--release 与 --dev(mcpp 2026.9.16.1+)是 build、run 已接受的简写,
优先级相同:三条命令上都是 --profile 优先于它们。
打包对它所执行的构建的陈述方式与 mcpp build 相同(mcpp 2026.10.1.2+):每个有工作的包
一行,运行期间显示状态行,并在第一行 Packing 之前写出 Finished。跨多个配置的打包先构建
全部配置,只写一次 Finished。由包提供的 --pkg-format 的第二遍以同样方式陈述,其后是
Distributing 与 Packed 行。构建同时运行 [build] jobs 个编译,与每一条会编译的命令相同。
2026.10.1.2 之前,打包中的构建不写出上述任何内容,重新编译数分钟的打包只显示 Planning。
-p 可以重复,--workspace 与 --exclude 选择成员的方式与 mcpp build 相同
(07 —— 工作区,§5.3)。这些成员作为一个选择一起规划、只构建一次,
然后每个成员在各自的暂存树中暂存并打包:
mcpp pack --workspace --pkg-format release # 每个有程序的成员
mcpp pack --workspace --exclude updater # 除一个之外的每个这样的成员
mcpp pack -p cli -p gui --pkg-format release对每个成员分别运行 mcpp pack -p <member>,每次都会重新规划图、重新运行构建程序、
重新启动构建,它们共用的成员也不例外。对多个成员的打包,每个配置(即 mcpp build
使用的分组)规划一次、构建一次,每个构建程序每遍运行一次:被多个成员依赖的包,
例如带构建程序的共享库,只准备并编译一次,它的程序在这次打包里不会再次运行。
每个成员得到的,就是 mcpp pack -p <member> 给它的:
- 产物。 成员的归档或目录,就是它单独打包时得到的那一份,位于它自己的
target/dist/之下。给出--output <dir>时,它写在该目录之下(目录不存在时会创建),使用默认的名字。 - 自己的暂存树。 使用被分派的
--pkg-format时,打包如上所述准备两次,第二遍告诉每个 构建程序它所代表的成员的暂存树。mcpp::pack_stage_dir()与${mcpp.stage_dir}按成员回答。一个程序代表它所属的成员;只有一个被打包成员依赖到的包(成员所依赖的 提供者)代表那个成员;被多个被打包成员依赖到的包不代表其中任何一个,它既不会被告知 格式,也不会被告知任何树,因此它的程序运行一次就够所有成员使用。 - 自己的可分发物。 本次请求引入的
artifactaction,属于提交它的那个程序或提供者 所代表的成员。每个成员的输出都会核实存在,并以该成员的Packed报告。
--workspace 只打包有程序 target 的成员,并说明跳过了哪些;用 -p 点名的成员若没有
程序 target 则被拒绝。成员按配置逐组打包,按 [workspace] members 的顺序报告,与 -p 的书写顺序无关。
某个成员失败(例如它的提供者对所请求的格式什么也没有提交)时,按名字报告,其余成员
照常打包;只要有一个成员失败,退出状态就非零。一个成员 —— 无论是用一个 -p 点名,还是
由命令所在的目录决定 —— 与以前完全一样地打包:mcpp pack -p X 只规划 X 的闭包。在虚拟
工作区根目录不带任何选择器的 mcpp pack 仍旧打包第一个有程序的成员;--workspace 则
打包全部。
在任何东西被编译之前拒绝,并点名被拒绝的对象:
| 输入 | 原因 |
|---|---|
| 位置参数 target 名 | 它点名的是某一个包的 target |
多于一个 --target |
一个程序只为一个 target 构建,而多 target 的 Android 打包暂存的是一个成员的各条腿 |
已存在且是文件的 --output |
每个成员的归档或目录写在该目录之下 |
用 -p 点名、却没有程序 target 的成员 |
它没有可打包的东西 |
使用被分派的 --pkg-format 时,没有任何提供者代表它的成员 |
该格式由成员依赖不到的包提供,或由被多个成员依赖到的包提供 |
| 两个会写同一个归档或目录的成员 | --output 只点名一个目录,而名字、版本与 target 相同的两个包共用同一个归档名 |
一个为多个成员提供格式、带构建程序的包,无法同时服务它们:提供者只会得到一棵暂存树。 请从每个成员自己的构建程序提供该格式(两者共用一个辅助函数即可),或者逐个打包成员。
--message-format json(mcpp 2026.9.16.1+)在命令结束后于 stdout 上输出一个
mcpp.pack 信封,所有给人看的行都改走 stderr。它的 data.artifacts 列出
产出的每一个文件或目录,带绝对路径、type(file 或 directory)、
--pkg-format 取值以及各条腿的三元组;data.stage 给出暂存树、它的 manifest,
以及闭包是否已经走通(见 50 —— 机器输出)。对多个成员的
打包列出每个成员的产物,每一条点名它的 member,data.stage 为 null,并在
data.stages 中为每个成员给出一棵树(2026.10.1.1+)。这条命令
上的 --pkg-format 表示的是包格式,因此机器输出改用 mcpp test 请求它的那种
方式。
tar、dir、zip 与 single-file 回答的问题,和 msi 与 appimage 回答的问题是同一个 ——
输出取什么形状 —— 所以它们是同一个 flag 的不同取值。引擎持有什么、包持有什么,分界如下:
mcpp pack拥有机制,以及不依赖任何其他发布的格式:一棵树、一个归档、一个文件。 其余每一种格式都住在某个包里,由mcpp pack分派过去。
这四种解开或启动即可运行,不需要了解任何别人的发布方式。此外的一切都 需要。dpkg 的 control 字段、AppImage 的 runtime、WiX 的 schema、Apple 的 公证、Android 的签名方案:其中任何一个被绑进引擎,都会把一次 mcpp 的发布耦合 到一次 mcpp 并不控制的发布上。这与本项目已经为语言做过的论证是同一个 —— Slang 被支持,而引擎里没有它的名字。
所以取值集合是开放的(mcpp 2026.9.11.1+)。--pkg-format <name> 在解析后的图里
找到声明了 <name> 的那个包,并把暂存树交给它;一个未知的取值点名的是
当下确实可用的那些,而不是一份固定清单:
error: unknown --pkg-format 'bogus'.
available in this build: tar, dir, appimage
A format past `tar` and `dir` comes from a package in the resolved graph, which declares
it with `mcpp::provides_pack_format("<name>")` in its build program. Add the package
that provides 'bogus' to [build-dependencies] and activate its feature.
这次拒绝发生在任何东西被编译之前。怎样写这样一个包,见
产出可分发物;
引擎为此新增的三样东西是:一棵 artifact action 可以消费的暂存树、
[package] 里其余字段进入构建程序、以及这次分派本身。每一样都与具体格式无关
—— 「与格式无关」正是判断某样东西该不该进引擎的判据。
被分派的格式作用于一个程序 target —— kind = "app" 也不例外,无论这一行
把它链接成什么文件(见 04 §2.2)。库包发布的是一份接口
加上按三元组给出的预构建产物,没有单独一棵暂存树,所以
mcpp pack <库> --pkg-format <name> 会被拒绝,而不是被忽略。
产物是共享目标文件的 kind = "app" target 可以接受一个以上的 --target
(mcpp 2026.9.13.2+):在每一行 Android 上,应用就是平台加载的那个共享库,
因此 mcpp pack myapp --target aarch64-linux-android --target x86_64-linux-android
会构建两条腿并把它们暂存进同一棵树,与库包本已具备的多三元组做法完全一致。
每条腿落在 lib/<abi>/lib<name>.so(aarch64 → arm64-v8a,x86_64 →
x86_64),各自的闭包暂存在它旁边(见
Android),声明的部署文件只暂存
一次,随后只对这棵合并后的树跑一次分派 —— 这正是 dist-apk 这样的成员能构建
出一个通用 APK 的原因。只给一个 --target 时,今天这种扁平的
lib/lib<name>.so 布局保持不变。产物在任何被请求的一行上是可执行文件的
target,第二个 --target 依旧被拒绝:为多个三元组打包同一个可执行文件需要
多个可执行文件,那是另一种机制(lipo 的通用二进制),不在这份能力之内。
-o 接受裸文件名时,输出落在 target/dist/ 下;含目录(相对或绝对)时,按
字面路径输出。
完整选项参见 mcpp pack --help。
mcpp run --target x86_64-linux-android --pkg-format apk
mcpp run --target aarch64-ios-sim --pkg-format app--pkg-format 与 mcpp pack 用的是同一个 flag,这里复用它是为了覆盖一种普通
mcpp run 够不到的情形:一个 Android 应用程序是一个 .apk,一个已安装的 iOS
应用程序是一个 .app,两者都不是 mcpp run 默认执行的链接产物。
mcpp run --pkg-format <name> 先为 <name> 打包 —— 与 mcpp pack --pkg-format <name> 相同的两遍构建与暂存树 —— 再经由为一个程序解析出的 runner 运行打包
报出的那个产物:先看项目的 [target.<triple>] runner,其次看依赖的
mcpp::runner(...),再次看载荷描述文件的。
打包报出的产物是这次请求的终端产物:在这次请求引入的 artifact 动作中,
没有被任何其他被引入的动作当作输入的那一个输出。一个提供者常常是一条链
(dist-apk:链接、加库、对齐、签名),链上每个输出都会被核验存在,但只有
最后一个是发布物,也只有它以 Packed 报出。链的末端产出两个文件的格式会被
mcpp run --pkg-format 拒绝并点名两者,因为一个 runner 只接受一个操作数。
一个格式可能报出很多输出,发布目录中的每个文件各一个。不超过八个时逐行打印;更多时按它们
在共同目录之下所处的条目打印,并附数量(Packed Release/app (1309 files)),--verbose
打印全部(2026.9.29.5+)。--message-format json 在两种情况下都列出全部输出。
未知的 <name> 会被拒绝,点名已解析图提供的格式集合,与
mcpp pack --pkg-format bogus 报出的是同一个集合。--pkg-format 与 --no-runner
同时出现会被拒绝 —— 一个 .apk 或一个已安装的 .app 无法被直接执行。在
kind = "app" 的形态是一个库的那一行上、不带 --pkg-format 运行它,同样会被
拒绝,点名该 flag 与同一个格式集合。mcpp test 不受影响:测试二进制在每一行
上都是一个程序。
与 mcpp build 相比有两点不同,都源于同一个原因:「这个产物要离开本机」。
profile 的兜底值是 release,不是 dev。 其余优先级不变 ——
--profile 优先于 [build] default-profile,后者优先于兜底值。只有最后
一步不同,因此已经声明过 profile 的工程仍然拿到它声明的那个,mcpp pack 也
绝不会产出一个 mcpp build 在同样条件下产不出来的 flag 组合。
有一条后果值得知道:裸
mcpp build与裸mcpp pack现在会写进不同的target/<triple>/<fingerprint>/目录 —— 指纹把 profile 算了进去。手工放到 构建产物旁边的文件(一个 DLL、一份数据)因此只在两条命令解析到同一个 profile 时才会被pack看见:要么把它写进[build] default-profile,要么 两条命令都带上--profile。声明式通道([runtime] deploy_files、runtime_search_dirs)不受影响。
调试信息会被剥离,发布者的路径随之消失。 未 strip 的产物携带 DWARF,而 DWARF 携带发布者源码树与构建目录的绝对路径。具体剥离什么取决于产物是什么 —— 这是 dh_strip 的分档方式,而其中归档那一行是要紧的:
| 产物 | strip 参数 | 不能剥得更彻底的原因 |
|---|---|---|
| 可执行文件 | --strip-all |
没有谁会链接它 |
| 共享库 | --strip-unneeded |
保留 .dynsym —— 那就是导出表 |
| 静态归档 | --strip-debug --enable-deterministic-archives |
--strip-all 会删掉归档的符号索引,消费方链接时随即报出 archive has no index; run ranlib to add one |
三档都会去掉 .comment 与 .note。段的删除按精确名字匹配,因此
.note.gnu.build-id 会被保留,--add-gnu-debuglink 仍然有东西可以配对。
--no-strip(或 [pack] strip = false)按构建原样发货。
--debug-symbols <dir> 则是分离而不是丢弃:写出 <dir>/<产物>.debug,并给
发货的产物加上指向它的 .gnu_debuglink —— 这正是调试器与 debuginfod 所
遵循的约定。
[pack] strip不是[profile.<name>].strip。后者给链接加上-s, 既碰不到静态归档,也无法分离出任何东西;前者管的是包里带什么。两个不同 的决定,两个不同的名字。
图构建出来的东西,在每一条把调试信息放在映像内部的行上都会被剥离;来自 store 或宿主的库不会(mcpp 2026.9.16.1+)。规则是 dh_strip 的「一个包 只剥离它自己构建的东西」,覆盖范围如下:
| 暂存的文件 | 是否剥离 | 原因 |
|---|---|---|
| 程序 | 是,按可执行文件剥离;在 Android 行上按共享库剥离,因为那里的程序本身就是共享库 | 本次构建编译了它 |
图中 SharedLibrary 链接单元产出的共享库 |
是,--strip-unneeded |
本次构建从源码编译了它 |
工具链自身运行时的暂存副本(NDK 的 libc++_shared.so) |
是,--strip-unneeded |
树里的副本不是那份共享载荷本身;Android Gradle 插件也会剥离同一个文件 |
| 来自 store、宿主,或某个包部署的预构建库 | 否 | 它的字节属于它的发布者,供应商的库可能带着签名 |
Mach-O 与 MSVC 的 PE 把调试信息放在映像之外,因此那里什么都不剥离,Packing
这一行只在确实发生剥离的那一行上写「stripped」。--no-strip 与
--debug-symbols 管辖每一个被剥离的文件。自己暂存库的构建程序,通过
mcpp::pack_strip() 与 mcpp::pack_debug_symbols_dir() 读到同一个决定
(30 —— build.mcpp)。
tarball 的内容包在一个顶层目录里,该目录的名字与 tarball 文件名(去掉
.tar.gz 后)保持一致 —— 这样图形界面的「右键解压」和命令行的 tar -xzf
都会得到同一个自包含的目录,而不会把内容散落到当前路径下。
闭包会被记录,一个不完整的闭包会使归档被拒绝(mcpp 2026.9.14.2+)。
暂存树旁边的 <tree>.stage-manifest 列出闭包解析过的每一个库名:树里携带的
库给出其暂存路径,目标机自己提供的库记为 platform,哪里都找不到的记为
unresolved(行格式见 50)。只要有一个名字
是 unresolved,--pkg-format tar 与 --pkg-format dir 就会失败并点名它,因为一棵
缺了这个库的树在安装处无法启动;被分派的格式仍然会收到这棵树,并带上
closure = not-walked。目标机提供、而本机没有的库,写进
[pack.bundle-project] also_skip。不打包任何东西的 mode(system、
static)不记录闭包。
target/dist/myapp-0.1.0-x86_64-linux-musl-static.tar.gz
└── myapp-0.1.0-x86_64-linux-musl-static/
├── bin/myapp ← fully static ELF (no PT_INTERP / RUNPATH)
├── myapp ← top-level entry point (thin shell wrapper, run ./myapp directly)
├── README.md ← copied automatically from the project root
└── LICENSE
target/dist/myapp-0.1.0-x86_64-linux-gnu.tar.gz
└── myapp-0.1.0-x86_64-linux-gnu/
├── bin/myapp ← dynamically linked, RUNPATH=$ORIGIN/../lib
│ PT_INTERP=/lib64/ld-linux-x86-64.so.2
├── lib/
│ ├── libcurl.so.4 ← project third-party dependency
│ ├── libssl.so.3
│ └── ...
├── myapp ← top-level entry point
├── README.md
└── LICENSE
跳过列表遵循
PEP 600 / manylinux2014 ——
默认假定目标系统已经具备 libc、libm、libstdc++、libgcc_s、
ld-linux-* 一类基础库,因此不把它们打进 tarball。
target/dist/myapp-0.1.0-x86_64-linux-gnu-bundle-all.tar.gz
└── myapp-0.1.0-x86_64-linux-gnu-bundle-all/
├── bin/myapp
├── lib/
│ ├── ld-linux-x86-64.so.2 ← complete loader and libc
│ ├── libc.so.6
│ ├── libstdc++.so.6
│ ├── libgcc_s.so.1
│ └── ...project dependencies
├── myapp ← one of two entry points
├── run.sh ← the other entry point (identical contents)
├── README.md
└── LICENSE
带 -o foo.tar.gz 时,顶层目录名也会变成 foo(包名与目录名始终保持一致)。
ELF 规范禁止 PT_INTERP 使用 $ORIGIN,因此在 self-contained 模式下,
loader 经由 run.sh(以及顶层的同名 wrapper)以绝对路径的方式被调用:
exec "$here/lib/ld-linux-x86-64.so.2" --library-path "$here/lib" "$here/bin/myapp" "$@"上面的布局与 wrapper 以 x86_64 为例。打包器会按 target 推导 loader 的名字;
在 aarch64 上它是 ld-linux-aarch64.so.1。
「由 loader 启动」带来一个上述布局看不出的后果:内核会把 /proc/self/exe
设为指向 loader,而不是程序本身;/proc/self/cmdline 里也混入了
--library-path 这个参数。于是每一处「到可执行文件旁边找资源」的逻辑,解析
出来的都是 lib/ 而不是 bundle 根目录 —— 而且是静默发生的。实际表现
是:GUI 工具箱因为找不到字体而渲染出空白文字、一个 assets/ 目录看起来像是
缺失了、随包分发的辅助二进制找不到自己的位置。从 /proc/self/cmdline 解析
argv 的代码,拿到的会是混入了 loader 参数的结果。
这只影响 self-contained。vendored、system 与 static 携带的都是内核
能直接使用的 PT_INTERP,/proc/self/exe 在那三种模式下是正确的。
wrapper 为此导出了 MCPP_BUNDLE_DIR(bundle 根目录)。应优先按它解析,
只在它未设置时才回退:
const char *base = getenv("MCPP_BUNDLE_DIR"); /* set by run.sh */
if (!base) {
/* not launched through the wrapper — /proc/self/exe is trustworthy */
}如果应用本身改不了 —— 比如某个第三方 GUI 框架自己做路径解析 —— 改用
--mode vendored。它把 PT_INTERP 重新指向宿主的 loader,代价是要求宿主的
glibc 版本不低于产物构建时所用的那份。
target/dist/myapp-0.1.0-wasm32-emscripten.tar.gz
└── myapp-0.1.0-wasm32-emscripten/
├── bin/myapp.js ← the launcher; the file a runner executes
├── bin/myapp.wasm ← implicit output of the link, staged with it
├── bin/myapp.data ← present only when the link carries --preload-file
├── README.md
└── LICENSE
打包器暂存启动器,以及同一次链接在它旁边写出的每一个 myapp.<任意后缀> ——
即词干家族。myapp.wasm 是必需的:链接边把它声明为隐式输出,它的缺席说明
构建目录与图对不上,mcpp pack 因此会拒绝,而不是暂存一个没有模块的启动器。
myapp.data 以及 emcc 用同一个词干写出的其余文件(myapp.worker.js、
myapp.wasm.map)是可选的,链接产出了才会跟着走。
一个 Windows target 产出的是一个 .zip,不是 .tar.gz,并且是扁平
布局:
target/dist/myapp-0.1.0-x86_64-pc-windows-msvc.zip
└── myapp-0.1.0-x86_64-pc-windows-msvc/
├── myapp.exe
├── vcruntime140.dll ← only under cxx_runtime = "toolchain-coupled"
├── mydep.dll ← third-party dependencies
├── README.md
└── LICENSE
没有 bin/ + lib/ 的分层,也没有入口 wrapper —— 这两点都不是风格选择。
Win32 的 loader 从可执行文件所在目录解析一个 DLL;PE 没有 RUNPATH 可以
指向别处,因此「放在 .exe 旁边」就是 ELF 上 $ORIGIN/../lib 提供的
那个机制。
Windows 自己的 DLL 永远不会被打进包里 —— kernel32.dll、ntdll.dll、
ucrtbase.dll,以及 api-ms-win-* 一族 API set。携带一份操作系统组件的私有
拷贝,不是让包变大,而是让程序坏掉(进程里出现了两份本应唯一的东西),
Microsoft 的再分发条款也从另一个角度说了同一件事。[pack.bundle-project] force_bundle 仍然可以覆盖这条规则,就像它能覆盖 ELF 的跳过列表一样。
vcruntime140.dll 与 msvcp140.dll 不是 Windows 自己的:它们属于 MSVC
toolset,正如 libstdc++.so 属于 gcc。它们要不要随产物一起走,由
cxx_runtime 决定(见 docs/04-mcpp-toml.md),不由上面这张表决定 ——
而 mcpp pack 会拒绝那些无法兑现契约的组合:
$ mcpp pack --mode system # with cxx_runtime = "toolchain-coupled"
error: cxx_runtime = "toolchain-coupled" and --mode system contradict each other.
这是可行的,而且不是什么特殊模式 —— 只需针对 Windows target 构建,然后打包:
mcpp pack --target x86_64-windows-gnu # from a Linux hostmcpp pack 曾经直接拒绝 Windows。真正的原因不在打包工具:ELF 的依赖闭包是
靠把产物跑起来(用 LD_TRACE_LOADED_OBJECTS)求出来的,这既跨不了操作
系统,也跨不了架构。PE 的闭包改为直接从文件的导入表里读出来,因此不需要
执行任何东西,打包所在的宿主也就自由了。压缩包本身同样由 mcpp 自己写出,理由
相同 —— 不是每个宿主上都存在某个 zip 工具。
有两点值得知道:
- 条目在变小时由 zlib DEFLATE 压缩,否则存储;
--compress gzip:<level>设定级别,--compress none存储全部条目。未给出格式时 Windows 程序写 zip;显式给出--pkg-format tar则写 tar。 - 压缩包是确定性的:不读取任何时间戳,因此同一棵树打两次包字节完全一致, 公布出去的校验和才有意义。
反方向 —— 在 Windows 上给 Linux 产物打包 —— 仍然不支持:ELF 程序的闭包要靠 目标自己的动态链接器解析,而 Windows 宿主没有办法运行它。Mach-O 程序的闭包是 从文件里读出来的,因此一个 macOS 程序可以在任何宿主上打包。
target/dist/myapp-0.1.0-aarch64-macos.tar.gz
└── myapp-0.1.0-aarch64-macos/
├── bin/myapp
├── bin/libmydep.dylib ← a dependency's dylib, beside the program
├── myapp ← entry-point wrapper
├── README.md
└── LICENSE
Mach-O 程序的闭包从它的 load command 里读出(mcpp 2026.9.14.2+),因此打包
时不运行任何东西。它的 dylib 暂存在它旁边的 bin/ 下,这也正是一个图内构建的
dylib 的消费者,其链接所带的 @loader_path rpath 本就会查找的位置:不编辑
任何 load command,不重新签名,也不 strip 这个程序。
一个被需要的名字,只有在加载器能把它解析到程序所在目录时才会被暂存:
| 名字 | 处理方式 |
|---|---|
/usr/lib/…、/System/Library/… |
不暂存;由操作系统提供 |
@rpath/<file> |
当需要它的映像或程序本身,有一条恰好等于 @loader_path 或 @executable_path 的 rpath 时暂存 |
@loader_path/<file>、@executable_path/<file> |
暂存到该路径,该路径必须留在程序所在目录之内 |
任何其他名字都会使闭包不完整,tar 与 dir 会拒绝并点名它。最常见的情形是
落在操作系统目录之外的绝对 install name:加载器会在每一台机器上都从那个路径
去读,无论树里携带了什么。--mode system 与 --mode static 在这一行上不
打包任何东西,与 PE 相同。kind = "lib" / "shared" 的 target 走库打包
路线,不受影响。
ok 映像经 okrun 在其架构的每个系统上运行(见 24),怎样打包由它的平台陈述
(35)。mode 的含义不变 —— 运行该包的机器必须提供什么:
| Mode | 包内内容 | 机器需提供 |
|---|---|---|
system |
下文的树:lib/<arch>/<name>.oke 与启动器 |
okrun 与它所需的每个映像 |
vendored(默认) |
树,带映像所需的 .okso 映像 |
okrun |
self-contained |
树,带 --systems 的每个系统与每个架构的运行器,位于 libexec/okrun/<os>-<arch>/ |
无 |
static(--linkage static --mode self-contained) |
每个架构一个静态 .oke 的 self-contained 树 |
无 |
一条腿与多条腿的布局相同(2026.10.30.1+)。--linkage static 构建不带 .okso 的映像。以平台载体的段(ELF 与
COFF 上为 .oke,Mach-O 上为 __OKE,__carried)携带映像的本机程序是单文件(--pkg-format single-file,见下文),
且只是单文件。
target/dist/myapp-0.1.0-x86_64-linux-android/
├── lib/libmyapp.so ← the application object
├── lib/libmydep.so ← a dependency's shared library
├── lib/libc++_shared.so ← the NDK's C++ runtime, when the object needs it
└── bin/<to>/… ← files `[runtime] deploy` placed
在 Android 行上,应用是一个暂存在 lib/ 下的共享目标文件,它的闭包从文件中
读出并暂存在它旁边(mcpp 2026.9.14.2+)。设备提供的名字不会被暂存:即出现在
该 API level 桩目录中的名字,这个目录由 mcpp 向该行自己的编译器询问得到(即
编译器找到 libc.so 的那个目录)—— libc.so、libm.so、libdl.so、
liblog.so 以及其他平台库。其余每一个名字都必须能在链接用过的某个目录中
解析到:构建的输出目录、图中各包的 [runtime] library_dirs 与
link_library_dirs,以及编译器的库搜索路径(libc++_shared.so 就在其中)。
哪里都解析不到的名字会使 tar 与 dir 拒绝,并点名它以及被搜索过的那些
目录。
给出多个 --target 三元组时,每条腿暂存在 lib/<abi>/ 下,各自带着自己的
闭包。--mode 不适用于这一行:设备恰好提供了平台库,因此既没有可以留给宿主
的库集合,也没有需要携带的 loader。
声明运行器的平台(openkal-platform 即是)的映像打成一棵树,腿数不论:--target x86_64-openkal 是一条腿的树
hello-0.1.0-x86_64-openkal/;--target openkal 是两条腿的树,以平台命名。一棵树服务它所含的每个系统与每个架构:
target/dist/hello-0.1.0-openkal/
├── bin/hello ← POSIX launcher
├── bin/hello.exe ← Windows launcher (a program)
├── lib/x86_64/hello.oke ← one set of images per leg
├── lib/aarch64/hello.oke
├── libexec/okrun/<os>-<arch>/ ← only with --mode self-contained
├── share/… ← resources, once
└── mcpp-package.json
其余的腿逐条规划、一起构建(Packed leg <triple>)。启动器由平台的 runner 生成:POSIX 脚本以自身目录的上一级为
包的根,按 uname -m(x86_64、amd64 → x86_64;aarch64、arm64 → aarch64)选择架构目录,Git Bash、MSYS 与
Cygwin 的 shell 交给 bin/<name>.exe,以该目录中的映像与自身的参数启动运行器,先在 libexec/<runner>/<os>-<arch>/
中、再在 PATH 上查找运行器。Windows 启动器是为 x86_64-windows-gnu 构建的引擎的程序:从自身路径找到包,选本机
架构的映像,以同样的方式启动运行器;不生成 .cmd。构建的平台为该行提供宿主安排时,启动器在 mcpp 的 openkal 行固定的
平台上构建,而不是项目选择的平台上,因为它使用的 mcpplibs.os 版本固定了该平台的 openkal。包中没有本机架构时以状态 126 退出,写出两者。
[runtime] deploy 放置的文件位于每条腿的映像旁边。
--mode self-contained 时运行器随包携带:对 --systems 的每个系统(linux、macos、windows 取腿的全部架构;
linux-aarch64 取一个组合;all;缺省为本机系统),mcpp 构建不携带任何内容的平台载体 —— 一个像 okrun 一样运行
其第一个参数所指映像的程序 —— 放入 libexec/<runner>/<os>-<arch>/。每个运行器在其行(<arch>-linux-musl、
<arch>-macos、<arch>-windows-gnu)上以平台的托管安排构建,因此无论在哪台机器上打包结果都是同一个程序,不需要
该系统的 SDK 与 C 运行时;Linux 的运行器为静态。本 mcpp 不构建程序的行(x86_64-macos 为 planned;
aarch64-windows-gnu 不在目标表中)不放入包并给出警告。在 Windows 主机上构建的映像与其他主机上的字节不同
(#850):树在 Linux 或 macOS 上打包。
在哪里运行。 打包把 runs_on 写进 mcpp-package.json,并在 Packed 之后写出:self 为凭包内携带的东西即可
运行的 <os>-<arch>,with.<runner> 为机器装有该运行器时可运行的组合(腿的架构 × 平台的 runner.hosts)。
Packed target/dist/hello-0.1.0-openkal-bundle-all
Runs on linux-aarch64, linux-x86_64, macos-aarch64, windows-x86_64
Not covered macos-x86_64 (no runner row: this mcpp builds no program for x86_64-macos)
平台未声明 runner.hosts 时按每个系统 × 每个架构读取。在任何地方都不能运行的树(不携带运行器,平台的运行器组合
也不含它的架构)被拒绝。
多台机器打出的包以 mcpp pack --merge a.zip --merge b.zip(也可 --merge a.zip,b.zip)合并。输入是本命令写出的
包目录或归档(.zip、.tar、.tar.gz、.tgz、.tar.zst、.tar.xz),不进行任何构建。腿、系统与 runs_on
取并集,每个文件只取一份,结果重新描述。启动器是派生文件:为合并后的腿重新写出,因此两棵单腿的树合并为两条腿
的树,不带 -o 时以平台命名(hello-0.1.0-x86_64-openkal → hello-0.1.0-openkal)。名字、版本、平台、链接或模式不同的输入被拒绝;同一路径有两个摘要时也被拒绝,
写出该路径、两个摘要与两个输入。容器取 --pkg-format,否则取 -o 的扩展名,否则取第一个输入的容器。
容器列表。 --pkg-format tar,okb(或 [pack] format = ["tar", "okb"])构建一次、暂存一棵树,按命名的顺序
从它写出各容器。列表只含同一棵树的容器 —— tar、zip 与提供者的格式 —— 不含 dir 与 single-file;列表要求
-o 为目录;--compress 作用于其中的引擎容器。okb 只含映像,因此格式只有 okb 的 self-contained 树被拒绝
(--pkg-format tar,okb)。mcpp run --pkg-format 只接受一个格式。
--pkg-format single-file 交付一个可执行文件:
| 目标与模式 | 文件 | 启动时 |
|---|---|---|
原生,vendored 或 self-contained |
引擎的启动器,以一个 zstd 帧携带暂存树 | 首次运行把树解到用户缓存,之后从那里启动 |
原生 Linux,--linkage static |
静态启动器加静态程序 | 同上,不依赖任何系统库 |
| ok 映像 | 平台的载体加映像,或全部映像的一个 zip | 载体从内存加载映像,不写盘 |
原生程序的文件为 target/dist/<name>-<version>-<triple>(Windows 上带 .exe),其树暂存在
target/single-file/stage/ 下。启动器是一个小程序,mcpp 每次打包写出它的源码(target/single-file/<name>/,
基于 mcpplibs.os 与 zstd)并为目标构建:Linux 上为静态(musl),macOS 与 Windows 上只引用系统自带的库。载荷是
树的一个 zstd 帧(--compress zstd[:<level>],缺省,或 none)。首次运行把它解到
<cache>/mcpp/single-file/<载荷的 sha256>/,<cache> 在 Linux 上为 $XDG_CACHE_HOME(否则 ~/.cache),
macOS 上为 ~/Library/Caches,Windows 上为 %LOCALAPPDATA%:先解到旁边一个自己的名字,全部文件写完后以不替换
的方式改名到位(.mcpp-complete 标记完整的树;Linux 与 macOS 上该目录只对当前用户可写)。帧携带内容的校验和,
不符的载荷从不解开。随后启动器成为程序 —— bin/<name>、self-contained 的 Linux 树根部的包装脚本,或 Windows
树根部的 <name>.exe —— 带着它的参数,退出状态即程序的退出状态。
ok 映像的文件为 target/dist/<name>-<version>-<os>-<arch>(Windows 上带 .exe),每条腿与该腿架构上
--systems 的每个系统(缺省为本机系统)各一个:--target openkal --pkg-format single-file -o dist/ 每个架构写一个。
--systems all 为本 mcpp 构建程序的每个行写出文件,并写出其余(Not covered);多于一个文件时 -o 只能是目录。映像不引用其他映像时载体只携带它,否则携带全部映像的一个 stored zip(每个映像对齐到
64 KiB),载体的入口经 openkal-run 的 bundle 接口读取(openkal-platform 0.17.2 起)。
WebAssembly 目标没有单文件:--pkg-format single-file 与 --target wasm32-emscripten 在任何编译之前被拒绝
(2026.10.29.1+),它的页面是 dist-web 提供者的格式。
--pkg-format polyglot 把映像的每条腿与每个系统的单文件(未给 --systems 时为 all)组装成一个文件
target/dist/<name>-<version>.exe:
mcpp pack --target openkal --pkg-format polyglot -o dist/
# Runs on linux-x86_64, windows-x86_64, linux-aarch64, macos-aarch64Windows 程序在前,其 DOS 头中是一段 POSIX shell 脚本;其余系统的程序原样附在其后。Windows 把该文件作为程序本身启动,
Authenticode 可以签名它。Linux 与 macOS 经 shell 启动它:sh pg-0.1.0.exe,或在 shell 中输入 ./pg-0.1.0.exe(shell
把内核拒绝执行的文件作为脚本运行);直接以 execve 启动它的程序得到 Exec format error。脚本把本系统的程序复制到单文件
缓存 <cache>/mcpp/single-file/<程序的 sha256>/,规则与原生单文件相同(独立的临时名、.mcpp-complete、不替换的改名、
只用本用户的目录),在缓存中校验其 sha256(sha256sum、shasum 或 openssl)后执行;之后的运行从缓存启动。文件中没有
本系统的程序时以状态 126 退出,并写出它包含的系统。--systems 不含 windows 时文件是一个脚本(#!/bin/sh),系统可直接
执行,文件名不带 .exe。
polyglot 是供人下载运行的形式:它不进 --pkg-format 列表;原生目标被拒绝(其单文件只属于一个系统);mcpp run --pkg-format polyglot 经 sh 启动它;mcpp emit xpkg 拒绝它(xlings 的条目安装一个系统的包,即树或单文件)。
每棵暂存树都带 mcpp-package.json:包、它的平台、三个选择、腿与系统、启动方式,以及每个文件的角色与 sha256。
提供的格式、--merge 与 mcpp emit xpkg 读取它(SPEC-014 §4.5):
{"schema": 1, "name": "hello", "version": "0.1.0", "platform": "openkal-platform@0.18.0",
"linkage": "shared", "mode": "vendored", "legs": ["x86_64-openkal", "aarch64-openkal"], "systems": [],
"launch": {"runner": ["okrun", "{}"], "entry": {"x86_64": "lib/x86_64/hello.oke", "aarch64": "lib/aarch64/hello.oke"}},
"files": [{"path": "lib/x86_64/hello.oke", "role": "image", "leg": "x86_64-openkal", "sha256": "…"}]}每次打包都写出该文件,提供者的格式也不例外。每个文件的 role 为 program、image、library、loader、
launcher、resource 或 metadata;树中 lib/<arch>/ 下的每个文件都是该腿的映像。符号链接不列出。
linkage 为 static、shared 或 declared(每个库按它自己的声明);launch 对单个原生腿为 program,
对树为 runner 加每个架构的 entry;runs_on 为包在哪里运行({"self": [...], "with": {"okrun": [...]}}),
systems 为它携带的运行器。
mcpp emit xpkg --package <归档> [--package …] --url <前缀> [-o <文件>] 由本命令写出的包的
mcpp-package.json 生成 xim-pkgindex 条目(spec 2)。树是一个资产,每个系统一项只写 runs_on 覆盖到的架构,
包不携带运行器时以运行器为依赖(deps = { "xim:okrun" }),runs_on 为空的树被拒绝;各系统各架构一个的包,每个系统一项,一个带 ${arch} 的
URL 与每个架构的 sha256。安装把归档的顶层目录移到位,并向 xvm 注册程序(Windows 上为 bin/<name>.exe);各系统的包
把程序放在不同目录时(Windows 的程序在包根,POSIX 的程序在 bin/),按系统注册各自的目录。每个归档各以一个
--package 给出。Linux、macOS 或 Windows 目标的单腿包按三元组的系统与架构描述。程序不是任何系统的程序的包被
拒绝,并写出分发它的途径:wasm32-emscripten 的包发布为页面(dist-web),ok 映像以平台的树(--target openkal)
或单文件发布,Android、iOS 与裸机的包没有条目。
--compress zstd|gzip|xz|none[:level](或 [pack] compress)选择 tar 的压缩(.tar.gz 为缺省,另有
.tar.zst、.tar.xz、.tar)。tar 写出归档,mcpp 用自己的编解码器(zlib、zstd、liblzma)压缩,因此不在
PATH 上查找压缩程序,同一归档在每个宿主上字节相同。zip 的条目以 DEFLATE 压缩(gzip[:level])或存储
(none);单文件的载荷为 zstd 或 none;okb 中的映像以 stored 存放,使加载器原地映射。
[pack] sign = ["<prog>", …, "{}"] 在写容器之前对暂存树中的每个程序、映像、库、加载器与 Windows 启动器各运行一次,也对每个单文件
运行一次;{} 为该文件。polyglot 文件的各程序在组装前逐个签名(Windows 程序除外),组装后的文件最后签名:Authenticode
把证书附在文件末尾,之前的字节与偏移不变。失败使打包停止,写出 argv 与其输出;mcpp-package.json 写出签名后文件的摘要。
打包行为通过 mcpp.toml 中的 [pack] 节配置,常用字段如下:
[pack]
default_mode = "static" # override the normal vendored default for bare `mcpp pack`
linkage = "shared" # the default of --linkage
format = "tar" # the default of --pkg-format; a list: ["tar", "okb"]
targets = ["openkal"] # the default of --target: targets and target sets
systems = ["linux", "macos"] # the default of --systems
compress = "zstd" # the default of --compress
sign = ["signtool", "sign", "{}"] # run on each program, image, library and loader before the container
strip = true # default. false ships the artifacts as built
debug_symbols = "dist/debug" # separate the debug info here instead of discarding it
include = ["share/**", "config/*.toml"] # 工程文件,放在包中同样的相对路径下,只放一份
exclude = ["debug/**"]
# Fine-tune the vendored filtering policy. The configuration key keeps its
# established `bundle-project` spelling.
[pack.bundle-project]
also_skip = ["libcustom.so"] # libraries assumed to exist on the target system
force_bundle = ["libfoo.so"] # bundle even if matched by the PEP 600 list[pack].default_mode 接受规范名 system、vendored、self-contained、static(2026.10.30.1+)与既有拼写
bundle-project、bundle-all。
[pack] universal、[platform] universal 与 --no-universal 不再决定打包的内容(2026.10.30.1+;
51):[pack] universal = true 被拒绝,出路为 [pack] targets = ["openkal"];false 与
[platform] universal 写出提示后忽略;--no-universal 与一个目标同用时写出提示,与目标集合同用时被拒绝。
static mode 还需要在 [target.<triple>] 下配置一套 musl 工具链;完整写法
参见 examples/03-pack-static 中的
mcpp.toml。
.deb、.rpm、AppImage、.msi等分发格式经--pkg-format <name>由包提供;引擎提供tar、dir、zip与single-file。- polyglot 文件只包含本 mcpp 能构建单文件的系统与架构(今天为 Linux x86_64 与 aarch64、macOS aarch64、Windows x86_64), 且只用于 ok 映像;POSIX 系统经 shell 启动它。
- WebAssembly 包既不是单文件,也不是 xlings 条目;它的页面是提供者的格式(
dist-web)。 - ELF 程序的闭包由其动态链接器解析,因此不能在 Windows 上打包 Linux 程序。