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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 131 additions & 7 deletions .agents/docs/2026-08-30-graphics-stack-coverage-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 管理留到最后。
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
132 changes: 119 additions & 13 deletions pkgs/c/compat.libinput.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -62,30 +74,103 @@ 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"] = [==[
#ifndef MCPP_LIBINPUT_CONFIG_H
#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, `<sysconfdir>/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
]==],
},
Expand Down Expand Up @@ -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",
},

Expand Down
22 changes: 22 additions & 0 deletions tests/examples/libinput/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading