From 2e04f268fe3d8b3d838cedacf1ff851e3b0a64c0 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 31 Aug 2026 08:39:24 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(gnome.gio):=20gio=20=E5=88=B0=E4=BD=8D?= =?UTF-8?q?,pango=20=E7=9A=84=E9=97=B8=E9=97=A8=E5=BC=80=E4=BA=86=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E4=BB=A5=E5=8F=8A=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=E8=A2=AB=E6=8E=A8=E7=BF=BB=E7=9A=84=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §19.7 说 gio 缺席,理由是 gdbus-codegen 有 8,351 行 Python,在 build.mcpp 里 复刻它「与消费者的需要不成比例」。前面两条测量是对的,结论推不出来 —— 它把 两件不同的事混成了一件: 复刻这个生成器 ≠ 拿到它的输出 构建从来不需要那个生成器,只需要 15,392 行 C,而那是五个 XML 与 codegen 版本 的纯函数。mcpp/tools/gengdbus.sh 用上游的原参数生成一次,产物提交进 mcpp/generated/,CI 在另一台机器、另一个 Python 上重新生成并 diff。 第三步是全部论证所在:提交进仓库的生成物有一个构建永远看不见的失败模式 —— 它可以停止匹配自己的输入,而且照样能编译。索引里本来就有先例 (freedesktop.wayland-protocols-* 的 195 个生成文件),当初却因为「生成器太大」 否掉了整个包,判据用错了对象。 ⭐ 而且这次改动挖出了一个已经发货的缺陷。把 glib-mkenums 从 gobject 的 4 个 enum 扩到 gio 的 82 个,逼着去读真正的算法,才发现它有两个前缀:enum_prefix 取自枚举成员(决定 nick),@ENUMPREFIX@ 取自类型名(决定宏名)。原实现用第一个 当了两个用,于是 gnome.gobject 2.82.5 发出去的是 G_UNICODE_TYPE_TYPE,上游是 G_TYPE_UNICODE_TYPE,四个宏全错。 每一个函数名都是对的,所以它编译通过、链接通过,并且通过了那个只检查函数和 nick 的测试。推广出来的一条:一个生成器有多个互相独立的输出,测了其中一个不 构成对其余的证据。CI 现在分开检查宏名、nick、enum/flags 归类;gobject 示例加 了一行 G_TYPE_UNICODE_TYPE == g_unicode_type_get_type() —— 宏拼错就是编译 错误,这是唯一能识破旧 tarball 的探针。 包版本仍是上游的 2.82.5(不加 fork 后缀):tag 原地重切,sha256 换成 628b8f98…,CN 容器 tag 走 2.82.5-3(gitcode 不允许替换同名 asset)。代价写进 了描述符:store 按 (name, version) 索引,已解开过旧 2.82.5 的机器要清一下。 gio 的四类输入里有三类缺了会静默降级,所以示例按名字断言每一类:inotify 读回 GFileMonitor 的类型名是不是 GInotifyFileMonitor(和轮询实现唯一能区分的地方)、 gvdb 用一段非 gvdb 字节要求它干净地拒绝、xdgmime 只断言「答得出来」而不断言 具体 MIME(那取决于 runner 有没有 /usr/share/mime)。 pkgs/g/gnome.gio.lua 新增 pkgs/g/gnome.{glib,gobject,gmodule}.lua 新 sha256;❌ 标注被推翻的 gio 段落 tests/examples/gio/ 30 项断言 tests/examples/gobject/ 加识破旧 tarball 的宏探针 .agents/docs/…design.md §19.7 就地标 ❌ 指向 §20;新增 §20 八小节 本地:四个示例在 gcc 16.1 与 llvm 22.1 上全绿;fork CI 四个 job 全绿。 --- ...26-08-30-graphics-stack-coverage-design.md | 186 ++++++++- pkgs/g/gnome.gio.lua | 171 ++++++++ pkgs/g/gnome.glib.lua | 95 +++-- pkgs/g/gnome.gmodule.lua | 33 +- pkgs/g/gnome.gobject.lua | 97 +++-- tests/examples/gio/mcpp.toml | 14 + tests/examples/gio/tests/gio.cpp | 385 ++++++++++++++++++ tests/examples/gobject/tests/gobject.cpp | 17 + 8 files changed, 887 insertions(+), 111 deletions(-) create mode 100644 pkgs/g/gnome.gio.lua create mode 100644 tests/examples/gio/mcpp.toml create mode 100644 tests/examples/gio/tests/gio.cpp 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 b0ef858..d36c44a 100644 --- a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -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.6,§11 总账 / §14 fork 规范 / §18 八角度复核 / §19 合成器闸门与 GObject 栈,含沙箱实测)** +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.7,§11 总账 / §14 fork 规范 / §18 八角度复核 / §19 合成器闸门与 GObject 栈,含沙箱实测 · §20 gio 与一次被推翻的判断)** ## 0. 这份文档解决什么 @@ -1676,22 +1676,27 @@ g++ 报 `expected primary-expression before 'static'`,而且解析再也没恢 1.49.1 就是按这条补的:上游的 meson 会装 `wayland-protocols/-enum.h`, fork 之前只生成 client/server 两种,而 wlroots 0.20 的**十个公共头**要它们。 -### 19.7 仍然缺的:gio,以及被它挡住的 pango +### 19.7 ❌ 本节的结论是错的 —— 见 §20 -这不是「还没做」,是量过的边界: +原文写的是「仍然缺的:gio,以及被它挡住的 pango」,理由是: -- gio 的六个生成器里有两个是 **`gdbus-codegen`,8,351 行 Python**,把 D-Bus - 接口 XML 变成 GObject skeleton。 -- **七个** gio 源码 include 它的产物,另有**两个**引用那七个。 -- 所以不能绕过它而仍然是 gio;在 `build.mcpp` 里复刻一个 8.3k 行的代码生成器 - 与消费者的需要不成比例。 +- gio 的六个生成器里有两个是 **`gdbus-codegen`,8,351 行 Python**; +- **七个** gio 源码 include 它的产物,另有**两个**引用那七个; +- 所以「在 `build.mcpp` 里复刻一个 8.3k 行的代码生成器与消费者的需要不成比例」。 -**pango 用 `GListModel`,它在 gio 里。** 所以文本排版那条线停在这里 —— 其余 -四个依赖(harfbuzz、fribidi、fontconfig、cairo)都已在索引中,只差这一环。 +**前两条测量是对的,第三条推不出结论。** 它把两件不同的事混成了一件: + +``` +复刻这个生成器 ≠ 拿到它的输出 +``` + +构建从来不需要那个生成器,只需要 **15,392 行 C**,而那是五个 XML 文件与 +codegen 版本的**纯函数**。`gnome.gio` 2.82.5 已经在索引里,pango 没有被挡住。 +完整记录见 **§20**。 `vulkan-renderer`(要 glslang)、`x11-backend` / `xwayland`(要 xcb)、 -`color-management`(要 lcms2)同理:每一个都在包里写明「为什么缺席」而不是 -默默不提。 +`color-management`(要 lcms2)仍然缺席,但那三条的理由是「依赖不在索引里」, +和本节原来的理由不是一回事 —— 不要把它们和这一条一起引用。 ### 19.8 「引用别的包生成的文件」是静默的 @@ -1782,3 +1787,160 @@ GRegex 当然不匹配汉字 —— 断言**正确地失败了**。 两条合起来是同一句:**判据本身也要能被证伪一次**,否则分不清「它在检查」和 「它在点头」。 + +--- + +## 20. gio:一次被推翻的「不成比例」判断(2026-08-31) + +§19.7 说 gio 缺席,理由是 `gdbus-codegen` 太大不值得复刻。**这条判断是错的**, +本节记录它错在哪、改完之后学到了什么。原节已就地标注 ❌ 并指向这里。 + +### 20.1 错在把「复刻生成器」当成了「拿到输出」 + +``` +复刻这个生成器 ≠ 拿到它的输出 +``` + +构建需要的是 **15,392 行 C**,而它是**五个 XML 文件与 codegen 版本的纯函数** +—— 没有 target、没有 host、没有 locale 进入其中。所以: + +1. `mcpp/tools/gengdbus.sh` 用上游 `gio/meson.build:238` 与 `:254` 的**原参数** + 生成一次; +2. 产物提交进 `mcpp/generated/`; +3. **CI 在另一台机器、另一个 Python 版本上重新生成并 `git diff --exit-code`**。 + +第 3 步是全部论证所在。**提交进仓库的生成物有一个构建永远看不见的失败模式: +它可以停止匹配自己的输入,而且照样能编译。** 有 diff,「checked in」才不等于 +「stale」。 + +这正是 `freedesktop.wayland-protocols-*` 已经在用的形态(195 个生成文件), +而且在那里是**被迫的** —— 产物本身就是头文件,包无法在构建期导出头(§19.6)。 +在 gio 这里并不被迫,它只是拿到那些代码最便宜的办法。 + +> 索引里已有先例,却因为「生成器太大」就否掉了整个包 —— 判据用错了对象: +> **要衡量的是产物能不能预先算出来,不是生成器有多大。** + +### 20.2 脚本必须在树外运行(不是整洁,是硬约束) + +`gengdbus.sh` 把 codegen 包**复制到临时目录**再跑。两样东西会写进 `upstream/`: + +| | | +|---|---| +| `codegen/config.py` | 它本身就是一个 `configure_file`,tarball 里只有 `.in` | +| `codegen/__pycache__/` | CPython import 时无条件写 | + +而 `upstream/ is the release tarball, unmodified` 是一个 CI job。原地跑就会踩它。 +CI 里因此有第二步断言:跑完 `git diff --exit-code -- upstream` 且 `find` 不到 +这两样东西。 + +### 20.3 ⭐ 同一个生成器的几个输出是**独立失败**的 —— 已发货的缺陷 + +把 `glib-mkenums` 从 gobject 的 4 个 enum 扩到 gio 的 82 个,逼着我去读真正的 +算法。读完发现:**mkenums 有两个前缀,来源不同。** + +| | 取自 | 决定 | +|---|---|---| +| `enum_prefix` | **枚举成员**(`G_UNICODE_`) | nick | +| `@ENUMPREFIX@` | **类型名**(`GUnicodeType` → `G`) | 宏名 | + +原来的实现用第一个当了两个用。结果 `gnome.gobject 2.82.5` 发出去的是: + +``` +G_UNICODE_TYPE_TYPE 上游是 G_TYPE_UNICODE_TYPE +G_UNICODE_BREAK_TYPE_TYPE G_TYPE_UNICODE_BREAK_TYPE +G_UNICODE_SCRIPT_TYPE_... G_TYPE_UNICODE_SCRIPT +G_NORMALIZE_TYPE_MODE G_TYPE_NORMALIZE_MODE +``` + +**每一个函数名都是对的**(`g_unicode_type_get_type`),所以它编译通过、链接通过、 +并且通过了我自己写的测试 —— 那个测试检查了**函数**和**nick**,从没检查过**宏**。 + +推广出来的一条: + +> **一个生成器有多个互相独立的输出;测了其中一个,不构成对其余的证据。** + +修法是让判据能分辨:CI 现在**分开**检查宏名、nick、以及 enum/flags 的归类; +索引里的 gobject 示例加了一条 `G_TYPE_UNICODE_TYPE == g_unicode_type_get_type()` +—— 宏拼错就是编译错误,这是唯一能识破旧 tarball 的探针。 + +顺带印证了 memory 里那条:**「绿 CI ≠ 被验证」**,以及**判据本身要被证伪一次**。 + +### 20.4 annotation 是唯一判据,两个方向都会错 + +gio 用 `/*< flags >*/` 7 次、`/*< nick=… >*/` 17 次、`/*< prefix=… >*/` 1 次 —— +但也用 `/*< private >*/`、`/*< public >*/`、`/*< protected >*/` **144 次**, +那些是给 **struct 成员**看的 GTK-DOC 注解,对 mkenums 没有意义。 + +- 往一个方向错:**读所有 `/*< … >*/`** → 静默丢掉枚举成员。 +- 往另一个方向错:**按类型名猜** → `GConverterFlags` 没有 `/*< flags >*/`, + 上游用 `g_enum_register_static` 注册它。按名字猜就和上游 ABI 不一致。 + +nick 是公开 API(`g_flags_get_value_by_nick` 读它):`G_CONVERTER_NO_FLAGS` +推导出来是 `"no-flags"`,上游写的是 `"none"`。 + +另一个扫描器坑:`} GTlsRehandshakeMode GIO_DEPRECATED_TYPE_IN_2_60;` —— +取 `}` 之后**整段**当类型名,会生成 +`_g_i_o__d_e_p_r_e_c_a_t_e_d__t_y_p_e__i_n_2_60_get_type`。只取第一个标识符。 + +**验证方式**:生成的 `gioenumtypes.h` 的 82 个 `#define G_TYPE_*` 与系统 +`/usr/include/glib-2.0/gio/gioenumtypes.h` 逐名相同;82 个 getter 全部存在于 +发行版 `libgio-2.0.so.0` 的 ABI 里。 + +### 20.5 gio 有四类输入,其中三类缺了会**静默降级** + +| 输入 | 缺了会怎样 | +|---|---| +| `gio/*.c` | undefined reference —— 链接器会抓。最便宜的一类 | +| `gio/xdgmime/` | `g_content_type_guess` 对一切回答 `application/octet-stream` | +| `gio/inotify/` | 文件监视**静默**退回轮询实现 | +| `subprojects/gvdb/` | GResource 与 GSettings 的 schema 读取根本走不到 | +| `mcpp/generated/` | 照样编译;portal 调用只在 Flatpak 沙箱里才失败 | + +所以测试按名字断言每一类,而不是「它构建了」: + +- inotify:读回 `GFileMonitor` 的**类型名**是不是 `GInotifyFileMonitor` —— + 这是轮询实现和它唯一能区分的地方; +- gvdb:拿一段**肯定不是 gvdb** 的字节给 `g_resource_new_from_data`,要求它 + **干净地拒绝**(返回 NULL 且 GError 有值)—— 那个答案只可能来自 + `gvdb_table_new_from_bytes`; +- xdgmime:只断言「答得出来」和两个转换互为逆 —— **不断言具体 MIME**,那取决 + 于 runner 有没有 `/usr/share/mime`,断言它就是在检查 runner 而不是这个构建。 + +`-DXDG_PREFIX=_gio_xdg` 是**必须**的:xdgmime 的头把每个 `xdg_mime_*` 宏改名成 +`_gio_xdg_mime_*`,而 `gcontenttype.c` 调的是不带前缀的写法 —— 两边看到的值必须 +一致,否则调用和定义是两个不同的符号。 + +### 20.6 排除模式「看起来完整」的两种方式 + +```toml +"!../../upstream/gio/gwin32*.c", # 漏了 giowin32-private.c、gmemorymonitorwin32.c +"!../../upstream/gio/gosxappinfo.c", # 上游是 .m,这条什么都没匹配到 +``` + +第一种**编译期报错**(`unknown type name 'gchar'`),第二种**完全静默** —— +和 §19.8 是同一类:**匹配不到任何文件的条目不会报错**。现在两类都在注释里写明。 + +### 20.7 版本形态:包版本就是上游版本,不加后缀 + +一度把它写成 `2.82.5.1`(「上游 2.82.5,fork 修订 1」)。**这是错的**,索引的 +约定是**包版本与上游版本对齐**: + +- fork 变了而上游没变时,**原地重切 tag**,描述符换新的 `sha256`; +- CN 镜像那边 gitcode **不允许替换同名 asset**,所以每份更正过的 tarball 需要 + 一个新的**容器 tag**(`2.82.5-3`),而**包版本不动**。 + +代价要写明白:**store 按 `(name, version)` 索引**,已经解开过 `2.82.5` 的机器 +会留着旧的那份。这次的探针(§20.3 那条宏断言)正是用来识破它的 —— +旧 tarball 上那行是编译错误。 + +### 20.8 结果 + +| | | +|---|---| +| `gnome.gio` 2.82.5 | 815 个对象,35 项断言,gcc 16.1 + llvm 22.1 双绿 | +| `gnome.gobject` 2.82.5 | 四个宏名更正,并加了识破旧 tarball 的探针 | +| fork CI | 四个 job:两条工具链 + `mcpp/generated/` 重新生成并 diff + `upstream/` 未改 | +| 消费者形态 | 一个同时点名 gobject、gmodule、gio 的程序,`G_TYPE_LIST_MODEL` 是活的接口 | + +**pango 的闸门开了**:它的五个依赖(gio、gobject、harfbuzz、fribidi、 +fontconfig、cairo)全部在索引里。 diff --git a/pkgs/g/gnome.gio.lua b/pkgs/g/gnome.gio.lua new file mode 100644 index 0000000..bc493b1 --- /dev/null +++ b/pkgs/g/gnome.gio.lua @@ -0,0 +1,171 @@ +-- gnome.gio — part of GLib 2.82.5. +-- +-- Files, streams, sockets, D-Bus, GApplication, GSettings, GResource — and +-- `GListModel`, which is what pango's `PangoFontMap` implements. +-- +-- Upstream ships glib as one source tree producing FOUR separate shared +-- libraries, and this index follows that split: `gnome.glib`, `gnome.gobject`, +-- `gnome.gmodule` and `gnome.gio` are four packages from one fork, because +-- that is what a consumer links. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⭐ THIS PACKAGE EXISTS BECAUSE AN EARLIER ARGUMENT WAS WRONG +-- +-- gio was left out of this index once, with the reason: two of its six +-- generators are `gdbus-codegen`, an 8,351-line Python program that turns +-- D-Bus interface XML into GObject skeletons, and reimplementing it in +-- `build.mcpp` is not proportionate. That premise was measured and correct. +-- The conclusion did not follow, because it conflated two different things: +-- +-- reproducing the generator ≠ obtaining its output +-- +-- The build never needed the generator. It needed 15,392 lines of C that are a +-- PURE FUNCTION of five XML files and the codegen version — no target, no +-- host, no locale enters it. So `mcpp/tools/gengdbus.sh` produces them once, +-- they live in `mcpp/generated/`, and a CI job REGENERATES AND DIFFS them on a +-- runner with a different Python than the one that produced them. +-- +-- That last part is the whole argument. Committed output has exactly one +-- failure mode a build never sees: it can stop matching its input, and it +-- still compiles. A diff is what makes "checked in" different from "stale". +-- +-- The same arrangement `freedesktop.wayland-protocols-*` already used for its +-- 195 generated files — where it was FORCED, since a package whose product is +-- headers cannot generate at build time. Here it is not forced; it is simply +-- the cheap way to get code a rewrite would otherwise have to produce. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE OTHER FIVE GENERATORS ARE C++, AS EVERYWHERE ELSE IN THIS FORK +-- +-- gio-visibility.h gen_visibility("GIO", …) +-- gnetworking.h one substitution +-- gioenumtypes.{h,c} glib-mkenums over 82 enums +-- +-- ⚠️ 82 ENUMS AGAINST gobject's FOUR, and what changes at that scale is not +-- the loop but the ANNOTATIONS — with a trap in each direction: +-- +-- gio writes `/*< private >*/`, `/*< public >*/` and `/*< protected >*/` +-- 144 times. Those are GTK-DOC annotations for STRUCT MEMBERS and mean +-- nothing to mkenums. A scanner that read every `/*< … >*/` would silently +-- drop enumerators. +-- +-- `GConverterFlags` has NO `/*< flags >*/` despite the name, so upstream +-- registers it with `g_enum_register_static`. A scanner that guessed from +-- the type name would disagree with upstream's ABI. +-- +-- The nicks are public API — `g_flags_get_value_by_nick` reads them — and gio +-- overrides seventeen of them. Derived, `G_CONVERTER_NO_FLAGS` would be +-- "no-flags"; upstream says "none". +-- +-- `gconstructor_as_data.h` is upstream's sixth generator and is NOT here: it +-- embeds `glib/gconstructor.h` as a C string for `glib-compile-resources`, a +-- TOOL. See below. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ WHAT gio NEEDS BESIDES gio/*.c — AND WHY EACH IS CHECKED BY NAME +-- +-- gio is assembled from four kinds of input. A missing `gio/*.c` is an +-- undefined reference, which the linker catches. The other three DEGRADE +-- SILENTLY, so the example test asserts each one by name rather than trusting +-- that it built: +-- +-- gio/xdgmime/ absent → g_content_type_guess answers +-- "application/octet-stream" for everything +-- gio/inotify/ absent → the file monitor quietly falls back to +-- polling; the test reads the backend's TYPE NAME, +-- which is the only place the two differ +-- subprojects/gvdb/ absent → GResource and GSettings' schema reader are +-- never reachable, and nothing says so +-- mcpp/generated/ stale → it still compiles; the portal calls fail +-- only inside a Flatpak sandbox +-- +-- `-DXDG_PREFIX=_gio_xdg` is load-bearing rather than hygiene: xdgmime's +-- header macro-renames every `xdg_mime_*` symbol, and `gcontenttype.c` calls +-- the unprefixed spelling. Both sides must see the same value or the calls and +-- the definitions are different symbols. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- NO TOOLS, AND NO EXTERNAL DEPENDENCY BUT zlib +-- +-- The ten tools — `gio-tool`, `gdbus-tool`, `glib-compile-schemas`, +-- `glib-compile-resources`, `gsettings`, … — are NOT built. Each provides +-- `main`, and a dependency that provides one collides with the consumer's; +-- that is the rule every package in this index follows. +-- +-- Upstream's meson names exactly one external dependency for the library +-- beyond glib itself: `libelf`, used by `gresource-tool.c` — an +-- `executable()`, not the library. Measured: no library source mentions elf at +-- all. There is therefore NO libelf feature, because there is nothing for it +-- to switch. +-- +-- `libmount`, `selinux` and `sysprof` are `auto` upstream and absent here. +-- Each is `#ifdef`-tested, so each is ABSENT rather than 0, and gio degrades +-- the way upstream intends: GUnixMountMonitor falls back to reading +-- /proc/self/mountinfo, and the SELinux and tracing attributes are not +-- offered. Defining any of them as 0 would say "yes" — see `gnome.glib` for +-- the nine macros that lesson came from. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ NAME WHAT YOU USE; DO NOT NAME glib +-- +-- [dependencies] +-- gnome.gio = "2.82.5" +-- gnome.gobject = "2.82.5" +-- # gnome.glib and gnome.gmodule arrive transitively +-- +-- gio declares glib, gobject and gmodule as workspace PATH dependencies — they +-- are built from one tree — and mcpp rejects a package requested both ways: +-- +-- error: dependency 'gnome.glib' is requested as both a version dep +-- (by 'your-package') and a path dep (by 'gnome.gio') +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ NO extern "C" WRAPPER +-- +-- gio decorates every header with G_BEGIN_DECLS, so a wrapper is redundant — +-- and harmful: gio.h reaches , which libc++ routes through , +-- which defines TEMPLATES. Inside an extern "C" block that is +-- `templates must have C++ linkage`, dozens of times, against a header the +-- consumer never named. libstdc++ does not route them that way, so gcc is +-- green and clang is a wall. +-- +-- There is no module, for the same reason as the rest of the family: the API +-- is macro-heavy (`G_DEFINE_TYPE`, `G_IS_FILE`, `g_signal_connect`) and macros +-- do not cross a module boundary. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- LINUX ONLY +-- +-- This is the Unix build: the Windows and Cocoa backends are excluded because +-- they are ALTERNATIVES to the Unix ones, not companions — both halves define +-- the volume monitor, the app-info backend and the settings backend. The +-- generated `glibconfig.h` fixes `G_OS_UNIX` besides, and the upstream tree +-- carries two `COPYING` symlinks a Windows extraction would not survive. +package = { + spec = "1", + namespace = "gnome", + name = "gio", + description = "GIO 2.82.5 — files, streams, sockets, D-Bus, GSettings and GListModel", + licenses = {"LGPL-2.1-or-later"}, + repo = "https://github.com/mcpplibs/glib", + type = "package", + + xpm = { + linux = { + ["2.82.5"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", + -- ⚠️ The container tag is `2.82.5-3`, not `2.82.5`. 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/glib/releases/download/2.82.5-3/glib-2.82.5.tar.gz", + }, + sha256 = "628b8f98a51238563704bf50817d575a46421acc32d987ca93edc941a1749d62", + }, + }, + }, + + mcpp = "*/mcpp/gio/mcpp.toml", +} diff --git a/pkgs/g/gnome.glib.lua b/pkgs/g/gnome.glib.lua index a4e03dd..0165648 100644 --- a/pkgs/g/gnome.glib.lua +++ b/pkgs/g/gnome.glib.lua @@ -2,44 +2,52 @@ -- -- Upstream ships glib as one source tree producing FOUR separate shared -- libraries, and this index follows that split: `gnome.glib`, --- `gnome.gobject` and `gnome.gmodule` are three packages from one fork, --- because that is what a consumer links. (The fourth, gio, is absent — see --- below.) +-- `gnome.gobject`, `gnome.gmodule` and `gnome.gio` are four packages from one +-- fork, because that is what a consumer links. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE PACKAGE VERSION IS UPSTREAM'S, WITH NOTHING APPENDED +-- +-- `2.82.5` means GLib 2.82.5. When the fork changes and upstream does not, the +-- tag is RE-CUT IN PLACE and the `sha256` below moves — there is no +-- fork-revision component for a consumer to read. +-- +-- ⚠️ The store keys on (name, version), so a machine that already extracted a +-- 2.82.5 keeps what it extracted. This tarball added `gnome.gio` and corrected +-- four generated macro names in `gnome.gobject`; if you do not see either, +-- clear that store entry. The container tag on the CN mirror is what tells the +-- two tarballs apart — see the `xpm` block. -- -- ───────────────────────────────────────────────────────────────────────── -- WHY A FORK -- --- Generators, not line count. GLib has six, and `build.mcpp` reimplements all --- of them, so the tree carries no `sh` and no `python`: +-- Generators, not line count. GLib has seven, and `build.mcpp` reimplements +-- six, so the tree carries no `sh` and no `python` in the build path: -- -- gen-visibility-macros.py versions-macros glib/gversionmacros.h --- gen-visibility-macros.py visibility-macros three *-visibility.h +-- gen-visibility-macros.py visibility-macros four *-visibility.h -- configure_file glibconfig.h -- configure_file gmodule/gmoduleconf.h +-- configure_file gio/gnetworking.h -- gobject/glib-mkenums (816 lines of Python) glib-enumtypes.{h,c} +-- gio/gioenumtypes.{h,c} -- configure_file config.h -- --- glib-mkenums is reproduced FOR THE ONE INPUT this build points it at — --- `glib/gunicode.h`, four enums — rather than wholesale. It reads upstream's --- `.template` files from the tree so a template change is picked up, and --- exits non-zero if that header stops yielding four enums: a silent drop --- would produce a library missing `g_unicode_script_get_type` and the failure --- would land in a consumer. +-- The seventh, `gdbus-codegen`, is NOT reimplemented — its output is checked +-- in and CI regenerates and diffs it. See gnome.gio for why that is the right +-- shape rather than a shortcut. +-- +-- glib-mkenums is reproduced for the inputs this build points it at — four +-- enums for gobject, 82 for gio — reading upstream's `.template` files from +-- the tree so a template change is picked up, and exiting non-zero if a +-- header stops yielding what it should: a silent drop would produce a library +-- missing `g_unicode_script_get_type` and the failure would land in a +-- consumer. -- -- https://github.com/mcpplibs/glib -- -- ───────────────────────────────────────────────────────────────────────── --- ⚠️ C++ CONSUMERS MUST WRAP THE INCLUDES --- --- glib's headers carry their own `G_BEGIN_DECLS`, so this is usually fine — --- but the generated `glib-enumtypes.h` comes from a template, and wrapping --- costs nothing: --- --- extern "C" { --- #include --- } --- --- There is no module. glib's API is macro-heavy — `G_DEFINE_TYPE`, +-- THERE IS NO MODULE. glib's API is macro-heavy — `G_DEFINE_TYPE`, -- `G_OBJECT`, `g_signal_connect` are all macros — and macros do not cross a -- module boundary, so an `import` would hand a consumer the declarations and -- withhold the half of the API that makes them usable. Compare @@ -48,22 +56,30 @@ -- not a house style. -- -- ───────────────────────────────────────────────────────────────────────── --- ⚠️ gio IS NOT IN THIS INDEX, AND THAT BLOCKS pango +-- ❌ THE "gio IS NOT IN THIS INDEX" SECTION THAT USED TO BE HERE WAS WRONG. +-- +-- It argued: two of gio's six generators are `gdbus-codegen`, an 8,351-line +-- Python program, and reimplementing it in `build.mcpp` is not proportionate +-- — therefore gio is absent and pango is blocked. +-- +-- The premise was right and the conclusion did not follow. It conflated +-- +-- reproducing the generator ≠ obtaining its output -- --- Two of gio's six generators are `gdbus-codegen`, an 8,351-line Python --- program that turns D-Bus interface XML into GObject skeletons. Seven gio --- sources include its output and two more reference those, so it cannot be --- dropped without changing what gio is, and reimplementing it in --- `build.mcpp` is not proportionate. +-- The build never needed the generator. It needed 15,392 lines of C that are +-- a pure function of five XML files and the codegen version. Those are now +-- produced once by a maintainer script, checked in, and REGENERATED AND +-- DIFFED BY CI — the same arrangement `freedesktop.wayland-protocols-*` had +-- already been using for its 195 generated files. -- --- pango uses `GListModel`, which lives in gio, so the text-layout line stops --- here. Measured and recorded rather than left as an unexplained gap. +-- `gnome.gio` is in this index. pango is not blocked. -- -- ───────────────────────────────────────────────────────────────────────── --- ⚠️ NAME gobject AND gmodule; DO NOT NAME glib +-- ⚠️ NAME WHAT YOU USE; DO NOT NAME glib -- -- [dependencies] -- gnome.gobject = "2.82.5" +-- gnome.gio = "2.82.5" -- gnome.gmodule = "2.82.5" -- # gnome.glib arrives transitively -- @@ -109,15 +125,14 @@ package = { ["2.82.5"] = { url = { GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", - -- ⚠️ The container tag is `2.82.5-1`, not `2.82.5`. The fork's - -- tag was re-cut once while this descriptor was still - -- unpublished — safe only because nothing had extracted it - -- yet — and gitcode refuses to REPLACE an asset of the same - -- name in an existing release. Verified: this URL's sha256 - -- equals the GLOBAL tarball's. - CN = "https://gitcode.com/mcpp-res/glib/releases/download/2.82.5-2/glib-2.82.5.tar.gz", + -- ⚠️ The container tag is `2.82.5-3`, not `2.82.5`. 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/glib/releases/download/2.82.5-3/glib-2.82.5.tar.gz", }, - sha256 = "fb65671207e5ec409a1fdb2156dafb49bf8c772ada252a17844991a14de99ea8", + sha256 = "628b8f98a51238563704bf50817d575a46421acc32d987ca93edc941a1749d62", }, }, }, diff --git a/pkgs/g/gnome.gmodule.lua b/pkgs/g/gnome.gmodule.lua index 5733d59..a3cbb81 100644 --- a/pkgs/g/gnome.gmodule.lua +++ b/pkgs/g/gnome.gmodule.lua @@ -48,22 +48,22 @@ -- not a house style. -- -- ───────────────────────────────────────────────────────────────────────── --- ⚠️ gio IS NOT IN THIS INDEX, AND THAT BLOCKS pango +-- ❌ THE "gio IS NOT IN THIS INDEX" SECTION THAT USED TO BE HERE WAS WRONG. -- --- Two of gio's six generators are `gdbus-codegen`, an 8,351-line Python --- program that turns D-Bus interface XML into GObject skeletons. Seven gio --- sources include its output and two more reference those, so it cannot be --- dropped without changing what gio is, and reimplementing it in --- `build.mcpp` is not proportionate. +-- It argued that reimplementing `gdbus-codegen` in `build.mcpp` was not +-- proportionate, and concluded that gio must be absent. The premise held; the +-- conclusion did not follow, because the build never needed the GENERATOR — +-- only its OUTPUT, which is a pure function of five XML files. That output is +-- now checked in and CI regenerates and diffs it. -- --- pango uses `GListModel`, which lives in gio, so the text-layout line stops --- here. Measured and recorded rather than left as an unexplained gap. +-- `gnome.gio` is in this index. See gnome.glib for the full note. -- -- ───────────────────────────────────────────────────────────────────────── --- ⚠️ NAME gobject AND gmodule; DO NOT NAME glib +-- ⚠️ NAME WHAT YOU USE; DO NOT NAME glib -- -- [dependencies] -- gnome.gobject = "2.82.5" +-- gnome.gio = "2.82.5" -- gnome.gmodule = "2.82.5" -- # gnome.glib arrives transitively -- @@ -109,15 +109,14 @@ package = { ["2.82.5"] = { url = { GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", - -- ⚠️ The container tag is `2.82.5-1`, not `2.82.5`. The fork's - -- tag was re-cut once while this descriptor was still - -- unpublished — safe only because nothing had extracted it - -- yet — and gitcode refuses to REPLACE an asset of the same - -- name in an existing release. Verified: this URL's sha256 - -- equals the GLOBAL tarball's. - CN = "https://gitcode.com/mcpp-res/glib/releases/download/2.82.5-2/glib-2.82.5.tar.gz", + -- ⚠️ The container tag is `2.82.5-3`, not `2.82.5`. 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/glib/releases/download/2.82.5-3/glib-2.82.5.tar.gz", }, - sha256 = "fb65671207e5ec409a1fdb2156dafb49bf8c772ada252a17844991a14de99ea8", + sha256 = "628b8f98a51238563704bf50817d575a46421acc32d987ca93edc941a1749d62", }, }, }, diff --git a/pkgs/g/gnome.gobject.lua b/pkgs/g/gnome.gobject.lua index 0d535a8..63e9189 100644 --- a/pkgs/g/gnome.gobject.lua +++ b/pkgs/g/gnome.gobject.lua @@ -2,25 +2,50 @@ -- -- Upstream ships glib as one source tree producing FOUR separate shared -- libraries, and this index follows that split: `gnome.glib`, --- `gnome.gobject` and `gnome.gmodule` are three packages from one fork, --- because that is what a consumer links. (The fourth, gio, is absent — see --- below.) +-- `gnome.gobject`, `gnome.gmodule` and `gnome.gio` are four packages from one +-- fork, because that is what a consumer links. -- -- ───────────────────────────────────────────────────────────────────────── --- WHY A FORK +-- ⭐ FOUR WRONG MACRO NAMES, AND WHY NOTHING CAUGHT THEM +-- +-- The FIRST 2.82.5 tarball — cut before `gnome.gio` existed, superseded by the +-- one this descriptor names — defined: +-- +-- what it defined what upstream defines +-- G_UNICODE_TYPE_TYPE G_TYPE_UNICODE_TYPE +-- G_UNICODE_BREAK_TYPE_TYPE G_TYPE_UNICODE_BREAK_TYPE +-- G_UNICODE_SCRIPT_TYPE_... G_TYPE_UNICODE_SCRIPT +-- G_NORMALIZE_TYPE_MODE G_TYPE_NORMALIZE_MODE +-- +-- ⭐ HOW IT GOT THROUGH IS THE PART WORTH KEEPING. `glib-mkenums` has TWO +-- prefixes and they come from different places: +-- +-- enum_prefix from the ENUMERATORS (G_UNICODE_) → drives the nicks +-- @ENUMPREFIX@ from the TYPE NAME (G) → drives the macro +-- +-- The reimplementation used the first for both. Every FUNCTION name was still +-- right — `g_unicode_type_get_type` — so the library compiled, linked, and +-- passed a test that checked the function and the nick. Only the macro was +-- wrong, and nothing in the fork named the macro. +-- +-- ⚠️ The version did NOT move, because the package version is upstream's: the +-- tag was re-cut and the sha256 below is the new one. The store keys on +-- (name, version), so a machine that extracted the first 2.82.5 keeps it — +-- clear that entry if `G_TYPE_UNICODE_TYPE` is still undefined. -- --- Generators, not line count. GLib has six, and `build.mcpp` reimplements all --- of them, so the tree carries no `sh` and no `python`: +-- The lesson generalised: one generator has several independent outputs, and +-- a test that exercises one of them is not evidence about the others. CI now +-- checks the macro, the nick, and enum-vs-flags separately. -- --- gen-visibility-macros.py versions-macros glib/gversionmacros.h --- gen-visibility-macros.py visibility-macros three *-visibility.h --- configure_file glibconfig.h --- configure_file gmodule/gmoduleconf.h --- gobject/glib-mkenums (816 lines of Python) glib-enumtypes.{h,c} --- configure_file config.h +-- ───────────────────────────────────────────────────────────────────────── +-- WHY A FORK -- --- glib-mkenums is reproduced FOR THE ONE INPUT this build points it at — --- `glib/gunicode.h`, four enums — rather than wholesale. It reads upstream's +-- Generators, not line count. GLib has seven; `build.mcpp` reimplements six +-- and the seventh's output is checked in and diffed. See `gnome.glib` for the +-- table and `gnome.gio` for the seventh. +-- +-- glib-mkenums is reproduced for the inputs this build points it at — for +-- gobject that is `glib/gunicode.h`, four enums. It reads upstream's -- `.template` files from the tree so a template change is picked up, and -- exits non-zero if that header stops yielding four enums: a silent drop -- would produce a library missing `g_unicode_script_get_type` and the failure @@ -28,17 +53,6 @@ -- -- https://github.com/mcpplibs/glib -- --- ───────────────────────────────────────────────────────────────────────── --- ⚠️ C++ CONSUMERS MUST WRAP THE INCLUDES --- --- glib's headers carry their own `G_BEGIN_DECLS`, so this is usually fine — --- but the generated `glib-enumtypes.h` comes from a template, and wrapping --- costs nothing: --- --- extern "C" { --- #include --- } --- -- There is no module. glib's API is macro-heavy — `G_DEFINE_TYPE`, -- `G_OBJECT`, `g_signal_connect` are all macros — and macros do not cross a -- module boundary, so an `import` would hand a consumer the declarations and @@ -48,22 +62,22 @@ -- not a house style. -- -- ───────────────────────────────────────────────────────────────────────── --- ⚠️ gio IS NOT IN THIS INDEX, AND THAT BLOCKS pango +-- ❌ THE "gio IS NOT IN THIS INDEX" SECTION THAT USED TO BE HERE WAS WRONG. -- --- Two of gio's six generators are `gdbus-codegen`, an 8,351-line Python --- program that turns D-Bus interface XML into GObject skeletons. Seven gio --- sources include its output and two more reference those, so it cannot be --- dropped without changing what gio is, and reimplementing it in --- `build.mcpp` is not proportionate. +-- It argued that reimplementing `gdbus-codegen` in `build.mcpp` was not +-- proportionate, and concluded that gio must be absent. The premise held; the +-- conclusion did not follow, because the build never needed the GENERATOR — +-- only its OUTPUT, which is a pure function of five XML files. That output is +-- now checked in and CI regenerates and diffs it. -- --- pango uses `GListModel`, which lives in gio, so the text-layout line stops --- here. Measured and recorded rather than left as an unexplained gap. +-- `gnome.gio` is in this index. See gnome.glib for the full note. -- -- ───────────────────────────────────────────────────────────────────────── --- ⚠️ NAME gobject AND gmodule; DO NOT NAME glib +-- ⚠️ NAME WHAT YOU USE; DO NOT NAME glib -- -- [dependencies] -- gnome.gobject = "2.82.5" +-- gnome.gio = "2.82.5" -- gnome.gmodule = "2.82.5" -- # gnome.glib arrives transitively -- @@ -109,15 +123,14 @@ package = { ["2.82.5"] = { url = { GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", - -- ⚠️ The container tag is `2.82.5-1`, not `2.82.5`. The fork's - -- tag was re-cut once while this descriptor was still - -- unpublished — safe only because nothing had extracted it - -- yet — and gitcode refuses to REPLACE an asset of the same - -- name in an existing release. Verified: this URL's sha256 - -- equals the GLOBAL tarball's. - CN = "https://gitcode.com/mcpp-res/glib/releases/download/2.82.5-2/glib-2.82.5.tar.gz", + -- ⚠️ The container tag is `2.82.5-3`, not `2.82.5`. 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/glib/releases/download/2.82.5-3/glib-2.82.5.tar.gz", }, - sha256 = "fb65671207e5ec409a1fdb2156dafb49bf8c772ada252a17844991a14de99ea8", + sha256 = "628b8f98a51238563704bf50817d575a46421acc32d987ca93edc941a1749d62", }, }, }, diff --git a/tests/examples/gio/mcpp.toml b/tests/examples/gio/mcpp.toml new file mode 100644 index 0000000..3c3a37e --- /dev/null +++ b/tests/examples/gio/mcpp.toml @@ -0,0 +1,14 @@ +# gnome.gio — files, streams, D-Bus, and the GListModel pango needs. +# +# ⚠️ NAMES gio ALONE. glib, gobject and gmodule arrive transitively, and naming +# any of them here would be `requested as both a version dep and a path dep`. +[indices] +gnome = { path = "../../.." } + +[package] +name = "gio-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.gnome] +gio = "2.82.5" diff --git a/tests/examples/gio/tests/gio.cpp b/tests/examples/gio/tests/gio.cpp new file mode 100644 index 0000000..d0235cf --- /dev/null +++ b/tests/examples/gio/tests/gio.cpp @@ -0,0 +1,385 @@ +// gnome.gio — exercised rather than merely linked. +// +// WHAT THIS ASSERTS AND WHY +// +// gio is 815 objects assembled from four different kinds of input, and each +// kind fails a different way. A test that only said `#include ` and +// linked would pass with any three of the four missing: +// +// upstream C gio/*.c — a missing one is an undefined reference, +// which the linker DOES catch. Cheapest case. +// VENDORED subtrees xdgmime/, inotify/, subprojects/gvdb/. Absent, gio +// still links: g_content_type_guess returns +// "application/octet-stream", the file monitor falls +// back to polling, and GResource is never touched by +// anything but glib-compile-resources. All three +// degrade SILENTLY, so each is checked by name below. +// CHECKED-IN CODEGEN mcpp/generated/*.c — 15,392 lines of gdbus-codegen +// output that no build step here produces. Checked in +// the FORK's own test, which can name the internal +// symbol; regenerated and diffed by the fork's CI. +// GENERATED HERE gioenumtypes.{h,c}, 82 GTypes from a reimplemented +// glib-mkenums. ⭐ This is the one that already shipped +// a bug once, in gnome.gobject 2.82.5: the macro names +// were wrong (`G_UNICODE_TYPE_TYPE` for `G_TYPE_UNICODE_ +// TYPE`) and it compiled, linked and passed a test that +// checked the FUNCTION and the NICK but never the MACRO. +// So this file checks the macro, the nick, AND whether +// the type is an enum or a flags — three independent +// outputs of the same generator, which is what it takes. +// +// Nothing here needs a session bus, a network, or a mime database. + +#ifdef __linux__ + +// ⚠️ NO extern "C" WRAPPER, and adding one BREAKS THIS UNDER libc++. +// +// gio decorates every header with G_BEGIN_DECLS/G_END_DECLS, which IS +// `extern "C" {`, so a wrapper is redundant. It is also harmful: gio.h reaches +// and , and libc++ routes those through +// /, which define TEMPLATES. Inside an extern "C" block that +// is `templates must have C++ linkage`, dozens of times, against a standard +// header this test never names. libstdc++ does not route them that way, so the +// gcc leg is green and the llvm leg is a wall. +// +// The rule: wrap a C header ONLY if it has no extern "C" of its own. +#include + +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-62s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +// ── GFile: paths, URIs, and the round trip between them ───────────────────── +void test_gfile() +{ + GFile *f = g_file_new_for_path("/etc/hostname"); + char *base = g_file_get_basename(f); + char *uri = g_file_get_uri(f); + check(base && std::strcmp(base, "hostname") == 0, "GFile: basename of /etc/hostname"); + check(uri && std::strcmp(uri, "file:///etc/hostname") == 0, "GFile: to URI"); + + GFile *back = g_file_new_for_uri(uri); + check(g_file_equal(f, back), "GFile: URI round trip compares equal"); + + GFile *parent = g_file_get_parent(f); + char *pp = parent ? g_file_get_path(parent) : nullptr; + check(pp && std::strcmp(pp, "/etc") == 0, "GFile: parent"); + + g_free(pp); + g_free(base); + g_free(uri); + g_clear_object(&parent); + g_clear_object(&back); + g_object_unref(f); +} + +// ── GListModel: the interface pango's PangoFontMap implements ─────────────── +// +// ⭐ THIS IS WHY gio IS HERE. pango was blocked on exactly this symbol, so the +// check is not "gio works" but "the thing pango needs is a live GType". +void test_glistmodel() +{ + check(G_TYPE_IS_INTERFACE(G_TYPE_LIST_MODEL), "GListModel is a registered interface"); + + GListStore *store = g_list_store_new(G_TYPE_FILE); + GFile *a = g_file_new_for_path("/a"); + GFile *b = g_file_new_for_path("/b"); + g_list_store_append(store, a); + g_list_store_append(store, b); + + GListModel *m = G_LIST_MODEL(store); + check(g_list_model_get_n_items(m) == 2, "GListModel: n_items after two appends"); + check(g_list_model_get_item_type(m) == G_TYPE_FILE, "GListModel: item type"); + + GFile *got = static_cast(g_list_model_get_item(m, 1)); + char *p = got ? g_file_get_path(got) : nullptr; + check(p && std::strcmp(p, "/b") == 0, "GListModel: get_item(1) is the second file"); + + g_free(p); + g_clear_object(&got); + g_object_unref(a); + g_object_unref(b); + g_object_unref(store); +} + +// ── gioenumtypes: 82 GTypes from the reimplemented glib-mkenums ───────────── +// +// Three DIFFERENT outputs of the generator, because they fail independently: +// +// the MACRO NAME mkenums derives it from the TYPE name, not the +// enumerator prefix. Getting that backwards is what +// shipped in gobject 2.82.5. A wrong macro is a compile +// error HERE, which is the whole point of naming it. +// the NICK public API — g_flags_get_value_by_nick reads it — and +// gio overrides it seventeen times. Derived, +// G_CONVERTER_NO_FLAGS would be "no-flags"; upstream says +// "none". +// ENUM vs FLAGS decided ONLY by a `/*< flags >*/` comment. gio has +// exactly seven. GMountMountFlags carries it; +// GConverterFlags does NOT, despite the name — so +// upstream registers it as an ENUM, and a scanner that +// guessed from the name would disagree with upstream's ABI. +void test_enumtypes() +{ + check(G_TYPE_IS_ENUM(G_TYPE_FILE_TYPE), "gioenumtypes: G_TYPE_FILE_TYPE is an enum"); + check(G_TYPE_IS_ENUM(G_TYPE_SOCKET_FAMILY), "gioenumtypes: G_TYPE_SOCKET_FAMILY is an enum"); + check(G_TYPE_IS_FLAGS(G_TYPE_MOUNT_MOUNT_FLAGS), + "gioenumtypes: GMountMountFlags has /*< flags >*/ -> FLAGS"); + check(G_TYPE_IS_ENUM(G_TYPE_CONVERTER_FLAGS), + "gioenumtypes: GConverterFlags has NO annotation -> ENUM"); + + GEnumClass *fc = G_ENUM_CLASS(g_type_class_ref(G_TYPE_FILE_TYPE)); + GEnumValue *v = g_enum_get_value(fc, G_FILE_TYPE_DIRECTORY); + check(v && std::strcmp(v->value_nick, "directory") == 0, "gioenumtypes: derived nick"); + check(v && std::strcmp(v->value_name, "G_FILE_TYPE_DIRECTORY") == 0, + "gioenumtypes: value name"); + g_type_class_unref(fc); + + GEnumClass *cc = G_ENUM_CLASS(g_type_class_ref(G_TYPE_CONVERTER_FLAGS)); + GEnumValue *nf = g_enum_get_value(cc, G_CONVERTER_NO_FLAGS); + check(nf && std::strcmp(nf->value_nick, "none") == 0, + "gioenumtypes: /*< nick=none >*/ overrides the derived \"no-flags\""); + g_type_class_unref(cc); + + GFlagsClass *mc = G_FLAGS_CLASS(g_type_class_ref(G_TYPE_MOUNT_MOUNT_FLAGS)); + check(g_flags_get_first_value(mc, G_MOUNT_MOUNT_NONE) != nullptr + || G_MOUNT_MOUNT_NONE == 0, + "gioenumtypes: flags class is usable"); + g_type_class_unref(mc); +} + +// ── GConverter over zlib: compat.zlib is a real dependency, not a note ────── +void test_zlib_converter() +{ + const std::string plain(4096, 'x'); + + GConverter *cz = G_CONVERTER(g_zlib_compressor_new(G_ZLIB_COMPRESSOR_FORMAT_ZLIB, -1)); + GInputStream *src = g_memory_input_stream_new_from_data(plain.data(), + static_cast(plain.size()), + nullptr); + GInputStream *comp = g_converter_input_stream_new(src, cz); + + // Both sides take GZlibCompressorFormat — there is no decompressor enum. + GConverter *dz = G_CONVERTER(g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_ZLIB)); + GInputStream *back = g_converter_input_stream_new(comp, dz); + + char buf[8192]; + gsize got = 0; + GError *err = nullptr; + gboolean ok = g_input_stream_read_all(back, buf, sizeof buf, &got, nullptr, &err); + + check(ok && got == plain.size() && std::memcmp(buf, plain.data(), got) == 0, + "zlib: compress -> decompress reproduces 4096 bytes"); + if (err) { + g_error_free(err); + } + g_object_unref(back); + g_object_unref(dz); + g_object_unref(comp); + g_object_unref(src); + g_object_unref(cz); +} + +// ── GDBus introspection, with no bus ──────────────────────────────────────── +void test_dbus_introspection() +{ + static const char xml[] = + "" + " " + " " + " " + " " + " " + " " + " " + ""; + + GError *err = nullptr; + GDBusNodeInfo *node = g_dbus_node_info_new_for_xml(xml, &err); + check(node != nullptr, "GDBus: introspection XML parses"); + + GDBusInterfaceInfo *iface = node ? g_dbus_node_info_lookup_interface(node, "org.example.Echo") + : nullptr; + GDBusMethodInfo *m = iface ? g_dbus_interface_info_lookup_method(iface, "Speak") : nullptr; + check(m && m->in_args && m->in_args[0] && std::strcmp(m->in_args[0]->signature, "s") == 0, + "GDBus: the parsed method's argument signature"); + + if (node) { + g_dbus_node_info_unref(node); + } + if (err) { + g_error_free(err); + } +} + +// ── gvdb: the reader upstream vendors under subprojects/ ──────────────────── +// +// Without gvdb-reader.c gio does not fail to link — nothing in the library +// calls it except GResource and GSettings' keyfile path, and both are lazy. So +// the check is to hand GResource a blob that is definitely NOT a gvdb file and +// require a CLEAN REFUSAL: a NULL return with GError set. That answer can only +// come from gvdb_table_new_from_bytes, which is the file in question. +void test_gvdb_reader() +{ + static const char junk[] = "this is not a gvdb table, not even slightly"; + GBytes *b = g_bytes_new_static(junk, sizeof junk - 1); + GError *err = nullptr; + GResource *r = g_resource_new_from_data(b, &err); + + check(r == nullptr && err != nullptr, + "gvdb: a non-gvdb blob is refused with a GError, not a crash"); + + if (r) { + g_resource_unref(r); + } + if (err) { + g_error_free(err); + } + g_bytes_unref(b); +} + +// ── xdgmime: the freedesktop mime reader behind g_content_type_* ──────────── +// +// ⚠️ WHAT IS NOT ASSERTED: the answer for a given filename. That depends on +// /usr/share/mime, which a CI container may not have — and a test that demanded +// "text/plain" would be checking the RUNNER's mime database, not this build. +// What IS asserted is the part xdgmime always answers: that the call returns a +// content type at all, and that the two conversion helpers are mutual inverses. +void test_content_type() +{ + char *t = g_content_type_guess("notes.txt", nullptr, 0, nullptr); + check(t != nullptr && *t != '\0', "xdgmime: g_content_type_guess answers"); + + char *mime = g_content_type_get_mime_type("text/plain"); + check(mime && std::strcmp(mime, "text/plain") == 0, "xdgmime: mime type round trip"); + + check(g_content_type_is_a("text/plain", "text/plain") == TRUE, + "xdgmime: a type is a subtype of itself"); + + g_free(mime); + g_free(t); +} + +// ── inotify: the Linux file-monitor backend ───────────────────────────────── +// +// Absent, `g_file_monitor_directory` still succeeds — GLocalFileMonitor falls +// back to a polling implementation and nothing reports the difference. So the +// check reads the TYPE NAME of what came back, which is the only place the two +// are distinguishable. +void test_inotify_backend() +{ + GError *err = nullptr; + GFile *dir = g_file_new_for_path("/tmp"); + GFileMonitor *mon = g_file_monitor_directory(dir, G_FILE_MONITOR_NONE, nullptr, &err); + + check(mon != nullptr, "GFileMonitor: /tmp can be watched"); + if (mon) { + const char *n = G_OBJECT_TYPE_NAME(mon); + std::printf(" (backend: %s)\n", n); + check(n && std::strstr(n, "Inotify") != nullptr, + "inotify/: the backend is GInotifyFileMonitor, not the polling fallback"); + g_file_monitor_cancel(mon); + g_object_unref(mon); + } + if (err) { + g_error_free(err); + } + g_object_unref(dir); +} + +// ── networking types, without touching the network ───────────────────────── +void test_networking() +{ + GInetAddress *a = g_inet_address_new_from_string("127.0.0.1"); + char *s = a ? g_inet_address_to_string(a) : nullptr; + check(s && std::strcmp(s, "127.0.0.1") == 0, "GInetAddress: parse and print"); + check(a && g_inet_address_get_is_loopback(a), "GInetAddress: 127.0.0.1 is loopback"); + + GSocketAddress *sa = a ? g_inet_socket_address_new(a, 8080) : nullptr; + check(sa && g_inet_socket_address_get_port(G_INET_SOCKET_ADDRESS(sa)) == 8080, + "GInetSocketAddress: port"); + + check(g_application_id_is_valid("org.example.App"), "GApplication: a valid id"); + check(!g_application_id_is_valid("nope"), "GApplication: a bare word is not an id"); + + g_clear_object(&sa); + g_free(s); + g_clear_object(&a); +} + +// ── the async machinery: GTask through a GMainContext ────────────────────── +struct AsyncState { + GMainLoop *loop = nullptr; + gssize got = -1; + char buf[64] = {}; +}; + +void on_read(GObject *src, GAsyncResult *res, gpointer user) +{ + AsyncState *st = static_cast(user); + st->got = g_input_stream_read_finish(G_INPUT_STREAM(src), res, nullptr); + g_main_loop_quit(st->loop); +} + +void test_async() +{ + static const char payload[] = "async"; + AsyncState st; + st.loop = g_main_loop_new(nullptr, FALSE); + + GInputStream *in = g_memory_input_stream_new_from_data(payload, sizeof payload - 1, nullptr); + g_input_stream_read_async(in, st.buf, sizeof st.buf, G_PRIORITY_DEFAULT, nullptr, + on_read, &st); + g_main_loop_run(st.loop); + + check(st.got == 5 && std::memcmp(st.buf, payload, 5) == 0, + "GTask: read_async completes through the main loop"); + + g_object_unref(in); + g_main_loop_unref(st.loop); +} + +} // namespace + +int main() +{ + std::printf("gio %d.%d.%d\n\n", glib_major_version, glib_minor_version, glib_micro_version); + + test_gfile(); + test_glistmodel(); + test_enumtypes(); + test_zlib_converter(); + test_dbus_introspection(); + test_gvdb_reader(); + test_content_type(); + test_inotify_backend(); + test_networking(); + test_async(); + + std::printf("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else + +#include + +int main() +{ + std::printf("gio: this package builds the Unix half of gio; skipping.\n"); + return 0; +} + +#endif diff --git a/tests/examples/gobject/tests/gobject.cpp b/tests/examples/gobject/tests/gobject.cpp index bcfcea8..d4a5f70 100644 --- a/tests/examples/gobject/tests/gobject.cpp +++ b/tests/examples/gobject/tests/gobject.cpp @@ -150,6 +150,23 @@ int main() && g_normalize_mode_get_type() != 0, "all four generated enum types register"); + // ⭐ THE MACRO NAMES, WHICH ARE A DIFFERENT OUTPUT OF THE SAME GENERATOR. + // + // Everything above reads the FUNCTION and the NICK, and an earlier 2.82.5 + // tarball got both of those right while defining `G_UNICODE_TYPE_TYPE` + // where upstream defines `G_TYPE_UNICODE_TYPE`. glib-mkenums has two + // prefixes — one from the ENUMERATORS (drives nicks), one from the TYPE + // NAME (drives this macro) — and conflating them changes only the macro. + // + // So it compiled, linked, and passed this very test. The four lines below + // are what it would NOT have passed: naming the macro is a compile error + // if it is spelled wrong, and it is the only spelling upstream has. + check(G_TYPE_UNICODE_TYPE == g_unicode_type_get_type() + && G_TYPE_UNICODE_SCRIPT == g_unicode_script_get_type() + && G_TYPE_UNICODE_BREAK_TYPE == g_unicode_break_type_get_type() + && G_TYPE_NORMALIZE_MODE == g_normalize_mode_get_type(), + "the G_TYPE_* macros are upstream's, not the enumerator prefix"); + // ── 3. a derived type, its property, its signal ────────────────────── const GType thing = test_thing_get_type(); check(thing != 0 && g_type_is_a(thing, G_TYPE_OBJECT), From 6158ada346ac361d94f78afc866c69bf19a96231 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 31 Aug 2026 08:45:37 +0800 Subject: [PATCH 2/3] fix: tests/examples/gio joins [workspace].members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mcpp test -p gio` is how CI runs it, and -p resolves against the root workspace manifest — a new example that is not listed there fails with error: workspace member 'gio' not found in [workspace].members before anything is downloaded, which reads like a package problem and is not. --- mcpp.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/mcpp.toml b/mcpp.toml index 28e540b..61e814b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -107,6 +107,7 @@ members = [ "tests/examples/glib", "tests/examples/gobject", "tests/examples/gmodule", + "tests/examples/gio", "tests/examples/libgbm", "tests/examples/libpng", "tests/examples/libwebp", From 553ca6702321f755db9b0049d4c96562c7afb81d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 31 Aug 2026 09:51:03 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(gnome.*):=20=E5=9B=9B=E4=B8=AA?= =?UTF-8?q?=E6=88=90=E5=91=98=E8=A1=A5=E4=B8=8A=20import=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20namespace=20=E6=98=AF=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 索引里 namespace 决定消费形态:compat.xxx 按头文件消费,归属 namespace 表示包 提供 import。freedesktop.cairo 导出 544 个名字、wlroots.wlroots 986、 freedesktop.libdisplay-info 206;gnome.* 四个是当时唯一的违例 —— 用了归属 namespace 却没有模块。 gnome.glib 2,732 gnome.gobject 495 转出 gnome.glib gnome.gmodule 20 转出 gnome.glib gnome.gio 2,850 转出全部三个 转出不是便利:glib 是 workspace path 依赖,消费者再点名它就会得到 `requested as both a version dep and a path dep`,所以 import gnome.gio; 必须 和 #include 一样完整。先以失败出现的:'GQuark' was not declared。 ⚠️ 两条路不能混用。同一个 TU 两条都走会经由两条路径到达 ,同一个 struct tm 变成两个实体(conflicting declaration 'struct tm')。选哪条由宏决定: 模块带不了宏,glib 的宏占 API 的一半。G_DEFINE_TYPE 那类代码走头文件;函数 API 走模块。每个成员因此各带两个测试,两扇门都被检验。 我原来在描述符里写「没有模块,因为 API 宏化」—— 观察对,结论错。已就地标 ❌。 ⭐ 生成器四次静默漏名,每次产出的包装体都能编译,全部由消费者或另一条工具链 发现,而不是由构建: = '{' 字符字面量里的花括号(G_VARIANT_CLASS_DICT_ENTRY)让 typedef 读取器失衡,吞掉 gvariant.h 剩余全部内容;每个 g_variant_* 消失,而另外 1,853 个名字让它看起来很完整 G_DECLARE_INTERFACE 展开成 typedef 和 _get_type,文本扫描器两个都看不见 —— GListModel、GListStore、g_list_model_get_type,正是 pango 等的那几个 拼写不带 glib/ 前缀,按 umbrella 推导头文件集合看不到, 而 gsize/gssize 正是在那里 typedef 的 void (g_free) (…) glib 把名字加括号防宏展开,声明符位于括号深度 1 还有一次结构性的:注释剥离与 typedef 读取器是两个读者,后者从没见过前者,于是 enum 里的注释变成了导出名。一个流上两个读者就是 bug。 ⭐⭐ 只有两条工具链能看见的一条:导出 enum 的 typedef 会让枚举量在 GCC 上可见, clang 拒绝同一个文件(use of undeclared identifier 'G_MODULE_BIND_LAZY')。 现在每个枚举量按名导出。这是本 fork 里「CI 跑两条工具链」最有力的论据。 tarball 重切:sha256 → 38a175bc…,CN 容器 tag → 2.82.5-4,包版本仍是上游的 2.82.5。 本地:四个示例 × 两条路线 × 两条工具链全绿;fork CI 四个 job 全绿。 --- ...26-08-30-graphics-stack-coverage-design.md | 105 ++++++++++++++++- pkgs/g/gnome.gio.lua | 48 +++++++- pkgs/g/gnome.glib.lua | 39 ++++++- pkgs/g/gnome.gmodule.lua | 58 ++++++++-- pkgs/g/gnome.gobject.lua | 58 ++++++++-- tests/examples/gio/tests/module.cpp | 102 +++++++++++++++++ tests/examples/glib/tests/module.cpp | 107 ++++++++++++++++++ tests/examples/gmodule/tests/module.cpp | 69 +++++++++++ tests/examples/gobject/tests/module.cpp | 85 ++++++++++++++ 9 files changed, 641 insertions(+), 30 deletions(-) create mode 100644 tests/examples/gio/tests/module.cpp create mode 100644 tests/examples/glib/tests/module.cpp create mode 100644 tests/examples/gmodule/tests/module.cpp create mode 100644 tests/examples/gobject/tests/module.cpp 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 d36c44a..bffe4e5 100644 --- a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -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.7,§11 总账 / §14 fork 规范 / §18 八角度复核 / §19 合成器闸门与 GObject 栈,含沙箱实测 · §20 gio 与一次被推翻的判断)** +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.8,§11 总账 / §14 fork 规范 / §18 八角度复核 / §19 合成器闸门与 GObject 栈,含沙箱实测 · §20 gio 与一次被推翻的判断 · §21 namespace 是契约:四个成员补上 import)** ## 0. 这份文档解决什么 @@ -1944,3 +1944,106 @@ nick 是公开 API(`g_flags_get_value_by_nick` 读它):`G_CONVERTER_NO_FLAGS` **pango 的闸门开了**:它的五个依赖(gio、gobject、harfbuzz、fribidi、 fontconfig、cairo)全部在索引里。 + +--- + +## 21. namespace 是契约:gnome.* 补上 import(2026-08-31) + +### 21.1 判据不是我原来以为的那个 + +索引里 **namespace 决定消费形态**: + +| | | +|---|---| +| `compat.xxx` | 按**头文件**消费,包不提供模块 | +| `<归属方>.xxx` | 包**提供 `import`** | + +全索引核对成立:`freedesktop.cairo` 导出 544 个名字、`wlroots.wlroots` 986、 +`freedesktop.libdisplay-info` 206、`freedesktop.wayland` 有 +`import freedesktop.wayland.client;`、`freedesktop.fontconfig` 有 +`src/fontconfig.cppm`;而 `compat.harfbuzz`/`freetype`/`pcre2`/`fribidi` 都只有 +头文件。**`gnome.*` 四个是当时唯一的违例** —— 用了归属 namespace 却没有模块。 + +我原来在描述符里写的是「没有模块,因为 API 宏化」。**观察对,结论错**:宏确实 +过不了模块边界,但那不代表模块无意义 —— 它仍然交付 2,732 个声明,而 gio 的函数 +API 根本不需要宏。已就地标 ❌ 并改正。 + +### 21.2 两条路不能混用(这一条决定了形态) + +``` +import gnome.gio; // 模块路线 +#include // 头文件路线 +``` + +同一个 TU 两条都走,会经由**两条路径**到达 `` —— 一次через模块的 global +fragment,一次直接 —— 于是**同一个文件里的同一个 `struct tm` 变成两个实体**: + +``` +error: conflicting declaration 'struct tm' +note: previous declaration as 'struct tm' (of module gnome.glib) +``` + +所以消费者**二选一**,而选哪条由**宏**决定:模块带不了宏,glib 的宏占 API 的一半 +(glib 1,337 `#define` 对 1,312 声明;gio 1,679 对 1,753)。用 +`G_DEFINE_TYPE`/`G_OBJECT`/`G_TYPE_*` 的代码走头文件;用函数 API 的(gio 的大 +多数)走模块,一个头都不用包。**每个成员因此各带两个测试**。 + +### 21.3 ❌ 两种更省事的形态,都实测否掉了 + +| 形态 | 为什么不行 | +|---|---| +| 宏侧头文件(抽出全部 1,928 个 `#define`,自身不 include 任何东西) | 小范围可行 —— 探针编译、链接、运行都过。**不 scale**:glib 的版本/弃用机制是**预处理器有状态**的,顺序由**include 图**决定而不是文件列表。摊平后依次得到「`#ifdef` 两个分支都被发出、后者静默重定义前者」→「`#error "GLIB_VERSION_MIN_REQUIRED must be <= GLIB_VERSION_CUR_STABLE"`」→「`missing binary operator before token 'GLIB_DEPRECATED_MACRO'`」。**每修一个就冒出下一个,因为摊平本身才是错的**;忠实投影等于写半个预处理器 | +| `export import ;`(具名模块转出 header unit) | 声明**确实**全部带过去了(实测),但导入方**拿不到宏**。所以导出清单无论如何都得列 | + +### 21.4 ⭐ 生成器四次「静默漏名」—— 全部由消费者或另一条工具链发现 + +每一次产出的包装体**都能编译**: + +| | 发生了什么 | +|---|---| +| `= '{'` | `G_VARIANT_CLASS_DICT_ENTRY = '{'` 是**字符字面量里的花括号**。typedef 读取器把它计入,再也没配平,于是**吞掉了 `gvariant.h` 剩下的全部内容** —— 每个 `g_variant_*` 函数消失,而另外 1,853 个名字让模块看起来很完整 | +| `G_DECLARE_INTERFACE` | 展开成 typedef **和** `_get_type`,文本扫描器两个都看不见。牺牲者是 `GListModel`、`GListStore`、`g_list_model_get_type` —— **正是 pango 等的那几个** | +| `` | 拼写不带 `glib/` 前缀,所以按 umbrella 的 `` 行推导头文件集合看不到它 —— 而 `gsize`/`gssize` 正是在那里 typedef 的 | +| `void (g_free) (…)` | glib 把名字**加括号**以防宏展开,声明符因此位于括号深度 1;深度 0 规则丢掉了 `g_free`、`g_string_free` 和 GVariant 的构造函数 | + +还有一次结构性的:**注释剥离与 typedef 读取器是两个读者**,后者有自己的 +`getline` 循环、从没见过前者,于是 `typedef enum { … } GUnicodeScript;` 里的 +脚本代码注释(`/* Geor */`)变成了导出名。**一个流上两个读者就是 bug**,改成 +「先整文件剥一次注释」才是修法 —— 和 §20.6 的排除模式、以及 gio 扫描器那次 +是同一类。 + +### 21.5 ⭐⭐ 只有两条工具链能看见的一条 + +生成器一度只导出 enum 的 typedef,依据是一次 mcpp 端到端实跑: +`GModuleFlags f = G_MODULE_BIND_LAZY;` 编译并运行了。 + +**它是在 GCC 上编译的。** clang 拒绝同一个文件: + +``` +error: use of undeclared identifier 'G_MODULE_BIND_LAZY' +error: use of undeclared identifier 'G_ZLIB_COMPRESSOR_FORMAT_ZLIB' +error: use of undeclared identifier 'G_UNICODE_OTHER_LETTER' +``` + +—— 因为 using 声明命名一个枚举**不引入它的枚举量**,GCC 的可达性是宽松解读。 +现在每个枚举量都按名导出。 + +> **这是本 fork 里「CI 跑两条工具链」最有力的一条论据**:单条工具链会把这个 +> 判成 done。 + +### 21.6 结果 + +| 模块 | 导出 | +|---|---| +| `gnome.glib` | 2,732 | +| `gnome.gobject` | 495 · 转出 `gnome.glib` | +| `gnome.gmodule` | 20 · 转出 `gnome.glib` | +| `gnome.gio` | 2,850 · 转出全部三个 | + +转出**不是便利**:glib 是 workspace **path** 依赖,消费者再点名它就会得到 +`requested as both a version dep and a path dep`。所以 `import gnome.gio;` +必须和 `#include ` 一样完整 —— 这一条也是先以失败出现的 +(`'GQuark' was not declared in this scope`,消费者什么都没做错)。 + +CI 加了导出数下限(glib ≥1900、gio ≥2400)、pango 那三个符号按名断言、一个 +枚举量、以及转出链。**塌陷是静默的,所以必须有东西去数。** diff --git a/pkgs/g/gnome.gio.lua b/pkgs/g/gnome.gio.lua index bc493b1..a40e631 100644 --- a/pkgs/g/gnome.gio.lua +++ b/pkgs/g/gnome.gio.lua @@ -129,9 +129,10 @@ -- consumer never named. libstdc++ does not route them that way, so gcc is -- green and clang is a wall. -- --- There is no module, for the same reason as the rest of the family: the API --- is macro-heavy (`G_DEFINE_TYPE`, `G_IS_FILE`, `g_signal_connect`) and macros --- do not cross a module boundary. +-- ❌ "THERE IS NO MODULE" USED TO BE HERE, AND IT WAS THE WRONG CONCLUSION. +-- The API being macro-heavy is true and does not make a module pointless: this +-- one hands a consumer 2,850 declarations, and gio's function API — GFile, +-- GSocket, GVariant, GListModel — needs no macros at all. See above. -- -- ───────────────────────────────────────────────────────────────────────── -- LINUX ONLY @@ -141,6 +142,41 @@ -- the volume monitor, the app-info backend and the settings backend. The -- generated `glibconfig.h` fixes `G_OS_UNIX` besides, and the upstream tree -- carries two `COPYING` symlinks a Windows extraction would not survive. +-- ───────────────────────────────────────────────────────────────────────── +-- ⭐ TWO WAYS TO CONSUME IT, AND YOU PICK ONE +-- +-- import gnome.gio; -- the module route +-- #include -- the header route +-- +-- The namespace is the contract in this index: `compat.xxx` means headers, an +-- owner namespace means the package exposes `import`. This module exports +-- 2,850 names. It re-exports all three siblings, because +-- `gio.h` reaches all three and because a consumer CANNOT name them itself — +-- they are workspace path dependencies. +-- +-- ⚠️ THE TWO ROUTES DO NOT COMPOSE. A TU that imports the module AND textually +-- includes a glib header reaches twice — once through the module's +-- global fragment, once directly — and the same `struct tm` from the same file +-- becomes two entities: +-- +-- error: conflicting declaration 'struct tm' +-- note: previous declaration as 'struct tm' (of module gnome.gio) +-- +-- WHICH ROUTE IS DECIDED BY MACROS. A module cannot carry them, and glib's are +-- half its API — 1,337 `#define` against 1,312 declarations for glib, 1,679 +-- against 1,753 for gio. `G_DEFINE_TYPE`, `G_OBJECT`, `g_signal_connect` and +-- every `G_TYPE_*` are macros, so code that defines a GObject subclass takes +-- the HEADER route. Code that uses the function API — most of gio — takes the +-- MODULE route and includes nothing at all. +-- +-- The wrapper is GENERATED from upstream's public headers, so a name upstream +-- adds or removes cannot be silently missed. That is not a preference at this +-- size: four separate silent misses were found by consumers rather than by the +-- build — a brace inside a char literal that swallowed the rest of a file, +-- `G_DECLARE_INTERFACE` (which expands to the names rather than spelling +-- them), `` being spelled without a `glib/` prefix, and glib's +-- habit of parenthesising a name to defend it from macro expansion. +-- package = { spec = "1", namespace = "gnome", @@ -155,14 +191,14 @@ package = { ["2.82.5"] = { url = { GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", - -- ⚠️ The container tag is `2.82.5-3`, not `2.82.5`. gitcode + -- ⚠️ The container tag is `2.82.5-4`, not `2.82.5`. 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/glib/releases/download/2.82.5-3/glib-2.82.5.tar.gz", + CN = "https://gitcode.com/mcpp-res/glib/releases/download/2.82.5-4/glib-2.82.5.tar.gz", }, - sha256 = "628b8f98a51238563704bf50817d575a46421acc32d987ca93edc941a1749d62", + sha256 = "38a175bcd8899f376b6540e2e3ef7450169205fc56bdb909cfb086a0438553e5", }, }, }, diff --git a/pkgs/g/gnome.glib.lua b/pkgs/g/gnome.glib.lua index 0165648..fc2c804 100644 --- a/pkgs/g/gnome.glib.lua +++ b/pkgs/g/gnome.glib.lua @@ -111,6 +111,39 @@ -- upstream tree also contains two `COPYING` symlinks, which a Windows -- extraction would not survive — one more reason the descriptor offers -- `linux` alone. +-- ───────────────────────────────────────────────────────────────────────── +-- ⭐ TWO WAYS TO CONSUME IT, AND YOU PICK ONE +-- +-- import gnome.glib; -- the module route +-- #include -- the header route +-- +-- The namespace is the contract in this index: `compat.xxx` means headers, an +-- owner namespace means the package exposes `import`. This module exports +-- 2,732 names. +-- +-- ⚠️ THE TWO ROUTES DO NOT COMPOSE. A TU that imports the module AND textually +-- includes a glib header reaches twice — once through the module's +-- global fragment, once directly — and the same `struct tm` from the same file +-- becomes two entities: +-- +-- error: conflicting declaration 'struct tm' +-- note: previous declaration as 'struct tm' (of module gnome.glib) +-- +-- WHICH ROUTE IS DECIDED BY MACROS. A module cannot carry them, and glib's are +-- half its API — 1,337 `#define` against 1,312 declarations for glib, 1,679 +-- against 1,753 for gio. `G_DEFINE_TYPE`, `G_OBJECT`, `g_signal_connect` and +-- every `G_TYPE_*` are macros, so code that defines a GObject subclass takes +-- the HEADER route. Code that uses the function API — most of gio — takes the +-- MODULE route and includes nothing at all. +-- +-- The wrapper is GENERATED from upstream's public headers, so a name upstream +-- adds or removes cannot be silently missed. That is not a preference at this +-- size: four separate silent misses were found by consumers rather than by the +-- build — a brace inside a char literal that swallowed the rest of a file, +-- `G_DECLARE_INTERFACE` (which expands to the names rather than spelling +-- them), `` being spelled without a `glib/` prefix, and glib's +-- habit of parenthesising a name to defend it from macro expansion. +-- package = { spec = "1", namespace = "gnome", @@ -125,14 +158,14 @@ package = { ["2.82.5"] = { url = { GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", - -- ⚠️ The container tag is `2.82.5-3`, not `2.82.5`. gitcode + -- ⚠️ The container tag is `2.82.5-4`, not `2.82.5`. 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/glib/releases/download/2.82.5-3/glib-2.82.5.tar.gz", + CN = "https://gitcode.com/mcpp-res/glib/releases/download/2.82.5-4/glib-2.82.5.tar.gz", }, - sha256 = "628b8f98a51238563704bf50817d575a46421acc32d987ca93edc941a1749d62", + sha256 = "38a175bcd8899f376b6540e2e3ef7450169205fc56bdb909cfb086a0438553e5", }, }, }, diff --git a/pkgs/g/gnome.gmodule.lua b/pkgs/g/gnome.gmodule.lua index a3cbb81..9ff185c 100644 --- a/pkgs/g/gnome.gmodule.lua +++ b/pkgs/g/gnome.gmodule.lua @@ -39,13 +39,16 @@ -- #include -- } -- --- There is no module. glib's API is macro-heavy — `G_DEFINE_TYPE`, --- `G_OBJECT`, `g_signal_connect` are all macros — and macros do not cross a --- module boundary, so an `import` would hand a consumer the declarations and --- withhold the half of the API that makes them usable. Compare --- `wlroots.wlroots`, where the module is the ONLY way in because the headers --- are not valid C++ at all: the shape follows what upstream's headers are, --- not a house style. +-- ❌ "THERE IS NO MODULE" USED TO BE HERE, AND IT WAS THE WRONG CONCLUSION. +-- +-- The observation was right: glib's API IS macro-heavy, and macros do not +-- cross a module boundary. What did not follow is that a module is therefore +-- pointless. It hands a consumer 2,732 declarations, which is everything +-- except the macros — and the function API is most of what gio is used for. +-- +-- It was also the wrong shape for this index, where the NAMESPACE is the +-- contract: `gnome.*` promises `import` the way `freedesktop.cairo` and +-- `wlroots.wlroots` do. See the module section above. -- -- ───────────────────────────────────────────────────────────────────────── -- ❌ THE "gio IS NOT IN THIS INDEX" SECTION THAT USED TO BE HERE WAS WRONG. @@ -95,6 +98,41 @@ -- upstream tree also contains two `COPYING` symlinks, which a Windows -- extraction would not survive — one more reason the descriptor offers -- `linux` alone. +-- ───────────────────────────────────────────────────────────────────────── +-- ⭐ TWO WAYS TO CONSUME IT, AND YOU PICK ONE +-- +-- import gnome.gmodule; -- the module route +-- #include -- the header route +-- +-- The namespace is the contract in this index: `compat.xxx` means headers, an +-- owner namespace means the package exposes `import`. This module exports +-- 20 names. It re-exports `gnome.glib`, because +-- `gmodule.h` includes `glib.h` and because a consumer CANNOT name +-- `gnome.glib` itself — it is a workspace path dependency. +-- +-- ⚠️ THE TWO ROUTES DO NOT COMPOSE. A TU that imports the module AND textually +-- includes a glib header reaches twice — once through the module's +-- global fragment, once directly — and the same `struct tm` from the same file +-- becomes two entities: +-- +-- error: conflicting declaration 'struct tm' +-- note: previous declaration as 'struct tm' (of module gnome.gmodule) +-- +-- WHICH ROUTE IS DECIDED BY MACROS. A module cannot carry them, and glib's are +-- half its API — 1,337 `#define` against 1,312 declarations for glib, 1,679 +-- against 1,753 for gio. `G_DEFINE_TYPE`, `G_OBJECT`, `g_signal_connect` and +-- every `G_TYPE_*` are macros, so code that defines a GObject subclass takes +-- the HEADER route. Code that uses the function API — most of gio — takes the +-- MODULE route and includes nothing at all. +-- +-- The wrapper is GENERATED from upstream's public headers, so a name upstream +-- adds or removes cannot be silently missed. That is not a preference at this +-- size: four separate silent misses were found by consumers rather than by the +-- build — a brace inside a char literal that swallowed the rest of a file, +-- `G_DECLARE_INTERFACE` (which expands to the names rather than spelling +-- them), `` being spelled without a `glib/` prefix, and glib's +-- habit of parenthesising a name to defend it from macro expansion. +-- package = { spec = "1", namespace = "gnome", @@ -109,14 +147,14 @@ package = { ["2.82.5"] = { url = { GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", - -- ⚠️ The container tag is `2.82.5-3`, not `2.82.5`. gitcode + -- ⚠️ The container tag is `2.82.5-4`, not `2.82.5`. 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/glib/releases/download/2.82.5-3/glib-2.82.5.tar.gz", + CN = "https://gitcode.com/mcpp-res/glib/releases/download/2.82.5-4/glib-2.82.5.tar.gz", }, - sha256 = "628b8f98a51238563704bf50817d575a46421acc32d987ca93edc941a1749d62", + sha256 = "38a175bcd8899f376b6540e2e3ef7450169205fc56bdb909cfb086a0438553e5", }, }, }, diff --git a/pkgs/g/gnome.gobject.lua b/pkgs/g/gnome.gobject.lua index 63e9189..8fcf0ca 100644 --- a/pkgs/g/gnome.gobject.lua +++ b/pkgs/g/gnome.gobject.lua @@ -53,13 +53,16 @@ -- -- https://github.com/mcpplibs/glib -- --- There is no module. glib's API is macro-heavy — `G_DEFINE_TYPE`, --- `G_OBJECT`, `g_signal_connect` are all macros — and macros do not cross a --- module boundary, so an `import` would hand a consumer the declarations and --- withhold the half of the API that makes them usable. Compare --- `wlroots.wlroots`, where the module is the ONLY way in because the headers --- are not valid C++ at all: the shape follows what upstream's headers are, --- not a house style. +-- ❌ "THERE IS NO MODULE" USED TO BE HERE, AND IT WAS THE WRONG CONCLUSION. +-- +-- The observation was right: glib's API IS macro-heavy, and macros do not +-- cross a module boundary. What did not follow is that a module is therefore +-- pointless. It hands a consumer 2,732 declarations, which is everything +-- except the macros — and the function API is most of what gio is used for. +-- +-- It was also the wrong shape for this index, where the NAMESPACE is the +-- contract: `gnome.*` promises `import` the way `freedesktop.cairo` and +-- `wlroots.wlroots` do. See the module section above. -- -- ───────────────────────────────────────────────────────────────────────── -- ❌ THE "gio IS NOT IN THIS INDEX" SECTION THAT USED TO BE HERE WAS WRONG. @@ -109,6 +112,41 @@ -- upstream tree also contains two `COPYING` symlinks, which a Windows -- extraction would not survive — one more reason the descriptor offers -- `linux` alone. +-- ───────────────────────────────────────────────────────────────────────── +-- ⭐ TWO WAYS TO CONSUME IT, AND YOU PICK ONE +-- +-- import gnome.gobject; -- the module route +-- #include -- the header route +-- +-- The namespace is the contract in this index: `compat.xxx` means headers, an +-- owner namespace means the package exposes `import`. This module exports +-- 495 names. It re-exports `gnome.glib`, because +-- `glib-object.h` includes `glib.h` and because a consumer CANNOT name +-- `gnome.glib` itself — it is a workspace path dependency. +-- +-- ⚠️ THE TWO ROUTES DO NOT COMPOSE. A TU that imports the module AND textually +-- includes a glib header reaches twice — once through the module's +-- global fragment, once directly — and the same `struct tm` from the same file +-- becomes two entities: +-- +-- error: conflicting declaration 'struct tm' +-- note: previous declaration as 'struct tm' (of module gnome.gobject) +-- +-- WHICH ROUTE IS DECIDED BY MACROS. A module cannot carry them, and glib's are +-- half its API — 1,337 `#define` against 1,312 declarations for glib, 1,679 +-- against 1,753 for gio. `G_DEFINE_TYPE`, `G_OBJECT`, `g_signal_connect` and +-- every `G_TYPE_*` are macros, so code that defines a GObject subclass takes +-- the HEADER route. Code that uses the function API — most of gio — takes the +-- MODULE route and includes nothing at all. +-- +-- The wrapper is GENERATED from upstream's public headers, so a name upstream +-- adds or removes cannot be silently missed. That is not a preference at this +-- size: four separate silent misses were found by consumers rather than by the +-- build — a brace inside a char literal that swallowed the rest of a file, +-- `G_DECLARE_INTERFACE` (which expands to the names rather than spelling +-- them), `` being spelled without a `glib/` prefix, and glib's +-- habit of parenthesising a name to defend it from macro expansion. +-- package = { spec = "1", namespace = "gnome", @@ -123,14 +161,14 @@ package = { ["2.82.5"] = { url = { GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", - -- ⚠️ The container tag is `2.82.5-3`, not `2.82.5`. gitcode + -- ⚠️ The container tag is `2.82.5-4`, not `2.82.5`. 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/glib/releases/download/2.82.5-3/glib-2.82.5.tar.gz", + CN = "https://gitcode.com/mcpp-res/glib/releases/download/2.82.5-4/glib-2.82.5.tar.gz", }, - sha256 = "628b8f98a51238563704bf50817d575a46421acc32d987ca93edc941a1749d62", + sha256 = "38a175bcd8899f376b6540e2e3ef7450169205fc56bdb909cfb086a0438553e5", }, }, }, diff --git a/tests/examples/gio/tests/module.cpp b/tests/examples/gio/tests/module.cpp new file mode 100644 index 0000000..02f384a --- /dev/null +++ b/tests/examples/gio/tests/module.cpp @@ -0,0 +1,102 @@ +// ⭐ THE MODULE, WHICH IS WHAT THE NAMESPACE PROMISES. +// +// In this index the namespace is the contract: `compat.xxx` is consumed with +// `#include`, an owner namespace like `gnome.xxx` exposes `import`. The test +// next to this file consumes gnome.gio through its headers; this one consumes +// the same package through its module, so both doors are checked from the +// OUTSIDE — which is what an index example is for. +// +// ⚠️ THE TWO DOORS DO NOT COMPOSE, and that is GCC's rule rather than a choice +// here. A TU that imports the module AND textually includes a glib header +// reaches twice — once through the module's global fragment, once +// directly — and the same `struct tm` from the same file becomes two entities: +// +// error: conflicting declaration 'struct tm' +// note: previous declaration as 'struct tm' (of module gnome.glib) +// +// So a consumer picks ONE route, and which one is decided by macros: a module +// cannot carry them, and glib's are half its API. Code using `G_DEFINE_TYPE`, +// `G_OBJECT` or `G_TYPE_*` takes the header route. Code using the FUNCTION API +// — most of gio, and much of glib — imports and includes nothing. +#ifdef __linux__ + +import gnome.gio; // re-exports gnome.glib, gnome.gobject, gnome.gmodule + +#include +#include + +int main() +{ + int failures = 0; + auto check = [&](bool ok, const char *what) { + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; + }; + + std::printf("import gnome.gio\n\n"); + + // ⭐ THE SYMBOL pango WAS BLOCKED ON, through the module route. + // G_TYPE_LIST_MODEL is a macro; g_list_model_get_type() is what it wraps. + const GType lm = g_list_model_get_type(); + check(lm != 0, "g_list_model_get_type() — the interface is registered"); + check(std::strcmp(g_type_name(lm), "GListModel") == 0, "…and it is GListModel"); + + // GFile: gio's function API needs no macros at all, which is the case the + // module route is actually for. + GFile *f = g_file_new_for_path("/etc/hostname"); + char *base = g_file_get_basename(f); + char *uri = g_file_get_uri(f); + check(base && std::strcmp(base, "hostname") == 0, "GFile: basename"); + check(uri && std::strcmp(uri, "file:///etc/hostname") == 0, "GFile: to URI"); + g_free(base); + g_free(uri); + + // A GListStore holding GFiles — g_file_get_type() rather than G_TYPE_FILE. + GListStore *store = g_list_store_new(g_file_get_type()); + g_list_store_append(store, f); + check(g_list_model_get_n_items(reinterpret_cast(store)) == 1, + "GListModel: one item after append"); + g_object_unref(store); + g_object_unref(f); + + // Streams, and the zlib converter — compat.zlib is a real dependency. + const char plain[] = "the module route needs no macros"; + GInputStream *src = g_memory_input_stream_new_from_data(plain, sizeof plain - 1, nullptr); + GConverter *cz = reinterpret_cast( + g_zlib_compressor_new(G_ZLIB_COMPRESSOR_FORMAT_ZLIB, -1)); + GInputStream *comp = g_converter_input_stream_new(src, cz); + GConverter *dz = reinterpret_cast( + g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_ZLIB)); + GInputStream *back = g_converter_input_stream_new(comp, dz); + char buf[128] = {}; + gsize got = 0; + g_input_stream_read_all(back, buf, sizeof buf, &got, nullptr, nullptr); + check(got == sizeof plain - 1 && std::memcmp(buf, plain, got) == 0, + "zlib: compress -> decompress round trip"); + g_object_unref(back); g_object_unref(dz); + g_object_unref(comp); g_object_unref(src); g_object_unref(cz); + + // D-Bus introspection, with no bus. + static const char xml[] = + "" + "" + ""; + GDBusNodeInfo *node = g_dbus_node_info_new_for_xml(xml, nullptr); + check(node != nullptr, "GDBus: introspection XML parses"); + if (node) g_dbus_node_info_unref(node); + + // The three siblings arrived through gnome.gio, matching gio.h's includes. + char *s = g_strdup("glib"); // gnome.glib + check(s && std::strcmp(s, "glib") == 0, "g_strdup — gnome.glib re-exported"); + g_free(s); + check(g_object_get_type() != 0, "g_object_get_type — gnome.gobject re-exported"); + check(g_module_supported(), "g_module_supported — gnome.gmodule re-exported"); + + std::printf("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else +#include +int main() { std::printf("linux only\n"); return 0; } +#endif diff --git a/tests/examples/glib/tests/module.cpp b/tests/examples/glib/tests/module.cpp new file mode 100644 index 0000000..a5c2180 --- /dev/null +++ b/tests/examples/glib/tests/module.cpp @@ -0,0 +1,107 @@ +// ⭐ THE MODULE, WHICH IS WHAT THE NAMESPACE PROMISES. +// +// In this index the namespace is the contract: `compat.xxx` is consumed with +// `#include`, an owner namespace like `gnome.xxx` exposes `import`. glib.cpp +// next to this file takes the header route; this one takes the module, so both +// doors are tested rather than assumed. +// +// ───────────────────────────────────────────────────────────────────────── +// ⚠️ THE TWO DOORS DO NOT COMPOSE, AND THAT IS GCC'S RULE, NOT A CHOICE HERE. +// +// A TU that imports gnome.glib AND textually includes a glib header reaches +// `` twice — once through the module's global fragment, once directly +// — and the same `struct tm` from the same file becomes two entities: +// +// error: conflicting declaration 'struct tm' +// note: previous declaration as 'struct tm' (of module gnome.glib) +// +// repeated for every type in it. So a consumer picks ONE route. +// +// WHAT THAT MEANS IN PRACTICE, stated plainly because it decides which route +// to pick: a module cannot carry macros, and glib's macros are half its API +// (1,337 `#define` against 1,312 declarations; for gio 1,679 against 1,753). +// Code that uses `G_DEFINE_TYPE`, `G_OBJECT` or `G_TYPE_*` takes the header +// route. Code that uses the FUNCTION API — most of gio, and a great deal of +// glib — can import and include nothing at all, which is what this file does. +#ifdef __linux__ + +import gnome.glib; + +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +} // namespace + +int main() +{ + std::printf("import gnome.glib — %d.%d.%d\n\n", + glib_major_version, glib_minor_version, glib_micro_version); + check(glib_major_version == 2 && glib_minor_version == 82, + "the version variables come through the module"); + + // ── strings: GString and the g_str* family ─────────────────────────── + GString *s = g_string_new("import"); + g_string_append(s, " works"); + check(std::strcmp(s->str, "import works") == 0, "GString: new + append"); + g_string_free(s, 1); // TRUE is a MACRO — not reachable through a module + + char *up = g_ascii_strup("mcpp", -1); + check(up && std::strcmp(up, "MCPP") == 0, "g_ascii_strup"); + g_free(up); + + // ── containers ─────────────────────────────────────────────────────── + GPtrArray *a = g_ptr_array_new(); + g_ptr_array_add(a, const_cast("one")); + g_ptr_array_add(a, const_cast("two")); + check(a->len == 2, "GPtrArray: two elements"); + g_ptr_array_free(a, 1); + + GHashTable *h = g_hash_table_new(g_str_hash, g_str_equal); + g_hash_table_insert(h, const_cast("k"), const_cast("v")); + const char *got = static_cast(g_hash_table_lookup(h, "k")); + check(got && std::strcmp(got, "v") == 0, "GHashTable: insert and look up"); + g_hash_table_destroy(h); + + // ── an ENUMERATOR, reached through its typedef ─────────────────────── + // glib writes `typedef enum { … } GNormalizeMode;` — an UNNAMED enum + // behind a typedef — and exporting the typedef is what makes the + // enumerators visible. Nothing in the module names them individually. + GNormalizeMode nfd = G_NORMALIZE_NFD, nfc = G_NORMALIZE_NFC; + check(nfd != nfc, "an enumerator arrives with its typedef"); + + // ── Unicode, which is where glib's own tables live ─────────────────── + check(g_unichar_isalpha(0x4E2D), "g_unichar_isalpha on U+4E2D"); + check(g_unichar_type(0x4E2D) == G_UNICODE_OTHER_LETTER, "g_unichar_type"); + + // ── GVariant: parse and read back ──────────────────────────────────── + GVariant *v = g_variant_new_int32(42); + check(g_variant_get_int32(v) == 42, "GVariant: int32 round trip"); + g_variant_unref(v); + + // ── the main loop ──────────────────────────────────────────────────── + GMainContext *ctx = g_main_context_new(); + check(ctx != nullptr, "GMainContext: created"); + g_main_context_unref(ctx); + + std::printf("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else + +#include +int main() { std::printf("linux only\n"); return 0; } + +#endif diff --git a/tests/examples/gmodule/tests/module.cpp b/tests/examples/gmodule/tests/module.cpp new file mode 100644 index 0000000..253f13d --- /dev/null +++ b/tests/examples/gmodule/tests/module.cpp @@ -0,0 +1,69 @@ +// ⭐ THE MODULE, WHICH IS WHAT THE NAMESPACE PROMISES. +// +// In this index the namespace is the contract: `compat.xxx` is consumed with +// `#include`, an owner namespace like `gnome.xxx` exposes `import`. The test +// next to this file consumes gnome.gmodule through its headers; this one consumes +// the same package through its module, so both doors are checked from the +// OUTSIDE — which is what an index example is for. +// +// ⚠️ THE TWO DOORS DO NOT COMPOSE, and that is GCC's rule rather than a choice +// here. A TU that imports the module AND textually includes a glib header +// reaches twice — once through the module's global fragment, once +// directly — and the same `struct tm` from the same file becomes two entities: +// +// error: conflicting declaration 'struct tm' +// note: previous declaration as 'struct tm' (of module gnome.glib) +// +// So a consumer picks ONE route, and which one is decided by macros: a module +// cannot carry them, and glib's are half its API. Code using `G_DEFINE_TYPE`, +// `G_OBJECT` or `G_TYPE_*` takes the header route. Code using the FUNCTION API +// — most of gio, and much of glib — imports and includes nothing. +#ifdef __linux__ + +import gnome.gmodule; // re-exports gnome.glib + +#include +#include + +int main() +{ + int failures = 0; + auto check = [&](bool ok, const char *what) { + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; + }; + + std::printf("import gnome.gmodule\n\n"); + + check(g_module_supported(), "g_module_supported() through the module"); + + // An enumerator, reached through its typedef — glib writes + // `typedef enum { … } GModuleFlags;` and exporting the typedef is what + // makes G_MODULE_BIND_LAZY visible. Nothing names it individually. + GModuleFlags f = G_MODULE_BIND_LAZY; + check(static_cast(f) == 1, "an enumerator arrives with its typedef"); + + GModule *m = g_module_open("/nonexistent/definitely-not-a-module.so", f); + check(m == nullptr, "opening a missing module fails"); + const char *err = g_module_error(); + check(err != nullptr && std::strstr(err, "definitely-not-a-module") != nullptr, + "…and g_module_error names the file — the dl loader is wired up"); + + // ⭐ gnome.glib ARRIVES THROUGH gnome.gmodule, and it has to: glib is a + // workspace PATH dependency, so a consumer that named it as well would get + // error: dependency 'gnome.glib' is requested as both a version dep + // and a path dep + // gmodule.h includes glib.h, so the module re-exports gnome.glib to match. + char *p = g_strdup("re-exported"); + check(p != nullptr && std::strcmp(p, "re-exported") == 0, + "g_strdup — gnome.glib came through gnome.gmodule"); + g_free(p); + + std::printf("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else +#include +int main() { std::printf("linux only\n"); return 0; } +#endif diff --git a/tests/examples/gobject/tests/module.cpp b/tests/examples/gobject/tests/module.cpp new file mode 100644 index 0000000..5ad7aa3 --- /dev/null +++ b/tests/examples/gobject/tests/module.cpp @@ -0,0 +1,85 @@ +// ⭐ THE MODULE, WHICH IS WHAT THE NAMESPACE PROMISES. +// +// In this index the namespace is the contract: `compat.xxx` is consumed with +// `#include`, an owner namespace like `gnome.xxx` exposes `import`. The test +// next to this file consumes gnome.gobject through its headers; this one consumes +// the same package through its module, so both doors are checked from the +// OUTSIDE — which is what an index example is for. +// +// ⚠️ THE TWO DOORS DO NOT COMPOSE, and that is GCC's rule rather than a choice +// here. A TU that imports the module AND textually includes a glib header +// reaches twice — once through the module's global fragment, once +// directly — and the same `struct tm` from the same file becomes two entities: +// +// error: conflicting declaration 'struct tm' +// note: previous declaration as 'struct tm' (of module gnome.glib) +// +// So a consumer picks ONE route, and which one is decided by macros: a module +// cannot carry them, and glib's are half its API. Code using `G_DEFINE_TYPE`, +// `G_OBJECT` or `G_TYPE_*` takes the header route. Code using the FUNCTION API +// — most of gio, and much of glib — imports and includes nothing. +#ifdef __linux__ + +import gnome.gobject; // re-exports gnome.glib + +#include +#include + +int main() +{ + int failures = 0; + auto check = [&](bool ok, const char *what) { + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; + }; + + std::printf("import gnome.gobject\n\n"); + + // ⚠️ EVERY G_TYPE_* IS A MACRO, so this file reaches the type registry + // through the FUNCTIONS those macros wrap. That is not a workaround — it + // is the shape of the module route, and it is why the header route stays + // supported next door. + const GType obj = g_object_get_type(); + check(obj != 0, "g_object_get_type() — the registry answers"); + check(std::strcmp(g_type_name(obj), "GObject") == 0, "g_type_name(GObject)"); + check(g_type_is_a(obj, obj), "g_type_is_a on GObject itself"); + + // The four GTypes build.mcpp's glib-mkenums reimplementation produced. + // ⭐ Their MACRO names are what shipped wrong once (G_UNICODE_TYPE_TYPE for + // G_TYPE_UNICODE_TYPE) — but a macro cannot come through a module, so this + // route checks the FUNCTIONS and the header route next door checks the + // macros. Two different outputs of one generator, tested separately on + // purpose. + check(g_unicode_script_get_type() != 0 && g_unicode_type_get_type() != 0 + && g_unicode_break_type_get_type() != 0 && g_normalize_mode_get_type() != 0, + "the four generated enum GTypes register"); + + // GValue, entirely through functions. + // G_VALUE_INIT is a MACRO; zero-initialising is what it expands to. + GValue v = {}; + g_value_init(&v, g_type_from_name("gint")); + g_value_set_int(&v, 42); + check(g_value_get_int(&v) == 42, "GValue: set and get an int"); + g_value_unset(&v); + + // A real object, created and reference-counted without a single macro. + GObject *o = static_cast(g_object_new(obj, nullptr)); + check(o != nullptr, "g_object_new(GObject)"); + g_object_ref(o); + g_object_unref(o); + g_object_unref(o); + + // gnome.glib arrived through gnome.gobject, as glib-object.h -> glib.h does. + char *s = g_strdup_printf("%d", 7); + check(s != nullptr && std::strcmp(s, "7") == 0, + "g_strdup_printf — gnome.glib came through gnome.gobject"); + g_free(s); + + std::printf("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else +#include +int main() { std::printf("linux only\n"); return 0; } +#endif