diff --git a/.agents/docs/2026-08-29-add-libgbm-plan.md b/.agents/docs/2026-08-29-add-libgbm-plan.md new file mode 100644 index 00000000..56e72b05 --- /dev/null +++ b/.agents/docs/2026-08-29-add-libgbm-plan.md @@ -0,0 +1,330 @@ +# Adding `compat.libgbm` — GBM bound to the ecosystem's Mesa + +> ## ⚠ SUPERSEDED IN PART — read this first +> +> This is the FIRST-ROUND design record, and its central mechanism no longer +> exists. It describes a constructor in a generated TU that set +> `GBM_BACKENDS_PATH`, a `lib/gbm/` backend farm, and an `mcpp_gbm.h`. **All +> three are gone.** +> +> Setting that variable was always Mesa's own mechanism and the ENVIRONMENT's +> job. `xim:mesa` now declares it through the graphics discovery layer +> (openxlings/xim-pkgindex#713), so the package sets nothing, generates no TU +> and ships no header of its own — 598 lines down to 303. What this document +> calls "the part that is actual work" turned out to be a workaround for a +> missing declaration one layer down. +> +> What still holds: the shape decision (a binding, not a source build) and the +> evidence behind it; the zero-host rule; the two-directory-key finding; and +> the test design. For the current state and the three rounds of correction +> that produced it, see +> [2026-08-30-gbm-cross-repo-closed-loop-plan.md](2026-08-30-gbm-cross-repo-closed-loop-plan.md) +> — especially §12.1, §16 and §17. + + +Date: 2026-08-29 · Package: `compat.libgbm@2026.08.29` · Member: `tests/examples/libgbm` + +## What GBM is, and what had to be decided + +GBM (Generic Buffer Management) is the API a program uses to get scanout-capable buffers out of a +DRM device — `gbm_device`, `gbm_bo`, `gbm_surface`. It sits under EGL on a KMS console, under a +Wayland compositor's back end, and under headless GPU rendering with no X server. + +The only real decision was the shape, and the first answer was wrong. The criterion that settled it +is not "which is less work" but **how much dependency surface the ecosystem takes on, and how much +of it touches the host**. + +## Shape: why this is a binding, not a source build + +### 1. Upstream does not ship libgbm as a separable unit + +Measured against Mesa 26.2.1. `src/gbm/meson.build`: + +```meson +libgbm = shared_library(libgbm_name, files_gbm, + link_with : [libloader], + dependencies : [dep_libdrm, idep_xmlconfig], …) +``` + +and `src/loader/meson.build` in turn: + +```meson +libloader = static_library('loader', ['loader_dri_helper.c', 'loader.c', sha1_h], + c_args : ['-DUSE_DRICONF'], + dependencies : [idep_mesautil, dep_libdrm, dep_thread, dep_xcb, dep_xcb_xrandr], …) +``` + +`idep_mesautil` is the whole of Mesa's internal util library — ~120 TUs plus Python-generated +tables — and libgbm reaches it for exactly **one** function, `loader_open_driver_lib`. `loader.c` +itself pulls `GL/gl.h`, `mesa_interface.h`, `util/xmlconfig.h` (expat), `drm-uapi/nouveau_drm.h`, +`pci_id_driver_map.h` and a generated `git_sha1.h`. + +This is not a recent refactor to route around: `main/backend.c` has included `loader.h` since at +least Mesa 23.3.6 (checked 23.3.6, 25.0.7, 26.2.1). + +GBM's frontend/backend `dlopen` split exists so **vendors can ship backends** — NVIDIA contributed +it in 2021 — not as an invitation to rebuild the frontend. Vendoring it means forking Mesa's +internals. + +The contrast with `compat.vulkan` is the point, and it is not a double standard: Khronos releases +the Vulkan-Loader as a **standalone project** whose entire purpose is to ship separately from any +driver. Mesa releases no such thing for GBM. + +### 2. In this ecosystem, Mesa already has an owner + +`xim:mesa` is a package, and `xim-x-mesa/25.0.7.2` already carries `lib/libgbm.so{,.1,.1.0.0}`, +`include/gbm.h` and `lib/gbm/dri_gbm.so`. Its `config()` declares `lib` into `/lib` and +`include` into `/usr/include`. + +So a source build would make mcpp-index re-import **libdrm + expat + xcb + a Mesa-util carve-out** +— four or more new packages — to duplicate a dependency graph the ecosystem has already resolved +hermetically. That grows the dependency surface in order to shrink nothing. + +### Measured surface of the chosen shape + +| surface | count | note | +|---|---|---| +| host | **0** | no `/usr/lib*` path, no `MCPP_HOST_*` override — see below | +| ecosystem | **1** | `xim:mesa`, not `xim:graphics`'s twenty-two | +| index | **0** | `deps = {}`; `gbm.h` includes only ``/`` | +| transitive | **0** | `libgbm.so.1`'s RUNPATH resolves entirely inside `xim-x-{mesa,libdrm,expat,libllvm,glibc,…}` | + +## Zero host — not "host, converged" + +This package reads `system.subos_sysrootdir()` and nothing else. That is stricter than either +neighbour: `compat.glx-runtime` keeps `MCPP_HOST_GL_LIBRARY_PATH` as "the ONLY door back to the +host", and `compat.vulkan-runtime` harvests `/usr/lib/x86_64-linux-gnu` outright. + +Those two have a reason this one does not: a **proprietary vendor driver** can only come from the +host. GBM has no such case — `xim:mesa` covers every host shape the graphics stack covers +(llvmpipe, radeonsi, iris, nouveau, zink, d3d12, RADV). + +And host libgbm specifically is a leak the ecosystem has already closed. From +`xim:nvidia-gl-host-link`: + +> The table was a list of what someone thought of, and it was missing libm, libdrm, **libgbm**, +> libgcc_s and libwayland-* — all of which were therefore coming from the HOST, silently, which is +> the leak this package exists to close (R7). + +Reopening it here would undo that. If a machine ever needs NVIDIA's own GBM backend, that belongs +in `xim:nvidia-gl-host-link` — the ecosystem's host-link layer, which owns host contact — and not +in this descriptor. + +## The part that is actual work: backend reachability + +Harvesting `libgbm.so` and `gbm.h` is the easy half and produces a package you can link and cannot +use. libgbm is a **loader**: every `gbm_create_device()` dlopens `/_gbm.so`, and the +path Mesa compiles in is `/usr/lib/gbm` (its `gbm.pc` says `gbmbackendspath=/usr/lib/gbm`), which +does not exist inside the sandbox. Measured before this package existed: + +``` +MESA-LOADER: failed to open dri: /usr/lib/gbm/dri_gbm.so: cannot open shared object file +(search paths /usr/lib/gbm, suffix _gbm) +``` + +`xim:mesa` declares `lib` into the view, but `lib/gbm/` is a **subdirectory** and does not follow. +Closing that is this package's real content, and it is why the shape is not a copy of +`compat.glx-runtime`. + +### The repair has to be invisible + +The first version of this package exposed a header, `mcpp_gbm.h`, and asked the consumer to call +`mcpp_gbm_use_sibling_backends()` before creating a device. That was wrong, and not merely +stylistically: **libgbm is mostly called from inside other libraries.** SDL2's KMSDRM backend, +wlroots and ffmpeg's VAAPI hwcontext all call `gbm_create_device()` out of their own sources, and +none of them will ever call a helper of ours. A design that only works for callers who have read +this descriptor leaves the important consumers exactly as broken as they were. + +So the package exposes stock `gbm.h`, and the path is wired from a **constructor** in the +package's own TU. Nothing has to be included, called, or known about. + +What makes that reliable is a property of mcpp that is usually a nuisance: a dependency's objects +enter the consumer's link *eagerly*, rather than being lazily selected the way an archive member +would be. Confirmed in the emitted `build.ninja`, which names the object on the link line: + +``` +build bin/gbm : cxx_link obj/gbm.o obj/compat_libgbm/mcpp_generated/gbm_backends.o +``` + +so the constructor cannot be dropped. Priority 101 — the first value not reserved for the +implementation — puts it ahead of default-priority constructors in case a consumer creates a +device from one. An already-set `GBM_BACKENDS_PATH` is left alone: this is a default, not an +override. + +### This is what every other ecosystem does, and none of them use an API + +| ecosystem | how the backend path is made right | +|---|---| +| Debian (`libgbm1`/`libgbm-dev`), Fedora (`mesa-libgbm`) | libgbm is a **binary package split out of the mesa source package**; one system-wide prefix makes Mesa's compiled-in `$libdir/gbm` correct by construction. Nothing to set. | +| Valve pressure-vessel (Steam Runtime) | Hit exactly this bug when Mesa 24.3 split the backends out — [steam-runtime#797](https://github.com/ValveSoftware/steam-runtime/issues/797) — and answers with `GBM_BACKENDS_PATH=/run/host/usr/lib64/gbm` **in the container environment**. | +| NixOS / Conda / AppImage | Same variable, set at environment-activation / wrapper level. | +| Anyone controlling the build | Mesa's own [`-Dgbm-backends-path=`](https://cgit.freedesktop.org/mesa/mesa/commit/?id=7f615c66fbdd0a7aa7a513d011956dcc6c0ac2e6) meson option, added for precisely this. | + +Every one of them is *environment* or *build-time*. None is "call our function". This package is +in the sandboxed case and cannot set a container-wide environment, so the constructor is the +in-process equivalent: same variable, same don't-override rule, scoped to processes that actually +link libgbm. + +**Where this really belongs.** The distro answer is the right one and it is one layer down: +`xim:mesa` either building with `-Dgbm-backends-path=` pointing into the subos view, or declaring +`lib/gbm/` into it the way it already declares `lib` and `include`. Then this package would carry +no constructor at all. Worth filing against `xim:mesa`; until then the wiring lives here, where it +is at least tested. + +### How the path is found, without pinning anything + +The farm is laid out so the backend directory is the sibling of the libgbm that actually loaded: + +``` +mcpp_generated/libgbm/lib/libgbm.so{,.1,.1.0.0} -> /lib/* +mcpp_generated/libgbm/lib/gbm/dri_gbm.so -> mesa payload lib/gbm/ +mcpp_generated/libgbm/include/{gbm.h,mcpp_gbm.h} +``` + +and `mcpp_gbm_use_sibling_backends()` resolves it at runtime: `dlsym(RTLD_DEFAULT)` a gbm symbol, +`dladdr` it, take the directory, append `/gbm`. + +Verified experimentally that `dladdr` reports the **farm** path and not the realpath — a library +loaded through a symlink on the RUNPATH reports the name the loader used — so the sibling lands +inside this package's own payload: + +``` +dli_fname = …/farmtest/lib/libgbm.so.1 +derived backends dir = …/farmtest/lib/gbm +``` + +and with `GBM_BACKENDS_PATH` set from it, the loader's own diagnostic confirms it searches there: +`(search paths …/farmtest/lib/gbm, suffix _gbm)`. + +The alternative — baking an absolute path into a generated header at install time — works and pins +the package to whichever mesa payload existed on install day, which is exactly what +`compat.glx-runtime`'s header comment warns about ("a payload path pins a version … and stops +resolving the day it is upgraded"). Runtime derivation has no such cost and puts no absolute path +in the descriptor at all. + +`RTLD_DEFAULT` rather than `&gbm_format_get_name`: the address of an imported function is the +consumer's own PLT stub, and `dladdr` would report the consumer. + +## Two mechanism findings worth keeping + +**`runtime.library_dirs` does not put `-L` on the link line.** The index README and the descriptor +catalog both said it did. Reading the emitted `build.ninja` on mcpp 2026.8.27.2: + +| key | renders as | +|---|---| +| `runtime.library_dirs` | `-Wl,-rpath` | +| `runtime.link_library_dirs` | `-L` | +| `runtime.transitive_needed_dirs` | `-Wl,-rpath-link` | + +`compat.glx-runtime` and `compat.vulkan-runtime` are unaffected — nothing links against their +farms, they exist so a bare-soname `dlopen` resolves at run time. This package does link against +its farm, and with `library_dirs` alone the farm is complete, the rpath correct, and the build dies +at `ld: cannot find -lgbm`. Both catalog rows have been corrected in this change. + +**`c_standard = "gnu11"` is still silently ignored** (mcpp 2026.8.27.2 emits `-std=c11` anyway), so +`dladdr`/`RTLD_DEFAULT` are reached with `cflags = { "-D_GNU_SOURCE" }`, exactly as `compat.libaio` +found for `syscall()`/`sigset_t`. + +## Target naming + +The lib target is `gbm_binding`, not `gbm`. A target called `gbm` would put a `libgbm.a` on the +link line beside the real `libgbm.so` the package exists to deliver, and which one `-lgbm` picked +would come down to search order. + +## Test member + +`tests/examples/libgbm`, linux-gated, no-op `main()` elsewhere (the `compat.libaio` / +`compat.wil` pattern). **Two binaries**, and the split is the point: + +- `tests/stock_usage.cpp` includes **stock `` and nothing else** — no `mcpp_gbm.h`, no + helper declaration, no knowledge that this package exists. It is what a ported consumer looks + like, and what a third-party library looks like from the inside. If the backend path ever + regresses to something the application must opt into, this file fails while `gbm.cpp` could + still pass. That asymmetry is why it exists. +- `tests/gbm.cpp` covers the rest, including the optional introspection header. + +`gbm.cpp`'s first assertion reads `GBM_BACKENDS_PATH` **before the program has called anything**, +which is the direct check that the constructor did its job. Its last one re-execs the binary with +the variable already set and asserts the child still sees the inherited value — the only way to +observe the don't-override rule, since by the time `main` runs the constructor is finished. + +All checks run with no GPU: + +- `gbm_format_get_name` over four fourccs — `XRGB8888`→`XR24`, `ARGB8888`→`AR24`, `NV12`→`NV12`, + `ABGR2101010`→`AB30`. +- The two **legacy enumerators**, which are the assertions that matter: `GBM_BO_FORMAT_XRGB8888` + is the value `0`, and only the library's own `format_canonicalize()` turns it into `"XR24"`. A + header-only reimplementation would pass the fourcc cases and fail these. +- Six `dlsym(RTLD_DEFAULT, …)` checks, so a header from a different Mesa than the library surfaces + here rather than as a link error. +- `gbm_create_device(-1) == nullptr`. +- **Backend reachability**: the derived directory exists and holds at least one `*_gbm.so`. This is + the assertion the package exists for and it needs no `/dev/dri`. +- `GBM_BACKENDS_PATH` is already set on entry to `main`, and an inherited one survives (checked in + a re-exec'd child). + +Real device creation is opt-in behind `MCPP_RUN_GBM_DEVICE=1` plus a working `/dev/dri`, following +`tests/examples/imgui-window`'s `MCPP_RUN_WINDOW=1`. + +## Verification + +With the CI-pinned mcpp (2026.8.27.2), after `rm -rf` of the member's `target/`, `.mcpp/` and the +package's build-cache entry: + +``` + Compiling compat.libgbm v2026.08.29 + Compiling gbm (test) + Running bin/gbm +GBM_BACKENDS_PATH is set on entry to main (nothing called) ok +… +an inherited GBM_BACKENDS_PATH survives the constructor ok +0 check(s) failed +stock_usage ... ok +a -only consumer inherits GBM_BACKENDS_PATH ok + ... and it is a directory ok + ... holding a backend libgbm can actually dlopen ok +0 check(s) failed + test result ok. 2 passed; 0 failed; finished in 45.69s +``` + +Also verified outside mcpp, as an independent check of the mechanism: a consumer compiled against +stock `gbm.h` and linked with no knowledge of the package prints the wired path before `main`. + +- `mcpp xpkg parse pkgs/c/compat.libgbm.lua` → `parse OK` (no unknown mcpp-segment keys). +- All eight lint gates reproduced locally (syntax, required fields, no leading `v`, mirror urls, + package name, cross-package refs, platform parity, duplicate versions) → clean. +- **Assertions confirmed failable**: moving `dri_gbm.so` out of the farm turns the reachability + check to `FAILED` and both binaries exit 1. +- The linked binary is honest: `NEEDED libgbm.so.1` with the farm on `RPATH` and **no host path + anywhere** on it. +- CN mirror published at `gitcode.com/mcpp-res/libgbm@2026.08.29`, fetched back and confirmed + byte-identical to GLOBAL (`95f3b4a6…`, 19165 bytes). GLOBAL sha computed twice before use. + +## Known, and not this package's defect + +On a host whose `xim-x-mesa` is 25.0.7.2 against `xim-x-glibc` 2.39, the backend is found and then +fails to load: + +``` +MESA-LOADER: failed to open dri: …/xim-x-glibc/2.39/lib64/libm.so.6: version `GLIBC_2.43' not +found (required by …/libgallium-25.0.7.so) (search paths …/lib/gbm, suffix _gbm) +``` + +Note the search path: the reachability gap **is** closed, and what remains is a glibc skew inside +the ecosystem's own Mesa build — the shape of mcpp#352, upstream of this package. It is why the +test asserts the backend is *present* at the derived path rather than that it *loads*: that +assertion is meaningful on a GPU-less runner and does not go green by accident when the stack is +broken. Worth reporting against `xim:mesa` separately. + +## What was deliberately left out + +- **No `capabilities` entry.** `compat.glx-runtime` declares `"x11.display"` because it needs the + sandbox to expose a socket it does not own. There is no verified DRM counterpart in the engine's + vocabulary, and coining one that may be silently ignored would document a guarantee this package + cannot make. +- **No `libraries` block.** Only `ldflags`. A name containing a dot in `libraries` is treated as a + package-relative path; the neutral block is only needed by `cl.exe` consumers. +- **No features.** There are no optional compilable components — the package has exactly one TU of + its own. +- **No non-linux `xpm` section.** GBM is the DRM buffer API; there is no port to declare, so + consumers gate it with `[target.'cfg(linux)'.dependencies]`. diff --git a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md new file mode 100644 index 00000000..a9684ffa --- /dev/null +++ b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md @@ -0,0 +1,1115 @@ +# GBM 闭环:mcpp / xim-pkgindex / mcpp-index 跨仓方案 + +Date: 2026-08-30 · 起因:`compat.libgbm`(mcpp-index PR #281)· 状态:待 review + +## 怎么读这份文档 + +它记录了**三轮修正**,后面的推翻前面的。**结论以最后一轮为准**,前面保留是为了记录 +被否掉的推理——因为每一次被否,否掉它的都是一次实测,而那些实测本身是资产。 + +| 想知道 | 看哪节 | 注意 | +|---|---|---| +| **最终结论与交付** | §12(实现结果)、§13(状态)、§14(要不要做 mesa 包) | 权威 | +| 为什么 `compat.libgbm` 该独立存在 | §3、§10.1 | 理由换过三次,结论没变 | +| 任务拆分与依赖 | §11 | | +| mcpp 侧该怎么修 | **§12.1** | ⚠ §2 的 C2(B1/B2)与 §8 的 B3 **都已作废** | +| 实测证据 | §1、§12.3、§12.4 | | + +**唯一反复改的是 mcpp 侧的形态**(B1 → B3 → 「装到全局 scope 就够了」),因为那是唯一 +一处我没有一开始就去读源码 / 做实验的部分。理由被换掉三次而结论不变的那几条,才是稳的。 + +## TL;DR + +让 `gbm_create_device()` 在 mcpp 工程里能用,**不需要新机制**。所需的机制三层全都已经存在 +并且已经在跑;缺的是**两个具体的接线点**,分别在两个仓: + +| # | 缺什么 | 在哪个仓 | 规模 | +|---|---|---|---| +| **R1** | `GBM_BACKENDS_PATH` 不在 graphics 的 `DISCOVERY` 表里 | xim-pkgindex | 一个常量 + 一行表项 + 一个照抄 `declare_dri` 的函数 | +| **R2** | 默认 runtime selection 读的是**工具链 subos**,而项目的 `xim:` 依赖声明在**项目 subos** | mcpp | 见 §8/§9:拆成 R2a/R2b/R2c,形态是**分层继承 + 首次构建自动供给** | + +R2 **不是 gbm 专属的**:同一条线上 `LIBGL_DRIVERS_PATH` 和 `__EGL_VENDOR_LIBRARY_DIRS` +也没到进程,也就是说**任何 mcpp 构建的 GL 程序都找不到 DRI 驱动**。gbm 只是让它显形。 + +R1 + R2 落地后,`compat.libgbm` 里的 constructor 与后端 farm **全部删除**,退化成一个 +只有「头 + `-lgbm`」的薄壳。 + +> **第二轮 review 修正(§8)**:R2 应拆成 R2a/R2b/R2c 三条,且 mcpp 侧正确形态是 +> **分层继承(B3)** —— 工具链 subos 作基座、项目 subos 作叠加层 —— 而不是原文建议的 +> B1(只合 env)或 B2(整体切换)。实测:项目 subos **缺 `libgcc_s` / `libstdc++`**, +> 不是工具链 subos 的超集,所以 B2 会回退;而 B1 只修运行期、不修构建期。 +> 另外实测确认:今天**唯一**能把 xim 层引进 mcpp 工程的东西就是 compat 包的 +> `xpm.deps.runtime`(`[xlings] deps` 只物化不供给、`[xlings] subos` 不能自举)。 +> +> **第三轮(§9/§10)**:R2b 的修法是**首次构建自动供给**,复用工具链首次运行同款流程; +> R2a 降级为「供给先于选择」的顺序约束。并且修正了一处预期 —— 即便三条全修完, +> `compat.libgbm` **也不会消失**,因为库→库的**传递依赖**只能靠包来表达, +> 不能靠消费者的 `[xlings] deps`。 + +--- + +## 1. 实测证据(全部可复现) + +### 1.1 机制在,但没接上 + +一个只依赖 `compat.libgbm` 的独立工程,`mcpp run` 打印自己的环境: + +``` +LIBGL_DRIVERS_PATH = +__EGL_VENDOR_LIBRARY_DIRS = +XDG_DATA_DIRS = /usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:… ← 宿主原样 +GBM_BACKENDS_PATH = …/compat-x-libgbm/…/mcpp_generated/libgbm/lib/gbm ← 仅来自本包 constructor +``` + +同一个工程,`mcpp.toml` 加上 `[xlings] subos = "_"` 之后: + +``` +LIBGL_DRIVERS_PATH = …/envprobe/.mcpp/.xlings/subos/_/usr/lib/dri ✔ +__EGL_VENDOR_LIBRARY_DIRS = …/envprobe/.mcpp/.xlings/subos/_/share/glvnd/egl_vendor.d ✔ +XDG_DATA_DIRS = …/subos/_/share:/usr/share/ubuntu:… ✔ prepend 合并正确 +``` + +**结论:`${subosdir}` 展开、`prepend` 语义、注入子进程 —— 整条链路都是好的。** +它只是默认情况下指向了一个空表。 + +### 1.2 两个 subos,声明在一个、读的是另一个 + +| subos | `.xlings.json` 的 `envs` | 谁写的 | 谁读的 | +|---|---|---|---| +| `/subos/default` | **`{}`** | 工具链安装(gcc/glibc) | **mcpp 默认读这个** | +| `/.mcpp/.xlings/subos/_` | mesa@25.0.7.2 的 3 条 `prepend` | 项目的 `xim:` 依赖 | 只有显式 `[xlings] subos` 时才读 | + +项目 subos 的实际内容: + +```json +"envs": { "mesa@25.0.7.2": [ + { "op": "prepend", "var": "LIBGL_DRIVERS_PATH", "value": "${subosdir}/usr/lib/dri" }, + { "op": "prepend", "var": "__EGL_VENDOR_LIBRARY_DIRS", "value": "${subosdir}/share/glvnd/egl_vendor.d" }, + { "op": "prepend", "var": "XDG_DATA_DIRS", "value": "${subosdir}/share" } +] } +``` + +`GBM_BACKENDS_PATH` 不在其中 —— 这就是 **R1**。 + +### 1.3 R2 的代码位置 + +`mcpp/src/platform/xlings/runtime_binding.cppm`: + +```cpp +std::filesystem::path subos_path(const RuntimeSelection& selection, + const GlobalConfig& cfg) { + if (selection.mode == Mode::McppDefault || selection.subosName == "default") + return cfg.xlingsHome() / "subos" / "default"; // ← envs 是 {} + return selection.ownerRoot / ".mcpp" / ".xlings" / "subos" + / selection.subosName; // ← 有 mesa 的声明 +} +``` + +`select_runtime()` 只在 `owner.xlings.subosDeclared` 为真时走第二个分支。 +普通工程不写 `[xlings] subos`,于是永远读第一个。 + +mcpp 侧其余部分**都是对的**,不需要动: + +- `subos_info.cppm` 已经把 `envs` 解析成 `EnvDecl{var, op, value}`; +- `runtime_binding.cppm:440` 把它收进 `binding.environment`; +- `execute.cppm` 的 `compute_subos_env()`(mcpp#352 的修复)已经把它注入 run/test 子进程; +- `binding.environment` 已经参与 `contractHash`,所以声明变化会正确地让快取失效。 + +一句话:**mcpp#352 修好了「怎么注入」,没修「从哪读」。** + +--- + +## 2. 三仓改动 + +### C1 — xim-pkgindex:把 GBM 加进 discovery 层 + +`libs/graphics.lua`。GBM 后端与 DRI 驱动是**同一类东西**(按路径 `dlopen`、不是链接目标), +所以照抄 `declare_dri` 而不是走 `sysroot.declare_libs` —— 后者会把它们摊进 `/lib`, +也就是**链接目录**,这一点 `declare_dri` 的注释已经论证过了。 + +```lua +-- 1) 常量,与 DRI_DIR / EGL_VENDOR_DIR 并列 +graphics.GBM_DIR = "usr/lib/gbm" + +-- 2) DISCOVERY 增加一行 +local DISCOVERY = { + { var = "LIBGL_DRIVERS_PATH", rel = graphics.DRI_DIR }, + { var = "__EGL_VENDOR_LIBRARY_DIRS", rel = graphics.EGL_VENDOR_DIR }, + { var = "XDG_DATA_DIRS", rel = graphics.SHARE_DIR }, ++ { var = "GBM_BACKENDS_PATH", rel = graphics.GBM_DIR }, +} + +-- 3) declare_dri 的镜像 +function graphics.declare_gbm(install_dir, rel_dir, tag) + if not xvm.files then return false end + if not os.isdir(path.join(install_dir, rel_dir)) then + log.warn("no %s in this payload -- GBM_BACKENDS_PATH would point at an " + .. "empty directory and gbm_create_device would find no backend", + rel_dir) + return false + end + xvm.files{ src = rel_dir, dst = graphics.GBM_DIR, binding = tag } + return true +end +``` + +`pkgs/m/mesa.lua` 的 `config()`,紧挨现有那几行: + +```lua + graphics.declare_dri(dir, "lib/dri", tag) ++ graphics.declare_gbm(dir, "lib/gbm", tag) + graphics.declare_egl_vendor(dir, "share/glvnd/egl_vendor.d/50_mesa.json", tag) +``` + +`consumer_envs()` / `declare_subos_env()` 都是从 `DISCOVERY` 生成的,所以 S2(xvm shim) +与 S3(subos shell)**自动**跟着获得 `GBM_BACKENDS_PATH`,无需再改。 + +**单独 C1 就能修好 xlings 侧的消费者**(godot 这类走 xvm shim 的),与 mcpp 无关。 + +> 根因备注:`xim-x-mesa` 是以 `--prefix=/usr` 构建的,`gbm.pc` 里写着 +> `gbmbackendspath=/usr/lib/gbm`,这个路径被编译进 `libgbm.so`。payload 重定位之后它必然 +> 是错的。DRI/EGL 早就用环境变量兜住了,GBM 只是没人补。 +> 另一条路是让 xlings-res 用 `-Dgbm-backends-path=` 重建 mesa,但那要求构建时就知道 +> 重定位后的绝对路径(含版本号),不如 discovery 层稳。**建议走 C1,不动构建。** + +### C2 — mcpp:让默认选择也能读到项目 subos 的声明 + +> **⚠ 本节已被第 8 节「深度自我 review」推翻。** 下面的 B1/B2 是一个假二分,正确的形态是 +> **分层继承**(B3)。保留原文是为了记录被否掉的推理,新方案见 §8。 + +两个方案,~~建议 B1~~。 + +#### B1(小,建议):只合并 env,不动其他 + +保持 runtime binding 仍旧绑在工具链 subos(sysroot / libc / loader 全部不变), +**额外**读取 `/.mcpp/.xlings/subos/_/.xlings.json` 的 `envs`, +合并进 `binding.environment`。 + +- 影响面只有 `binding.environment` 一个字段; +- 它**已经**参与 `contractHash`,快取失效天然正确; +- `resolve_env()` 的 `${subosdir}` 必须按**声明来源的那个 subos** 展开,不是 binding 的 + `subosDir` —— 这是 B1 唯一需要小心的点,签名要带上来源目录; +- 语义清楚:「工具链从工具链 subos 来,项目的依赖声明从项目 subos 来」。 + +#### B2(大):默认选择直接切到项目 subos + +即 `select_runtime()` 在未声明 `[xlings] subos` 且 `/.mcpp/.xlings/subos/_` +存在时,选它。等价于把实测 1.1 里手写的 `subos = "_"` 变成默认。 + +- 好处:一个改动,env / sysroot / 库搜索全部统一到项目视图,概念最干净; +- 代价:`subos_path()` 的结果同时决定 sysroot、libc、loader、搜索目录 —— 爆炸半径大得多, + 且会让所有既有工程的 fingerprint 变化(实测确实触发 full rebuild)。 + +**建议 B1 先落地修复现象,B2 作为后续的架构统一单独评估。** + +#### 无论哪个方案都要补的回归测试 + +一个依赖 `xim:mesa` 的最小工程,`mcpp run` 断言 `LIBGL_DRIVERS_PATH` 非空且指向 +`/usr/lib/dri`。**这条测试今天就会红**,正是 R2 的证据。 + +### C3 — mcpp-index:`compat.libgbm` 退化为薄壳 + +C1 + C2 落地后删掉: + +- `mcpp_generated/gbm_backends.c` 整个 TU(constructor + `mcpp_gbm_backends_dir` + + `mcpp_gbm_use_sibling_backends`), +- `install()` 里的后端 farm(`lib/gbm/` 那部分), +- `mcpp_gbm.h`, +- `tests/gbm.cpp` 中与 constructor 有关的断言。 + +保留: + +- `install()` 从 `system.subos_sysrootdir()` 取 `libgbm.so*` 与 `gbm.h` 的 farm, +- `include_dirs` / `ldflags = {"-lgbm"}` / `runtime.{library_dirs, link_library_dirs}`, +- `deps.runtime = { "xim:mesa" }`, +- **`tests/stock_usage.cpp` 原样保留** —— 它只 include 上游 ``,C1+C2 之后它断言的 + 就不再是 constructor 而是**整条闭环**,是最有价值的那条回归。 + +**过渡期**:C1/C2 未落地之前,constructor 是唯一能让 mcpp 消费者用上 gbm 的东西 +(见 1.1 的表)。它应当保留,但在描述符里写明删除条件。 + +--- + +## 3. 为什么 `compat.libgbm` 仍然应该独立存在 + +这一节回答「要不要干脆不做这个包」。结论:**要做,而且独立成包是对的,但必须是薄壳。** + +### 行业证据:拆分轴是「接口」,不是「源码项目」 + +| | 源码单位 | 消费单位 | +|---|---|---| +| Debian | 一个 `mesa` 源码包 | `libgbm1` / `libegl1` / `libgl1` **三个二进制包** | +| Conan | — | `opengl/system`、`egl/system` **两个包**(无 `gbm`,见下) | +| pkg-config | — | `gbm.pc` / `gl.pc` / `egl.pc` **三个 .pc** | + +所以「优先源码/原项目一起」与「按接口拆包」不冲突,它们是两个轴: +**构建单位是整个 Mesa**(`xlings-res/mesa` 已经如此),**消费单位是接口**。 +发行版的标准做法正是「一个源码包 → 多个二进制包」。 + +### Conan 的形态(实查 conan-center-index) + +- `libgbm` / `gbm` / `mesa` → **全部 404,Conan 根本不打包 gbm**; +- `opengl` / `egl` / `xorg` → 存在,但是 `version = "system"` 的**虚包**: + `package_id()` 清空、`system_requirements()` 调 apt/dnf/pacman 装发行版 `-dev` 包、 + `package_info()` 用 `PkgConfig(...).fill_cpp_info(is_system=True)` 读系统 `.pc`, + 并且 `includedirs = []` / `libdirs = []` —— **一个目录都不贡献**; +- `libdrm` / `wayland` / `libglvnd` / `vulkan-loader` → 真配方,真源码构建。 + +判据很清楚:**上游作为独立项目发布 → 真构建;是平台/驱动栈的一个切面 → 薄虚包交给平台。** +GBM 属于后者。 + +`compat.libgbm` 就是这个形态在 mcpp 里的对应物,**「平台」由 `xim:mesa` 扮演**: + +| Conan | mcpp-index | +|---|---| +| `system_requirements()` → apt/dnf | `xpm.linux.deps.runtime = { "xim:mesa" }` | +| `package_info()` → 系统 pkg-config | `include_dirs` / `ldflags` / `runtime.*_dirs` | +| 不 vendor 任何源码 | 同 | + +一处**必要的**差异:Conan 的 system 包能 `includedirs = []` 是因为有系统 pkg-config; +沙箱里没有,所以薄壳必须自己把 xim payload 指出来(`install()` 从 `system.subos_sysrootdir()` +取)。这是沙箱带来的,不是多做 —— `compat.glx-runtime` 同款。 + +### 为什么不做成一个大 `compat.mesa` + +按接口拆已有三方一致的先例(上表)。合成一个大包会让只要 gbm 的消费者拖上 GL/EGL 的 +include 根,并与既有的 `compat.opengl` / `compat.glx-headers` 抢 `GL/` 目录 —— +后者已经在 `compat.glx-headers` 的注释里被记为一个真实的踩坑。 + +### constructor 为什么不属于「包该做的事」 + +Conan 的 system 包**不做任何 runtime env wiring**,因为发行版里编译进去的 `$libdir/gbm` +本来就对。只有**被重定位的栈**才需要,而那一层的标准做法是容器/环境级: +Valve 的 pressure-vessel 在 mesa 24.3 拆出后端后踩到同一个 bug(steam-runtime#797), +用 `GBM_BACKENDS_PATH=/run/host/usr/lib64/gbm` 解决;Nix / Conda / AppImage 同理。 + +在 mcpp 生态里,「环境级」就是 subos env manifest —— 也就是 C1 + C2。 +所以 constructor 是**在补 R2 的洞**,不是包的职责。 + +--- + +## 4. 闭环验证矩阵 + +每一条都要能独立跑、独立红。 + +| # | 断言 | 在哪 | C1 前 | C1 后 | C1+C2 后 | +|---|---|---|---|---|---| +| V1 | `.xlings.json` 的 `envs` 含 `GBM_BACKENDS_PATH` | xim-pkgindex 测试 | 红 | **绿** | 绿 | +| V2 | xvm shim 程序(godot 类)`GBM_BACKENDS_PATH` 非空 | xim-pkgindex | 红 | **绿** | 绿 | +| V3 | `mcpp run` 下 `LIBGL_DRIVERS_PATH` 指向 `/usr/lib/dri` | mcpp 回归 | 红 | 红 | **绿** | +| V4 | 只 include `` 的消费者拿到 `GBM_BACKENDS_PATH` | `tests/examples/libgbm/tests/stock_usage.cpp` | 绿*(constructor)* | 绿*(constructor)* | **绿(闭环)** | +| V5 | 删掉 constructor 后 V4 仍绿 | 同上 | — | — | **这就是 C3 的准入条件** | + +V4/V5 是关键:V4 现在靠 constructor 绿,C1+C2 之后靠体系绿。 +**V5 通过就是删 constructor 的信号**,不必靠人判断。 + +反向验证(证明断言非空转):把 `dri_gbm.so` 移出目标目录,V4 必须转红 —— 已实测,退出码 1。 + +--- + +## 5. 落地顺序 + +三个仓可以**并行开工**,但合并有序: + +``` +C1 (xim-pkgindex) ──┐ + ├─→ V3 绿 ──→ C3 (mcpp-index 删 constructor,V5 把关) +C2 (mcpp) ──┘ +``` + +- **C1 独立可合**:不依赖任何人,合了立刻修好 xlings 侧消费者(V1/V2)。 +- **C2 独立可合**:不依赖 C1,合了立刻修好 GL/EGL 的 `LIBGL_DRIVERS_PATH`(V3)—— + **这本身就是一个比 gbm 重要得多的修复**。 +- **C3 最后**:两者都在、且 V5 绿,才删 constructor。 + +在 C3 之前,mcpp-index PR #281 以现状合并是安全的:它自包含、CI 全绿、对消费者透明, +唯一代价是一份临时的 constructor + 后端 farm,已在描述符里注明删除条件。 + +## 6. 风险与回滚 + +| 风险 | 评估 | 处置 | +|---|---|---| +| C1 让 `GBM_BACKENDS_PATH` 指向空目录 | mesa 无 `lib/gbm` 的构建是合法配置 | `declare_gbm` 照抄 `declare_dri` 的 `os.isdir` 检查并 `log.warn` 后返回 false | +| C1 覆盖用户自设的值 | 不会 | `DISCOVERY` 一律 `prepend`,`graphics.lua` 已论证过 `set` 会抹掉 NVIDIA 目录 | +| C2/B1 的 `${subosdir}` 展开错源 | 真实风险 | 展开必须用**声明来源**的 subos 目录,不是 `binding.subosDir`;回归测试须断言路径前缀 | +| C2/B2 改变所有工程的 fingerprint | 实测会触发 full rebuild | 这正是不建议 B2 先行的理由 | +| C3 删早了 | V5 把关 | V5 红就不删 | + +## 7. 一个顺带发现,值得单独报 + +`xim-x-mesa 25.0.7.2` 的 `libgallium-25.0.7.so` 需要 `GLIBC_2.43`,而栈里配的 +`xim-x-glibc` 是 `2.39`: + +``` +MESA-LOADER: failed to open dri: …/xim-x-glibc/2.39/lib64/libm.so.6: +version `GLIBC_2.43' not found (required by …/libgallium-25.0.7.so) +``` + +后端**找得到**了(`search paths` 已经是对的),但**加载不了**。这与本方案正交, +是 `xim:mesa` 自身的构建/运行时错位(mcpp#352 的形状),应当单独提。 + +这也是为什么全套测试断言的是「后端**存在于**将被搜索的路径上」而不是「后端能加载」—— +后者在这台机器上永远红,且红的原因不在本方案范围内。 + +--- + +## 8. 深度自我 review(2026-08-30 第二轮) + +> **⚠ 本节的 8.1(结论:mcpp 侧应采用「分层继承 B3」)已被 §12.1 推翻。** 实现时测出: +> 装到**全局 scope** 之后 sysroot 天然看得见,B3 不需要。8.1 那张「项目 subos 缺 +> libgcc_s/libstdc++」的表仍是事实,但它证明的是「别往项目 scope 装」,不是「需要叠加」。 +> 8.2–8.6 的其余内容仍然成立。**mcpp 侧怎么修,以 §12.1 为准。** + + +第一轮方案有四处实质错误。逐条记录,因为其中三条是「没做实验就下结论」。 + +### 8.1 B1/B2 是假二分,正确形态是**分层继承**(B3) + +**实测**:项目 subos `_` 里有什么: + +| | | +|---|---| +| `libc.so.6` / `crt1.o` / `libm.so.6` / `ld-linux-x86-64.so.2` | ✔ | +| **`libgcc_s.so.1` / `libstdc++.so.6`** | **✗ 没有**(工具链 subos 里有) | +| `libgbm.so{,.1}` / `libEGL.so.1` / `libGL.so.1` / `usr/include/gbm.h` | ✔ | + +所以**项目 subos 不是工具链 subos 的超集**。由此: + +- **B2(切换)会回退**:sysroot 换过去就丢了 gcc runtime。我当时反对 B2 的理由是「爆炸半径大 / + 会 full rebuild」—— 那是**弱理由**(fingerprint 变化本就由 `contractHash` 正确处理,一次性重建 + 不是正确性问题)。真正的反对理由是**它会丢东西**,而我没测出来。 +- **B1(只合 env)不够**:它修好 `GBM_BACKENDS_PATH`,但 `gbm.h` 与 `-lgbm` 仍然只能靠 compat 包 + 自己声明。也就是说 B1 把「运行期」修好了,「构建期」原样留着。 + +**B3 = 默认用 mcpp 的工具链 subos 作**基座**,项目 subos 作**叠加层**:** + +``` +sysroot : --sysroot= ← 不变,保住 gcc runtime +叠加 : -isystem /usr/include + -L /lib + -Wl,-rpath,/lib +env : 项目 subos 的 envs 按 prepend 合并(项目在前) +``` + +一个编译器只吃一个 `--sysroot`,所以「继承」在实现上必然是「基座 sysroot + 叠加 `-isystem`/`-L`」, +而不是换 sysroot。这既拿到了 B2 想要的东西(头和库直接可见),又不承担 B2 的回退风险, +比 B1 多修一个构建期。 + +### 8.2 我从没验证过「不要 compat 包」这条路走不走得通 + +第一轮直接断言「薄壳仍然要保留」,没有证伪替代方案。补测之后: + +| 尝试 | 结果 | +|---|---| +| `[xlings] deps = ["mesa"]`(无 compat 依赖) | `.mcpp/.xlings.json` 里确实写进了 `"deps": ["mesa"]`,但**没有安装、没有建 subos**,`fatal error: gbm.h: No such file or directory` | +| `[xlings] subos = "_"`(subos 不存在时) | **报错**:`selected SubOS '_' does not exist … create/bootstrap that environment instead of falling back`,mcpp 不会自举 | +| 包声明 `xpm.deps.runtime = { "xim:mesa" }` | ✔ 项目 subos 被建出来,mesa 进去 | + +**⇒ 今天唯一能把 xim 层引进一个 mcpp 工程的东西,就是 compat 包的 `xpm.deps.runtime`。** + +结论没变(薄壳要保留),但**理由变了**,而且这个理由本身是第三个洞: + +- **R2a**:`[xlings] subos` 只能**选择**已存在的 subos,不能创建。 +- **R2b**:`[xlings] deps` 只被**物化**进 `.mcpp/.xlings.json`,没有被**供给**(install/expose)。 + +R2a + R2b 不修,B3 从 manifest 侧就是不可达的 —— 用户写 `[xlings]` 也拿不到东西, +只能绕道「随便依赖一个声明了 `xim:mesa` 的 compat 包」。这恰好就是 `compat.libgbm` 现在的处境: +**它事实上在扮演「xim 层的入口」,而这不该是一个库包的职责。** + +### 8.3 V 矩阵把两种 C2 混在一起了 + +原矩阵的 V3/V5 默认「C2 之后头和库也就有了」。只有 B3 成立;B1 之下 V5 永远不可能绿, +因为删掉 constructor 只影响 env,而 `include_dirs`/`ldflags` 本来就来自包本身。 + +修正:V5 的准入条件应写成「**C2 采用 B3** 且 V3 绿」。若最终只做 B1,则 +`compat.libgbm` 的薄壳形态是**长期**的,不是过渡的 —— 这对 §3「是否独立成包」的结论没有影响 +(仍然该独立、该薄),但对「多久之后能删 constructor」的预期影响很大。 + +### 8.4 R2 应拆成三条 + +原文一条 R2 说不清。正确的分解: + +| | 缺陷 | 影响 | +|---|---|---| +| **R2a** | `[xlings] subos` 不能自举 | manifest 侧无法建立项目 subos | +| **R2b** | `[xlings] deps` 只物化不供给 | 声明了也拿不到东西 | +| **R2c** | 默认选择只读工具链 subos,无分层 | 项目 subos 的 env / 头 / 库全部不可见 | + +`mcpp#352` 修的是「怎么注入 env」,R2c 是「从哪读」,R2a/R2b 是「谁来建」。三者独立。 + +### 8.5 修正后的建议 + +- **C2 采用 B3(分层继承)**,不是 B1。理由见 8.1:B1 只修一半,B2 会丢 gcc runtime。 +- **C2 的前置**是 R2a/R2b,否则 B3 只能被包触发,manifest 侧仍然不可用。 +- **C3 的准入条件**改为「B3 落地 + V3 绿 + V5 绿」。 +- §3 的结论(`compat.libgbm` 应独立且薄)**不变**,但要补一句:它今天还额外承担了 + 「xim 层入口」这个不属于它的职责,R2a/R2b 修好之后这份职责才真正卸掉。 + +### 8.6 仍然成立的部分 + +- §1 的全部实测证据(两个 `.xlings.json`、`subos = "_"` 的前后对比)。 +- **R1 与 C1 完全不受影响** —— 它在另一个仓,独立可合,且单独就能修好 xlings 侧消费者。 +- §3 的行业论证(按接口拆包;Conan 无 gbm recipe、`/system` 形态)。 +- §7 的 glibc 错位,与本方案正交。 + +--- + +## 9. R2b 的修法:首次构建自动供给 `[xlings] deps` + +### 9.1 现状 + +`[xlings]` 段被 1:1 物化成 `ProjectEnv`(`src/platform/xlings/xlings.cppm:307`): + +```cpp +struct ProjectEnv { + std::vector deps; // → .xlings.json "deps" + ... workspace / subos / envs +}; +``` + +`seed_xlings_json(env, repos, mirror, penv)` 把它写进 `/.mcpp/.xlings.json`。 +**写完就结束了 —— 没有任何一处去装它。** 实测:`deps = ["mesa"]` 写进了文件, +`gbm.h` 依然 not found,项目 subos 根本没被创建。 + +### 9.2 mcpp 里已经有两条现成的「声明 → 自动安装」路径 + +**(a) 工具链首次运行**(`src/build/prepare.cppm` ~1690): + +```cpp +mcpp::ui::info("First run", + std::format("no toolchain configured — installing {} ({}) as default", …)); + +mcpp::fetcher::Fetcher fetcher(**cfg); +mcpp::fetcher::InstallProgressHandler progress; +for (auto dep : {"xim:glibc", "xim:linux-headers"}) + (void)fetcher.resolve_xpkg_path(dep, /*autoInstall=*/true, &progress); +auto payload = fetcher.resolve_xpkg_path(defaultPkg.target(), /*autoInstall=*/true, &progress); +``` + +**(b) 项目作用域安装**(`src/build/prepare.cppm` ~2936),已经带实时进度 UI: + +```cpp +auto projEnv = mcpp::config::make_project_xlings_env(**cfg, *root); +auto argsJson = std::format(R"({{"targets":["{}"],"yes":true}})", target); +mcpp::fetcher::InstallProgressHandler progress; +auto r = mcpp::xlings::call(projEnv, "install_packages", argsJson, &progress); +``` + +注释里写得很清楚:安装目的地由**包的 scope(project vs global)**决定,不由 transport 决定 —— +也就是说 (b) 装出来的东西正好落在**项目**作用域,而项目 subos 正是这样被建出来的。 + +### 9.3 提案 + +在 `seed_xlings_json` 物化 `ProjectEnv` 之后、runtime selection 之前, +若 `penv.deps` 非空且尚未满足,走 **(b)** 的同一条路把它们装上: + +```cpp +if (!penv.deps.empty() && !already_provisioned(penv.deps)) { + mcpp::ui::info("First run", + std::format("provisioning [xlings] deps — installing {}", join(penv.deps))); + auto projEnv = mcpp::config::make_project_xlings_env(cfg, root); + auto argsJson = to_targets_json(penv.deps); // {"targets":[…],"yes":true} + mcpp::fetcher::InstallProgressHandler progress; + auto r = mcpp::xlings::call(projEnv, "install_packages", argsJson, &progress); + if (!r) return std::unexpected(/* 与工具链同款:给出手工命令 */); +} +``` + +要点: + +- **复用 (b) 而不是新写一条**,因为 scope 语义、进度 UI、错误捕获(`captured_error()`) + 都已经是对的; +- 失败信息照抄工具链那条的形状:说明失败了、并给出**手工可执行的等价命令**; +- 幂等:已装则跳过 —— 与工具链首次运行一样只在缺失时触发; +- **供给必须发生在 runtime selection 之前**,否则 §8.2 那条 + `selected SubOS '_' does not exist` 会先一步报错。这条顺序约束就是 **R2a 的实质**: + R2a 与其说是「subos 要能自举」,不如说是「**供给先于选择**」。修好顺序, + R2a 作为独立缺陷基本消失,`[xlings] subos` 可以继续保持「只选择、不创建」的严格语义。 + +### 9.4 R2b 修好之后,直接消费这条路就通了 + +预期(修完应当能实测通过,即新增回归): + +```toml +[xlings] +deps = ["mesa"] + +[build] +ldflags = ["-lgbm"] +``` + +配合 B3 分层,`#include ` / `-lgbm` / `GBM_BACKENDS_PATH` 全部可用, +**不需要任何 mcpp-index 包**。 + +--- + +## 10. 综合 review(第三轮):这套方案自洽吗? + +### 10.1 R2b + B3 之后,`compat.libgbm` 还需要存在吗? + +**需要,而且理由比前两轮更硬 —— 是「传递依赖」。** + +§9.4 那条路只对**应用自己的 manifest** 成立。而 GBM 的真实消费者多数是**库**: +`compat.sdl2` 的 KMSDRM 后端、wlroots、ffmpeg 的 VAAPI hwcontext。 +一个库包**无法往消费者的 `mcpp.toml` 里注入 `[xlings] deps`** —— 它只能声明一条依赖边。 + +所以两条路各有各的用途,不重复: + +| 场景 | 用什么 | +|---|---| +| 应用自己要用 gbm | `[xlings] deps = ["mesa"]`(R2b 之后) | +| **库**要用 gbm,并让它随依赖图传播 | **`compat.libgbm`** | + +Conan 也正是这么并存的:`opengl/system` 是一个**包**而不是「让用户自己写 +system_requirements」,因为 `sdl`、`glfw` 这些库需要 `requires` 一个东西。 +`compat.libgbm` 在 mcpp 里承担同一角色。 + +`ldflags = ["-lgbm"]` 也一样:让每个消费者手写是错的,那属于包的 `package_info()`。 + +### 10.2 三轮下来,哪些结论真正稳定 + +| 结论 | 第1轮 | 第2轮 | 第3轮 | +|---|---|---|---| +| R1 / C1(GBM 进 DISCOVERY) | ✔ | ✔ | ✔ **从未动摇,且独立可合** | +| 按接口拆包、包要薄 | ✔ | ✔ | ✔ | +| `compat.libgbm` 应独立存在 | ✔(理由弱) | ✔(理由:唯一入口) | ✔ **(理由:传递依赖)** | +| constructor 不属于包的职责 | ✔ | ✔ | ✔ | +| mcpp 侧该怎么修 | B1 | **B3** | B3 + R2b(供给) | +| R2 的分解 | 一条 | 三条 | 三条,且 **R2a 降级为顺序约束** | + +理由被换掉三次而结论不变的那几条,才是真的稳。**唯一反复改的是 mcpp 侧的形态** —— +因为那是我唯一没有一开始就去读源码/做实验的部分。 + +### 10.3 还没验证、需要在实现时确认的假设 + +诚实列出,不假装已闭环: + +1. **B3 的叠加是否会与包自己的 `include_dirs` 撞车。** 项目 subos 的 `usr/include` 会带进 + `EGL/`、`GL/`、`KHR/`(mesa + libglvnd),而 `compat.opengl` / `compat.glx-headers` + 也提供 `GL/` —— `compat.glx-headers` 的注释已经记过这个重叠是真实踩坑。 + **叠加顺序必须让包的 `include_dirs` 优先于 subos 叠加层**,否则等于给所有消费者 + 换了一套 GL 头。这是 B3 最需要测的一点。 +2. **`install_packages` 对「已装」是否幂等**,以及在离线/无网时的行为。 +3. **`${subosdir}` 的展开源**:必须是声明所在的那个 subos,不是 `binding.subosDir`(§2 B1 已记)。 +4. **workspace 成员**:`select_runtime` 用的是 `workspaceManifest` 的 `[xlings]` + (`owner = workspaceManifest ? … : projectManifest`),所以 mcpp-index 这种虚拟 workspace + 里,`[xlings]` 该写在根还是成员,需要确认;写错会静默不生效。 + +### 10.4 对 PR #281 的最终判断 + +不变:**以现状合并是安全的**,它自包含、CI 全绿、对消费者透明(`#include ` 即可), +唯一代价是一份带删除条件的临时 constructor + 后端 farm。 + +但 §10.1 修正了一处预期:即便 R1 + R2b + B3 全部落地,**这个包也不会消失**, +只会瘦下来 —— 它作为「库→库」传递依赖的载体是长期的。 + +--- + +## 11. 任务拆分与依赖关系(执行版) + +### 11.1 任务表 + +| ID | 仓 | 内容 | 依赖 | 可并行 | +|---|---|---|---|---| +| **T1** | xim-pkgindex | `graphics.GBM_DIR` + `DISCOVERY` 一行 + `declare_gbm()`;`mesa.lua` 调用 | — | ✔ 起点 | +| **T2** | xim-pkgindex | T1 的测试(vendor-form harness 同款) | T1 | | +| **T3** | mcpp | R2b:`[xlings] deps` 首次构建自动供给 | — | ✔ 与 T1 并行 | +| **T4** | mcpp | B3:工具链 subos 作基座 + 项目 subos 叠加 | T3(供给先于选择) | | +| **T5** | mcpp | T3/T4 回归测试 | T4 | | +| **T6** | mcpp-index | `compat.libgbm` 定型 + 文档 | — | ✔ 与 T1/T3 并行 | +| **T7** | mcpp-index | `stock_usage.cpp` 保留为闭环回归 | T6 | | +| **T8** | 三仓 | 规范/文档同步(含 zh) | T1/T4/T6 | | +| **T9** | — | release + gtc 补 CN 资源 | T6 | | +| **T10** | — | 生态真实验证(`xlings subos --sandbox --cmd`) | T1(+T4) | 终点 | + +关键路径:**T1 → T10**。T3/T4 是另一条独立链,不阻塞 GBM 闭环 —— 这是把 +R1 与 R2 拆开的最大收益。 + +### 11.2 多角度评估 + +**架构** —— 每个改动都落在**已经拥有该职责**的那一层:发现路径归 `graphics.lua` +(它已经管 DRI/EGL/XDG),供给归 mcpp(它已经为工具链做过一次),接口暴露归 compat 包。 +没有任何一层被要求承担新职责,所以没有新的抽象。 + +**稳定性** —— T1 是纯增量:`DISCOVERY` 多一行,旧消费者读不到新变量也不会坏; +`declare_gbm` 缺目录时 `warn + return false`,不中断安装。T4 是叠加而非替换, +不会丢工具链的 `libgcc_s`/`libstdc++`(§8.1 实测)。 + +**优雅简洁** —— T1 全部收益来自「把 GBM 加进一张已经存在的表」,`consumer_envs()` +与 `declare_subos_env()` 自动跟随,不需要第二处改动。T6 之后包里**没有一行** +GBM 特有的运行期逻辑。 + +**用户体验** —— 终态是 `#include ` 就能用,不需要知道任何 mcpp/xlings 概念。 +T3 的失败信息照抄工具链那条:说明失败并给出**手工可执行的等价命令**。 + +**兼容性** —— `prepend` 而非 `set`,尊重用户已 export 的值(`graphics.lua` 自己论证过 +`set` 会抹掉 NVIDIA 目录)。旧 mesa 版本没有 `lib/gbm` 时安装照常成功。 + +**跨平台** —— GBM 是 Linux DRM 概念,`xpm` 只有 `linux` 段,测试在非 Linux 编译成 +no-op `main()`(`compat.libaio`/`compat.wil` 同款)。T3/T4 是平台无关的 subos 逻辑。 + +**一致性** —— `declare_gbm` 是 `declare_dri` 的镜像;`GBM_DIR` 与 `DRI_DIR` 同在 +`usr/` 下(受 `is_permitted_file_destination` 白名单约束,见 11.3); +T3 复用 `install_packages`,T4 复用既有 `EnvDecl`。 + +**无感升级** —— 三个改动都不需要消费者改任何东西:T1 在下次 `xlings install/update` +时随 mesa 的 `config()` 生效;T4 之后旧工程只是多拿到几个变量;T6 删 constructor 时 +公开 API 没有变化(本来就是 `#include `)。`binding.environment` 参与 +`contractHash`,声明变化自动使快取失效,不会出现「新声明 + 旧产物」。 + +**测试覆盖** —— §4 的 V1–V5,其中 **V5 是删 constructor 的机械准入**; +`stock_usage.cpp` 是防止「退回 opt-in 修法」的回归;反向验证(移走 `dri_gbm.so`) +已实测能让断言转红。 + +### 11.3 实现期发现的硬约束(写进代码注释) + +`graphics.lua` 自己记着:**xlings 只允许 `usr`/`etc`/`share` 开头的 file asset 目标** +(`xvm/bindings.cppm` 的 `is_permitted_file_destination`),而且**被拒绝不是错误 —— +放置只是不发生**。所以 `GBM_DIR` 必须是 `usr/lib/gbm`;写成 `lib/gbm` 会得到一个 +安装干净、变量指向不存在目录、`gbm_create_device()` 静默返回 NULL 的配方。 +这与 `DRI_DIR = "usr/lib/dri"` 的理由完全相同。 + +--- + +## 12. 实现与验证结果(2026-08-30 执行记录) + +### 12.1 结论先行:**B3 不需要** + +第 8 节推导出「必须分层继承(B3)」,实现时被实验推翻。**真正的修法是把 +`[xlings] deps` 装到 GLOBAL scope**,因为那个 registry 的 SubOS **就是** mcpp 的 +`--sysroot`。一旦装对了地方,头和库天然可见,不需要任何 `-isystem`/`-L` 叠加。 + +推翻的过程(每一步都是实测): + +| 试的东西 | 结果 | +|---|---| +| `install_packages` + `make_project_xlings_env`(**项目** scope) | 装成功、`.xlings.json` 也写了,但头落在 `/.mcpp/.xlings/subos/_/usr/include`,而 `--sysroot` 指的是 `/registry/subos/default` → **`gbm.h` 仍然找不到** | +| `resolve_xpkg_path`(工具链同款,**全局** scope) | 头进了 sysroot,`gbm.h` 找到了 ✔ —— 但它要求 `@`,裸名报 `invalid xpkg target 'xim:mesa': expected @` | +| **`install_packages` + `make_xlings_env`(全局 scope)** | ✔ 全对:裸名、带命名空间、带版本都能用,歧义名还会列出候选 | + +所以 §8.1 那张「项目 subos 缺 libgcc_s/libstdc++」的表仍然是**事实**,但它证明的不是 +「需要分层」,而是「**不该往项目 subos 装**」。结论方向反了,数据没错。 + +**这也让 mcpp 侧的改动小了一个数量级**:没有碰 `linkmodel.cppm`、 +`plan.runtimeSearch`、`link_line.cppm` 的任何排序不变量 —— 那几处的注释明确写着 +「一个可变视图排在已链接产物之前会让后续安装悄悄改变加载的库,这不是假设,正是本模块诞生的原因」。 +不动它们是这次实现最重要的克制。 + +### 12.2 已实现并验证 + +**T1 / C1 — xim-pkgindex** ([openxlings/xim-pkgindex#713](https://github.com/openxlings/xim-pkgindex/pull/713)) + +`graphics.GBM_DIR` + 一行 `DISCOVERY` + `declare_gbm()`,`mesa.lua` 调用。 +测试:`tests/test_graphics_gbm_discovery.py`(5 例)+ 纯 Lua harness。 +**两种静默失败都实测转红**:删掉 DISCOVERY 行、把 `GBM_DIR` 写成 `lib/gbm`(白名单外)。 +既有 graphics/mesa 测试 22 项全过。 + +**T3 / R2b — mcpp**(`feat/xlings-subos-layering`) + +`[xlings] deps` 首次构建自动供给,全局 scope,内容级幂等(stamp)。 +实测第二次 `mcpp run` 输出 0 行 `Provisioning`。 + +**T6 — mcpp-index**(PR #281,CI 全绿) + +`compat.libgbm` + 两个测试二进制,constructor 形态,已在 §10 论证为传递依赖载体。 + +### 12.3 生态真实验证(`xlings subos --sandbox --gpu`) + +全新 subos `eco-gbm-20260830`,`xlings install xim:mesa@25.0.7.2`: + +``` +[xlings] subos eco-gbm-20260830: 4 env var(s) from 1 package(s) ← 原本 3 +GBM_BACKENDS_PATH=/home/speak/.xlings/subos/eco-gbm-20260830/usr/lib/gbm +/usr/lib/gbm/dri_gbm.so +``` + +沙箱内真跑(`--sandbox --gpu`): + +``` +search paths /usr/lib/gbm, suffix _gbm ← 是我们的路径,不是 /usr/lib/gbm +/dev/dri/renderD128 gbm_create_device = 0x27080ed0 backend = drm +/dev/dri/card0 gbm_create_device = 0x27080ed0 backend = drm +/dev/dri/card0 gbm_bo_create = 0x2708b8a0 ← 真的分配出了 buffer object +RESULT: PASS +``` + +`renderD128` 上 `gbm_bo_create` 返回 NULL 并打印 +`KMS: DRM_IOCTL_MODE_CREATE_DUMB failed: Permission denied` —— 那是 render node +的权限边界(dumb buffer 需要 KMS 权限),不是打包问题;`card0` 上分配成功。 + +### 12.4 mcpp 侧闭环(全新 MCPP_HOME,零 mcpp-index 依赖) + +```toml +[package] +name = "nopkg" +version = "0.1.0" + +[xlings] +deps = ["xim:mesa"] + +[build] +ldflags = ["-lgbm"] +``` + +`src/main.cpp` 只 `#include `: + +``` +Provisioning [xlings] deps (xim:mesa) +Compiling nopkg v0.1.0 (.) +Running `target/.../bin/nopkg` +XR24 | GBM_BACKENDS_PATH=/subos/default/usr/lib/gbm +``` + +编译、链接、运行、环境变量四项全通,**没有任何 mcpp-index 包**。 + +### 12.5 唯一仍未闭合的一环,且不在本方案范围内 + +`xim-x-mesa/25.0.7.2` 的 payload RUNPATH 指向 `xim-x-glibc/2.39/lib64`, +而它自己的 `libgallium-25.0.7.so` 需要 `GLIBC_2.43`(store 里有 2.44 和 2.44.2): + +``` +MESA-LOADER: failed to open dri: …/xim-x-glibc/2.39/lib64/libm.so.6: +version `GLIBC_2.43' not found (required by …/libgallium-25.0.7.so) +(search paths /usr/lib/gbm, suffix _gbm) +``` + +注意 search path 已经是对的 —— **可达性已闭合**,倒在下一跳。12.3 那次成功的运行 +是把 `LD_LIBRARY_PATH` 指向 2.44.2 之后取得的,用来隔离出这一个变量。 + +`mesa.lua` 自己不做任何 patchelf(全仓只有 `graphics.lua`/`hostlib.lua` 用 patchelf, +且都是只读的 `--print-rpath`),所以这要在 `xlings-res` 侧重建/重打 payload 解决, +**不该由 recipe 或 compat 包绕过**。已单独记录待报。 + +### 12.6 验证期踩到的两个坑(留给下一个人) + +1. **`MCPP_HOME` 会再拼一层 `registry/`**。设 `MCPP_HOME=` 时实际用的是 + `/registry/subos/default`。我一度在检查 `/subos/default` 并得出「envs 是空的」 + 的错误结论,而真正在用的那个里面有东西。查 sysroot 请以 `build.ninja` 里的 + `--sysroot=` 为准,别自己推。 +2. **mcpp 有自己的一份索引副本**(`/registry/data/xim-pkgindex`), + 与 `~/.xlings/data/xim-pkgindex` 是两份。改了后者,mcpp 侧不会看到, + 要等合并 + artifact 重新发布(本地验证时手工同步了两份)。 + +--- + +## 13. 交付状态(每仓一个 PR) + +| ID | 仓 | 状态 | 落在哪 | +|---|---|---|---| +| **T1** | xim-pkgindex | ✅ 完成 | [#713](https://github.com/openxlings/xim-pkgindex/pull/713) | +| **T2** | xim-pkgindex | ✅ 完成 | 同上(`test_graphics_gbm_discovery.py` 5 例 + Lua harness) | +| **T3** | mcpp | ✅ 完成 | [#531](https://github.com/mcpp-community/mcpp/pull/531) | +| **T4** | mcpp | ⛔ **不需要**,已退休 | 见 §12.1 | +| **T5** | mcpp | ✅ 完成 | `mcpp test` 96 passed;SubOS-env 轴由既有 `tests/e2e/200_subos_env_reaches_program.sh` 覆盖 | +| **T6** | mcpp-index | ✅ 完成 | [#281](https://github.com/mcpplibs/mcpp-index/pull/281) | +| **T7** | mcpp-index | ✅ 完成 | 同上(`stock_usage.cpp`) | +| **T8** | 三仓 | ✅ 完成 | 各自 PR 内(mcpp-index 含 zh) | +| **T9** | — | ✅ 完成 | `gitcode.com/mcpp-res/libgbm@2026.08.29`,与 GLOBAL 逐字节一致 | +| **T10** | — | ✅ 完成 | §12.3 / §12.4 | + +**每个仓恰好一个 PR。** + +### 13.1 「SubOS 环境那半哪去了?」 + +这是 review 时最该问的问题,答案是:**它一直都在 mcpp 里,不需要新写**。 + +| 环节 | 在哪 | 状态 | +|---|---|---| +| 解析 `.xlings.json` 的 `envs` | `subos_info.cppm` → `EnvDecl{var,op,value}` | 早已有 | +| 收进 binding | `runtime_binding.cppm` → `binding.environment` | 早已有 | +| 展开 `${subosdir}` / 应用 `prepend` / 注入子进程 | `execute.cppm::compute_subos_env()`(mcpp#352) | 早已有 | +| **回归测试** | `tests/e2e/200_subos_env_reaches_program.sh` | 早已有 | +| **声明里有 GBM 这一项** | `libs/graphics.lua` 的 DISCOVERY | ← **T1,这次补的** | +| **SubOS 里有东西可读** | `[xlings] deps` 自动供给 | ← **T3,这次补的** | + +所以 #531 只包含供给逻辑是**正确的**:环境注入不缺,缺的是「声明里没有 GBM」和 +「mcpp 读的那个 SubOS 里没有 mesa」。§12.1 的三次实测把 T4 也一并否掉了 —— +装对 scope 之后,sysroot 天然看得见,不需要叠加层。 + +一句话:**mcpp#352 修好了「怎么注入」;T1 补上「注入什么」;T3 补上「从哪读得到」。** + +--- + +## 14. 要不要把 mesa(或它内部可分离的库)做成 mcpp-index 包? + +mcpp **确实**支持共享库包 —— `docs/package-types.md` 的形态 F(「共享库 compat:必须是 +唯一的那个 `.so`」),`compat.x11` 家族与 linux 上的 `compat.vulkan` 就是。所以这不是 +「能不能」的问题,是「该不该」。 + +### 14.1 mesa 本体:不建议 + +三条,前两条已实测: + +1. **一个进程里会出现两个 `libgbm.so.1`。** `xim:mesa` 已经提供一个,索引再建一个, + 谁被加载取决于搜索顺序。`compat.vulkan-runtime` 的注释对同一件事的结论是 + 「宿主自己的 `libvulkan.so*` **刻意不** harvest……一个进程一个 loader 才是重点」。 + +2. **混合链接形态 = 静默符号劫持。** 索引里的 `compat.*` 全是**静态**包,而 mesa 的 + payload 是 shared 闭包。实测(glib/zlib,见 [[mcpp-prebuilt-package-route]]):exe 的 + `.dynsym` 导出 86 个 zlib 符号,`LD_DEBUG=bindings` 显示 libgio 的 12 次 zlib 调用 + **全部绑到 exe**,捆进去的 `libz.so.1` 被完全遮蔽成死重 —— **跑得通、无告警**。 + vcpkg 用 triplet、Conan 用 `shared` option 沿图传播来禁止「一次链接里混形态」, + mcpp/xlings 缺的正是这个全图开关。 + +3. **成本。** mesa 要 meson + LLVM + ~30 个依赖;而 `xlings-res/mesa` 已经把这件事做完了。 + +### 14.2 内部「可分离的库」:`compat.libdrm` 值得做 + +用户的直觉在这里是对的 —— **有些确实是独立项目**,判据仍是 §3 那条(上游是否作为 +可独立分发的单元发布): + +| 候选 | 是独立项目吗 | Conan 有没有 | 建议 | +|---|---|---|---| +| **libdrm** | ✔ 独立发布 | ✔ 真配方(源码构建) | **值得加**,见下 | +| expat | ✔ 独立发布 | ✔ | 可加,按需 | +| libglvnd | ✔ 独立发布 | ✔ | 索引已有 `compat.glx-headers`(只取头) | +| **libgbm / libEGL / libGL** | ✗ 是 mesa 内部 target | ✗ **Conan 根本没有** | 保持绑定形态 | + +**`compat.libdrm` 有一条现成的理由**:`compat.vulkan-runtime` 今天从**宿主** +`/usr/lib/x86_64-linux-gnu` harvest `libdrm*.so.*` —— 这正是本方案第 2 节反复要消除的 +host 依赖。一个源码构建的 `compat.libdrm` 能把那条 host 边关掉,而且它**不触发 14.1 的 +任何一条**:libdrm 是独立项目、索引里没有第二份、可以静态构建从而与其余 `compat.*` +形态一致。 + +**结论**:不复制 mesa;把「可分离」这条判据用在真正可分离的东西上,而 libdrm 是最该先做的 +那个。`compat.libgbm` 保持 §3/§10.1 论证过的薄绑定形态。 + +--- + +## 15. xlings 版本 pin:**不下调,现状已经是对的** + +任务里有一条「顺带 pin 一下内部依赖的 xlings 版本到 `2026.8.27.4`,到时候应该就发布了」。 +查完之后这条建立在一个反过来的前提上,所以**没有执行**,理由如下。 + +### 15.1 事实 + +``` +gh release list --repo openxlings/xlings +2026.8.27.5 Latest v2026.8.27.5 2026-08-27T13:29:41Z ← 已发布,8 个 asset +2026.8.27.4 v2026.8.27.4 2026-08-27T10:18:31Z ← 更旧 +``` + +`.4` **不是待发布的新版,是已经发布的旧版**;`.5` 才是 Latest,而且三仓的 pin 已经都在 `.5`: + +| 仓 | pin | 状态 | +|---|---|---| +| mcpp | `src/xlings/xlings.cppm` `kXlingsVersion = "2026.8.27.5"` | `.github/tools/check_version_pins.sh` → `OK: xlings pins all at 2026.8.27.5` | +| xim-pkgindex | `pkgs/x/xlings.lua` `latest = 2026.8.27.5` | 一致 | +| mcpp-index | 只 pin mcpp(`MCPP_VERSION`),不 pin xlings | 无关 | + +所以「pin 到已发布的 xlings」这个**意图本身已经满足**,而且是被更好地满足了。 + +### 15.2 下调会丢掉一个有记录的性质 + +`src/xlings/xlings.cppm` 的注释写明了为什么是 `.5` 而不是 `.4`: + +> 2026.8.27.4 和 .5 都改成读索引,因此保持一致。**`.5` 还让声明在解析时压过索引, +> 所以即使 `latest` 不是表里最高的那条,它也成立。** + +`.5` 之下(≤ `.4`)那条「声明压过索引」的行为没有。同一段注释还记录了 `.5` 作为 +**下限**要挡住的故障:低于 `2026.8.27.2` 的 xlings 从编译进去的常量取 runtime binding, +于是一个 home 可以声明一个 glibc、装另一个,而报错的是 mcpp: + +``` +error: selected RuntimeBinding glibc@2.44 requires payload + '/xim-x-glibc/2.44', but it is not installed +``` + +`kXlingsVersion` 是**全仓每一处 xlings pin 的唯一真源**,`check_version_pins.sh` 会强制 +`.github/` 下所有地方与它一致 —— 下调会同时改动 release/CI/bootstrap 的十几处。 + +### 15.3 那条 warning 的正确解法不是下调 + +本地看到的 + +``` +Note vendored xlings 2026.8.27.4 is older than the pinned 2026.8.27.5, +but no newer source is available (keeping it; run `xlings self update`) +``` + +说的是**随 mcpp 发行版打包进去的那份 vendored 副本**是 `.4`,不是说 `.5` 拿不到 —— +`.5` 有 8 个 release asset,可下载。这条消息自己给了解法:`xlings self update`。 +把 pin 降到 `.4` 只会让「pin 与 vendored 一致」,代价是整个生态退回到没有 +「声明压过索引」的那个版本。 + +**结论**:保持 `2026.8.27.5`。若确实要下调,那是一次独立的、影响 release/CI/bootstrap +的变更,应当单独评估,不该搭在本方案里顺带做。 + +--- + +## 16. V5 已验证:合并 #713 之后 constructor 可以删 + +§4 把「删 constructor」定义成一个**机械准入条件**而不是判断题。现在它通过了。 + +### 16.1 做法 + +把合并后的状态在本地模拟出来:`libs/graphics.lua` + `pkgs/m/mesa.lua` 的 #713 版本 +复制进 `~/.mcpp/registry/data/xim-pkgindex`,再 `xlings install xim:mesa@25.0.7.2` +让 `config()` 重跑: + +``` +mesa vars: ['LIBGL_DRIVERS_PATH', '__EGL_VENDOR_LIBRARY_DIRS', 'XDG_DATA_DIRS', 'GBM_BACKENDS_PATH'] +/subos/default/usr/lib/gbm/dri_gbm.so +``` + +然后走**真实依赖路径**(`[dependencies.compat] libgbm`,无 `[xlings]`、无 `ldflags`, +`#include `),经 CN 镜像冷跑: + +``` +XRGB8888 -> XR24 +GBM_BACKENDS_PATH = /home/speak/.mcpp/registry/subos/default/usr/lib/gbm +``` + +### 16.2 为什么这一行就是证明 + +值是 **subos 路径**,不是包自己的 farm(`…/compat-x-libgbm/…/mcpp_generated/libgbm/lib/gbm`)。 +constructor 的实现是 **`if (getenv("GBM_BACKENDS_PATH")) return;`** —— 只在未设置时才写。 +既然进程里读到的是 subos 的值,说明**在 constructor 运行之前它已经被生态设好了**, +constructor 这一步是空转。 + +对比同一个包在**未打补丁**的索引下(本文档 §12.4 之前的所有测量),同一条路径给出的是 +包内 farm 的值 —— 差别只有一个变量:索引里有没有那一行 DISCOVERY。 + +### 16.3 为什么现在还不能删 —— ⚠ 已过时,#713 已合并,constructor 已删除(见 §18) + +`#713` 尚未合并,artifact 也没重新发布。删了之后: + +* 用**已发布索引**的消费者拿不到 `GBM_BACKENDS_PATH`,`gbm_create_device()` 回到返回 NULL; +* mcpp-index CI 会红 —— `tests/stock_usage.cpp` 断言的正是「只 include `` 的消费者 + 能拿到这个变量」,而 CI 用的是已发布的 xim 索引。 + +**这正是我们想要的顺序保证**:准入条件由 CI 机械把关,而不是靠人记得。 + +### 16.4 合并之后的收尾(一步) + +`#713` 合并 + artifact 重新发布后,在 `pkgs/c/compat.libgbm.lua` 删掉: + +* `generated_files`/`install()` 里写的 `mcpp_generated/gbm_backends.c` 整个 TU + (constructor + `mcpp_gbm_backends_dir` + `mcpp_gbm_use_sibling_backends`); +* `install()` 里的后端 farm(`lib/gbm/` 那段与 `mesa_libdir()` 辅助函数); +* `mcpp_gbm.h`,以及 `include_dirs` 中对它的依赖(`gbm.h` 仍从 subos view 取); +* `tests/gbm.cpp` 里与 constructor 相关的断言(§0 的「入口即已设置」改为断言来源是 subos; + re-exec 那条随 constructor 一起删)。 + +`tests/stock_usage.cpp` **原样保留** —— 删掉 constructor 之后,它断言的就从 +「本包的 constructor 生效了」变成「**整条生态闭环生效了**」,是这个包最有价值的一条回归。 + +--- + +## 17. §14.2 的更正,以及真正该做的下一步 + +§14.2 把 `compat.libdrm` 列为「最该先做的那个」,理由是它能关掉 +`compat.vulkan-runtime` 从宿主 harvest `libdrm*.so.*` 那条边。**这个理由是错的**, +而查错的过程指向了一个更好的下一步。 + +### 17.1 为什么那个理由不成立 + +`compat.vulkan-runtime` 的农场存在的原因是:被 `dlopen` 的 **ICD 本身来自宿主**, +而它有自己的 `DT_NEEDED`(libdrm、LLVM、xcb…),这些必须在同一个目录里解析得到。 +所以那里需要的是**宿主兼容的共享库**。 + +一个索引内源码构建的 `compat.libdrm` 是**静态**包(索引里的 `compat.*` 都是), +**满足不了一个 `.so` 的 `DT_NEEDED`**。所以它根本替代不了那条边 —— 我把「有个包叫 +libdrm」和「农场里那条 `libdrm*.so.*` 需求」当成同一件事了,它们不是。 + +`compat.libdrm` 仍然可能有价值(给**构建期**消费者,例如将来若真要源码建 GBM 前端), +但**不是**因为它能关掉 vulkan-runtime 的 host 边。 + +### 17.2 真正该做的下一步:vulkan-runtime 走 glx-runtime 走过的那条路 + +实测 `xim:mesa` 的 payload: + +``` +share/vulkan/icd.d/radeon_icd.x86_64.json +lib/libvulkan_radeon.so +``` + +而 `mesa.lua` 的 `config()` 里已经有: + +```lua +graphics.declare_vulkan_icd(dir, "share/vulkan/icd.d", tag) +``` + +配合 DISCOVERY 的 `XDG_DATA_DIRS`(Vulkan loader 搜 `$XDG_DATA_DIRS/vulkan/icd.d`), +**生态已经能 hermetic 地提供 RADV 这一条 ICD**。 + +而 `compat.vulkan-runtime` 至今 `deps = {}`,并且照旧从 +`/usr/lib/x86_64-linux-gnu` 一把抓 `libvulkan_*.so` / `libdrm*.so.*` / `libLLVM*.so.*` … +—— **这正是 2026.08.08 之前 `compat.glx-runtime` 的处境**:生态自己有了,包却还在够宿主。 + +`glx-runtime` 当时的修法就是答案:声明 `deps = { runtime = { "xim:graphics" } }`, +从生态栈取,宿主那扇门只留给生态覆盖不到的厂商(它保留 `MCPP_HOST_GL_LIBRARY_PATH` +并在注释里写明代价)。 + +**但要诚实的一点**:生态的 Vulkan 覆盖目前**只有 AMD**(RADV)。`mesa.lua` 自己写着 +「anv(Intel Vulkan)与 NVK 仍然不在这里」。所以 vulkan-runtime **不能**像本方案对 +`compat.libgbm` 那样做到「零 host」,它应当变成 glx-runtime 那个形态: +**生态优先 + 宿主兜底**,而不是现在的**只有宿主**。 + +### 17.3 收益与排序 + +| 候选 | 关掉的 host 边 | 依据 | +|---|---|---| +| **vulkan-runtime 接 `xim:mesa`** | AMD 机器上的 ICD + 其整条传递闭包(libdrm/LLVM/xcb…) | 生态已有 RADV,机制已有 `declare_vulkan_icd` | +| `compat.libdrm` | **无**(见 17.1) | 仅构建期价值 | + +所以下一步是 **vulkan-runtime**,不是 libdrm。这与本方案的主线是同一条: +**先问「生态是不是已经拥有它」,再决定是 vendor、绑定、还是够宿主。** + + +--- + +## 18. 最终状态(2026-08-30,#713 合并之后) + +§16.4 列的那一步已经做完了。 + +### 18.1 包最终长什么样 + +| | 行数 | +|---|---| +| 带 constructor 的形态 | 598 | +| **最终** | **303** | + +删掉的:`gbm_backends.c`(constructor + 两个 helper)、`lib/gbm/` 后端 farm、 +`mesa_libdir()`、`mcpp_gbm.h`,以及为它们辩护的那段注释。 + +剩下的就是一个绑定:`deps = { runtime = { "xim:mesa" } }`、一个把 +`gbm.h` 与 `libgbm.so*` 从 subos view 软链出来的 `install()`,加上 +`include_dirs` / `ldflags` / `runtime.{library_dirs,link_library_dirs}`。 +**它不编译任何上游源码、不带自己的头文件、不设任何环境变量。** + +设 `GBM_BACKENDS_PATH` 从来就是 Mesa 自己的机制、而且是**环境**的职责; +包去做那件事的那个版本是权宜之计,不是设计。 + +### 18.2 验证(对着**已发布**的 artifact,不是本地打补丁的副本) + +`xlings update` 从发布的 artifact 同步索引 → 重装 `xim:mesa` → `config()` 重跑: + +``` +mesa vars: ['LIBGL_DRIVERS_PATH', '__EGL_VENDOR_LIBRARY_DIRS', 'XDG_DATA_DIRS', 'GBM_BACKENDS_PATH'] +/subos/default/usr/lib/gbm/dri_gbm.so +``` + +`mcpp test -p libgbm`(CN 镜像,冷跑):**2 passed**,且 + +``` +backends dir: /subos/default/usr/lib/gbm ← subos 给的,不是包给的 +``` + +### 18.3 测试现在守的是什么 + +两个二进制断言的对象**移到了本仓之外**,这是刻意的: + +| 断言 | 一旦谁坏了会红 | +|---|---| +| `GBM_BACKENDS_PATH` 已设置 | xim-pkgindex 的 DISCOVERY 行被删 / mcpp 停止注入 subos env | +| 它指向的目录里有 `*_gbm.so` | `xim:mesa` 不再把后端放进 subos | +| `GBM_BO_FORMAT_XRGB8888`(值 0)→ `"XR24"` | 头与库来自不同 Mesa | + +所以这个 member 现在是**整条生态链的绊线**,而不只是这个包的自测。 + +### 18.4 顺带修掉的一个更大的问题 + +`index.toml` 的 `min_mcpp = 2026.8.3.3` 是**假的**,而且是本包证明的 —— 详见提交 +`feat(libgbm): the package sheds its workaround; index floor corrected`。 +已上调到 `2026.8.27.2`,同时把「floor 与 CI pin 一起动」这条早已漂移的不变量恢复了。 diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index ff83d7a5..979281b7 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -30,7 +30,8 @@ in the [root README](../README.md#reference-examples). | Header package filling a gap in the index | [`compat.glx-headers`](../pkgs/c/compat.glx-headers.lua) (libglvnd's `GL/glx.h`, absent from the Khronos registry and required by SDL's X11 backend) | | C++ application framework compat (dependencies reuse packages already in the index) | [`compat.eui-neo`](../pkgs/e/compat.eui-neo.lua) (upstream's `3rd/` ships 8 vendored dependencies; none of them is compiled here — all are redirected to the same-version `compat.*` packages in this index) | | Mutually exclusive backends (one of several inside one package) | [`compat.eui-neo`](../pkgs/e/compat.eui-neo.lua): `vulkan` / `sdl2` each **replace** the default OpenGL / GLFW, and the default backend is expressed by *naming no feature at all* — there is no `opengl`/`glfw` feature. A `default` feature cannot express exclusivity: its own `defines`/`sources`/`deps` have no effect whatsoever, while its `implies` always applies and cannot be overridden by a named feature (which is, conversely, exactly the solution for the "always-on interface define" row below). The workable answer is to read the `-DMCPP_FEATURE_` mcpp passes anyway and decide up front in a force-included header. Note also that `cflags` only reaches C TUs — C++ needs `cxxflags`, so a backend define written only into `cflags` never reaches any `.cpp` | -| Host runtime adaptation (drivers are not vendored) | [`compat.glx-runtime`](../pkgs/c/compat.glx-runtime.lua) · [`compat.vulkan-runtime`](../pkgs/c/compat.vulkan-runtime.lua) (mcpp binaries run against a bundled glibc, so a bare-soname `dlopen` never reaches the host drivers; a symlink farm plus `runtime.library_dirs` bridges that. Note the farm holds only versioned sonames — `library_dirs` also joins the link line) | +| Host runtime adaptation (drivers are not vendored) | [`compat.glx-runtime`](../pkgs/c/compat.glx-runtime.lua) · [`compat.vulkan-runtime`](../pkgs/c/compat.vulkan-runtime.lua) (mcpp binaries run against a bundled glibc, so a bare-soname `dlopen` never reaches the host drivers; a symlink farm plus `runtime.library_dirs` bridges that. The farm holds only versioned sonames, so nothing there can shadow an index package. Note that `runtime.library_dirs` renders as `-Wl,-rpath` and **not** as `-L` — the `-L` key is `runtime.link_library_dirs`, which these two do not need because nothing links against their farms; see the row below for one that does) | +| Ecosystem-stack binding (zero host) | [`compat.libgbm`](../pkgs/c/compat.libgbm.lua) (Mesa's GBM — buffer allocation out of a DRM device. The row above reaches the HOST; this one reaches the ECOSYSTEM and nothing else, and the distinction is the whole design. **Why not a source build**: libgbm is a build target *inside* Mesa, not a project — `src/gbm/meson.build` is `link_with: [libloader]`, and `libloader` wants `idep_mesautil`, the whole of Mesa's internal util library (~120 TUs plus Python-generated tables) for exactly one function, `loader_open_driver_lib`. Building it would make this index re-import libdrm + expat + xcb + a Mesa-util carve-out to duplicate what `xim:mesa` has already resolved hermetically. Contrast `compat.vulkan`, which *does* build the Khronos loader from source — Khronos releases that as a standalone project, Mesa releases no such thing for GBM. **Zero host, with no escape hatch**: unlike its two neighbours it has no `/usr/lib*` path and no `MCPP_HOST_*` override, because host libgbm is a leak the ecosystem already closed — `xim:nvidia-gl-host-link` names it directly ("the table … was missing libm, libdrm, **libgbm**, libgcc_s … all of which were therefore coming from the HOST, silently, which is the leak this package exists to close"). NVIDIA's own GBM backend, if ever needed, belongs in that host-link layer rather than here. The measured surface is 1 ecosystem package (`xim:mesa`, not `xim:graphics`'s twenty-two), zero index deps, and zero transitive burden — libgbm.so.1's own RUNPATH resolves entirely inside `xim-x-*`. **What it deliberately does NOT do**: set the backend search path. libgbm is a loader — `gbm_create_device()` dlopens `/_gbm.so`, and Mesa's compiled-in `/usr/lib/gbm` is right on a distro and wrong the moment the payload is relocated. The mechanism to fix that is Mesa's own (`GBM_BACKENDS_PATH`) and the job belongs to the ENVIRONMENT, which is where every other relocated stack puts it — Valve's pressure-vessel answers the identical breakage with `GBM_BACKENDS_PATH=/run/host/usr/lib64/gbm` (steam-runtime#797), Nix and Conda set it at activation. In this ecosystem `xim:mesa` now declares it through the graphics discovery layer (openxlings/xim-pkgindex#713), so this package sets nothing, generates no TU and ships no header of its own. It briefly did carry a constructor that set the variable itself; that was a workaround for the missing declaration, and deleting it took the descriptor from 598 lines to 303. **Two directory keys, not one**: `library_dirs` renders as `-Wl,-rpath` and `link_library_dirs` as `-L`, so a package that is *linked against* (unlike glx-runtime/vulkan-runtime, whose farms are only dlopen'd) needs both — with `library_dirs` alone the farm is complete, the rpath correct, and the build still dies at `ld: cannot find -lgbm`. It ships **two** test binaries: `stock_usage.cpp` includes stock `` and nothing else, which is the minimal consumer and the tripwire on the two things outside this repo the package now depends on — xim-pkgindex's DISCOVERY row and mcpp's subos-env injection · [`compat.libdrm`](../pkgs/c/compat.libdrm.lua) (the layer under GBM — `drmModeAddFB2`/`drmModeSetCrtc` turn an allocated buffer into a scanout. Passes the separable-unit test that libgbm fails — libdrm is an independent freedesktop project and Conan carries a real recipe — but is still a binding for the second reason: `xim:libdrm` exists, Mesa's own payload has DT_NEEDED on it, and two `libdrm.so.2` in one process means two DRM handle tables. **Two include roots**, which is the thing that bites: the public headers sit at the include root and the uapi headers they include sit under `libdrm/`, and `xf86drm.h` line 40 is a bare `#include ` — expose one root and nothing compiles at all) · [`compat.egl`](../pkgs/c/compat.egl.lua) (what makes libgbm useful for RENDERING rather than only allocation: `eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm_device, NULL)`. Provider is libglvnd, not Mesa — EGL is a spec and the thing you link is a vendor-neutral dispatch library that must be the only one in the process. Ships **only** `EGL/` out of a payload that also carries `GL/`, `GLES2/`, `KHR/`: a third provider of `GL/` would make compat.glx-headers' documented two-provider race a three-way one, and `KHR/` comes from the index's existing `compat.khrplatform` instead — load-bearing, since `eglplatform.h` opens with `#include `. X11 is deliberately NOT a dependency: that include is `USE_X11`-gated, and forcing Xorg on headless GBM users would be exactly wrong) · [`compat.wayland`](../pkgs/c/compat.wayland.lua) (client, server, cursor and EGL shim all harvested, but only `-lwayland-client` on `ldflags`. A dependency's ldflags reach the consumer's link line with no way to opt out, so forcing the server library on every client would be unfixable downstream; a compositor author adds `-lwayland-server` themselves and it resolves out of the farm. The test member does exactly that, so the documented escape hatch has a regression guarding it. `wayland-scanner` and the protocol XML are NOT here — that is a code generator plus a data package, the `compat.protobuf` `protoc` shape, and a separate package)) | Always-on interface define | `CURL_STATICLIB` in [`compat.curl`](../pkgs/c/compat.curl.lua): `cflags` is always on but package-private, while a feature's `defines` reaches consumers yet has to be named — `default = { implies = … }` applies unconditionally and happens to give both | | Multiple majors in one package (shape switches with the version) | [`compat.catch2`](../pkgs/c/compat.catch2.lua) (3.x compiles `src/catch2/` into a static library; 2.x goes header-only through `single_include/`) | | External build system (`install()` builds from source) | [`compat.openblas`](../pkgs/c/compat.openblas.lua) (Make) · [`compat.openssl`](../pkgs/c/compat.openssl.lua) (Perl Configure + Make, static libssl/libcrypto) | diff --git a/docs/package-types.md b/docs/package-types.md index 817afc97..84a85c73 100644 --- a/docs/package-types.md +++ b/docs/package-types.md @@ -19,6 +19,7 @@ combined as needed. | **F. Shared-library compat** | has to be the **only** copy of that `.so` in the process (third parties `dlopen` it) | the X11 family such as `pkgs/c/compat.x11.lua`, and `compat.vulkan.lua` (linux) | `targets = { kind = "shared", soname = … }` | | **G. Host runtime adaptation** | things that cannot be vendored, such as drivers — only a symlink farm plus metadata | `pkgs/c/compat.glx-runtime.lua`, `compat.vulkan-runtime.lua` | `runtime.library_dirs` / `capabilities` | | **H. Host tool provider** | the upstream tarball also holds a **code generator** consumers run at build time | `pkgs/c/compat.protobuf.lua` (`protoc`) | a `targets` entry with `kind = "bin"` + `main`, plus `required_features` | +| **I. Ecosystem-stack binding** | the library is an internal build target of a project the **ecosystem already owns**, so vendoring it would fork that project | `pkgs/c/compat.libgbm.lua` (Mesa's GBM, via `xim:mesa`) | `xpm..deps.runtime = { "xim:" }` + a farm from `system.subos_sysrootdir()`, with `runtime.library_dirs` **and** `link_library_dirs` | For the complete sample index, see [Descriptor examples by shape](descriptor-examples.md). @@ -193,10 +194,23 @@ every ICD manifest and yet cannot open a single driver. Two details that keep biting: -- **Put only versioned sonames in the farm** (`lib*.so.*`). `runtime.library_dirs` also joins the **link line**, so a - bare `libxcb.so` shadows this repository's own `compat.xcb` and the link fails with - `undefined reference to XauDisposeAuth` (mcpp#304). Versioned names are invisible to the linker and are exactly what - `dlopen` asks for. +- **Put only versioned sonames in the farm** (`lib*.so.*`). A bare `libxcb.so` shadowed this repository's own + `compat.xcb` and the link failed with `undefined reference to XauDisposeAuth` (mcpp#304). Versioned names are + invisible to the linker and are exactly what `dlopen` asks for, so this remains the rule. +- **Know which directory key produces which flag** — they are not interchangeable, and the split is what makes the + point above version-dependent. Read off the emitted `build.ninja` on mcpp 2026.8.27.2: + + | key | renders as | + |---|---| + | `runtime.library_dirs` | `-Wl,-rpath` | + | `runtime.link_library_dirs` | `-L` | + | `runtime.transitive_needed_dirs` | `-Wl,-rpath-link` | + + So on this pin `library_dirs` alone does **not** put the farm on the link line (the separate `-L` key arrived in + 2026.8.10.3; mcpp#304 predates it). Shape G packages want exactly that — nothing links against their farms, they + exist so a bare-soname `dlopen` resolves at run time. A package that *is* linked against needs + `link_library_dirs` too, or the build dies at `ld: cannot find -l` with a farm that is perfectly correct; + see shape I. - **The closure has to be complete.** A farm holding `libxcb.so.1` but not the `libXau.so.6` it depends on shadows the host copy that would otherwise have resolved, and the executable simply fails to start. diff --git a/docs/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index 7680309e..786e44b9 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -28,7 +28,8 @@ | 补索引空缺的头文件包 | [`compat.glx-headers`](../../pkgs/c/compat.glx-headers.lua)(libglvnd 的 `GL/glx.h`,Khronos registry 不含,SDL 的 X11 后端必需) | | C++ 应用框架 compat(依赖复用索引内既有包) | [`compat.eui-neo`](../../pkgs/e/compat.eui-neo.lua)(上游 `3rd/` 自带 8 个 vendored 依赖,此处一个不编,全部改指索引内同版本 `compat.*`) | | 互斥后端(同包多后端二选一) | [`compat.eui-neo`](../../pkgs/e/compat.eui-neo.lua):`vulkan` / `sdl2` 各自**替换**默认的 OpenGL / GLFW,默认后端由"不点名任何 feature"表达,并不存在 `opengl`/`glfw` feature。`default` feature 表达不了互斥 —— 它自带的 `defines`/`sources`/`deps` 完全不生效,而 `implies` 又恒生效、无法被点名的 feature 覆盖(后者反而正好是本表『恒开的 interface define』一行的解法)。可行解是读 mcpp 本就会传的 `-DMCPP_FEATURE_`,在强制包含头里做前置判定。另注意 `cflags` 只作用于 C TU,C++ 需 `cxxflags` —— 只写进 `cflags` 的后端 define 到不了任何 `.cpp` | -| 宿主运行时适配(不 vendor 驱动) | [`compat.glx-runtime`](../../pkgs/c/compat.glx-runtime.lua) · [`compat.vulkan-runtime`](../../pkgs/c/compat.vulkan-runtime.lua)(mcpp 产物跑在自带 glibc 下,裸 soname 的 `dlopen` 够不到宿主驱动;用符号链接农场 + `runtime.library_dirs` 打通。注意 farm 只放带版本号的 soname —— `library_dirs` 同时进链接行) | +| 宿主运行时适配(不 vendor 驱动) | [`compat.glx-runtime`](../../pkgs/c/compat.glx-runtime.lua) · [`compat.vulkan-runtime`](../../pkgs/c/compat.vulkan-runtime.lua)(mcpp 产物跑在自带 glibc 下,裸 soname 的 `dlopen` 够不到宿主驱动;用符号链接农场 + `runtime.library_dirs` 打通。farm 只放带版本号的 soname,so 里面的东西遮蔽不了索引包。注意 `runtime.library_dirs` 渲染成 `-Wl,-rpath` 而**不是** `-L` —— `-L` 那个键叫 `runtime.link_library_dirs`;这两个包不需要它,因为没人链接它们的 farm,需要的例子见下一行) | +| 生态栈绑定(零 host) | [`compat.libgbm`](../../pkgs/c/compat.libgbm.lua)(Mesa 的 GBM —— 从 DRM 设备分配 buffer。上一行伸手够的是 **host**,这一行够的是**生态**、且仅止于此,这个区别就是整个设计。**为什么不源码构建**:libgbm 是 Mesa **内部**的一个 build target,不是一个项目 —— `src/gbm/meson.build` 写着 `link_with: [libloader]`,而 `libloader` 又要 `idep_mesautil`(Mesa 整个内部 util 库,~120 个 TU 加 Python 生成的表),只为了其中一个函数 `loader_open_driver_lib`。真去建,就得让本索引把 libdrm + expat + xcb + 一块 Mesa-util 重新进口一遍,去复刻 `xim:mesa` 已经 hermetic 解析好的东西。对照 `compat.vulkan` —— 它**确实**从源码建 Khronos loader,因为 Khronos 把 loader 作为独立项目发布,而 Mesa 对 GBM 没有对应物。**零 host,且不留后门**:与相邻两个包不同,它没有 `/usr/lib*` 路径、也没有 `MCPP_HOST_*` 覆盖项 —— 因为 host libgbm 是生态**已经堵上**的泄漏,`xim:nvidia-gl-host-link` 里直接点了名("the table … was missing libm, libdrm, **libgbm**, libgcc_s … all of which were therefore coming from the HOST, silently, which is the leak this package exists to close")。NVIDIA 自家的 GBM 后端若将来需要,归那个 host-link 层管,不归这里。实测依赖面:生态 1 个包(`xim:mesa`,不是 `xim:graphics` 的 22 个)、索引依赖 0、传递负担 0 —— libgbm.so.1 自己的 RUNPATH 全落在 `xim-x-*` 内。**它刻意不做的事**:设置后端搜索路径。libgbm 是个 loader —— `gbm_create_device()` 会 dlopen `/_gbm.so`,而 Mesa 编译进去的 `/usr/lib/gbm` 在发行版上是对的、payload 一被重定位就是错的。修它的机制是 **Mesa 自己的**(`GBM_BACKENDS_PATH`),而这件事归**环境**管 —— 所有被重定位的栈都是这么做的:Valve 的 pressure-vessel 用 `GBM_BACKENDS_PATH=/run/host/usr/lib64/gbm` 解决同一个 bug(steam-runtime#797),Nix/Conda 在环境激活时设。在本生态里,`xim:mesa` 现在通过 graphics discovery 层声明它(openxlings/xim-pkgindex#713),所以这个包**什么都不设**、不生成 TU、也不带自己的头文件。它一度自带一个 constructor 去设这个变量,那是声明缺失时的权宜之计;删掉它让描述符从 598 行降到 303 行。**两个目录键都要**:`library_dirs` 渲染成 `-Wl,-rpath`、`link_library_dirs` 渲染成 `-L`,所以一个**会被链接**的包(不像 glx-runtime/vulkan-runtime 的农场只被 dlopen)两个都得写 —— 只写 `library_dirs` 的话,农场完整、rpath 正确,构建照样死在 `ld: cannot find -lgbm`。它带**两个**测试二进制:`stock_usage.cpp` 只 include 原样的 ``,是最小消费者,也是本仓之外那两件依赖的绊线 —— xim-pkgindex 的 DISCOVERY 行,以及 mcpp 的 subos-env 注入 · [`compat.libdrm`](../../pkgs/c/compat.libdrm.lua)(GBM 下面那一层 —— `drmModeAddFB2`/`drmModeSetCrtc` 把分配出来的 buffer 变成能扫描输出的画面。它**通过**了 libgbm 没通过的「可独立分发」判据(libdrm 是独立的 freedesktop 项目,Conan 也有真配方),但仍然做成绑定,理由是第二条:`xim:libdrm` 已存在、Mesa 自己的 payload 对它有 DT_NEEDED,一个进程里两份 `libdrm.so.2` 等于两张 DRM 句柄表。**两个 include 根**是真正会咬人的地方:公开头在 include 根、它们 include 的 uapi 头在 `libdrm/` 子目录,而 `xf86drm.h` 第 40 行就是裸的 `#include ` —— 只暴露一个根的话什么都编不过) · [`compat.egl`](../../pkgs/c/compat.egl.lua)(让 libgbm 从「只能分配」变成「能渲染」的那一环:`eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm_device, NULL)`。提供方是 libglvnd 而不是 Mesa —— EGL 是规范,真正链接的是厂商中立的 dispatch 库,而它必须是进程里唯一的一份。payload 里还有 `GL/`、`GLES2/`、`KHR/`,但本包**只**暴露 `EGL/`:再出一个 `GL/` 提供方会把 compat.glx-headers 记录过的「两个提供方」竞争变成三方;`KHR/` 则交给索引已有的 `compat.khrplatform` —— 这条依赖是必需的,因为 `eglplatform.h` 开头就 `#include `。X11 **刻意不**作为依赖:那个 include 被 `USE_X11` 门控,而给无显示的 headless GBM 用户强塞 Xorg 恰恰是错的) · [`compat.wayland`](../../pkgs/c/compat.wayland.lua)(client/server/cursor/egl 四个库全部收进 farm,但 `ldflags` 只放 `-lwayland-client`。依赖的 ldflags 会进消费者的链接行且无法退出,所以给每个客户端强塞 server 库是下游修不掉的;compositor 作者自己加 `-lwayland-server`,从 farm 里解析得到。测试成员就是这么写的,于是这条被文档化的退路有回归守着。`wayland-scanner` 与协议 XML **不在这里** —— 那是代码生成器加数据包,属于 `compat.protobuf` 的 `protoc` 形态,应当是另一个包)) | 恒开的 interface define | [`compat.curl`](../../pkgs/c/compat.curl.lua) 的 `CURL_STATICLIB`:`cflags` 恒开但包私有,feature `defines` 可达消费端但需点名 —— `default = { implies = … }` 无条件生效,恰好两者兼得 | | 单包多 major(形态随版本切换) | [`compat.catch2`](../../pkgs/c/compat.catch2.lua)(3.x 编 `src/catch2/` 出静态库;2.x 走 `single_include/` header-only) | | 外部构建系统(`install()` 从源码构建) | [`compat.openblas`](../../pkgs/c/compat.openblas.lua)(Make) · [`compat.openssl`](../../pkgs/c/compat.openssl.lua)(Perl Configure + Make,静态 libssl/libcrypto) | diff --git a/docs/zh/package-types.md b/docs/zh/package-types.md index 1aa0f80d..4133184c 100644 --- a/docs/zh/package-types.md +++ b/docs/zh/package-types.md @@ -17,6 +17,7 @@ A–D 是四种**基础**形态,先按它们判定;E–G 是在基础形态之 | **F. 共享库 compat** | 必须是**唯一**的那个 `.so`(会被第三方 `dlopen`) | `pkgs/c/compat.x11.lua` 等 X11 家族、`compat.vulkan.lua`(linux) | `targets = { kind = "shared", soname = … }` | | **G. 宿主运行时适配** | 驱动之类无法 vendor 的东西,只做符号链接农场 + 元数据 | `pkgs/c/compat.glx-runtime.lua`、`compat.vulkan-runtime.lua` | `runtime.library_dirs` / `capabilities` | | **H. 宿主工具提供方** | 上游 tarball 里除了库,还带着消费者在构建期要跑的**代码生成器** | `pkgs/c/compat.protobuf.lua`(`protoc`) | `targets` 里一条 `kind = "bin"` + `main`,配 `required_features` | +| **I. 生态栈绑定** | 这个库是某个**生态已经拥有**的项目的内部 build target,vendor 它就等于 fork 那个项目 | `pkgs/c/compat.libgbm.lua`(Mesa 的 GBM,经 `xim:mesa`) | `xpm..deps.runtime = { "xim:" }` + 从 `system.subos_sysrootdir()` 建农场,`runtime.library_dirs` **和** `link_library_dirs` 都要 | 完整的样例索引见[描述符示例总览(按形态)](descriptor-examples.md)。 @@ -179,9 +180,22 @@ runtime = { 两个反复踩到的细节: -- **农场里只放带版本号的 soname**(`lib*.so.*`)。`runtime.library_dirs` 同时进**链接行**,一个裸 `libxcb.so` - 会遮蔽本仓自己的 `compat.xcb`,链接报 `undefined reference to XauDisposeAuth`(mcpp#304)。带版本号的名字对 - 链接器不可见,而恰好是 `dlopen` 要的。 +- **农场里只放带版本号的 soname**(`lib*.so.*`)。曾经一个裸 `libxcb.so` 遮蔽了本仓自己的 `compat.xcb`, + 链接报 `undefined reference to XauDisposeAuth`(mcpp#304)。带版本号的名字对链接器不可见,而恰好是 + `dlopen` 要的,所以这条规矩继续保留。 +- **要分清哪个目录键出哪个 flag** —— 三者不可互换,而正是这个拆分让上一条变得与版本有关。 + 在 mcpp 2026.8.27.2 上从生成的 `build.ninja` 读出来: + + | 键 | 渲染成 | + |---|---| + | `runtime.library_dirs` | `-Wl,-rpath` | + | `runtime.link_library_dirs` | `-L` | + | `runtime.transitive_needed_dirs` | `-Wl,-rpath-link` | + + 也就是说在这个 pin 上,只写 `library_dirs` **不会**把农场放进链接行(单独的 `-L` 键是 2026.8.10.3 才有的, + mcpp#304 早于它)。形态 G 的包要的正是这个效果 —— 没人链接它们的农场,它们存在的意义是让裸 soname 的 + `dlopen` 在**运行期**解析得到。而一个**会被链接**的包还需要 `link_library_dirs`,否则农场完全正确、 + 构建却死在 `ld: cannot find -l`;见形态 I。 - **闭包必须完整**。农场里有 `libxcb.so.1` 却没有它依赖的 `libXau.so.6`,会遮蔽掉本来能解析的宿主副本,可执行 文件直接起不来。 diff --git a/index.toml b/index.toml index 2793f7ff..2a1fc559 100644 --- a/index.toml +++ b/index.toml @@ -5,7 +5,40 @@ # (E0006 on violation). Bump min_mcpp ONLY together with the CI MCPP_VERSION # pin — lint parses descriptors with the pinned mcpp, which enforces the # "floor first, new grammar after" rollout rule mechanically. +# +# ── 2026-08-30: raised 2026.8.3.3 -> 2026.8.27.2, with the CI pin ────────── +# +# The rule above says to move this only together with MCPP_VERSION, and that is +# what this is: validate.yml has pinned 2026.8.27.2 for some time while this +# contract still claimed 2026.8.3.3, so the two had drifted apart and the floor +# was advertising support nobody tested. +# +# It was not merely untested, it was FALSE, and measurably so. Two independent +# reasons, both reproduced against this index: +# +# * 2026.8.3.3 silently ignores `runtime.link_library_dirs` (the key does not +# exist in that binary at all) yet still reports `parse OK`, so +# `compat.libgbm` resolves, builds, and then dies at link: +# libdrm.so.2, needed by .../libgbm.so, not found +# undefined reference to `drmGetVersion' +# A silently-ignored key is invisible to the lint, which is why the +# "floor first, new grammar after" guard did not catch this: the guard +# assumes unknown keys are REJECTED, and `runtime.*` subkeys are not. +# +# * 2026.8.10.3 does not get that far. It cannot bootstrap against this index +# at all: +# error: selected RuntimeBinding glibc@2.44 requires payload +# '/xim-x-glibc/2.44', but it is not installed +# which is the compiled-in-runtime-binding defect mcpp's own +# src/xlings/xlings.cppm documents as the reason it floors xlings at +# 2026.8.27.2. Every client below that line is already broken here, +# for reasons that predate any package in this index. +# +# So E0006 is strictly better than what those clients get today: a clear +# refusal at the index-open choke point instead of an undefined reference or a +# missing payload three layers down. 2026.8.27.2 is the version this index is +# actually tested against, and now the contract says so. [index] spec = "1" -min_mcpp = "2026.8.3.3" -latest_mcpp = "2026.8.3.3" +min_mcpp = "2026.8.27.2" +latest_mcpp = "2026.8.27.2" diff --git a/mcpp.toml b/mcpp.toml index 2156b06c..125581ec 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -81,6 +81,10 @@ members = [ "tests/examples/freetype", "tests/examples/glad", "tests/examples/libaio", + "tests/examples/libdrm", + "tests/examples/wayland", + "tests/examples/egl", + "tests/examples/libgbm", "tests/examples/libpng", "tests/examples/libwebp", "tests/examples/llamacpp", diff --git a/pkgs/c/compat.egl.lua b/pkgs/c/compat.egl.lua new file mode 100644 index 00000000..82a30645 --- /dev/null +++ b/pkgs/c/compat.egl.lua @@ -0,0 +1,207 @@ +-- compat.egl — EGL 1.5, the window-system binding layer: `eglGetPlatformDisplay`, +-- `eglCreateContext`, `eglCreateWindowSurface`, `eglMakeCurrent`, and the +-- `eglCreateImage` / dmabuf import path. +-- +-- It is the piece that makes compat.libgbm useful for RENDERING rather than +-- only for allocation. The canonical headless-GPU sequence is +-- +-- int fd = open("/dev/dri/renderD128", O_RDWR); +-- struct gbm_device *gbm = gbm_create_device(fd); // compat.libgbm +-- EGLDisplay dpy = eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm, NULL); +-- +-- and without EGL the first two lines have nowhere to go. Together with +-- compat.libdrm (which turns the resulting buffer into a scanout via +-- `drmModeAddFB2`) these three are the whole KMS/DRM stack. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: a binding, and the provider is libglvnd rather than Mesa +-- +-- EGL is a Khronos SPECIFICATION; the thing you link is a vendor-neutral +-- dispatch library, and on Linux that is libglvnd's `libEGL.so.1`, which +-- dlopens the actual vendor implementation. `xim:libglvnd` already ships it — +-- and it MUST be the only one in the process, because glvnd's whole job is to +-- be the single dispatch point. Building a second `libEGL.so.1` here would be +-- the `compat.vulkan-runtime` mistake ("one loader per process is the whole +-- point") applied to EGL. +-- +-- Measured surface: +-- +-- host 0 no /usr/lib* path, no escape-hatch variable +-- ecosystem 1 `xim:libglvnd` +-- index 1 `compat.khrplatform` — see below, it is not optional +-- transitive 0 libEGL.so.1 resolves inside xim-x-{libglvnd,glibc} +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHAT IS DELIBERATELY NOT SHIPPED +-- +-- libglvnd's include tree carries `EGL/`, `GL/`, `GLES2/`, `GLES3/`, `KHR/` +-- and `glvnd/`. This package exposes ONLY `EGL/`. +-- +-- `GL/` is the reason. `compat.opengl` and `compat.glx-headers` already +-- provide it, and compat.glx-headers' own comment records the consequence of +-- two providers: "Depend on ONE of the two, not both, or the winner depends on +-- include-dir order." Shipping a third `GL/` would make that a three-way race +-- for every consumer that wants EGL and GL together — which is most of them. +-- +-- `KHR/` is left out for the same reason and solved properly instead: +-- `EGL/eglplatform.h` opens with `#include `, so the header +-- genuinely needs it, and `compat.khrplatform` is the index's existing +-- provider (from the Khronos EGL-Registry, the same upstream). Hence the one +-- index dependency, and it is load-bearing rather than decorative: without it +-- `#include ` does not parse. +-- +-- X11 IS NOT A DEPENDENCY, and that is worth stating because it usually is. +-- `eglplatform.h` reaches for `` only under `#elif defined(USE_X11)` +-- (line 106 of the shipped header), so the default path needs nothing from +-- Xorg. A consumer that defines `USE_X11` must add `compat.x11` and +-- `compat.xorgproto` itself — this package cannot know, and forcing the X11 +-- stack on every EGL user (including the GBM/headless ones, who have no +-- display at all) would be exactly wrong. +package = { + spec = "1", + namespace = "compat", + name = "egl", + description = "EGL 1.5 window-system binding (libglvnd dispatch), bound to the ecosystem's xim:libglvnd", + licenses = {"MIT"}, + repo = "https://github.com/NVIDIA/libglvnd", + type = "package", + + xpm = { + linux = { + deps = { runtime = { "xim:libglvnd" } }, + ["2026.08.30"] = { + -- Inert anchor; nothing downloaded is read. See + -- compat.libgbm for why this is a README and not a header. + url = { + GLOBAL = "https://raw.githubusercontent.com/NVIDIA/libglvnd/v1.7.0/README.md", + CN = "https://gitcode.com/mcpp-res/egl/releases/download/2026.08.30/egl-2026.08.30.md", + }, + sha256 = "f84a3eca98cc5bdf5318741124c38c5e877f856df8c7e229ee5065e5c61038c2", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + include_dirs = { "mcpp_generated/egl/include" }, + + generated_files = { + ["mcpp_generated/egl_anchor.c"] = + "int mcpp_compat_egl_anchor(void) { return 0; }\n", + }, + sources = { "mcpp_generated/egl_anchor.c" }, + + -- NOT named `egl`: a target called `egl` would put a `libegl.a` beside + -- the real `libEGL.so` and let search order decide. Same rule as + -- compat.libgbm's `gbm_binding`. + targets = { ["egl_binding"] = { kind = "lib" } }, + + ldflags = { "-lEGL" }, + + -- Load-bearing: EGL/eglplatform.h includes . + deps = { + ["compat.khrplatform"] = "2026.05.31", + }, + + runtime = { + -- Both keys: `library_dirs` renders as -Wl,-rpath and + -- `link_library_dirs` as -L, and this package IS linked against. + library_dirs = { "mcpp_generated/egl/lib" }, + link_library_dirs = { "mcpp_generated/egl/lib" }, + provides = { "egl.dispatch" }, + }, + }, +} + +import("xim.libxpkg.pkginfo") +import("xim.libxpkg.system") +import("xim.libxpkg.log") + +local log_path = nil + +local function say(msg) + if log_path == nil then return end + local prev = io.readfile(log_path) or "" + io.writefile(log_path, prev .. msg .. "\n") +end + +local function fail(msg) + say("FAILED: " .. msg) + log.error("[egl] %s", msg) + return false +end + +local function sh_quote(value) + return "'" .. tostring(value):gsub("'", "'\\''") .. "'" +end + +local function link_matching(srcdir, pattern, outdir) + os.exec( + "for f in " .. sh_quote(srcdir) .. "/" .. pattern .. + "; do [ -e \"$f\" ] || continue; " .. + "ln -sf \"$f\" " .. sh_quote(outdir) .. "/\"$(basename \"$f\")\"; " .. + "done" + ) +end + +function install() + local prefix = pkginfo.install_dir() + os.mkdir(prefix) + + log_path = path.join(prefix, "mcpp_egl_build.log") + io.writefile(log_path, "compat.egl install()\n") + + local view = system.subos_sysrootdir() + say("subos view: " .. tostring(view)) + + local view_lib = path.join(view, "lib") + local view_inc = path.join(view, "usr", "include") + + local root = path.join(prefix, "mcpp_generated", "egl") + local out_lib = path.join(root, "lib") + local out_inc = path.join(root, "include") + local out_egl = path.join(out_inc, "EGL") + + os.mkdir(out_lib) + os.mkdir(out_inc) + os.mkdir(out_egl) + + -- 1. The dispatch library, from the subos view and nowhere else. + say("linking libEGL.so* from " .. view_lib) + link_matching(view_lib, "libEGL.so*", out_lib) + + for _, required in ipairs({"libEGL.so", "libEGL.so.1"}) do + if not os.isfile(path.join(out_lib, required)) then + return fail(required .. " is not in this subos. libEGL comes from " + .. "`xim:libglvnd`, which this package declares as a " + .. "runtime dependency; if it is declared and this " + .. "still fires, that install did not finish") + end + end + say("libEGL.so and libEGL.so.1 present") + + for _, bad in ipairs({"libc.so.6", "libm.so.6", "ld-linux-x86-64.so.2"}) do + if os.isfile(path.join(out_lib, bad)) then + return fail(bad .. " was linked into the EGL farm; it would reach " + .. "every consumer's RUNPATH and pair a second libc " + .. "with mcpp's loader") + end + end + + -- 2. ONLY the EGL headers. GL/, GLES2/, GLES3/ and KHR/ stay behind — see + -- the header comment for why a second provider of any of them is a bug + -- rather than a convenience. + say("linking EGL/*.h from " .. path.join(view_inc, "EGL")) + link_matching(path.join(view_inc, "EGL"), "*.h", out_egl) + if not os.isfile(path.join(out_egl, "egl.h")) then + return fail("EGL/egl.h is not in this subos (expected " + .. path.join(view_inc, "EGL", "egl.h") .. ")") + end + say("EGL headers present") + + say("done") + return true +end diff --git a/pkgs/c/compat.libdrm.lua b/pkgs/c/compat.libdrm.lua new file mode 100644 index 00000000..4922b614 --- /dev/null +++ b/pkgs/c/compat.libdrm.lua @@ -0,0 +1,241 @@ +-- compat.libdrm — libdrm, the userspace wrapper over the kernel's DRM ioctls: +-- `drmOpen`/`drmGetVersion`, the whole `drmMode*` KMS family (connectors, CRTCs, +-- framebuffers, page flips), PRIME import/export, and the `drm.h` / +-- `drm_mode.h` / `drm_fourcc.h` uapi headers. +-- +-- It is the layer directly under compat.libgbm: GBM allocates a buffer, and +-- libdrm is what turns that buffer into something a display controller +-- scans out (`drmModeAddFB2` + `drmModeSetCrtc`). Without it a consumer can +-- allocate and never present. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: the compat.libgbm binding, and the criterion is the same +-- +-- The index's rule is "build it from source", and the question is always +-- whether upstream ships the thing as a separable unit. libdrm PASSES that +-- test — it is an independent freedesktop project with its own releases, and +-- Conan carries it as a real recipe rather than a `system` virtual package. +-- So a source build would be legitimate here, unlike compat.libgbm where the +-- library is a target inside Mesa. +-- +-- It is nevertheless a BINDING, for the second criterion rather than the +-- first: `xim:libdrm` already exists, mesa depends on it, and it is already +-- installed in any subos that has a graphics stack. Building a second copy +-- would put two `libdrm.so.2` in reach of one process — and this is the one +-- library where that matters most, because Mesa's own payload +-- (`libgbm.so.1`, `libgallium`, the Vulkan ICDs) has DT_NEEDED on the +-- ecosystem's copy. A consumer linking ours while Mesa loads the ecosystem's +-- would get two DRM handle tables in one address space. +-- +-- Measured surface, the same four axes compat.libgbm reports: +-- +-- host 0 no /usr/lib* path, no escape-hatch variable +-- ecosystem 1 `xim:libdrm` — not `xim:mesa`, which would drag the +-- whole GL stack in for a consumer that only wants ioctls +-- index 0 `deps = {}` +-- transitive 0 libdrm.so.2 needs only libc/libm, both from the payload +-- +-- ───────────────────────────────────────────────────────────────────────── +-- TWO INCLUDE ROOTS, AND THIS IS THE ONE THING THAT BITES +-- +-- libdrm installs its public headers at the include ROOT (`xf86drm.h`, +-- `xf86drmMode.h`, `libsync.h`) but the uapi headers they include in a +-- `libdrm/` SUBDIRECTORY (`drm.h`, `drm_mode.h`, `drm_fourcc.h`, …). And +-- `xf86drm.h` line 40 is a bare `#include `. +-- +-- So one include root is not enough. Measured while writing compat.libgbm's +-- test, which tried exactly that: +-- +-- xf86drm.h:40:10: fatal error: drm.h: No such file or directory +-- +-- Upstream's own `libdrm.pc` says `Cflags: -I${includedir}/libdrm`, and the +-- root is on the path by default, so a pkg-config consumer gets both. This +-- package therefore exposes BOTH directories — the root for `` +-- and `libdrm/` for the `` it pulls in. +-- +-- The vendor libraries (`libdrm_amdgpu`, `libdrm_intel`, `libdrm_nouveau`, +-- `libdrm_radeon`) are deliberately NOT harvested. They are separate `-l` +-- names with their own headers, only meaningful to code targeting one GPU +-- family, and nothing in the generic KMS path touches them. Adding them would +-- put four more sonames on every consumer's link line for no one's benefit. +package = { + spec = "1", + namespace = "compat", + name = "libdrm", + description = "libdrm — userspace DRM/KMS ioctl wrapper, bound to the ecosystem's xim:libdrm", + licenses = {"MIT"}, + repo = "https://gitlab.freedesktop.org/mesa/drm", + type = "package", + + xpm = { + linux = { + -- PLATFORM level, beside the version entries rather than inside + -- one: compat.glx-runtime established that a per-version `deps` + -- parses fine and never installs. + deps = { runtime = { "xim:libdrm" } }, + ["2026.08.30"] = { + -- Inert anchor. Nothing downloaded here is read — the payload + -- is what install() links out of the subos view. The xpm schema + -- wants a url + sha256 per version, and a README cannot be + -- mistaken for a shipped header (compat.libgbm learned that one + -- the hard way by anchoring on a `.h`). + url = { + GLOBAL = "https://gitlab.freedesktop.org/mesa/drm/-/raw/libdrm-2.4.123/README.rst", + CN = "https://gitcode.com/mcpp-res/libdrm/releases/download/2026.08.30/libdrm-2026.08.30.rst", + }, + sha256 = "46183785b2f012d0773646d1974374cbfc754f043d1a423afb0ffea0af2569c1", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + -- Both roots, for the reason in the header comment: `` from + -- the first, the `` it includes from the second. + include_dirs = { + "mcpp_generated/libdrm/include", + "mcpp_generated/libdrm/include/libdrm", + }, + + generated_files = { + ["mcpp_generated/libdrm_anchor.c"] = + "int mcpp_compat_libdrm_anchor(void) { return 0; }\n", + }, + sources = { "mcpp_generated/libdrm_anchor.c" }, + + -- NOT named `drm`: a target called `drm` would put a `libdrm.a` on the + -- link line beside the real `libdrm.so`, and which one `-ldrm` picks + -- would come down to search order. Same rule as compat.libgbm's + -- `gbm_binding`. + targets = { ["drm_binding"] = { kind = "lib" } }, + + ldflags = { "-ldrm" }, + deps = {}, + + runtime = { + -- Two keys, two flags, not interchangeable: `library_dirs` renders + -- as `-Wl,-rpath` and `link_library_dirs` as `-L`. A package that + -- is LINKED against needs both — with only the first, the farm is + -- complete, the rpath correct, and the build dies at + -- `ld: cannot find -ldrm`. (compat.glx-runtime and + -- compat.vulkan-runtime declare only `library_dirs` because + -- nothing links against their farms.) + library_dirs = { "mcpp_generated/libdrm/lib" }, + link_library_dirs = { "mcpp_generated/libdrm/lib" }, + provides = { "drm.libdrm" }, + }, + }, +} + +import("xim.libxpkg.pkginfo") +import("xim.libxpkg.system") +import("xim.libxpkg.log") + +-- install() is a blind spot by default: log.error does not reach the CI log +-- and a call outside the sandbox's xmake-API subset kills the hook silently. +-- So the log comes first and every step announces itself. validate.yml's +-- failure step collects `mcpp_*_build.log`, which is what this name matches. +local log_path = nil + +local function say(msg) + if log_path == nil then return end + local prev = io.readfile(log_path) or "" + io.writefile(log_path, prev .. msg .. "\n") +end + +local function fail(msg) + say("FAILED: " .. msg) + log.error("[libdrm] %s", msg) + return false +end + +local function sh_quote(value) + return "'" .. tostring(value):gsub("'", "'\\''") .. "'" +end + +local function link_matching(srcdir, pattern, outdir) + os.exec( + "for f in " .. sh_quote(srcdir) .. "/" .. pattern .. + "; do [ -e \"$f\" ] || continue; " .. + "ln -sf \"$f\" " .. sh_quote(outdir) .. "/\"$(basename \"$f\")\"; " .. + "done" + ) +end + +function install() + local prefix = pkginfo.install_dir() + os.mkdir(prefix) + + log_path = path.join(prefix, "mcpp_libdrm_build.log") + io.writefile(log_path, "compat.libdrm install()\n") + + local view = system.subos_sysrootdir() + say("subos view: " .. tostring(view)) + + local view_lib = path.join(view, "lib") + local view_inc = path.join(view, "usr", "include") + + local root = path.join(prefix, "mcpp_generated", "libdrm") + local out_lib = path.join(root, "lib") + local out_inc = path.join(root, "include") + local out_uapi = path.join(out_inc, "libdrm") + + os.mkdir(out_lib) + os.mkdir(out_inc) + os.mkdir(out_uapi) + + -- 1. The library. From the subos view and nowhere else. `libdrm.so*` only: + -- the vendor variants are separate sonames nobody on the generic KMS + -- path links, and the glob is anchored so `libdrm_amdgpu.so` cannot + -- match it. + say("linking libdrm.so* from " .. view_lib) + link_matching(view_lib, "libdrm.so*", out_lib) + + for _, required in ipairs({"libdrm.so", "libdrm.so.2"}) do + if not os.isfile(path.join(out_lib, required)) then + return fail(required .. " is not in this subos. libdrm comes from " + .. "`xim:libdrm`, which this package declares as a " + .. "runtime dependency; if it is declared and this " + .. "still fires, that install did not finish") + end + end + say("libdrm.so and libdrm.so.2 present") + + -- The glob cannot match a C runtime, but compat.glx-runtime's rule is to + -- ASSERT rather than trust: a stray libc here faults inside the dynamic + -- linker before main with no output at all. + for _, bad in ipairs({"libc.so.6", "libm.so.6", "ld-linux-x86-64.so.2"}) do + if os.isfile(path.join(out_lib, bad)) then + return fail(bad .. " was linked into the libdrm farm; it would " + .. "reach every consumer's RUNPATH and pair a second " + .. "libc with mcpp's loader") + end + end + + -- 2. The public headers, at the root where upstream installs them. + say("linking public headers from " .. view_inc) + for _, h in ipairs({"xf86drm.h", "xf86drmMode.h", "libsync.h"}) do + link_matching(view_inc, h, out_inc) + end + if not os.isfile(path.join(out_inc, "xf86drm.h")) then + return fail("xf86drm.h is not in this subos (expected " + .. path.join(view_inc, "xf86drm.h") .. ")") + end + + -- 3. The uapi headers, in the `libdrm/` subdirectory the public ones + -- include from. Without this, `` parses down to line 40 and + -- fails on `#include `. + say("linking uapi headers from " .. path.join(view_inc, "libdrm")) + link_matching(path.join(view_inc, "libdrm"), "*.h", out_uapi) + if not os.isfile(path.join(out_uapi, "drm.h")) then + return fail("libdrm/drm.h is not in this subos; would fail " + .. "to parse at its own `#include `") + end + say("headers present") + + say("done") + return true +end diff --git a/pkgs/c/compat.libgbm.lua b/pkgs/c/compat.libgbm.lua new file mode 100644 index 00000000..32d294ca --- /dev/null +++ b/pkgs/c/compat.libgbm.lua @@ -0,0 +1,303 @@ +-- compat.libgbm — GBM (Generic Buffer Management), the buffer-allocation API a +-- program uses to get scanout-capable buffers out of a DRM device: gbm_device, +-- gbm_bo, gbm_surface. It is what sits under EGL on a KMS console, under a +-- Wayland compositor's back end, and under headless GPU rendering with no X +-- server anywhere. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY THIS IS A BINDING AND NOT A SOURCE BUILD +-- +-- The rule this index otherwise follows is "build it from source". Two facts +-- put libgbm on the other side of that line. +-- +-- 1. UPSTREAM DOES NOT SHIP IT AS A SEPARABLE UNIT. libgbm is a build target +-- inside Mesa, not a project. `src/gbm/meson.build` is +-- +-- link_with : [libloader] +-- dependencies : [dep_libdrm, idep_xmlconfig] +-- +-- and `libloader` in turn wants `idep_mesautil` — the whole of Mesa's +-- internal util library, ~120 TUs plus Python-generated tables — for +-- exactly ONE function, `loader_open_driver_lib`. Add `-DUSE_DRICONF` +-- (expat), libdrm, xcb and xcb-randr. The GBM frontend/backend dlopen split +-- exists so VENDORS CAN SHIP BACKENDS; it was never an invitation to +-- rebuild the frontend. +-- +-- Contrast `compat.vulkan`, which DOES build the Khronos Vulkan-Loader from +-- source: Khronos releases the loader as a standalone project whose entire +-- purpose is to ship separately from any driver. Mesa releases no such +-- thing for GBM. Conan reaches the same conclusion by not carrying a gbm +-- recipe at all, while carrying `libdrm` and `libglvnd` as real ones. +-- +-- 2. IN THIS ECOSYSTEM, MESA ALREADY HAS AN OWNER: `xim:mesa`. A source build +-- would make mcpp-index re-import libdrm + expat + xcb + a Mesa-util +-- carve-out to duplicate a dependency graph the ecosystem has already +-- resolved hermetically, and would put a second `libgbm.so.1` in processes +-- that already have one. +-- +-- The measured surface of the binding: +-- +-- host 0 no /usr/lib* path, and no escape-hatch variable +-- ecosystem 1 `xim:mesa`, not `xim:graphics`'s twenty-two +-- index 0 `deps = {}`; gbm.h includes only / +-- transitive 0 libgbm.so.1's own RUNPATH resolves entirely inside +-- xim-x-{mesa,libdrm,expat,libllvm,glibc,…} +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ZERO HOST. NOT "HOST, CONVERGED" — ZERO. +-- +-- This package looks at `system.subos_sysrootdir()` and nowhere else. No +-- `/usr/lib*` candidate directory and, deliberately, NO escape-hatch +-- environment variable — a stricter rule than either neighbour, since +-- `compat.glx-runtime` keeps `MCPP_HOST_GL_LIBRARY_PATH` and +-- `compat.vulkan-runtime` harvests /usr/lib/x86_64-linux-gnu outright. +-- +-- Those two have a reason this one does not: a PROPRIETARY VENDOR DRIVER can +-- only come from the host. GBM has no such case — `xim:mesa` covers every host +-- shape the graphics stack covers. And host libgbm is a leak this ecosystem +-- has already CLOSED; `xim:nvidia-gl-host-link` records it by name: +-- +-- "The table … was missing libm, libdrm, libgbm, libgcc_s and +-- libwayland-* -- all of which were therefore coming from the HOST, +-- silently, which is the leak this package exists to close (R7)." +-- +-- If a machine ever needs NVIDIA's own GBM backend, that belongs in +-- `xim:nvidia-gl-host-link`, the layer that owns host contact. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHAT THIS PACKAGE DOES NOT DO: THE BACKEND PATH +-- +-- Worth stating, because this package used to do it and no longer needs to. +-- +-- libgbm is a LOADER: `gbm_create_device()` dlopens `/_gbm.so`, +-- where comes from `GBM_BACKENDS_PATH` or, failing that, the +-- `DEFAULT_BACKENDS_PATH` compiled into the library. Mesa is built +-- `--prefix=/usr`, so that compiled-in path is `/usr/lib/gbm` — correct on a +-- distro, where the backends really are there, and wrong the moment the +-- payload is relocated: +-- +-- MESA-LOADER: failed to open dri: /usr/lib/gbm/dri_gbm.so: cannot open +-- shared object file (search paths /usr/lib/gbm, suffix _gbm) +-- +-- The mechanism to fix that is MESA'S OWN and needs nothing invented here: set +-- `GBM_BACKENDS_PATH`. Every relocated stack does exactly that — Valve's +-- pressure-vessel answers the identical breakage with +-- GBM_BACKENDS_PATH=/run/host/usr/lib64/gbm (steam-runtime#797), and Nix and +-- Conda set it at environment-activation time. +-- +-- In this ecosystem that job belongs to the environment too, and now holds it: +-- `xim:mesa` places its backends into the subos and declares the variable +-- through the graphics discovery layer (openxlings/xim-pkgindex#713), so every +-- consumer inherits it. So this package sets nothing, generates no TU, ships +-- no header of its own, and exposes stock `gbm.h` — which is what a libgbm +-- package should look like. +package = { + spec = "1", + namespace = "compat", + name = "libgbm", + description = "GBM buffer management API (Mesa), bound to the ecosystem's xim:mesa — zero host dependency", + licenses = {"MIT"}, + repo = "https://gitlab.freedesktop.org/mesa/mesa", + type = "package", + + xpm = { + linux = { + -- PLATFORM level, beside the version entries rather than inside + -- one. compat.glx-runtime paid a CI cycle to establish this: a + -- per-version `deps` leaves the descriptor parsing fine and the + -- dependency simply never installed, and the error names the + -- missing library rather than the misplaced key. + -- + -- `xim:mesa` and not `xim:graphics`: this package needs Mesa, not + -- the GL dispatch and X11 halves of the twenty-two-package stack. + deps = { runtime = { "xim:mesa" } }, + ["2026.08.29"] = { + -- NOTHING DOWNLOADED HERE IS EVER READ, and the file is chosen + -- so that cannot be misread. + -- + -- Both halves of this package's payload come from the SUBOS: + -- install() symlinks `gbm.h` out of `/usr/include` and + -- `libgbm.so*` out of `/lib`. The xpm schema still wants + -- a url + sha256 per version, so this is a stable, inert + -- anchor and nothing more — exactly what compat.glx-runtime + -- does with an OpenGL-Registry README and compat.vulkan-runtime + -- with a Vulkan-Loader README. + -- + -- It was briefly Mesa's own `src/gbm/main/gbm.h`, and that was + -- a mistake: an anchor NAMED like the header this package + -- installs reads as though the download is the shipped header, + -- which is the one thing it is not. A README cannot be + -- mistaken for a payload. + -- + -- A raw file at a tag is byte-stable, unlike a GitLab-generated + -- archive; sha256 confirmed twice, and the CN asset re-fetched + -- and compared byte-for-byte against GLOBAL. + url = { + GLOBAL = "https://gitlab.freedesktop.org/mesa/mesa/-/raw/mesa-25.0.7/README.rst", + CN = "https://gitcode.com/mcpp-res/libgbm/releases/download/2026.08.29/libgbm-2026.08.29.rst", + }, + sha256 = "03f0fd62094179bb70fb885042baa4254d392f5f7bb64e4d8856bec8a5ff8386", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + -- Built by install(): `gbm.h` symlinked out of the subos view, so the + -- header and the library can never come from different Mesa builds. + include_dirs = { "mcpp_generated/libgbm/include" }, + + -- An anchor so the lib target has something to compile, the + -- compat.glx-runtime / compat.glx-headers shape. This package has no + -- code of its own. + generated_files = { + ["mcpp_generated/libgbm_anchor.c"] = + "int mcpp_compat_libgbm_anchor(void) { return 0; }\n", + }, + sources = { "mcpp_generated/libgbm_anchor.c" }, + + -- NOT named `gbm`. A target called `gbm` would put a `libgbm.a` on the + -- link line beside the real `libgbm.so` this package exists to + -- deliver, and which of the two `-lgbm` picks would come down to + -- search order. + targets = { ["gbm_binding"] = { kind = "lib" } }, + + -- The link against Mesa's libgbm itself, resolved through the farm. + ldflags = { "-lgbm" }, + + -- Zero. gbm.h includes only and . + deps = {}, + + runtime = { + -- TWO DIRECTORY KEYS, TWO DIFFERENT FLAGS, and they are not + -- interchangeable. Measured on mcpp 2026.8.27.2 by reading the + -- emitted build.ninja: + -- + -- library_dirs -> -Wl,-rpath (RUNPATH only) + -- link_library_dirs -> -L + -- + -- `library_dirs` ALONE is what compat.glx-runtime and + -- compat.vulkan-runtime declare, and it is right for them: nothing + -- links against their farms, they exist so a bare-soname `dlopen` + -- resolves at RUN time. This package does link against its farm, + -- so it needs the `-L` too — with only `library_dirs` the farm is + -- complete, the rpath correct, and the build still dies at + -- `ld: cannot find -lgbm`. + library_dirs = { "mcpp_generated/libgbm/lib" }, + link_library_dirs = { "mcpp_generated/libgbm/lib" }, + provides = { "drm.gbm" }, + -- No `capabilities` entry. compat.glx-runtime declares + -- "x11.display" because it needs the sandbox to expose a socket it + -- does not own; there is no verified DRM counterpart in the + -- engine's vocabulary, and coining one that may be silently + -- ignored would document a guarantee this package cannot make. + }, + }, +} + +import("xim.libxpkg.pkginfo") +import("xim.libxpkg.system") +import("xim.libxpkg.log") + +-- The install() hook is a blind spot by default: log.error does not reach the +-- CI log, and a call outside the sandbox's xmake-API subset terminates the +-- hook with no message at all. So the log file comes first and every step +-- announces itself before doing anything. validate.yml's failure step collects +-- `mcpp_*_build.log`, which is what this name matches. +local log_path = nil + +local function say(msg) + if log_path == nil then + return + end + local prev = io.readfile(log_path) or "" + io.writefile(log_path, prev .. msg .. "\n") +end + +local function fail(msg) + say("FAILED: " .. msg) + log.error("[libgbm] %s", msg) + return false +end + +local function sh_quote(value) + return "'" .. tostring(value):gsub("'", "'\\''") .. "'" +end + +-- Link every file matching `pattern` in `srcdir` into `outdir`, by basename. +local function link_matching(srcdir, pattern, outdir) + os.exec( + "for f in " .. sh_quote(srcdir) .. "/" .. pattern .. + "; do [ -e \"$f\" ] || continue; " .. + "ln -sf \"$f\" " .. sh_quote(outdir) .. "/\"$(basename \"$f\")\"; " .. + "done" + ) +end + +function install() + local prefix = pkginfo.install_dir() + os.mkdir(prefix) + + log_path = path.join(prefix, "mcpp_libgbm_build.log") + io.writefile(log_path, "compat.libgbm install()\n") + + local view = system.subos_sysrootdir() + say("subos view: " .. tostring(view)) + + local view_lib = path.join(view, "lib") + local view_inc = path.join(view, "usr", "include") + + local root = path.join(prefix, "mcpp_generated", "libgbm") + local out_lib = path.join(root, "lib") + local out_inc = path.join(root, "include") + + os.mkdir(out_lib) + os.mkdir(out_inc) + + -- 1. The library. From the subos view and from nowhere else — see the + -- header comment: this package has no host path and no override. + say("linking libgbm.so* from " .. view_lib) + link_matching(view_lib, "libgbm.so*", out_lib) + + for _, required in ipairs({"libgbm.so", "libgbm.so.1"}) do + if not os.isfile(path.join(out_lib, required)) then + return fail(required .. " is not in this subos. libgbm comes from " + .. "`xim:mesa`, which this package declares as a " + .. "runtime dependency; if it is declared and this " + .. "still fires, that install did not finish") + end + end + say("libgbm.so and libgbm.so.1 present") + + -- The farm glob is `libgbm.so*` and cannot match a C runtime, but + -- compat.glx-runtime's rule is to ASSERT rather than trust: the failure a + -- stray libc here produces is a fault inside the dynamic linker before + -- main, with no output at all, and the glob is one careless edit from + -- matching more than it meant to. + for _, bad in ipairs({"libc.so.6", "libm.so.6", "ld-linux-x86-64.so.2"}) do + if os.isfile(path.join(out_lib, bad)) then + return fail(bad .. " was linked into the libgbm farm; it would " + .. "reach every consumer's RUNPATH and pair a second " + .. "libc with mcpp's loader") + end + end + + -- 2. The header, from the same view, so it is necessarily the one that + -- matches the library above. + say("linking gbm.h from " .. view_inc) + link_matching(view_inc, "gbm.h", out_inc) + if not os.isfile(path.join(out_inc, "gbm.h")) then + return fail("gbm.h is not in this subos (expected " + .. path.join(view_inc, "gbm.h") .. ")") + end + say("gbm.h present") + + -- No step 3. The backend search path is the ENVIRONMENT's job and + -- `xim:mesa` now does it (openxlings/xim-pkgindex#713) — see the header + -- comment. This package deliberately owns no part of it. + say("done") + return true +end diff --git a/pkgs/c/compat.wayland.lua b/pkgs/c/compat.wayland.lua new file mode 100644 index 00000000..ea119622 --- /dev/null +++ b/pkgs/c/compat.wayland.lua @@ -0,0 +1,199 @@ +-- compat.wayland — the Wayland core libraries: the client library a GUI +-- application links (`wl_display_connect`, the `wl_registry` / proxy +-- machinery), the server library a compositor links, the cursor-theme loader, +-- and the `wl_egl_window` shim that binds a surface to EGL. +-- +-- It completes the display half of the stack these packages now cover: with +-- compat.libdrm and compat.libgbm a program can allocate and scan out on a +-- bare KMS console, and with this it can instead be a client of — or itself +-- be — a Wayland compositor. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: a binding, same criterion as compat.libdrm +-- +-- Wayland is an independent freedesktop project with its own releases, so a +-- source build would be defensible on the "separable unit" test. It is a +-- binding for the second reason: `xim:wayland` already exists and Mesa depends +-- on it (`libEGL_mesa` has a DT_NEEDED on `libwayland-client`, which is why +-- mesa.lua lists it as a hard dependency rather than an option). A second +-- `libwayland-client.so.0` in a process that also loads Mesa's EGL would mean +-- two proxy tables for one connection. +-- +-- host 0 no /usr/lib* path, no escape-hatch variable +-- ecosystem 1 `xim:wayland` +-- index 0 `deps = {}` +-- transitive 0 the wayland libs need only libc/libm/libffi, all +-- resolved inside xim-x-* +-- +-- ───────────────────────────────────────────────────────────────────────── +-- FOUR LIBRARIES, ONE DEFAULT ON THE LINK LINE +-- +-- The payload carries `libwayland-client`, `libwayland-server`, +-- `libwayland-cursor` and `libwayland-egl`, and all four are harvested — the +-- farm is on `-L`, so any of them can be linked. But `ldflags` names only +-- `-lwayland-client`. +-- +-- That asymmetry is deliberate. A client is overwhelmingly the common case, +-- and it is the one where getting it wrong is silent; a compositor author +-- knows they need `-lwayland-server` and will say so. Putting all four in +-- `ldflags` would instead force every consumer to carry the server library — +-- and `ldflags` from a dependency reaches the consumer's link line, so there +-- is no way for them to opt out short of not using this package. +-- +-- A consumer wanting more adds them to its own `[build] ldflags`, and they +-- resolve out of this package's farm without any further declaration: +-- +-- [build] +-- ldflags = ["-lwayland-server"] # or -lwayland-cursor, -lwayland-egl +-- +-- WHAT IS NOT HERE: the protocol XML and `wayland-scanner`. Real clients +-- generate `xdg-shell` and friends from `wayland-protocols` at build time, and +-- that is a code generator plus a data package — a different shape (the +-- compat.protobuf `protoc` shape, a `kind = "bin"` target) and a separate +-- package. This one is the runtime libraries and the core headers only, which +-- is what `wl_display_connect` and the EGL platform need. +package = { + spec = "1", + namespace = "compat", + name = "wayland", + description = "Wayland core client/server libraries, bound to the ecosystem's xim:wayland", + licenses = {"MIT"}, + repo = "https://gitlab.freedesktop.org/wayland/wayland", + type = "package", + + xpm = { + linux = { + deps = { runtime = { "xim:wayland" } }, + ["2026.08.30"] = { + -- Inert anchor; nothing downloaded is read. See compat.libgbm + -- for why this is a README rather than a header. + url = { + GLOBAL = "https://gitlab.freedesktop.org/wayland/wayland/-/raw/1.23.1/README.md", + CN = "https://gitcode.com/mcpp-res/wayland/releases/download/2026.08.30/wayland-2026.08.30.md", + }, + sha256 = "147f133b07a9ea767e426944c7c5e3946d642cfbf392f63e28a37888b700fb54", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + include_dirs = { "mcpp_generated/wayland/include" }, + + generated_files = { + ["mcpp_generated/wayland_anchor.c"] = + "int mcpp_compat_wayland_anchor(void) { return 0; }\n", + }, + sources = { "mcpp_generated/wayland_anchor.c" }, + + -- NOT named `wayland`: a `libwayland.a` beside the real shared objects + -- would let search order decide. Same rule as compat.libgbm's + -- `gbm_binding`. + targets = { ["wayland_binding"] = { kind = "lib" } }, + + -- The client only; see the header comment. The other three are in the + -- farm and reachable through a consumer's own ldflags. + ldflags = { "-lwayland-client" }, + deps = {}, + + runtime = { + library_dirs = { "mcpp_generated/wayland/lib" }, + link_library_dirs = { "mcpp_generated/wayland/lib" }, + provides = { "wayland.client" }, + }, + }, +} + +import("xim.libxpkg.pkginfo") +import("xim.libxpkg.system") +import("xim.libxpkg.log") + +local log_path = nil + +local function say(msg) + if log_path == nil then return end + local prev = io.readfile(log_path) or "" + io.writefile(log_path, prev .. msg .. "\n") +end + +local function fail(msg) + say("FAILED: " .. msg) + log.error("[wayland] %s", msg) + return false +end + +local function sh_quote(value) + return "'" .. tostring(value):gsub("'", "'\\''") .. "'" +end + +local function link_matching(srcdir, pattern, outdir) + os.exec( + "for f in " .. sh_quote(srcdir) .. "/" .. pattern .. + "; do [ -e \"$f\" ] || continue; " .. + "ln -sf \"$f\" " .. sh_quote(outdir) .. "/\"$(basename \"$f\")\"; " .. + "done" + ) +end + +function install() + local prefix = pkginfo.install_dir() + os.mkdir(prefix) + + log_path = path.join(prefix, "mcpp_wayland_build.log") + io.writefile(log_path, "compat.wayland install()\n") + + local view = system.subos_sysrootdir() + say("subos view: " .. tostring(view)) + + local view_lib = path.join(view, "lib") + local view_inc = path.join(view, "usr", "include") + + local root = path.join(prefix, "mcpp_generated", "wayland") + local out_lib = path.join(root, "lib") + local out_inc = path.join(root, "include") + + os.mkdir(out_lib) + os.mkdir(out_inc) + + -- 1. All four libraries. Only the client is on the link line by default, + -- but the others must be PRESENT or a consumer's own + -- `-lwayland-server` would have nothing to resolve against. + say("linking libwayland-*.so* from " .. view_lib) + link_matching(view_lib, "libwayland-*.so*", out_lib) + + for _, required in ipairs({"libwayland-client.so", "libwayland-client.so.0"}) do + if not os.isfile(path.join(out_lib, required)) then + return fail(required .. " is not in this subos. The wayland " + .. "libraries come from `xim:wayland`, which this " + .. "package declares as a runtime dependency; if it is " + .. "declared and this still fires, that install did " + .. "not finish") + end + end + say("libwayland-client present") + + for _, bad in ipairs({"libc.so.6", "libm.so.6", "ld-linux-x86-64.so.2"}) do + if os.isfile(path.join(out_lib, bad)) then + return fail(bad .. " was linked into the wayland farm; it would " + .. "reach every consumer's RUNPATH and pair a second " + .. "libc with mcpp's loader") + end + end + + -- 2. The core headers, which upstream installs flat at the include root. + -- `wayland-client.h`, `-server.h`, `-cursor.h`, `-egl.h` and the + -- `-core`/`-protocol` halves they include. + say("linking wayland-*.h from " .. view_inc) + link_matching(view_inc, "wayland-*.h", out_inc) + if not os.isfile(path.join(out_inc, "wayland-client.h")) then + return fail("wayland-client.h is not in this subos (expected " + .. path.join(view_inc, "wayland-client.h") .. ")") + end + say("wayland headers present") + + say("done") + return true +end diff --git a/tests/examples/egl/mcpp.toml b/tests/examples/egl/mcpp.toml new file mode 100644 index 00000000..c8a8cc61 --- /dev/null +++ b/tests/examples/egl/mcpp.toml @@ -0,0 +1,17 @@ +# EGL test project. +# +# Linux-only like its neighbours, and the dependency is gated so the test +# compiles to a no-op main() elsewhere. +# +# The interesting assertion is not "does -lEGL resolve" but that +# `#include ` PARSES: eglplatform.h opens with +# `#include `, which this package does not ship and +# deliberately takes from compat.khrplatform instead of adding a third +# provider of KHR/ to the index. If that dependency edge is ever dropped, this +# member stops compiling rather than failing subtly later. +[package] +name = "egl-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +egl = "2026.08.30" diff --git a/tests/examples/egl/tests/egl.cpp b/tests/examples/egl/tests/egl.cpp new file mode 100644 index 00000000..9a5e659f --- /dev/null +++ b/tests/examples/egl/tests/egl.cpp @@ -0,0 +1,107 @@ +// compat.egl — behavioral test, runnable with no GPU and no display. +// +// What can be wrong here, in order of how quietly it fails: +// +// 1. THE HEADER DOES NOT PARSE. `EGL/eglplatform.h` opens with +// `#include `, and this package deliberately does not +// ship KHR/ — it takes it from compat.khrplatform rather than becoming a +// third provider of that directory. So compilation itself is the first +// assertion, and it is the one that breaks if the dependency edge goes. +// +// 2. THE DISPATCH LIBRARY IS ABSENT while headers are present. The dlsym +// checks pin that. +// +// 3. THE GBM PLATFORM TOKEN IS MISSING. `EGL_PLATFORM_GBM_KHR` is what makes +// compat.libgbm useful for rendering rather than only allocation -- +// eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm_device, NULL) is the +// whole headless-GPU entry point. A libglvnd built without that extension +// would leave the GBM package able to allocate and unable to render. +// +// Creating a display needs a real GPU, so that is opt-in +// (MCPP_RUN_EGL_DISPLAY=1). Everything else runs on a bare CI runner. + +#ifdef __linux__ + +#include +#include + +#include + +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +} // namespace + +int main() +{ + // ── 1. The headers parsed, and KHR came from compat.khrplatform ────── + // Reaching this line means resolved. Assert a type + // that comes from it so the dependency is explicit rather than implied. + check(sizeof(khronos_int32_t) == 4, + "KHR/khrplatform.h resolved (via compat.khrplatform)"); + check(EGL_SUCCESS == 0x3000, "EGL/egl.h provides the EGL_SUCCESS token"); + + // ── 2. The GBM platform token exists ───────────────────────────────── + // This is the seam with compat.libgbm. Without it the two packages cannot + // be combined, which is most of the reason to want EGL here at all. + check(EGL_PLATFORM_GBM_KHR == 0x31D7, + "EGL_PLATFORM_GBM_KHR is present (the compat.libgbm seam)"); + + // ── 3. The dispatch library is really linked ───────────────────────── + for (const char *sym : {"eglGetPlatformDisplay", "eglInitialize", + "eglCreateContext", "eglMakeCurrent", + "eglQueryString", "eglGetProcAddress"}) { + check(::dlsym(RTLD_DEFAULT, sym) != nullptr, + (std::string("libEGL exports ") + sym).c_str()); + } + + // ── 4. The client-extension query answers without a display ────────── + // EGL_EXT_client_extensions makes this legal on EGL_NO_DISPLAY, and it is + // the one call that exercises the dispatch layer without hardware. + const char *ext = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); + std::printf(" client extensions: %s\n", + ext ? (ext[0] ? ext : "(empty)") : "(null)"); + check(ext != nullptr, + "eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS) answers"); + + // ── 5. A real display, opt-in ──────────────────────────────────────── + if (std::getenv("MCPP_RUN_EGL_DISPLAY") != nullptr) { + EGLDisplay dpy = eglGetDisplay(EGL_DEFAULT_DISPLAY); + std::printf(" eglGetDisplay = %p\n", (void *)dpy); + if (dpy != EGL_NO_DISPLAY) { + EGLint major = 0, minor = 0; + const EGLBoolean ok = eglInitialize(dpy, &major, &minor); + std::printf(" eglInitialize = %d (EGL %d.%d)\n", (int)ok, major, minor); + check(ok == EGL_TRUE, "eglInitialize on the default display"); + if (ok) eglTerminate(dpy); + } + } else { + std::printf(" (display creation is opt-in: set MCPP_RUN_EGL_DISPLAY=1)\n"); + } + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/tests/examples/libdrm/mcpp.toml b/tests/examples/libdrm/mcpp.toml new file mode 100644 index 00000000..c091632b --- /dev/null +++ b/tests/examples/libdrm/mcpp.toml @@ -0,0 +1,17 @@ +# libdrm test project. +# +# DRM/KMS is a Linux kernel interface, so like tests/examples/libgbm the +# descriptor has a `linux` section only, the dependency is gated, and the test +# compiles to a no-op main() elsewhere. +# +# The interesting failure here is the include layout, not the link. libdrm +# installs its public headers at the include root and the uapi headers they +# include in a `libdrm/` subdirectory, and `xf86drm.h` line 40 is a bare +# `#include ` — so a package exposing one root builds nothing at all. +# This member's first assertion is simply that the translation unit compiles. +[package] +name = "libdrm-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +libdrm = "2026.08.30" diff --git a/tests/examples/libdrm/tests/drm.cpp b/tests/examples/libdrm/tests/drm.cpp new file mode 100644 index 00000000..5f23d6f4 --- /dev/null +++ b/tests/examples/libdrm/tests/drm.cpp @@ -0,0 +1,117 @@ +// compat.libdrm — behavioral test, runnable with no GPU and no DRM node. +// +// Three things can be wrong with this package and none is a missing symbol. +// +// 1. THE INCLUDE LAYOUT. libdrm splits its headers across two roots — the +// public ones (`xf86drm.h`) at the include root, the uapi ones (`drm.h`, +// `drm_mode.h`, `drm_fourcc.h`) under `libdrm/` — and `xf86drm.h` itself +// does `#include `. Exposing one root fails at line 40 of the +// header, before any of this code exists. So the fact that this file +// COMPILES is the first assertion, and it is not a trivial one. +// +// 2. THE LIBRARY COULD BE ABSENT while the headers are present. The dlsym +// checks below pin that: the farm resolves `-ldrm` out of the subos view, +// and a header-only package would sail past compilation and fail here. +// +// 3. THE UAPI CONSTANTS COULD DISAGREE with the library's. drm_fourcc.h is +// the kernel's, and DRM_FORMAT_XRGB8888 must be the same fourcc GBM calls +// GBM_FORMAT_XRGB8888 — the two APIs exchange exactly these values across +// the gbm_bo -> drmModeAddFB2 boundary, so a mismatch would surface as a +// display that shows the wrong colours rather than as an error. +// +// Opening a real DRM device is opt-in (MCPP_RUN_DRM_DEVICE=1): CI runners have +// no /dev/dri at all. + +#ifdef __linux__ + +#include // public API; pulls from the second root +#include // the KMS family +#include // uapi, from the libdrm/ root + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +} // namespace + +int main() +{ + // ── 1. Both include roots resolved ─────────────────────────────────── + // Reaching this line at all means found the it + // includes. Assert a constant from each root so the check is explicit + // rather than implied by compilation succeeding. + check(DRM_MODE_TYPE_PREFERRED != 0, + "xf86drmMode.h (public root) provides DRM_MODE_TYPE_PREFERRED"); + check(DRM_FORMAT_XRGB8888 != 0, + "drm_fourcc.h (libdrm/ root) provides DRM_FORMAT_XRGB8888"); + + // ── 2. The fourcc agrees with GBM's ────────────────────────────────── + // gbm_bo_get_format() returns a value handed straight to drmModeAddFB2. + // 'X','R','2','4' little-endian — the same number compat.libgbm asserts as + // "XR24". If these ever diverge, a KMS consumer shows wrong colours and + // nothing reports an error. + check(DRM_FORMAT_XRGB8888 == ((std::uint32_t)'X' | ((std::uint32_t)'R' << 8) + | ((std::uint32_t)'2' << 16) + | ((std::uint32_t)'4' << 24)), + "DRM_FORMAT_XRGB8888 is the 'XR24' fourcc GBM also uses"); + + // ── 3. The library is really linked ────────────────────────────────── + for (const char *sym : {"drmGetVersion", "drmFreeVersion", "drmModeGetResources", + "drmModeAddFB2", "drmModeSetCrtc", "drmPrimeHandleToFD"}) { + check(::dlsym(RTLD_DEFAULT, sym) != nullptr, + (std::string("libdrm exports ") + sym).c_str()); + } + + // ── 4. An invalid fd is rejected, not crashed on ───────────────────── + check(drmGetVersion(-1) == nullptr, "drmGetVersion(-1) == nullptr"); + + // ── 5. A real device, opt-in ───────────────────────────────────────── + if (std::getenv("MCPP_RUN_DRM_DEVICE") != nullptr) { + const int fd = ::open("/dev/dri/card0", O_RDWR); + if (fd < 0) { + std::printf(" MCPP_RUN_DRM_DEVICE set but /dev/dri/card0 did not " + "open; skipping\n"); + } else { + drmVersionPtr v = drmGetVersion(fd); + check(v != nullptr, "drmGetVersion on a real DRM node"); + if (v != nullptr) { + std::printf(" driver: %s\n", v->name ? v->name : "(null)"); + drmFreeVersion(v); + } + ::close(fd); + } + } else { + std::printf(" (device access is opt-in: set MCPP_RUN_DRM_DEVICE=1 on " + "a machine with /dev/dri)\n"); + } + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/tests/examples/libgbm/mcpp.toml b/tests/examples/libgbm/mcpp.toml new file mode 100644 index 00000000..fd9e7c01 --- /dev/null +++ b/tests/examples/libgbm/mcpp.toml @@ -0,0 +1,39 @@ +# libgbm test project. +# +# GBM is the DRM buffer-management API, so like tests/examples/libaio the +# descriptor has a `linux` section and nothing else, the dependency is gated, +# and the test compiles to a no-op main() elsewhere. +# +# What this member proves is not "does -lgbm resolve". compat.libgbm is a thin +# BINDING onto the ecosystem's Mesa — it compiles no upstream source and sets +# no environment — so the interesting failures are all in the seams: +# +# * the header could come from a different Mesa than the library (both are +# taken from the same subos view precisely so they cannot); +# * the backend directory could be unreachable, which is the default state of +# an mcpp sandbox — Mesa compiles `/usr/lib/gbm` in as its search path and +# that does not exist there; +# * and the repair for that lives OUTSIDE this repository: `xim:mesa` +# declares GBM_BACKENDS_PATH through the graphics discovery layer, and mcpp +# carries subos declarations into the processes it launches. +# +# That last point is why this member matters more than a link check: it is the +# only place in this repo that notices if either of those two breaks. +# +# Two binaries, deliberately: +# +# tests/gbm.cpp the full surface — format canonicalization through +# the real library, symbol presence, invalid-device +# handling, and the backend-path assertions. +# tests/stock_usage.cpp the minimal consumer: one header, one library, +# nothing else. Smallest thing that still detects all +# three regressions above, so a failure is unambiguous. +# +# Everything asserted here runs on a CI runner with no /dev/dri. Real device +# creation is opt-in behind MCPP_RUN_GBM_DEVICE=1. +[package] +name = "libgbm-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +libgbm = "2026.08.29" diff --git a/tests/examples/libgbm/tests/gbm.cpp b/tests/examples/libgbm/tests/gbm.cpp new file mode 100644 index 00000000..7f5a2d31 --- /dev/null +++ b/tests/examples/libgbm/tests/gbm.cpp @@ -0,0 +1,191 @@ +// compat.libgbm — behavioral test, runnable on a machine with no GPU. +// +// The package is a BINDING onto the ecosystem's Mesa rather than a source +// build, so the ways it can be wrong are not missing symbols: +// +// 1. The header could come from a different Mesa than the library. Both are +// taken from the subos view for exactly this reason, and the assertions +// below call through the header into the library to keep that honest. +// +// 2. The library could be present and the BACKEND unreachable. libgbm is a +// loader: gbm_create_device() dlopens `/_gbm.so`, where +// is GBM_BACKENDS_PATH or the `/usr/lib/gbm` compiled into Mesa — +// correct on a distro, wrong the moment the payload is relocated: +// +// MESA-LOADER: failed to open dri: /usr/lib/gbm/dri_gbm.so: cannot +// open shared object file (search paths /usr/lib/gbm, suffix _gbm) +// +// Setting that variable is the ENVIRONMENT's job, not this package's, and +// `xim:mesa` now does it through the graphics discovery layer +// (openxlings/xim-pkgindex#713). So the check below is a check on the +// ECOSYSTEM: if the declaration is ever dropped from that table, or the +// backends stop being placed into the subos, this goes red here. +// +// WHY THE LEGACY ENUM IS ASSERTED. gbm_format_get_name(GBM_FORMAT_XRGB8888) is +// a weak test on its own — the answer is four bytes of the fourcc and a +// header-only reimplementation would produce it. GBM_BO_FORMAT_XRGB8888 is the +// value 0, and only the LIBRARY's format_canonicalize() turns it into "XR24". +// So that case is what proves the calls land in Mesa's libgbm. +// +// Creating a real device is opt-in (MCPP_RUN_GBM_DEVICE=1) and needs +// /dev/dri: CI runners have no DRM device, and on a host whose xim-x-mesa is +// built against a newer glibc than xim-x-glibc the backend is found and then +// fails to dlopen — an ecosystem-stack skew this package does not cause and +// must not assert its way around. + +#ifdef __linux__ + +// Stock libgbm, and nothing else. This package ships no header of its own. +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +// gbm_format_get_name writes into desc->name and returns it. +std::string format_name(std::uint32_t format) +{ + gbm_format_name_desc desc {}; + gbm_format_get_name(format, &desc); + return std::string(desc.name); +} + +bool dir_has_backend(const char *dir) +{ + DIR *d = ::opendir(dir); + if (d == nullptr) { + return false; + } + + // Mesa's BACKEND_LIB_SUFFIX is "_gbm", so a backend is "_gbm.so" + // -- 7 trailing characters, and the name must be longer than the suffix + // alone for there to be a driver name in front of it. + static const char suffix[] = "_gbm.so"; + const std::size_t suffix_len = sizeof(suffix) - 1; + + bool found = false; + while (dirent *e = ::readdir(d)) { + const std::size_t n = std::strlen(e->d_name); + if (n > suffix_len && + std::strcmp(e->d_name + (n - suffix_len), suffix) == 0) { + found = true; + break; + } + } + ::closedir(d); + return found; +} + +} // namespace + +int main() +{ + // ── 1. The calls reach Mesa's libgbm ───────────────────────────────── + // Pure functions: no device, no GPU, no DRM node. + check(format_name(GBM_FORMAT_XRGB8888) == "XR24", + "gbm_format_get_name(GBM_FORMAT_XRGB8888) == \"XR24\""); + check(format_name(GBM_FORMAT_ARGB8888) == "AR24", + "gbm_format_get_name(GBM_FORMAT_ARGB8888) == \"AR24\""); + check(format_name(GBM_FORMAT_NV12) == "NV12", + "gbm_format_get_name(GBM_FORMAT_NV12) == \"NV12\""); + check(format_name(GBM_FORMAT_ABGR2101010) == "AB30", + "gbm_format_get_name(GBM_FORMAT_ABGR2101010) == \"AB30\""); + + // The one that cannot be answered by the header: the legacy enumerator is + // 0, and format_canonicalize() inside the library maps it to the fourcc. + check(format_name(static_cast(GBM_BO_FORMAT_XRGB8888)) == "XR24", + "library canonicalizes GBM_BO_FORMAT_XRGB8888 (== 0) to \"XR24\""); + check(format_name(static_cast(GBM_BO_FORMAT_ARGB8888)) == "AR24", + "library canonicalizes GBM_BO_FORMAT_ARGB8888 (== 1) to \"AR24\""); + + // ── 2. Header and library are the same Mesa ────────────────────────── + // Every function the header declares and this test names must actually be + // in the loaded object. A header from a newer Mesa than the library shows + // up here rather than as a link error, because the farm resolves -lgbm to + // whatever the subos view holds. + for (const char *sym : {"gbm_format_get_name", "gbm_create_device", + "gbm_device_destroy", "gbm_device_get_backend_name", + "gbm_bo_create", "gbm_surface_create"}) { + check(::dlsym(RTLD_DEFAULT, sym) != nullptr, + (std::string("libgbm exports ") + sym).c_str()); + } + + // ── 3. An invalid device is rejected, not crashed on ───────────────── + check(gbm_create_device(-1) == nullptr, + "gbm_create_device(-1) == nullptr"); + + // ── 4. The ECOSYSTEM supplies the backend path ─────────────────────── + // Nothing in this package sets this. It comes from `xim:mesa`'s + // declaration in the graphics discovery layer, carried into the process by + // mcpp's subos-env handling. Asserting it here is what makes a regression + // in EITHER of those two land on this package's CI rather than silently on + // a user. + const char *dir = std::getenv("GBM_BACKENDS_PATH"); + check(dir != nullptr, + "GBM_BACKENDS_PATH is set by the ecosystem, not by this package"); + + if (dir != nullptr) { + std::printf(" backends dir: %s\n", dir); + + struct ::stat st {}; + check(::stat(dir, &st) == 0 && S_ISDIR(st.st_mode), + "it names a directory that exists"); + check(dir_has_backend(dir), + "it contains at least one *_gbm.so backend"); + } + + // ── 5. A real device, opt-in ───────────────────────────────────────── + if (std::getenv("MCPP_RUN_GBM_DEVICE") != nullptr) { + const int fd = ::open("/dev/dri/renderD128", O_RDWR); + if (fd < 0) { + std::printf(" MCPP_RUN_GBM_DEVICE set but /dev/dri/renderD128 " + "did not open; skipping\n"); + } else { + gbm_device *dev = gbm_create_device(fd); + check(dev != nullptr, "gbm_create_device on a real DRM node"); + if (dev != nullptr) { + const char *backend = gbm_device_get_backend_name(dev); + std::printf(" backend: %s\n", backend ? backend : "(null)"); + check(backend != nullptr && backend[0] != '\0', + "the device reports a backend name"); + gbm_device_destroy(dev); + } + ::close(fd); + } + } else { + std::printf(" (device creation is opt-in: set MCPP_RUN_GBM_DEVICE=1 " + "on a machine with /dev/dri)\n"); + } + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/tests/examples/libgbm/tests/stock_usage.cpp b/tests/examples/libgbm/tests/stock_usage.cpp new file mode 100644 index 00000000..9c1538e5 --- /dev/null +++ b/tests/examples/libgbm/tests/stock_usage.cpp @@ -0,0 +1,88 @@ +// The minimal consumer, as a separate translation unit and a separate binary. +// +// This file includes STOCK and nothing else -- and since the package +// ships no header of its own any more, that is now simply what a consumer +// looks like. It is what a project ported from any other build system looks +// like, and more importantly what a THIRD-PARTY library looks like from the +// inside: SDL2's KMSDRM backend, wlroots and ffmpeg's VAAPI hwcontext all call +// gbm_create_device() out of their own sources, having included only . +// +// WHAT IT GUARDS, now that the package sets nothing itself. GBM_BACKENDS_PATH +// arrives from the ECOSYSTEM: `xim:mesa` places its backends into the subos and +// declares the variable through the graphics discovery layer +// (openxlings/xim-pkgindex#713), and mcpp carries subos declarations into the +// processes it launches. Neither of those is this repository's code, so this +// binary is the tripwire on both -- if the DISCOVERY row is dropped, or the +// backends stop being placed, or mcpp stops injecting subos env, it goes red +// here rather than silently on a user whose gbm_create_device() returns NULL. +// +// It is deliberately a SECOND binary rather than more assertions inside +// gbm.cpp: a consumer that includes one header and links one library is the +// smallest thing that can still detect all three of those regressions, and +// keeping it minimal is what makes a failure here unambiguous. + +#ifdef __linux__ + +#include + +#include +#include + +#include +#include +#include + +int main() +{ + int failures = 0; + auto check = [&](bool ok, const char *what) { + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } + }; + + // Nothing in this program has run yet that could have set this. + const char *dir = std::getenv("GBM_BACKENDS_PATH"); + check(dir != nullptr, + "a -only consumer inherits GBM_BACKENDS_PATH"); + + if (dir != nullptr) { + std::printf(" %s\n", dir); + + struct ::stat st {}; + check(::stat(dir, &st) == 0 && S_ISDIR(st.st_mode), + " ... and it is a directory"); + + bool found = false; + if (DIR *d = ::opendir(dir)) { + static const char suffix[] = "_gbm.so"; + const std::size_t len = sizeof(suffix) - 1; + while (dirent *e = ::readdir(d)) { + const std::size_t n = std::strlen(e->d_name); + if (n > len && std::strcmp(e->d_name + (n - len), suffix) == 0) { + found = true; + break; + } + } + ::closedir(d); + } + check(found, " ... holding a backend libgbm can actually dlopen"); + } + + // The stock call still behaves on a bad fd rather than crashing, which is + // the only device-level thing assertable without a DRM node. + check(gbm_create_device(-1) == nullptr, "gbm_create_device(-1) == nullptr"); + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/tests/examples/wayland/mcpp.toml b/tests/examples/wayland/mcpp.toml new file mode 100644 index 00000000..736fec60 --- /dev/null +++ b/tests/examples/wayland/mcpp.toml @@ -0,0 +1,25 @@ +# Wayland test project. +# +# Linux-only, dependency gated, no-op main() elsewhere — same shape as its +# neighbours. +# +# No compositor is required and none is assumed: the assertions cover the +# header/library seam and the ONE call that is meaningful without a server +# (`wl_display_connect` against a socket that is not there must fail cleanly +# rather than crash). CI runners have no WAYLAND_DISPLAY. +# +# The second binary checks the part this package deliberately does not put on +# the link line: libwayland-server is in the farm, so a consumer can reach it +# through its own ldflags. If the farm ever stops carrying it, that is a silent +# regression for compositor authors, and this is what catches it. +[package] +name = "wayland-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +wayland = "2026.08.30" + +# Exactly the escape hatch the descriptor documents: the package puts only +# -lwayland-client on the link line, and a consumer adds what else it needs. +[target.'cfg(linux)'.build] +ldflags = ["-lwayland-server"] diff --git a/tests/examples/wayland/tests/wayland.cpp b/tests/examples/wayland/tests/wayland.cpp new file mode 100644 index 00000000..8bed74d6 --- /dev/null +++ b/tests/examples/wayland/tests/wayland.cpp @@ -0,0 +1,97 @@ +// compat.wayland — behavioral test, runnable with no compositor. +// +// Two seams, and the second is the one a compositor author depends on. +// +// 1. THE CLIENT LIBRARY, which the package puts on the link line itself. +// `wl_display_connect` on a machine with no compositor must return NULL +// cleanly -- that exercises the real library (it reads WAYLAND_DISPLAY, +// builds a socket path and fails to connect) without needing a server. +// +// 2. THE LIBRARIES THE PACKAGE DELIBERATELY DOES NOT LINK. libwayland-server +// is harvested into the farm but kept off `ldflags`, so a consumer reaches +// it by adding `-lwayland-server` to its own build — which this member's +// mcpp.toml does. If the farm ever stops carrying the server library, the +// documented escape hatch silently stops working; this file is what turns +// that into a link error here instead. + +#ifdef __linux__ + +#include +#include + +#include + +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +} // namespace + +int main() +{ + // ── 1. Headers resolved from the flat include root ─────────────────── + check(WL_DISPLAY_ERROR_INVALID_OBJECT == 0, + "wayland-client.h provides the core protocol enums"); + + // ── 2. Both libraries are really linked ────────────────────────────── + for (const char *sym : {"wl_display_connect", "wl_display_disconnect", + "wl_proxy_marshal", "wl_registry_interface"}) { + check(::dlsym(RTLD_DEFAULT, sym) != nullptr, + (std::string("libwayland-client exports ") + sym).c_str()); + } + // This one comes from the farm via the consumer's own -lwayland-server, + // not from anything this package puts on the link line. + for (const char *sym : {"wl_display_create", "wl_display_destroy"}) { + check(::dlsym(RTLD_DEFAULT, sym) != nullptr, + (std::string("libwayland-server exports ") + sym).c_str()); + } + + // ── 3. Connecting with no compositor fails cleanly ─────────────────── + // Deliberately points at a socket that cannot exist, so the result does + // not depend on whether the machine running the test has a session. + ::setenv("WAYLAND_DISPLAY", "mcpp-no-such-compositor", 1); + ::unsetenv("WAYLAND_SOCKET"); + wl_display *dpy = wl_display_connect(nullptr); + std::printf(" wl_display_connect (no compositor) = %p\n", (void *)dpy); + check(dpy == nullptr, + "wl_display_connect returns NULL rather than crashing"); + if (dpy != nullptr) { + wl_display_disconnect(dpy); + } + + // ── 4. The server library can actually build a display ─────────────── + // No socket is bound, so this needs no privileges and no session; it is + // the cheapest proof that libwayland-server is functional and not merely + // present. + wl_display *server = wl_display_create(); + std::printf(" wl_display_create = %p\n", (void *)server); + check(server != nullptr, "wl_display_create succeeds (server library live)"); + if (server != nullptr) { + wl_display_destroy(server); + } + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else + +int main() +{ + return 0; +} + +#endif