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
97 changes: 96 additions & 1 deletion .agents/docs/2026-08-30-graphics-stack-coverage-design.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# mcpp 图形栈:从「能跑通」到「能开发」的覆盖面设计

Date: 2026-08-30 · 前置:[`2026-08-30-gbm-cross-repo-closed-loop-plan.md`](2026-08-30-gbm-cross-repo-closed-loop-plan.md) §19/§20 · **状态:已实现并闭环验证(v1.9,§11 总账 / §14 fork 规范 / §18 八角度复核 / §19 合成器闸门与 GObject 栈,含沙箱实测 · §20 gio 与一次被推翻的判断 · §21 namespace 是契约 · §22 pango:文本排版接上了)**
Date: 2026-08-30 · 前置:[`2026-08-30-gbm-cross-repo-closed-loop-plan.md`](2026-08-30-gbm-cross-repo-closed-loop-plan.md) §19/§20 · **状态:已实现并闭环验证(v2.0,§11 总账 / §14 fork 规范 / §18 八角度复核 / §19 合成器闸门与 GObject 栈,含沙箱实测 · §20 gio 与一次被推翻的判断 · §21 namespace 是契约 · §22 pango:文本排版接上了 · §23 模块名与包装体完备性)**

## 0. 这份文档解决什么

Expand Down Expand Up @@ -2174,3 +2174,98 @@ error: dependency 'gnome.gobject' is requested as both a version dep
| 端到端 | 1,492 个像素 |

**文本排版那条线接上了。**

---

## 23. 模块名与包装体完备性:两个已发布包的修正(2026-08-31)

### 23.1 托管方不是负责方 —— 模块名改了两个

| 包(索引键) | 旧模块名 | 新模块名 |
|---|---|---|
| `freedesktop.cairo` | `freedesktop.cairo` | **`cairo`** |
| `freedesktop.libdisplay-info` | `freedesktop.displayinfo` | **`displayinfo`** |

**索引 namespace 是货架标签,从不进模块名。** cairo 是它自己的项目,
freedesktop.org 托管 git 但不拥有那套接口;libdisplay-info 解析的是 **VESA** 的
EDID/DisplayID,`freedesktop.*` 这个模块前缀留给 freedesktop 自己的规范(像
`freedesktop.wayland.client`)。同一条规则下 `freedesktop.egl` 导出的是
`khronos.egl`。

改名会让老消费者**编译报错并点名模块** —— 这是最该发现问题的地方。

### 23.2 ⭐ 两个包装体都不完备,而且都是「没人问过」

| | 导出 | 缺了什么 |
|---|---|---|
| `cairo` | 470 → **697** | `cairo_t`,以及**全部 192 个枚举量** |
| `displayinfo` | 206 → **501** | **全部 295 个枚举量** |

`cairo_t` 的原因很具体:正则是 `cairo_[a-z0-9_]+_t`,要求 `cairo_` 与 `_t` 之间
至少有一个字符 —— 于是
```c
typedef struct _cairo cairo_t; /* cairo.h:135 */
```
**这个库里用得最多的类型,恰好是它唯一匹配不到的名字。**

**为什么一直没人发现,这才是要记的:**

- cairo:唯一 import 那个模块的测试**同时**写了 `#include <cairo.h>`,头文件把
模块缺的都补上了。**一个从没被要求独立站住的 import,不是被测试了,是被装饰了。**
- displayinfo:**没有任何测试点过一个枚举量**。而且只在 GCC 上加一个也发现不了 ——
导出 enum 会让枚举量在 GCC 可见,clang 拒绝同一个文件(§21.5)。

### 23.3 它是从**外面**被发现的

`gnome.pangocairo` **不能混用两条消费路线**(`pangocairo.h` 经 glib 到
`<stdio.h>`),所以它只能问模块要 `cairo_t` —— 结果什么都没拿到。

