From cc94a86c2542e37740165c08c9c936c7487dbfdc Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 12:25:55 +0800 Subject: [PATCH 1/5] =?UTF-8?q?freedesktop.egl=201.7.0:libglvnd=20?= =?UTF-8?q?=E6=BA=90=E7=A0=81=E6=9E=84=E5=BB=BA=20+=20C++23=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=B1=82,=E6=9B=BF=E6=8E=89=20compat.egl=20=E7=BB=91?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compat.egl 绑的是 xim:libglvnd,而它自己的注释就写着这是错的: libglvnd **is** a separable project, so by the criterion this should be a source build; it is still a binding for effort alone 判据里没有「工作量」这一项。工作量的部分是生成的 dispatch(约 1000 行 Python 处理 2.7MB gl.xml)加上整个 libGLdispatch.so —— mcpplibs/libglvnd 这个 fork 把生成物签进仓、两个库都建出来,于是构建期不跑 Python、不跑第二套构建系统, mcpp build 就是全部工具链。upstream/ 是 freedesktop 发布物原样,fork 加的东西 全在 mcpp/ 下,CI 两边都 diff。 一个索引条目,两个库。libGLdispatch.so.0 由同 workspace 的兄弟成员构建,用 path 依赖引入而不是走索引:「进程里只有一个 dispatch 点」是 GLVND 存在的全部 理由,两个索引条目会让消费者把两个都写上、解析出两个包实例,而 soname 复用 只映射其中一个、另一个被静默丢弃。 按架构选文件放在 build.mcpp 里。GLdispatch 的 entry stub 既分架构又分线程存储 模型,而且和 libffi 的不同,它们自身没有任何门控 —— 在 sources 里写死 x86_64 会让包只能在 x86_64 上用而且哪儿都不写明;build.mcpp 用 mcpp::target_arch() 做的正是上游 gl_dispatch_type 的那个选择。 编译进去的 vendor 搜索路径刻意留空。上游烤进 /share/glvnd/egl_vendor.d, 重定位之后那就是 host 的目录。留空能让「生态没声明」表现为「找不到 vendor」, 而不是悄悄把宿主驱动装进沙箱进程 —— 与 compat.libgbm 对 GBM_BACKENDS_PATH 的 立场一致,真正的路径由 xim:mesa 通过 discovery 层声明(xim-pkgindex#713)。 测试成员的每条断言在零 vendor 的机器上都成立,这是被 runner 教的:原来那条 「扩展串里有 EGL_EXT_」在我这台过、在干净 runner 挂,因为上游在 vendor 列表 为空时直接返回空串(libegl.c:928)—— 那条断言测的是机器有没有 GPU 驱动。 换成两条更好的:EGL_VERSION 在任何 vendor 之前由 libglvnd 自己答成字面量 "1.5 libglvnd"(既是活性检查也是身份检查),以及 dladdr 确认加载的确实是本包 构建的 libEGL.so.1 而不是 xim:libglvnd payload 的同 soname 副本 —— 后者在装了 图形栈的机器上是真实可达的,和 compat.libdrm 测试里那条同理。 --- docs/descriptor-examples.md | 4 +- docs/zh/descriptor-examples.md | 4 +- pkgs/c/compat.egl.lua | 216 ------------------------------- pkgs/f/freedesktop.egl.lua | 117 +++++++++++++++++ tests/examples/egl/mcpp.toml | 25 ++-- tests/examples/egl/tests/egl.cpp | 162 ++++++++++++++--------- 6 files changed, 232 insertions(+), 296 deletions(-) delete mode 100644 pkgs/c/compat.egl.lua create mode 100644 pkgs/f/freedesktop.egl.lua diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index 1b0c8298..dc6e9959 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -32,8 +32,8 @@ 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. **Why not a source build**: libgbm is a build target *inside* Mesa, not a separable project — `src/gbm/meson.build` is `link_with: [libloader]`, and `libloader` wants `idep_mesautil`, ~120 TUs of Mesa's internal util library for exactly one function, `loader_open_driver_lib`; and it is a **loader**, whose `gbm_create_device()` dlopens Mesa's own `dri_gbm.so`, so built apart from Mesa it would have nothing to load. **The test on this row is separability, NOT whether the payload also carries a copy** — `compat.libdrm`, from the same stack, passes it and is a source build (see the row below); the two coexist, measured. **What it deliberately does NOT do**: set the backend search path. `GBM_BACKENDS_PATH` is Mesa's own mechanism and the ENVIRONMENT's job — `xim:mesa` declares it through the graphics discovery layer (openxlings/xim-pkgindex#713), so this package sets nothing. **Two directory keys, not one**: `library_dirs` renders as `-Wl,-rpath` and `link_library_dirs` as `-L`, so a package that is *linked against* needs both — with the first alone the build still dies at `ld: cannot find -lgbm`) · [`compat.egl`](../pkgs/c/compat.egl.lua) (the EGL dispatch, provided by libglvnd rather than Mesa. libglvnd **is** a separable project, so by the criterion this should be a source build; it is still a binding for effort alone: `libEGL.so` also needs `egl_dispatch_stubs` (Python-generated `g_egldispatchstubs.c/.h`), `winsys_dispatch` and the whole of `libGLdispatch.so`, plus the vendor discovery path `__EGL_VENDOR_LIBRARY_DIRS` — a fork of the same size as wayland's. 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` — load-bearing, since `eglplatform.h` opens with `#include `. X11 is deliberately NOT a dependency: that include is `USE_X11`-gated) | -| Source build that needs a real project (Form A fork) | [`freedesktop.wayland`](../pkgs/f/freedesktop.wayland.lua) · [`freedesktop.wayland-server`](../pkgs/f/freedesktop.wayland-server.lua) · [`freedesktop.wayland-scanner`](../pkgs/f/freedesktop.wayland-scanner.lua) · [`freedesktop.wayland-util`](../pkgs/f/freedesktop.wayland-util.lua) (wayland 1.26.0, four entries out of one tarball — [mcpplibs/wayland](https://github.com/mcpplibs/wayland), a fork that patches no upstream file. **Why a fork and not an inline descriptor**: wayland is mostly GENERATED — `protocol/wayland.xml` describes every interface and wayland-scanner emits ~13,000 lines from it — and the generator is a C program in the same tree, so it must be COMPILED before it can run. An inline descriptor has no build step, and `install()` cannot do it either: mcpp compiles a package's sources at CONSUMER-BUILD time, so no package binary exists while another package installs. `build.mcpp` is the mechanism, and it only exists for a real mcpp project. **Why four packages**: `libwayland-client.so.0` and `libwayland-server.so.0` are distinct SONAMEs that Mesa's libEGL_mesa needs BOTH of, and mcpp links every library target against ALL of a package's sources — measured: a feature-gated second target still receives the feature's objects — so one package cannot emit two libraries with disjoint contents. mcpp's own diagnostic names the remedy: "split into a workspace member". **The module layer ships with the library, not beside it**: each package carries its C library AND its `.cppm`, so there is one package per library rather than a C one and a module one. The wrappers add no API — every exported name is upstream's, and the lists are generated from the public headers so a version bump cannot silently drop one. **Macros are the one thing that cannot cross**: `export` names entities and a macro is not one, so `wayland-util` maps each to what it actually is — `WL_MARSHAL_FLAG_DESTROY` to a constexpr, `wl_container_of` to a function template, the six `*_for_each` loops to ranges — and its test instantiates all of them including the `_safe` removal guarantee) | +| 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. **Why not a source build**: libgbm is a build target *inside* Mesa, not a separable project — `src/gbm/meson.build` is `link_with: [libloader]`, and `libloader` wants `idep_mesautil`, ~120 TUs of Mesa's internal util library for exactly one function, `loader_open_driver_lib`; and it is a **loader**, whose `gbm_create_device()` dlopens Mesa's own `dri_gbm.so`, so built apart from Mesa it would have nothing to load. **The test on this row is separability, NOT whether the payload also carries a copy** — `compat.libdrm`, from the same stack, passes it and is a source build (see the row below); the two coexist, measured. **What it deliberately does NOT do**: set the backend search path. `GBM_BACKENDS_PATH` is Mesa's own mechanism and the ENVIRONMENT's job — `xim:mesa` declares it through the graphics discovery layer (openxlings/xim-pkgindex#713), so this package sets nothing. **Two directory keys, not one**: `library_dirs` renders as `-Wl,-rpath` and `link_library_dirs` as `-L`, so a package that is *linked against* needs both — with the first alone the build still dies at `ld: cannot find -lgbm`) (EGL used to sit on this row as `compat.egl`, bound to `xim:libglvnd`. Its own comment recorded why that was wrong — "libglvnd **is** a separable project, so by the criterion this should be a source build; it is still a binding for effort alone" — and effort is not the criterion, so it moved to the row below as [`freedesktop.egl`](../pkgs/f/freedesktop.egl.lua)) | +| Source build that needs a real project (Form A fork) | [`freedesktop.wayland`](../pkgs/f/freedesktop.wayland.lua) · [`freedesktop.wayland-server`](../pkgs/f/freedesktop.wayland-server.lua) · [`freedesktop.wayland-scanner`](../pkgs/f/freedesktop.wayland-scanner.lua) · [`freedesktop.wayland-util`](../pkgs/f/freedesktop.wayland-util.lua) (wayland 1.26.0, four entries out of one tarball — [mcpplibs/wayland](https://github.com/mcpplibs/wayland), a fork that patches no upstream file. **Why a fork and not an inline descriptor**: wayland is mostly GENERATED — `protocol/wayland.xml` describes every interface and wayland-scanner emits ~13,000 lines from it — and the generator is a C program in the same tree, so it must be COMPILED before it can run. An inline descriptor has no build step, and `install()` cannot do it either: mcpp compiles a package's sources at CONSUMER-BUILD time, so no package binary exists while another package installs. `build.mcpp` is the mechanism, and it only exists for a real mcpp project. **Why four packages**: `libwayland-client.so.0` and `libwayland-server.so.0` are distinct SONAMEs that Mesa's libEGL_mesa needs BOTH of, and mcpp links every library target against ALL of a package's sources — measured: a feature-gated second target still receives the feature's objects — so one package cannot emit two libraries with disjoint contents. mcpp's own diagnostic names the remedy: "split into a workspace member". **The module layer ships with the library, not beside it**: each package carries its C library AND its `.cppm`, so there is one package per library rather than a C one and a module one. The wrappers add no API — every exported name is upstream's, and the lists are generated from the public headers so a version bump cannot silently drop one. **Macros are the one thing that cannot cross**: `export` names entities and a macro is not one, so `wayland-util` maps each to what it actually is — `WL_MARSHAL_FLAG_DESTROY` to a constexpr, `wl_container_of` to a function template, the six `*_for_each` loops to ranges — and its test instantiates all of them including the `_safe` removal guarantee) · [`freedesktop.egl`](../pkgs/f/freedesktop.egl.lua) (libglvnd 1.7.0 — [mcpplibs/libglvnd](https://github.com/mcpplibs/libglvnd), and the entry that replaced the `compat.egl` binding on the row above. **The generated code is checked in rather than generated**: upstream drives ~1000 lines of Python over a 2.7 MB `gl.xml` to emit the dispatch tables, and that output is deterministic from a pinned registry — so it lives in `mcpp/generated/` and NO generator runs at build time, with CI regenerating and diffing it instead. `mcpp build` is the whole toolchain. **The per-architecture choice lives in `build.mcpp`, not in the manifest**: GLdispatch's entry stubs are per-arch *and* per-thread-storage-model, and unlike libffi's they carry no guards of their own, so hard-coding x86_64 in `sources` would make the package x86_64-only and say so nowhere; `build.mcpp` makes upstream's own `gl_dispatch_type` choice from `mcpp::target_arch()`. **One index entry, two libraries**: `libGLdispatch.so.0` is a sibling workspace member reached by a PATH dependency — being the one dispatch point in a process is GLVND's whole purpose, and a second index entry would let a consumer name both and resolve two instances, with soname reuse mapping one and silently discarding the other. **The compiled-in vendor path is deliberately EMPTY**: upstream bakes in `/share/glvnd/egl_vendor.d`, which after relocation means the HOST's directory, so an empty fallback makes a missing ecosystem declaration surface as "no vendor found" rather than as silently loading the host's driver — the same stance compat.libgbm takes with `GBM_BACKENDS_PATH`) | | Source-built library sharing a soname with the payload | [`compat.libdrm`](../pkgs/c/compat.libdrm.lua) (libdrm 2.4.134 — five TUs plus an inlined `generated_static_table_fourcc.h`, and zero dependencies. It carries the same `libdrm.so.2` the ecosystem's Mesa payload does, and that is **not** a reason to bind: a DT_NEEDED soname already in the link map is REUSED, ld.so never searches again, so Mesa's `libgbm.so.1` binds to the copy the consumer linked — measured: one mapping in the process, and Mesa's GBM allocated a buffer through it. It holds only for `kind = "shared"` with the right soname: as the default `kind = "lib"` there is no `.so` to reuse, the payload's loads for Mesa, the consumer keeps its own merged one, and libdrm's file-static state (`drmHashTable`, `nr_fds`, `connection`) exists twice over one set of fds. The test asserts with `dladdr` that the loaded library is this package's. **Two include roots**: the public headers sit at the source root and the uapi headers under `include/drm/`, and `xf86drm.h` line 40 is a bare `#include `) · [`compat.libffi`](../pkgs/c/compat.libffi.lua) (3.4.8. The x86 assembly **gates itself** — `ffi.c`/`ffiw64.c`/`unix64.S`/`sysv.S`/`win64.S` each open with `#ifdef __x86_64__` or `__i386__` — so all of them are listed and the preprocessor selects. `fficonfig.h` is configure's record of what it probed and cannot be derived, so it is inlined; `ffi.h` CAN be derived, so install() performs configure's four substitutions on `ffi.h.in` and stays honest across a version bump) · [`compat.expat`](../pkgs/c/compat.expat.lua) (2.7.1, three TUs — `xmltok_impl.c` and `xmltok_ns.c` are `#include`d BY `xmltok.c`, five times between them, so listing them would produce duplicate symbols) | | 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/`) | diff --git a/docs/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index f7f45f36..78eb62e7 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -30,8 +30,8 @@ | 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`;而且它是个 **loader**,`gbm_create_device()` dlopen 的后端就是 Mesa 自己的 `dri_gbm.so`,脱离 Mesa 建出来就是个没东西可加载的 loader。**注意这一行的判据是「可否独立分发」,不是「会不会和 payload 共存」** —— 同处一栈的 `compat.libdrm` 满足前者,于是走源码构建(见「与 payload 同 soname 的源码库」一行),两者共存已实测无碍。**它刻意不做的事**:设置后端搜索路径。`GBM_BACKENDS_PATH` 是 Mesa 自己的机制,归**环境**管 —— `xim:mesa` 通过 graphics discovery 层声明它(openxlings/xim-pkgindex#713),所以这个包什么都不设。**两个目录键都要**:`library_dirs` 渲染成 `-Wl,-rpath`、`link_library_dirs` 渲染成 `-L`,一个**会被链接**的包两个都得写,只写前者会死在 `ld: cannot find -lgbm`) · [`compat.egl`](../../pkgs/c/compat.egl.lua)(EGL dispatch,提供方是 libglvnd 而不是 Mesa。libglvnd **是**独立项目,按判据本该源码构建,现在仍是绑定纯粹是工作量问题:`libEGL.so` 还要 `egl_dispatch_stubs`(Python 生成的 `g_egldispatchstubs.c/.h`)、`winsys_dispatch`,以及完整的 `libGLdispatch.so`,加上 vendor 发现路径 `__EGL_VENDOR_LIBRARY_DIRS` —— 与 wayland 同级的一个 fork 工程。payload 里还有 `GL/`、`GLES2/`、`KHR/`,本包**只**暴露 `EGL/`:再出一个 `GL/` 提供方会把 compat.glx-headers 记录过的「两个提供方」竞争变成三方;`KHR/` 交给索引已有的 `compat.khrplatform` —— 这条依赖是必需的,因为 `eglplatform.h` 开头就 `#include `。X11 **刻意不**作为依赖:那个 include 被 `USE_X11` 门控))| -| 需要真实工程的源码构建(Form A fork) | [`freedesktop.wayland`](../../pkgs/f/freedesktop.wayland.lua) · [`freedesktop.wayland-server`](../../pkgs/f/freedesktop.wayland-server.lua) · [`freedesktop.wayland-scanner`](../../pkgs/f/freedesktop.wayland-scanner.lua) · [`freedesktop.wayland-util`](../../pkgs/f/freedesktop.wayland-util.lua)(wayland 1.26.0,一个 tarball 出四个条目 —— [mcpplibs/wayland](https://github.com/mcpplibs/wayland),不改上游任何文件的 fork。**为什么要 fork 而不是内联描述符**:wayland 大部分是**生成**出来的 —— `protocol/wayland.xml` 描述了每个接口,wayland-scanner 从它生成约 13000 行 —— 而生成器是同一棵树里的 C 程序,**必须先被编译**才能跑。内联描述符没有构建步骤;`install()` 也做不到,因为 mcpp 是在**消费者构建期**才编译包源码,别的包安装时不存在任何包产物。`build.mcpp` 正是为此而设,而它只存在于真实的 mcpp 工程里。**为什么是四个包**:`libwayland-client.so.0` 与 `libwayland-server.so.0` 是两个不同 soname,Mesa 的 libEGL_mesa 对**两者都**有 DT_NEEDED,所以必须是内容互不相交的两个文件;而 mcpp 把包的源码编一次、让**每个**库 target 都链接全部对象 —— 实测:feature 门控的第二个 target 照样拿到该 feature 的对象 —— 一个包出不了两个互不相交的库。mcpp 自己的告警就点了办法:「split into a workspace member」。**模块层与库同包**:每个包同时带 C 库和它的 `.cppm`,所以是一库一包,而不是 C 一个、模块一个。包装层**不增加 API**:导出名全是上游的,而且列表是从公开头**生成**的,版本一动不会悄悄少一个名字。**宏是唯一过不去的东西**:`export` 命名的是实体而宏不是,所以 `wayland-util` 把每个宏映射成它实际是的东西 —— `WL_MARSHAL_FLAG_DESTROY` → constexpr、`wl_container_of` → 函数模板、六个 `*_for_each` → range,它的测试把它们全部实例化,包括 `_safe` 的「循环体内可删除当前元素」保证)| +| 生态栈绑定(零 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`;而且它是个 **loader**,`gbm_create_device()` dlopen 的后端就是 Mesa 自己的 `dri_gbm.so`,脱离 Mesa 建出来就是个没东西可加载的 loader。**注意这一行的判据是「可否独立分发」,不是「会不会和 payload 共存」** —— 同处一栈的 `compat.libdrm` 满足前者,于是走源码构建(见「与 payload 同 soname 的源码库」一行),两者共存已实测无碍。**它刻意不做的事**:设置后端搜索路径。`GBM_BACKENDS_PATH` 是 Mesa 自己的机制,归**环境**管 —— `xim:mesa` 通过 graphics discovery 层声明它(openxlings/xim-pkgindex#713),所以这个包什么都不设。**两个目录键都要**:`library_dirs` 渲染成 `-Wl,-rpath`、`link_library_dirs` 渲染成 `-L`,一个**会被链接**的包两个都得写,只写前者会死在 `ld: cannot find -lgbm`)(EGL 原本就在这一行,叫 `compat.egl`,绑的是 `xim:libglvnd`。它自己的注释已经写明了那是错的 ——「libglvnd **是**独立项目,按判据本该源码构建,现在仍是绑定纯粹是工作量问题」—— 而判据里没有「工作量」这一项,所以它挪到了下一行,成为 [`freedesktop.egl`](../../pkgs/f/freedesktop.egl.lua))| +| 需要真实工程的源码构建(Form A fork) | [`freedesktop.wayland`](../../pkgs/f/freedesktop.wayland.lua) · [`freedesktop.wayland-server`](../../pkgs/f/freedesktop.wayland-server.lua) · [`freedesktop.wayland-scanner`](../../pkgs/f/freedesktop.wayland-scanner.lua) · [`freedesktop.wayland-util`](../../pkgs/f/freedesktop.wayland-util.lua)(wayland 1.26.0,一个 tarball 出四个条目 —— [mcpplibs/wayland](https://github.com/mcpplibs/wayland),不改上游任何文件的 fork。**为什么要 fork 而不是内联描述符**:wayland 大部分是**生成**出来的 —— `protocol/wayland.xml` 描述了每个接口,wayland-scanner 从它生成约 13000 行 —— 而生成器是同一棵树里的 C 程序,**必须先被编译**才能跑。内联描述符没有构建步骤;`install()` 也做不到,因为 mcpp 是在**消费者构建期**才编译包源码,别的包安装时不存在任何包产物。`build.mcpp` 正是为此而设,而它只存在于真实的 mcpp 工程里。**为什么是四个包**:`libwayland-client.so.0` 与 `libwayland-server.so.0` 是两个不同 soname,Mesa 的 libEGL_mesa 对**两者都**有 DT_NEEDED,所以必须是内容互不相交的两个文件;而 mcpp 把包的源码编一次、让**每个**库 target 都链接全部对象 —— 实测:feature 门控的第二个 target 照样拿到该 feature 的对象 —— 一个包出不了两个互不相交的库。mcpp 自己的告警就点了办法:「split into a workspace member」。**模块层与库同包**:每个包同时带 C 库和它的 `.cppm`,所以是一库一包,而不是 C 一个、模块一个。包装层**不增加 API**:导出名全是上游的,而且列表是从公开头**生成**的,版本一动不会悄悄少一个名字。**宏是唯一过不去的东西**:`export` 命名的是实体而宏不是,所以 `wayland-util` 把每个宏映射成它实际是的东西 —— `WL_MARSHAL_FLAG_DESTROY` → constexpr、`wl_container_of` → 函数模板、六个 `*_for_each` → range,它的测试把它们全部实例化,包括 `_safe` 的「循环体内可删除当前元素」保证)· [`freedesktop.egl`](../../pkgs/f/freedesktop.egl.lua)(libglvnd 1.7.0 —— [mcpplibs/libglvnd](https://github.com/mcpplibs/libglvnd),就是把上一行 `compat.egl` 绑定换掉的那个条目。**生成物是签进仓的,不是构建时生成的**:上游要跑约 1000 行 Python 处理 2.7 MB 的 `gl.xml` 才能得到 dispatch 表,而这份输出在 registry 固定的前提下是确定性的 —— 于是它躺在 `mcpp/generated/`,构建期**不跑任何生成器**,改由 CI 重新生成并 diff。`mcpp build` 就是全部工具链。**按架构选文件这件事放在 `build.mcpp` 里,不在清单里**:GLdispatch 的 entry stub 既分架构又分线程存储模型,而且和 libffi 的不同,它们自身**没有**任何门控 —— 在 `sources` 里写死 x86_64 会让这个包只能在 x86_64 上用、而且哪儿都不写明;`build.mcpp` 用 `mcpp::target_arch()` 做的正是上游 `gl_dispatch_type` 的那个选择。**一个索引条目,两个库**:`libGLdispatch.so.0` 由同 workspace 的兄弟成员构建,通过 **path** 依赖引入而不是走索引 —— 「进程里只有一个 dispatch 点」是 GLVND 存在的全部理由,而两个索引条目会让消费者把两个都写上、解析出两个包实例,soname 复用只映射其中一个、另一个被静默丢弃。**编译进去的 vendor 路径刻意是空的**:上游烤进 `/share/glvnd/egl_vendor.d`,重定位之后那就是 **host** 的目录,所以留空能让「生态没声明」表现为「找不到 vendor」,而不是悄悄把宿主驱动装进沙箱进程 —— 与 compat.libgbm 对 `GBM_BACKENDS_PATH` 的立场一致)| | 与 payload 同 soname 的源码库 | [`compat.libdrm`](../../pkgs/c/compat.libdrm.lua)(libdrm 2.4.134,5 个 TU + 内联的 `generated_static_table_fourcc.h`,零依赖。它和生态 payload 里的 `libdrm.so.2` 同名共存,而这**不是**绑定的理由:DT_NEEDED 的 soname 一旦在 link map 中就被复用,ld.so 不再搜索,所以 Mesa 的 `libgbm.so.1` 绑到的是消费者链接的这一份 —— 实测进程里只有一份,且 Mesa 的 GBM 用它分配出了 buffer。前提是 `kind = "shared"` **且 soname 正确**:用默认的 `kind = "lib"` 就没有 `.so` 可复用,payload 那份照常为 Mesa 加载,消费者另有一份合并进来的,libdrm 的 file-static 状态(`drmHashTable`/`nr_fds`/`connection`)在同一批 fd 上分成两套账。测试用 `dladdr` 断言加载的确实是本包所建。**两个 include 根**:公开头在源码根、uapi 头在 `include/drm/`,而 `xf86drm.h` 第 40 行就是裸的 `#include `) · [`compat.libffi`](../../pkgs/c/compat.libffi.lua)(3.4.8。x86 汇编**自带架构门控**,`ffi.c`/`ffiw64.c`/`unix64.S`/`sysv.S`/`win64.S` 各自 `#ifdef __x86_64__`/`__i386__`,所以全部列进 sources 由预处理器筛;`fficonfig.h` 是 configure 的探测结果、无法从 tarball 推导,内联;`ffi.h` **能**推导,于是 install() 照 configure 的四个替换从 `ffi.h.in` 生成,版本一动就跟着动) · [`compat.expat`](../../pkgs/c/compat.expat.lua)(2.7.1,3 个 TU —— `xmltok_impl.c`/`xmltok_ns.c` 是被 `xmltok.c` **include** 五次的,列进 sources 会产生重复符号) | | 恒开的 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) | diff --git a/pkgs/c/compat.egl.lua b/pkgs/c/compat.egl.lua deleted file mode 100644 index 124195ff..00000000 --- a/pkgs/c/compat.egl.lua +++ /dev/null @@ -1,216 +0,0 @@ --- 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 = { - -- Pinned so the version below stays true: this package's version - -- means "the EGL dispatch libglvnd 1.7.0 ships", and a floating - -- payload would silently invalidate that. - deps = { runtime = { "xim:libglvnd@1.7.0.1" } }, - -- libglvnd's version, not EGL's. The spec level is 1.5 and is a - -- property of the dispatch, not of this package; what a consumer - -- pins here is which libglvnd it links. `compat.glx-headers` uses - -- the same number for the same reason. No binding-revision suffix: - -- `egl` != `libglvnd`, so there is no store collision to dodge — - -- see compat.libdrm for the case where there is. - ["1.7.0"] = { - -- 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/1.7.0/egl-1.7.0.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/f/freedesktop.egl.lua b/pkgs/f/freedesktop.egl.lua new file mode 100644 index 00000000..00cd6302 --- /dev/null +++ b/pkgs/f/freedesktop.egl.lua @@ -0,0 +1,117 @@ +-- freedesktop.egl — libEGL.so.1, plus `import egl;`. +-- +-- EGL is the window-system binding: `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. With compat.libdrm +-- (which turns the resulting buffer into a scanout via `drmModeAddFB2`) these +-- three are the whole KMS/DRM stack. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: a source build, because libglvnd is a separable project +-- +-- EGL is a Khronos SPECIFICATION; the thing you link on Linux is libglvnd's +-- vendor-neutral `libEGL.so.1`, which dlopens the actual vendor driver. This +-- package builds it, from a fork of libglvnd v1.7.0 that adds mcpp build +-- support and patches no upstream file. +-- +-- This entry REPLACES the earlier `compat.egl`, which bound `xim:libglvnd`. +-- That binding's own comment recorded the reason it was one — "libglvnd IS a +-- separable project, so by the criterion this should be a source build; it is +-- still a binding for effort alone" — and the criterion is the only thing that +-- decides. The effort was the generated dispatch (~1000 lines of Python over +-- the Khronos XML) plus `libGLdispatch.so.0`; the fork checks the generated +-- code in and builds both, so no Python and no second build system run here. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ONE ENTRY, TWO LIBRARIES +-- +-- `libGLdispatch.so.0` is built by a sibling workspace member that this one +-- reaches by PATH rather than through the index — see the fork's +-- `mcpp/egl/mcpp.toml`. GLVND exists to be the ONE dispatch point in a +-- process, and a second index entry would let a consumer name both and get two +-- package instances each building their own copy; soname reuse maps one and +-- silently discards the other. It becomes a published entry the day something +-- other than libEGL needs it — `libGL` and `libGLX` would. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE MODULE ADDS NO API +-- +-- `import egl;` replaces `#include ` and changes nothing else: +-- every exported name is upstream's, spelled upstream's way. Unlike wayland's +-- module this one needed no forwarders — EGL's entry points are declared +-- `EGLAPI … EGLAPIENTRY` with external linkage rather than `static inline`, so +-- `using ::name;` reaches all of them. +-- +-- ONE CONSEQUENCE WORTH KNOWING: the module interface unit is compiled INTO +-- `libEGL.so.1`, because mcpp links every library target against all of a +-- package's sources. So this libEGL carries `DT_NEEDED` on libstdc++, libm and +-- libgcc_s, where the ecosystem payload's carries only libGLdispatch and libc. +-- It is not a defect and not specific to EGL — `freedesktop.wayland`'s +-- libraries have exactly the same five entries for exactly the same reason — +-- but a consumer counting a dispatch library's footprint should expect it. +-- +-- The `EGL_*` CONSTANTS are macros and no module can export a macro, so a +-- consumer that needs `EGL_NO_DISPLAY` includes `` beside the +-- import — the same header the module was generated from. This is the same +-- split freedesktop.wayland-util exists to close for wayland; EGL's macros are +-- plain integer tokens rather than the statement-expressions and +-- `offsetof` arithmetic wayland's are, so there is nothing to reinterpret. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHERE THE VENDORS COME FROM, AND WHY THIS PACKAGE SETS NOTHING +-- +-- Upstream compiles in `/share/glvnd/egl_vendor.d` as the vendor +-- search path — right on a distribution, wrong the moment the payload is +-- relocated, the same shape as libgbm's compiled-in backend path. The fork +-- compiles in an EMPTY default on purpose, so a missing declaration surfaces +-- as "no vendor found" rather than as silently loading the HOST's driver into +-- a sandboxed process. +-- +-- The mechanism for the real path is libglvnd's own +-- `__EGL_VENDOR_LIBRARY_DIRS`, and supplying it is the ENVIRONMENT's job: +-- `xim:mesa` declares it through the graphics discovery layer +-- (openxlings/xim-pkgindex#713), exactly as it declares `GBM_BACKENDS_PATH` +-- for compat.libgbm. So this package declares no runtime dependency on Mesa +-- either: a consumer that only wants the client-side API (`eglQueryString`, +-- `eglGetProcAddress`, the device enumeration extensions) needs no driver at +-- all, and one that wants to render adds compat.libgbm, which pins the payload. +-- +-- X11 IS NOT A DEPENDENCY, and that is worth stating because it usually is. +-- `ENABLE_EGL_X11` is deliberately unset in the fork, so the X11 platform is +-- not compiled and Xorg reaches no consumer — including the headless GBM ones, +-- which have no display at all. Consumers that want it add compat.x11 and +-- define `USE_X11` for the header's own `#elif`. +package = { + spec = "1", + namespace = "freedesktop", + name = "egl", + description = "libEGL — GLVND's vendor-neutral EGL dispatch, built from source, with a C++23 module", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/libglvnd", + type = "package", + + xpm = { + linux = { + -- libglvnd's version, not EGL's: the spec level is 1.5 and is a + -- property of the dispatch rather than of this package. What a + -- consumer pins here is which libglvnd it links. + ["1.7.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/libglvnd/archive/refs/tags/v1.7.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/libglvnd/releases/download/1.7.0/libglvnd-1.7.0.tar.gz", + }, + sha256 = "2bc59cb214ac2750cf09764f9b5822132be96f1c4f6fe40a2553dae8efb36663", + }, + }, + }, + + mcpp = "*/mcpp/egl/mcpp.toml", +} diff --git a/tests/examples/egl/mcpp.toml b/tests/examples/egl/mcpp.toml index e7d1acf2..8e214cbb 100644 --- a/tests/examples/egl/mcpp.toml +++ b/tests/examples/egl/mcpp.toml @@ -1,17 +1,18 @@ # 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. +# THIS MEMBER DECLARES ITS OWN [indices], and has to. The workspace root maps +# the `compat` namespace to this checkout, and an index is looked up BY +# NAMESPACE — so `freedesktop.*` would resolve against the published index +# rather than the package under test. A member-level table REPLACES the root's +# rather than merging. +[indices] +freedesktop = { path = "../../.." } + [package] -name = "egl-tests" -version = "0.1.0" +name = "egl-tests" +version = "0.1.0" +standard = "c++23" -[target.'cfg(linux)'.dependencies.compat] +# Linux-only like its neighbours; the test compiles to a no-op main() elsewhere. +[target.'cfg(linux)'.dependencies.freedesktop] egl = "1.7.0" diff --git a/tests/examples/egl/tests/egl.cpp b/tests/examples/egl/tests/egl.cpp index 9a5e659f..a69c7b21 100644 --- a/tests/examples/egl/tests/egl.cpp +++ b/tests/examples/egl/tests/egl.cpp @@ -1,29 +1,36 @@ -// compat.egl — behavioral test, runnable with no GPU and no display. +// freedesktop.egl — behavioral test, runnable with no GPU and no display. // -// What can be wrong here, in order of how quietly it fails: +// Every FUNCTION and TYPE below comes from `import egl;`. is +// included for the EGL_* CONSTANTS only: they are macros, and no module can +// export a macro. So the file compiling is itself the first assertion — the +// module's export list has to cover everything used here — and the file +// linking is the second. // -// 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. +// ───────────────────────────────────────────────────────────────────────── +// EVERY ASSERTION HOLDS WITH ZERO VENDOR DRIVERS INSTALLED. // -// 2. THE DISPATCH LIBRARY IS ABSENT while headers are present. The dlsym -// checks pin that. +// libEGL is a DISPATCH: with no vendor it can answer questions about itself +// and nothing else. Upstream returns the empty string outright when the vendor +// list is empty (`libegl.c:928`), so an "it lists some EGL_EXT_ extension" +// check measures whether the MACHINE has a driver, not whether this package +// built libEGL. Vendor-dependent facts are reported below, or asserted only +// once their precondition is visibly met. // -// 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. +// ───────────────────────────────────────────────────────────────────────── +// THE PART THAT IS EASY TO GET FALSELY GREEN // -// 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. +// The ecosystem payload `xim:libglvnd` carries its own `libEGL.so.1` with the +// SAME soname, and on a machine with the graphics stack installed it is +// reachable. Only ONE library with a given soname is ever mapped, and nothing +// warns about the other — so a test that merely calls EGL functions can pass +// while this package's build is never loaded at all. Checks 2 and 3 pin the +// identity from both directions: what the library says it is, and what path +// its code actually came from. compat.libdrm's test does the same thing for +// the same reason. #ifdef __linux__ -#include -#include +#include // the EGL_* macros only #include @@ -32,6 +39,8 @@ #include #include +import egl; + namespace { int failures = 0; @@ -39,58 +48,86 @@ int failures = 0; void check(bool ok, const char *what) { std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); - if (!ok) { - ++failures; - } + 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()); + // ── 1. The module carries the API ──────────────────────────────────── + EGLDisplay none = EGL_NO_DISPLAY; + check(none == EGL_NO_DISPLAY, "EGLDisplay and EGL_NO_DISPLAY agree"); + + // ── 2. It is GLVND's libEGL ────────────────────────────────────────── + // EGL_VERSION on EGL_NO_DISPLAY is answered by libglvnd before any vendor + // is consulted — a literal "1.5 libglvnd" — so it needs no driver and no + // display, and a different implementation would answer differently. + const char *version = eglQueryString(EGL_NO_DISPLAY, EGL_VERSION); + check(version != nullptr && std::strcmp(version, "1.5 libglvnd") == 0, + "eglQueryString(EGL_NO_DISPLAY, EGL_VERSION) is libglvnd's"); + std::printf(" EGL_VERSION: %s\n", version ? version : "(null)"); + + // ── 3. …and it is THIS package's build, not the payload's ──────────── + // Same soname, so only one libEGL.so.1 is mapped and the loser is silent. + // dladdr reports the object a symbol actually came from; if the path names + // the ecosystem payload, this package built a library nobody loads. + { + Dl_info info{}; + const bool located = + ::dladdr(reinterpret_cast(&eglQueryString), &info) != 0 + && info.dli_fname != nullptr; + check(located, "dladdr locates the loaded libEGL"); + if (located) { + const std::string from = info.dli_fname; + std::printf(" loaded from: %s\n", from.c_str()); + check(from.find("xim-x-libglvnd") == std::string::npos, + "the loaded libEGL is not the ecosystem payload's copy"); + } } - // ── 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, + // ── 4. The error path works, with no driver ────────────────────────── + // EGL_VENDOR without a display is invalid; libglvnd must report + // EGL_BAD_DISPLAY rather than crash or silently answer. This exercises + // __eglReportError and the thread-local error state. + check(eglQueryString(EGL_NO_DISPLAY, EGL_VENDOR) == nullptr, + "an invalid no-display query returns null"); + check(eglGetError() == EGL_BAD_DISPLAY, + "…and leaves EGL_BAD_DISPLAY in the thread's error state"); + + // ── 5. libEGL reaches libGLdispatch ────────────────────────────────── + // Resolving a CORE entry point runs the path from libEGL into + // libGLdispatch. That library is built by a sibling member of the same + // fork and reached by PATH rather than through the index — so this is also + // the check that the intra-package edge survived publication. + check(eglGetProcAddress("eglInitialize") != nullptr, + "eglGetProcAddress(\"eglInitialize\") resolves"); + check(eglGetProcAddress("eglNoSuchFunctionEXT") == nullptr, + "…and a name that does not exist resolves to null"); + + // ── 6. What the environment supplies, reported ─────────────────────── + // The package compiles in an EMPTY vendor-config default on purpose, so a + // driver is only ever found through `__EGL_VENDOR_LIBRARY_DIRS`, which + // `xim:mesa` declares (xim-pkgindex#713) exactly as it declares + // GBM_BACKENDS_PATH for compat.libgbm. A runner with no GPU stack is not a + // defect in this package, so none of this is asserted — but when a vendor + // IS found, the extension list must look like one. + std::puts(""); + const char *dirs = std::getenv("__EGL_VENDOR_LIBRARY_DIRS"); + std::printf(" __EGL_VENDOR_LIBRARY_DIRS = %s\n", + dirs ? dirs : "(unset — the ecosystem declares it)"); + + const char *client_exts = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); + check(client_exts != 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); - } + if (client_exts != nullptr && client_exts[0] != '\0') { + std::printf(" client extensions: %.90s%s\n", + client_exts, std::strlen(client_exts) > 90 ? "…" : ""); + check(std::strstr(client_exts, "EGL_EXT_client_extensions") != nullptr, + "a vendor was found, so the client extension list is populated"); } else { - std::printf(" (display creation is opt-in: set MCPP_RUN_EGL_DISPLAY=1)\n"); + std::puts(" client extensions: (none — no vendor driver here; the " + "dispatch itself is fine)"); } std::printf("\n%d check(s) failed\n", failures); @@ -99,9 +136,6 @@ int main() #else -int main() -{ - return 0; -} +int main() { return 0; } #endif From e8f0840bc94d35857e5ae9ade977e0dbaced2b6d Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 12:57:00 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=90=8D=E5=B8=A6?= =?UTF-8?q?=E4=B8=8A=E6=8B=A5=E6=9C=89=E6=8E=A5=E5=8F=A3=E7=9A=84=E7=BB=84?= =?UTF-8?q?=E7=BB=87,=E5=B9=B6=E4=BF=AE=E5=A5=BD=20wayland=20=E5=9B=A0?= =?UTF-8?q?=E9=87=8D=E5=88=87=20tag=20=E8=80=8C=E5=A4=B1=E6=95=88=E7=9A=84?= =?UTF-8?q?=20sha256?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠ 这个提交修复 main 上的一处失效:mcpplibs/wayland 的 v1.26.0 被重切以承载模块 重命名,于是已合并描述符里的 sha256 不再匹配,freedesktop.wayland@1.26.0 目前 装不下来。四个 wayland 描述符的 sha256 都已更新,合并即恢复。 模块重命名: wayland.client → freedesktop.wayland.client wayland.server → freedesktop.wayland.server wayland.util → freedesktop.wayland.util egl → khronos.egl 规则是「模块名跟拥有**接口**的组织,而不是发实现代码的人」。freedesktop 确实拥有 wayland 协议;EGL 则是 Khronos 的规范,libglvnd 只是实现之一、freedesktop 只是托管方 —— 所以包仍叫 freedesktop.egl,导出的模块叫 khronos.egl。 mcpplibs.openkal 已经为这条规则付过学费:它的 0.1.0 是被撤回而不是保留的,原因写在 描述符里 —— "placed the module a consumer imports under the control of the implementation, which contradicts what the specification is for"。 模块名是全局且长期的,包名不是。换实现不该逼消费者改 import,否则一个「除了 include 那一行什么都不改」的包装层就失去了意义。副作用是好的:两个 EGL 提供方现在会在模块名 上硬冲突,而不是静默共存 —— 这正是 GLVND「一个进程一个 dispatch 点」想要的。 索引里既有的惯例本来就是「namespace 不进模块名」(chriskohlhoff.asio → import asio, fmtlib.fmt → import fmt,compat.opencv → import opencv.cv),这次是把它推进一步: 不是随便一个名字,而是接口所有者的名字。 --- pkgs/f/freedesktop.egl.lua | 38 ++++++++++++++++++------ pkgs/f/freedesktop.wayland-scanner.lua | 2 +- pkgs/f/freedesktop.wayland-server.lua | 2 +- pkgs/f/freedesktop.wayland-util.lua | 2 +- pkgs/f/freedesktop.wayland.lua | 2 +- tests/examples/egl/tests/egl.cpp | 4 +-- tests/examples/wayland/tests/modules.cpp | 14 ++++----- 7 files changed, 42 insertions(+), 22 deletions(-) diff --git a/pkgs/f/freedesktop.egl.lua b/pkgs/f/freedesktop.egl.lua index 00cd6302..43611b00 100644 --- a/pkgs/f/freedesktop.egl.lua +++ b/pkgs/f/freedesktop.egl.lua @@ -1,4 +1,4 @@ --- freedesktop.egl — libEGL.so.1, plus `import egl;`. +-- freedesktop.egl — libEGL.so.1, plus `import khronos.egl;`. -- -- EGL is the window-system binding: `eglGetPlatformDisplay`, -- `eglCreateContext`, `eglCreateWindowSurface`, `eglMakeCurrent`, and the @@ -42,13 +42,33 @@ -- other than libEGL needs it — `libGL` and `libGLX` would. -- -- ───────────────────────────────────────────────────────────────────────── --- THE MODULE ADDS NO API --- --- `import egl;` replaces `#include ` and changes nothing else: --- every exported name is upstream's, spelled upstream's way. Unlike wayland's --- module this one needed no forwarders — EGL's entry points are declared --- `EGLAPI … EGLAPIENTRY` with external linkage rather than `static inline`, so --- `using ::name;` reaches all of them. +-- THE MODULE ADDS NO API, AND IS NAMED FOR THE SPECIFICATION +-- +-- `import khronos.egl;` replaces `#include ` and changes nothing +-- else: every exported name is upstream's, spelled upstream's way. Unlike +-- wayland's module this one needed no forwarders — EGL's entry points are +-- declared `EGLAPI … EGLAPIENTRY` with external linkage rather than +-- `static inline`, so `using ::name;` reaches all of them. +-- +-- `khronos.` AND NOT `freedesktop.`, even though the package is +-- `freedesktop.egl`. EGL is a Khronos SPECIFICATION with several +-- implementations; libglvnd is one, and freedesktop is only where it is +-- hosted. A module name is global and permanent in a way a package name is +-- not, so it should name the INTERFACE's owner rather than whoever supplies +-- the definitions — otherwise swapping implementations forces every consumer +-- to edit its imports, which defeats a wrapper whose whole promise is that it +-- changes nothing but the include line. +-- +-- `mcpplibs.openkal` paid for this lesson already: its 0.1.0 was withdrawn +-- rather than kept because it "placed the module a consumer imports under the +-- control of the implementation, which contradicts what the specification is +-- for". `freedesktop.wayland` resolves the other way for the same reason — +-- freedesktop DOES own the wayland protocol, so those modules are +-- `freedesktop.wayland.{client,server,util}`. +-- +-- A side effect worth having: two EGL providers in one build now collide on +-- the module name instead of coexisting silently, which is what GLVND's +-- one-dispatch-point design wants anyway. -- -- ONE CONSEQUENCE WORTH KNOWING: the module interface unit is compiled INTO -- `libEGL.so.1`, because mcpp links every library target against all of a @@ -108,7 +128,7 @@ package = { GLOBAL = "https://github.com/mcpplibs/libglvnd/archive/refs/tags/v1.7.0.tar.gz", CN = "https://gitcode.com/mcpp-res/libglvnd/releases/download/1.7.0/libglvnd-1.7.0.tar.gz", }, - sha256 = "2bc59cb214ac2750cf09764f9b5822132be96f1c4f6fe40a2553dae8efb36663", + sha256 = "d50579071c5e0b883cddae82fac3f8d7c575b6523c9ea64b855a3d6f50951d47", }, }, }, diff --git a/pkgs/f/freedesktop.wayland-scanner.lua b/pkgs/f/freedesktop.wayland-scanner.lua index 6b928130..dc748c60 100644 --- a/pkgs/f/freedesktop.wayland-scanner.lua +++ b/pkgs/f/freedesktop.wayland-scanner.lua @@ -36,7 +36,7 @@ package = { GLOBAL = "https://github.com/mcpplibs/wayland/archive/refs/tags/v1.26.0.tar.gz", CN = "https://gitcode.com/mcpp-res/wayland/releases/download/1.26.0/wayland-1.26.0.tar.gz", }, - sha256 = "0a5dd54ab01e675c9a150774ade0c1262a7ad4285e0362327f26e5612d0a2f47", + sha256 = "961a900dafe76e713c2e46d9a064ff3a6375622f4057013472a04c44d8bcb6e8", }, }, }, diff --git a/pkgs/f/freedesktop.wayland-server.lua b/pkgs/f/freedesktop.wayland-server.lua index 5b062195..89dd0854 100644 --- a/pkgs/f/freedesktop.wayland-server.lua +++ b/pkgs/f/freedesktop.wayland-server.lua @@ -36,7 +36,7 @@ package = { GLOBAL = "https://github.com/mcpplibs/wayland/archive/refs/tags/v1.26.0.tar.gz", CN = "https://gitcode.com/mcpp-res/wayland/releases/download/1.26.0/wayland-1.26.0.tar.gz", }, - sha256 = "0a5dd54ab01e675c9a150774ade0c1262a7ad4285e0362327f26e5612d0a2f47", + sha256 = "961a900dafe76e713c2e46d9a064ff3a6375622f4057013472a04c44d8bcb6e8", }, }, }, diff --git a/pkgs/f/freedesktop.wayland-util.lua b/pkgs/f/freedesktop.wayland-util.lua index 4fcf02e7..bc12e7b7 100644 --- a/pkgs/f/freedesktop.wayland-util.lua +++ b/pkgs/f/freedesktop.wayland-util.lua @@ -36,7 +36,7 @@ package = { GLOBAL = "https://github.com/mcpplibs/wayland/archive/refs/tags/v1.26.0.tar.gz", CN = "https://gitcode.com/mcpp-res/wayland/releases/download/1.26.0/wayland-1.26.0.tar.gz", }, - sha256 = "0a5dd54ab01e675c9a150774ade0c1262a7ad4285e0362327f26e5612d0a2f47", + sha256 = "961a900dafe76e713c2e46d9a064ff3a6375622f4057013472a04c44d8bcb6e8", }, }, }, diff --git a/pkgs/f/freedesktop.wayland.lua b/pkgs/f/freedesktop.wayland.lua index 3a28049e..a9855c5d 100644 --- a/pkgs/f/freedesktop.wayland.lua +++ b/pkgs/f/freedesktop.wayland.lua @@ -36,7 +36,7 @@ package = { GLOBAL = "https://github.com/mcpplibs/wayland/archive/refs/tags/v1.26.0.tar.gz", CN = "https://gitcode.com/mcpp-res/wayland/releases/download/1.26.0/wayland-1.26.0.tar.gz", }, - sha256 = "0a5dd54ab01e675c9a150774ade0c1262a7ad4285e0362327f26e5612d0a2f47", + sha256 = "961a900dafe76e713c2e46d9a064ff3a6375622f4057013472a04c44d8bcb6e8", }, }, }, diff --git a/tests/examples/egl/tests/egl.cpp b/tests/examples/egl/tests/egl.cpp index a69c7b21..5c3e2970 100644 --- a/tests/examples/egl/tests/egl.cpp +++ b/tests/examples/egl/tests/egl.cpp @@ -1,6 +1,6 @@ // freedesktop.egl — behavioral test, runnable with no GPU and no display. // -// Every FUNCTION and TYPE below comes from `import egl;`. is +// Every FUNCTION and TYPE below comes from `import khronos.egl;`. is // included for the EGL_* CONSTANTS only: they are macros, and no module can // export a macro. So the file compiling is itself the first assertion — the // module's export list has to cover everything used here — and the file @@ -39,7 +39,7 @@ #include #include -import egl; +import khronos.egl; namespace { diff --git a/tests/examples/wayland/tests/modules.cpp b/tests/examples/wayland/tests/modules.cpp index 8047a20b..54c1462b 100644 --- a/tests/examples/wayland/tests/modules.cpp +++ b/tests/examples/wayland/tests/modules.cpp @@ -1,6 +1,6 @@ // The module wrappers, used the way a consumer would. // -// The claim they make is narrow and testable: `import wayland.client;` gives +// The claim they make is narrow and testable: `import freedesktop.wayland.client;` gives // you the same entities `#include ` does, spelled the same // way. So this file includes NOTHING from wayland — no header — and still // calls the stock API. If an export were missing, this would not compile; if a @@ -15,9 +15,9 @@ #include #include -import wayland.client; -import wayland.server; -import wayland.util; +import freedesktop.wayland.client; +import freedesktop.wayland.server; +import freedesktop.wayland.util; namespace { @@ -46,7 +46,7 @@ int main() // A protocol interface object: generated code, exported by the module. check(std::string(wl_registry_interface.name) == "wl_registry", - "wl_registry_interface arrives through wayland.client"); + "wl_registry_interface arrives through freedesktop.wayland.client"); check(std::string(wl_compositor_interface.name) == "wl_compositor", "so does wl_compositor_interface"); } @@ -54,7 +54,7 @@ int main() // ── 2. Server entities ─────────────────────────────────────────────── { wl_display *s = wl_display_create(); - check(s != nullptr, "wl_display_create through wayland.server"); + check(s != nullptr, "wl_display_create through freedesktop.wayland.server"); if (s != nullptr) { wl_event_loop *loop = wl_display_get_event_loop(s); check(loop != nullptr, "wl_display_get_event_loop returns a loop"); @@ -63,7 +63,7 @@ int main() } // ── 3. The macros, as entities ─────────────────────────────────────── - // These are the names a module cannot export as macros; wayland.util + // These are the names a module cannot export as macros; freedesktop.wayland.util // carries them as a constant, a template and ranges. Exercised here in a // consumer rather than only in the package's own test. { From cf6023bc09d9b54b727efff807ef2cad32fb5076 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 13:05:39 +0800 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20=E7=AC=AC=E5=9B=9B=E8=BD=AE?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=20=E2=80=94=E2=80=94=20EGL=20=E6=BA=90?= =?UTF-8?q?=E7=A0=81=E5=8C=96=E3=80=81=E6=A8=A1=E5=9D=97=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E5=BD=92=E5=B1=9E=E3=80=81=E6=B2=99=E7=AE=B1=E5=B9=B2=E5=87=80?= =?UTF-8?q?=E6=88=BF=E9=97=B4=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 跨仓主文档补上 §19,涵盖这一轮的四件事: §19.2 模块命名。规则定为「跟拥有**接口**的组织,不跟发实现的人」,依据是 mcpplibs.openkal 已经付过的学费(0.1.0 被撤回,因为它把消费者 import 的模块放在了 实现的控制之下)。于是 wayland 三个模块加 freedesktop 前缀,EGL 的模块叫 khronos.egl —— 包名与模块名故意不一致,因为 freedesktop 发这份代码而 Khronos 拥有这份规范。 §19.3 生成器该放哪。判据是「依不依赖目标平台」:能预先算定的签进仓 + CI diff(两个 fork 的构建路径上都没有 Python),依赖架构算不出来的才进 build.mcpp(GLdispatch 的 entry stub)。把 genmod.py 改写成 build.mcpp 反而会把生成器塞进每个消费者的构建。 §19.4 沙箱干净房间验证。合成 home、从零 clone + 装 + 构建,而沙箱里 /usr 是宿主的 —— 所以证的不是「宿主不在」,而是「宿主在、可达、且全部落败」。整条链跑通, eglInitialize 拿到 EGL 1.5 vendor Mesa Project,gbm_bo_create 真分配出 256x256。 顺带记了三个坑,其中 [indices] 同一 path 挂两个键会产生二义那条最耗时间。 §19.6 记一处自己造成的破坏:为承载模块重命名原地重切了 wayland 的 v1.26.0,于是在本 PR 合并前 main 上的 sha256 对不上。正确次序应当是先改描述符再重切 tag。同版本重切还 会被 store 掩盖(只按 name+version 索引),清理命令一并记下。 §19.5 xlings pin:§15「不下调到 .4」的结论仍然成立,但那条要求指出的缺口是真的 —— 两个 fork 的 CI 根本没钉,现已钉到与 kXlingsVersion 一致的 .5。 --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 190 ++++++++++++++++++ 1 file changed, 190 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 a9684ffa..ea73cd83 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 @@ -9,6 +9,11 @@ Date: 2026-08-30 · 起因:`compat.libgbm`(mcpp-index PR #281)· 状态:待 revi | 想知道 | 看哪节 | 注意 | |---|---|---| +| **最新一轮(EGL 源码化 + 模块命名)** | **§19** | 最权威;§19.6 记了一处自己造成的破坏 | +| **沙箱干净房间验证** | **§19.4** | 宿主库在场且可达却全部落败 | +| 模块该叫什么名字 | §19.2 | 跟接口的所有者,不跟发实现的人 | +| 生成器放 build.mcpp 还是签进仓 | §19.3 | 判据是「依不依赖目标平台」 | +| xlings 该 pin 哪个版本 | §15 + §19.5 | §15 的结论仍成立,§19.5 补了它指出的真缺口 | | **最终结论与交付** | §12(实现结果)、§13(状态)、§14(要不要做 mesa 包) | 权威 | | 为什么 `compat.libgbm` 该独立存在 | §3、§10.1 | 理由换过三次,结论没变 | | 任务拆分与依赖 | §11 | | @@ -1113,3 +1118,188 @@ backends dir: /subos/default/usr/lib/gbm ← subos 给的,不是 `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 一起动」这条早已漂移的不变量恢复了。 + +--- + +## 19. 第四轮:EGL 从绑定改为源码构建,并统一模块命名(2026-08-30 下午) + +§18 之后栈里还剩一处不自洽:`compat.egl` 是绑定,而它**自己的注释**写着这是错的 —— +「libglvnd **is** a separable project,按判据本该源码构建,现在仍是绑定纯粹是工作量问题」。 +判据里没有「工作量」这一项。这一节把这笔账还上,并顺带解决了模块命名的归属问题。 + +### 19.1 交付物 + +| 仓 | 变更 | 状态 | +|---|---|---| +| **mcpplibs/libglvnd**(新建) | libglvnd v1.7.0 + mcpp 构建支持,出 `libEGL.so.1` 与 `libGLdispatch.so.0` | `v1.7.0`,CI 四 job 全绿 | +| **mcpplibs/wayland** | 三个模块重命名;CI 钉 xlings | `v1.26.0` 重切,CI 全绿 | +| **mcpp-index** | `compat.egl` → `freedesktop.egl`;5 个描述符 sha256;测试成员;文档 | PR #293 | +| **mcpp-community/mcpp** | 示例 09 改用 `freedesktop.egl`,打印两个发现变量 | 待 #293 合并后推 | +| mcpp-res(gitcode) | wayland / libglvnd 两个 CN 镜像 | 已发布,与 GLOBAL 字节一致 | + +### 19.2 模块命名:跟**接口的所有者**,不跟发实现的人 + +索引既有的惯例是「namespace 不进模块名」,这一点有反例可证: + +| 包 | 模块 | +|---|---| +| `chriskohlhoff.asio` | `import asio` | +| `fmtlib.fmt` | `import fmt` | +| `boost-ext.ut` | `import boost.ut` | +| `opencv.opencv` | `import opencv.cv` | + +这一轮把它推进一步:模块名不是随便挑的,而是**拥有那份接口的组织**。于是 + +``` +wayland.client → freedesktop.wayland.client freedesktop 确实拥有 wayland 协议 +wayland.server → freedesktop.wayland.server +wayland.util → freedesktop.wayland.util +egl → khronos.egl EGL 是 Khronos 规范 +``` + +**注意 EGL 的包名与模块名故意不一致**:包是 `freedesktop.egl`(freedesktop 确实在发 +这份代码),模块是 `khronos.egl`(Khronos 拥有这份规范,libglvnd 只是实现之一)。 + +依据不是审美,是 `mcpplibs.openkal` 已经付过的学费 —— 它的 0.1.0 是**被撤回**而不是保留的: + +> It placed the module a consumer imports **under the control of the +> implementation**, which contradicts what the specification is for + +模块名是全局且长期的,包名不是。换实现不该逼消费者改 `import`,否则一个「除了 include +那一行什么都不改」的包装层就失去了意义。副作用是好的:两个 EGL 提供方现在会在模块名上 +**硬冲突**,而不是静默共存 —— 这正是 GLVND「一个进程一个 dispatch 点」想要的。 + +### 19.3 build.mcpp 与「签进仓的生成物」的分界 + +这一轮两次遇到「生成器该放哪」,答案不一样,规则是同一条: + +> **build.mcpp 管的是「依赖目标平台、算不出来」的决定;能预先算定的生成物就签进仓 + CI diff。** + +| 生成物 | 依赖目标? | 放哪 | +|---|---|---| +| libglvnd 的 dispatch 表(~1000 行 Python × 2.7MB gl.xml) | 否 | `mcpp/generated/`,CI 重生成并 diff | +| wayland 的协议代码 + 模块包装(`genmod.py`) | 否 | `mcpp/generated/`,CI 重生成并 diff | +| GLdispatch 的 **entry stub 选择** | **是**(架构 × 线程存储模型) | `build.mcpp`,用 `mcpp::target_arch()` | + +两个 fork 的**构建路径上都没有 Python**(`mcpp.toml` / `build.mcpp` / 根清单都不提它), +消费者只需要 mcpp。把 `genmod.py` 改写成 build.mcpp 反而会**把生成器塞进每个消费者的 +构建**,并且会撞上 **mcpp#534**(build.mcpp 的 action 产物与本包编译之间没有 order-only 边) +—— wayland 的协议代码当初签进仓正是因为这个。 + +GLdispatch 那条则相反:entry stub 分架构**且**分线程存储模型,而且和 libffi 的不同, +**它们自身没有任何门控**。在 `sources` 里写死 x86_64 会让包只能在 x86_64 上用、而且哪儿 +都不写明;`build.mcpp` 用 `mcpp::target_arch()` 做的正是上游 `gl_dispatch_type` 的选择。 + +### 19.4 生态真实验证:沙箱干净房间(`--sandbox --gpu`) + +§12.3 验证过的是 **GBM**。这一轮验证的是**整条链**,而且在一个 `/home/speak` 是合成的、 +开发机上任何 checkout / 缓存 / 构建树都进不来的沙箱里,**从零 clone + 安装 + 构建**。 + +关键在于:**沙箱里 `/usr` 是宿主的**。 + +``` +host has /usr/lib/x86_64-linux-gnu/libEGL.so.1 +host has /usr/lib/x86_64-linux-gnu/libgbm.so.1 +host has /usr/lib/x86_64-linux-gnu/libdrm.so.2 +host has /usr/lib/x86_64-linux-gnu/libwayland-client.so.0 +``` + +所以要证的**不是**「宿主不在」,而是「宿主在、可达、且依然全部落败」—— 后者才是用户机器 +上会发生的情形。用二进制自己的 loader 列闭包: + +``` +libgbm.so.1 => /xpkgs/compat-x-libgbm/25.0.7/.../libgbm.so.1 +libdrm.so.2 => /target/.../bin/libdrm.so.2 +libEGL.so.1 => /target/.../bin/libEGL.so.1 +libGLdispatch.so.0 => /target/.../bin/libGLdispatch.so.0 +libwayland-client.so.0 => /target/.../bin/libwayland-client.so.0 +libwayland-server.so.0 => /target/.../bin/libwayland-server.so.0 +libffi.so.8 => /target/.../bin/libffi.so.8 + +PASS: the host's copies were present and reachable, and none of them won +``` + +真跑(`--gpu`): + +``` +EGL_VERSION 1.5 libglvnd ← 自建 dispatch 在答 +/dev/dri/renderD128 drm driver nvidia-drm + eglInitialize EGL 1.5, vendor Mesa Project +/dev/dri/card0 drm driver simpledrm + gbm_bo_create 256x256 stride=1024 ← 真分配出 buffer object + eglInitialize EGL 1.5, vendor Mesa Project +``` + +`import khronos.egl;` 与两个 `import freedesktop.wayland.*;` 一起编译链接通过 —— +模块层与 C 库在同一次干净构建里都成立。 + +复现脚本见提交历史;三个坑值得记下: + +1. **`[indices]` 同一个 path 不能挂两个键**。写 `compat` 和 `freedesktop` 都指向同一个 + checkout,会注册成两个独立 project,之后每次查找都报 + `package 'compat:libdrm@2.4.134' is ambiguous, candidates: … 'compat' … 'freedesktop'`。 + 只重定向被测的那个 namespace,其余走已发布索引。 +2. **沙箱里 `find ~/.xlings -name mcpp` 会抓到别的 subos 的二进制**,它们的 interpreter + 不在,失败信息是干巴巴的 `not found`,读起来像「mcpp 没装上」。要先试本 subos 的 `bin/`。 +3. **沙箱不挂 cwd**,落点是合成的 `/home/speak`(只有 dotfile)。脚本要放进 subos 目录 + 才进得去。 + +### 19.5 xlings pin:§15 的结论仍然成立,但它指出的缺口是真的 + +任务里再次出现「pin 内部依赖的 xlings 到 `2026.8.27.4`」。**§15 已经查过并否掉了**: +`.4` 比 `.5` **旧**三小时,三仓的 `kXlingsVersion` 已经都在 `.5`,而 `.5` 有 `.4` 没有的 +行为(声明在解析时压过索引)。下调等于让生态退回一个能力更弱的版本。 + +但这条要求指出的**缺口是真的,只是位置不对**:两个 fork 的 CI **根本没钉**, +`curl … | bash` 拿的是当天最新。已改为钉 `XLINGS_VERSION: "2026.8.27.5"`,与 +`kXlingsVersion` 一致。 + +**mcpp 本身仍不钉**,这是刻意的:这些包依赖的正是**生态**(`xim:mesa` 声明 +`GBM_BACKENDS_PATH` / `__EGL_VENDOR_LIBRARY_DIRS`),而钉死的 mcpp tarball 带的是生态的 +冻结快照。**钉住装的工具,放开被测的生态** —— 这个切分才让失败可归因。 + +### 19.6 一个自己造成的破坏,记下来 + +为了承载模块重命名,`mcpplibs/wayland` 的 `v1.26.0` 被**原地重切**(上游没有更新版本 +可跟,而版本号必须与上游对齐)。后果是立即的: + +``` +main 上 freedesktop.wayland.lua 的 sha256 0a5dd54a… +tag 上 tarball 的实际 sha256 961a900d… ← 不匹配 +``` + +在 PR #293 合并之前,`freedesktop.wayland@1.26.0` 从 main 装不下来。**正确的次序应当是 +先改描述符、合并,再重切 tag**,而不是反过来。 + +还有一个更隐蔽的:store 只按 `(name, version)` 索引,所以**已经装过 1.26.0 的机器不会 +重新下载**,会继续用旧模块名而毫无提示 —— 这正是 +`stale-global-index-masks-descriptor-bugs` 那条。开发机上需要手动清: + +```bash +rm -rf ~/.mcpp/registry/data/xpkgs/freedesktop-x-wayland*/1.26.0 \ + ~/.mcpp/registry/data/xpkgs/freedesktop-x-egl/1.7.0 \ + ~/.mcpp/build-cache/v1/pkg/freedesktop ~/.mcpp/build-cache/v1/tool/freedesktop +``` + +### 19.7 多角度审视 + +| 角度 | 这一轮的结果 | +|---|---| +| **架构** | 五个包里只剩 GBM 一个绑定,而它是唯一真正过不了「可否独立分发」的。判据终于和实现一致 | +| **稳定性** | CI 新增「符号内容 + obj 计数」——SONAME 检查对**空库**也会绿。两个 fork 都钉了 xlings | +| **优雅/简洁** | 一个索引条目出两个库(`libGLdispatch` 走 path 依赖),消费者只写一行 | +| **用户体验** | `import khronos.egl;` 换实现不用改代码;`EGL_*` 宏仍需 include,这一点写进了描述符 | +| **兼容性** | 模块重命名是**破坏性**的。选在 wayland 合并当天做,消费者只有示例 09 | +| **跨平台** | 按架构选 stub 移进 `build.mcpp`,aarch64/ppc64 由构造成立而非靠改代码 | +| **一致性** | 模块命名规则统一为「接口所有者」,与 openkal 的教训对齐 | +| **无感升级** | ⚠ **没做到**,见 §19.6:同版本重切 tag 会被 store 掩盖。这是本轮唯一的真缺陷 | + +### 19.8 仍未闭合 + +- **PR #293 必须合并**才能修好 main 上 wayland 的 sha256(§19.6)。 +- 示例 09 的更新(mcpp PR #532)在本地待推,依赖 #293 先合。 +- `libGL` / `libGLX` / `libGLESv{1,2}` 未构建。各自是 fork 里一个成员加一张 + `mcpp/generated/` 里的 dispatch 表;索引里目前没有消费者。 +- `freedesktop.egl` 的 `libEGL.so.1` 比 payload 的多三条 `DT_NEEDED` + (libstdc++/libm/libgcc_s),因为模块接口单元被编译进库里。`freedesktop.wayland` 完全 + 同形,是「模块层随库一起发」的既定结果,已写进描述符。 From 2f4458d8dfb871985b01194349a2af3224a54543 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 13:07:02 +0800 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20=E6=A8=A1=E5=9D=97=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E8=A7=84=E5=88=99=E5=86=99=E8=BF=9B=E4=B8=A4=E4=BB=BD?= =?UTF-8?q?=20descriptor-examples,=E5=B9=B6=E4=BF=AE=E6=8E=89=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E7=AC=A6=E9=A6=96=E8=A1=8C=E7=9A=84=E6=97=A7=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 规则:模块名跟拥有**接口**的组织。索引早先的惯例是把 namespace 整个丢掉 (chriskohlhoff.asio → import asio),这一条是把那个惯例说具体 —— 模块名全局且长期, 包名不是,所以它该说清楚谁拥有这份接口。EGL 那一行顺带解释了包名与模块名为什么故意 不一致。 --- docs/descriptor-examples.md | 2 +- docs/zh/descriptor-examples.md | 2 +- pkgs/f/freedesktop.wayland-server.lua | 2 +- pkgs/f/freedesktop.wayland.lua | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index dc6e9959..2e5a3327 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -33,7 +33,7 @@ in the [root README](../README.md#reference-examples). | 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. **Why not a source build**: libgbm is a build target *inside* Mesa, not a separable project — `src/gbm/meson.build` is `link_with: [libloader]`, and `libloader` wants `idep_mesautil`, ~120 TUs of Mesa's internal util library for exactly one function, `loader_open_driver_lib`; and it is a **loader**, whose `gbm_create_device()` dlopens Mesa's own `dri_gbm.so`, so built apart from Mesa it would have nothing to load. **The test on this row is separability, NOT whether the payload also carries a copy** — `compat.libdrm`, from the same stack, passes it and is a source build (see the row below); the two coexist, measured. **What it deliberately does NOT do**: set the backend search path. `GBM_BACKENDS_PATH` is Mesa's own mechanism and the ENVIRONMENT's job — `xim:mesa` declares it through the graphics discovery layer (openxlings/xim-pkgindex#713), so this package sets nothing. **Two directory keys, not one**: `library_dirs` renders as `-Wl,-rpath` and `link_library_dirs` as `-L`, so a package that is *linked against* needs both — with the first alone the build still dies at `ld: cannot find -lgbm`) (EGL used to sit on this row as `compat.egl`, bound to `xim:libglvnd`. Its own comment recorded why that was wrong — "libglvnd **is** a separable project, so by the criterion this should be a source build; it is still a binding for effort alone" — and effort is not the criterion, so it moved to the row below as [`freedesktop.egl`](../pkgs/f/freedesktop.egl.lua)) | -| Source build that needs a real project (Form A fork) | [`freedesktop.wayland`](../pkgs/f/freedesktop.wayland.lua) · [`freedesktop.wayland-server`](../pkgs/f/freedesktop.wayland-server.lua) · [`freedesktop.wayland-scanner`](../pkgs/f/freedesktop.wayland-scanner.lua) · [`freedesktop.wayland-util`](../pkgs/f/freedesktop.wayland-util.lua) (wayland 1.26.0, four entries out of one tarball — [mcpplibs/wayland](https://github.com/mcpplibs/wayland), a fork that patches no upstream file. **Why a fork and not an inline descriptor**: wayland is mostly GENERATED — `protocol/wayland.xml` describes every interface and wayland-scanner emits ~13,000 lines from it — and the generator is a C program in the same tree, so it must be COMPILED before it can run. An inline descriptor has no build step, and `install()` cannot do it either: mcpp compiles a package's sources at CONSUMER-BUILD time, so no package binary exists while another package installs. `build.mcpp` is the mechanism, and it only exists for a real mcpp project. **Why four packages**: `libwayland-client.so.0` and `libwayland-server.so.0` are distinct SONAMEs that Mesa's libEGL_mesa needs BOTH of, and mcpp links every library target against ALL of a package's sources — measured: a feature-gated second target still receives the feature's objects — so one package cannot emit two libraries with disjoint contents. mcpp's own diagnostic names the remedy: "split into a workspace member". **The module layer ships with the library, not beside it**: each package carries its C library AND its `.cppm`, so there is one package per library rather than a C one and a module one. The wrappers add no API — every exported name is upstream's, and the lists are generated from the public headers so a version bump cannot silently drop one. **Macros are the one thing that cannot cross**: `export` names entities and a macro is not one, so `wayland-util` maps each to what it actually is — `WL_MARSHAL_FLAG_DESTROY` to a constexpr, `wl_container_of` to a function template, the six `*_for_each` loops to ranges — and its test instantiates all of them including the `_safe` removal guarantee) · [`freedesktop.egl`](../pkgs/f/freedesktop.egl.lua) (libglvnd 1.7.0 — [mcpplibs/libglvnd](https://github.com/mcpplibs/libglvnd), and the entry that replaced the `compat.egl` binding on the row above. **The generated code is checked in rather than generated**: upstream drives ~1000 lines of Python over a 2.7 MB `gl.xml` to emit the dispatch tables, and that output is deterministic from a pinned registry — so it lives in `mcpp/generated/` and NO generator runs at build time, with CI regenerating and diffing it instead. `mcpp build` is the whole toolchain. **The per-architecture choice lives in `build.mcpp`, not in the manifest**: GLdispatch's entry stubs are per-arch *and* per-thread-storage-model, and unlike libffi's they carry no guards of their own, so hard-coding x86_64 in `sources` would make the package x86_64-only and say so nowhere; `build.mcpp` makes upstream's own `gl_dispatch_type` choice from `mcpp::target_arch()`. **One index entry, two libraries**: `libGLdispatch.so.0` is a sibling workspace member reached by a PATH dependency — being the one dispatch point in a process is GLVND's whole purpose, and a second index entry would let a consumer name both and resolve two instances, with soname reuse mapping one and silently discarding the other. **The compiled-in vendor path is deliberately EMPTY**: upstream bakes in `/share/glvnd/egl_vendor.d`, which after relocation means the HOST's directory, so an empty fallback makes a missing ecosystem declaration surface as "no vendor found" rather than as silently loading the host's driver — the same stance compat.libgbm takes with `GBM_BACKENDS_PATH`) | +| Source build that needs a real project (Form A fork) | [`freedesktop.wayland`](../pkgs/f/freedesktop.wayland.lua) · [`freedesktop.wayland-server`](../pkgs/f/freedesktop.wayland-server.lua) · [`freedesktop.wayland-scanner`](../pkgs/f/freedesktop.wayland-scanner.lua) · [`freedesktop.wayland-util`](../pkgs/f/freedesktop.wayland-util.lua) (wayland 1.26.0, four entries out of one tarball — [mcpplibs/wayland](https://github.com/mcpplibs/wayland), a fork that patches no upstream file. **Why a fork and not an inline descriptor**: wayland is mostly GENERATED — `protocol/wayland.xml` describes every interface and wayland-scanner emits ~13,000 lines from it — and the generator is a C program in the same tree, so it must be COMPILED before it can run. An inline descriptor has no build step, and `install()` cannot do it either: mcpp compiles a package's sources at CONSUMER-BUILD time, so no package binary exists while another package installs. `build.mcpp` is the mechanism, and it only exists for a real mcpp project. **Why four packages**: `libwayland-client.so.0` and `libwayland-server.so.0` are distinct SONAMEs that Mesa's libEGL_mesa needs BOTH of, and mcpp links every library target against ALL of a package's sources — measured: a feature-gated second target still receives the feature's objects — so one package cannot emit two libraries with disjoint contents. mcpp's own diagnostic names the remedy: "split into a workspace member". **The module layer ships with the library, not beside it**: each package carries its C library AND its `.cppm`, so there is one package per library rather than a C one and a module one. The wrappers add no API — every exported name is upstream's, and the lists are generated from the public headers so a version bump cannot silently drop one. **A module is named for the organization that owns the INTERFACE**, which is why these are `freedesktop.wayland.{client,server,util}`: freedesktop owns the wayland protocol. The index's older convention was to drop the namespace entirely (`chriskohlhoff.asio` → `import asio`, `compat.opencv` → `import opencv.cv`); this is that rule made specific, because a module name is global and permanent in a way a package name is not. `mcpplibs.openkal` paid for the lesson — its 0.1.0 was withdrawn rather than kept because it "placed the module a consumer imports under the control of the implementation, which contradicts what the specification is for". **Macros are the one thing that cannot cross**: `export` names entities and a macro is not one, so `wayland-util` maps each to what it actually is — `WL_MARSHAL_FLAG_DESTROY` to a constexpr, `wl_container_of` to a function template, the six `*_for_each` loops to ranges — and its test instantiates all of them including the `_safe` removal guarantee) · [`freedesktop.egl`](../pkgs/f/freedesktop.egl.lua) (libglvnd 1.7.0 — [mcpplibs/libglvnd](https://github.com/mcpplibs/libglvnd), and the entry that replaced the `compat.egl` binding on the row above. **The generated code is checked in rather than generated**: upstream drives ~1000 lines of Python over a 2.7 MB `gl.xml` to emit the dispatch tables, and that output is deterministic from a pinned registry — so it lives in `mcpp/generated/` and NO generator runs at build time, with CI regenerating and diffing it instead. `mcpp build` is the whole toolchain. **The per-architecture choice lives in `build.mcpp`, not in the manifest**: GLdispatch's entry stubs are per-arch *and* per-thread-storage-model, and unlike libffi's they carry no guards of their own, so hard-coding x86_64 in `sources` would make the package x86_64-only and say so nowhere; `build.mcpp` makes upstream's own `gl_dispatch_type` choice from `mcpp::target_arch()`. **The module is `khronos.egl`, not `freedesktop.egl`**, and the package/module mismatch is the point: freedesktop ships this code, but EGL is a *Khronos* specification with several implementations. Naming the module after the implementation would force every consumer to edit its imports the day a different libEGL is used — defeating a wrapper whose whole promise is that it changes nothing but the include line. It also makes two EGL providers in one build a hard conflict rather than silent coexistence, which is what GLVND wants anyway. **One index entry, two libraries**: `libGLdispatch.so.0` is a sibling workspace member reached by a PATH dependency — being the one dispatch point in a process is GLVND's whole purpose, and a second index entry would let a consumer name both and resolve two instances, with soname reuse mapping one and silently discarding the other. **The compiled-in vendor path is deliberately EMPTY**: upstream bakes in `/share/glvnd/egl_vendor.d`, which after relocation means the HOST's directory, so an empty fallback makes a missing ecosystem declaration surface as "no vendor found" rather than as silently loading the host's driver — the same stance compat.libgbm takes with `GBM_BACKENDS_PATH`) | | Source-built library sharing a soname with the payload | [`compat.libdrm`](../pkgs/c/compat.libdrm.lua) (libdrm 2.4.134 — five TUs plus an inlined `generated_static_table_fourcc.h`, and zero dependencies. It carries the same `libdrm.so.2` the ecosystem's Mesa payload does, and that is **not** a reason to bind: a DT_NEEDED soname already in the link map is REUSED, ld.so never searches again, so Mesa's `libgbm.so.1` binds to the copy the consumer linked — measured: one mapping in the process, and Mesa's GBM allocated a buffer through it. It holds only for `kind = "shared"` with the right soname: as the default `kind = "lib"` there is no `.so` to reuse, the payload's loads for Mesa, the consumer keeps its own merged one, and libdrm's file-static state (`drmHashTable`, `nr_fds`, `connection`) exists twice over one set of fds. The test asserts with `dladdr` that the loaded library is this package's. **Two include roots**: the public headers sit at the source root and the uapi headers under `include/drm/`, and `xf86drm.h` line 40 is a bare `#include `) · [`compat.libffi`](../pkgs/c/compat.libffi.lua) (3.4.8. The x86 assembly **gates itself** — `ffi.c`/`ffiw64.c`/`unix64.S`/`sysv.S`/`win64.S` each open with `#ifdef __x86_64__` or `__i386__` — so all of them are listed and the preprocessor selects. `fficonfig.h` is configure's record of what it probed and cannot be derived, so it is inlined; `ffi.h` CAN be derived, so install() performs configure's four substitutions on `ffi.h.in` and stays honest across a version bump) · [`compat.expat`](../pkgs/c/compat.expat.lua) (2.7.1, three TUs — `xmltok_impl.c` and `xmltok_ns.c` are `#include`d BY `xmltok.c`, five times between them, so listing them would produce duplicate symbols) | | 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/`) | diff --git a/docs/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index 78eb62e7..d0f5269e 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -31,7 +31,7 @@ | 互斥后端(同包多后端二选一) | [`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`;而且它是个 **loader**,`gbm_create_device()` dlopen 的后端就是 Mesa 自己的 `dri_gbm.so`,脱离 Mesa 建出来就是个没东西可加载的 loader。**注意这一行的判据是「可否独立分发」,不是「会不会和 payload 共存」** —— 同处一栈的 `compat.libdrm` 满足前者,于是走源码构建(见「与 payload 同 soname 的源码库」一行),两者共存已实测无碍。**它刻意不做的事**:设置后端搜索路径。`GBM_BACKENDS_PATH` 是 Mesa 自己的机制,归**环境**管 —— `xim:mesa` 通过 graphics discovery 层声明它(openxlings/xim-pkgindex#713),所以这个包什么都不设。**两个目录键都要**:`library_dirs` 渲染成 `-Wl,-rpath`、`link_library_dirs` 渲染成 `-L`,一个**会被链接**的包两个都得写,只写前者会死在 `ld: cannot find -lgbm`)(EGL 原本就在这一行,叫 `compat.egl`,绑的是 `xim:libglvnd`。它自己的注释已经写明了那是错的 ——「libglvnd **是**独立项目,按判据本该源码构建,现在仍是绑定纯粹是工作量问题」—— 而判据里没有「工作量」这一项,所以它挪到了下一行,成为 [`freedesktop.egl`](../../pkgs/f/freedesktop.egl.lua))| -| 需要真实工程的源码构建(Form A fork) | [`freedesktop.wayland`](../../pkgs/f/freedesktop.wayland.lua) · [`freedesktop.wayland-server`](../../pkgs/f/freedesktop.wayland-server.lua) · [`freedesktop.wayland-scanner`](../../pkgs/f/freedesktop.wayland-scanner.lua) · [`freedesktop.wayland-util`](../../pkgs/f/freedesktop.wayland-util.lua)(wayland 1.26.0,一个 tarball 出四个条目 —— [mcpplibs/wayland](https://github.com/mcpplibs/wayland),不改上游任何文件的 fork。**为什么要 fork 而不是内联描述符**:wayland 大部分是**生成**出来的 —— `protocol/wayland.xml` 描述了每个接口,wayland-scanner 从它生成约 13000 行 —— 而生成器是同一棵树里的 C 程序,**必须先被编译**才能跑。内联描述符没有构建步骤;`install()` 也做不到,因为 mcpp 是在**消费者构建期**才编译包源码,别的包安装时不存在任何包产物。`build.mcpp` 正是为此而设,而它只存在于真实的 mcpp 工程里。**为什么是四个包**:`libwayland-client.so.0` 与 `libwayland-server.so.0` 是两个不同 soname,Mesa 的 libEGL_mesa 对**两者都**有 DT_NEEDED,所以必须是内容互不相交的两个文件;而 mcpp 把包的源码编一次、让**每个**库 target 都链接全部对象 —— 实测:feature 门控的第二个 target 照样拿到该 feature 的对象 —— 一个包出不了两个互不相交的库。mcpp 自己的告警就点了办法:「split into a workspace member」。**模块层与库同包**:每个包同时带 C 库和它的 `.cppm`,所以是一库一包,而不是 C 一个、模块一个。包装层**不增加 API**:导出名全是上游的,而且列表是从公开头**生成**的,版本一动不会悄悄少一个名字。**宏是唯一过不去的东西**:`export` 命名的是实体而宏不是,所以 `wayland-util` 把每个宏映射成它实际是的东西 —— `WL_MARSHAL_FLAG_DESTROY` → constexpr、`wl_container_of` → 函数模板、六个 `*_for_each` → range,它的测试把它们全部实例化,包括 `_safe` 的「循环体内可删除当前元素」保证)· [`freedesktop.egl`](../../pkgs/f/freedesktop.egl.lua)(libglvnd 1.7.0 —— [mcpplibs/libglvnd](https://github.com/mcpplibs/libglvnd),就是把上一行 `compat.egl` 绑定换掉的那个条目。**生成物是签进仓的,不是构建时生成的**:上游要跑约 1000 行 Python 处理 2.7 MB 的 `gl.xml` 才能得到 dispatch 表,而这份输出在 registry 固定的前提下是确定性的 —— 于是它躺在 `mcpp/generated/`,构建期**不跑任何生成器**,改由 CI 重新生成并 diff。`mcpp build` 就是全部工具链。**按架构选文件这件事放在 `build.mcpp` 里,不在清单里**:GLdispatch 的 entry stub 既分架构又分线程存储模型,而且和 libffi 的不同,它们自身**没有**任何门控 —— 在 `sources` 里写死 x86_64 会让这个包只能在 x86_64 上用、而且哪儿都不写明;`build.mcpp` 用 `mcpp::target_arch()` 做的正是上游 `gl_dispatch_type` 的那个选择。**一个索引条目,两个库**:`libGLdispatch.so.0` 由同 workspace 的兄弟成员构建,通过 **path** 依赖引入而不是走索引 —— 「进程里只有一个 dispatch 点」是 GLVND 存在的全部理由,而两个索引条目会让消费者把两个都写上、解析出两个包实例,soname 复用只映射其中一个、另一个被静默丢弃。**编译进去的 vendor 路径刻意是空的**:上游烤进 `/share/glvnd/egl_vendor.d`,重定位之后那就是 **host** 的目录,所以留空能让「生态没声明」表现为「找不到 vendor」,而不是悄悄把宿主驱动装进沙箱进程 —— 与 compat.libgbm 对 `GBM_BACKENDS_PATH` 的立场一致)| +| 需要真实工程的源码构建(Form A fork) | [`freedesktop.wayland`](../../pkgs/f/freedesktop.wayland.lua) · [`freedesktop.wayland-server`](../../pkgs/f/freedesktop.wayland-server.lua) · [`freedesktop.wayland-scanner`](../../pkgs/f/freedesktop.wayland-scanner.lua) · [`freedesktop.wayland-util`](../../pkgs/f/freedesktop.wayland-util.lua)(wayland 1.26.0,一个 tarball 出四个条目 —— [mcpplibs/wayland](https://github.com/mcpplibs/wayland),不改上游任何文件的 fork。**为什么要 fork 而不是内联描述符**:wayland 大部分是**生成**出来的 —— `protocol/wayland.xml` 描述了每个接口,wayland-scanner 从它生成约 13000 行 —— 而生成器是同一棵树里的 C 程序,**必须先被编译**才能跑。内联描述符没有构建步骤;`install()` 也做不到,因为 mcpp 是在**消费者构建期**才编译包源码,别的包安装时不存在任何包产物。`build.mcpp` 正是为此而设,而它只存在于真实的 mcpp 工程里。**为什么是四个包**:`libwayland-client.so.0` 与 `libwayland-server.so.0` 是两个不同 soname,Mesa 的 libEGL_mesa 对**两者都**有 DT_NEEDED,所以必须是内容互不相交的两个文件;而 mcpp 把包的源码编一次、让**每个**库 target 都链接全部对象 —— 实测:feature 门控的第二个 target 照样拿到该 feature 的对象 —— 一个包出不了两个互不相交的库。mcpp 自己的告警就点了办法:「split into a workspace member」。**模块层与库同包**:每个包同时带 C 库和它的 `.cppm`,所以是一库一包,而不是 C 一个、模块一个。包装层**不增加 API**:导出名全是上游的,而且列表是从公开头**生成**的,版本一动不会悄悄少一个名字。**模块名跟的是拥有「接口」的组织**,所以这三个叫 `freedesktop.wayland.{client,server,util}` —— wayland 协议确实归 freedesktop。索引早先的惯例是把 namespace 整个丢掉(`chriskohlhoff.asio` → `import asio`、`compat.opencv` → `import opencv.cv`),这一条是把那个惯例说具体:模块名是**全局且长期**的,包名不是。`mcpplibs.openkal` 已经为此付过学费 —— 它的 0.1.0 是被**撤回**而不是保留的,理由是「把消费者 import 的模块放在了实现的控制之下,这与规范存在的意义相悖」。**宏是唯一过不去的东西**:`export` 命名的是实体而宏不是,所以 `wayland-util` 把每个宏映射成它实际是的东西 —— `WL_MARSHAL_FLAG_DESTROY` → constexpr、`wl_container_of` → 函数模板、六个 `*_for_each` → range,它的测试把它们全部实例化,包括 `_safe` 的「循环体内可删除当前元素」保证)· [`freedesktop.egl`](../../pkgs/f/freedesktop.egl.lua)(libglvnd 1.7.0 —— [mcpplibs/libglvnd](https://github.com/mcpplibs/libglvnd),就是把上一行 `compat.egl` 绑定换掉的那个条目。**生成物是签进仓的,不是构建时生成的**:上游要跑约 1000 行 Python 处理 2.7 MB 的 `gl.xml` 才能得到 dispatch 表,而这份输出在 registry 固定的前提下是确定性的 —— 于是它躺在 `mcpp/generated/`,构建期**不跑任何生成器**,改由 CI 重新生成并 diff。`mcpp build` 就是全部工具链。**按架构选文件这件事放在 `build.mcpp` 里,不在清单里**:GLdispatch 的 entry stub 既分架构又分线程存储模型,而且和 libffi 的不同,它们自身**没有**任何门控 —— 在 `sources` 里写死 x86_64 会让这个包只能在 x86_64 上用、而且哪儿都不写明;`build.mcpp` 用 `mcpp::target_arch()` 做的正是上游 `gl_dispatch_type` 的那个选择。**模块叫 `khronos.egl` 而不是 `freedesktop.egl`**,包名与模块名故意不一致,而这正是重点:freedesktop 确实在发这份代码,但 EGL 是 **Khronos** 的规范、有多个实现。用实现方命名模块,等于将来换一个 libEGL 就要逼所有消费者改 import —— 而这个包装层的全部承诺就是「除了 include 那一行什么都不改」。副作用也是好的:两个 EGL 提供方现在会硬冲突而不是静默共存,这本来就是 GLVND 想要的。**一个索引条目,两个库**:`libGLdispatch.so.0` 由同 workspace 的兄弟成员构建,通过 **path** 依赖引入而不是走索引 —— 「进程里只有一个 dispatch 点」是 GLVND 存在的全部理由,而两个索引条目会让消费者把两个都写上、解析出两个包实例,soname 复用只映射其中一个、另一个被静默丢弃。**编译进去的 vendor 路径刻意是空的**:上游烤进 `/share/glvnd/egl_vendor.d`,重定位之后那就是 **host** 的目录,所以留空能让「生态没声明」表现为「找不到 vendor」,而不是悄悄把宿主驱动装进沙箱进程 —— 与 compat.libgbm 对 `GBM_BACKENDS_PATH` 的立场一致)| | 与 payload 同 soname 的源码库 | [`compat.libdrm`](../../pkgs/c/compat.libdrm.lua)(libdrm 2.4.134,5 个 TU + 内联的 `generated_static_table_fourcc.h`,零依赖。它和生态 payload 里的 `libdrm.so.2` 同名共存,而这**不是**绑定的理由:DT_NEEDED 的 soname 一旦在 link map 中就被复用,ld.so 不再搜索,所以 Mesa 的 `libgbm.so.1` 绑到的是消费者链接的这一份 —— 实测进程里只有一份,且 Mesa 的 GBM 用它分配出了 buffer。前提是 `kind = "shared"` **且 soname 正确**:用默认的 `kind = "lib"` 就没有 `.so` 可复用,payload 那份照常为 Mesa 加载,消费者另有一份合并进来的,libdrm 的 file-static 状态(`drmHashTable`/`nr_fds`/`connection`)在同一批 fd 上分成两套账。测试用 `dladdr` 断言加载的确实是本包所建。**两个 include 根**:公开头在源码根、uapi 头在 `include/drm/`,而 `xf86drm.h` 第 40 行就是裸的 `#include `) · [`compat.libffi`](../../pkgs/c/compat.libffi.lua)(3.4.8。x86 汇编**自带架构门控**,`ffi.c`/`ffiw64.c`/`unix64.S`/`sysv.S`/`win64.S` 各自 `#ifdef __x86_64__`/`__i386__`,所以全部列进 sources 由预处理器筛;`fficonfig.h` 是 configure 的探测结果、无法从 tarball 推导,内联;`ffi.h` **能**推导,于是 install() 照 configure 的四个替换从 `ffi.h.in` 生成,版本一动就跟着动) · [`compat.expat`](../../pkgs/c/compat.expat.lua)(2.7.1,3 个 TU —— `xmltok_impl.c`/`xmltok_ns.c` 是被 `xmltok.c` **include** 五次的,列进 sources 会产生重复符号) | | 恒开的 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) | diff --git a/pkgs/f/freedesktop.wayland-server.lua b/pkgs/f/freedesktop.wayland-server.lua index 89dd0854..ea499dc4 100644 --- a/pkgs/f/freedesktop.wayland-server.lua +++ b/pkgs/f/freedesktop.wayland-server.lua @@ -1,4 +1,4 @@ --- freedesktop.wayland-server — libwayland-server, plus `import wayland.server;`. +-- freedesktop.wayland-server — libwayland-server, plus `import freedesktop.wayland.server;`. -- -- Form A: the manifest lives in mcpplibs/wayland, a fork of freedesktop's -- wayland 1.26.0 that adds mcpp build support and patches no upstream file. diff --git a/pkgs/f/freedesktop.wayland.lua b/pkgs/f/freedesktop.wayland.lua index a9855c5d..dd16dcc5 100644 --- a/pkgs/f/freedesktop.wayland.lua +++ b/pkgs/f/freedesktop.wayland.lua @@ -1,4 +1,4 @@ --- freedesktop.wayland — libwayland-client, plus `import wayland.client;`. +-- freedesktop.wayland — libwayland-client, plus `import freedesktop.wayland.client;`. -- -- Form A: the manifest lives in mcpplibs/wayland, a fork of freedesktop's -- wayland 1.26.0 that adds mcpp build support and patches no upstream file. From 7c7f129f16b1e1643eb3033d3cc289109af6e664 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 13:15:37 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E6=B2=99=E7=AE=B1=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E7=AD=BE=E8=BF=9B=E4=BB=93,=C2=A719.4=20?= =?UTF-8?q?=E6=8D=A2=E6=88=90=E5=AE=8C=E6=95=B4=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前 §19.4 只有方法论和输出摘录,脚本躺在一个 subos 目录里 —— 那既不可复现也不算记录 下来了。现在: * tests/verify_graphics_closed_loop_sandbox.sh 签进仓,与 check_graphics_install_side_effects.sh 同处 tests/,带复现命令与参数说明; * §19.4 收录**完整**输出(七个步骤全文),外加一张「这份输出证明了什么」的断言→证据 对照表,以及两条不是缺陷的输出各自的解释; * 三个坑写进文档:[indices] 同一 path 挂两个键会二义、沙箱里 find 会抓到别的 subos 的 mcpp、沙箱不挂 cwd。 签进仓的这一版是重新跑过的,不是把之前的输出誊抄过来 —— 分支 feat/freedesktop-egl @ 2f4458d,结果 PASS。 §19.1 与 §19.8 同步:示例 09 已推到 mcpp PR #532。之前扣着不推的理由是「会把已绿的 CI 弄红」,那是假设、没查 —— mcpp 的 CI 根本不构建 examples/09-graphics-stack。 --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 145 +++++++++---- tests/verify_graphics_closed_loop_sandbox.sh | 194 ++++++++++++++++++ 2 files changed, 301 insertions(+), 38 deletions(-) create mode 100755 tests/verify_graphics_closed_loop_sandbox.sh 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 ea73cd83..05ce84a1 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 @@ -1134,7 +1134,7 @@ backends dir: /subos/default/usr/lib/gbm ← subos 给的,不是 | **mcpplibs/libglvnd**(新建) | libglvnd v1.7.0 + mcpp 构建支持,出 `libEGL.so.1` 与 `libGLdispatch.so.0` | `v1.7.0`,CI 四 job 全绿 | | **mcpplibs/wayland** | 三个模块重命名;CI 钉 xlings | `v1.26.0` 重切,CI 全绿 | | **mcpp-index** | `compat.egl` → `freedesktop.egl`;5 个描述符 sha256;测试成员;文档 | PR #293 | -| **mcpp-community/mcpp** | 示例 09 改用 `freedesktop.egl`,打印两个发现变量 | 待 #293 合并后推 | +| **mcpp-community/mcpp** | 示例 09 改用 `freedesktop.egl`,模块改名,打印两个发现变量,闭包实测重新测量 | PR #532,已推 | | mcpp-res(gitcode) | wayland / libglvnd 两个 CN 镜像 | 已发布,与 GLOBAL 字节一致 | ### 19.2 模块命名:跟**接口的所有者**,不跟发实现的人 @@ -1190,59 +1190,126 @@ GLdispatch 那条则相反:entry stub 分架构**且**分线程存储模型,而 **它们自身没有任何门控**。在 `sources` 里写死 x86_64 会让包只能在 x86_64 上用、而且哪儿 都不写明;`build.mcpp` 用 `mcpp::target_arch()` 做的正是上游 `gl_dispatch_type` 的选择。 -### 19.4 生态真实验证:沙箱干净房间(`--sandbox --gpu`) +### 19.4 生态真实验证:沙箱干净房间(`--sandbox --gpu`)—— 已完成并记录 -§12.3 验证过的是 **GBM**。这一轮验证的是**整条链**,而且在一个 `/home/speak` 是合成的、 -开发机上任何 checkout / 缓存 / 构建树都进不来的沙箱里,**从零 clone + 安装 + 构建**。 +§12.3 验证过的是 **GBM**。这一节验证的是**整条链**,包含这一轮新增的源码构建 EGL 与 +重命名后的模块,而且是**从零安装 + 从零构建**。 -关键在于:**沙箱里 `/usr` 是宿主的**。 +**脚本已签进仓**:[`tests/verify_graphics_closed_loop_sandbox.sh`](../../tests/verify_graphics_closed_loop_sandbox.sh) -``` -host has /usr/lib/x86_64-linux-gnu/libEGL.so.1 -host has /usr/lib/x86_64-linux-gnu/libgbm.so.1 -host has /usr/lib/x86_64-linux-gnu/libdrm.so.2 -host has /usr/lib/x86_64-linux-gnu/libwayland-client.so.0 +复现: + +```bash +# 一个装了图形栈的 subos,例如 xlings install xim:mesa@25.0.7.2 +cp tests/verify_graphics_closed_loop_sandbox.sh ~/.xlings/subos//verify.sh +xlings subos use --sandbox --gpu \ + --cmd "BRANCH= sh /home/speak/.xlings/subos//verify.sh" ``` -所以要证的**不是**「宿主不在」,而是「宿主在、可达、且依然全部落败」—— 后者才是用户机器 -上会发生的情形。用二进制自己的 loader 列闭包: +拷贝那一步不是多余的:**沙箱不挂当前工作目录**,放在别处的脚本在里面根本看不到。 -``` -libgbm.so.1 => /xpkgs/compat-x-libgbm/25.0.7/.../libgbm.so.1 -libdrm.so.2 => /target/.../bin/libdrm.so.2 -libEGL.so.1 => /target/.../bin/libEGL.so.1 -libGLdispatch.so.0 => /target/.../bin/libGLdispatch.so.0 -libwayland-client.so.0 => /target/.../bin/libwayland-client.so.0 -libwayland-server.so.0 => /target/.../bin/libwayland-server.so.0 -libffi.so.8 => /target/.../bin/libffi.so.8 - -PASS: the host's copies were present and reachable, and none of them won -``` +#### 为什么这个沙箱有说服力 -真跑(`--gpu`): +关键不是「隔离得干净」,而是**沙箱里 `/usr` 仍然是宿主的**。所以要证的**不是**「宿主 +不在」—— 一个把 `/usr` 藏起来的沙箱对用户的机器什么也证明不了 —— 而是「宿主在、可达、 +且依然全部落败」。后者才是真实机器上会发生的情形。 + +#### 完整输出(2026-08-30,`eco-gbm-20260830`,分支 `feat/freedesktop-egl` @ 2f4458d) ``` -EGL_VERSION 1.5 libglvnd ← 自建 dispatch 在答 -/dev/dri/renderD128 drm driver nvidia-drm - eglInitialize EGL 1.5, vendor Mesa Project -/dev/dri/card0 drm driver simpledrm - gbm_bo_create 256x256 stride=1024 ← 真分配出 buffer object - eglInitialize EGL 1.5, vendor Mesa Project +===== 0. where we are ===== + home contents: . .. .bashrc closed-loop .config .mcpp mcpp-index .profile .xlings .zshrc + host has /usr/lib/x86_64-linux-gnu/libEGL.so.1 + host has /usr/lib/x86_64-linux-gnu/libgbm.so.1 + host has /usr/lib/x86_64-linux-gnu/libdrm.so.2 + host has /usr/lib/x86_64-linux-gnu/libwayland-client.so.0 + GBM_BACKENDS_PATH = /usr/lib/gbm + __EGL_VENDOR_LIBRARY_DIRS = /share/glvnd/egl_vendor.d + +===== 1. install mcpp through xlings ===== + mcpp: /bin/mcpp +mcpp 2026.8.29.1 + +===== 2. clone the index under test ===== + branch feat/freedesktop-egl @ 2f4458d + +===== 3. a project that names the whole stack ===== + +===== 4. build from scratch ===== + build.mcpp compiling + build.mcpp running + Target x86_64-linux-gnu → x86_64-unknown-linux-gnu + Inferred sources [src/**/*.{cppm,cpp,cc,c,S,s,asm}] + Inferred target closed-loop (bin from src/main.cpp) + Compiling closed-loop v0.1.0 (.) + Cached compat.libdrm v2.4.134 (5 units) + Cached compat.libgbm v25.0.7 (1 unit) + Compiling freedesktop.egl v1.7.0 + Cached freedesktop.wayland v1.26.0 (1 unit) + Cached freedesktop.wayland-server v1.26.0 (1 unit) + Finished dev [unoptimized + debuginfo] in 0.31s + +===== 5. what the loader actually resolved ===== + libgbm.so.1 => /xpkgs/compat-x-libgbm/25.0.7/mcpp_generated/libgbm/lib/libgbm.so.1 + libdrm.so.2 => /target/x86_64-linux-gnu/78e15402517d6f84/bin/libdrm.so.2 + libEGL.so.1 => /target/x86_64-linux-gnu/78e15402517d6f84/bin/libEGL.so.1 + libwayland-client.so.0 => /target/x86_64-linux-gnu/78e15402517d6f84/bin/libwayland-client.so.0 + libwayland-server.so.0 => /target/x86_64-linux-gnu/78e15402517d6f84/bin/libwayland-server.so.0 + libexpat.so.1 => /xpkgs/xim-x-expat/2.6.2/lib/libexpat.so.1 + libGLdispatch.so.0 => /target/x86_64-linux-gnu/78e15402517d6f84/bin/libGLdispatch.so.0 + libffi.so.8 => /target/x86_64-linux-gnu/78e15402517d6f84/bin/libffi.so.8 + +===== 6. did anything come from the host? ===== + PASS: the host's copies were present and reachable, and none of them won + +===== 7. run it ===== +KMS: DRM_IOCTL_MODE_CREATE_DUMB failed: Permission denied +kmsro: driver missing + -- the modules carry the API -- + EGL_VERSION 1.5 libglvnd + wl_display_create 0x2bd0dbf0 + -- DRM node -> GBM device -> EGL display -- + /dev/dri/renderD128 + drm driver nvidia-drm + gbm_create_device 0x2bd65e60 + gbm_bo_create (driver declined) + eglInitialize EGL 1.5, vendor Mesa Project + /dev/dri/card0 + drm driver simpledrm + gbm_create_device 0x2bd65e60 + gbm_bo_create 256x256 stride=1024 + eglInitialize EGL 1.5, vendor Mesa Project + + reached EGL on a real device: yes + +===== RESULT ===== + PASS ``` -`import khronos.egl;` 与两个 `import freedesktop.wayland.*;` 一起编译链接通过 —— -模块层与 C 库在同一次干净构建里都成立。 +#### 这份输出证明了什么 + +| 断言 | 证据 | +|---|---| +| 模块层在干净构建里成立 | `import khronos.egl;` + 两个 `import freedesktop.wayland.*;` 编译链接通过 | +| 加载的是**自建**的 EGL 而非 payload | `libEGL.so.1 => /target/…`,且 `EGL_VERSION` 由它答出 | +| 自建 dispatch 真能加载 vendor | `eglInitialize → EGL 1.5, vendor Mesa Project` | +| GBM 真分配了显存 | `gbm_bo_create 256x256 stride=1024`(card0) | +| 宿主一条都没赢 | 第 6 步:四个宿主库在场且可达,闭包里零条来自 `/usr/lib` | +| 发现路径来自生态而非包 | 两个变量都由 `xim:mesa` 声明,包自己什么都不设 | + +两条**不是**缺陷的输出:`renderD128` 上 `gbm_bo_create` 被驱动拒绝,是 NVIDIA 后端不接受 +该 format/usage 组合;`DRM_IOCTL_MODE_CREATE_DUMB failed: Permission denied` 是 render node +的权限边界(dumb buffer 需 KMS 权限)。`card0` 上两者都成功。 -复现脚本见提交历史;三个坑值得记下: +#### 三个坑(留给下一个人) 1. **`[indices]` 同一个 path 不能挂两个键**。写 `compat` 和 `freedesktop` 都指向同一个 - checkout,会注册成两个独立 project,之后每次查找都报 + checkout,会注册成两个独立 project repo,之后每次查找都报 `package 'compat:libdrm@2.4.134' is ambiguous, candidates: … 'compat' … 'freedesktop'`。 只重定向被测的那个 namespace,其余走已发布索引。 2. **沙箱里 `find ~/.xlings -name mcpp` 会抓到别的 subos 的二进制**,它们的 interpreter 不在,失败信息是干巴巴的 `not found`,读起来像「mcpp 没装上」。要先试本 subos 的 `bin/`。 -3. **沙箱不挂 cwd**,落点是合成的 `/home/speak`(只有 dotfile)。脚本要放进 subos 目录 - 才进得去。 +3. **沙箱不挂 cwd**,落点是合成的 `/home/speak`(只有 dotfile)。脚本必须拷进 subos 目录。 ### 19.5 xlings pin:§15 的结论仍然成立,但它指出的缺口是真的 @@ -1296,8 +1363,10 @@ rm -rf ~/.mcpp/registry/data/xpkgs/freedesktop-x-wayland*/1.26.0 \ ### 19.8 仍未闭合 -- **PR #293 必须合并**才能修好 main 上 wayland 的 sha256(§19.6)。 -- 示例 09 的更新(mcpp PR #532)在本地待推,依赖 #293 先合。 +- **PR #293 必须合并**才能修好 main 上 wayland 的 sha256(§19.6)。这是唯一真正阻塞的一条。 +- 示例 09 的更新已推到 mcpp PR #532。曾以为「推上去会把已绿的 CI 弄红」而扣着不推, + **那是假设、没查**:mcpp 的 CI 根本不构建 `examples/09-graphics-stack`(只有 + `openkal-cross.yml` 碰一个无关示例)。扣着不推让工作停在本地,比推上去更糟。 - `libGL` / `libGLX` / `libGLESv{1,2}` 未构建。各自是 fork 里一个成员加一张 `mcpp/generated/` 里的 dispatch 表;索引里目前没有消费者。 - `freedesktop.egl` 的 `libEGL.so.1` 比 payload 的多三条 `DT_NEEDED` diff --git a/tests/verify_graphics_closed_loop_sandbox.sh b/tests/verify_graphics_closed_loop_sandbox.sh new file mode 100755 index 00000000..07fadcd7 --- /dev/null +++ b/tests/verify_graphics_closed_loop_sandbox.sh @@ -0,0 +1,194 @@ +#!/bin/sh +# +# Clean-room verification of the GBM/DRM/EGL/Wayland closed loop. +# +# WHAT MAKES THIS DIFFERENT FROM `mcpp test` +# +# The member tests under tests/examples/ run on the developer's machine, where +# the ecosystem is already installed and the developer's caches are warm. This +# one runs inside `xlings subos use --sandbox --gpu`, where: +# +# * /home/speak is SYNTHETIC — none of the developer's checkouts, caches or +# build trees reach in, so mcpp, the index and every package are fetched +# from scratch exactly as a new user would get them; +# * /usr is still the HOST's, so /usr/lib/x86_64-linux-gnu/libEGL.so.1, +# libgbm.so.1, libdrm.so.2 and libwayland-client.so.0 are all present and +# reachable. +# +# The second point is what makes the result mean anything. The claim under test +# is NOT "the host was absent" — a sandbox that hides /usr proves nothing about +# a user's machine. It is "the host was there, reachable, and lost anyway", +# which is the only version of the claim a real machine can match. +# +# USAGE +# +# # a subos with the graphics stack, e.g. `xlings install xim:mesa@25.0.7.2` +# cp tests/verify_graphics_closed_loop_sandbox.sh \ +# ~/.xlings/subos//verify.sh +# xlings subos use --sandbox --gpu --cmd "sh /home/speak/.xlings/subos//verify.sh" +# +# The copy step is not optional: the sandbox does not bind the current working +# directory, so a script anywhere else is simply not visible inside. +# +# BRANCH= index ref to test (default: main) +# SUBOS= subos name, needed to locate its bin/ (default: from $0) +# +set -e + +BRANCH="${BRANCH:-main}" +W=/home/speak/closed-loop +SUBOS="${SUBOS:-$(dirname "$0")}" + +say() { printf '\n===== %s =====\n' "$1"; } + +say "0. where we are" +echo " home contents: $(ls -a /home/speak | tr '\n' ' ')" +for l in libEGL.so.1 libgbm.so.1 libdrm.so.2 libwayland-client.so.0; do + f=/usr/lib/x86_64-linux-gnu/$l + [ -e "$f" ] && echo " host has $f" || echo " host lacks $l" +done +echo " GBM_BACKENDS_PATH = ${GBM_BACKENDS_PATH:-}" +echo " __EGL_VENDOR_LIBRARY_DIRS = ${__EGL_VENDOR_LIBRARY_DIRS:-}" + +say "1. install mcpp through xlings" +XL=$(command -v xlings 2>/dev/null) || XL="$SUBOS/bin/xlings" +[ -x "$XL" ] || XL="$SUBOS/bin/xlings" +"$XL" install mcpp 2>&1 | tail -3 + +# Search THIS subos first. A bare `find ~/.xlings -name mcpp` picks up binaries +# belonging to OTHER subos environments; those are linked against a different +# sysroot and fail with a bare "not found" (their interpreter is missing), +# which reads like mcpp was never installed at all. +MCPP="" +for c in "$SUBOS/bin/mcpp" "$(command -v mcpp 2>/dev/null)"; do + [ -n "$c" ] && [ -x "$c" ] && "$c" --version >/dev/null 2>&1 && { MCPP="$c"; break; } +done +[ -n "$MCPP" ] || { echo "FAIL: no runnable mcpp after install"; exit 1; } +echo " mcpp: $MCPP" +"$MCPP" --version 2>&1 | head -1 + +say "2. clone the index under test" +rm -rf /home/speak/mcpp-index +git clone -q --depth 1 -b "$BRANCH" https://github.com/mcpplibs/mcpp-index /home/speak/mcpp-index +echo " branch $BRANCH @ $(git -C /home/speak/mcpp-index rev-parse --short HEAD)" + +say "3. a project that names the whole stack" +rm -rf "$W"; mkdir -p "$W/src" +# ONE index key for the checkout, not two. Declaring the same path under both +# `compat` and `freedesktop` registers two INDEPENDENT project repos, and every +# lookup then fails with "package 'compat:libdrm@2.4.134' is ambiguous, +# candidates: … from project repo 'compat' … from project repo 'freedesktop'". +# So only the namespace under test is redirected; compat.* comes from the +# published index, which is what a user gets. +cat > "$W/mcpp.toml" < "$W/src/main.cpp" <<'CPP' +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +import khronos.egl; +import freedesktop.wayland.client; +import freedesktop.wayland.server; + +int main() +{ + std::puts(" -- the modules carry the API --"); + std::printf(" EGL_VERSION %s\n", + eglQueryString(EGL_NO_DISPLAY, EGL_VERSION)); + + if (wl_display *s = wl_display_create()) { + std::printf(" wl_display_create %p\n", (void *)s); + wl_display_destroy(s); + } else { std::puts(" wl_display_create FAILED"); return 1; } + + std::puts(" -- DRM node -> GBM device -> EGL display --"); + int reached = 0; + for (const char *node : {"/dev/dri/renderD128", "/dev/dri/card0"}) { + int fd = ::open(node, O_RDWR); + if (fd < 0) { std::printf(" %-22s (no access)\n", node); continue; } + std::printf(" %s\n", node); + if (drmVersionPtr v = drmGetVersion(fd)) { + std::printf(" drm driver %s\n", v->name); + drmFreeVersion(v); + } + if (gbm_device *g = gbm_create_device(fd)) { + std::printf(" gbm_create_device %p\n", (void *)g); + if (gbm_bo *bo = gbm_bo_create(g, 256, 256, GBM_FORMAT_XRGB8888, + GBM_BO_USE_RENDERING)) { + std::printf(" gbm_bo_create 256x256 stride=%u\n", + gbm_bo_get_stride(bo)); + gbm_bo_destroy(bo); + } else { + std::puts(" gbm_bo_create (driver declined)"); + } + EGLDisplay d = eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, g, nullptr); + if (d != EGL_NO_DISPLAY) { + EGLint ma = 0, mi = 0; + if (eglInitialize(d, &ma, &mi)) { + std::printf(" eglInitialize EGL %d.%d, vendor %s\n", + ma, mi, eglQueryString(d, EGL_VENDOR)); + reached = 1; + eglTerminate(d); + } + } + gbm_device_destroy(g); + } + ::close(fd); + } + std::printf("\n reached EGL on a real device: %s\n", reached ? "yes" : "no"); + return 0; +} +CPP + +say "4. build from scratch" +cd "$W" +"$MCPP" build 2>&1 | tail -12 + +say "5. what the loader actually resolved" +BIN=$(find "$W/target" -name closed-loop -type f -perm -u+x | head -1) +[ -n "$BIN" ] || { echo "FAIL: no binary"; exit 1; } +I=$(readelf -p .interp "$BIN" | grep -oE '/[^ ]*ld-linux[^ ]*') +"$I" --list "$BIN" | grep -E 'libEGL|libGLdispatch|libgbm|libdrm|libwayland|libffi|libexpat' \ + | sed "s|$SUBOS||g; s|$W||g" + +say "6. did anything come from the host?" +if "$I" --list "$BIN" | grep -E 'libEGL|libGLdispatch|libgbm|libdrm|libwayland|libffi|libexpat' \ + | grep -qE '=> /(usr/)?lib/'; then + echo " FAIL: a graphics library resolved to the host" + "$I" --list "$BIN" | grep -E 'libEGL|libGLdispatch|libgbm|libdrm|libwayland' | grep -E '=> /(usr/)?lib/' + exit 1 +fi +echo " PASS: the host's copies were present and reachable, and none of them won" + +say "7. run it" +"$BIN" + +say "RESULT" +echo " PASS"