Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
895 changes: 895 additions & 0 deletions .agents/docs/2026-09-07-general-build-infrastructure-gaps-design.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions .github/tools/build_examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ BUILD=(
examples/03-pack-static
examples/04-workspace
examples/08-build-rules/app
# The CPU-only path of the multi-backend example: no payloads, and it is
# where `cfg(accelerator = "none")` is exercised. The device paths are
# opt-in via --accel and are covered by the rule packages' own CI.
# Built here for one reason worth the cost: it is the only example whose
# CPU-only configuration exercises `cfg(accelerator = "none")` and two rule
# packages in one build program, and both of those are engine paths that a
# description cannot cover. Its `[toolchain] default = "llvm@22.1.8"` means
# this job installs an LLVM payload it otherwise would not -- the CUDA leg
# takes the clang route, because the nvcc route on the 12.9 line is refused
# by nvcc's own front end and the 13.x line raises the driver floor to r580.
# The device payloads are NOT installed: they are gated on the accelerator,
# and this builds without one.
examples/09-heterogeneous/multi-backend
)

# `key|reason`.
Expand All @@ -38,6 +51,7 @@ SKIP=(
"examples/09-heterogeneous/hip/app|same, for the HIP payloads"
"examples/09-heterogeneous/sycl/app|needs the dpcpp payload (over a gigabyte) and a device its runtime accepts"
"examples/09-heterogeneous/vulkan/app|built AND RUN by the next step of this job, on the lavapipe payload, which needs no GPU"
"examples/09-heterogeneous/cann/app|its device leg needs the Ascend DRIVER, which a runner does not have: the kernel compiles and the object links, and then `libascend_hal.so` is missing, which is correct on a machine with no NPU. Its CPU leg does build -- and is not built here only because the plugins pin would make this job resolve a fifth rule package for one example. Covered by the measurements in its README"
)

# Every ROOT manifest in the tree: a directory with an `mcpp.toml` that has no
Expand Down
122 changes: 122 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,128 @@

## [Unreleased]

### 写第二个后端的时候,才发现接口只对第一个成立

多后端示例本身没建起来,而它暴露的四件事都不是示例的问题。

**设备源可以什么都没编到,而构建成功。** 设备类源是引擎唯一没有编译规则的源:它交给
本包的构建程序,要么作为 action 回来,要么根本不被编译 —— 而**没有任何东西在检查它
回来了**。示例没有 `build.mcpp`,于是两个设备 glob 被静默丢掉,读数是链接期
`undefined reference to opkit_cuda_saxpy`:那条消息点的是符号,从不是那个本该定义它的
文件;`kind = "lib"` 的目标连这条都没有,因为静态库不做解析。现在拒绝,并点名文件,
且区分「根本没有构建程序」与「程序跑了但没有 action 认领它们」——两者的修法不同。
判据取 action 的**输入**,这同时是 action 本就该满足的条件:编译某文件却不声明它为
输入的 action,在该文件变化时不会重跑。

**规则声明的载荷,消费者的构建程序够不到。** 规则的代码跑在**消费者的**构建程序里,
所以 `mcpp::xpkg_dir` 是在那边被问的,而载荷是规则在自己的 `[feature-xlings]` 里声明
的。依赖图早就会安装它;缺的只是回答 —— `fillXpkgDirs` 只读一份 manifest,于是地址被
下载、解包,然后对唯一想用它的那段代码不可见,读数是「工具包没装」而它就在盘上。

**`accelerator` 被当成了「解析出来的层」。** 五个真正的层(`c-abi`、`compiler` …)由
依赖解析回答,所以以它们为谓词的 `[xlings]` 表被拒绝是对的。`accelerator` 不是:它是
`--accel`,或 `[build] accel`,在查找第一个包之前就已读入。混在一起的代价每次构建都在
付 —— 厂商工具包只能无条件声明或者不声明,于是**不带加速器的那次构建**(最便宜的、
也是 CI 跑的那次)会为一个它没在编译的设备下载数 GB。现在
`[target.'cfg(accelerator = "cuda")'.xlings.workspace]` 与同谓词下的
`[dependencies]` 都生效。

**忘了 `host-module = true`,消息里没有这个词。** 实测读数是 GCC 的
`failed to read compiled module` 加一句「imports 必须先被构建」—— 都对,而既没点出会
提供这个模块的包,也没点出那个让它可导入的键。构建程序能编译的名字是一个闭集
(`std`、`std.compat`、内置 `mcpp`、可导入的 host 模块),所以集合之外的名字在编译器
被调用**之前**就被拒绝,并列出声明了却没写 `host-module = true` 的依赖。

