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 1109950..547d68c 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.5,§11 总账 / §14 fork 规范 / §17 桌面栈补齐 / §18 八角度实现后复核)** +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 栈)** ## 0. 这份文档解决什么 @@ -1548,3 +1548,147 @@ tag**——把新内容作为 release 资产发布,旧资产原样保留,零窗 设计时的判断有一半没扛过实现——而这正是「验证要更新到文档」的意义:留下的应该是 **被证伪之后的那一版**。 + +--- + +## 19. 合成器闸门与 GObject 栈(2026-08-31) + +§18 结尾说 wlroots「是『要不要』而不是『能不能』」。这一轮做了,答案是能 —— +连同 pango 那条线上能做的部分,以及**做不到的那一段的确切边界**。 + +| 包 | 版本 | 形态 | 为什么 | +|---|---|---|---| +| `wlroots.wlroots` | 0.20.2 | fork | 六个生成器 | +| `compat.pcre2` | 10.44 | 描述符 | 133k 行,**零生成器**(发布包自带 `.generic`/`.dist`) | +| `compat.fribidi` | 1.0.16 | 描述符 | 有八个生成程序,但**输出随发布包一起发** | +| `gnome.glib` / `gobject` / `gmodule` | 2.82.5 | fork | 六个生成器,含 816 行的 `glib-mkenums` | +| `freedesktop.wayland-protocols-*` | 1.49.1 | 补 enum 头 | 上游装、fork 之前没生成 | + +**判据再次被证明是「生成器」而不是行数**:pcre2 比 cairo 还大 29k 行,仍然是 +描述符;libdisplay-info 两千行是 fork。fribidi 更细一层 —— 它**有**生成器,但 +release tarball 里带着七张表的输出,所以没有东西需要跑。 + +### 19.1 `import wlroots;` 不是便利,是唯一入口 + +这是本轮最值得记的一条。wlroots 的 121 个公共头**没有一个 `extern "C"`**,而且 +其中两个**根本不是合法 C++**: + +```c +void wlr_scene_rect_set_color(struct wlr_scene_rect *rect, + const float color[static 4]); /* C99 专有 */ +``` + +g++ 报 `expected primary-expression before 'static'`,而且解析再也没恢复,于是 +文件里**后面每一条**声明都被报成「has not been declared」—— 把人引向「是不是 +少了 feature 守卫」。另有三个结构体成员叫 `namespace`、`delete`、`class`。 + +所以 C++ 消费者用任何 `extern "C"` 组合都 include 不了这些头。fork 的做法: + +| 上游 C | C++ | +|---|---| +| `[static N]` | `[N]` | +| `wlr_layer_surface_v1::namespace` | `::namespace_` | +| `wlr_input_method_v2::delete` | `::delete_` | + +关键字成员用 `#ifdef __cplusplus` 双臂给出,`#else` 一字不改是上游原文 —— +**同一偏移、同一 ABI,一种语言一种拼法**。C++ 拼不出 `namespace` 这个名字,所以 +给出 `namespace_` 不是改上游 API,而是它在这一侧唯一存在的形态。 + +**与 glib 对照可以看出这不是家风。** glib 没有模块:它的 API 是宏重的 +(`G_DEFINE_TYPE`、`g_signal_connect` 全是宏),而**宏不跨模块边界**,`import` +会把声明给你、把让声明可用的那一半扣下。形态跟着上游的头长什么样,不跟风格。 + +### 19.2 模块的两条固有限制 + +| | | +|---|---| +| 宏不跨模块 | `WLR_HAS_*`、`wl_container_of` 都来自头。`#include ` 与 import 并存是安全的 —— 它只有 `#define` | +| **会声明东西的头不能与模块并存** | `` 没有 `extern "C"`,并存会让那三个名字拿到 C++ 链接,而模块里是 C 链接。链接错误是 ``undefined reference to `wlr_version_get_major()'`` —— **括号就是线索**:名字被修饰了 | + +### 19.3 四个「看起来成功、其实拿错文件」的失败 + +本轮代价最高的一类,全部只在 CI 出现、本地四种清理方式都复现不了: + +1. **`config.h` 是 C 里最挤的文件名。** wlroots 的 include 路径上有**六个**, + 而 `mcpp::include_dir()`(build 程序发的)排在**所有依赖之后** —— 我们那份是 + 59/59。十一个 wlroots 源码读到的是 `compat.libinput` 的,`if (!HAVE_EVENTFD)` + ——wlroots 把它当**普通 C 表达式**用——报 `HAVE_EVENTFD undeclared`。 + +2. **清单里的 `include_dirs` 排在最前**,所以生成的头要写进清单点名的目录。 + 但 mcpp 在**运行 build 程序之前**就构造好命令行,并且**静默丢弃**尚不存在的 + 条目 —— 干净 clone 的第一次构建里,config.h 生成正确然后被无视。 + `include/.gitkeep` 就是为这个提交的。 + +3. 同样的道理适用于**协议头**。先是用 `#include_next` 让 shim 找到真头,CI 报 + `use of enum 'zwlr_layer_surface_v1_keyboard_interactivity' without previous + declaration` —— 一个**成功了但拿到错文件**的 include(文件缺失会明说)。改成 + 绝对路径后,另一个头又以同样方式失败。最终把**所有生成的头**都放进清单首位 + 目录,这一类才根除。 + +4. **本地绿可能链的是别的库。** `mcpp::link_lib("EGL")` 只发一个**标志**,ninja + 拿不到到 `bin/libEGL.so` 的边;本地能过是因为 mcpp subos 里正好有一份 + `libEGL.so.1`,`ldd` 指着它。lld 在干净树上直接说 `unable to find library + -lEGL`。 + +### 19.4 `[feature-deps]` 只对 `kind = "lib"` 有效 + +实测,而且判据很干脆: + +- `kind = "lib"` 的 feature 依赖(libseat、libinput、libudev、libdisplay-info) + **正常** —— 它们的对象并进本包,压根不需要 `-l`。 +- `kind = "shared"` 的(egl、glesv2)**不链接**,报 30 多个 `undefined symbol: + glActiveTexture / eglMakeCurrent`。 + +所以 EGL/GLESv2/gbm 移到普通 `[dependencies]`:它既链接又建立 ninja 边。代价是 +`default-features = false` 也会构建它们;feature 仍然决定**渲染器源码编不编** +和 `WLR_HAS_GLES2_RENDERER` 说什么,那才是消费者观察得到的部分。 + +### 19.5 探测宏:两个方向都踩过 + +§17 记的是「`#ifdef` 测的宏不能写 0」。这轮同时踩到了它和它的反面: + +| 文件 | 测法 | 正确写法 | +|---|---|---| +| glib `config.h` | `#ifdef HAVE_ISSETUGID` | **缺席**。写 `0` 让 glib 调了 glibc 没有的 BSD 接口 | +| wlroots `wlr/config.h` | `#if WLR_HAS_DRM_BACKEND` | **总是定义**,0 或 1。漏掉会让 `#if` 静默为假 | +| glib `gmoduleconf.h.in` | `#if (@X@)` | **值**,所以 0 是对的,省略是语法错 | + +**测法决定,每一次。** 名字长得像不算数。 + +还有一对几乎同名的宏:`USE_SYSTEM_PRINTF`(config.h,**选择**)与 +`GLIB_USING_SYSTEM_PRINTF`(公共 glibconfig.h,只**报告**)。只设第二个,glib +继续调它自带的 gnulib printf,而那部分根本没编 —— 一页 +`undefined reference to _g_gnulib_snprintf`。 + +### 19.6 包无法导出生成的头(2026-08-31 实测) + +两个包的探针,结论干脆: + +| | 消费者看得见吗 | +|---|---| +| `mcpp::include_dir()`(build 程序发的) | ❌ **包私有**,消费者报 `No such file or directory` | +| `[build] include_dirs`(清单里的) | ✅ 传播 | + +**产物本身就是头的包,不能在构建期生成它们。** 这解释了 +`freedesktop.wayland-protocols-*` 为什么把 195 个生成文件签进仓 —— 那不是历史 +包袱,是被迫的。§14 的规则对**自用**生成物成立,对**导出**生成物不成立。 + +1.49.1 就是按这条补的:上游的 meson 会装 `wayland-protocols/-enum.h`, +fork 之前只生成 client/server 两种,而 wlroots 0.20 的**十个公共头**要它们。 + +### 19.7 仍然缺的:gio,以及被它挡住的 pango + +这不是「还没做」,是量过的边界: + +- gio 的六个生成器里有两个是 **`gdbus-codegen`,8,351 行 Python**,把 D-Bus + 接口 XML 变成 GObject skeleton。 +- **七个** gio 源码 include 它的产物,另有**两个**引用那七个。 +- 所以不能绕过它而仍然是 gio;在 `build.mcpp` 里复刻一个 8.3k 行的代码生成器 + 与消费者的需要不成比例。 + +**pango 用 `GListModel`,它在 gio 里。** 所以文本排版那条线停在这里 —— 其余 +四个依赖(harfbuzz、fribidi、fontconfig、cairo)都已在索引中,只差这一环。 + +`vulkan-renderer`(要 glslang)、`x11-backend` / `xwayland`(要 xcb)、 +`color-management`(要 lcms2)同理:每一个都在包里写明「为什么缺席」而不是 +默默不提。 diff --git a/mcpp.toml b/mcpp.toml index 7c0ccc0..5b33e5c 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -101,6 +101,9 @@ members = [ "tests/examples/mtdev", "tests/examples/pixman", "tests/examples/wayland-protocols", + "tests/examples/wlroots", + "tests/examples/pcre2", + "tests/examples/fribidi", "tests/examples/libgbm", "tests/examples/libpng", "tests/examples/libwebp", diff --git a/pkgs/c/compat.fribidi.lua b/pkgs/c/compat.fribidi.lua new file mode 100644 index 0000000..1ee5351 --- /dev/null +++ b/pkgs/c/compat.fribidi.lua @@ -0,0 +1,160 @@ +-- compat.fribidi — the Unicode bidirectional algorithm. +-- +-- THE GATE ON pango. Arabic and Hebrew text is stored in logical order and +-- drawn in visual order, and working out which run goes which way is UAX #9 — +-- a specified algorithm with a conformance test suite, not something a text +-- layout engine reimplements. pango's meson makes it a hard dependency, so +-- nothing above it renders right-to-left text without this. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY AN INLINE DESCRIPTOR AND NOT A FORK +-- +-- fribidi generates seven tables from the Unicode Character Database, with +-- eight C programs under `gen.tab/` that are compiled and RUN during a normal +-- build. That is exactly the shape that forced forks for libevdev, libxkbcommon +-- and libdisplay-info — except that the RELEASE TARBALL SHIPS THE OUTPUT: +-- +-- lib/arabic-misc.tab.i lib/brackets-type.tab.i +-- lib/arabic-shaping.tab.i lib/joining-type.tab.i +-- lib/bidi-type.tab.i lib/mirroring.tab.i +-- lib/brackets.tab.i +-- lib/fribidi-unicode-version.h +-- +-- Verified: the seven `#include "*.tab.i"` in `lib/*.c` are matched one for one +-- by files in the tarball, and `fribidi-unicode-version.h` — which meson also +-- generates — is there too. So there is nothing to generate and no fork to +-- justify. The criterion has always been "does something have to be RUN", and +-- here the answer is no. +-- +-- Only `fribidi-config.h` is missing, because it is a `configure_file` +-- substitution rather than a generated table, and every value in it is a +-- property of this build. It is written out below. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- kind = "lib", NOT "shared" +-- +-- Upstream ships libfribidi.so.0. Nothing dlopens it — pango links it at build +-- time — so merging the objects avoids a SONAME that would then have to coexist +-- with a distribution's copy. Same choice as freedesktop.libdisplay-info, +-- freedesktop.libevdev and wlroots, and for the same reason. +package = { + spec = "1", + namespace = "compat", + name = "fribidi", + description = "GNU FriBidi 1.0.16 — the Unicode bidirectional algorithm (UAX #9), the dependency pango needs for right-to-left text", + licenses = {"LGPL-2.1-or-later"}, + repo = "https://github.com/fribidi/fribidi", + type = "package", + + xpm = { + linux = { + ["1.0.16"] = { + url = { + GLOBAL = "https://github.com/fribidi/fribidi/releases/download/v1.0.16/fribidi-1.0.16.tar.xz", + CN = "https://gitcode.com/mcpp-res/fribidi/releases/download/1.0.16/fribidi-1.0.16.tar.xz", + }, + sha256 = "1b1cde5b235d40479e91be2f0e88a309e3214c8ab470ec8a2744d82a5a9ea05c", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + -- `*/lib` carries both the public headers and the seven `.tab.i` + -- tables, which the sources include by bare name. + include_dirs = { "mcpp_generated", "*/lib", "*" }, + + generated_files = { + -- meson's `configure_file` output. Upstream's template + -- (lib/fribidi-config.h.in) substitutes ten placeholders; nine are + -- constants of this release and the tenth is sizeof(int). + ["mcpp_generated/fribidi-config.h"] = [[ +/* compat.fribidi: meson's configure_file output for lib/fribidi-config.h.in, + written out because every substitution is a constant of this release or a + property of the target. */ +#ifndef FRIBIDI_CONFIG_H +#define FRIBIDI_CONFIG_H + +#define FRIBIDI "fribidi" +#define FRIBIDI_NAME "GNU FriBidi" +#define FRIBIDI_BUGREPORT "https://github.com/fribidi/fribidi/issues/new" + +#define FRIBIDI_VERSION "1.0.16" +#define FRIBIDI_MAJOR_VERSION 1 +#define FRIBIDI_MINOR_VERSION 0 +#define FRIBIDI_MICRO_VERSION 16 + +/* meson.build:16 — bumped by upstream when the ABI changes, and reported + through fribidi_version_info. Not derived from the release number. */ +#define FRIBIDI_INTERFACE_VERSION 4 +#define FRIBIDI_INTERFACE_VERSION_STRING "4" + +/* Every target this index builds for is ILP32-or-LP64 with a 32-bit int. + fribidi uses it only to size FriBidiLevel arithmetic. */ +#define FRIBIDI_SIZEOF_INT 4 + +/* @FRIBIDI_MSVC_BUILD_PLACEHOLDER@ expands to nothing on a non-MSVC build, + which is what it is doing here by being absent. */ + +#endif /* FRIBIDI_CONFIG_H */ +]], + + -- The autotools-style probe answers. fribidi reads five of these + -- and every one is `#ifdef`-tested, so "defined" is the whole + -- statement and the value is decoration. + ["mcpp_generated/config.h"] = [[ +#ifndef MCPP_FRIBIDI_CONFIG_H +#define MCPP_FRIBIDI_CONFIG_H + +/* HAVE_FRIBIDI_CONFIG_H makes fribidi-common.h include the header above + instead of falling back to its built-in defaults — which are for a + hand-built copy and get the version wrong. */ +#define HAVE_FRIBIDI_CONFIG_H 1 + +/* HAVE_STRINGIZE says the preprocessor supports `#x`, which every C99 + compiler does. Without it fribidi-common.h takes a K&R path. */ +#define HAVE_STRINGIZE 1 + +#define HAVE_STDLIB_H 1 +#define HAVE_STRING_H 1 +#define HAVE_STRINGS_H 1 +#define HAVE_MEMORY_H 1 +#define SIZEOF_INT 4 + +/* HAVE_FRIBIDI_CUSTOM_H and HAVE_FRIBIDI_UNICODE_VERSION_H are + DELIBERATELY ABSENT rather than 0. + - fribidi_custom.h does not exist here (it is a downstream hook), and + `#ifdef HAVE_FRIBIDI_CUSTOM_H` would then include a missing file. + - fribidi-unicode-version.h DOES exist in the tarball, but the macro that + guards it is only consulted on the autotools path; the meson path — the + one whose file layout this package reproduces — includes it directly. */ + +#endif +]], + }, + + -- lib/*.c, all eighteen. There is nothing to exclude: the charset + -- converters (cp1255, cp1256, iso8859-6/8, cap-rtl) are part of the + -- library upstream ships, `fribidi-deprecated.c` keeps the ABI, and + -- the `bin/` programs (fribidi_benchmark, the CLI) are not here for + -- the usual reason — a dependency that provides `main` collides with + -- the consumer's. + sources = { "*/lib/*.c" }, + + cflags = { + "-DHAVE_CONFIG_H", + "-D_GNU_SOURCE", + -- Upstream's own, from lib/meson.build: without it every public + -- symbol would be hidden and pango would fail to link. + "-DFRIBIDI_BUILD", + -- pango is linked into shared objects, so the objects must be + -- position independent. + "-fPIC", + }, + + targets = { fribidi = { kind = "lib" } }, + }, +} diff --git a/pkgs/c/compat.pcre2.lua b/pkgs/c/compat.pcre2.lua new file mode 100644 index 0000000..2caf71c --- /dev/null +++ b/pkgs/c/compat.pcre2.lua @@ -0,0 +1,301 @@ +-- compat.pcre2 — Perl-compatible regular expressions, second edition. +-- +-- THE GATE ON glib. `glib/gregex.c` is written against pcre2's 8-bit API and +-- glib's meson makes it a hard dependency (`libpcre2-8`, required: true), so +-- nothing in the GLib/GObject/Pango stack can be built without it. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY AN INLINE DESCRIPTOR AND NOT A FORK +-- +-- The criterion in this index is GENERATORS, not line count — 133k lines here +-- against cairo's 104k, and cairo needed no fork either. pcre2 has three +-- generated inputs and the release tarball SHIPS ALL THREE, which is +-- deliberate on upstream's part: they exist so a build without autotools or +-- CMake is possible. +-- +-- src/pcre2.h.generic the public header +-- src/config.h.generic the probe answers +-- src/pcre2_chartables.c.dist the default character tables +-- +-- The last one is the interesting case. Upstream normally builds a +-- `pcre2_dftables` program and RUNS IT to emit `pcre2_chartables.c` from the +-- build machine's C library locale — the same build-machine-dependence that +-- made libdisplay-info and wlroots pin their `pnp.ids`. `.dist` is the C-locale +-- output, checked in by upstream precisely so a build need not run a program to +-- get a deterministic answer. Using it is both simpler AND more reproducible. +-- +-- ⚠️ pcre2.h AND config.h ARE WRITTEN OUT HERE rather than copied from the +-- `.generic` files, because both encode choices this package is making: +-- SUPPORT_UNICODE and the code-unit width are not properties of pcre2, they +-- are properties of the library this index wants glib to link. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ONE CODE-UNIT WIDTH, AND WHY THAT IS NOT A LIMITATION +-- +-- pcre2 compiles the SAME sources once per code-unit width, with every public +-- symbol renamed by `PCRE2_SUFFIX` — so libpcre2-8, -16 and -32 are three +-- libraries with disjoint symbol sets built from one directory. Building all +-- three would triple the object count for consumers that want one. +-- +-- 8 is the width glib uses, and it is the width UTF-8 needs. A consumer that +-- wanted 16- or 32-bit would need its own package, not a feature here: the +-- symbol names differ, so the two cannot coexist in one target anyway. +-- +-- ⚠️ `PCRE2_CODE_UNIT_WIDTH` must ALSO be defined by the consumer before +-- `#include `, because the header uses it to pick which set of +-- declarations to expose. Omitting it is a compile error that says so: +-- "PCRE2_CODE_UNIT_WIDTH must be defined before including pcre2.h" +-- glib does this itself (`#define PCRE2_CODE_UNIT_WIDTH 8` in gregex.c), so +-- nothing extra is needed for the stack this package exists for. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- JIT IS OFF +-- +-- `pcre2_jit_compile.c` IS compiled — it has to be, because +-- `pcre2_jit_compile()` and friends are part of the API and must exist as +-- symbols — but without SUPPORT_JIT they are stubs that return +-- PCRE2_ERROR_JIT_BADOPTION. That is upstream's own arrangement, not a +-- mutilation: `pcre2_config(PCRE2_CONFIG_JIT)` reports 0 and every caller that +-- checks (glib does) takes the interpreter path. +-- +-- The reason to leave it off is that the JIT is a per-architecture assembler +-- (sljit) with its own execution-memory allocator, and enabling it on a target +-- this index has not tested would trade a working regex engine for a possible +-- SIGSEGV. It can be turned on later by one line, once there is a test that +-- would notice. +package = { + spec = "1", + namespace = "compat", + name = "pcre2", + description = "PCRE2 10.44 — 8-bit Perl-compatible regular expressions with Unicode properties, the dependency glib's GRegex is written against", + licenses = {"BSD-3-Clause"}, + repo = "https://github.com/PCRE2Project/pcre2", + type = "package", + + xpm = { + linux = { + ["10.44"] = { + url = { + GLOBAL = "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.44/pcre2-10.44.tar.gz", + CN = "https://gitcode.com/mcpp-res/pcre2/releases/download/10.44/pcre2-10.44.tar.gz", + }, + sha256 = "86b9cb0aa3bcb7994faa88018292bc704cdbb708e785f7c74352ff6ea7d3175b", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + -- `*` is the extract directory. `*/src` puts `pcre2_chartables.c.dist` + -- and the internal headers on the path; `mcpp_generated` carries the + -- two headers written below, and comes FIRST so `pcre2.h` resolves to + -- ours rather than to any host copy. + include_dirs = { "mcpp_generated", "*/src", "*" }, + + generated_files = { + -- The public header. Upstream's `pcre2.h.in` is a template whose + -- only substitutions are the version numbers and, in the `.generic` + -- form, nothing else — so this is that file's content with the + -- three version macros filled in. + -- + -- Rather than reproduce 700 lines of declarations, this includes + -- the `.generic` copy upstream ships and supplies only what the + -- template would have substituted. `#include` does not care about + -- the file extension. + ["mcpp_generated/pcre2.h"] = [[ +#ifndef MCPP_PCRE2_H +#define MCPP_PCRE2_H +/* compat.pcre2: upstream's own pre-substituted public header. + * + * pcre2.h.generic is what upstream ships for builds that do not run configure, + * and it is byte-identical to what autotools would produce for this version — + * the only substitutions in pcre2.h.in are the three version numbers, and the + * .generic copy already has them. Including it rather than copying it means a + * version bump cannot leave a stale duplicate behind. */ +#include "pcre2.h.generic" +#endif +]], + + -- The probe answers. Every one describes THIS package's build, not + -- pcre2's defaults — the distinction that turned eight undefined + -- references into an afternoon on fontconfig. + ["mcpp_generated/config.h"] = [[ +#ifndef MCPP_PCRE2_CONFIG_H +#define MCPP_PCRE2_CONFIG_H + +/* ── what this build supports ─────────────────────────────────────────── */ + +/* SUPPORT_UNICODE brings in pcre2_ucd.c's property tables (about 100 KB) and + * makes \p{...}, \X and PCRE2_UTF work. glib's GRegex exposes all three + * through its own API, so this is not optional for the stack this package + * exists for. */ +#define SUPPORT_UNICODE 1 + +/* SUPPORT_JIT is DELIBERATELY ABSENT rather than 0. + * + * pcre2 tests it with `#ifdef SUPPORT_JIT` (pcre2_jit_compile.c:60 and + * pcre2_internal.h), so `#define SUPPORT_JIT 0` would say YES — the same trap + * that cost cairo an hour of silently-empty drawing. Absent means absent. + * + * pcre2_jit_compile.c is still COMPILED; without this macro it provides the + * public JIT entry points as stubs returning PCRE2_ERROR_JIT_BADOPTION, which + * is upstream's arrangement and what pcre2_config(PCRE2_CONFIG_JIT) reports. */ + +/* Same rule: these are all `#ifdef`-tested and so are left undefined. + * SUPPORT_PCRE2GREP_JIT, SUPPORT_PCRE2GREP_CALLOUT — pcre2grep only + * SUPPORT_LIBBZ2, SUPPORT_LIBZ, SUPPORT_LIBEDIT, + * SUPPORT_LIBREADLINE — pcre2grep/pcre2test only + * EBCDIC, EBCDIC_NL25 — not this century + * NEVER_BACKSLASH_C, PCRE2_DEBUG — behaviour changes + * SUPPORT_VALGRIND — instrumentation + */ + +/* ── the numeric limits, upstream's own defaults ──────────────────────── */ +/* Not tuned: these are the values every distribution ships, and a consumer + * that needs different ones sets them per-match through the match context. */ +#define HEAP_LIMIT 20000000 +#define LINK_SIZE 2 +#define MATCH_LIMIT 10000000 +#define MATCH_LIMIT_DEPTH MATCH_LIMIT +#define MAX_NAME_COUNT 10000 +#define MAX_NAME_SIZE 32 +#define MAX_VARLOOKBEHIND 255 +#define PARENS_NEST_LIMIT 250 +#define NEWLINE_DEFAULT 2 /* LF — the Unix convention, and glib's assumption */ + +/* ── the C library ────────────────────────────────────────────────────── */ +/* glibc and any musl this index would target have had all of these for + * decades. They are `#ifdef`-tested, so defining them to 1 and defining them + * at all are the same statement. */ +#define HAVE_ATTRIBUTE_UNINITIALIZED 1 +#define HAVE_BUILTIN_MUL_OVERFLOW 1 +#define HAVE_DIRENT_H 1 +#define HAVE_INTTYPES_H 1 +#define HAVE_LIMITS_H 1 +#define HAVE_MEMFD_CREATE 1 +#define HAVE_MEMMOVE 1 +#define HAVE_MKOSTEMP 1 +#define HAVE_SECURE_GETENV 1 +#define HAVE_STDINT_H 1 +#define HAVE_STDLIB_H 1 +#define HAVE_STRERROR 1 +#define HAVE_STRING_H 1 +#define HAVE_STRINGS_H 1 +#define HAVE_SYS_STAT_H 1 +#define HAVE_SYS_TYPES_H 1 +#define HAVE_SYS_WAIT_H 1 +#define HAVE_UNISTD_H 1 + +/* Identification, used by pcre2_config(PCRE2_CONFIG_VERSION) and in error + * text. Kept exactly in step with the tarball this descriptor names. */ +#define PACKAGE "pcre2" +#define PACKAGE_NAME "PCRE2" +#define PACKAGE_STRING "PCRE2 10.44" +#define PACKAGE_TARNAME "pcre2" +#define PACKAGE_VERSION "10.44" +#define VERSION "10.44" + +/* ── the export decoration ────────────────────────────────────────────── */ + +/* PCRE2_EXPORT must be DEFINED, and empty is the right value. + * + * pcre2_internal.h:150 builds the declaration specifier out of it — + * `#define PCRE2_EXP_DECL extern PCRE2_EXPORT` — so leaving it out does not + * mean "no decoration", it means the token survives into the source and every + * exported declaration becomes a syntax error: + * + * pcre2.h.generic:800: error: expected ';' before 'const' + * PCRE2_EXP_DECL const uint8_t *PCRE2_CALL_CONVENTION + * + * — reported against pcre2's own header, which reads like a broken tarball. + * Empty is what upstream's config.h.generic uses on Unix: the objects merge + * into the consumer (kind = "lib"), so there is no shared-library boundary to + * decorate and default visibility is correct. + * + * PCRE2_EXP_DEFN and PCRE2_STATIC stay undefined for the same reason + * config.h.generic leaves them undefined on Unix: both are Windows or + * special-environment hooks, and pcre2_internal.h derives PCRE2_EXP_DEFN from + * PCRE2_EXP_DECL when it is absent. */ +#define PCRE2_EXPORT + +#endif +]], + + -- The default character tables. Upstream builds and RUNS + -- `pcre2_dftables` to produce this file from the build machine's + -- locale; `.dist` is the C-locale output they ship so that nobody + -- has to. Including it keeps the answer independent of where the + -- package was built — the same reasoning that pins `pnp.ids` in + -- freedesktop.libdisplay-info and wlroots. + ["mcpp_generated/pcre2_chartables.c"] = [[ +/* compat.pcre2: upstream's C-locale default tables, compiled as-is. + * + * Upstream's normal path compiles `pcre2_dftables` and runs it against the + * BUILD MACHINE's locale, so `isalpha()` in a Turkish locale would produce a + * different table than in C. `.dist` is the C-locale answer, checked in by + * upstream for exactly this reason. + * + * Found through the "*"-glob src directory on the include path; #include does + * not care about the file extension. */ +#include "pcre2_chartables.c.dist" +]], + }, + + -- Upstream's CMakeLists PCRE2_SOURCES, in order, minus + -- `pcre2_chartables.c` (generated above). The programs — pcre2test, + -- pcre2grep, pcre2demo, the fuzzer, the JIT test — are not here: they + -- have their own `main`, and a dependency that provides one collides + -- with the consumer's (this index has measured that). + -- + -- `pcre2posix.c` is also absent. It is a separate library upstream + -- (libpcre2-posix) whose symbols are regcomp/regexec — names glibc + -- already defines — so merging it into a consumer would be a + -- duplicate-symbol trap for a POSIX API nothing here asks for. + sources = { + "*/src/pcre2_auto_possess.c", + "mcpp_generated/pcre2_chartables.c", + "*/src/pcre2_chkdint.c", + "*/src/pcre2_compile.c", + "*/src/pcre2_config.c", + "*/src/pcre2_context.c", + "*/src/pcre2_convert.c", + "*/src/pcre2_dfa_match.c", + "*/src/pcre2_error.c", + "*/src/pcre2_extuni.c", + "*/src/pcre2_find_bracket.c", + "*/src/pcre2_jit_compile.c", + "*/src/pcre2_maketables.c", + "*/src/pcre2_match.c", + "*/src/pcre2_match_data.c", + "*/src/pcre2_newline.c", + "*/src/pcre2_ord2utf.c", + "*/src/pcre2_pattern_info.c", + "*/src/pcre2_script_run.c", + "*/src/pcre2_serialize.c", + "*/src/pcre2_string_utils.c", + "*/src/pcre2_study.c", + "*/src/pcre2_substitute.c", + "*/src/pcre2_substring.c", + "*/src/pcre2_tables.c", + "*/src/pcre2_ucd.c", + "*/src/pcre2_valid_utf.c", + "*/src/pcre2_xclass.c", + }, + + -- `-DPCRE2_CODE_UNIT_WIDTH=8` is what selects the library being built; + -- every source reads it. `-DHAVE_CONFIG_H` makes them include the + -- config.h above. `-fPIC` because glib is linked into shared objects + -- and a non-PIC object cannot be. + cflags = { + "-DHAVE_CONFIG_H", + "-DPCRE2_CODE_UNIT_WIDTH=8", + "-D_GNU_SOURCE", + "-fPIC", + }, + + targets = { ["pcre2-8"] = { kind = "lib" } }, + }, +} diff --git a/pkgs/w/wlroots.wlroots.lua b/pkgs/w/wlroots.wlroots.lua new file mode 100644 index 0000000..6f3194d --- /dev/null +++ b/pkgs/w/wlroots.wlroots.lua @@ -0,0 +1,138 @@ +-- wlroots — the compositor library this whole stack was built toward. +-- +-- Every dependency wlroots has was already in this index before this +-- descriptor existed: wayland-server/client/protocols/scanner/egl, libdrm, +-- pixman, xkbcommon, libinput, libudev, libseat, egl, glesv2, gbm and +-- libdisplay-info. This is the package that spends them. +-- +-- [dependencies] +-- wlroots.wlroots = "0.20.2" +-- +-- import wlroots; +-- wlr_scene *scene = wlr_scene_create(); +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY A FORK +-- +-- Six generators, which is what decides fork-versus-descriptor in this index +-- (the criterion is generators, not line count — cairo is 104k lines and +-- needed no fork): +-- +-- 45 Wayland protocols -> wayland-scanner, three outputs each +-- backend/drm/gen_pnpids.sh +-- render/gles2/shaders/embed.sh +-- configure_file -> include/wlr/config.h, include/config.h, +-- include/wlr/version.h +-- +-- Three are shell scripts upstream. The fork has no `sh` and no `python`: +-- `build.mcpp` is compiled and run by mcpp, so each generator is a function +-- in it. https://github.com/mcpplibs/wlroots +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ `import wlroots;` IS NOT A CONVENIENCE — IT IS THE ONLY WAY IN +-- +-- wlroots' 121 public headers contain NOT ONE `extern "C"` block, and two of +-- them are not valid C++ at all. Five declarations use C99's array-parameter +-- form: +-- +-- void wlr_scene_rect_set_color(struct wlr_scene_rect *rect, +-- const float color[static 4]); +-- +-- which g++ reports as `expected primary-expression before 'static'` — and +-- then, the parse never recovering, every LATER declaration in the file as +-- "has not been declared", which sends you looking for missing feature +-- guards. Three struct members are named `namespace`, `delete` and `class`. +-- +-- So a C++ consumer cannot `#include ` under any +-- arrangement of `extern "C"`. The fork's build program writes C++-safe copies +-- of the affected headers — `[static N]` reduced to `[N]`, the keyword members +-- given an `#ifdef __cplusplus` spelling at the same offset, the `#else` arm +-- upstream verbatim — and the module wraps the lot. +-- +-- upstream C C++ +-- wlr_layer_surface_v1::namespace ::namespace_ +-- wlr_input_method_v2::delete ::delete_ +-- +-- Two limits, both inherent rather than defects in the wrapper: +-- +-- * MACROS DO NOT CROSS A MODULE BOUNDARY. `WLR_HAS_*`, `wl_container_of` +-- and `wl_list_for_each` come from headers. `#include ` +-- alongside the import is safe — it is nothing but `#define`s. +-- * A HEADER THAT DECLARES SOMETHING MUST NOT BE INCLUDED ALONGSIDE THE +-- MODULE. `` has no `extern "C"`, so including it gives +-- those three names C++ linkage while the module's have C linkage, and the +-- link fails with ``undefined reference to `wlr_version_get_major()'`` — +-- the parentheses in that message are the tell. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- FEATURES +-- +-- wlroots.wlroots = { version = "0.20.2", default-features = false, +-- features = ["session", "drm"] } +-- +-- Default: `drm`, `libinput`, `session`, `gles2`, `gbm`; `udmabuf` is also +-- available. `drm` and `libinput` both require `session`, and the build +-- program says so at configure time rather than letting it become a link +-- failure naming `wlr_session_*`. +-- +-- Not offered, each for a stated reason rather than an omission: +-- +-- vulkan-renderer needs glslang to compile four shaders to SPIR-V; +-- glslang is not in this index, and a feature that +-- cannot build is worse than an absent one +-- x11-backend needs xcb + xcb-errors; the point of this stack is +-- xwayland a compositor that does not drag X11 in +-- color-management needs lcms2; `color_fallback.c` is built instead, +-- which is upstream's own arrangement without it +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE PINNED pnp.ids +-- +-- The DRM backend turns a monitor's three-letter PNP id into a manufacturer +-- name from a table generated at build time. Upstream's `gen_pnpids.sh` reads +-- `/usr/share/hwdata/pnp.ids` OFF THE BUILD MACHINE, so the answer would +-- depend on where the package was built — the same trap +-- `freedesktop.libdisplay-info` documents. The fork pins hwdata v0.410 and +-- checks it in beside the generator. +-- +-- The fork's generator is also stricter than upstream's: `PNP_ID` keeps only +-- the low five bits of each character, so ids differing in case collide — +-- hwdata really does carry `inu` alongside uppercase ids — and a collision is +-- reported naming both rather than emitted as two identical `case` labels. +package = { + spec = "1", + namespace = "wlroots", + name = "wlroots", + description = "wlroots 0.20.2 — modular Wayland compositor library, with DRM/libinput/session/GLES2/pixman/GBM built and X11 left out", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/wlroots", + type = "package", + + xpm = { + linux = { + ["0.20.2"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/wlroots/archive/refs/tags/0.20.2.tar.gz", + -- ⚠️ THE CONTAINER TAG IS NOT THE PACKAGE VERSION. + -- + -- The fork's tag was re-cut three times while this descriptor was + -- still unpublished — safe precisely because nothing in the + -- world had extracted it yet, which is the only time re- + -- cutting ever is — and gitcode refuses to REPLACE an asset + -- of the same name in an existing release. So each corrected + -- tarball needed a new container: `0.20.2`, `0.20.2-1` and + -- now `0.20.2-3`. Only the last is referenced; the earlier + -- two hold superseded bytes and are left alone rather than + -- deleted, so a stale reference fails a checksum rather than + -- a download. + -- + -- Verified: this URL's sha256 equals the GLOBAL one's below. + CN = "https://gitcode.com/mcpp-res/wlroots/releases/download/0.20.2-3/wlroots-0.20.2.tar.gz", + }, + sha256 = "a9a9c2d8604486f655c919c5f94f90f75d04b09710eb3b151f2ec6ea866fd04e", + }, + }, + }, + + mcpp = "*/mcpp/wlroots/mcpp.toml", +} diff --git a/tests/examples/fribidi/mcpp.toml b/tests/examples/fribidi/mcpp.toml new file mode 100644 index 0000000..c3a46dd --- /dev/null +++ b/tests/examples/fribidi/mcpp.toml @@ -0,0 +1,11 @@ +# compat.fribidi — the bidi algorithm pango needs for right-to-left text. +[indices] +compat = { path = "../../.." } + +[package] +name = "fribidi-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.compat] +fribidi = "1.0.16" diff --git a/tests/examples/fribidi/tests/fribidi.cpp b/tests/examples/fribidi/tests/fribidi.cpp new file mode 100644 index 0000000..586bedb --- /dev/null +++ b/tests/examples/fribidi/tests/fribidi.cpp @@ -0,0 +1,181 @@ +// compat.fribidi — the Unicode bidirectional algorithm, on text that needs it. +// +// WHAT THIS ASSERTS AND WHY +// +// The tables are the whole package. fribidi's seven `.tab.i` files encode the +// Unicode Character Database, and a build that compiled none of them would +// still link, still run, and quietly treat Arabic as left-to-right. So every +// check below asks the library a question whose answer is IN a table: +// +// the Unicode version fribidi-unicode-version.h, generated upstream +// bidi character types bidi-type.tab.i +// base direction the resolution algorithm over those types +// visual reordering UAX #9 itself, the reason pango needs this +// mirroring mirroring.tab.i — "(" becomes ")" in an RTL run +// joining types joining-type.tab.i, which shapes Arabic +// +// A pass here means the tarball's pre-generated tables really were compiled — +// the claim that lets this be a descriptor instead of a fork. + +#ifdef __linux__ + +// ⚠️ WRAPPED IN extern "C". fribidi's public headers have no `extern "C"` of +// their own — measured, zero occurrences across fribidi.h and the fifteen +// headers it pulls — so a C++ translation unit mangles every declaration and +// the link fails naming symbols the library plainly contains. Same situation +// as compat.libseat in this index, and the same answer: the consumer wraps. +extern "C" { +#include +} + +#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() +{ + // ── 1. the version, and the Unicode version behind the tables ──────── + std::printf(" %s\n", fribidi_version_info); + check(std::strstr(fribidi_version_info, "1.0.16") != nullptr, + "fribidi_version_info names 1.0.16, as the descriptor does"); + std::printf(" Unicode %s\n", FRIBIDI_UNICODE_VERSION); + check(FRIBIDI_UNICODE_VERSION[0] >= '1', + "FRIBIDI_UNICODE_VERSION came from the generated header"); + + // ── 2. bidi character types — bidi-type.tab.i ──────────────────────── + // 'a' is L (left-to-right). U+0627 ARABIC LETTER ALEF is AL (arabic + // letter). U+05D0 HEBREW LETTER ALEF is R. Three different answers from + // one table; a build with no table would give the same answer for all. + const FriBidiCharType t_a = fribidi_get_bidi_type(0x0061); + const FriBidiCharType t_alef = fribidi_get_bidi_type(0x0627); + const FriBidiCharType t_hebr = fribidi_get_bidi_type(0x05D0); + std::printf(" types: 'a'=%s U+0627=%s U+05D0=%s\n", + fribidi_get_bidi_type_name(t_a), + fribidi_get_bidi_type_name(t_alef), + fribidi_get_bidi_type_name(t_hebr)); + // There is no FRIBIDI_IS_LTR: the header offers FRIBIDI_IS_RTL and the + // left-to-right case is its negation, which is the honest spelling anyway + // because "not RTL" is what the algorithm actually decides. + check(FRIBIDI_IS_LETTER(t_a) && !FRIBIDI_IS_RTL(t_a), "'a' is a left-to-right letter"); + check(FRIBIDI_IS_ARABIC(t_alef), "U+0627 ARABIC ALEF is an Arabic letter"); + check(FRIBIDI_IS_RTL(t_hebr) && !FRIBIDI_IS_ARABIC(t_hebr), + "U+05D0 HEBREW ALEF is right-to-left but not Arabic"); + + // ── 3. base direction of a paragraph ───────────────────────────────── + // The algorithm scans for the first strong character. A Hebrew-initial + // paragraph is RTL; a Latin-initial one is LTR. This is what pango calls + // to decide which way a line runs. + // ⚠️ fribidi_get_par_direction takes BIDI TYPES, not characters. Passing + // code points compiles — FriBidiCharType and FriBidiChar are both 32-bit + // unsigned — and returns a plausible wrong answer, which is why the check + // below reads the types out of the table first. + { + const FriBidiChar rtl[] = {0x05D0, 0x0020, 0x0041}; // ALEF SPACE A + FriBidiCharType types[3]; + fribidi_get_bidi_types(rtl, 3, types); + check(FRIBIDI_IS_RTL(fribidi_get_par_direction(types, 3)), + "a Hebrew-initial paragraph resolves to RTL"); + + const FriBidiChar ltr[] = {0x0041, 0x0020, 0x05D0}; // A SPACE ALEF + fribidi_get_bidi_types(ltr, 3, types); + check(!FRIBIDI_IS_RTL(fribidi_get_par_direction(types, 3)), + "…and a Latin-initial one to LTR"); + } + + // ── 4. reordering: the point of the library ────────────────────────── + // "abc" + Hebrew "אבג" in logical order, in an RTL paragraph. UAX #9 puts + // the Hebrew run first visually and REVERSES it, because Hebrew is stored + // in logical order and drawn right to left. + // + // This is an INTERMEDIATE QUANTITY, not a rendered image: it says exactly + // where the algorithm put each character, so a wrong answer points at the + // reordering rather than at "the text looked odd". + { + FriBidiChar logical[] = {0x0061, 0x0062, 0x0063, 0x0020, + 0x05D0, 0x05D1, 0x05D2}; // "abc אבג" + const FriBidiStrIndex len = 7; + FriBidiCharType types[7]; + FriBidiBracketType brackets[7]; + FriBidiLevel levels[7]; + FriBidiChar visual[7]; + FriBidiParType par = FRIBIDI_PAR_RTL; + + fribidi_get_bidi_types(logical, len, types); + fribidi_get_bracket_types(logical, len, types, brackets); + const FriBidiLevel max = fribidi_get_par_embedding_levels_ex( + types, brackets, len, &par, levels); + check(max > 0, "fribidi_get_par_embedding_levels_ex resolved levels"); + + std::memcpy(visual, logical, sizeof logical); + FriBidiStrIndex map[7]; + for (FriBidiStrIndex i = 0; i < len; ++i) { + map[i] = i; + } + // warn_unused_result: the return is the maximum level reached, and + // ignoring it is a warning that CI treats as noise worth removing. + const FriBidiLevel rmax = fribidi_reorder_line( + FRIBIDI_FLAGS_DEFAULT, types, len, 0, par, levels, visual, map); + check(rmax > 0, "fribidi_reorder_line reordered at least one level"); + + std::printf(" visual order: "); + for (FriBidiStrIndex i = 0; i < len; ++i) { + std::printf("U+%04X ", visual[i]); + } + std::printf("\n"); + + // In an RTL paragraph the Hebrew run comes first and is reversed: + // U+05D2 U+05D1 U+05D0, then the space, then "abc" left to right. + check(visual[0] == 0x05D2 && visual[1] == 0x05D1 && visual[2] == 0x05D0, + "the Hebrew run is placed first and reversed"); + check(visual[4] == 0x0061 && visual[5] == 0x0062 && visual[6] == 0x0063, + "…and the Latin run keeps its own order"); + } + + // ── 5. mirroring — mirroring.tab.i ─────────────────────────────────── + // In a right-to-left run, "(" must be DRAWN as ")". The glyph substitution + // is a table lookup, and a build without the table returns the input. + { + FriBidiChar mirrored = 0; + const bool got = fribidi_get_mirror_char(0x0028, &mirrored) != 0; + std::printf(" mirror of '(' = U+%04X\n", mirrored); + check(got && mirrored == 0x0029, "'(' mirrors to ')'"); + + FriBidiChar none = 0; + check(fribidi_get_mirror_char(0x0061, &none) == 0, + "…and 'a' has no mirror, rather than mirroring to itself"); + } + + // ── 6. joining types — joining-type.tab.i ──────────────────────────── + // Arabic letters change shape by position. ALEF is right-joining (it links + // to the preceding letter only); BEH is dual-joining. Getting this wrong + // is what makes Arabic render as disconnected letterforms. + { + const FriBidiJoiningType j_alef = fribidi_get_joining_type(0x0627); + const FriBidiJoiningType j_beh = fribidi_get_joining_type(0x0628); + std::printf(" joining: ALEF=%s BEH=%s\n", + fribidi_get_joining_type_name(j_alef), + fribidi_get_joining_type_name(j_beh)); + check(FRIBIDI_IS_JOINING_TYPE_R(j_alef), "ARABIC ALEF is right-joining"); + check(FRIBIDI_IS_JOINING_TYPE_D(j_beh), "ARABIC BEH is dual-joining"); + } + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif diff --git a/tests/examples/pcre2/mcpp.toml b/tests/examples/pcre2/mcpp.toml new file mode 100644 index 0000000..ed3c1e0 --- /dev/null +++ b/tests/examples/pcre2/mcpp.toml @@ -0,0 +1,11 @@ +# compat.pcre2 — glib's regex engine, exercised the way glib uses it. +[indices] +compat = { path = "../../.." } + +[package] +name = "pcre2-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.compat] +pcre2 = "10.44" diff --git a/tests/examples/pcre2/tests/pcre2.cpp b/tests/examples/pcre2/tests/pcre2.cpp new file mode 100644 index 0000000..8cdda1a --- /dev/null +++ b/tests/examples/pcre2/tests/pcre2.cpp @@ -0,0 +1,160 @@ +// compat.pcre2 — exercised the way glib's GRegex uses it. +// +// WHAT THIS ASSERTS AND WHY EACH ONE IS HERE +// +// The failures worth catching in a regex engine are not "it did not link". +// They are the ones where the library builds, links, runs, and gives a subtly +// wrong answer because a probe macro said something untrue. Each check below +// names a specific decision in the descriptor: +// +// compile + match the sources really were compiled, not just linked +// \p{Han} / \X SUPPORT_UNICODE, i.e. that pcre2_ucd.c's tables are in +// PCRE2_UTF that UTF-8 mode works, which is all glib ever uses +// the character tables that pcre2_chartables.c.dist was compiled, and in the +// C locale — the file the descriptor does NOT let a +// `pcre2_dftables` run produce from the build machine +// pcre2_config(JIT) that JIT is OFF and SAYS so, rather than crashing +// capture groups pcre2_substring_*, which glib's g_match_info_fetch is + +#ifdef __linux__ + +// ⚠️ REQUIRED BEFORE THE HEADER, and pcre2.h says so with an #error if it is +// missing. The header uses it to choose which of the three code-unit widths to +// declare, and this package builds the 8-bit one. glib does exactly this in +// gregex.c. +#define PCRE2_CODE_UNIT_WIDTH 8 +#include + +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-60s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +// Compile a pattern and report whether it matched, so each case below is one +// line. `subject` is UTF-8 throughout, which is the only encoding glib uses. +bool matches(const char *pattern, const char *subject, uint32_t options = 0) +{ + int err = 0; + PCRE2_SIZE off = 0; + pcre2_code *re = pcre2_compile(reinterpret_cast(pattern), + PCRE2_ZERO_TERMINATED, options, &err, &off, nullptr); + if (re == nullptr) { + PCRE2_UCHAR buf[256]; + pcre2_get_error_message(err, buf, sizeof buf); + std::printf(" compile failed at %zu: %s\n", off, reinterpret_cast(buf)); + return false; + } + pcre2_match_data *md = pcre2_match_data_create_from_pattern(re, nullptr); + const int rc = pcre2_match(re, reinterpret_cast(subject), + std::strlen(subject), 0, 0, md, nullptr); + pcre2_match_data_free(md); + pcre2_code_free(re); + return rc > 0; +} + +} // namespace + +int main() +{ + // ── 1. the version the descriptor names ────────────────────────────── + char ver[64] = {}; + pcre2_config(PCRE2_CONFIG_VERSION, ver); + std::printf(" pcre2 %s\n", ver); + check(std::strncmp(ver, "10.44", 5) == 0, + "pcre2_config reports 10.44, the version the descriptor names"); + + // ── 2. it compiles and matches at all ──────────────────────────────── + check(matches("^a+b$", "aaab"), "compile and match a trivial pattern"); + check(!matches("^a+b$", "aaac"), "…and decline one that does not match"); + + // ── 3. SUPPORT_UNICODE ─────────────────────────────────────────────── + // \p{...} is a compile-time ERROR without SUPPORT_UNICODE, not a silent + // mismatch — which is the good case: it fails loudly. The descriptor + // defines the macro (never as 0; pcre2 tests it with #ifdef). + check(matches("\\p{Han}", "漢", PCRE2_UTF | PCRE2_UCP), + "\\p{Han} matches a Han character — SUPPORT_UNICODE is on"); + check(!matches("\\p{Han}", "a", PCRE2_UTF | PCRE2_UCP), + "…and does not match a Latin one"); + + // \X is an extended grapheme cluster: "é" written as e + U+0301 is ONE + // grapheme and two code points. This distinguishes real UCD tables from a + // build that merely accepted the syntax. + check(matches("^\\X$", "e\xCC\x81", PCRE2_UTF), + "\\X treats e + combining acute as one grapheme cluster"); + + // ── 4. UTF-8 mode ──────────────────────────────────────────────────── + // Without PCRE2_UTF, "." matches one BYTE; with it, one code point. A + // three-byte character therefore distinguishes the two modes exactly. + check(matches("^.$", "漢", PCRE2_UTF), "with PCRE2_UTF, `.` is one code point"); + check(!matches("^.$", "漢"), "…and without it, one byte — so a 3-byte char does not match"); + + // ── 5. the character tables ────────────────────────────────────────── + // `\w` and `[[:alpha:]]` consult pcre2_chartables.c, which upstream would + // otherwise produce by RUNNING pcre2_dftables against the build machine's + // locale. The descriptor compiles upstream's C-locale `.dist` instead, so + // these answers cannot depend on where the package was built. + // + // The discriminating case is the second one: in a C locale, byte 0xE9 + // (é in Latin-1) is NOT alphabetic. In an fr_FR.ISO-8859-1 locale it is. + check(matches("^\\w+$", "abc_123"), "\\w accepts letters, digits and underscore"); + check(!matches("^[[:alpha:]]$", "\xE9"), + "…and byte 0xE9 is not alphabetic, i.e. the tables are C-locale"); + + // ── 6. JIT is off, and says so ─────────────────────────────────────── + // The descriptor compiles pcre2_jit_compile.c without SUPPORT_JIT, so the + // entry points exist as stubs. A consumer that checks — glib does — takes + // the interpreter path. The failure this catches is the other one: a build + // that claims JIT and then has no code behind it. + uint32_t jit = 0; + pcre2_config(PCRE2_CONFIG_JIT, &jit); + std::printf(" pcre2_config(JIT) = %u\n", jit); + check(jit == 0, "pcre2_config reports JIT off, matching the descriptor"); + + int err = 0; + PCRE2_SIZE off = 0; + pcre2_code *re = pcre2_compile(reinterpret_cast("(\\w+)@(\\w+)"), + PCRE2_ZERO_TERMINATED, 0, &err, &off, nullptr); + check(re != nullptr, "compile a pattern with capture groups"); + if (re != nullptr) { + // The JIT stub must FAIL CLEANLY rather than crash or pretend. + const int jrc = pcre2_jit_compile(re, PCRE2_JIT_COMPLETE); + std::printf(" pcre2_jit_compile = %d (PCRE2_ERROR_JIT_BADOPTION is %d)\n", + jrc, PCRE2_ERROR_JIT_BADOPTION); + check(jrc != 0, "…and pcre2_jit_compile declines instead of crashing"); + + // ── 7. capture groups, which is g_match_info_fetch ─────────────── + pcre2_match_data *md = pcre2_match_data_create_from_pattern(re, nullptr); + const char *subject = "user@example"; + const int rc = pcre2_match(re, reinterpret_cast(subject), + std::strlen(subject), 0, 0, md, nullptr); + check(rc == 3, "match yields two capture groups plus the whole match"); + if (rc == 3) { + PCRE2_SIZE len = 0; + PCRE2_UCHAR buf[64] = {}; + len = sizeof buf; + pcre2_substring_copy_bynumber(md, 1, buf, &len); + std::printf(" group 1 = %s\n", reinterpret_cast(buf)); + check(std::strcmp(reinterpret_cast(buf), "user") == 0, + "…and group 1 is the text before the @"); + } + pcre2_match_data_free(md); + pcre2_code_free(re); + } + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif diff --git a/tests/examples/wayland-protocols/mcpp.toml b/tests/examples/wayland-protocols/mcpp.toml index e51aa31..fd5be5a 100644 --- a/tests/examples/wayland-protocols/mcpp.toml +++ b/tests/examples/wayland-protocols/mcpp.toml @@ -31,4 +31,4 @@ standard = "c++23" # is upstream's semantics rather than a defect, and testing it would mean # asserting a link failure. [target.'cfg(linux)'.dependencies.freedesktop] -wayland-protocols-staging = "1.49" +wayland-protocols-staging = "1.49.1" diff --git a/tests/examples/wayland-protocols/tests/wayland-protocols.cpp b/tests/examples/wayland-protocols/tests/wayland-protocols.cpp index 19e50ca..b8184a8 100644 --- a/tests/examples/wayland-protocols/tests/wayland-protocols.cpp +++ b/tests/examples/wayland-protocols/tests/wayland-protocols.cpp @@ -93,6 +93,25 @@ int main() return failures == 0 ? 0 : 1; } +// ── 1.49.1: the enum headers upstream installs ─────────────────────────────── +// +// `wayland-scanner enum-header` output, which this fork did not generate before +// 1.49.1. wlroots 0.20 includes these from TEN of its PUBLIC headers, so a +// consumer of wlroots writing the ordinary `#include ` needs them on +// the include path — which is why they belong to THIS package rather than to +// wlroots. +// +// The enum header carries only the protocol's enums: no interface symbols and +// no libwayland dependency. Including it in a translation unit that links +// nothing proves exactly that. +#include +#include + +static_assert(XDG_TOPLEVEL_STATE_MAXIMIZED == 1, + "xdg-shell-enum.h did not come from the scanner"); +static_assert(ZWP_TABLET_TOOL_V2_TYPE_PEN == 0x140, + "tablet-v2-enum.h did not come from the scanner"); + #else int main() { return 0; } diff --git a/tests/examples/wlroots/mcpp.toml b/tests/examples/wlroots/mcpp.toml new file mode 100644 index 0000000..e9e12d8 --- /dev/null +++ b/tests/examples/wlroots/mcpp.toml @@ -0,0 +1,18 @@ +# wlroots — the compositor library, consumed the way a compositor would. +# +# ⚠️ The `wlroots` index redirect is what makes this test the INDEX's rather +# than the published package's. It has to be keyed on the namespace the +# dependency below requests: a redirect declared under a name nothing asks for +# is silently not registered, and the resolution falls back to the remote +# index — at which point this checks a package that is already published +# rather than the descriptor in this PR. +[indices] +wlroots = { path = "../../.." } + +[package] +name = "wlroots-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.wlroots] +wlroots = "0.20.2" diff --git a/tests/examples/wlroots/tests/wlroots.cpp b/tests/examples/wlroots/tests/wlroots.cpp new file mode 100644 index 0000000..f7db05a --- /dev/null +++ b/tests/examples/wlroots/tests/wlroots.cpp @@ -0,0 +1,169 @@ +// wlroots — what can be asserted without a seat, a GPU or a compositor. +// +// WHAT THIS TEST IS FOR +// +// A compositor library is mostly untestable in CI: creating a backend wants a +// DRM master, a libseat session and an input device, and none of a runner's +// three exist. So the temptation is to assert that it linked, which is worth +// almost nothing — a package can link and still have compiled none of the +// files a consumer needs (this index has measured exactly that). +// +// Everything below is chosen to be an INTERMEDIATE QUANTITY: a value the +// library computes on its own, from data it carries, with no device involved. +// Each one names a specific generator in build.mcpp, so a failure says which +// one broke rather than "wlroots is broken": +// +// version → include/wlr/version.h +// WLR_HAS_* → include/wlr/config.h, and the feature set behind it +// the PNP table → pnpids.c, from the pinned hwdata +// the box maths → util/box.c, i.e. that ordinary sources compiled +// the module itself → 986 `using ::` declarations that must all resolve +// `namespace_` → the C++ keyword-member rewrite +// +// ⚠️ NO extern "C" WRAPPER, and that is the point. wlroots' public headers +// contain not one `extern "C"` block, and two of them are not even valid C++ +// (C99 `[static N]` array parameters). A C++ consumer cannot include them at +// all. `import wlroots;` is not a nicety here — it is the only way in. + +#ifdef __linux__ + +// ⚠️ THE MODULE CANNOT CARRY MACROS, so `WLR_HAS_*` comes from the header. +// Macros are preprocessor entities and nothing exports them. +// +// ⚠️ AND ONLY A MACRO-ONLY HEADER MAY BE INCLUDED ALONGSIDE THE MODULE. +// `` is nothing but `#define`s, so it is safe. `` +// is not: it DECLARES wlr_version_get_major and has no `extern "C"` of its +// own, so including it here gives those three names C++ linkage while the +// module's have C linkage — two different entities, and the link fails with +// +// undefined reference to `wlr_version_get_major()' +// +// naming a function the library plainly contains. The parentheses in that +// message are the tell: a mangled name, i.e. the wrong one. Take declarations +// from the module and macros from the header, never the same name from both. +#include + +import wlroots; + +// `get_pnp_manufacturer` is INTERNAL to the DRM backend — declared in the +// private `backend/drm/util.h`, so it is not in the module and not in any +// installed header. Declared here because the generated table behind it is +// the one thing in this package whose failure looks exactly like success. +// `const char code[static 3]` and `const char *` are the same parameter after +// array-to-pointer decay, so this declaration is ABI-identical to upstream's. +extern "C" const char *get_pnp_manufacturer(const char *code); + +#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; + } +} + +} // namespace + +int main() +{ + // ── 1. version.h was generated, and says what the manifest says ─────── + std::printf(" wlroots %d.%d.%d\n", wlr_version_get_major(), + wlr_version_get_minor(), wlr_version_get_micro()); + check(wlr_version_get_major() == 0 && wlr_version_get_minor() == 20, + "version.h reports 0.20, the version the manifest declares"); + + // ── 2. config.h reports THIS build's features ──────────────────────── + // Not "some features are on" — the exact default set, so a feature that + // silently stopped being compiled is a failure and not a shrug. + std::printf(" drm=%d libinput=%d session=%d gles2=%d gbm=%d | " + "x11=%d vulkan=%d xwayland=%d\n", + WLR_HAS_DRM_BACKEND, WLR_HAS_LIBINPUT_BACKEND, WLR_HAS_SESSION, + WLR_HAS_GLES2_RENDERER, WLR_HAS_GBM_ALLOCATOR, + WLR_HAS_X11_BACKEND, WLR_HAS_VULKAN_RENDERER, WLR_HAS_XWAYLAND); + check(WLR_HAS_DRM_BACKEND && WLR_HAS_LIBINPUT_BACKEND && WLR_HAS_SESSION + && WLR_HAS_GLES2_RENDERER && WLR_HAS_GBM_ALLOCATOR, + "the five default features are all on"); + check(!WLR_HAS_X11_BACKEND && !WLR_HAS_VULKAN_RENDERER && !WLR_HAS_XWAYLAND, + "…and X11, Vulkan and Xwayland are off, as the manifest says"); + + // ── 3. util/box.c: ordinary sources really were compiled ───────────── + // Pure arithmetic on two rectangles, no device. If the package had linked + // without compiling anything this would not resolve. + wlr_box a{.x = 0, .y = 0, .width = 100, .height = 50}; + wlr_box b{.x = 60, .y = 20, .width = 100, .height = 50}; + wlr_box out{}; + const bool overlap = wlr_box_intersection(&out, &a, &b); + std::printf(" intersection = %d,%d %dx%d\n", out.x, out.y, out.width, out.height); + check(overlap && out.x == 60 && out.y == 20 && out.width == 40 && out.height == 30, + "wlr_box_intersection computes the overlap"); + + check(wlr_box_contains_point(&a, 10, 10) && !wlr_box_contains_point(&a, 10, 90), + "wlr_box_contains_point agrees with the box it was given"); + + // ── 4. the scene graph allocates ───────────────────────────────────── + // wlr_scene_create needs no backend — it is the one non-trivial object a + // test can build. It also proves types/scene/*.c compiled. + wlr_scene *scene = wlr_scene_create(); + check(scene != nullptr, "wlr_scene_create without a backend"); + if (scene != nullptr) { + const float colour[4] = {1.0f, 0.0f, 0.0f, 1.0f}; + wlr_scene_rect *rect = wlr_scene_rect_create(&scene->tree, 32, 16, colour); + check(rect != nullptr, "…and a rect in it"); + // wlr_scene_rect_set_color is one of the five declarations that use + // C99 `[static 4]`. Calling it is what proves the rewritten header + // declares the same function the library defines. + if (rect != nullptr) { + const float other[4] = {0.0f, 1.0f, 0.0f, 1.0f}; + wlr_scene_rect_set_color(rect, other); + check(true, "…and wlr_scene_rect_set_color, whose C99 [static 4] was rewritten"); + } + wlr_scene_node_destroy(&scene->tree.node); + } + + // ── 5. the C++ keyword-member rewrite ──────────────────────────────── + // `struct wlr_layer_surface_v1` has a member C++ cannot name. The header + // this build compiles against renames it under `#ifdef __cplusplus`, at + // the same offset. Reading it through a null-object offset would be UB, so + // this asserts on a real object's storage instead. + wlr_layer_surface_v1 ls{}; + ls.namespace_ = const_cast("panel"); + check(std::strcmp(ls.namespace_, "panel") == 0, + "wlr_layer_surface_v1::namespace_ is reachable from C++"); + + // ── 6. the PNP table generated from the PINNED hwdata ──────────────── + // The failure this catches looks exactly like a passing build: upstream's + // gen_pnpids.sh reads /usr/share/hwdata/pnp.ids off the BUILD MACHINE, so + // the manufacturer a monitor reports would depend on where the package was + // built. `get_pnp_manufacturer` is wlroots-internal rather than exported, + // so it is reached through the DRM backend header's declaration. + // + // "ACR" is the id in the EDID of the Acer monitor freedesktop.libdisplay- + // info tests against, which is where the expected string comes from. + const char *acr = get_pnp_manufacturer("ACR"); + const char *del = get_pnp_manufacturer("DEL"); + // ⚠️ "JJJ", not an arbitrary byte triple. PNP_ID keeps only the LOW FIVE + // BITS of each character, so "\x01\x02\x03" packs to the same key as "ABC" + // and returns AboCom System Inc. — measured, and it is upstream's encoding + // rather than a bug here. An id that is genuinely absent has to be one + // whose 15-bit key no entry occupies; "JJJ" (key 10570) is one of them. + const char *nope = get_pnp_manufacturer("JJJ"); + std::printf(" ACR=%s DEL=%s JJJ=%s\n", acr ? acr : "(null)", + del ? del : "(null)", nope ? nope : "(null)"); + check(acr != nullptr && std::strstr(acr, "Acer") != nullptr, + "the pinned pnp.ids resolved ACR to Acer"); + check(del != nullptr && std::strstr(del, "Dell") != nullptr, "…and DEL to Dell"); + check(nope == nullptr, "…and an unoccupied key returns NULL, not a neighbour"); + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif