diff --git a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md index 0cc4ced..50ff941 100644 --- a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -600,17 +600,141 @@ share/{bash-completion,man}` —— **不带 xkb 数据**。所以 `XKB_CONFIG_R ⚠ 顺带一提:一个**只需要 `xkb_keymap_new_from_string`** 的合成器不需要这份数据 —— 那正是它从客户端收到 keymap 的路径,也是本索引测试跑通 parser 的方式。 +#### 10.9.1 已落地(openxlings/xim-pkgindex#732) + +上面的结论已实现,并在实现过程中改了 discovery 层的两处形状: + +**`XKB_CONFIG_ROOT` 是表里第一个非列表项。** DISCOVERY 的另外四行都是冒号分隔的搜索 +路径,`prepend` 在那里既正确又无损(而且必须是 prepend:一个提供方 `set` 会盖掉另一 +个,NVIDIA vendor 目录消失就是这么来的)。`XKB_CONFIG_ROOT` 是**标量** —— libxkbcommon +读一次当一个目录用,`prepend` 上第二个提供方就变成 `dirA:dirB`,一个不存在的路径,而 +报错只会说「keymap 编译失败」。所以给 DISCOVERY 行加了可选的 `op`,默认仍是 `prepend`, +这一行写 `set`。 + +> 这与记忆里那条 `xvm envs 是 PATH 式合并`(标量 env 会被冒号拼接)是同一件事的第二次 +> 撞见。第一次的答案是「生成 launcher」;这次因为发现点在 discovery 层内部,所以修在 +> 了机制里。 + +**声明变量 ≠ 放置内容。** 第一版只加了 DISCOVERY 行和 `declare_subos_env` 调用,装完之后: + +``` +XKB_CONFIG_ROOT=[.../subos/eco-2026-8-30-1/share/X11/xkb] +ls: cannot access '.../share/X11/xkb': No such file or directory +``` + +变量在 shell 里读得好好的,指向不存在的目录。补 `graphics.declare_xkb`,与 +`declare_dri` / `declare_gbm` 同形(`xvm.files` 把树放进 subos)。 + +**跨索引闭环已实测** —— mcpp 侧的 `freedesktop.libxkbcommon` 消费 xim 侧的数据集: + +``` +XKB_CONFIG_ROOT = .../subos/eco-2026-8-30-1/share/X11/xkb +xkb_keymap_new_from_names(evdev/pc105/us) against that root ok + real layout: keycode 24 -> "q" +…and the real us layout maps keycode 24 to "q" ok +0 check(s) failed +``` + +消费侧断言写在 `tests/examples/libxkbcommon`,且是**条件式**的:`XKB_CONFIG_ROOT` 未 +设置时只报告,设置了才要求编得出来。没有数据集的机器不是这个包的缺陷。 + +**合并后又从「已发布索引 + 全新 subos + 沙箱」复验了一遍**,因为上面那次用的是 +`xlings config --add-xpkg` 的本地副本: + +``` +xim:xkeyboard-config@2.48 installed ← 来自已发布索引,不是 local: +ROOT=[.../subos/eco-2026-8-30-2/share/X11/xkb] +compat geometry keycodes rules symbols types + rules/evdev 行数: 512 + 布局数: 151 +``` + +**这次复验抓到一个真实陷阱,而且差点得出"包坏了"的结论**:第一次从已发布索引装, +数据**没有**放进去。原因不在包 —— store 里还留着本地验证时的 +`local:xkeyboard-config@2.48`,而 xlings 的 store 查找**忽略 namespace**, +`(name, version)` 撞上就把 `install()` 静默跳过,payload 是空的,而 `config()` 照常跑。 +清掉 store 重装即正常。 + +**两条要记住的**: + +1. 用 `--add-xpkg` 本地验证过的包,发布后必须**先清 store 再验一遍**,否则验的 + 仍然是本地那份。 +2. `declare_xkb` 的 `os.isdir` 守卫确实触发了,但那条 `log.warn` + **在 install 输出里根本没出现**(同一次输出里 xlings 自己的 `[warn]` 是打出来的)。 + 所以 `declare_dri` / `declare_gbm` / `declare_xkb` 的警告都不能当诊断依赖 —— + 判断内容有没有真放进去只能直接 `ls`。 + +#### 10.9.2 同一形状的第三处:libinput quirks + +写 `compat.libinput` 时把 `LIBINPUT_QUIRKS_DIR` 也编译成空,注释里当时写的是「编译期 +路径,空即用内置默认」。**这句话不准确**:`libinput.c:1911` 是 +`getenv("LIBINPUT_QUIRKS_DIR")` 优先、编译期值兜底 —— 和 `GBM_BACKENDS_PATH` +一模一样的形状。 + +所以这不是死路,是**缺提供方**:`.quirks` 文件就在 libinput 自己的 tarball 里,但 +mcpp-index 没有发布数据目录的机制(与 §10.9 同因)。在有东西填上之前,libinput 打印 + +``` +failed to find data files ... will negatively affect device behavior +``` + +并跑在内置默认上。这是**优雅降级**:枚举、事件、手势都正常,丢的是逐机型调校(比如 +某块触摸板的压力区间)。`tests/examples/libinput` 就是在这条消息存在的情况下全绿的。 + +补法与 xkeyboard-config 完全对称(xim 数据包 + 一行 DISCOVERY),优先级低于布局数据: +布局缺了是**硬失败**,quirks 缺了只是不够贴合。 + ### 10.10 仍未做 - ~~G4b `libxkbcommon`~~ **已做**(fork,bison parser 预生成)。它需要的数据集见 §10.9: 属于生态,不属于这里。 -- ~~G5 `libinput`~~ **描述符已做**;测试成员待本 PR 合并、`freedesktop.libevdev` 发布 - 后接上 —— 它同时需要 `compat.*` 和 `freedesktop.*` 两个 namespace 指向同一个 - checkout,而成员级 `[indices]` 是替换而非合并。 +- ~~G5 `libinput`~~ **已做,并且测试成员一接上就抓出了四个 bug**(#298)。 + 形态问题解决得很朴素:这个成员**不声明 `[indices]`**,继承根的 + `compat = { path = "." }` —— compat 来自 checkout,`freedesktop.libevdev` 来自已发布 + 索引(它确实已发布)。声明 `freedesktop` 反而会让 `compat.libinput` 去查已发布索引。 + + 四个 bug 值得单列,因为它们**全都是"包进了索引但从没被编译过"造成的** —— + 没有测试成员消费它,而没人编译的包不会编译失败(记忆里那条「绿 CI 不等于包被编译」 + 的教科书案例): + + | 问题 | 症状落点 | + |------|---------| + | 缺 `libinput-version.h`(meson 从 `.h.in` 生成) | `libinput-private.h:45` 无条件 include,40 个源文件全挂 | + | `config.h` 只写了一半 | `HTTP_DOC_LINK` / `LIBINPUT_QUIRKS_OVERRIDE_FILE` / `LIBINPUT_PLUGIN_{LIB,ETC}DIR` / `HAVE_VERSIONSORT` / `HAVE_MTDEV` | + | `include_dirs` 少了包根 | 全树只有一个文件写 `#include "src/evdev-frame.h"` | + | `typeof` 是 GNU 关键字 | `-std=c11` 下 cast 塌成 `int`,错误落在十几个没提 typeof 的文件里 | + + 其中两条有普遍意义: + + **`HAVE_VERSIONSORT`** —— 不定义它,`libinput-versionsort.h` 会给出自己的 + `static strverscmp`,而 glibc 已经 `extern` 声明过。这是硬错误,不是遮蔽。 + 「少定义一个 HAVE_ 宏最多退化」的直觉在这里是错的。 + + **`c_standard = "gnu11"` 不生效,这次是实测的** —— mcpp 收下这个字符串,仍然发 + `-std=c11`,那些 typeof 错误原样还在。所以走 `-Dtypeof=__typeof__`。这与记忆里 + 「c_standard 的 gnu 模式被静默忽略」一致,但那条记的是 `_GNU_SOURCE` 类的**库特性 + 宏**;`typeof` 是**语言方言**,`-D_GNU_SOURCE` 对它无效。两者要分开记。 - ~~G6 `libudev` / `libseat`~~ **已做**,而且没有碰 systemd:libudev 用 **libudev-zero**(三个实现里唯一既活着又可独立分发的),libseat 只开 seatd 与 builtin 后端。两者的代价都在描述符里点名了。 -- **xkeyboard-config**:见 §10.9,应进 xim-pkgindex。 - -前两条是普通工作量;G6 是需要决策的。合成器可以在「已有 DRM master」的前提下开发 -(从 TTY 直接启动、或 `SEATD_SOCK`),把 session 管理留到最后。 +- ~~**xkeyboard-config**~~ **已做**(xim-pkgindex#732),见 §10.9.1。跨索引闭环已实测。 +- **libinput quirks 数据**:见 §10.9.2。形状与 xkeyboard-config 对称,优先级更低 —— + 布局缺了是硬失败,quirks 缺了只是不够贴合。**未做**。 + +至此渲染链与输入链都不再有「静默落到宿主」的边: + +| 子系统 | 发现变量 | 提供方 | 状态 | +|--------|---------|--------|------| +| DRI 驱动 | `LIBGL_DRIVERS_PATH` | `xim:mesa` | ✅ | +| EGL vendor | `__EGL_VENDOR_LIBRARY_DIRS` | `xim:mesa` + host-link 哨兵 | ✅ | +| Vulkan ICD | `XDG_DATA_DIRS` / 共享 vendor 目录 | 同上 | ✅ #731 待合 | +| GBM 后端 | `GBM_BACKENDS_PATH` | `xim:mesa` | ✅ | +| 键盘布局 | `XKB_CONFIG_ROOT` | `xim:xkeyboard-config` | ✅ #732 待合 | +| 输入 quirks | `LIBINPUT_QUIRKS_DIR` | *(无)* | ⬜ 优雅降级中 | +| USB 名字库 | `USB_IDS_PATH` | *(无)* | ⬜ 优雅降级中 | + +最后两行是同一类:机制在、提供方缺、缺了只丢锦上添花的东西。**它们缺失时会说出来** +—— 这正是当初把编译期默认值一律留空要换的东西。 + +G6 仍是需要决策的:合成器可以在「已有 DRM master」的前提下开发(从 TTY 直接启动、或 +`SEATD_SOCK`),把 session 管理留到最后。 diff --git a/mcpp.toml b/mcpp.toml index 0f4aa13..c94aeef 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -89,6 +89,7 @@ members = [ "tests/examples/egl", "tests/examples/gl", "tests/examples/libevdev", + "tests/examples/libinput", "tests/examples/libseat", "tests/examples/libudev", "tests/examples/libxkbcommon", diff --git a/pkgs/c/compat.libinput.lua b/pkgs/c/compat.libinput.lua index ada2e39..6aa13a7 100644 --- a/pkgs/c/compat.libinput.lua +++ b/pkgs/c/compat.libinput.lua @@ -31,13 +31,25 @@ -- way. Nothing in this index provides it, and without it libinput falls back -- to generic tablet handling rather than failing. -- --- The quirks database (`quirks.c`) reads `.quirks` files at runtime from a --- directory compiled in as `LIBINPUT_QUIRKS_DIR`. That path is EMPTY here for --- the same reason libgbm's backend path and libxkbcommon's config root are: --- upstream's default points into the build prefix, which after relocation is --- the HOST's dataset. Empty means devices get libinput's built-in defaults --- rather than a host machine's model quirks — correct behaviour, one fewer --- silent host edge. +-- The quirks database (`quirks.c`) reads `.quirks` files at runtime — model +-- specific tuning like a touchpad's pressure range. `libinput.c:1911` takes the +-- directory from `getenv("LIBINPUT_QUIRKS_DIR")` and falls back to a +-- compiled-in path, and the compiled-in path is EMPTY here for the same reason +-- libgbm's backend path and libxkbcommon's config root are: upstream's default +-- points into the build prefix, which after relocation is the HOST's dataset. +-- +-- So this is a WIRING question, not a dead end — the same shape as +-- GBM_BACKENDS_PATH, down to the environment variable. What is missing is a +-- provider: the `.quirks` files ship inside libinput's own tarball, but +-- mcpp-index has no way for a package to publish a data DIRECTORY, so nothing +-- currently fills the variable. Until something does, libinput logs +-- +-- failed to find data files ... will negatively affect device behavior +-- +-- and runs on its built-in defaults. That is a real degradation and it is +-- graceful: enumeration, events and gestures all work — what is lost is +-- per-model tuning. Verified in tests/examples/libinput, which passes with the +-- message present. package = { spec = "1", namespace = "compat", @@ -62,9 +74,20 @@ package = { mcpp = { language = "c++23", import_std = false, + -- Upstream asks for `c_std=gnu99` and means it — see the `typeof` + -- note in cflags. `c_standard = "gnu11"` was tried here first and is + -- NOT the fix: mcpp accepts the string and still emits `-std=c11`, so + -- the descriptor would claim a dialect the compiler never sees. c_standard = "c11", - include_dirs = { "*/src", "*/include", "mcpp_generated" }, + -- `"*"` is the package ROOT, and it is here for exactly one file: + -- `libinput-plugin-mouse-wheel-lowres.c:31` writes + -- `#include "src/evdev-frame.h"` while every other source in the tree + -- writes `#include "evdev-frame.h"`. Upstream gets both spellings for + -- free because meson compiles from the project root; a package that + -- only puts `*/src` on the search path resolves 39 files and fails the + -- fortieth. + include_dirs = { "*", "*/src", "*/include", "mcpp_generated" }, generated_files = { ["mcpp_generated/config.h"] = [==[ @@ -72,20 +95,82 @@ package = { #define MCPP_LIBINPUT_CONFIG_H /* meson's config.h, written out for Linux with a GCC-compatible toolchain. - LIBINPUT_QUIRKS_DIR is EMPTY on purpose. quirks.c reads model-specific - `.quirks` files from it at runtime; upstream's default points into the - build prefix, which after relocation is the HOST's dataset. Empty means a - device gets libinput's built-in defaults instead of another machine's - quirks — the same stance compat.libgbm takes with GBM_BACKENDS_PATH. + LIBINPUT_QUIRKS_DIR is EMPTY on purpose, and it is a FALLBACK rather than + the only way in: `libinput.c:1911` reads `getenv("LIBINPUT_QUIRKS_DIR")` + first. Upstream's compiled-in default points into the build prefix, which + after relocation is the HOST's dataset — so empty means a device gets + libinput's built-in defaults instead of another machine's quirks, and an + environment that has a dataset can still name it. Exactly the shape + compat.libgbm has with GBM_BACKENDS_PATH, down to the variable. HAVE_LUA and HAVE_LIBWACOM are absent: both are optional upstream and both would add a dependency this package does not need to do its job. */ #define LIBINPUT_QUIRKS_DIR "" #define LIBINPUT_QUIRKS_SRCDIR "" +/* The user's quirks override, `/libinput/local-overrides.quirks` + upstream. Empty for the reason above: relocated, it names the HOST's file. */ +#define LIBINPUT_QUIRKS_OVERRIDE_FILE "" + +/* Plugin search paths, both EMPTY and both required to exist: + `libinput_plugin_system_append_default_paths` (libinput-plugin.c:387) names + them unconditionally, outside any HAVE_PLUGINS guard. Empty paths append + nothing, which is the behaviour this package wants — see the header comment + on why the Lua plugin system is off. */ +#define LIBINPUT_PLUGIN_LIBDIR "" +#define LIBINPUT_PLUGIN_ETCDIR "" + +/* Printed into log messages that point a user at the documentation for the + behaviour being reported. meson builds it from the version: micro < 90 means + a release, so the URL names this release rather than `latest`. */ +#define HTTP_DOC_LINK "https://wayland.freedesktop.org/libinput/doc/1.31.3" + #define HAVE_LIBEVDEV_DISABLE_PROPERTY 1 #define HAVE_MEMFD_CREATE 1 #define HAVE_LOCALE_H 1 #define HAVE_STRERRORNAME_NP 1 +#define HAVE_SIGABBREV_NP 1 +#define HAVE_PIDFD_OPEN 1 + +/* glibc has versionsort(3), and saying so is load-bearing rather than + cosmetic: `libinput-versionsort.h` provides its OWN static fallback when + this is unset, and a static definition of a name glibc already declared + extern is a hard error, not a shadow. */ +#define HAVE_VERSIONSORT 1 + +/* mtdev is a declared dependency of this package, so the plugin that uses it + is compiled unconditionally (see `libinput-plugin-mtdev.c` in sources). */ +#define HAVE_MTDEV 1 + +/* HAVE_C23_AUTO is deliberately ABSENT. meson probes for it by compiling + `auto foo = gmtime(NULL);`, which needs C23; this package builds as c11, so + the probe would fail here too and upstream's non-auto path is correct. */ +#endif +]==], + + -- meson's `src/libinput-version.h.in`, substituted for 1.31.3. + -- + -- `libinput-private.h:45` includes it unconditionally, so every one + -- of the 40 sources below needs it — this is not an optional + -- convenience header. It went missing in the first version of this + -- descriptor and NOTHING CAUGHT IT: the package validated, entered + -- the index and shipped, because no test member consumed it and a + -- package nobody compiles cannot fail to compile. The + -- `tests/examples/libinput` member exists so that stays true only + -- until someone builds it — which is what found this. + -- + -- The three numbers are the version in `xpm` above, split. They are + -- the package's public version macros, so a consumer doing + -- `#if LIBINPUT_VERSION_MAJOR >= 1` gets a real answer; leaving + -- them at 0 would compile just as well and lie. + ["mcpp_generated/libinput-version.h"] = [==[ +#ifndef LIBINPUT_VERSION_H +#define LIBINPUT_VERSION_H + +#define LIBINPUT_VERSION_MAJOR 1 +#define LIBINPUT_VERSION_MINOR 31 +#define LIBINPUT_VERSION_MICRO 3 +#define LIBINPUT_VERSION "1.31.3" + #endif ]==], }, @@ -144,6 +229,27 @@ package = { cflags = { "-D_GNU_SOURCE", "-DHAVE_CONFIG_H", + + -- The GNU `typeof` keyword, supplied by hand. + -- + -- `util-mem.h:180` is `(typeof(*ptr_))_steal(ptr_)` — the cast that + -- makes every `steal()` in the tree type-safe. Bare `typeof` is a + -- GNU extension (and C23's spelling); ISO C11 has only + -- `__typeof__`. Under `-std=c11` GCC parses `typeof(...)` as a call + -- to an undeclared function, the cast collapses to `int`, and the + -- damage lands as `-Wint-conversion` errors in a dozen unrelated + -- files that never mention typeof. + -- + -- This is a `-D` rather than `c_standard = "gnu11"` because the + -- gnu dialects do not reach the compiler: mcpp accepts the string + -- and still emits `-std=c11`. Measured, not assumed — gnu11 was + -- set here and these exact errors survived it. + -- + -- Safe because `__typeof__` IS `typeof`, always available in any + -- dialect, and nothing can be named `typeof` in code that expects + -- gnu99 anyway. + "-Dtypeof=__typeof__", + "-fPIC", }, diff --git a/tests/examples/libinput/mcpp.toml b/tests/examples/libinput/mcpp.toml new file mode 100644 index 0000000..2e0c7fe --- /dev/null +++ b/tests/examples/libinput/mcpp.toml @@ -0,0 +1,22 @@ +# libinput test project — the top of the input chain. +# +# NO `[indices]` OF ITS OWN, and that is the interesting part. +# +# This member needs packages from TWO namespaces: `compat.libinput` (and the +# `compat.libudev` / `compat.mtdev` it depends on) from this checkout, and +# `freedesktop.libevdev` which libinput also links. A member-level `[indices]` +# REPLACES the root's rather than merging, and one path can only be registered +# under one namespace — so declaring `freedesktop = { path = "../../.." }` here +# would make `compat.libinput` resolve against the published index instead. +# +# Inheriting the root's `compat = { path = "." }` is what works: the compat +# packages come from the checkout, and `freedesktop.libevdev` resolves from the +# PUBLISHED index — which is correct, because it is published. Same arrangement +# tests/examples/asio-ssl relies on for the opposite reason. +[package] +name = "libinput-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.compat] +libinput = "1.31.3" diff --git a/tests/examples/libxkbcommon/tests/libxkbcommon.cpp b/tests/examples/libxkbcommon/tests/libxkbcommon.cpp index 87d24c9..644bd60 100644 --- a/tests/examples/libxkbcommon/tests/libxkbcommon.cpp +++ b/tests/examples/libxkbcommon/tests/libxkbcommon.cpp @@ -16,6 +16,7 @@ #include #include +#include namespace { @@ -103,6 +104,48 @@ int main() check(xkb_keysym_from_name("Escape", XKB_KEYSYM_NO_FLAGS) == XKB_KEY_Escape, "xkb_keysym_from_name resolves a name to its keysym"); + // ── 6. RMLVO, if a dataset is reachable ────────────────────────────── + // + // This is the OTHER half of libxkbcommon, and the half this package + // deliberately cannot satisfy on its own: `xkb_keymap_new_from_names` + // takes rules/model/layout/variant/options and reads them out of + // xkeyboard-config's data tree. DFLT_XKB_CONFIG_ROOT is compiled in EMPTY + // here on purpose, so the path has to come from the environment — + // XKB_CONFIG_ROOT, which is what an ecosystem package declares. + // + // Reported rather than asserted when unset: a machine with no dataset is + // not a defect in this package. When it IS set, the compile must succeed, + // because then the dataset is the thing under test. + { + const char *root = std::getenv("XKB_CONFIG_ROOT"); + std::printf("\n XKB_CONFIG_ROOT = %s\n", + root ? root : "(unset — the ecosystem declares it)"); + if (root != nullptr) { + xkb_context *c2 = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + xkb_rule_names names{}; + names.rules = "evdev"; + names.model = "pc105"; + names.layout = "us"; + xkb_keymap *k2 = c2 ? xkb_keymap_new_from_names( + c2, &names, XKB_KEYMAP_COMPILE_NO_FLAGS) : nullptr; + check(k2 != nullptr, + "xkb_keymap_new_from_names(evdev/pc105/us) against that root"); + if (k2 != nullptr) { + xkb_state *s2 = xkb_state_new(k2); + if (s2 != nullptr) { + char b[16] = {0}; + xkb_state_key_get_utf8(s2, 24, b, sizeof b); + std::printf(" real layout: keycode 24 -> \"%s\"\n", b); + check(b[0] == 'q', + "…and the real us layout maps keycode 24 to \"q\""); + xkb_state_unref(s2); + } + xkb_keymap_unref(k2); + } + if (c2) xkb_context_unref(c2); + } + } + xkb_keymap_unref(km); xkb_context_unref(ctx);