配套判据:e2e 622(规则载荷跨到消费者)、623(设备源三条腿,含 `--no-accel` 的反向腿)、
624(点名拒绝,且它给的修法真的能修好)、625(按加速器开关载荷,两个方向)。622 与 625
都在已发布的 2026.9.6.4 上跑过对照并如期失败。

### 两个新示例,以及一处所有既有示例都写错了的地方

**`examples/09-heterogeneous/multi-backend`** —— 多个后端进**同一个产物**,运行期选择。
既有四个示例每个都是**一道接缝**:设备文件与 CPU 文件定义同一个符号、永不同时进入一次
链接,所以恰好存在一个,选择在构建期做完。那是程序的正确形态。库做不了这个选择:它
只编译一次,而消费者的机器各不相同,所以它的后端是**叠加**的。这个示例是那种形态。

它同时是 `cfg(accelerator = "none")` 的第一个真实用例:分发器要在 cuda、在 vulkan、在
两者同时、以及在一个还不存在的后端下都被构建,而 `not(any(cuda, vulkan))` 每次生态新增
后端都要改一遍 —— 忘掉的那次是静默的。默认 `accel` 为空,所以 `mcpp build` 不需要任何
载荷,CI 因此能真正构建它,`none` 那条路径也就真的被覆盖。

**`examples/09-heterogeneous/cann`** —— 昇腾。**目前构建不了**,README 点明缺的两块
(`mcpp.rules.ascendc` 规则包,以及承载毕昇与仿真器的 `xim:cann-toolkit`),并记下已经
查实的三件事:两者在同一个工具包里;工具包镜像可匿名拉取;`sim` 模式无需硬件且保留岛,
而 `cpu` 模式**不保留**因而不能充当设备判据。manifest 写出来而不是描述出来,是为了让
形状具体。CANN 自己的算子库本来就是 `op_kernel/` 与 `op_host/` 分开的,岛不是 mcpp 强加
给昇腾的形状。

### 规则包应当声明在 `[build-dependencies]`

既有四个示例都把 `mcpp:plugins` 写在 `[dependencies]` 里,而 docs/05 §2.6.1 自己立的规则
恰恰点名这种情形:**库绝不该到达目标,而它的规则仍然被需要**。两条轴是分开的 ——
`host-module = true` 说要哪种构建期产物,section 说这个包是否到达目标,规则包在第二条轴
上答"否"。四个示例与两份文档已改正。

写在 `[dependencies]` 里同样能工作,这正是这条区分必须被**陈述**而不能指望由一次失败来
教会的原因。


### 共享库能说出自己发布哪些符号:`exports`

两个平台的默认都是"全导出":ELF 给符号默认可见性,PE 由引擎自动生成列出全部符号的
`.def`。**缺的是反方向** —— 声明式地只发布一组。

两类工程需要它。有稳定 ABI 的运行时只发布一份经评审的集合;与同类并存的插件不能撞名
—— 一个把内部符号也导出的 Vulkan ICD 会与 loader 以及同进程内另一个 ICD 相撞。本仓库
自己就有现成的例子:SYCL 示例构建时重复符号检查报的那 68 个 `_Unwind_*`,是一个镜像里
两个 C++ 运行时都在导出 unwinder 符号。

`exports` 接受一个符号模式文件或一个内联数组,由引擎按平台渲染成 version script /
`-exported_symbols_list` / `.def` —— 一句中立的话三种渲染,与 `[runtime]` 已确立的形状
相同,而不是让作者写三份平台专用文件。

**它不隐含编译期 hidden。** 三种格式上收窄都是链接期属性,所以一个键只有一个效果;
`-fvisibility=hidden` 仍可经 `[build] cxxflags` 取得代码生成收益,而那是单独的决定,
因为它同时改变本库各 TU 之间如何看见彼此。符号**版本化**(`foo@@LIB_1.0`)不在此列,
它是 ELF 独有、无法中立表达的能力。

判据 e2e 621 把同一份源码构建两次并要求两次读数**不同**:只断言公开符号在,会对"导出
全部"同样成立(那正是本特性之前的状态);只断言内部符号不在,分不开"正确地隐藏了"与
"根本没链上"。


### 构建程序能发出它算出来的链接标志:`mcpp:link-flag`

`link-lib`、`link-search`、`link-script` 各自命名一类东西,于是一条**算出来的**标志无处
可去:内容依赖 feature 组合的版本脚本、运行时接管 C 库符号用的 `-Wl,--wrap=malloc`、
以及 `-Wl,--exclude-libs,ALL`(静态吞入的第三方不得成为本包 ABI 的一部分)。签入仓库
的标志一直可以走 `[build] ldflags`,生成的不行。

