From 865f2c0fad6dabf5320c09ebd94afad49a588e3a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 23:22:59 +0800 Subject: [PATCH 01/19] feat(libgbm): add compat.libgbm 2026.08.29, GBM bound to the ecosystem's Mesa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GBM 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 compositor's back end, and under headless GPU rendering. Shape I, new: an ECOSYSTEM-STACK BINDING. Not a source build, and the reason is a dependency-surface argument rather than a convenience one. * Upstream ships no separable unit. `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; plus -DUSE_DRICONF (expat), libdrm, xcb, xcb-randr. GBM's frontend/backend dlopen split exists so vendors can ship BACKENDS, not so third parties rebuild the frontend. (compat.vulkan is not a precedent the other way: Khronos releases the loader as a standalone project; Mesa does not.) * In this ecosystem Mesa already has an owner, `xim:mesa`. A source build would make the index re-import libdrm + expat + xcb + a Mesa-util carve-out to duplicate a graph the ecosystem has already resolved — growing the surface to shrink nothing. Measured surface: host 0, ecosystem 1 (xim:mesa, not xim:graphics's 22), index 0 (deps = {}), transitive 0 — libgbm.so.1's own RUNPATH resolves entirely inside xim-x-*. ZERO HOST, with no escape hatch. Stricter than either neighbour on purpose: glx-runtime keeps MCPP_HOST_GL_LIBRARY_PATH and vulkan-runtime harvests /usr/lib outright, both because a proprietary vendor driver can only come from the host. GBM has no such case, and host libgbm is a leak the ecosystem already closed — xim:nvidia-gl-host-link names it: "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 (R7)". NVIDIA's own GBM backend, if ever wanted, belongs in that host-link layer. The part that is actual work: the backend is unreachable in the sandbox. libgbm is a loader and Mesa compiles /usr/lib/gbm in as its search path, which does not exist there — MESA-LOADER: failed to open dri: /usr/lib/gbm/dri_gbm.so: cannot open shared object file (search paths /usr/lib/gbm, suffix _gbm) and xim:mesa declares `lib` into the view while `lib/gbm/` is a subdirectory that does not follow. So install() also harvests the backends, as a SIBLING of the farm's libgbm, and a generated TU derives the path at runtime: dlsym(RTLD_DEFAULT) a gbm symbol, dladdr, append "/gbm". Verified dladdr reports the FARM path, not the realpath, so the sibling lands in this package's own payload and nothing is pinned — unlike baking an absolute path into a generated header, which would fix the package to whichever mesa payload existed on install day. Two mechanism findings, both now in the docs: * runtime.library_dirs renders as -Wl,-rpath and NOT as -L; the -L key is runtime.link_library_dirs (added 2026.8.10.3), and transitive_needed_dirs is -Wl,-rpath-link. The catalog and package-types both asserted library_dirs joined the link line, which mcpp#304 did observe but the pinned mcpp no longer does. With library_dirs alone the farm is complete, the rpath right, and the build dies at `ld: cannot find -lgbm`. Corrected in all four docs. * c_standard = "gnu11" is still silently ignored, so dladdr/RTLD_DEFAULT come from cflags -D_GNU_SOURCE (the compat.libaio finding). Target is gbm_binding, not gbm: a target named gbm would put a libgbm.a beside the real libgbm.so and let search order decide -lgbm. Test member asserts what is checkable without a GPU — 19 checks, all green on a runner with no /dev/dri. The two legacy enumerators are the load-bearing ones: GBM_BO_FORMAT_XRGB8888 is the value 0, and only the library's own format_canonicalize() turns it into "XR24", so a header-only reimplementation would pass the fourcc cases and fail these. Backend reachability is asserted as PRESENCE at the derived path rather than a successful dlopen, which stays honest on a host where the stack's own mesa/glibc skew breaks the load. Device creation is opt-in behind MCPP_RUN_GBM_DEVICE=1. CN mirror published at gitcode mcpp-res/libgbm, fetched back and confirmed byte-identical to GLOBAL. Verified with the CI-pinned mcpp (2026.8.27.2): `mcpp test -p libgbm` green from cold, all lint gates and `mcpp xpkg parse` clean across all 136 descriptors, and the assertions confirmed failable — removing dri_gbm.so from the farm turns the reachability check red and the binary exits 1. --- .agents/docs/2026-08-29-add-libgbm-plan.md | 238 +++++++++ docs/descriptor-examples.md | 3 +- docs/package-types.md | 22 +- docs/zh/descriptor-examples.md | 3 +- docs/zh/package-types.md | 20 +- mcpp.toml | 1 + pkgs/c/compat.libgbm.lua | 569 +++++++++++++++++++++ tests/examples/libgbm/mcpp.toml | 25 + tests/examples/libgbm/tests/gbm.cpp | 202 ++++++++ 9 files changed, 1074 insertions(+), 9 deletions(-) create mode 100644 .agents/docs/2026-08-29-add-libgbm-plan.md create mode 100644 pkgs/c/compat.libgbm.lua create mode 100644 tests/examples/libgbm/mcpp.toml create mode 100644 tests/examples/libgbm/tests/gbm.cpp 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..0809d0e9 --- /dev/null +++ b/.agents/docs/2026-08-29-add-libgbm-plan.md @@ -0,0 +1,238 @@ +# Adding `compat.libgbm` — GBM bound to the ecosystem's Mesa + +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`. + +### 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). Nineteen checks, all runnable 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 set from the derived path, and an explicitly set one is left alone. + +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 +… 19 checks … +0 check(s) failed + test result ok. 1 passed; 0 failed; finished in 23.61s +``` + +- `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 the binary exits 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/docs/descriptor-examples.md b/docs/descriptor-examples.md index ff83d7a5..9d0faf95 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-*`. **The part that is real work**: libgbm is a loader, and Mesa compiles `/usr/lib/gbm` in as its backend search path, which does not exist in the sandbox, so `gbm_create_device` finds nothing and no link check notices. `xim:mesa` declares `lib` into the view but `lib/gbm/` is a subdirectory and does not follow. So install() also harvests the backends, laid out as a **sibling** of the farm's libgbm, and a generated TU derives the path at runtime — `dlsym(RTLD_DEFAULT)` a gbm symbol, `dladdr` it, append `/gbm` — which reports the farm path rather than the realpath and therefore pins nothing; baking an absolute path into a generated header instead would fix the package to whichever mesa payload existed on install day. This is also the package that needs **both** directory keys: `library_dirs` for the RUNPATH and `link_library_dirs` for the `-L` that lets `-lgbm` resolve at all. Its test asserts the backend is *present at the derived path* rather than that it loads, which is checkable on a runner with no `/dev/dri` — and stays honest on a host where the stack's own mesa/glibc skew makes the dlopen fail) | | 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..9474c2cc 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,而 Mesa 把 `/usr/lib/gbm` 编译进去当后端搜索路径,沙箱里没有这个目录,于是 `gbm_create_device` 找不到后端,而且任何链接检查都发现不了。`xim:mesa` 把 `lib` 声明进 view,但 `lib/gbm/` 是子目录,不会跟着进去。所以 install() 把后端也收进来,并且放成 farm 里 libgbm 的**同级**目录,再由一个生成的 TU 在运行时推导路径 —— `dlsym(RTLD_DEFAULT)` 取一个 gbm 符号、`dladdr`、拼上 `/gbm` —— 它报的是 farm 路径而非 realpath,因此**什么都不固定**;换成在 install 时把绝对路径烧进生成头,包就被钉死在安装当天那个 mesa payload 上。这也是唯一**两个**目录键都要的包:`library_dirs` 给 RUNPATH,`link_library_dirs` 给那个让 `-lgbm` 能解析的 `-L`。它的测试断言后端**存在于推导出的路径上**,而不是断言它能加载 —— 前者在没有 `/dev/dri` 的 runner 上可验证,并且在生态自身 mesa/glibc 错位导致 dlopen 失败的机器上也不会说谎) | | 恒开的 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/mcpp.toml b/mcpp.toml index 2156b06c..0f861113 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -81,6 +81,7 @@ members = [ "tests/examples/freetype", "tests/examples/glad", "tests/examples/libaio", + "tests/examples/libgbm", "tests/examples/libpng", "tests/examples/libwebp", "tests/examples/llamacpp", diff --git a/pkgs/c/compat.libgbm.lua b/pkgs/c/compat.libgbm.lua new file mode 100644 index 00000000..d9bea09e --- /dev/null +++ b/pkgs/c/compat.libgbm.lua @@ -0,0 +1,569 @@ +-- 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, and the second is the decisive +-- one. +-- +-- 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. Vendoring it means forking Mesa's internals. +-- +-- Contrast `compat.vulkan`, which DOES build the Khronos Vulkan-Loader from +-- source. That is not the same situation: 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. +-- +-- 2. IN THIS ECOSYSTEM, MESA ALREADY HAS AN OWNER: `xim:mesa`. 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 to shrink nothing. +-- +-- The measured surface of the binding, by contrast: +-- +-- host 0 (see below — this is the whole point) +-- 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,…}. Mesa's +-- build already placed libdrm/expat/LLVM in the +-- ecosystem; nothing is asked of us or of the host. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ZERO HOST. NOT "HOST, CONVERGED" — ZERO. +-- +-- This package looks at `system.subos_sysrootdir()` and NOWHERE else. It has +-- no `/usr/lib*` candidate directory and, deliberately, NO escape-hatch +-- environment variable. That is a stricter rule 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 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)." +-- +-- 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: THE BACKEND IS UNREACHABLE IN THE SANDBOX +-- +-- Harvesting libgbm.so and gbm.h is the easy half and would produce a package +-- you can link and cannot use. libgbm is a LOADER: every gbm_create_device() +-- dlopens `/_gbm.so`. The path compiled into Mesa is +-- `/usr/lib/gbm` (`gbmbackendspath` in its gbm.pc), 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: No such file or directory +-- (search paths /usr/lib/gbm, suffix _gbm) +-- +-- `xim:mesa`'s config() declares `lib` into `/lib` and `include` into +-- `/usr/include`, so libgbm.so and gbm.h both reach the view — but +-- `lib/gbm/` is a SUBDIRECTORY and does not. Closing that is this package's +-- real content, and it is why the shape is not a copy of compat.glx-runtime. +-- +-- 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 got loaded: +-- +-- mcpp_generated/libgbm/lib/libgbm.so{,.1,.1.0.0} -> /lib/* +-- mcpp_generated/libgbm/lib/gbm/dri_gbm.so -> mesa payload +-- +-- so `mcpp_gbm_use_sibling_backends()` below resolves it at RUNTIME: +-- dlsym(RTLD_DEFAULT) a gbm symbol, dladdr it, take the directory, append +-- "/gbm". Verified that dladdr reports the FARM path rather than 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. +-- +-- The alternative was to bake an absolute path into a generated header at +-- install time. That works and it pins the package to whichever mesa payload +-- existed on the day it was installed, which is precisely the failure +-- 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 no absolute path anywhere in the descriptor. +-- +-- WHY THE FARM CARRIES THE UNVERSIONED `libgbm.so`, when compat.vulkan-runtime +-- is emphatic that its farm must hold versioned sonames only. That rule exists +-- because `runtime.library_dirs` joins the LINK line too, so a bare +-- `libxcb.so` there would shadow this index's own compat.xcb. Here the +-- unversioned name is exactly what is wanted — it is how `-lgbm` resolves — +-- and there is nothing to shadow, because no other package in this index +-- provides gbm. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- 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. That is why the test +-- member asserts the backend is PRESENT at the derived path rather than that +-- it loads — an assertion that is meaningful on a CI runner with no GPU, and +-- that does not go green by accident when the stack is broken. +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 you get 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 is used. The payload is what install() + -- builds out of the subos view, so this is only a stable, + -- well-formed anchor for the xpm entry — the trick + -- compat.glx-runtime plays with an OpenGL-Registry README. + -- + -- Mesa's own gbm.h at the tag the ecosystem's mesa ships, so + -- the anchor at least records which header this package was + -- written against. A raw file at a tag is byte-stable, unlike + -- a GitLab-generated archive; sha256 confirmed twice. + url = { + GLOBAL = "https://gitlab.freedesktop.org/mesa/mesa/-/raw/mesa-25.0.7/src/gbm/main/gbm.h", + CN = "https://gitcode.com/mcpp-res/libgbm/releases/download/2026.08.29/libgbm-2026.08.29.h", + }, + sha256 = "95f3b4a6ee5175c7cc5d47368d4efb100063fe49e5a6f5b19030ac2ceed73b81", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + -- Both built by install(): gbm.h symlinked out of the subos view (so + -- header and library can never be from different Mesa builds) and + -- mcpp_gbm.h, this package's own two declarations. + include_dirs = { "mcpp_generated/libgbm/include" }, + + -- dladdr and RTLD_DEFAULT live behind __USE_GNU. `c_standard = + -- "gnu11"` is the spelling that looks right and is a trap — mcpp + -- 2026.8.27.2 accepts the string and still emits `-std=c11` + -- (compat.libaio's header comment records the same finding), so the + -- define is what actually takes effect. + cflags = { "-D_GNU_SOURCE" }, + + sources = { "mcpp_generated/gbm_backends.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. It resolves through the farm, + -- which `runtime.library_dirs` puts on the link line as well as the + -- runtime path. `-ldl` is belt-and-braces: glibc >= 2.34 folds libdl + -- into libc and xim-x-glibc is 2.39, but the flag costs nothing. + ldflags = { "-lgbm", "-ldl" }, + + -- Zero. gbm.h includes and ; the generated TU + -- includes , and . + deps = {}, + + runtime = { + -- THREE DIRECTORY KEYS, THREE DIFFERENT LINKER 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 + -- transitive_needed_dirs-> -Wl,-rpath-link + -- + -- `library_dirs` ALONE is what compat.glx-runtime and + -- compat.vulkan-runtime declare, and it is right for them: they + -- exist so a bare-soname `dlopen` resolves at RUN time, and + -- nothing links against their farms. This package does link + -- against its farm, so it needs the `-L` too — with only + -- `library_dirs` the farm is complete, the rpath is correct, and + -- the build still dies at `ld: cannot find -lgbm`. + -- + -- (The docs used to say "library_dirs also joins the link line", + -- which mcpp#304 observed for real. The separate `-L` key landed + -- in 2026.8.10.3 and the pinned mcpp no longer behaves that way, + -- so the claim is version-dependent; docs/package-types.md now + -- carries the table above rather than the bare assertion.) + 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. + }, + + generated_files = { + -- Resolves GBM_BACKENDS_PATH from whichever libgbm is actually + -- loaded. See the header comment for why this is derived at + -- runtime instead of baked in at install time. + -- + -- Declared here AND written by install(); install() does not wipe + -- the payload, so the two never race — this entry is what + -- guarantees the TU exists whatever order the two run in. + ["mcpp_generated/gbm_backends.c"] = +[[ +/* compat.libgbm — locate the GBM backend directory without pinning a path. + * + * Mesa compiles `/usr/lib/gbm` in as its default backend search path, which + * does not exist inside an mcpp sandbox. This package's farm instead places + * the backends in `gbm/` NEXT TO the libgbm it ships, so the directory can be + * derived from the loaded library itself and no absolute path is ever stored. + */ +#define _GNU_SOURCE +#include +#include +#include + +static char mcpp_gbm_dir_buf[4096]; + +const char *mcpp_gbm_backends_dir(void) +{ + Dl_info info; + const char *slash; + void *sym; + size_t n; + + if (mcpp_gbm_dir_buf[0] != '\0') + return mcpp_gbm_dir_buf; + + /* RTLD_DEFAULT rather than &gbm_format_get_name: the address of an + imported function is this object's own PLT stub, and dladdr would + report the CONSUMER instead of libgbm. */ + sym = dlsym(RTLD_DEFAULT, "gbm_format_get_name"); + if (sym == NULL) + return NULL; + + if (dladdr(sym, &info) == 0 || info.dli_fname == NULL) + return NULL; + + slash = strrchr(info.dli_fname, '/'); + if (slash == NULL) + return NULL; + + n = (size_t)(slash - info.dli_fname); + if (n + sizeof("/gbm") > sizeof(mcpp_gbm_dir_buf)) + return NULL; + + memcpy(mcpp_gbm_dir_buf, info.dli_fname, n); + memcpy(mcpp_gbm_dir_buf + n, "/gbm", sizeof("/gbm")); + return mcpp_gbm_dir_buf; +} + +int mcpp_gbm_use_sibling_backends(void) +{ + const char *dir; + + /* An explicit GBM_BACKENDS_PATH is the caller's decision and is left + alone -- this is a default, not an override. */ + if (getenv("GBM_BACKENDS_PATH") != NULL) + return 1; + + dir = mcpp_gbm_backends_dir(); + if (dir == NULL) + return 0; + + return setenv("GBM_BACKENDS_PATH", dir, 1) == 0; +} +]], + }, + }, +} + +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 + +-- Where mesa's own lib directory is, derived from the view rather than named. +-- `/lib/libgbm.so.1` is a symlink into the xim-x-mesa payload, so its +-- realpath gives the payload's lib dir and with it `lib/gbm/`, which the view +-- does not carry. Doing it this way keeps the descriptor free of any mesa +-- version. os.exec's return value is not trustworthy, so the answer is taken +-- from the file it writes. +local function mesa_libdir(prefix, view_lib) + local probe = path.join(prefix, "mcpp_libgbm_realpath.txt") + os.exec("readlink -f " .. sh_quote(path.join(view_lib, "libgbm.so.1")) .. + " > " .. sh_quote(probe) .. " 2>/dev/null || true") + + local real = io.readfile(probe) + if real == nil then + return nil + end + real = real:gsub("%s+$", "") + if real == "" then + return nil + end + + local slash = real:match("^(.*)/[^/]*$") + return slash +end + +local consumer_header = [[ +#ifndef MCPP_COMPAT_LIBGBM_H +#define MCPP_COMPAT_LIBGBM_H + +/* compat.libgbm — Mesa's gbm.h plus the two helpers this package adds. + * + * Mesa's compiled-in backend search path (/usr/lib/gbm) does not exist inside + * an mcpp sandbox, so gbm_create_device() would find no backend. Call + * mcpp_gbm_use_sibling_backends() once before creating a device; it points + * GBM_BACKENDS_PATH at the backends shipped beside this package's libgbm, and + * leaves an explicitly set GBM_BACKENDS_PATH alone. + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* The backend directory, derived from the loaded libgbm. NULL if libgbm is not + in this process. Does not test whether the directory exists. */ +const char *mcpp_gbm_backends_dir(void); + +/* Set GBM_BACKENDS_PATH to that directory unless it is already set. + Returns non-zero on success. */ +int mcpp_gbm_use_sibling_backends(void); + +#ifdef __cplusplus +} +#endif + +#endif /* MCPP_COMPAT_LIBGBM_H */ +]] + +-- Kept identical to the generated_files entry above; install() does not wipe +-- the payload, so whichever of the two lands second writes the same bytes. +local backends_tu = [[ +/* compat.libgbm — locate the GBM backend directory without pinning a path. + * + * Mesa compiles `/usr/lib/gbm` in as its default backend search path, which + * does not exist inside an mcpp sandbox. This package's farm instead places + * the backends in `gbm/` NEXT TO the libgbm it ships, so the directory can be + * derived from the loaded library itself and no absolute path is ever stored. + */ +#define _GNU_SOURCE +#include +#include +#include + +static char mcpp_gbm_dir_buf[4096]; + +const char *mcpp_gbm_backends_dir(void) +{ + Dl_info info; + const char *slash; + void *sym; + size_t n; + + if (mcpp_gbm_dir_buf[0] != '\0') + return mcpp_gbm_dir_buf; + + /* RTLD_DEFAULT rather than &gbm_format_get_name: the address of an + imported function is this object's own PLT stub, and dladdr would + report the CONSUMER instead of libgbm. */ + sym = dlsym(RTLD_DEFAULT, "gbm_format_get_name"); + if (sym == NULL) + return NULL; + + if (dladdr(sym, &info) == 0 || info.dli_fname == NULL) + return NULL; + + slash = strrchr(info.dli_fname, '/'); + if (slash == NULL) + return NULL; + + n = (size_t)(slash - info.dli_fname); + if (n + sizeof("/gbm") > sizeof(mcpp_gbm_dir_buf)) + return NULL; + + memcpy(mcpp_gbm_dir_buf, info.dli_fname, n); + memcpy(mcpp_gbm_dir_buf + n, "/gbm", sizeof("/gbm")); + return mcpp_gbm_dir_buf; +} + +int mcpp_gbm_use_sibling_backends(void) +{ + const char *dir; + + /* An explicit GBM_BACKENDS_PATH is the caller's decision and is left + alone -- this is a default, not an override. */ + if (getenv("GBM_BACKENDS_PATH") != NULL) + return 1; + + dir = mcpp_gbm_backends_dir(); + if (dir == NULL) + return 0; + + return setenv("GBM_BACKENDS_PATH", dir, 1) == 0; +} +]] + +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 generated = path.join(prefix, "mcpp_generated") + local root = path.join(generated, "libgbm") + local out_lib = path.join(root, "lib") + local out_inc = path.join(root, "include") + local out_bk = path.join(out_lib, "gbm") + + os.mkdir(generated) + os.mkdir(out_lib) + os.mkdir(out_inc) + os.mkdir(out_bk) + + say("writing mcpp_generated/gbm_backends.c") + io.writefile(path.join(generated, "gbm_backends.c"), backends_tu) + + say("writing include/mcpp_gbm.h") + io.writefile(path.join(out_inc, "mcpp_gbm.h"), consumer_header) + + -- 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") + + -- 3. The backends. NOT required: a Mesa built without the dri backend is a + -- legitimate configuration, and so is a machine that will only ever use + -- the pure-function half of the API. The test member reports what it + -- finds rather than assuming. + local mesa_lib = mesa_libdir(prefix, view_lib) + if mesa_lib == nil then + say("NOTE: could not resolve the mesa payload lib dir; no backends linked") + return true + end + + local src_bk = path.join(mesa_lib, "gbm") + say("mesa payload lib dir: " .. mesa_lib) + if not os.isdir(src_bk) then + say("NOTE: " .. src_bk .. " does not exist; no backends linked") + return true + end + + say("linking *_gbm.so from " .. src_bk) + link_matching(src_bk, "*_gbm.so", out_bk) + say("done") + + return true +end diff --git a/tests/examples/libgbm/mcpp.toml b/tests/examples/libgbm/mcpp.toml new file mode 100644 index 00000000..9a77079f --- /dev/null +++ b/tests/examples/libgbm/mcpp.toml @@ -0,0 +1,25 @@ +# 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 has to prove is not "does -lgbm resolve". compat.libgbm is a +# BINDING onto the ecosystem's Mesa, and it can be wrong in a way that still +# links: +# +# * the header could come from a different Mesa than the library; +# * the backend directory could be unreachable, which is the state every +# mcpp sandbox is in by default (Mesa compiles in /usr/lib/gbm, which does +# not exist there) and which no link check would notice. +# +# So the assertions cover the pure-function half of the API, which needs no GPU +# and no device, and then check that the directory libgbm will actually search +# exists and holds a backend. That second one is the interesting assertion: +# it is meaningful on a CI runner with no /dev/dri at all. +[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..8067a1c7 --- /dev/null +++ b/tests/examples/libgbm/tests/gbm.cpp @@ -0,0 +1,202 @@ +// 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. That is the +// default state of every mcpp sandbox: Mesa compiles `/usr/lib/gbm` in as +// its backend search path and that directory does not exist there, so +// gbm_create_device() finds nothing. 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) +// +// A link check cannot see this, and neither can any assertion that needs +// a GPU. The backend-reachability check below is the one that can: it +// asserts the directory libgbm will actually search exists and holds a +// backend, which is checkable on a CI runner with no /dev/dri. +// +// 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__ + +#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 backend is REACHABLE ────────────────────────────────────── + // The assertion this package exists for, and it needs no GPU. + const char *dir = mcpp_gbm_backends_dir(); + check(dir != nullptr, "mcpp_gbm_backends_dir() resolves libgbm's location"); + + if (dir != nullptr) { + std::printf(" backends dir: %s\n", dir); + + struct ::stat st {}; + check(::stat(dir, &st) == 0 && S_ISDIR(st.st_mode), + "the derived backend directory exists"); + check(dir_has_backend(dir), + "it contains at least one *_gbm.so backend"); + } + + check(mcpp_gbm_use_sibling_backends() != 0, + "mcpp_gbm_use_sibling_backends() succeeds"); + + const char *env = std::getenv("GBM_BACKENDS_PATH"); + check(env != nullptr && dir != nullptr && std::strcmp(env, dir) == 0, + "GBM_BACKENDS_PATH now names that directory"); + + // An already-set value is the caller's decision and must be left alone. + ::setenv("GBM_BACKENDS_PATH", "/nonexistent/chosen/by/caller", 1); + mcpp_gbm_use_sibling_backends(); + const char *kept = std::getenv("GBM_BACKENDS_PATH"); + check(kept != nullptr && std::strcmp(kept, "/nonexistent/chosen/by/caller") == 0, + "an explicit GBM_BACKENDS_PATH is not overwritten"); + ::unsetenv("GBM_BACKENDS_PATH"); + + // ── 5. A real device, opt-in ───────────────────────────────────────── + if (std::getenv("MCPP_RUN_GBM_DEVICE") != nullptr) { + mcpp_gbm_use_sibling_backends(); + + 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 From 3e42e5a5e04019ff7d5d719d4c26bf68c38b99ec Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 23:38:50 +0800 Subject: [PATCH 02/19] =?UTF-8?q?fix(libgbm):=20make=20the=20backend=20rep?= =?UTF-8?q?air=20invisible=20=E2=80=94=20stock=20=20is=20the=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first cut exposed `mcpp_gbm.h` and asked the consumer to call `mcpp_gbm_use_sibling_backends()` before creating a device. That changed the ordinary way to use libgbm, and it was not only a style problem: it does not work for the consumers that matter. libgbm is mostly called from INSIDE other libraries — SDL2's KMSDRM backend, wlroots, ffmpeg's VAAPI hwcontext all call gbm_create_device() from their own sources. None of them will ever call a helper of ours, so an opt-in repair leaves exactly those callers as broken as they were, while the package's own tests go green. The API is now stock `#include ` and nothing else. GBM_BACKENDS_PATH is wired from a CONSTRUCTOR in the package's own TU (priority 101, ahead of default-priority constructors in case one creates a device). An inherited value is still left alone — this is a default, not an override. That is reliable because a dependency's objects enter the consumer's link eagerly rather than being lazily selected; the emitted build.ninja names the object on the link line directly, so the constructor cannot be dropped. It is also what every other ecosystem does, and none of them use an API: distros (Debian libgbm1, Fedora mesa-libgbm) split libgbm out of the mesa SOURCE package so the compiled-in $libdir/gbm is right by construction; relocated and sandboxed stacks set the environment variable instead — Valve's pressure-vessel hit this exact bug when mesa 24.3 split the backends out (steam-runtime#797) and answers with GBM_BACKENDS_PATH, as do Nix, Conda and AppImage at activation time; and Mesa offers -Dgbm-backends-path= for packagers who control the build. This package is in the sandboxed case and cannot set a container-wide environment, so the constructor is the in-process equivalent. Longer term the wiring belongs in xim:mesa (build with -Dgbm-backends-path=, or declare lib/gbm/ into the view as it already declares lib and include), and then this package would carry no constructor at all; noted in the design doc. mcpp_gbm.h stays, demoted to optional introspection for diagnostics and for the tests, and says so in its own first line. Tests now come in two binaries, and the split is the regression guard for this very mistake: * tests/stock_usage.cpp includes STOCK and nothing else — no mcpp_gbm.h, no helper declaration. If the repair ever goes back to being opt-in, this fails while the fuller gbm.cpp could still pass. * tests/gbm.cpp reads GBM_BACKENDS_PATH before calling anything at all, and re-execs itself with the variable preset to prove an inherited value survives the constructor — the only way to observe that rule, since by the time main runs the constructor is finished. Also drops the generated_files copy of the TU: install() is the only writer, and the parser takes literals only so the two could not share one source. Verified cold with the CI-pinned mcpp: 2 passed, 0 failed; both binaries confirmed failable by removing dri_gbm.so from the farm. --- .agents/docs/2026-08-29-add-libgbm-plan.md | 80 +++++++- docs/descriptor-examples.md | 2 +- docs/zh/descriptor-examples.md | 2 +- pkgs/c/compat.libgbm.lua | 199 +++++++++++--------- tests/examples/libgbm/mcpp.toml | 25 ++- tests/examples/libgbm/tests/gbm.cpp | 97 ++++++---- tests/examples/libgbm/tests/stock_usage.cpp | 78 ++++++++ 7 files changed, 343 insertions(+), 140 deletions(-) create mode 100644 tests/examples/libgbm/tests/stock_usage.cpp diff --git a/.agents/docs/2026-08-29-add-libgbm-plan.md b/.agents/docs/2026-08-29-add-libgbm-plan.md index 0809d0e9..20888304 100644 --- a/.agents/docs/2026-08-29-add-libgbm-plan.md +++ b/.agents/docs/2026-08-29-add-libgbm-plan.md @@ -104,6 +104,51 @@ MESA-LOADER: failed to open dri: /usr/lib/gbm/dri_gbm.so: cannot open shared obj 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: @@ -167,7 +212,21 @@ would come down to search order. ## Test member `tests/examples/libgbm`, linux-gated, no-op `main()` elsewhere (the `compat.libaio` / -`compat.wil` pattern). Nineteen checks, all runnable with no GPU: +`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`. @@ -179,7 +238,8 @@ would come down to search order. - `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 set from the derived path, and an explicitly set one is left alone. +- `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`. @@ -193,16 +253,26 @@ package's build-cache entry: Compiling compat.libgbm v2026.08.29 Compiling gbm (test) Running bin/gbm -… 19 checks … +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 - test result ok. 1 passed; 0 failed; finished in 23.61s +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 the binary exits 1. + 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 diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index 9d0faf95..6fdaf973 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -31,7 +31,7 @@ in the [root README](../README.md#reference-examples). | 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. 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-*`. **The part that is real work**: libgbm is a loader, and Mesa compiles `/usr/lib/gbm` in as its backend search path, which does not exist in the sandbox, so `gbm_create_device` finds nothing and no link check notices. `xim:mesa` declares `lib` into the view but `lib/gbm/` is a subdirectory and does not follow. So install() also harvests the backends, laid out as a **sibling** of the farm's libgbm, and a generated TU derives the path at runtime — `dlsym(RTLD_DEFAULT)` a gbm symbol, `dladdr` it, append `/gbm` — which reports the farm path rather than the realpath and therefore pins nothing; baking an absolute path into a generated header instead would fix the package to whichever mesa payload existed on install day. This is also the package that needs **both** directory keys: `library_dirs` for the RUNPATH and `link_library_dirs` for the `-L` that lets `-lgbm` resolve at all. Its test asserts the backend is *present at the derived path* rather than that it loads, which is checkable on a runner with no `/dev/dri` — and stays honest on a host where the stack's own mesa/glibc skew makes the dlopen fail) | +| 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-*`. **The part that is real work**: libgbm is a loader, and Mesa compiles `/usr/lib/gbm` in as its backend search path, which does not exist in the sandbox, so `gbm_create_device` finds nothing and no link check notices. `xim:mesa` declares `lib` into the view but `lib/gbm/` is a subdirectory and does not follow. So install() also harvests the backends, laid out as a **sibling** of the farm's libgbm, and a generated TU wires `GBM_BACKENDS_PATH` from a **constructor** — `dlsym(RTLD_DEFAULT)` a gbm symbol, `dladdr` it, append `/gbm` — which reports the farm path rather than the realpath and therefore pins nothing (baking an absolute path into a generated header instead would fix the package to whichever mesa payload existed on install day). **The repair has to be invisible, and that is the load-bearing design decision**: the API stays stock `#include `, because libgbm is mostly called from *inside* other libraries (SDL2's KMSDRM backend, wlroots, ffmpeg's VAAPI hwcontext) which will never call a helper of ours — an opt-in fix would leave exactly those consumers broken. It works because mcpp links a dependency's objects eagerly, so the constructor cannot be dropped. This is also what every other ecosystem does: distros split `libgbm1` out of the mesa source package and rely on the compiled-in `$libdir/gbm`, while relocated stacks set the environment variable (Valve's pressure-vessel hit this exact bug in steam-runtime#797; Nix and Conda do it at activation) and Mesa offers `-Dgbm-backends-path=` for packagers who control the build — never an API. Longer term this belongs in `xim:mesa` itself. This is also the package that needs **both** directory keys: `library_dirs` for the RUNPATH and `link_library_dirs` for the `-L` that lets `-lgbm` resolve at all. It ships **two** test binaries, and the split is the point: `stock_usage.cpp` includes stock `` and nothing else, so a regression back to an opt-in fix fails there while the fuller `gbm.cpp` could still pass. Both assert the backend is *present at the derived path* rather than that it loads, which is checkable on a runner with no `/dev/dri` — and stays honest on a host where the stack's own mesa/glibc skew makes the dlopen fail) | | 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/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index 9474c2cc..40845718 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -29,7 +29,7 @@ | 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,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,而 Mesa 把 `/usr/lib/gbm` 编译进去当后端搜索路径,沙箱里没有这个目录,于是 `gbm_create_device` 找不到后端,而且任何链接检查都发现不了。`xim:mesa` 把 `lib` 声明进 view,但 `lib/gbm/` 是子目录,不会跟着进去。所以 install() 把后端也收进来,并且放成 farm 里 libgbm 的**同级**目录,再由一个生成的 TU 在运行时推导路径 —— `dlsym(RTLD_DEFAULT)` 取一个 gbm 符号、`dladdr`、拼上 `/gbm` —— 它报的是 farm 路径而非 realpath,因此**什么都不固定**;换成在 install 时把绝对路径烧进生成头,包就被钉死在安装当天那个 mesa payload 上。这也是唯一**两个**目录键都要的包:`library_dirs` 给 RUNPATH,`link_library_dirs` 给那个让 `-lgbm` 能解析的 `-L`。它的测试断言后端**存在于推导出的路径上**,而不是断言它能加载 —— 前者在没有 `/dev/dri` 的 runner 上可验证,并且在生态自身 mesa/glibc 错位导致 dlopen 失败的机器上也不会说谎) | +| 生态栈绑定(零 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,而 Mesa 把 `/usr/lib/gbm` 编译进去当后端搜索路径,沙箱里没有这个目录,于是 `gbm_create_device` 找不到后端,而且任何链接检查都发现不了。`xim:mesa` 把 `lib` 声明进 view,但 `lib/gbm/` 是子目录,不会跟着进去。所以 install() 把后端也收进来,并且放成 farm 里 libgbm 的**同级**目录,再由一个生成的 TU 在**构造函数**里设好 `GBM_BACKENDS_PATH` —— `dlsym(RTLD_DEFAULT)` 取一个 gbm 符号、`dladdr`、拼上 `/gbm` —— 它报的是 farm 路径而非 realpath,因此**什么都不固定**(换成在 install 时把绝对路径烧进生成头,包就被钉死在安装当天那个 mesa payload 上)。**修复必须是隐形的,这是决定性的设计约束**:API 保持原样的 `#include `,因为 libgbm 绝大多数是被**别的库内部**调用的(SDL2 的 KMSDRM 后端、wlroots、ffmpeg 的 VAAPI hwcontext),它们永远不会调用我们的 helper —— 需要消费者主动 opt-in 的方案,恰好把最重要的那批消费者留在坏掉的状态。它能成立是因为 mcpp 把依赖的对象**急切全量入链**,构造函数不可能被丢掉。这也正是其他生态的做法:发行版把 `libgbm1` 从 mesa 源码包里拆成二进制包、依赖编译进去的 `$libdir/gbm`;而被重定位的栈改设环境变量(Valve 的 pressure-vessel 正是踩到这个 bug —— steam-runtime#797,Nix/Conda 在环境激活时设),Mesa 还给能控制构建的打包者留了 `-Dgbm-backends-path=` —— **没有一个是用 API 解决的**。更长远看这件事该落在 `xim:mesa` 自己身上。这也是唯一**两个**目录键都要的包:`library_dirs` 给 RUNPATH,`link_library_dirs` 给那个让 `-lgbm` 能解析的 `-L`。它带**两个**测试二进制,拆分本身就是重点:`stock_usage.cpp` 只 include 原样的 ``、别的什么都不include,一旦退化回「要消费者 opt-in」的方案,它会失败而内容更全的 `gbm.cpp` 仍可能通过。两者都断言后端**存在于推导出的路径上**,而不是断言它能加载 —— 前者在没有 `/dev/dri` 的 runner 上可验证,并且在生态自身 mesa/glibc 错位导致 dlopen 失败的机器上也不会说谎) | | 恒开的 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/pkgs/c/compat.libgbm.lua b/pkgs/c/compat.libgbm.lua index d9bea09e..bb35ee8a 100644 --- a/pkgs/c/compat.libgbm.lua +++ b/pkgs/c/compat.libgbm.lua @@ -89,17 +89,46 @@ -- `lib/gbm/` is a SUBDIRECTORY and does not. Closing that is this package's -- real content, and it is why the shape is not a copy of compat.glx-runtime. -- +-- THE FIX MUST BE INVISIBLE. This is the constraint that decides the design, +-- and getting it wrong produces a package that passes its own tests and fails +-- its real consumers. +-- +-- A libgbm consumer writes `#include ` and calls `gbm_create_device()`. +-- That is the whole API, and it has to keep working unchanged — because the +-- consumers that matter most are not the ones reading this file. SDL2's +-- KMSDRM backend, wlroots and ffmpeg's VAAPI hwcontext all call +-- gbm_create_device() from INSIDE a third-party library. Any scheme that +-- requires the application to call a helper first is unreachable for exactly +-- those callers, and would leave them as broken as they were. +-- +-- So the package exposes stock `gbm.h` and wires the backend path from a +-- CONSTRUCTOR in its own TU (mcpp_generated/gbm_backends.c). By the time any +-- code runs, GBM_BACKENDS_PATH is already set; nothing has to be included, +-- called or known about. Verified with a consumer compiled against gbm.h alone +-- and linked with no knowledge of this package. +-- +-- What makes that reliable is a property of mcpp that is usually a nuisance: a +-- dependency's objects enter the consumer's link EAGERLY, all of them, rather +-- than being lazily selected the way an archive member would be. Confirmed in +-- the emitted build.ninja, which names our object on the link line directly: +-- +-- build bin/gbm : cxx_link obj/gbm.o obj/compat_libgbm/…/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. +-- -- 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 got loaded: -- -- mcpp_generated/libgbm/lib/libgbm.so{,.1,.1.0.0} -> /lib/* -- mcpp_generated/libgbm/lib/gbm/dri_gbm.so -> mesa payload -- --- so `mcpp_gbm_use_sibling_backends()` below resolves it at RUNTIME: --- dlsym(RTLD_DEFAULT) a gbm symbol, dladdr it, take the directory, append --- "/gbm". Verified that dladdr reports the FARM path rather than 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. +-- and the constructor resolves it at RUNTIME: dlsym(RTLD_DEFAULT) a gbm +-- symbol, dladdr it, take the directory, append "/gbm". Verified that dladdr +-- reports the FARM path rather than 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. -- -- The alternative was to bake an absolute path into a generated header at -- install time. That works and it pins the package to whichever mesa payload @@ -108,6 +137,32 @@ -- version … and stops resolving the day it is upgraded"). Runtime derivation -- has no such cost and no absolute path anywhere in the descriptor. -- +-- An already-set GBM_BACKENDS_PATH is left alone: this is a default, not an +-- override, and a user who has pointed it somewhere deliberately outranks us. +-- +-- THIS IS WHAT EVERY OTHER ECOSYSTEM DOES, and none of them do it with an API. +-- Distributions (Debian's libgbm1/libgbm-dev, Fedora's mesa-libgbm) split +-- libgbm out of the mesa SOURCE package and never touch the path, because one +-- system-wide prefix makes Mesa's compiled-in `$libdir/gbm` correct by +-- construction. Relocated and sandboxed stacks cannot rely on that and set the +-- environment variable instead — Valve's pressure-vessel hit precisely this +-- bug when Mesa 24.3 split the backends out (steam-runtime#797) and answers +-- with GBM_BACKENDS_PATH=/run/host/usr/lib64/gbm; Nix, Conda and AppImage do +-- the same at environment-activation time. Mesa itself offers the third route, +-- `-Dgbm-backends-path=`, for packagers who control the build. +-- +-- This package is in the sandboxed case and cannot set a container-wide +-- environment, so the constructor is the in-process equivalent: same effect, +-- same variable, same "don't override an explicit value" rule, scoped to +-- processes that actually link libgbm. +-- +-- WHERE THIS REALLY BELONGS. Long term 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; until then the wiring lives here, where it +-- can at least be tested. +-- -- WHY THE FARM CARRIES THE UNVERSIONED `libgbm.so`, when compat.vulkan-runtime -- is emphatic that its farm must hold versioned sonames only. That rule exists -- because `runtime.library_dirs` joins the LINK line too, so a bare @@ -235,80 +290,10 @@ package = { -- ignored would document a guarantee this package cannot make. }, - generated_files = { - -- Resolves GBM_BACKENDS_PATH from whichever libgbm is actually - -- loaded. See the header comment for why this is derived at - -- runtime instead of baked in at install time. - -- - -- Declared here AND written by install(); install() does not wipe - -- the payload, so the two never race — this entry is what - -- guarantees the TU exists whatever order the two run in. - ["mcpp_generated/gbm_backends.c"] = -[[ -/* compat.libgbm — locate the GBM backend directory without pinning a path. - * - * Mesa compiles `/usr/lib/gbm` in as its default backend search path, which - * does not exist inside an mcpp sandbox. This package's farm instead places - * the backends in `gbm/` NEXT TO the libgbm it ships, so the directory can be - * derived from the loaded library itself and no absolute path is ever stored. - */ -#define _GNU_SOURCE -#include -#include -#include - -static char mcpp_gbm_dir_buf[4096]; - -const char *mcpp_gbm_backends_dir(void) -{ - Dl_info info; - const char *slash; - void *sym; - size_t n; - - if (mcpp_gbm_dir_buf[0] != '\0') - return mcpp_gbm_dir_buf; - - /* RTLD_DEFAULT rather than &gbm_format_get_name: the address of an - imported function is this object's own PLT stub, and dladdr would - report the CONSUMER instead of libgbm. */ - sym = dlsym(RTLD_DEFAULT, "gbm_format_get_name"); - if (sym == NULL) - return NULL; - - if (dladdr(sym, &info) == 0 || info.dli_fname == NULL) - return NULL; - - slash = strrchr(info.dli_fname, '/'); - if (slash == NULL) - return NULL; - - n = (size_t)(slash - info.dli_fname); - if (n + sizeof("/gbm") > sizeof(mcpp_gbm_dir_buf)) - return NULL; - - memcpy(mcpp_gbm_dir_buf, info.dli_fname, n); - memcpy(mcpp_gbm_dir_buf + n, "/gbm", sizeof("/gbm")); - return mcpp_gbm_dir_buf; -} - -int mcpp_gbm_use_sibling_backends(void) -{ - const char *dir; - - /* An explicit GBM_BACKENDS_PATH is the caller's decision and is left - alone -- this is a default, not an override. */ - if (getenv("GBM_BACKENDS_PATH") != NULL) - return 1; - - dir = mcpp_gbm_backends_dir(); - if (dir == NULL) - return 0; - - return setenv("GBM_BACKENDS_PATH", dir, 1) == 0; -} -]], - }, + -- No `generated_files`. The one TU and the two headers are written by + -- install() below, which is the only writer — a `generated_files` copy + -- of the same C source would be a second copy to keep in sync, and the + -- parser takes only literals so it could not share one. }, } @@ -379,13 +364,23 @@ local consumer_header = [[ #ifndef MCPP_COMPAT_LIBGBM_H #define MCPP_COMPAT_LIBGBM_H -/* compat.libgbm — Mesa's gbm.h plus the two helpers this package adds. +/* compat.libgbm -- OPTIONAL. You do not need this header. + * + * The way to use this package is the way you would use libgbm anywhere else: + * + * #include + * struct gbm_device *dev = gbm_create_device(fd); * * Mesa's compiled-in backend search path (/usr/lib/gbm) does not exist inside - * an mcpp sandbox, so gbm_create_device() would find no backend. Call - * mcpp_gbm_use_sibling_backends() once before creating a device; it points - * GBM_BACKENDS_PATH at the backends shipped beside this package's libgbm, and - * leaves an explicitly set GBM_BACKENDS_PATH alone. + * an mcpp sandbox, but the package repairs that from a constructor in its own + * translation unit, before any of your code runs. Nothing has to be called and + * nothing has to be included -- which is the point: libgbm is mostly called + * from INSIDE other libraries (SDL2's KMSDRM backend, wlroots, ffmpeg's VAAPI + * hwcontext), and those will never call a helper of ours. + * + * What is below is introspection for diagnostics and for this package's own + * tests. Reach for it when you want to report which backend directory was + * chosen, or to override the choice explicitly. */ #include @@ -398,8 +393,9 @@ extern "C" { in this process. Does not test whether the directory exists. */ const char *mcpp_gbm_backends_dir(void); -/* Set GBM_BACKENDS_PATH to that directory unless it is already set. - Returns non-zero on success. */ +/* Point GBM_BACKENDS_PATH at that directory unless it is already set. The + constructor has already done this; calling it again is harmless. Returns + non-zero on success. */ int mcpp_gbm_use_sibling_backends(void); #ifdef __cplusplus @@ -412,12 +408,24 @@ int mcpp_gbm_use_sibling_backends(void); -- Kept identical to the generated_files entry above; install() does not wipe -- the payload, so whichever of the two lands second writes the same bytes. local backends_tu = [[ -/* compat.libgbm — locate the GBM backend directory without pinning a path. +/* compat.libgbm -- point GBM_BACKENDS_PATH at the backends shipped beside this + * package's libgbm, automatically and before anything else runs. + * + * WHY A CONSTRUCTOR. Mesa compiles `/usr/lib/gbm` in as its backend search + * path and that directory does not exist inside an mcpp sandbox, so + * gbm_create_device() finds nothing. Repairing it through a function the + * application must call would not work: libgbm is mostly called from inside + * OTHER libraries -- SDL2's KMSDRM backend, wlroots, ffmpeg's VAAPI hwcontext + * -- and none of them will ever call ours. A constructor reaches all of them, + * and keeps `#include ` the whole of the API. * - * Mesa compiles `/usr/lib/gbm` in as its default backend search path, which - * does not exist inside an mcpp sandbox. This package's farm instead places - * the backends in `gbm/` NEXT TO the libgbm it ships, so the directory can be - * derived from the loaded library itself and no absolute path is ever stored. + * Priority 101 is the first value not reserved for the implementation, so this + * runs ahead of default-priority constructors in case one creates a device. + * + * The path is derived, never stored: dlsym the loaded libgbm, dladdr it, and + * take the sibling `gbm/` directory. dladdr reports the path the loader used + * -- this package's farm -- rather than the realpath, so the answer stays + * correct without pinning a mesa version. */ #define _GNU_SOURCE #include @@ -464,7 +472,8 @@ int mcpp_gbm_use_sibling_backends(void) const char *dir; /* An explicit GBM_BACKENDS_PATH is the caller's decision and is left - alone -- this is a default, not an override. */ + alone -- this is a default, not an override. Same rule the sandboxed + stacks that set this variable follow (pressure-vessel, Nix, Conda). */ if (getenv("GBM_BACKENDS_PATH") != NULL) return 1; @@ -474,6 +483,12 @@ int mcpp_gbm_use_sibling_backends(void) return setenv("GBM_BACKENDS_PATH", dir, 1) == 0; } + +__attribute__((constructor(101))) +static void mcpp_gbm_wire_backends(void) +{ + mcpp_gbm_use_sibling_backends(); +} ]] function install() diff --git a/tests/examples/libgbm/mcpp.toml b/tests/examples/libgbm/mcpp.toml index 9a77079f..1670007b 100644 --- a/tests/examples/libgbm/mcpp.toml +++ b/tests/examples/libgbm/mcpp.toml @@ -5,18 +5,29 @@ # and the test compiles to a no-op main() elsewhere. # # What this member has to prove is not "does -lgbm resolve". compat.libgbm is a -# BINDING onto the ecosystem's Mesa, and it can be wrong in a way that still -# links: +# BINDING onto the ecosystem's Mesa, and it can be wrong in three ways that all +# still link: # # * the header could come from a different Mesa than the library; # * the backend directory could be unreachable, which is the state every # mcpp sandbox is in by default (Mesa compiles in /usr/lib/gbm, which does -# not exist there) and which no link check would notice. +# not exist there) and which no link check would notice; +# * the repair could require the CONSUMER to opt in, which would make it +# worthless — libgbm is mostly called from inside other libraries (SDL2's +# KMSDRM backend, wlroots, ffmpeg's VAAPI hwcontext) that will never call +# a helper of ours. # -# So the assertions cover the pure-function half of the API, which needs no GPU -# and no device, and then check that the directory libgbm will actually search -# exists and holds a backend. That second one is the interesting assertion: -# it is meaningful on a CI runner with no /dev/dri at all. +# Hence TWO test binaries, and the split is deliberate: +# +# tests/gbm.cpp the full surface, including the optional +# introspection header. +# tests/stock_usage.cpp includes STOCK and nothing else. If the +# backend path ever regresses to something the +# application has to ask for, this one fails while +# gbm.cpp could still pass. +# +# 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" diff --git a/tests/examples/libgbm/tests/gbm.cpp b/tests/examples/libgbm/tests/gbm.cpp index 8067a1c7..84eb09b2 100644 --- a/tests/examples/libgbm/tests/gbm.cpp +++ b/tests/examples/libgbm/tests/gbm.cpp @@ -16,10 +16,14 @@ // MESA-LOADER: failed to open dri: /usr/lib/gbm/dri_gbm.so: cannot // open shared object file (search paths /usr/lib/gbm, suffix _gbm) // -// A link check cannot see this, and neither can any assertion that needs -// a GPU. The backend-reachability check below is the one that can: it -// asserts the directory libgbm will actually search exists and holds a -// backend, which is checkable on a CI runner with no /dev/dri. +// 3. **The repair could require the consumer to opt in** — which would make +// it useless. This is what section 0 below exists to prevent, and it is +// the assertion most worth having: libgbm is mostly called from INSIDE +// other libraries (SDL2's KMSDRM backend, wlroots, ffmpeg's VAAPI +// hwcontext), and none of them will ever call a helper of ours. So the +// package must work for a consumer that writes `#include ` and +// nothing else. Section 0 asserts precisely that, by reading the +// environment before this program has called anything at all. // // 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 @@ -35,12 +39,17 @@ #ifdef __linux__ +// Stock libgbm. This include alone is the supported way to use the package. +#include + +// Optional; only for the introspection the reachability assertions need. #include #include #include #include #include +#include #include #include @@ -95,10 +104,38 @@ bool dir_has_backend(const char *dir) return found; } +const char *const kSentinel = "/mcpp/sentinel/gbm"; +const char *const kChildMarker = "MCPP_GBM_SELFTEST_CHILD"; + } // namespace -int main() +int main(int, char **argv) { + // Re-executed by section 4 with GBM_BACKENDS_PATH already set. All this + // child does is report whether the constructor left that value alone. + if (std::getenv(kChildMarker) != nullptr) { + const char *v = std::getenv("GBM_BACKENDS_PATH"); + return (v != nullptr && std::strcmp(v, kSentinel) == 0) ? 0 : 1; + } + + // ── 0. The package works without being asked to ────────────────────── + // Read FIRST, before this program has called anything. If this passes, + // a consumer that only ever writes `#include ` — including a + // third-party library doing so inside its own code — gets a working + // gbm_create_device(). If it fails, the package is only usable by callers + // who know its private helper, which is to say not usable at all. + const char *env_at_entry = std::getenv("GBM_BACKENDS_PATH"); + check(env_at_entry != nullptr, + "GBM_BACKENDS_PATH is set on entry to main (nothing called)"); + if (env_at_entry != nullptr) { + std::printf(" backends dir: %s\n", env_at_entry); + struct ::stat st {}; + check(::stat(env_at_entry, &st) == 0 && S_ISDIR(st.st_mode), + "it names a directory that exists"); + check(dir_has_backend(env_at_entry), + "it contains at least one *_gbm.so backend"); + } + // ── 1. The calls reach Mesa's libgbm ───────────────────────────────── // Pure functions: no device, no GPU, no DRM node. check(format_name(GBM_FORMAT_XRGB8888) == "XR24", @@ -133,40 +170,32 @@ int main() check(gbm_create_device(-1) == nullptr, "gbm_create_device(-1) == nullptr"); - // ── 4. The backend is REACHABLE ────────────────────────────────────── - // The assertion this package exists for, and it needs no GPU. + // The introspection helper agrees with what the constructor published. const char *dir = mcpp_gbm_backends_dir(); - check(dir != nullptr, "mcpp_gbm_backends_dir() resolves libgbm's location"); - - if (dir != nullptr) { - std::printf(" backends dir: %s\n", dir); - - struct ::stat st {}; - check(::stat(dir, &st) == 0 && S_ISDIR(st.st_mode), - "the derived backend directory exists"); - check(dir_has_backend(dir), - "it contains at least one *_gbm.so backend"); + check(dir != nullptr && env_at_entry != nullptr && + std::strcmp(dir, env_at_entry) == 0, + "mcpp_gbm_backends_dir() agrees with the wired value"); + + // ── 4. An explicit setting outranks us ─────────────────────────────── + // The constructor is a DEFAULT, not an override, so a value inherited from + // the environment has to survive it. That can only be observed from a + // fresh process, because by the time main runs the constructor is done. + { + const pid_t pid = ::fork(); + if (pid == 0) { + ::setenv(kChildMarker, "1", 1); + ::setenv("GBM_BACKENDS_PATH", kSentinel, 1); + ::execv("/proc/self/exe", argv); + ::_exit(127); + } + int status = 0; + check(pid > 0 && ::waitpid(pid, &status, 0) == pid && + WIFEXITED(status) && WEXITSTATUS(status) == 0, + "an inherited GBM_BACKENDS_PATH survives the constructor"); } - check(mcpp_gbm_use_sibling_backends() != 0, - "mcpp_gbm_use_sibling_backends() succeeds"); - - const char *env = std::getenv("GBM_BACKENDS_PATH"); - check(env != nullptr && dir != nullptr && std::strcmp(env, dir) == 0, - "GBM_BACKENDS_PATH now names that directory"); - - // An already-set value is the caller's decision and must be left alone. - ::setenv("GBM_BACKENDS_PATH", "/nonexistent/chosen/by/caller", 1); - mcpp_gbm_use_sibling_backends(); - const char *kept = std::getenv("GBM_BACKENDS_PATH"); - check(kept != nullptr && std::strcmp(kept, "/nonexistent/chosen/by/caller") == 0, - "an explicit GBM_BACKENDS_PATH is not overwritten"); - ::unsetenv("GBM_BACKENDS_PATH"); - // ── 5. A real device, opt-in ───────────────────────────────────────── if (std::getenv("MCPP_RUN_GBM_DEVICE") != nullptr) { - mcpp_gbm_use_sibling_backends(); - const int fd = ::open("/dev/dri/renderD128", O_RDWR); if (fd < 0) { std::printf(" MCPP_RUN_GBM_DEVICE set but /dev/dri/renderD128 " diff --git a/tests/examples/libgbm/tests/stock_usage.cpp b/tests/examples/libgbm/tests/stock_usage.cpp new file mode 100644 index 00000000..8a727531 --- /dev/null +++ b/tests/examples/libgbm/tests/stock_usage.cpp @@ -0,0 +1,78 @@ +// The whole point of the package, as a separate translation unit. +// +// This file includes STOCK and nothing else — no mcpp_gbm.h, no helper +// declaration, no knowledge that compat.libgbm exists. It is what a consumer +// ported from any other build system looks like, and more importantly it is +// 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 and will never call anything of ours. +// +// So if the backend path ever goes back to being something the application has +// to opt into, this file fails while gbm.cpp — which does include the optional +// header — could still pass. That asymmetry is the reason it exists. + +#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 From 3c8c21f662998d41d93119ae5a82d6b9fcaa0e2c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 00:39:43 +0800 Subject: [PATCH 03/19] docs(libgbm): cross-repo closed-loop plan for GBM (mcpp / xim-pkgindex / mcpp-index) Records what the PR #281 discussion established: making gbm_create_device() work in an mcpp project needs no new mechanism. Every layer already exists and runs; two wiring points are missing, in two different repos. R1 (xim-pkgindex): GBM_BACKENDS_PATH is absent from graphics.lua's DISCOVERY table. DRI and EGL vendor dirs are there; GBM is the same class of thing (dlopen'd by path, not a link target) and simply never got its counterpart. R2 (mcpp): the default runtime selection reads /subos/default, whose envs is {}, while a project's xim: deps declare into /.mcpp/.xlings/subos/_. Measured: with [xlings] subos = "_" the whole chain works -- ${subosdir} expansion, prepend merge, child injection. So mcpp#352 fixed HOW to inject and not WHERE to read from. R2 is not gbm-specific: LIBGL_DRIVERS_PATH is unset too, so any mcpp-built GL program currently cannot find a DRI driver. Carries the measured evidence, the code locations, per-repo diffs, a verification matrix whose V5 is the mechanical precondition for deleting compat.libgbm's constructor, and the Conan/distro evidence for why the package should stay independent but thin (split axis is the INTERFACE -- gbm.pc vs gl.pc -- not the source project; Conan has no gbm recipe at all and models this class as /system virtual packages). --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 321 ++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 .agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md 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..aa5b4df6 --- /dev/null +++ b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md @@ -0,0 +1,321 @@ +# GBM 闭环:mcpp / xim-pkgindex / mcpp-index 跨仓方案 + +Date: 2026-08-30 · 起因:`compat.libgbm`(mcpp-index PR #281)· 状态:待 review + +## TL;DR + +让 `gbm_create_device()` 在 mcpp 工程里能用,**不需要新机制**。所需的机制三层全都已经存在 +并且已经在跑;缺的是**两个具体的接线点**,分别在两个仓: + +| # | 缺什么 | 在哪个仓 | 规模 | +|---|---|---|---| +| **R1** | `GBM_BACKENDS_PATH` 不在 graphics 的 `DISCOVERY` 表里 | xim-pkgindex | 一个常量 + 一行表项 + 一个照抄 `declare_dri` 的函数 | +| **R2** | 默认 runtime selection 读的是**工具链 subos**,而项目的 `xim:` 依赖声明在**项目 subos** | mcpp | 一处选择/合并逻辑 | + +R2 **不是 gbm 专属的**:同一条线上 `LIBGL_DRIVERS_PATH` 和 `__EGL_VENDOR_LIBRARY_DIRS` +也没到进程,也就是说**任何 mcpp 构建的 GL 程序都找不到 DRI 驱动**。gbm 只是让它显形。 + +R1 + R2 落地后,`compat.libgbm` 里的 constructor 与后端 farm **全部删除**,退化成一个 +只有「头 + `-lgbm`」的薄壳。 + +--- + +## 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 的声明 + +两个方案,**建议 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 的形状),应当单独提。 + +这也是为什么全套测试断言的是「后端**存在于**将被搜索的路径上」而不是「后端能加载」—— +后者在这台机器上永远红,且红的原因不在本方案范围内。 From f5ebfb327ca7aabe03f057c89985113aa2eda815 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 00:46:59 +0800 Subject: [PATCH 04/19] docs(libgbm): second-round self-review supersedes the mcpp-side proposal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four substantive errors in the first cut, three of them 'concluded without running the experiment'. 1. B1/B2 was a false dichotomy. Measured the project subos: it has libc, crt1.o, libm and ld-linux but NOT libgcc_s or libstdc++, so it is not a superset of the toolchain subos. B2 (switch selection) would therefore REGRESS, which is the real objection -- the one I gave (blast radius / full rebuild) was weak, since contractHash already handles invalidation. And B1 (merge envs only) fixes runtime while leaving gbm.h and -lgbm to the package forever. The correct shape is layered inheritance (B3): keep --sysroot on the toolchain subos and overlay the project subos as -isystem / -L / -rpath plus a prepend env merge. One compiler takes one --sysroot, so 'inherit' necessarily means base + overlay, not swap. 2. Never tested whether the compat package could be avoided entirely. It cannot, but not for the reason given: [xlings] deps materializes "deps": ["mesa"] into .mcpp/.xlings.json and then installs nothing (gbm.h not found, no subos created), and [xlings] subos errors rather than bootstrapping a missing subos. So a package's xpm.deps.runtime is currently the ONLY door into the xim layer for an mcpp project -- which is itself a gap, and means compat.libgbm is presently doing a job that is not a library package's to do. 3. The verification matrix conflated the two C2 variants: V5 can only go green under B3. Under B1 the thin shim is permanent, not transitional. 4. R2 was one line where it is three: R2a ([xlings] subos cannot bootstrap), R2b ([xlings] deps materialized but not provisioned), R2c (default selection reads only the toolchain subos, no layering). R1/C1 and the industry argument in §3 are unaffected. --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 107 +++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) 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 index aa5b4df6..47c21d78 100644 --- 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 @@ -18,6 +18,13 @@ R2 **不是 gbm 专属的**:同一条线上 `LIBGL_DRIVERS_PATH` 和 `__EGL_VEND 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` 不能自举)。 + --- ## 1. 实测证据(全部可复现) @@ -146,7 +153,10 @@ end ### C2 — mcpp:让默认选择也能读到项目 subos 的声明 -两个方案,**建议 B1**。 +> **⚠ 本节已被第 8 节「深度自我 review」推翻。** 下面的 B1/B2 是一个假二分,正确的形态是 +> **分层继承**(B3)。保留原文是为了记录被否掉的推理,新方案见 §8。 + +两个方案,~~建议 B1~~。 #### B1(小,建议):只合并 env,不动其他 @@ -319,3 +329,98 @@ version `GLIBC_2.43' not found (required by …/libgallium-25.0.7.so) 这也是为什么全套测试断言的是「后端**存在于**将被搜索的路径上」而不是「后端能加载」—— 后者在这台机器上永远红,且红的原因不在本方案范围内。 + +--- + +## 8. 深度自我 review(2026-08-30 第二轮) + +第一轮方案有四处实质错误。逐条记录,因为其中三条是「没做实验就下结论」。 + +### 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 错位,与本方案正交。 From b0b843686288f15cabfb069fbcb3e7817f9665f8 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 00:55:05 +0800 Subject: [PATCH 05/19] docs(libgbm): R2b fix (auto-provision [xlings] deps) + third-round review R2b: [xlings] deps is materialized into .mcpp/.xlings.json and then nothing installs it. mcpp already has two 'declare -> auto-install' paths to mirror: the toolchain first-run flow (prepare.cppm ~1690, fetcher.resolve_xpkg_path with autoInstall=true) and the project-scope install_packages capability (~2936, which already carries the live progress UI and whose install destination is chosen by package scope, so it lands in the project scope -- which is exactly what creates the project subos). Proposal reuses the latter, keyed off penv.deps, idempotent, with a toolchain-shaped error that prints the manual equivalent. Ordering matters and subsumes R2a: provisioning must run BEFORE runtime selection, or 'selected SubOS does not exist' fires first. So R2a is better understood as 'provision before select' than as 'subos must bootstrap', and [xlings] subos can keep its strict select-only semantics. Third-round review corrects an expectation the earlier rounds got wrong: even with R1 + R2b + B3 all landed, compat.libgbm does NOT disappear. The [xlings] deps route only works for an application's OWN manifest, and GBM's real consumers are mostly LIBRARIES (SDL2's KMSDRM backend, wlroots, ffmpeg VAAPI) which cannot inject [xlings] deps into their consumer's manifest -- they can only declare a dependency edge. Same reason Conan ships opengl/system as a package rather than telling users to write system_requirements themselves. Also records four assumptions still unverified, the sharpest being that B3's overlay must rank BELOW a package's own include_dirs or it would swap out everyone's GL headers (the overlap compat.glx-headers already documents as a real trap). --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 158 +++++++++++++++++- 1 file changed, 157 insertions(+), 1 deletion(-) 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 index 47c21d78..b9b47688 100644 --- 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 @@ -10,7 +10,7 @@ Date: 2026-08-30 · 起因:`compat.libgbm`(mcpp-index PR #281)· 状态:待 revi | # | 缺什么 | 在哪个仓 | 规模 | |---|---|---|---| | **R1** | `GBM_BACKENDS_PATH` 不在 graphics 的 `DISCOVERY` 表里 | xim-pkgindex | 一个常量 + 一行表项 + 一个照抄 `declare_dri` 的函数 | -| **R2** | 默认 runtime selection 读的是**工具链 subos**,而项目的 `xim:` 依赖声明在**项目 subos** | mcpp | 一处选择/合并逻辑 | +| **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 只是让它显形。 @@ -24,6 +24,11 @@ R1 + R2 落地后,`compat.libgbm` 里的 constructor 与后端 farm **全部删 > 不是工具链 subos 的超集,所以 B2 会回退;而 B1 只修运行期、不修构建期。 > 另外实测确认:今天**唯一**能把 xim 层引进 mcpp 工程的东西就是 compat 包的 > `xpm.deps.runtime`(`[xlings] deps` 只物化不供给、`[xlings] subos` 不能自举)。 +> +> **第三轮(§9/§10)**:R2b 的修法是**首次构建自动供给**,复用工具链首次运行同款流程; +> R2a 降级为「供给先于选择」的顺序约束。并且修正了一处预期 —— 即便三条全修完, +> `compat.libgbm` **也不会消失**,因为库→库的**传递依赖**只能靠包来表达, +> 不能靠消费者的 `[xlings] deps`。 --- @@ -424,3 +429,154 @@ R2a + R2b 不修,B3 从 manifest 侧就是不可达的 —— 用户写 `[xlings - **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 全部落地,**这个包也不会消失**, +只会瘦下来 —— 它作为「库→库」传递依赖的载体是长期的。 From 713c82eb9fa400752542ed8163cf5efe7a72cedb Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 01:32:53 +0800 Subject: [PATCH 06/19] docs(libgbm): task breakdown, and the measurements that killed B3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §11 splits the work into T1–T10 with dependencies, and evaluates it against architecture / stability / simplicity / UX / compatibility / cross-platform / consistency / seamless-upgrade / test-coverage. Key structural point: R1 (xim-pkgindex) and R2 (mcpp) are independent chains, so the GBM closed loop does not wait on the larger mcpp work. §12 records what implementation actually found, and it overturns §8's central conclusion. B3 (layer the project SubOS over the toolchain SubOS) is NOT needed. The fix is to provision `[xlings] deps` at GLOBAL scope, because that registry's SubOS *is* mcpp's `--sysroot`; once the payload lands there, headers and libraries are visible with no -isystem/-L overlay at all. Three measurements got there: * project scope -> installs fine, headers land in the SubOS the compiler does not read, gbm.h still not found * resolve_xpkg_path (global) -> headers reach the sysroot, but it demands @ and rejects a bare name * install_packages + make_xlings_env (global) -> correct for bare, namespaced and pinned spellings alike So §8.1's "the project SubOS lacks libgcc_s/libstdc++" table is still fact; it just proves "do not install there" rather than "layer over it". The data was right and the conclusion was backwards. This also keeps the change an order of magnitude smaller — nothing touches linkmodel.cppm, plan.runtimeSearch or link_line.cppm, whose comments document exactly the defect that reordering a mutable view would reintroduce. Verification recorded in full: the real ecosystem run under `xlings subos use --sandbox --gpu` allocating an actual gbm buffer object on card0, and the fresh-MCPP_HOME mcpp run closing compile/link/run/env with zero mcpp-index packages. Also the one thing still open and out of scope — the xim-x-mesa payload whose RUNPATH names glibc 2.39 while its own libgallium needs GLIBC_2.43 — and two verification traps worth knowing (MCPP_HOME appends another `registry/`, and mcpp keeps its own index copy separate from ~/.xlings). PRs: openxlings/xim-pkgindex#713 (C1), mcpp-community/mcpp#531 (R2b). --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) 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 index b9b47688..29fd201a 100644 --- 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 @@ -580,3 +580,190 @@ system_requirements」,因为 `sdl`、`glfw` 这些库需要 `requires` 一个 但 §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 重新发布(本地验证时手工同步了两份)。 From bb82e0c0a72db80eb6a8558014bd721d2b7bee2a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 01:41:50 +0800 Subject: [PATCH 07/19] docs(libgbm): delivery status, one PR per repo, and where the SubOS-env half lives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records T1-T10 against the three PRs, and answers the review question this plan invited: PR mcpp#531 contains only provisioning because the SubOS ENVIRONMENT half was never missing from mcpp. subos_info.cppm parses envs, runtime_binding collects them, execute.cppm::compute_subos_env injects them with ${subosdir} expanded and prepend applied, and tests/e2e/200_subos_env_reaches_program.sh has asserted exactly that since mcpp#352. What was missing was the GBM row in the declaration (T1, xim-pkgindex#713) and a payload in the SubOS mcpp reads (T3, mcpp#531). T4 is retired outright per §12.1. mcpp#352 fixed HOW to inject; T1 supplies WHAT to inject; T3 supplies something to read it from. --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) 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 index 29fd201a..5f3aefd9 100644 --- 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 @@ -767,3 +767,41 @@ version `GLIBC_2.43' not found (required by …/libgallium-25.0.7.so) 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 补上「从哪读得到」。** From fff0d75352d9d2537305e6e628d91ae86ff146da Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 01:44:56 +0800 Subject: [PATCH 08/19] docs(libgbm): should mesa (or its separable libraries) become mcpp-index packages? mcpp does support shared-library packages -- package-types.md shape F, compat.x11 and linux compat.vulkan -- so this is a should, not a can. mesa itself: no. Two libgbm.so.1 in one process with xim:mesa already providing one (compat.vulkan-runtime reached the same conclusion for libvulkan: 'one loader per process is the whole point'); and mixing a shared closure into an index whose compat.* are all static is a measured silent symbol hijack -- 86 zlib symbols exported from the exe, libgio's 12 zlib calls all bound there, the bundled libz.so.1 fully shadowed, running fine with no warning. vcpkg forbids that with triplets and Conan with a propagated shared option; mcpp/xlings has no such whole-graph switch. But the instinct is right for the separable pieces, and the criterion is the one from section 3: does upstream ship it as a separable unit. libdrm does, Conan has a real recipe for it, and compat.vulkan-runtime currently harvests libdrm*.so.* from the HOST -- exactly the host edge this plan exists to close. A source-built compat.libdrm would close it and trips none of the mesa objections. Recorded as the recommended next package. --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) 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 index 5f3aefd9..eca5d0b9 100644 --- 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 @@ -805,3 +805,49 @@ version `GLIBC_2.43' not found (required by …/libgallium-25.0.7.so) 装对 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 论证过的薄绑定形态。 From 91de93867008da480d69de8881dab6cd7d9fe499 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 01:46:53 +0800 Subject: [PATCH 09/19] docs(libgbm): the constructor's removal condition is now mechanical The descriptor said the real fix 'is worth filing'. It is filed and implemented: openxlings/xim-pkgindex#713 puts GBM_BACKENDS_PATH in the graphics discovery table, so xim:mesa declares it into the subos and every consumer inherits it (measured: '4 env var(s) from 1 package(s)' where it was 3, and a real gbm_bo_create on card0). So the comment now names the removal CONDITION instead of an intention, and the condition is checkable rather than a judgement call: delete the TU, the lib/gbm farm and mcpp_gbm.h, re-run tests/stock_usage.cpp -- which includes stock and nothing else -- and if it stays green the ecosystem is supplying the value. Also records that it is NOT green yet as of today: the value only arrives in a home whose installed xim:mesa was configured by an index carrying #713, i.e. after that PR merges and the artifact republishes. Verified again after the edit: parse OK, 2 passed via the CN mirror, cold. --- pkgs/c/compat.libgbm.lua | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/pkgs/c/compat.libgbm.lua b/pkgs/c/compat.libgbm.lua index bb35ee8a..263f65d4 100644 --- a/pkgs/c/compat.libgbm.lua +++ b/pkgs/c/compat.libgbm.lua @@ -156,12 +156,26 @@ -- same variable, same "don't override an explicit value" rule, scoped to -- processes that actually link libgbm. -- --- WHERE THIS REALLY BELONGS. Long term 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; until then the wiring lives here, where it --- can at least be tested. +-- WHERE THIS REALLY BELONGS, AND WHEN THIS CODE GOES AWAY. The distro answer is +-- the right one and it is one layer down. That is now DONE rather than +-- proposed: openxlings/xim-pkgindex#713 adds `GBM_BACKENDS_PATH` to the +-- graphics discovery table, so `xim:mesa` declares it into the subos and every +-- consumer inherits it — measured in a fresh subos, `4 env var(s) from 1 +-- package(s)` where it used to be 3, and a real `gbm_bo_create` on card0. +-- +-- THE REMOVAL CONDITION IS MECHANICAL, not a judgement call: +-- `tests/stock_usage.cpp` includes stock `` and nothing else and +-- asserts the variable is already set. Delete this TU, the `lib/gbm/` farm and +-- `mcpp_gbm.h`, and re-run it. If it stays green, the ecosystem is supplying +-- the value and none of this is needed any more. +-- +-- As of 2026-08-30 it is NOT yet green without the constructor: the value +-- arrives only in a home whose installed `xim:mesa` was configured by an index +-- carrying #713, which means after that PR merges and the artifact is +-- republished. Until then this is the only thing that makes +-- `gbm_create_device()` work for an mcpp consumer, and it is deliberately the +-- narrowest possible mechanism — one constructor, one variable, no override of +-- an explicit value. -- -- WHY THE FARM CARRIES THE UNVERSIONED `libgbm.so`, when compat.vulkan-runtime -- is emphatic that its farm must hold versioned sonames only. That rule exists From 989d2315e7bab2e8c24b876e90b36f772d8af5da Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 01:51:53 +0800 Subject: [PATCH 10/19] =?UTF-8?q?docs(libgbm):=20reading=20guide=20?= =?UTF-8?q?=E2=80=94=20the=20doc=20records=20three=20rounds,=20last=20one?= =?UTF-8?q?=20wins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plan now supersedes itself twice (section 2's C2 by section 8, section 8's B3 by section 12.1), and a reader going top-down would act on retired advice. A short guide up front says which sections are authoritative and which are kept only as a record of reasoning that was overturned — worth keeping, because what overturned each round was a measurement, and those measurements are the durable part. Also states the honest pattern: the only thing that kept changing was the mcpp-side shape, and that is exactly the part I did not read the source or run an experiment on before proposing. --- ...2026-08-30-gbm-cross-repo-closed-loop-plan.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 index eca5d0b9..41ccd958 100644 --- 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 @@ -2,6 +2,22 @@ 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 工程里能用,**不需要新机制**。所需的机制三层全都已经存在 From ebce6761ee90446dc1de95ec42f058472afc0db2 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 02:06:50 +0800 Subject: [PATCH 11/19] docs(libgbm): why the xlings pin stays at 2026.8.27.5 The task asked to pin the internal xlings dependency to 2026.8.27.4, "which should be released by then". Checked, and the premise is inverted: .4 is an OLDER already-published release (2026-08-27T10:18) while .5 is Latest (2026-08-27T13:29, 8 assets), and every pin is already on .5 -- check_version_pins.sh reports "OK: xlings pins all at 2026.8.27.5", and xim-pkgindex's xlings.lua already has latest = 2026.8.27.5. mcpp-index pins mcpp, not xlings. So the intent -- pin to a released xlings -- is already satisfied, and satisfied better. Downgrading would also drop a property the source documents: .5 makes the declaration outrank the index during resolution, so it holds even when "latest" is not the highest entry in the table; .4 does not. The same comment records what the pin is a FLOOR against: below 2026.8.27.2 the bundled xlings takes a subos runtime binding from a compiled-in constant, so a home can declare one glibc and install another, and mcpp is the party that fails. kXlingsVersion is the single source of truth for every pin under .github/, so a downgrade would touch release/CI/bootstrap in a dozen places. The local warning that probably prompted this -- "vendored xlings 2026.8.27.4 is older than the pinned 2026.8.27.5, but no newer source is available" -- is about the copy bundled into the mcpp release tarball, not about .5 being unavailable; .5 has 8 downloadable assets. That message names its own fix, which is to self-update xlings rather than to move mcpp's pin backwards. Not executed; recorded in section 15 instead. --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) 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 index 41ccd958..926a62f6 100644 --- 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 @@ -867,3 +867,64 @@ host 依赖。一个源码构建的 `compat.libdrm` 能把那条 host 边关掉, **结论**:不复制 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 +的变更,应当单独评估,不该搭在本方案里顺带做。 From f4009b18d5c1f4029a60ce2de46c15127bd451b7 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 02:08:44 +0800 Subject: [PATCH 12/19] docs(libgbm): mark section 8.1 superseded at its own heading Section 2's C2 already carried a supersession banner; section 8 did not, so a reader landing there directly would act on the retired B3 conclusion. The reading guide said so, but a guide only helps someone who started at the top. Scoped to 8.1 rather than the whole section: 8.2-8.6 (the untested-alternative finding, the V-matrix correction, the R2a/R2b/R2c split) all still stand, and 8.1's own measurement is still fact -- it just proves "do not install into the project scope" rather than "layer over it". --- .agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md | 6 ++++++ 1 file changed, 6 insertions(+) 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 index 926a62f6..e08fa594 100644 --- 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 @@ -355,6 +355,12 @@ version `GLIBC_2.43' not found (required by …/libgallium-25.0.7.so) ## 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) From a02ee328a2a9edd1ca7296708a9c268821949e3f Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 02:16:57 +0800 Subject: [PATCH 13/19] =?UTF-8?q?docs(libgbm):=20V5=20passes=20=E2=80=94?= =?UTF-8?q?=20the=20constructor=20is=20removable=20once=20#713=20lands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 4 defined deleting the constructor as a mechanical precondition rather than a judgement call. Simulated the post-merge state locally (copied #713's graphics.lua and mesa.lua into ~/.mcpp/registry's index copy, re-ran xlings install so mesa's config() re-declared) and then walked the REAL dependency path -- [dependencies.compat] libgbm, no [xlings], no ldflags, just #include -- cold over the CN mirror: GBM_BACKENDS_PATH = /home/speak/.mcpp/registry/subos/default/usr/lib/gbm That is the SUBOS path, not the package's own farm. The constructor is `if (getenv("GBM_BACKENDS_PATH")) return;`, so a subos-shaped value proves the ecosystem set it first and the constructor was a no-op. Against an unpatched index the same path yields the in-package farm value; the only variable between the two runs is whether the index carries the DISCOVERY row. Not deleting yet, and the reason is the same gate working as designed: until #713 merges and the artifact republishes, consumers on the published index would lose the variable and tests/stock_usage.cpp would go red in CI -- which is precisely the mechanical check, enforced rather than remembered. Section 16.4 lists the exact one-step follow-up, and notes that stock_usage.cpp stays: after the removal it stops asserting "our constructor ran" and starts asserting "the whole ecosystem loop works", which is the most valuable regression this package has. --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) 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 index e08fa594..f560e5f9 100644 --- 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 @@ -934,3 +934,62 @@ but no newer source is available (keeping it; run `xlings self update`) **结论**:保持 `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` 尚未合并,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 生效了」变成「**整条生态闭环生效了**」,是这个包最有价值的一条回归。 From f2105127b32c3a8717b9a298878b1a0cd41327ef Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 02:18:29 +0800 Subject: [PATCH 14/19] =?UTF-8?q?docs(libgbm):=20correct=20section=2014.2?= =?UTF-8?q?=20=E2=80=94=20compat.libdrm=20does=20not=20close=20that=20host?= =?UTF-8?q?=20edge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 14.2 recommended compat.libdrm on the grounds that it would close compat.vulkan-runtime's host harvest of libdrm*.so.*. That reasoning is wrong, and chasing it found a better next step. vulkan-runtime's farm exists because the dlopen'd ICD ITSELF comes from the host and carries its own DT_NEEDED (libdrm, LLVM, xcb), which must resolve through the same directory. That needs host-compatible SHARED libraries. An in-index compat.libdrm would be a static package like every other compat.*, and a static archive cannot satisfy a .so's DT_NEEDED. I had conflated "there is a package called libdrm" with "the farm needs libdrm*.so.*"; they are not the same thing. libdrm may still be worth adding for build-time consumers, but not for that reason. The real next step is one layer up. xim:mesa already ships share/vulkan/icd.d/radeon_icd.x86_64.json and lib/libvulkan_radeon.so, and mesa.lua already calls graphics.declare_vulkan_icd(); with DISCOVERY's XDG_DATA_DIRS the loader finds it. Meanwhile compat.vulkan-runtime still has deps = {} and still sweeps /usr/lib/x86_64-linux-gnu -- which is exactly where compat.glx-runtime stood before 2026.08.08, and glx-runtime's fix is the template: depend on the ecosystem stack, keep the host door only for vendors the ecosystem does not cover. Stated honestly: the ecosystem's Vulkan coverage is AMD-only today (mesa.lua says anv and NVK are still absent), so vulkan-runtime cannot reach the zero-host position compat.libgbm reached. It should become ecosystem-first with a host fallback, rather than host-only as it is now. --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) 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 index f560e5f9..412c28f4 100644 --- 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 @@ -993,3 +993,65 @@ 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、绑定、还是够宿主。** From 738c2fe9619373e1abefbe5896080a8c979cba3d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 02:23:36 +0800 Subject: [PATCH 15/19] =?UTF-8?q?fix(libgbm):=20the=20xpm=20anchor=20is=20?= =?UTF-8?q?inert=20=E2=80=94=20stop=20naming=20it=20like=20the=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asked whether libgbm-2026.08.29.h actually needs downloading, or whether the runtime payload already supplies the header. The payload supplies it: install() symlinks gbm.h out of /usr/include and libgbm.so* out of /lib, and the downloaded file is never read. The anchor exists only because the xpm schema wants a url + sha256 per version -- the same inert-anchor trick compat.glx-runtime plays with an OpenGL-Registry README and compat.vulkan-runtime with a Vulkan-Loader README. It used to be Mesa's own src/gbm/main/gbm.h, on the theory that the anchor may as well record which header the package was written against. 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 -- and the first person to read the descriptor asked exactly that question. A README cannot be mistaken for a payload, so the anchor is now Mesa's README.rst at the same tag. CN asset published alongside (gtc, mcpp-res/libgbm@2026.08.29), re-fetched and compared byte-for-byte against GLOBAL: 03f0fd62... , 1720 bytes. sha256 taken twice before use. Version deliberately NOT bumped: 2026.08.29 has never been published from this index (the package is still in PR), so no consumer can have resolved the old anchor. Verified cold on both mirrors: MIRROR=CN and MIRROR=GLOBAL each 2 passed. --- pkgs/c/compat.libgbm.lua | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/pkgs/c/compat.libgbm.lua b/pkgs/c/compat.libgbm.lua index 263f65d4..81fc86db 100644 --- a/pkgs/c/compat.libgbm.lua +++ b/pkgs/c/compat.libgbm.lua @@ -219,20 +219,33 @@ package = { -- the GL dispatch and X11 halves of the twenty-two-package stack. deps = { runtime = { "xim:mesa" } }, ["2026.08.29"] = { - -- Nothing downloaded is used. The payload is what install() - -- builds out of the subos view, so this is only a stable, - -- well-formed anchor for the xpm entry — the trick - -- compat.glx-runtime plays with an OpenGL-Registry README. + -- NOTHING DOWNLOADED HERE IS EVER READ, and the file is chosen + -- so that cannot be misread. -- - -- Mesa's own gbm.h at the tag the ecosystem's mesa ships, so - -- the anchor at least records which header this package was - -- written against. A raw file at a tag is byte-stable, unlike - -- a GitLab-generated archive; sha256 confirmed twice. + -- 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 used to be Mesa's own `src/gbm/main/gbm.h`, on the theory + -- that the anchor may as well record which header the package + -- was written against. 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 — and the first reader to see it asked exactly that. 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/src/gbm/main/gbm.h", - CN = "https://gitcode.com/mcpp-res/libgbm/releases/download/2026.08.29/libgbm-2026.08.29.h", + 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 = "95f3b4a6ee5175c7cc5d47368d4efb100063fe49e5a6f5b19030ac2ceed73b81", + sha256 = "03f0fd62094179bb70fb885042baa4254d392f5f7bb64e4d8856bec8a5ff8386", }, }, }, From 62b24199b87026a63dfdb4295c593ba5c28f2e22 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 02:41:34 +0800 Subject: [PATCH 16/19] feat(libgbm): the package sheds its workaround; index floor corrected Two things, both found by reviewing the PR against the ecosystem rather than against itself. 1. THE PACKAGE IS NOW WHAT IT SHOULD ALWAYS HAVE BEEN. 598 -> 303 lines. openxlings/xim-pkgindex#713 merged and the index artifact republished, so xim:mesa now declares GBM_BACKENDS_PATH into the subos and every consumer inherits it. Everything this package carried to compensate is gone: the constructor TU, the lib/gbm backend farm, mesa_libdir(), and mcpp_gbm.h. What remains is a binding and nothing else -- deps = { runtime = xim:mesa }, an install() that symlinks gbm.h and libgbm.so* out of the subos view, and include_dirs / ldflags / runtime dirs. It compiles no upstream source, ships no header of its own, and sets no environment variable. Setting GBM_BACKENDS_PATH was always Mesa's own mechanism and the environment's job; the package doing it was the workaround, not the design. tests/gbm.cpp drops the mcpp_gbm.h include and the re-exec test, and now asserts the ECOSYSTEM supplies the path -- which makes this repo's CI the tripwire on xim-pkgindex's DISCOVERY row and on mcpp's subos-env injection. 2. THE INDEX FLOOR WAS FALSE, and this package proved it. index.toml claimed min_mcpp = 2026.8.3.3. Measured against that version: compat.libgbm reports `parse OK` and then fails to link -- libdrm.so.2, needed by .../libgbm.so, not found undefined reference to `drmGetVersion' because 2026.8.3.3 does not know runtime.link_library_dirs (the string does not occur in the binary) and SILENTLY IGNORES it. Silently-ignored keys are invisible to the lint, so the "floor first, new grammar after" guard could not catch this: it assumes unknown keys are rejected, and runtime.* subkeys are not. 2026.8.10.3 does not even get that far -- it cannot bootstrap against this index at all ("selected RuntimeBinding glibc@2.44 requires payload ... but it is not installed"), which is the compiled-in-binding defect mcpp's own xlings.cppm cites as its reason to floor xlings at 2026.8.27.2. So every client below 2026.8.27.2 was already broken here for reasons predating this package. Floor and latest raised to 2026.8.27.2 -- the version validate.yml has pinned all along, which also restores the "move it with the CI pin" invariant that had quietly drifted. Verified: `mcpp test -p libgbm` 2 passed cold via the CN mirror, against an index synced from the PUBLISHED artifact (xlings update) rather than a hand-patched copy; all lint gates and mcpp xpkg parse clean across 136 descriptors. --- index.toml | 37 +- pkgs/c/compat.libgbm.lua | 478 ++++---------------- tests/examples/libgbm/mcpp.toml | 39 +- tests/examples/libgbm/tests/gbm.cpp | 98 ++-- tests/examples/libgbm/tests/stock_usage.cpp | 30 +- 5 files changed, 190 insertions(+), 492 deletions(-) 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/pkgs/c/compat.libgbm.lua b/pkgs/c/compat.libgbm.lua index 81fc86db..32d294ca 100644 --- a/pkgs/c/compat.libgbm.lua +++ b/pkgs/c/compat.libgbm.lua @@ -8,8 +8,7 @@ -- 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, and the second is the decisive --- one. +-- 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 @@ -22,182 +21,76 @@ -- 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. Vendoring it means forking Mesa's internals. +-- rebuild the frontend. -- -- Contrast `compat.vulkan`, which DOES build the Khronos Vulkan-Loader from --- source. That is not the same situation: 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. +-- 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`. 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 to shrink nothing. +-- 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, by contrast: +-- The measured surface of the binding: -- --- host 0 (see below — this is the whole point) +-- 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,…}. Mesa's --- build already placed libdrm/expat/LLVM in the --- ecosystem; nothing is asked of us or of the host. +-- xim-x-{mesa,libdrm,expat,libllvm,glibc,…} -- -- ───────────────────────────────────────────────────────────────────────── -- ZERO HOST. NOT "HOST, CONVERGED" — ZERO. -- --- This package looks at `system.subos_sysrootdir()` and NOWHERE else. It has --- no `/usr/lib*` candidate directory and, deliberately, NO escape-hatch --- environment variable. That is a stricter rule 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. +-- 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 (llvmpipe, radeonsi, iris, nouveau, zink, --- d3d12, RADV). --- --- And host libgbm specifically is a leak this ecosystem has already CLOSED. --- `xim:nvidia-gl-host-link` records it by name: +-- 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)." -- --- 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. +-- If a machine ever needs NVIDIA's own GBM backend, that belongs in +-- `xim:nvidia-gl-host-link`, the layer that owns host contact. -- -- ───────────────────────────────────────────────────────────────────────── --- THE PART THAT IS ACTUAL WORK: THE BACKEND IS UNREACHABLE IN THE SANDBOX --- --- Harvesting libgbm.so and gbm.h is the easy half and would produce a package --- you can link and cannot use. libgbm is a LOADER: every gbm_create_device() --- dlopens `/_gbm.so`. The path compiled into Mesa is --- `/usr/lib/gbm` (`gbmbackendspath` in its gbm.pc), 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: No such file or directory --- (search paths /usr/lib/gbm, suffix _gbm) --- --- `xim:mesa`'s config() declares `lib` into `/lib` and `include` into --- `/usr/include`, so libgbm.so and gbm.h both reach the view — but --- `lib/gbm/` is a SUBDIRECTORY and does not. Closing that is this package's --- real content, and it is why the shape is not a copy of compat.glx-runtime. --- --- THE FIX MUST BE INVISIBLE. This is the constraint that decides the design, --- and getting it wrong produces a package that passes its own tests and fails --- its real consumers. --- --- A libgbm consumer writes `#include ` and calls `gbm_create_device()`. --- That is the whole API, and it has to keep working unchanged — because the --- consumers that matter most are not the ones reading this file. SDL2's --- KMSDRM backend, wlroots and ffmpeg's VAAPI hwcontext all call --- gbm_create_device() from INSIDE a third-party library. Any scheme that --- requires the application to call a helper first is unreachable for exactly --- those callers, and would leave them as broken as they were. --- --- So the package exposes stock `gbm.h` and wires the backend path from a --- CONSTRUCTOR in its own TU (mcpp_generated/gbm_backends.c). By the time any --- code runs, GBM_BACKENDS_PATH is already set; nothing has to be included, --- called or known about. Verified with a consumer compiled against gbm.h alone --- and linked with no knowledge of this package. --- --- What makes that reliable is a property of mcpp that is usually a nuisance: a --- dependency's objects enter the consumer's link EAGERLY, all of them, rather --- than being lazily selected the way an archive member would be. Confirmed in --- the emitted build.ninja, which names our object on the link line directly: --- --- build bin/gbm : cxx_link obj/gbm.o obj/compat_libgbm/…/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. --- --- 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 got loaded: --- --- mcpp_generated/libgbm/lib/libgbm.so{,.1,.1.0.0} -> /lib/* --- mcpp_generated/libgbm/lib/gbm/dri_gbm.so -> mesa payload --- --- and the constructor resolves it at RUNTIME: dlsym(RTLD_DEFAULT) a gbm --- symbol, dladdr it, take the directory, append "/gbm". Verified that dladdr --- reports the FARM path rather than 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. --- --- The alternative was to bake an absolute path into a generated header at --- install time. That works and it pins the package to whichever mesa payload --- existed on the day it was installed, which is precisely the failure --- 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 no absolute path anywhere in the descriptor. --- --- An already-set GBM_BACKENDS_PATH is left alone: this is a default, not an --- override, and a user who has pointed it somewhere deliberately outranks us. --- --- THIS IS WHAT EVERY OTHER ECOSYSTEM DOES, and none of them do it with an API. --- Distributions (Debian's libgbm1/libgbm-dev, Fedora's mesa-libgbm) split --- libgbm out of the mesa SOURCE package and never touch the path, because one --- system-wide prefix makes Mesa's compiled-in `$libdir/gbm` correct by --- construction. Relocated and sandboxed stacks cannot rely on that and set the --- environment variable instead — Valve's pressure-vessel hit precisely this --- bug when Mesa 24.3 split the backends out (steam-runtime#797) and answers --- with GBM_BACKENDS_PATH=/run/host/usr/lib64/gbm; Nix, Conda and AppImage do --- the same at environment-activation time. Mesa itself offers the third route, --- `-Dgbm-backends-path=`, for packagers who control the build. +-- WHAT THIS PACKAGE DOES NOT DO: THE BACKEND PATH -- --- This package is in the sandboxed case and cannot set a container-wide --- environment, so the constructor is the in-process equivalent: same effect, --- same variable, same "don't override an explicit value" rule, scoped to --- processes that actually link libgbm. +-- Worth stating, because this package used to do it and no longer needs to. -- --- WHERE THIS REALLY BELONGS, AND WHEN THIS CODE GOES AWAY. The distro answer is --- the right one and it is one layer down. That is now DONE rather than --- proposed: openxlings/xim-pkgindex#713 adds `GBM_BACKENDS_PATH` to the --- graphics discovery table, so `xim:mesa` declares it into the subos and every --- consumer inherits it — measured in a fresh subos, `4 env var(s) from 1 --- package(s)` where it used to be 3, and a real `gbm_bo_create` on card0. +-- 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: -- --- THE REMOVAL CONDITION IS MECHANICAL, not a judgement call: --- `tests/stock_usage.cpp` includes stock `` and nothing else and --- asserts the variable is already set. Delete this TU, the `lib/gbm/` farm and --- `mcpp_gbm.h`, and re-run it. If it stays green, the ecosystem is supplying --- the value and none of this is needed any more. --- --- As of 2026-08-30 it is NOT yet green without the constructor: the value --- arrives only in a home whose installed `xim:mesa` was configured by an index --- carrying #713, which means after that PR merges and the artifact is --- republished. Until then this is the only thing that makes --- `gbm_create_device()` work for an mcpp consumer, and it is deliberately the --- narrowest possible mechanism — one constructor, one variable, no override of --- an explicit value. --- --- WHY THE FARM CARRIES THE UNVERSIONED `libgbm.so`, when compat.vulkan-runtime --- is emphatic that its farm must hold versioned sonames only. That rule exists --- because `runtime.library_dirs` joins the LINK line too, so a bare --- `libxcb.so` there would shadow this index's own compat.xcb. Here the --- unversioned name is exactly what is wanted — it is how `-lgbm` resolves — --- and there is nothing to shadow, because no other package in this index --- provides gbm. --- --- ───────────────────────────────────────────────────────────────────────── --- 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. That is why the test --- member asserts the backend is PRESENT at the derived path rather than that --- it loads — an assertion that is meaningful on a CI runner with no GPU, and --- that does not go green by accident when the stack is broken. +-- 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", @@ -212,8 +105,8 @@ package = { -- 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 you get names - -- the missing library rather than the misplaced key. + -- 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. @@ -230,13 +123,11 @@ package = { -- does with an OpenGL-Registry README and compat.vulkan-runtime -- with a Vulkan-Loader README. -- - -- It used to be Mesa's own `src/gbm/main/gbm.h`, on the theory - -- that the anchor may as well record which header the package - -- was written against. 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 — and the first reader to see it asked exactly that. A - -- README cannot be mistaken for a payload. + -- 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 @@ -255,19 +146,18 @@ package = { import_std = false, c_standard = "c11", - -- Both built by install(): gbm.h symlinked out of the subos view (so - -- header and library can never be from different Mesa builds) and - -- mcpp_gbm.h, this package's own two declarations. + -- 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" }, - -- dladdr and RTLD_DEFAULT live behind __USE_GNU. `c_standard = - -- "gnu11"` is the spelling that looks right and is a trap — mcpp - -- 2026.8.27.2 accepts the string and still emits `-std=c11` - -- (compat.libaio's header comment records the same finding), so the - -- define is what actually takes effect. - cflags = { "-D_GNU_SOURCE" }, - - sources = { "mcpp_generated/gbm_backends.c" }, + -- 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 @@ -275,38 +165,27 @@ package = { -- search order. targets = { ["gbm_binding"] = { kind = "lib" } }, - -- The link against Mesa's libgbm itself. It resolves through the farm, - -- which `runtime.library_dirs` puts on the link line as well as the - -- runtime path. `-ldl` is belt-and-braces: glibc >= 2.34 folds libdl - -- into libc and xim-x-glibc is 2.39, but the flag costs nothing. - ldflags = { "-lgbm", "-ldl" }, + -- The link against Mesa's libgbm itself, resolved through the farm. + ldflags = { "-lgbm" }, - -- Zero. gbm.h includes and ; the generated TU - -- includes , and . + -- Zero. gbm.h includes only and . deps = {}, runtime = { - -- THREE DIRECTORY KEYS, THREE DIFFERENT LINKER FLAGS, and they are - -- not interchangeable. Measured on mcpp 2026.8.27.2 by reading the + -- 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 - -- transitive_needed_dirs-> -Wl,-rpath-link + -- 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: they - -- exist so a bare-soname `dlopen` resolves at RUN time, and - -- nothing links against their farms. This package does link - -- against its farm, so it needs the `-L` too — with only - -- `library_dirs` the farm is complete, the rpath is correct, and - -- the build still dies at `ld: cannot find -lgbm`. - -- - -- (The docs used to say "library_dirs also joins the link line", - -- which mcpp#304 observed for real. The separate `-L` key landed - -- in 2026.8.10.3 and the pinned mcpp no longer behaves that way, - -- so the claim is version-dependent; docs/package-types.md now - -- carries the table above rather than the bare assertion.) + -- 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" }, @@ -316,11 +195,6 @@ package = { -- engine's vocabulary, and coining one that may be silently -- ignored would document a guarantee this package cannot make. }, - - -- No `generated_files`. The one TU and the two headers are written by - -- install() below, which is the only writer — a `generated_files` copy - -- of the same C source would be a second copy to keep in sync, and the - -- parser takes only literals so it could not share one. }, } @@ -331,8 +205,8 @@ 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. +-- 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) @@ -363,161 +237,6 @@ local function link_matching(srcdir, pattern, outdir) ) end --- Where mesa's own lib directory is, derived from the view rather than named. --- `/lib/libgbm.so.1` is a symlink into the xim-x-mesa payload, so its --- realpath gives the payload's lib dir and with it `lib/gbm/`, which the view --- does not carry. Doing it this way keeps the descriptor free of any mesa --- version. os.exec's return value is not trustworthy, so the answer is taken --- from the file it writes. -local function mesa_libdir(prefix, view_lib) - local probe = path.join(prefix, "mcpp_libgbm_realpath.txt") - os.exec("readlink -f " .. sh_quote(path.join(view_lib, "libgbm.so.1")) .. - " > " .. sh_quote(probe) .. " 2>/dev/null || true") - - local real = io.readfile(probe) - if real == nil then - return nil - end - real = real:gsub("%s+$", "") - if real == "" then - return nil - end - - local slash = real:match("^(.*)/[^/]*$") - return slash -end - -local consumer_header = [[ -#ifndef MCPP_COMPAT_LIBGBM_H -#define MCPP_COMPAT_LIBGBM_H - -/* compat.libgbm -- OPTIONAL. You do not need this header. - * - * The way to use this package is the way you would use libgbm anywhere else: - * - * #include - * struct gbm_device *dev = gbm_create_device(fd); - * - * Mesa's compiled-in backend search path (/usr/lib/gbm) does not exist inside - * an mcpp sandbox, but the package repairs that from a constructor in its own - * translation unit, before any of your code runs. Nothing has to be called and - * nothing has to be included -- which is the point: libgbm is mostly called - * from INSIDE other libraries (SDL2's KMSDRM backend, wlroots, ffmpeg's VAAPI - * hwcontext), and those will never call a helper of ours. - * - * What is below is introspection for diagnostics and for this package's own - * tests. Reach for it when you want to report which backend directory was - * chosen, or to override the choice explicitly. - */ - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* The backend directory, derived from the loaded libgbm. NULL if libgbm is not - in this process. Does not test whether the directory exists. */ -const char *mcpp_gbm_backends_dir(void); - -/* Point GBM_BACKENDS_PATH at that directory unless it is already set. The - constructor has already done this; calling it again is harmless. Returns - non-zero on success. */ -int mcpp_gbm_use_sibling_backends(void); - -#ifdef __cplusplus -} -#endif - -#endif /* MCPP_COMPAT_LIBGBM_H */ -]] - --- Kept identical to the generated_files entry above; install() does not wipe --- the payload, so whichever of the two lands second writes the same bytes. -local backends_tu = [[ -/* compat.libgbm -- point GBM_BACKENDS_PATH at the backends shipped beside this - * package's libgbm, automatically and before anything else runs. - * - * WHY A CONSTRUCTOR. Mesa compiles `/usr/lib/gbm` in as its backend search - * path and that directory does not exist inside an mcpp sandbox, so - * gbm_create_device() finds nothing. Repairing it through a function the - * application must call would not work: libgbm is mostly called from inside - * OTHER libraries -- SDL2's KMSDRM backend, wlroots, ffmpeg's VAAPI hwcontext - * -- and none of them will ever call ours. A constructor reaches all of them, - * and keeps `#include ` the whole of the API. - * - * Priority 101 is the first value not reserved for the implementation, so this - * runs ahead of default-priority constructors in case one creates a device. - * - * The path is derived, never stored: dlsym the loaded libgbm, dladdr it, and - * take the sibling `gbm/` directory. dladdr reports the path the loader used - * -- this package's farm -- rather than the realpath, so the answer stays - * correct without pinning a mesa version. - */ -#define _GNU_SOURCE -#include -#include -#include - -static char mcpp_gbm_dir_buf[4096]; - -const char *mcpp_gbm_backends_dir(void) -{ - Dl_info info; - const char *slash; - void *sym; - size_t n; - - if (mcpp_gbm_dir_buf[0] != '\0') - return mcpp_gbm_dir_buf; - - /* RTLD_DEFAULT rather than &gbm_format_get_name: the address of an - imported function is this object's own PLT stub, and dladdr would - report the CONSUMER instead of libgbm. */ - sym = dlsym(RTLD_DEFAULT, "gbm_format_get_name"); - if (sym == NULL) - return NULL; - - if (dladdr(sym, &info) == 0 || info.dli_fname == NULL) - return NULL; - - slash = strrchr(info.dli_fname, '/'); - if (slash == NULL) - return NULL; - - n = (size_t)(slash - info.dli_fname); - if (n + sizeof("/gbm") > sizeof(mcpp_gbm_dir_buf)) - return NULL; - - memcpy(mcpp_gbm_dir_buf, info.dli_fname, n); - memcpy(mcpp_gbm_dir_buf + n, "/gbm", sizeof("/gbm")); - return mcpp_gbm_dir_buf; -} - -int mcpp_gbm_use_sibling_backends(void) -{ - const char *dir; - - /* An explicit GBM_BACKENDS_PATH is the caller's decision and is left - alone -- this is a default, not an override. Same rule the sandboxed - stacks that set this variable follow (pressure-vessel, Nix, Conda). */ - if (getenv("GBM_BACKENDS_PATH") != NULL) - return 1; - - dir = mcpp_gbm_backends_dir(); - if (dir == NULL) - return 0; - - return setenv("GBM_BACKENDS_PATH", dir, 1) == 0; -} - -__attribute__((constructor(101))) -static void mcpp_gbm_wire_backends(void) -{ - mcpp_gbm_use_sibling_backends(); -} -]] - function install() local prefix = pkginfo.install_dir() os.mkdir(prefix) @@ -531,24 +250,14 @@ function install() local view_lib = path.join(view, "lib") local view_inc = path.join(view, "usr", "include") - local generated = path.join(prefix, "mcpp_generated") - local root = path.join(generated, "libgbm") - local out_lib = path.join(root, "lib") - local out_inc = path.join(root, "include") - local out_bk = path.join(out_lib, "gbm") + local root = path.join(prefix, "mcpp_generated", "libgbm") + local out_lib = path.join(root, "lib") + local out_inc = path.join(root, "include") - os.mkdir(generated) os.mkdir(out_lib) os.mkdir(out_inc) - os.mkdir(out_bk) - - say("writing mcpp_generated/gbm_backends.c") - io.writefile(path.join(generated, "gbm_backends.c"), backends_tu) - say("writing include/mcpp_gbm.h") - io.writefile(path.join(out_inc, "mcpp_gbm.h"), consumer_header) - - -- 1. The library. From the subos view and from nowhere else -- see the + -- 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) @@ -586,26 +295,9 @@ function install() end say("gbm.h present") - -- 3. The backends. NOT required: a Mesa built without the dri backend is a - -- legitimate configuration, and so is a machine that will only ever use - -- the pure-function half of the API. The test member reports what it - -- finds rather than assuming. - local mesa_lib = mesa_libdir(prefix, view_lib) - if mesa_lib == nil then - say("NOTE: could not resolve the mesa payload lib dir; no backends linked") - return true - end - - local src_bk = path.join(mesa_lib, "gbm") - say("mesa payload lib dir: " .. mesa_lib) - if not os.isdir(src_bk) then - say("NOTE: " .. src_bk .. " does not exist; no backends linked") - return true - end - - say("linking *_gbm.so from " .. src_bk) - link_matching(src_bk, "*_gbm.so", out_bk) + -- 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/tests/examples/libgbm/mcpp.toml b/tests/examples/libgbm/mcpp.toml index 1670007b..fd9e7c01 100644 --- a/tests/examples/libgbm/mcpp.toml +++ b/tests/examples/libgbm/mcpp.toml @@ -4,27 +4,30 @@ # 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 has to prove is not "does -lgbm resolve". compat.libgbm is a -# BINDING onto the ecosystem's Mesa, and it can be wrong in three ways that all -# still link: +# 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; -# * the backend directory could be unreachable, which is the state every -# mcpp sandbox is in by default (Mesa compiles in /usr/lib/gbm, which does -# not exist there) and which no link check would notice; -# * the repair could require the CONSUMER to opt in, which would make it -# worthless — libgbm is mostly called from inside other libraries (SDL2's -# KMSDRM backend, wlroots, ffmpeg's VAAPI hwcontext) that will never call -# a helper of ours. +# * 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. # -# Hence TWO test binaries, and the split is deliberate: +# 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. # -# tests/gbm.cpp the full surface, including the optional -# introspection header. -# tests/stock_usage.cpp includes STOCK and nothing else. If the -# backend path ever regresses to something the -# application has to ask for, this one fails while -# gbm.cpp could still pass. +# 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. diff --git a/tests/examples/libgbm/tests/gbm.cpp b/tests/examples/libgbm/tests/gbm.cpp index 84eb09b2..7f5a2d31 100644 --- a/tests/examples/libgbm/tests/gbm.cpp +++ b/tests/examples/libgbm/tests/gbm.cpp @@ -7,23 +7,19 @@ // 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. That is the -// default state of every mcpp sandbox: Mesa compiles `/usr/lib/gbm` in as -// its backend search path and that directory does not exist there, so -// gbm_create_device() finds nothing. Measured before this package -// existed: +// 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) // -// 3. **The repair could require the consumer to opt in** — which would make -// it useless. This is what section 0 below exists to prevent, and it is -// the assertion most worth having: libgbm is mostly called from INSIDE -// other libraries (SDL2's KMSDRM backend, wlroots, ffmpeg's VAAPI -// hwcontext), and none of them will ever call a helper of ours. So the -// package must work for a consumer that writes `#include ` and -// nothing else. Section 0 asserts precisely that, by reading the -// environment before this program has called anything at all. +// 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 @@ -39,17 +35,13 @@ #ifdef __linux__ -// Stock libgbm. This include alone is the supported way to use the package. +// Stock libgbm, and nothing else. This package ships no header of its own. #include -// Optional; only for the introspection the reachability assertions need. -#include - #include #include #include #include -#include #include #include @@ -104,38 +96,10 @@ bool dir_has_backend(const char *dir) return found; } -const char *const kSentinel = "/mcpp/sentinel/gbm"; -const char *const kChildMarker = "MCPP_GBM_SELFTEST_CHILD"; - } // namespace -int main(int, char **argv) +int main() { - // Re-executed by section 4 with GBM_BACKENDS_PATH already set. All this - // child does is report whether the constructor left that value alone. - if (std::getenv(kChildMarker) != nullptr) { - const char *v = std::getenv("GBM_BACKENDS_PATH"); - return (v != nullptr && std::strcmp(v, kSentinel) == 0) ? 0 : 1; - } - - // ── 0. The package works without being asked to ────────────────────── - // Read FIRST, before this program has called anything. If this passes, - // a consumer that only ever writes `#include ` — including a - // third-party library doing so inside its own code — gets a working - // gbm_create_device(). If it fails, the package is only usable by callers - // who know its private helper, which is to say not usable at all. - const char *env_at_entry = std::getenv("GBM_BACKENDS_PATH"); - check(env_at_entry != nullptr, - "GBM_BACKENDS_PATH is set on entry to main (nothing called)"); - if (env_at_entry != nullptr) { - std::printf(" backends dir: %s\n", env_at_entry); - struct ::stat st {}; - check(::stat(env_at_entry, &st) == 0 && S_ISDIR(st.st_mode), - "it names a directory that exists"); - check(dir_has_backend(env_at_entry), - "it contains at least one *_gbm.so backend"); - } - // ── 1. The calls reach Mesa's libgbm ───────────────────────────────── // Pure functions: no device, no GPU, no DRM node. check(format_name(GBM_FORMAT_XRGB8888) == "XR24", @@ -170,28 +134,24 @@ int main(int, char **argv) check(gbm_create_device(-1) == nullptr, "gbm_create_device(-1) == nullptr"); - // The introspection helper agrees with what the constructor published. - const char *dir = mcpp_gbm_backends_dir(); - check(dir != nullptr && env_at_entry != nullptr && - std::strcmp(dir, env_at_entry) == 0, - "mcpp_gbm_backends_dir() agrees with the wired value"); - - // ── 4. An explicit setting outranks us ─────────────────────────────── - // The constructor is a DEFAULT, not an override, so a value inherited from - // the environment has to survive it. That can only be observed from a - // fresh process, because by the time main runs the constructor is done. - { - const pid_t pid = ::fork(); - if (pid == 0) { - ::setenv(kChildMarker, "1", 1); - ::setenv("GBM_BACKENDS_PATH", kSentinel, 1); - ::execv("/proc/self/exe", argv); - ::_exit(127); - } - int status = 0; - check(pid > 0 && ::waitpid(pid, &status, 0) == pid && - WIFEXITED(status) && WEXITSTATUS(status) == 0, - "an inherited GBM_BACKENDS_PATH survives the constructor"); + // ── 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 ───────────────────────────────────────── diff --git a/tests/examples/libgbm/tests/stock_usage.cpp b/tests/examples/libgbm/tests/stock_usage.cpp index 8a727531..9c1538e5 100644 --- a/tests/examples/libgbm/tests/stock_usage.cpp +++ b/tests/examples/libgbm/tests/stock_usage.cpp @@ -1,15 +1,25 @@ -// The whole point of the package, as a separate translation unit. +// The minimal consumer, as a separate translation unit and a separate binary. // -// This file includes STOCK and nothing else — no mcpp_gbm.h, no helper -// declaration, no knowledge that compat.libgbm exists. It is what a consumer -// ported from any other build system looks like, and more importantly it is -// 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 and will never call anything of ours. +// 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 . // -// So if the backend path ever goes back to being something the application has -// to opt into, this file fails while gbm.cpp — which does include the optional -// header — could still pass. That asymmetry is the reason it exists. +// 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__ From 90a4a846aaf2863782a02e45866cff9f1a490026 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 03:00:56 +0800 Subject: [PATCH 17/19] docs(libgbm): the shipped docs still described the deleted design Review pass 2. Three documents that ship with this PR still described the constructor, the lib/gbm farm and mcpp_gbm.h as though they existed: * docs/descriptor-examples.md and its zh counterpart -- the catalog row is the first thing a reader opens when looking for this shape, and it described the workaround as the design. Rewritten around what the package actually is: it sets nothing, and the backend path comes from xim:mesa's declaration. The historical note stays in one clause, because "it briefly did carry a constructor, and deleting it took 598 lines to 303" is the useful part to remember. * .agents/docs/2026-08-29-add-libgbm-plan.md -- the first-round design record, whose central section is titled "the part that is actual work" and is about machinery that no longer exists. Banner at the top rather than a rewrite: the shape decision, the zero-host rule, the two-directory-key finding and the test design all still hold, and the reasoning that was overturned is worth keeping next to what overturned it. Nothing in the descriptor or the tests changed here; this is the documentation catching up with the code. --- .agents/docs/2026-08-29-add-libgbm-plan.md | 22 ++++++++++++++++++++++ docs/descriptor-examples.md | 2 +- docs/zh/descriptor-examples.md | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.agents/docs/2026-08-29-add-libgbm-plan.md b/.agents/docs/2026-08-29-add-libgbm-plan.md index 20888304..56e72b05 100644 --- a/.agents/docs/2026-08-29-add-libgbm-plan.md +++ b/.agents/docs/2026-08-29-add-libgbm-plan.md @@ -1,5 +1,27 @@ # 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 diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index 6fdaf973..281938a7 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -31,7 +31,7 @@ in the [root README](../README.md#reference-examples). | 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. 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-*`. **The part that is real work**: libgbm is a loader, and Mesa compiles `/usr/lib/gbm` in as its backend search path, which does not exist in the sandbox, so `gbm_create_device` finds nothing and no link check notices. `xim:mesa` declares `lib` into the view but `lib/gbm/` is a subdirectory and does not follow. So install() also harvests the backends, laid out as a **sibling** of the farm's libgbm, and a generated TU wires `GBM_BACKENDS_PATH` from a **constructor** — `dlsym(RTLD_DEFAULT)` a gbm symbol, `dladdr` it, append `/gbm` — which reports the farm path rather than the realpath and therefore pins nothing (baking an absolute path into a generated header instead would fix the package to whichever mesa payload existed on install day). **The repair has to be invisible, and that is the load-bearing design decision**: the API stays stock `#include `, because libgbm is mostly called from *inside* other libraries (SDL2's KMSDRM backend, wlroots, ffmpeg's VAAPI hwcontext) which will never call a helper of ours — an opt-in fix would leave exactly those consumers broken. It works because mcpp links a dependency's objects eagerly, so the constructor cannot be dropped. This is also what every other ecosystem does: distros split `libgbm1` out of the mesa source package and rely on the compiled-in `$libdir/gbm`, while relocated stacks set the environment variable (Valve's pressure-vessel hit this exact bug in steam-runtime#797; Nix and Conda do it at activation) and Mesa offers `-Dgbm-backends-path=` for packagers who control the build — never an API. Longer term this belongs in `xim:mesa` itself. This is also the package that needs **both** directory keys: `library_dirs` for the RUNPATH and `link_library_dirs` for the `-L` that lets `-lgbm` resolve at all. It ships **two** test binaries, and the split is the point: `stock_usage.cpp` includes stock `` and nothing else, so a regression back to an opt-in fix fails there while the fuller `gbm.cpp` could still pass. Both assert the backend is *present at the derived path* rather than that it loads, which is checkable on a runner with no `/dev/dri` — and stays honest on a host where the stack's own mesa/glibc skew makes the dlopen fail) | +| 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) | 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/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index 40845718..36cdc686 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -29,7 +29,7 @@ | 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,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,而 Mesa 把 `/usr/lib/gbm` 编译进去当后端搜索路径,沙箱里没有这个目录,于是 `gbm_create_device` 找不到后端,而且任何链接检查都发现不了。`xim:mesa` 把 `lib` 声明进 view,但 `lib/gbm/` 是子目录,不会跟着进去。所以 install() 把后端也收进来,并且放成 farm 里 libgbm 的**同级**目录,再由一个生成的 TU 在**构造函数**里设好 `GBM_BACKENDS_PATH` —— `dlsym(RTLD_DEFAULT)` 取一个 gbm 符号、`dladdr`、拼上 `/gbm` —— 它报的是 farm 路径而非 realpath,因此**什么都不固定**(换成在 install 时把绝对路径烧进生成头,包就被钉死在安装当天那个 mesa payload 上)。**修复必须是隐形的,这是决定性的设计约束**:API 保持原样的 `#include `,因为 libgbm 绝大多数是被**别的库内部**调用的(SDL2 的 KMSDRM 后端、wlroots、ffmpeg 的 VAAPI hwcontext),它们永远不会调用我们的 helper —— 需要消费者主动 opt-in 的方案,恰好把最重要的那批消费者留在坏掉的状态。它能成立是因为 mcpp 把依赖的对象**急切全量入链**,构造函数不可能被丢掉。这也正是其他生态的做法:发行版把 `libgbm1` 从 mesa 源码包里拆成二进制包、依赖编译进去的 `$libdir/gbm`;而被重定位的栈改设环境变量(Valve 的 pressure-vessel 正是踩到这个 bug —— steam-runtime#797,Nix/Conda 在环境激活时设),Mesa 还给能控制构建的打包者留了 `-Dgbm-backends-path=` —— **没有一个是用 API 解决的**。更长远看这件事该落在 `xim:mesa` 自己身上。这也是唯一**两个**目录键都要的包:`library_dirs` 给 RUNPATH,`link_library_dirs` 给那个让 `-lgbm` 能解析的 `-L`。它带**两个**测试二进制,拆分本身就是重点:`stock_usage.cpp` 只 include 原样的 ``、别的什么都不include,一旦退化回「要消费者 opt-in」的方案,它会失败而内容更全的 `gbm.cpp` 仍可能通过。两者都断言后端**存在于推导出的路径上**,而不是断言它能加载 —— 前者在没有 `/dev/dri` 的 runner 上可验证,并且在生态自身 mesa/glibc 错位导致 dlopen 失败的机器上也不会说谎) | +| 生态栈绑定(零 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 注入) | 恒开的 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) | From 1c08e2f8007363cf57e97f4702b253285e6e683b Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 03:01:37 +0800 Subject: [PATCH 18/19] =?UTF-8?q?docs(libgbm):=20record=20the=20final=20st?= =?UTF-8?q?ate=20=E2=80=94=20the=20follow-up=20in=2016.4=20is=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 16 said the constructor could not be deleted until #713 merged. It has, so 16.3 is marked stale and a new section 18 records what the package actually ended up as: 598 lines to 303, with the constructor, the backend farm, mesa_libdir() and mcpp_gbm.h all gone. Also records what the two test binaries now guard, which is the part worth knowing: their assertions point OUTSIDE this repository -- at xim-pkgindex's DISCOVERY row, at xim:mesa still placing its backends, and at mcpp still injecting subos env. The member stopped being a self-test and became the ecosystem's tripwire. Verification in 18.2 is against the PUBLISHED artifact (xlings update), not a locally patched index copy, which is the distinction section 12.6 warns about. --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) 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 index 412c28f4..a9684ffa 100644 --- 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 @@ -970,7 +970,7 @@ constructor 这一步是空转。 对比同一个包在**未打补丁**的索引下(本文档 §12.4 之前的所有测量),同一条路径给出的是 包内 farm 的值 —— 差别只有一个变量:索引里有没有那一行 DISCOVERY。 -### 16.3 为什么现在还不能删 +### 16.3 为什么现在还不能删 —— ⚠ 已过时,#713 已合并,constructor 已删除(见 §18) `#713` 尚未合并,artifact 也没重新发布。删了之后: @@ -1055,3 +1055,61 @@ graphics.declare_vulkan_icd(dir, "share/vulkan/icd.d", tag) 所以下一步是 **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 一起动」这条早已漂移的不变量恢复了。 From 340b45bf63923b1c76292bd47de2ef1e3b96333d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 03:26:37 +0800 Subject: [PATCH 19/19] =?UTF-8?q?feat(graphics):=20compat.libdrm,=20compat?= =?UTF-8?q?.egl=20and=20compat.wayland=20=E2=80=94=20the=20KMS/DRM=20stack?= =?UTF-8?q?=20closes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compat.libgbm on its own can allocate a buffer and do nothing with it. These three are the rest of the stack, all on the binding shape libgbm validated: compat.libdrm the layer underneath -- drmModeAddFB2 / drmModeSetCrtc turn an allocated buffer into a scanout compat.egl the layer that makes it renderable -- eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm_dev, NULL) compat.wayland the other display path -- be a client of, or be, a compositor WHY BINDINGS AND NOT SOURCE BUILDS. libdrm and wayland both PASS the separable-unit test that libgbm fails -- independent freedesktop projects with their own releases, and Conan carries libdrm as a real recipe. They are bindings for the second criterion instead: the ecosystem already owns them, Mesa's own payload has DT_NEEDED on both, and a second libdrm.so.2 or libwayland-client.so.0 in a process that also loads Mesa means two handle tables for one connection. EGL is the sharpest case: it is a spec, the thing you link is glvnd's vendor-neutral dispatch library, and building a second one would be the "one loader per process" mistake compat.vulkan-runtime already documents. Three things worth knowing, each found by making it work: * libdrm needs TWO include roots. Public headers at the include root, the uapi headers they include under libdrm/, and xf86drm.h line 40 is a bare `#include `. Expose one root and nothing compiles -- measured while writing compat.libgbm's own test, which hit exactly that. * compat.egl ships ONLY EGL/, out of a payload that also carries GL/, GLES2/, GLES3/ and KHR/. A third provider of GL/ would turn the two-provider race compat.glx-headers documents into a three-way one; KHR/ comes from the index's existing compat.khrplatform, and that edge is load-bearing rather than tidy -- without it EGL/egl.h does not parse. X11 is deliberately not a dependency: that include is USE_X11-gated, and forcing Xorg onto headless GBM users would be exactly wrong. * compat.wayland harvests four libraries and puts only -lwayland-client on ldflags. A dependency's ldflags reach the consumer's link line with no way to opt out, so forcing libwayland-server on every client would be unfixable downstream. A compositor author adds it themselves and it resolves out of the farm -- and the test member does exactly that, so the documented escape hatch has a regression guarding it rather than a promise. Verified: all three `mcpp test` green cold via the CN mirror. EGL's client extension list includes EGL_KHR_platform_gbm / EGL_MESA_platform_gbm, which is the seam with compat.libgbm; libdrm's DRM_FORMAT_XRGB8888 is asserted equal to the 'XR24' fourcc compat.libgbm asserts, because those two values cross the gbm_bo -> drmModeAddFB2 boundary and a mismatch shows wrong colours rather than an error. Lint + xpkg parse clean across 139 descriptors. CN mirrors published for all three and re-fetched byte-identical against GLOBAL. --- docs/descriptor-examples.md | 2 +- docs/zh/descriptor-examples.md | 2 +- mcpp.toml | 3 + pkgs/c/compat.egl.lua | 207 +++++++++++++++++++ pkgs/c/compat.libdrm.lua | 241 +++++++++++++++++++++++ pkgs/c/compat.wayland.lua | 199 +++++++++++++++++++ tests/examples/egl/mcpp.toml | 17 ++ tests/examples/egl/tests/egl.cpp | 107 ++++++++++ tests/examples/libdrm/mcpp.toml | 17 ++ tests/examples/libdrm/tests/drm.cpp | 117 +++++++++++ tests/examples/wayland/mcpp.toml | 25 +++ tests/examples/wayland/tests/wayland.cpp | 97 +++++++++ 12 files changed, 1032 insertions(+), 2 deletions(-) create mode 100644 pkgs/c/compat.egl.lua create mode 100644 pkgs/c/compat.libdrm.lua create mode 100644 pkgs/c/compat.wayland.lua create mode 100644 tests/examples/egl/mcpp.toml create mode 100644 tests/examples/egl/tests/egl.cpp create mode 100644 tests/examples/libdrm/mcpp.toml create mode 100644 tests/examples/libdrm/tests/drm.cpp create mode 100644 tests/examples/wayland/mcpp.toml create mode 100644 tests/examples/wayland/tests/wayland.cpp diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index 281938a7..979281b7 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -31,7 +31,7 @@ in the [root README](../README.md#reference-examples). | 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. 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) +| 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/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index 36cdc686..786e44b9 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -29,7 +29,7 @@ | 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,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 注入) +| 生态栈绑定(零 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/mcpp.toml b/mcpp.toml index 0f861113..125581ec 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -81,6 +81,9 @@ 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", 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.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/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