于是 pango 那一版带了个变通:**自己扫 `cairo.h`**。现在 cairo 修好了,
**那个变通已经删掉**,普通的 `export import cairo;` 就够了 —— 本来就该够。

> 一个包的包装体缺不缺,自己的测试可能永远问不出来;**是别的包在用它的时候问出来的。**

### 23.4 发布形态:发资产,不重切 tag

这两个包用的是 **release 资产**(`cairo-1.18.2-mcpp2.tar.gz`)而不是 tag 归档,
所以修正走 `-mcpp3` 新资产:**tag 不动,零窗口**,而且资产名自带修订号。
(对比 glib/pango 用 tag 归档,只能原地重切 + 换 sha。)

⚠️ 本地 store 按 `(name, version)` 索引,所以已经解开过 `1.18.2` 的机器**拿不到
新资产** —— 本次验证时就撞上了(`failed to read compiled module`),清掉 store
条目才对。这是「包版本与上游对齐」这条约定的固定代价,值得写在这里。

### 23.5 ⭐ 改名的次序是被**索引机制**逼出来的,不是偏好

`gnome.pangocairo` 转出 cairo 的模块。所以改名要么两个包同时到达消费者,要么
中间有一段是坏的 —— 而**一个 PR 做不到**,原因是硬的:

```
error: xlings install_packages failed … with 2 index repos configured
[freedesktop -> …, gnome -> …]; ≥2 project-level index repos is a known
xlings resolution gap
```

**一个成员只能声明一个项目级 index repo**(xlings#374 / mcpp#238,今天仍是硬
错误)。于是 `tests/examples/pangocairo` 不可能同时用**本 PR 的 `gnome.pango*`
描述符**和**本 PR 的 `freedesktop.cairo` 描述符** —— 它只能重定向一个 namespace,
另一个必然来自**已发布的**索引。

所以次序只能是:

1. **本 PR**:cairo / libdisplay-info 改名 + 补齐。pango **不动**,继续
`import freedesktop.cairo;`,`cairo.h` 变通**保留**。
2. **合并之后**:pango 改成 `import cairo;` 并删掉变通 —— 那时新 cairo 已在
已发布索引里,示例才有得测。

⚠️ 我一度反过来做了(先把 pango 的变通删掉),两边都红。**代码是对的,次序是
错的**;已经回滚并把这段理由写进了 `gnome.pangocairo` 的描述符,免得下次再犯。

### 23.6 结果