新增 `mcpp:link-flag=` 与 `mcpp::link_flag()`(协议 v8)。原样传递 —— 引擎不解析链接器
词汇。**它到达消费者**,与 `[build] ldflags` 一致:编译接口有声明式公开对应物因而
`include-dir` 必须私有,链接标志没有这个分裂,让"算出来"的形态与它自己的声明式孪生
行为不同才是不一致。后果写明:依赖发出的 `--version-script` 也会落到消费者链接行上,
而这个隐患不是新的。

判据是 e2e 620,它断言**链接器的行为**而不是命令行文本:程序算出
`-Wl,--defsym=mcpp_e2e_620=42`,产物打印那个符号的地址。值只可能来自链接器真的收到了
这条标志。

### `cfg(accelerator = "none")` —— 开放词表不能靠枚举取反

CPU 回退此前只能写成 `not(any(accelerator = "cuda", accelerator = "vulkan"))`。
`accelerator` 的取值是**开放的**(docs/20:第五个后端是一个包,不是引擎改动),所以这条
谓词的含义会随生态增长**静默改变** —— 新增一个后端之后,每个已写好的回退谓词都开始把
"命名了新后端的构建"当成"没有加速器",于是 CPU 实现与设备实现一起编进去。

`accelerator = "none"` 为真当且仅当加速器集合为空。拼法沿用本仓库已有的
`os = "none"`(裸机),不新造词。不用 `cpu`:那会让这条轴同时承载两个问题,并且
`cfg(accelerator = "cpu")` 在 `accel = "cuda"` 下的真假无法自洽地定下来。

判据 `test_cfg_accelerator_none.cpp` 直接**模拟第五个后端到来**:枚举写法当场开始说谎,
`none` 不变。这是这项改动的全部理由,单后端下跑绿零信息量。


### 工具也有两条解析轴:`[target.<selector>.xlings…]`

一条工具条目回答的是两个不同问题中的一个:它是在构建机上执行的(宿主),还是产物编译
Expand Down
4 changes: 3 additions & 1 deletion docs/01-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ examples.
| 06 | [`examples/06-openkal-cross`](../examples/06-openkal-cross/) | One program asking each machine what it is, built for four targets from any host | `--target`, openkal, cross-compilation without editing the source |
| 07 | [`examples/07-project-subos`](../examples/07-project-subos/) | A build program that finds its tools in the environment the project declared | `[xlings] subos`, `[xlings.workspace]`, a build program whose `PATH` is the environment the project named |
| 08 | [`examples/08-build-rules`](../examples/08-build-rules/) | Two rule packages and a project that uses both | `host-module = true`, `[build-dependencies]`, `mcpp::action` with `role = "check"` |
| 09 | [`examples/09-heterogeneous`](../examples/09-heterogeneous/) | One computation on a device, in four programming models, with a CPU fallback in each | `accel`, constrained source globs, the seam module, rule packages from `mcpp:plugins`, `cfg(accelerator = …)` |
| 09 | [`examples/09-heterogeneous`](../examples/09-heterogeneous/) | One computation on a device, in several programming models, with a CPU fallback in each; plus one artifact carrying several backends at once | `accel`, constrained source globs, the seam module, rule packages from `mcpp:plugins`, `cfg(accelerator = …)` |
| 09a | [`…/cuda`](../examples/09-heterogeneous/cuda/) | A CUDA kernel behind a seam module | `mcpp.rules.cuda`, `mcpp::action` with `role = "object"`, the driver stated as a fact and a floor |
| 09b | [`…/vulkan`](../examples/09-heterogeneous/vulkan/) | The same computation as a Vulkan compute shader, on a GPU or on the CPU | `mcpp.rules.spirv`, `mcpp::action` with `role = "source"`, generated headers, a software driver as a payload |
| 09c | [`…/sycl`](../examples/09-heterogeneous/sycl/) | The same computation as a SYCL kernel, compiled by a second compiler | `mcpp.rules.sycl`, the `.sycl` device extension, a chained `mcpp::action` for the device link, `compat:sycl-runtime` |
| 09d | [`…/hip`](../examples/09-heterogeneous/hip/) | The same computation in HIP, reaching an NVIDIA device | `mcpp.rules.hip`, HIP as a header layer over the CUDA runtime, a two-chunk `accel` |
| 09e | [`…/multi-backend`](../examples/09-heterogeneous/multi-backend/) | Several backends in ONE artifact, chosen at run time — the library shape, not the program shape | `accel` as a set, `cfg(accelerator = "none")` and its negation, a dispatch chain, a module seam over a C island boundary |
| 09f | [`…/cann`](../examples/09-heterogeneous/cann/) | An Ascend C kernel behind the same seam. **Does not build yet** — its README names the two missing pieces | the `.asc` device extension, `op_kernel`/`op_host` as an island CANN already has, `accelerator = "none"` for the fallback |

## Suggested Reading Order

Expand Down
Loading
Loading