Skip to content

Commit c04dd06

Browse files
committed
feat(pack): --format <name> dispatches to a package, and no distribution format lives in the engine (2026.9.11.1)
`tar` and `dir` answer the same question `msi` and `appimage` answer -- what shape does the output take -- so they are values of one flag rather than the beginning of a second one. The split that keeps every other format out of the engine is: `mcpp pack` owns the mechanism and the one universal format, and every other format lives in a package that `mcpp pack` dispatches to. The universal format is what it already produces: an archive that extracts and runs, universal in the only sense that matters here -- it needs no knowledge of anyone else's release. Everything past it does. dpkg's control fields, AppImage's runtime, WiX's schema, Apple's notarisation, Android's signing scheme: each one bound into the engine couples an mcpp release to a release mcpp does not control. The project already made this argument for languages, where Slang is supported without being named in the engine, and a distribution format has less claim to a name in the engine than a language does. Three additions, each FORMAT-NEUTRAL, which is the test for whether something belongs in the engine at all: - A staged tree an artifact action can consume. `mcpp pack` already computes one -- the dependency closure after the strip policy, the debug-symbol split and `include`/`exclude` -- and then compressed it and the directory was gone, so a `.deb`, an AppImage, a `.app` and an `.msi` each had to rebuild it. `${mcpp.stage_dir}` exposes it. - The rest of `[package]` in the build program: `MCPP_PKG_VERSION` / `_DESCRIPTION` / `_LICENSE` / `_AUTHORS` / `_REPO` and the matching `mcpp::package_*()`. Every installer states a version; without these a project restates it in the member's own options, where the copy drifts from `[package]` with nothing able to detect it. - `--format` resolving its value through the graph. A package declares with `mcpp::provides_pack_format("<name>")`; `--format <name>` finds the provider among the resolved dependencies. The refusal for an unknown value names what IS available rather than a constant, and arrives before anything is compiled. DECLARE UNCONDITIONALLY, SUBMIT CONDITIONALLY. This is the load-bearing rule of the dispatch and the one a member author is most likely to get wrong, because a member that gets it wrong still works for whoever wrote it -- they always pass their own format. The declaration must not be gated, or the engine can never answer "which formats does this graph provide"; the submission must be, or a plain `mcpp build` grows an edge it must not have. A format nothing submitted for is refused by name rather than reported as a pack that produced no package. `mcpp pack --format <name>` PREPARES TWICE, AND NOTHING IS RE-DERIVED BETWEEN THE PASSES. An artifact action is a ninja edge and the staged tree is produced after the link, so the tree cannot be an input of the pass that built it. The first pass collects declarations and refuses an unknown format; the build and the staging follow; the second pass sets `pack_format` and `pack_stage_dir` and builds the submitted edge. Its triple and staged path come from what the first pass and `make_plan` already answered -- `stagingRoot` is a function of the resolved triple, and a second derivation of it before prepare is the shape where two answers agree on every machine the author has. build.ninja's header line gains a fourth field, `dist=`, and the fast paths require it to read `none`. The format is deliberately NOT in the fingerprint -- putting it there would cost a full recompile to package an already-built tree -- so the two graphs share a directory, and `target/<triple>/<fp>/build.ninja` is shared mutable state two fast paths replay. That is the third instance of the failure `graph=` and `accel=` each already record. The criterion is a unit test rather than an end-to-end assertion: measured on 2026-09-11, a plain build after the pack pass regenerates the graph even with the field ignored, so an end-to-end check would pass whether or not the field works and would keep passing if it were deleted. `${mcpp.stage_dir}` REFUSES rather than expanding to nothing, in two places: a build that is not packaging, and a role other than `artifact`. An empty path is still a token the command accepts, and the tool then reads the build directory root, which exists -- so the mistake produces a plausible artifact instead of a diagnostic. The measured prototype is a valid, empty, 52 KB installer with nothing said about it. An action that names the placeholder automatically gains a dependency on `<staged tree>.stage-manifest` -- a sibling, never a member, so it never travels inside anyone's installer. The engine adds it because the use implies it: without it the edge is dirty only when a link output changes, and a closure that grew a dependency's shared library while the program's own bytes did not would leave the previous distributable in place, reported as up to date. Build-program protocol v9 (`mcpp:pack-format=`). The row carries a non-empty `tag`, so the declaration is replayed from the build program's cache record -- the pass that reads the set is `mcpp pack`, which is never a project's first build, and an unpersisted declaration would be absent exactly when a user names a format. Tests: 6 unit tests for the staged-tree contract, the graph-shape field in both of its two readers, and the directive row's persistence; one e2e holding the four properties of the dispatch, each with the wrong answer it excludes. Docs: `docs/10` for the `--format` axis, `docs/30` for the three-category taxonomy and the new placeholders and accessors, `docs/31` for the six constraints a distribution member owes its consumer. Both languages.
1 parent 2d10281 commit c04dd06