| | |
|---|---|
| `cairo` | 697 个导出;测试拆成两个文件,一条路线一个,module.cpp **一个头都不包** |
| `displayinfo` | 501 个导出;测试比较两个枚举量,导出清单再塌陷就是编译错误 |
| `gnome.pangocairo` | 变通删除,`export import cairo;` |
| 验证 | 五个示例 × 两条工具链全绿 |
36 changes: 33 additions & 3 deletions pkgs/f/freedesktop.cairo.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,36 @@
-- `cairo_paint` working, and gives every PATH garbage fixed-point coordinates:
-- `cairo_rectangle(4,4,16,16)` came out with extents -8.03e+06 … 4.37e+06 and
-- `cairo_fill` changed zero pixels. Measured.
--
-- ─────────────────────────────────────────────────────────────────────────
-- ⚠️ THE MODULE IS `cairo`, NOT `freedesktop.cairo`
--
-- import cairo;
--
-- cairo is its own project; freedesktop.org hosts the git and does not own the
-- interface. **The index namespace is a shelf label and never enters the module
-- name** — the same reason `freedesktop.egl` exports `khronos.egl`.
--
-- This changed in the mcpp3 asset. A consumer on the old name gets a compile
-- error naming the module, which is the right place to find out.
--
-- ⭐ AND THE WRAPPER WAS INCOMPLETE: 470 names, missing `cairo_t` and all 192
-- enumerators. It was found from OUTSIDE, by `gnome.pangocairo`, which cannot
-- mix the two consumption routes and therefore had to ask the module for
-- `cairo_t` and got nothing.
--
-- cairo_t `typedef struct _cairo cairo_t;` — the typedef regex
-- needed a character between `cairo_` and `_t`, so the
-- most-used type in the library was the one it could not
-- match.
-- 192 enumerators every CAIRO_FORMAT_*, CAIRO_STATUS_*, CAIRO_OPERATOR_*.
-- The scan never entered a `typedef enum` body.
--
-- ⚠️ WHY NOBODY NOTICED, and the lesson worth keeping: the only test that
-- imported the module ALSO wrote `#include <cairo.h>`, so the header supplied
-- whatever the module lacked. **An import that is never asked to stand on its
-- own is not tested, it is decorated.** The fork now has two test files, one
-- per route, and exports 697 names.
package = {
spec = "1",
namespace = "freedesktop",
Expand All @@ -60,10 +90,10 @@ package = {
linux = {
["1.18.2"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/cairo/releases/download/v1.18.2/cairo-1.18.2-mcpp2.tar.gz",
CN = "https://gitcode.com/mcpp-res/cairo/releases/download/1.18.2/cairo-1.18.2-mcpp2.tar.gz",
GLOBAL = "https://github.com/mcpplibs/cairo/releases/download/v1.18.2/cairo-1.18.2-mcpp3.tar.gz",
CN = "https://gitcode.com/mcpp-res/cairo/releases/download/1.18.2/cairo-1.18.2-mcpp3.tar.gz",
},
sha256 = "202352a38d0847628c55cd0f9c67e85ac78667ad76bc88691ab47606d1104ef7",
sha256 = "5a82cfb52365f40b7ecc679d7d67c3c34459aef4a69b4b75c2c0c964d74ec6e1",
},
},
},
Expand Down
29 changes: 25 additions & 4 deletions pkgs/f/freedesktop.libdisplay-info.lua
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,35 @@
-- THE MODULE IS GENERATED, not hand-written: 206 names read out of the public
-- headers, so a version bump cannot silently drop one.
--
-- import freedesktop.displayinfo;
-- import displayinfo;
--
-- and that also removes a real burden — not one of the seven public headers has
-- an `extern "C"` block, so a C++ TU that #includes them mangles every
-- declaration and fails to link with `undefined reference to
-- di_info_get_make(di_info const*)`. The module does that wrapping once, inside
-- the module purview, so a consumer does not. (compat.libseat has the same
-- upstream problem and no module, so there the consumer still wraps.)
--
-- ─────────────────────────────────────────────────────────────────────────
-- ⚠️ THE MODULE IS `displayinfo`, NOT `freedesktop.displayinfo`
--
-- import displayinfo;
--
-- freedesktop.org HOSTS this library; the interfaces it parses are **VESA's**
-- (EDID, DisplayID). `freedesktop.*` in a module name is for freedesktop's own
-- specifications, the way `freedesktop.wayland.client` is. The index namespace
-- is a shelf label and never enters the module name.
--
-- This changed in the mcpp3 asset; a consumer on the old name gets a compile
-- error naming the module.
--
-- ⭐ AND THE WRAPPER EXPORTED 206 NAMES AND NOT ONE ENUMERATOR — the module
-- carried `enum di_edid_…` but nothing inside it. 295 of them.
--
-- ⚠️ It went unnoticed because no test named an enumerator, and adding one on
-- GCC alone would not have noticed either: exporting an enum makes its
-- enumerators visible there, and clang rejects the same file. The same gap was
-- in `freedesktop.cairo`. 501 names now, and the test compares two enumerators.
package = {
spec = "1",
namespace = "freedesktop",
Expand All @@ -66,10 +87,10 @@ package = {
linux = {
["0.2.0"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/libdisplay-info/releases/download/v0.2.0/libdisplay-info-0.2.0-mcpp2.tar.gz",
CN = "https://gitcode.com/mcpp-res/libdisplay-info/releases/download/0.2.0/libdisplay-info-0.2.0-mcpp2.tar.gz",
GLOBAL = "https://github.com/mcpplibs/libdisplay-info/releases/download/v0.2.0/libdisplay-info-0.2.0-mcpp3.tar.gz",
CN = "https://gitcode.com/mcpp-res/libdisplay-info/releases/download/0.2.0/libdisplay-info-0.2.0-mcpp3.tar.gz",
},
sha256 = "8df9a8064146b2b38378bd8b146894d084ffda43b88b43c485b954da073a7617",
sha256 = "8b2238a7b275e7da831d96c4f73b607a522d03620e6d5e0f12c605ef04659c8b",
},
},
},
Expand Down
6 changes: 3 additions & 3 deletions pkgs/p/gnome.pango.lua
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ package = {
["1.56.1"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/pango/archive/refs/tags/1.56.1.tar.gz",
-- ⚠️ The container tag is `1.56.1-4`, not `1.56.1`. gitcode
-- ⚠️ The container tag is `1.56.1-6`, not `1.56.1`. gitcode
-- refuses to REPLACE an asset of the same name in an
-- existing release, so each corrected tarball needs a new
-- container tag while the PACKAGE version stays upstream's.
-- Verified byte-identical to the GLOBAL tag archive.
CN = "https://gitcode.com/mcpp-res/pango/releases/download/1.56.1-4/pango-1.56.1.tar.gz",
CN = "https://gitcode.com/mcpp-res/pango/releases/download/1.56.1-6/pango-1.56.1.tar.gz",
},
sha256 = "b470a658e05ef0e14d779bc852371ce52dd11efa76d10782347be40f1d63476b",
sha256 = "19507c6712304750a0e9cc282135dda1919a1b89c0c4ce61e22411df0532a980",
},
},
},
Expand Down
34 changes: 25 additions & 9 deletions pkgs/p/gnome.pangocairo.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,28 @@
-- then segfaulted. The fork's test now reads the font map's FONT TYPE and
-- requires `CAIRO_FONT_TYPE_FT`, which catches it immediately.
--
-- ⚠️ AND freedesktop.cairo's MODULE IS NOT SUFFICIENT ON ITS OWN. Measured on
-- 1.18.2: 470 names, ZERO enumerators (no `CAIRO_FORMAT_ARGB32`, no
-- `CAIRO_FONT_TYPE_FT`) and no `cairo_t`. The index's own cairo example does
-- not notice, because it writes BOTH `#include <cairo.h>` and
-- `import freedesktop.cairo;`. pangocairo cannot mix the routes, so its module
-- scans cairo.h itself; that can go when cairo's wrapper is fixed.--
-- ⭐ AND THIS PACKAGE IS WHERE cairo's MODULE GAP WAS FOUND. On the
-- `1.18.2-mcpp2` asset `freedesktop.cairo` exported 470 names, ZERO
-- enumerators and no `cairo_t`. The index's own cairo example did not notice,
-- because it wrote BOTH `#include <cairo.h>` and `import freedesktop.cairo;`
-- — the header supplied what the module lacked, so that import had never been
-- asked to stand on its own.
--
-- It surfaced here because pangocairo CANNOT mix the two routes
-- (`pangocairo.h` reaches glib and therefore <stdio.h>), so it had to ask the
-- module for `cairo_t` and got nothing. This package therefore scans cairo.h
-- itself.
--
-- ⚠️ THAT WORKAROUND STAYS UNTIL cairo's FIX IS PUBLISHED, not merely written.
-- It is fixed in `1.18.2-mcpp3` (697 names), but this package resolves cairo
-- from the PUBLISHED index — and the change cannot even be TESTED in one index
-- PR, because a member may declare only ONE project-level index repo:
--
-- error: ≥2 project-level index repos is a known xlings resolution gap
--
-- so an example cannot use a local `gnome.*` descriptor and a local
-- `freedesktop.*` descriptor at the same time. cairo lands first; this
-- follows.--
-- ─────────────────────────────────────────────────────────────────────────
-- ⭐ TWO WAYS TO CONSUME IT, AND YOU PICK ONE
--
Expand Down Expand Up @@ -90,14 +106,14 @@ package = {
["1.56.1"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/pango/archive/refs/tags/1.56.1.tar.gz",
-- ⚠️ The container tag is `1.56.1-4`, not `1.56.1`. gitcode
-- ⚠️ The container tag is `1.56.1-6`, not `1.56.1`. gitcode
-- refuses to REPLACE an asset of the same name in an
-- existing release, so each corrected tarball needs a new
-- container tag while the PACKAGE version stays upstream's.
-- Verified byte-identical to the GLOBAL tag archive.
CN = "https://gitcode.com/mcpp-res/pango/releases/download/1.56.1-4/pango-1.56.1.tar.gz",
CN = "https://gitcode.com/mcpp-res/pango/releases/download/1.56.1-6/pango-1.56.1.tar.gz",
},
sha256 = "b470a658e05ef0e14d779bc852371ce52dd11efa76d10782347be40f1d63476b",
sha256 = "19507c6712304750a0e9cc282135dda1919a1b89c0c4ce61e22411df0532a980",
},
},
},
Expand Down
6 changes: 3 additions & 3 deletions pkgs/p/gnome.pangoft2.lua
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ package = {
["1.56.1"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/pango/archive/refs/tags/1.56.1.tar.gz",
-- ⚠️ The container tag is `1.56.1-4`, not `1.56.1`. gitcode
-- ⚠️ The container tag is `1.56.1-6`, not `1.56.1`. gitcode
-- refuses to REPLACE an asset of the same name in an
-- existing release, so each corrected tarball needs a new
-- container tag while the PACKAGE version stays upstream's.
-- Verified byte-identical to the GLOBAL tag archive.
CN = "https://gitcode.com/mcpp-res/pango/releases/download/1.56.1-4/pango-1.56.1.tar.gz",
CN = "https://gitcode.com/mcpp-res/pango/releases/download/1.56.1-6/pango-1.56.1.tar.gz",
},
sha256 = "b470a658e05ef0e14d779bc852371ce52dd11efa76d10782347be40f1d63476b",
sha256 = "19507c6712304750a0e9cc282135dda1919a1b89c0c4ce61e22411df0532a980",
},
},
},
Expand Down
19 changes: 17 additions & 2 deletions tests/examples/cairo/tests/cairo.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// freedesktop.cairo — draw something, read the pixels back, and check that the
// cairo, via its HEADERS — draw something, read the pixels back, and check that the
// feature split actually took effect.
//
// Cairo needs no display and no fonts on disk to draw: an image surface is
Expand All @@ -14,11 +14,26 @@
#ifdef __linux__

#include <cairo.h>
// ⚠️ cairo.h does NOT reach the feature headers. This name used to arrive from
// the module — which is precisely the confusion this split removes: on the
// header route you include what you use.
#include <cairo-ft.h>

#include <cstdio>
#include <cstring>

import freedesktop.cairo;
// ⚠️ THIS FILE IS THE HEADER ROUTE, AND IT NO LONGER IMPORTS.
//
// It used to do BOTH — `#include <cairo.h>` and `import freedesktop.cairo;` —
// and that is why nobody noticed the module was incomplete: the header
// supplied every name the module was missing, including `cairo_t` and all 192
// enumerators. An import that is never asked to stand on its own is not
// tested, it is decorated.
//
// `tests/module.cpp` next door takes the module route alone. Two files,
// because on GCC the two routes cannot be combined in one TU anyway: reaching
// a system header both textually and through a module's global fragment makes
// the same type two entities.

namespace {

Expand Down
Loading
Loading