Skip to content

Commit 46b1e9a

Browse files
committed
docs(plan): the four general build-infrastructure gaps
Separates what is general build infrastructure from what belongs to the heterogeneous domain, by a stated criterion: an item qualifies when at least two of CMake / Meson / Autotools / Cargo have a counterpart and its reason for existing names no domain concept. RDC fails that test and is left to docs/20; the primitive it would reuse already exists. Four gaps survive, and two of them were smaller than they first looked once main was measured rather than recalled: * config.h generation is NOT an engine gap. All four pieces are present (toolchain_dir/sysroot_dir for the right compiler, a real program to write the file, include-dir to make the package's TUs see it, rerun-if-changed for incrementality). What is missing is a shared probe library, which is a package rather than an engine change. * package layout does NOT need a new section. `[runtime].artifacts` already declares a relative path plus a role; the white list is missing exactly one role -- a data file read by a loader outside this package. Adding a section would have duplicated an answer another section already gives, which docs/05 Appendix A refuses. The two that remain are an `exports` declaration rendered per platform (one neutral statement, three renderings, the same shape `[runtime]` already established) and a generic `link-flag` directive, which is the member the link-lib / link-search / link-script family is missing and the escape hatch a generated version script needs. Every criterion is two-sided, and C6 is the one that cannot be verified on a developer machine: a probe implementation that wrongly reads the host is green wherever /usr/bin/cc exists, so it has to run in the hermetic container job.
1 parent 325e5ff commit 46b1e9a

1 file changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
# 构建系统的四处通用缺口
2+
3+
2026-09-07。本文只处理**通用构建基础设施**:其他构建系统都有对应物、与异构无关、
4+
与本仓库的领域无关的那些能力。异构方向特有的缺口(RDC)不在本文,理由见 §0.2。
5+
6+
## 0. 范围
7+
8+
### 0.1 判据:什么算"通用"
9+
10+
一项能力进入本文,当且仅当 CMake / Meson / Autotools / Cargo 中至少两个有对应物,
11+
且它的存在理由不引用任何领域概念(设备、加速器、内核)。
12+
13+
|| 对应物 | 结论 |
14+
|---|---|---|
15+
| 导出面 | CMake `CXX_VISIBILITY_PRESET``WINDOWS_EXPORT_ALL_SYMBOLS`;Meson `vs_module_defs` | 通用 |
16+
| 通用链接标志 | Cargo `cargo:rustc-link-arg` | 通用 |
17+
| 包内布局 | `install(FILES ...)` / Meson `install_data` | 通用 |
18+
| 探测库 | `check_include_file` / `check_function_exists` / `check_type_size` | 通用 |
19+
| 配置头 | `configure_file` | 通用,但**不是引擎改动**(§5) |
20+
21+
### 0.2 RDC 为什么不在本文
22+
23+
relocatable device code 的存在理由是"设备编译器把 `__device__` 函数的跨 TU 调用
24+
推迟到一次设备链接"。这句话无法脱离设备概念陈述,因此它属于 docs/20 的领域,
25+
不属于本文。
26+
27+
它复用的原语在引擎里已经存在:`mcpp::action``object` 归宿就是"一个外部步骤
28+
产出的对象加入普通链接",SYCL lane 的 `sycl_device_link.o` 正是这个形状。所以
29+
RDC 是一个 rule 包的工作量,不是本文四项中的任何一项。
30+
31+
## 1. 现状,实测
32+
33+
四条,均在 2026-09-07 于 `main` 上核实:
34+
35+
1. 引擎认识 25 条 `mcpp:` 指令。链接相关只有三条:`link-lib``link-search`
36+
`link-script`(即 `-T`)。**不存在通用的链接标志出口。**
37+
2. 共享库的导出面在两个平台上都是"全导出":ELF 走默认 visibility;PE 由
38+
`src/build/coff_exports.cppm` 自动生成 `.def`,语义对齐 CMake 的
39+
`WINDOWS_EXPORT_ALL_SYMBOLS`**不存在收窄导出面的声明。**
40+
3. `[runtime].artifacts` 已经是"包内相对路径 + role + provenance"的声明,role 是
41+
封闭白名单。**包内布局不需要新 section。**
42+
4. 配置头生成的四块拼图都在:`toolchain_dir()` / `sysroot_dir()` 给出正确的编译器
43+
与 sysroot,build.mcpp 是真正的程序因而能写文件,`include-dir` 让本包 TU 看见,
44+
`rerun-if-changed` 保证增量。**这一项不是引擎缺口。**
45+
46+
## 2. 缺口一:导出面
47+
48+
### 2.1 问题
49+
50+
一个 `.so` / `.dylib` / `.dll` 目前只有一种导出策略:全部导出。这在两类项目上不成立。
51+
52+
**ICD / 插件。** Vulkan loader 只按名字取 `vk_icdGetInstanceProcAddr`
53+
`vk_icdNegotiateLoaderICDInterfaceVersion`。一个把内部符号一并导出的 ICD,会与
54+
loader 以及同进程内的另一个 ICD 撞名。
55+
56+
**一个镜像里两个 C++ 运行时。** 实测:SYCL 示例构建时 mcpp 自己的重复符号检查报告
57+
58+
warning: sycl-saxpy: 68 symbols in this image are also provided by a library
59+
it loads. _Unwind_DeleteException() _Unwind_GetGR() ...
60+
61+
`libsycl.so` 对着 libstdc++ 编译,mcpp 产物链 libc++,双方都导出 unwinder 符号。
62+
收窄导出面是这类问题的标准解法。
63+
64+
绕过办法今天存在:`cflag` / `cxxflag``-fvisibility=hidden`。它能用,但它是标志
65+
不是声明,而且 PE 上没有对应物 —— 那里只有"全导出"或作者自己手写 `.def`
66+
67+
### 2.2 设计:一个中立声明,三种平台渲染
68+
69+
```toml
70+
[targets.mydriver]
71+
kind = "shared"
72+
soname = "libmydriver.so.1"
73+
exports = "abi/mydriver.exports" # 或内联:exports = ["vk_icd*"]
74+
```
75+
76+
文件内容是符号模式,一行一条,`#` 起注释:
77+
78+
```
79+
vk_icdGetInstanceProcAddr
80+
vk_icdNegotiateLoaderICDInterfaceVersion
81+
```
82+
83+
引擎按平台渲染同一份声明:
84+
85+
| 平台 | 渲染为 |
86+
|---|---|
87+
| ELF | version script,经 `-Wl,--version-script=` |
88+
| Mach-O | `-exported_symbols_list` |
89+
| PE | `.def`,**取代**自动生成的全导出版本 |
90+
91+
**这与 `[runtime]` 的既有先例同构。** `[runtime]` 存在的理由正是"一句中立的话,由
92+
引擎按方言渲染",而不是让作者写三份平台专用文件。导出面是同一形状的第二个实例,
93+
因此它不是一个新概念,是一条既有原则的应用。
94+
95+
### 2.3 声明 `exports` 隐含编译期默认 hidden
96+
97+
仅有 version script 会收窄动态符号表,但对象里的符号仍是默认可见性,链接期优化拿
98+
不到收益,而且 Mach-O 与 PE 的渲染需要编译期配合。因此:**声明 `exports` 时,引擎
99+
同时把编译期默认置为隐藏**(ELF/Mach-O 的 `-fvisibility=hidden`)。
100+
101+
这是一处行为变化,必须写进文档:一个此前依赖默认可见性做跨 DSO 内部调用的项目,
102+
在声明 `exports` 之后会链接失败。这正是作者声明 `exports` 时所要求的语义,失败点
103+
也在链接期而非运行期,因此是可接受的。
104+
105+
### 2.4 不做什么
106+
107+
- **不做符号版本的完整语法。** `foo@@LIB_1.0``foo@LIB_0.9` 并存是 ELF 独有的
108+
能力,无法中立表达。需要它的项目把 map 文件签入仓库,经 `[build] ldflags` 使用;
109+
需要**生成** map 的项目走 §3 的出口。
110+
- **不做 per-symbol 的属性宏。** `__declspec(dllexport)` 那一套是源码的事。
111+
112+
## 3. 缺口二:通用链接标志
113+
114+
### 3.1 问题
115+
116+
`link-lib``link-search``link-script` 之外没有出口,因此**构建程序算出来的**链接
117+
标志无法送达。三个具体场景:
118+
119+
| 标志 | 谁需要 |
120+
|---|---|
121+
| `-Wl,--version-script=<生成的 map>` | 导出面随 feature 组合变化的库(§2.4) |
122+
| `-Wl,--wrap=malloc` | 接管 C 库符号的运行时:内存池、tracing、sanitizer |
123+
| `-Wl,--exclude-libs,ALL` | 静态吞入的第三方库不得再导出,否则其符号成为本包 ABI 的一部分 |
124+
125+
第三条与 §2 是同一问题的两半:一半管自己的符号,一半管吞进来的符号。
126+
127+
### 3.2 设计
128+
129+
```
130+
mcpp:link-flag=<flag> mcpp::link_flag(s)
131+
```
132+
133+
按发出顺序追加,位置在 manifest 的 `[build] ldflags` 之后。
134+
135+
### 3.3 传播性:私有
136+
137+
`link-flag` **只作用于本包的链接,不到达消费者**,与 `include-dir` 同规。理由相同:
138+
一个依赖发出的任意标志落到消费者的链接行上,正是 `include-dir` 的私有性所要避免的
139+
耦合。
140+
141+
`link-script` 是既有的例外,而它的例外理由在文档里写得很清楚 —— 板级内存布局是
142+
消费者无法自行写出的东西。任意标志不具备这条性质,因此不继承这个例外。
143+
144+
依赖确实需要改变消费者链接方式的情形,已有 `[runtime]` 的 link intent 承担,且那条
145+
路径是中立的、可按方言渲染的。
146+
147+
## 4. 缺口三:包内布局
148+
149+
### 4.1 问题不是 `install()`
150+
151+
mcpp 世界里没有系统前缀:消费者解析包,不扫路径。`[resources]` 是把资产**嵌入产物**
152+
(图标、版本元数据),不是安装。因此 `install(FILES ... DESTINATION /usr/share)` 这个
153+
形状在这里是错的。
154+
155+
真实需求窄得多,且**只有被第三方按路径扫描的文件才有**:
156+
157+
| 机制 | 谁扫 |
158+
|---|---|
159+
| `/usr/share/vulkan/icd.d/*.json`,或 `VK_DRIVER_FILES` 指向的文件 | Vulkan loader |
160+
| `OCL_ICD_FILENAMES`(追加语义)/ `OCL_ICD_VENDORS`(替换语义) | OpenCL ICD loader |
161+
| 任意 dlopen 插件目录 | 宿主程序 |
162+
163+
关键点:**那个 JSON 不是给 mcpp 消费者读的,是给 loader 读的。** 它必须是包内某个
164+
确定相对路径上的真实文件。
165+
166+
### 4.2 设计:扩 role,不开新 section
167+
168+
`[runtime].artifacts` 已经是"包内相对路径 + role + provenance"的声明。按 docs/05
169+
附录 A 第二条(一个键若重复了别处已给出的答案则不予准入),这里**不得**新开 section。
170+
171+
新增一个 role:
172+
173+
```toml
174+
[runtime]
175+
artifacts = [
176+
{ role = "library", path = "lib/libmydriver.so.1", provenance = "built" },
177+
{ role = "manifest", path = "share/vulkan/icd.d/mydriver.json", provenance = "built" },
178+
]
179+
```
180+
181+
`role = "manifest"` 的含义:**一个被本包之外的加载器按路径读取的数据文件**。它与
182+
`library` 的区别不是格式,是读者 —— 这是 role 白名单里唯一缺的那一类。
183+
184+
**已知约束**:打包之后 `runtime.artifacts` 是封闭白名单,而已发布的描述符会跳过它
185+
不认识的键。因此新增 role **必须**先落地引擎、发布,再由包使用;顺序反了会让老
186+
引擎静默丢掉这条 artifact。这与 SPEC-004 §4.3 的规则同源。
187+
188+
### 4.3 生成文件与路径回填
189+
190+
ICD JSON 的内容里要写 `.so` 的位置,而那是构建期才知道的。两条约束:
191+
192+
1. JSON 里写的**必须**是相对于包根的路径,不得是构建目录的绝对路径。否则包一经
193+
移动或分发即失效 —— 这是本仓库已经付过学费的形态(载荷内嵌绝对路径)。
194+
2. 因此生成它的是 build.mcpp,而声明它的是 `[runtime].artifacts`。二者的接缝需要
195+
一条指令让构建程序贡献一个 artifact 条目:
196+
197+
```
198+
mcpp:artifact=<role>=<relpath> mcpp::artifact(role, relpath)
199+
```
200+
201+
**开放问题**:`mcpp pack` 目前决定哪些文件进入产物。构建程序贡献的 artifact 条目
202+
与 pack 的选择规则如何合并,需要在实现前读 `src/pack` 确定,本文不预设答案。
203+
204+
## 5. 缺口四:探测库(是包,不是引擎)
205+
206+
### 5.1 现状
207+
208+
配置头生成今天就能写(§1.4)。缺的不是能力,是**公共实现**:每个移植过来的 C 项目
209+
都要自己写一遍"这个头在不在""这个函数能不能链上""这个类型多宽"。不做的后果是
210+
CMake 模块生态碎片化的重演 —— 每个项目一份略有差异的 `check_function_exists`
211+
212+
### 5.2 一条硬约束:探测不得读宿主
213+
214+
这是本文唯一一条会被写错的设计。autotools 的探测按构造读宿主,而本生态的不变量是
215+
相反的:**探测必须用生态解析出的编译器与 sysroot 进行**
216+
217+
因此探测库的每个入口都经 `toolchain_dir()` / `toolchain_sysroot()` /
218+
`toolchain_binutils_dir()` 组装命令行,任何一条走 `/usr/bin/cc` 的实现都是错的。
219+
这与 rule 包驱动第二编译器时的规则是同一条(v2 设计 §1 第 1、2 条推论)。
220+
221+
### 5.3 形状
222+
223+
一个包 `mcpplibs:probe`,供 build.mcpp 导入:
224+
225+
```cpp
226+
import mcpp;
227+
import mcpp.probe;
228+
229+
int main() {
230+
mcpp::probe::Ctx cx; // 从 toolchain_* 组装,不读宿主
231+
bool mman = cx.has_header("sys/mman.h");
232+
bool slcpy = cx.links("strlcpy", "#include <string.h>");
233+
int lw = cx.sizeof_type("long");
234+
mcpp::probe::configure_file(cx, "config.h.in", out / "config.h");
235+
mcpp::include_dir(out);
236+
}
237+
```
238+
239+
探测结果必须**按工具链指纹缓存**,否则每次构建重探。缓存键取
240+
`toolchain_fingerprint` 已有的值,不新造。
241+
242+
## 6. 准入自检(docs/05 附录 A)
243+
244+
|| 是否重复了别处已给出的答案 |
245+
|---|---|
246+
| `exports` | 否。没有任何 section 回答"这个产物发布哪些符号" |
247+
| `link-flag` | 否。`ldflags` 是声明式的,本项是构建程序算出来的 |
248+
| `role = "manifest"` | 否,且**刻意复用** `[runtime].artifacts` 而非新开 section |
249+
| 探测库 | 不是键 |
250+
251+
四项均为封闭语法、开放词表:`exports` 的内容由作者定,引擎只负责渲染;
252+
`link-flag` 的内容引擎不解释;role 的白名单加一项而语义由读者定义。
253+
254+
## 7. 判据
255+
256+
每条都要求两侧可测 —— 拿掉实现会红,而不是"没测成"与"通过"同读数。
257+
258+
| # | 判据 |
259+
|---|---|
260+
| C1 | 声明 `exports` 的共享库,`nm -D --defined-only` 只列出声明的符号;不声明时列出全部。两侧都断言,否则"少了几个"与"根本没链上"读数相同 |
261+
| C2 | 同一份 `exports` 在 ELF 与 PE 上各渲染一次,两边导出集合**相同**。跨平台是这条设计的全部理由,单平台绿零信息量 |
262+
| C3 | 声明 `exports` 后编译期默认为 hidden:一个依赖默认可见性做跨 DSO 内部调用的夹具**链接失败**,且失败点在链接期 |
263+
| C4 | 构建程序发出的 `link-flag` 出现在链接命令行上,顺序在 `ldflags` 之后;**且不出现在消费者的链接行上**(私有性的反向断言) |
264+
| C5 | `role = "manifest"` 的文件在打包后位于声明的相对路径上,内容里的路径为包内相对路径。判据读**打包后的产物**,不读构建目录 |
265+
| C6 | 探测库在一台**没有宿主编译器**的机器上仍能完成探测。这是 §5.2 唯一能证伪的判据 |
266+
267+
C6 值得单独说明:它是这批里唯一无法在开发机上验证的判据 —— 开发机总有
268+
`/usr/bin/cc`,一个错误读宿主的实现在那里永远绿。它必须跑在 hermetic 容器里,
269+
本仓库已有 `hermetic e2e (no host toolchain, container)` 这个 job。
270+
271+
## 8. 分期
272+
273+
|| 内容 | 依据 |
274+
|---|---|---|
275+
|| `link-flag` | 最小:一条指令 + 一处透传。且它是 §2.4 的逃生口,应先于 `exports` 落地 |
276+
|| `exports` + 隐含 hidden | 打开"可发布稳定 ABI 的 `.so`"这一档,同时惠及运行时与驱动 |
277+
|| `role = "manifest"` + `mcpp:artifact` | 只有驱动这一档需要;且受 §4.2 的发布顺序约束,越早落地引擎越好 |
278+
|| `mcpplibs:probe` | 与引擎正交,任何时候可做;但 C6 要求它一开始就跑在 hermetic job 里 |
279+
280+
前三期合计的引擎改动量小于 RDC 一项,且互不阻塞。

0 commit comments

Comments
 (0)