30 files changed

Lines changed: 2496 additions & 54 deletions

.agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md

Lines changed: 801 additions & 0 deletions
Large diffs are not rendered by default.

.agents/docs/README.md

Lines changed: 6 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-
274 records.
21+
275 records.
2222

2323
## By subject
2424

@@ -40,10 +40,15 @@ Records that declare one. Everything else is listed by date below.
4040

4141
- [Two answers and two silences: the scanner's second grammar, and the manifest keys nothing reads](2026-09-09-two-answers-and-two-silences.md) — active
4242

43+
### plugins
44+
45+
- [The category the plugin taxonomy does not name, and what a platform actually decomposes into](2026-09-11-distribution-plugins-and-platform-decomposition.md) — active
46+
4347
## By date
4448

4549
### 2026-09
4650

51+
- [The category the plugin taxonomy does not name, and what a platform actually decomposes into](2026-09-11-distribution-plugins-and-platform-decomposition.md) — active
4752
- [Two answers and two silences: the scanner's second grammar, and the manifest keys nothing reads](2026-09-09-two-answers-and-two-silences.md) — active
4853
- [A dlopen surface no closure walks, and a process with two unwinders](2026-09-09-dlopen-surface-and-two-unwinders.md) — landed
4954
- [The documentation as a book: a chapter-by-chapter design](2026-09-08-the-documentation-as-a-book.md) — active

CHANGELOG.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,71 @@
55
66
## [Unreleased]
77

8+
## [2026.9.11.1] - 2026-09-11
9+
10+
### `mcpp pack --format <name>` 分派到包,而引擎里不再需要住进任何一种分发格式
11+
12+
`tar``dir` 回答的问题,和 `msi``appimage` 回答的问题是同一个 —— 输出取什么
13+
形状 —— 所以它们是一个 flag 的取值,而不是第二个 flag 的开端。分界是:`mcpp pack`
14+
拥有机制,以及那一种通用格式(一个解开就能跑的归档,它不需要知道任何别人的发布);
15+
其余每一种格式都住在包里。dpkg 的 control 字段、AppImage 的 runtime、WiX 的 schema、
16+
Apple 的公证,其中任何一个被绑进引擎,都会把一次 mcpp 的发布耦合到一次 mcpp 并不控制
17+
的发布上。这与本项目早已为语言做过的论证是同一个 —— Slang 被支持,而引擎里没有它的
18+
名字。
19+
20+
三样与格式无关的东西被加进引擎,「与格式无关」正是判断某样东西该不该进引擎的判据:
21+
22+
- **一棵 `artifact` action 可以消费的暂存树。** `mcpp pack` 一直在算它 —— 依赖闭包,
23+
过了 strip 策略、调试信息拆分与 `include`/`exclude` —— 然后把它压掉,目录就没了。
24+
`${mcpp.stage_dir}` 把它暴露出来。它是一个 **bundle** 树(`bin/``lib/`、可重定位),
25+
这正是 AppImage、`.app``.msi` 要的形状;要一棵 FHS 树的格式(`.deb``.rpm`)自己
26+
负责重排,因为一个文件该落在哪个目录是那个格式的知识。
27+
- **`[package]` 的其余字段进入构建程序。** `MCPP_PKG_VERSION` / `_DESCRIPTION` /
28+
`_LICENSE` / `_AUTHORS` / `_REPO`,以及对应的 `mcpp::package_*()`。每一种安装包格式
29+
都要写版本号;在这之前,项目只能把版本号在成员自己的 options 里再写一遍,而那份副本
30+
会与 `[package]` 漂移,且没有任何东西能发现。
31+
- **`--format` 经图解析它的取值。** 包用 `mcpp::provides_pack_format("<name>")` 声明,
32+
`--format <name>` 在解析后的依赖里找到提供方并把暂存树交给它。未知取值点名**当下确实
33+
可用**的那些,而不是一份固定清单,并且这次拒绝发生在任何东西被编译之前。
34+
35+
**无条件声明,有条件提交。** 这是整套分派最承重的一条规则,也是最容易被成员作者写错
36+
的一条 —— 因为写错了对作者自己仍然照常工作:他永远传的是自己那个格式。声明必须不加闸,
37+
否则引擎永远回答不出「这张图提供哪些格式」;提交必须加闸,否则普通 `mcpp build` 会多出
38+
一条它不该有的边。对一个谁都没为之提交的格式,mcpp 会拒绝并点名,而不是报告一次「什么
39+
包都没产出」的成功打包。
40+
41+
**`mcpp pack --format <name>` 会 prepare 两次,而两次之间没有任何值被重新推导。** 一条
42+
`artifact` action 是一条 ninja 边,而暂存树是 mcpp 在链接**之后**产出的,所以这棵树不
43+
可能成为构建出它自己那一次 pass 的输入。第一趟收集声明并拒绝未知格式;随后是构建与暂存;
44+
第二趟设上 `pack_format``pack_stage_dir` 并构建提供方提交的那条边。第二趟用的三元组
45+
与暂存路径,都是第一趟和 `make_plan` 已经回答过的 —— 重新推导一遍会得到那种「在作者所有
46+
机器上都一致、只在他没有的那台上不一致」的缺陷。
47+
48+
**build.ninja 的头行多了第四个字段 `dist=`,而快路径要求它读作 `none`** 格式故意
49+
**不进指纹**:进了就要为「把一棵已经构建好的树打成包」付一次全量重编。于是两张图落在同
50+
一个目录里,而 `target/<triple>/<fp>/build.ninja` 是被两条快路径回放的共享可变状态 ——
51+
这正是这一行上另外两个字段(`graph=``accel=`)已经各自记过一次的那种失败。判据放在
52+
单元测试里而不是端到端:实测(2026-09-11)即使忽略这个字段,pack 之后的普通构建也会因为
53+
更早的一个新鲜度条件而重新生成图,所以端到端断言无论字段是否生效都会通过,连字段被删掉
54+
都照样通过。
55+
56+
**`${mcpp.stage_dir}` 在两种位置上是拒绝而不是空展开:** 本次构建不在打包时,以及
57+
role 不是 `artifact` 时。一个空路径仍然是命令接受的 token,而工具随后读到的是构建目录
58+
根 —— 那个目录存在,所以这个错误会产出一个看起来合理的产物而不是一条诊断。实测过的原型
59+
是那个「有效的、空的、52 KB 的安装包,并且没有任何诊断」。
60+
61+
写了 `${mcpp.stage_dir}` 的 action 会自动获得一条对 `<暂存树>.stage-manifest` 的依赖
62+
(一个兄弟文件,永不是成员,所以它不会跑进任何人的安装包里)。依赖由引擎添加,因为「用
63+
了」本身就意味着「依赖」:没有它,这条边只在链接产物变化时才变脏,而一个闭包多出了某个
64+
依赖的共享库、同时程序自己的字节没变的情况,会把上一次的可分发物原地留下并报告为最新。
65+
66+
构建程序协议升到 v9(`mcpp:pack-format=`)。这条指令带非空 `tag`,因此会随构建程序的
67+
缓存记录一起被回放 —— 读它的那一趟是 `mcpp pack`,而那从来不是一个项目的第一次构建。
68+
69+
文档:`docs/10`(`--format` 那一个轴)、`docs/30`(三类成员的分类表、新占位符与新访问器)、
70+
`docs/31`(分发成员的六条约束),中英双份。
71+
72+
873
## [2026.9.10.2] - 2026-09-10
974

1075
### dlopen 面检查:不适用的那一趟也会发布记录,并且不会盖掉已经量出来的答案

docs/10-pack-and-release.md

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,55 @@ mcpp pack --mode self-contained # alias: --mode bundle-all
140140
mcpp pack --target x86_64-linux-musl # equivalent to --mode static
141141
mcpp pack --target aarch64-linux-musl # ARM64 equivalent
142142
mcpp pack --format dir # output as a directory, no tarball
143+
mcpp pack --format appimage # a format a package in the graph provides
143144
mcpp pack -o myapp.tar.gz # filename only: lands at target/dist/myapp.tar.gz
144145
mcpp pack -o /abs/path/myapp.tar.gz # includes a directory: output to the literal path
145146
mcpp pack --profile dev # build with a different profile (default: release)
146147
mcpp pack --no-strip # ship the artifacts as built
147148
mcpp pack --debug-symbols dbg/ # write the separated *.debug files under dbg/
148149
```
149150

151+
### `--format` owns one axis, and the engine owns two of its values
152+
153+
`tar` and `dir` answer the same question `msi` and `appimage` answer — what
154+
shape does the output take — so they are values of one flag rather than the
155+
beginning of a second one. The split between what the engine holds and what a
156+
package holds is:
157+
158+
> **`mcpp pack` owns the mechanism and the one universal format. Every other
159+
> format lives in a package, and `mcpp pack` dispatches to it.**
160+
161+
The universal format is what it already produces: an archive that extracts and
162+
runs. It is universal in the only sense that matters here — it needs no
163+
knowledge of anyone else's release. Everything past it does. dpkg's control
164+
fields, AppImage's runtime, WiX's schema, Apple's notarisation, Android's
165+
signing scheme: each one bound into the engine would couple an mcpp release to
166+
a release mcpp does not control. The same argument the project already made for
167+
languages, where Slang is supported without being named in the engine.
168+
169+
So the value set is open (mcpp 2026.9.11.1+). `--format <name>` finds the
170+
package in the resolved graph that declares `<name>` and hands it the staged
171+
tree; an unknown value names what *is* available rather than a fixed list:
172+
173+
```
174+
error: unknown --format 'bogus'.
175+
available in this build: tar, dir, appimage
176+
A format past `tar` and `dir` comes from a package in the resolved graph, which declares
177+
it with `mcpp::provides_pack_format("<name>")` in its build program. Add the package
178+
that provides 'bogus' to [build-dependencies] and activate its feature.
179+
```
180+
181+
The refusal arrives before anything is compiled. Writing such a package is
182+
[Producing a distributable](30-build-mcpp.md#producing-a-distributable-pack_format--stage_dir-20269111);
183+
the engine's three additions are a staged tree an `artifact` action can consume,
184+
the rest of `[package]` in the build program, and this dispatch. Each is
185+
format-neutral, which is the test for whether something belongs in the engine
186+
at all.
187+
188+
A dispatched format applies to a **program** target. A library package ships an
189+
interface plus prebuilt binaries per triple and has no single staged tree, so
190+
`mcpp pack <lib> --format <name>` is refused rather than ignored.
191+
150192
When `-o` is given a bare filename, the output is placed under `target/dist/`;
151193
when it includes a directory (relative or absolute), the literal path is used.
152194

@@ -421,8 +463,13 @@ macOS **program** bundling (the Mach-O dependency closure, via `otool -L` /
421463
`LC_LOAD_DYLIB`, and `install_name_tool` for relocation) is still on the
422464
roadmap; until it lands `mcpp pack <program>` refuses on that format rather than
423465
producing something that only looks like a bundle. Windows DLL bundling beyond
424-
the current `.zip`, and distribution formats such as `.deb` / `.rpm` / AppImage,
425-
are also on the roadmap. This document evolves alongside the
426-
`mcpp pack` implementation; for the latest options, refer to
427-
`mcpp pack --help`.
466+
the current `.zip` is also on the roadmap.
467+
468+
Distribution formats such as `.deb`, `.rpm`, AppImage and `.msi` are **not** on
469+
this list, and that is a decision rather than an omission: they live in
470+
packages and reach the user through `--format <name>`, for the reason the
471+
section above gives. Nothing further needs to join `[pack]`'s built-in modes.
472+
473+
This document evolves alongside the `mcpp pack` implementation; for the latest
474+
options, refer to `mcpp pack --help`.
428475

0 commit comments

Comments
 (0)