From 2425e93d163fefa701d4d59a0c90aaaf319c28ff Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 22:41:19 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(fontconfig):=20=E5=AD=97=E4=BD=93?= =?UTF-8?q?=E5=8F=91=E7=8E=B0=E2=80=94=E2=80=94=E4=B8=83=E4=B8=AA=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=89=A9=E5=85=A8=E9=83=A8=E7=94=B1=20build.mcpp=20?= =?UTF-8?q?=E4=BA=A7=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit freetype 光栅化字形、harfbuzz 整形一段文字,但两者都答不了「给我一个带 CJK 的 无衬线字体」。那是 fontconfig,而 cairo 和 pango 都要它。 上游 meson 用 python 和 gperf 产出七个生成物,七个全是对 tarball 里已有文件的 文本变换,所以 fork 的 build.mcpp(mcpp 编译并运行的 C++)把它们全做了。这个包 不需要 python3、sh 或 gperf。 两个大的与上游**逐字节一致**,fork CI 每次重跑上游脚本再 diff: fclang.h 4897 行 281 个 .orth -> charset 位图 fccase.h 368 行 Unicode 大小写折叠 转写只有被拿去和原件对照才值得有。 fcobjshash.h 是**判断**而非转写:上游跑 C 预处理器 + gperf 做 72 条目的完美哈希, 而唯一消费者 fcobjs.c 只调 FcObjectTypeLookup 读一个字段。排序表 + 二分查找语义 相同,少两个工具、少一遍预处理。 五条运行期路径留空(FC_DEFAULT_FONTS / FC_FONTPATH / FC_CACHEDIR / CONFIGDIR / FONTCONFIG_PATH)——上游指向构建前缀,重定位后是宿主的字体与配置。 FONTCONFIG_FILE / FONTCONFIG_PATH / FONTCONFIG_SYSROOT 是环境出口。 ⚠ HAVE_FT_GET_BDF_PROPERTY 与 HAVE_FT_GET_PS_FONT_INFO **关掉**,因为 compat.freetype 不编 BDF 和 Type1 模块。声明了能编过,链接时八个未定义引用。 探测答案必须描述**这一套** freetype,不是上游默认构建。 测试直打三张生成表:FcNameParse 走对象表、FcLangGetCharSet 走 fclang (zh-cn 6765 码点、ru 与 en 不同集)、FcStrDowncase 走 fccase(含西里尔两字节折叠)。 只改 pkgs/ 与 tests/examples/。 --- mcpp.toml | 1 + pkgs/f/freedesktop.fontconfig.lua | 78 ++++++++++ tests/examples/fontconfig/mcpp.toml | 14 ++ .../examples/fontconfig/tests/fontconfig.cpp | 135 ++++++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 pkgs/f/freedesktop.fontconfig.lua create mode 100644 tests/examples/fontconfig/mcpp.toml create mode 100644 tests/examples/fontconfig/tests/fontconfig.cpp diff --git a/mcpp.toml b/mcpp.toml index 05895f7..34ed3d5 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -79,6 +79,7 @@ members = [ "tests/examples/redis-plus-plus-async", "tests/examples/sdl2", "tests/examples/spdlog", + "tests/examples/fontconfig", "tests/examples/freetype", "tests/examples/glad", "tests/examples/libaio", diff --git a/pkgs/f/freedesktop.fontconfig.lua b/pkgs/f/freedesktop.fontconfig.lua new file mode 100644 index 0000000..e725ef0 --- /dev/null +++ b/pkgs/f/freedesktop.fontconfig.lua @@ -0,0 +1,78 @@ +-- freedesktop.fontconfig — font discovery and matching. +-- +-- `compat.freetype` rasterizes a glyph and `compat.harfbuzz` shapes a run, but +-- neither can answer "give me a sans-serif that has CJK". That is fontconfig, +-- and it is what every toolkit above this layer links: cairo and pango both +-- require it, and any program that names a font rather than a font FILE needs +-- it. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- EVERY GENERATED FILE IS PRODUCED BY build.mcpp — NO python, sh OR gperf +-- +-- Upstream's meson emits SEVEN artifacts through python and gperf. All seven +-- are text transforms over files already in the tarball, so the fork's +-- `build.mcpp` — C++ that mcpp compiles and runs — does them: +-- +-- fcstdint.h a copy +-- fcalias.h/fcaliastail.h internal-alias headers (makealias.py) +-- fcftalias.h/… the same over fcfreetype.h +-- fcobjshash.h object-name lookup (cutout.py + gperf) +-- fclang.h 281 .orth files -> charset bitmaps +-- fccase.h Unicode case folding +-- src/fontconfig.cppm the C++23 module wrapper +-- +-- THE TWO BIG ONES ARE VERIFIED BYTE-FOR-BYTE against upstream's own Python, +-- and the fork's CI re-runs those scripts and diffs on every push: `fclang.h` +-- is 4,897 lines, `fccase.h` is 368. A transliteration is only worth having if +-- something checks it against the original. +-- +-- `fcobjshash.h` is a DECISION rather than a transliteration: upstream runs the +-- C preprocessor and gperf to build a perfect hash over 72 entries, and the one +-- consumer — `fcobjs.c` — calls `FcObjectTypeLookup(str, strlen(str))` and +-- reads a single field. A sorted table with a binary search has identical +-- semantics and removes both tools. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE RUNTIME PATHS ARE EMPTY, AND THAT IS THE USUAL DECISION +-- +-- `FC_DEFAULT_FONTS`, `FC_FONTPATH`, `FC_CACHEDIR`, `CONFIGDIR` and +-- `FONTCONFIG_PATH` point into the build prefix upstream, which after +-- relocation is the HOST's fonts and the HOST's configuration — the same silent +-- host edge that gave Vulkan an llvmpipe device instead of the GPU. +-- +-- fontconfig has the escape hatches that make empty workable: `FONTCONFIG_FILE` +-- (which config to read), `FONTCONFIG_PATH` (where configs live) and +-- `FONTCONFIG_SYSROOT` (a prefix over both), all read in fccfg.c. An ecosystem +-- that ships fonts declares them, exactly as xim:xkeyboard-config declares +-- XKB_CONFIG_ROOT — and until something does, `FcInit()` finds no fonts and +-- says so rather than rendering with the developer's. +-- +-- ⚠ TWO FREETYPE PROBES ARE OFF, and this is worth knowing before someone +-- "fixes" them: `HAVE_FT_GET_BDF_PROPERTY` and `HAVE_FT_GET_PS_FONT_INFO` are +-- absent because `compat.freetype` does not build the BDF and Type1 modules. +-- Claiming them compiles cleanly and fails at LINK with eight undefined +-- references from fcfreetype.c. A probe answer has to describe THIS freetype, +-- not upstream freetype's default build. +package = { + spec = "1", + namespace = "freedesktop", + name = "fontconfig", + description = "fontconfig 2.15.0 — font discovery and matching, every generated file produced by build.mcpp", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/fontconfig", + type = "package", + + xpm = { + linux = { + ["2.15.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/fontconfig/releases/download/v2.15.0/fontconfig-2.15.0-mcpp2.tar.gz", + CN = "https://gitcode.com/mcpp-res/fontconfig/releases/download/2.15.0/fontconfig-2.15.0-mcpp2.tar.gz", + }, + sha256 = "a7ac9d403f0035d2c6065237ebed8c6b0b68aab98be948ea2cc907b886be9dc2", + }, + }, + }, + + mcpp = "*/mcpp/fontconfig/mcpp.toml", +} diff --git a/tests/examples/fontconfig/mcpp.toml b/tests/examples/fontconfig/mcpp.toml new file mode 100644 index 0000000..225bd2d --- /dev/null +++ b/tests/examples/fontconfig/mcpp.toml @@ -0,0 +1,14 @@ +# fontconfig test member — the three generated tables. +[indices] +freedesktop = { path = "../../.." } + +[package] +name = "fontconfig-tests" +version = "0.1.0" +standard = "c++23" + +# `compat.expat` and `compat.freetype` arrive transitively; naming them here as +# version deps as well is fine because the package declares them the same way +# (not as path deps), unlike the wayland members. +[target.'cfg(linux)'.dependencies.freedesktop] +fontconfig = "2.15.0" diff --git a/tests/examples/fontconfig/tests/fontconfig.cpp b/tests/examples/fontconfig/tests/fontconfig.cpp new file mode 100644 index 0000000..c2e6a5f --- /dev/null +++ b/tests/examples/fontconfig/tests/fontconfig.cpp @@ -0,0 +1,135 @@ +// freedesktop.fontconfig — the three generated tables, exercised through the +// public API. +// +// WHAT CAN BE ASSERTED WITHOUT FONTS +// +// This package compiles the four runtime paths EMPTY on purpose (see the +// manifest), so `FcInit()` on a runner with no `FONTCONFIG_FILE` finds no +// configuration and no fonts. That is the intended behaviour, not a defect, +// and it means a font-matching test would only be asserting that the runner +// has a desktop. +// +// The three GENERATED tables need none of that, and they are what this fork +// exists to produce: +// +// fclang.h 281 .orth files compiled into charset bitmaps +// fccase.h Unicode case folding +// fcobjshash.h object-name → id, the gperf replacement +// +// Each is reachable through a public entry point that takes no config. + +#ifdef __linux__ + +#include + +#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() +{ + std::printf(" FcGetVersion() = %d\n", FcGetVersion()); + check(FcGetVersion() >= 21500, "FcGetVersion reports 2.15.0 or newer"); + + // ── fcobjshash.h — the gperf replacement ───────────────────────────── + // + // Exercised through `FcNameParse`, which is the public route into the + // object table: the parser maps every "name=value" key through + // `FcObjectLookupIdByName`, which IS the generated lookup. `FcObject` and + // `FcObjectFromName` are internal (fcint.h), so reaching for them would + // test the table without testing that a consumer can get to it. + { + FcPattern *p = FcNameParse((const FcChar8 *) "Arial:pixelsize=12:slant=100"); + check(p != nullptr, "fcobjshash: FcNameParse accepted three object names"); + if (p != nullptr) { + FcChar8 *fam = nullptr; + double px = 0; + int slant = 0; + const bool gf = FcPatternGetString(p, FC_FAMILY, 0, &fam) == FcResultMatch; + const bool gp = FcPatternGetDouble(p, FC_PIXEL_SIZE, 0, &px) == FcResultMatch; + const bool gs = FcPatternGetInteger(p, FC_SLANT, 0, &slant) == FcResultMatch; + std::printf(" family=%s pixelsize=%g slant=%d\n", + gf && fam ? (const char *) fam : "(none)", px, slant); + check(gf && fam && std::strcmp((const char *) fam, "Arial") == 0, + "…and \"family\" round-tripped to the right object"); + check(gp && px == 12.0, "…and \"pixelsize\" did too"); + check(gs && slant == 100, "…and \"slant\" did too"); + FcPatternDestroy(p); + } + + // A name the table does not contain must not resolve to a builtin — + // fontconfig assigns it a dynamic id instead. This is the branch a + // binary search gets wrong when its bounds are off by one. + FcPattern *q = FcNameParse((const FcChar8 *) ":nosuchproperty=1"); + check(q != nullptr, "…and an unknown property does not crash the parser"); + if (q) FcPatternDestroy(q); + } + + // ── fclang.h — the language charsets ───────────────────────────────── + // `FcLangGetCharSet` indexes straight into fcLangCharSets. Three languages + // with very different coverage, so a table that was truncated or misordered + // fails on at least one. + for (const char *l : {"en", "ru", "zh-cn"}) { + const FcCharSet *cs = FcLangGetCharSet((const FcChar8 *) l); + const FcChar32 n = cs ? FcCharSetCount(cs) : 0; + std::printf(" lang %-6s -> %u codepoints\n", l, n); + check(cs != nullptr && n > 0, "fclang: the language has a charset"); + } + // Latin and Cyrillic must not be the same set — they are separate entries + // whose leaves were de-duplicated, and a dedup bug shows up here. + { + const FcCharSet *en = FcLangGetCharSet((const FcChar8 *) "en"); + const FcCharSet *ru = FcLangGetCharSet((const FcChar8 *) "ru"); + check(en && ru && !FcCharSetEqual(en, ru), + "…and English and Russian are different sets"); + check(en && FcCharSetHasChar(en, 'A'), "…and English covers 'A'"); + check(ru && FcCharSetHasChar(ru, 0x0416), "…and Russian covers U+0416"); + check(en && !FcCharSetHasChar(en, 0x0416), "…and English does not"); + } + + // A language that does not exist must return null rather than index out of + // range — the fastpath table `fcLangCharSetRanges` is what decides that. + check(FcLangGetCharSet((const FcChar8 *) "zz-nowhere") == nullptr, + "fclang: an unknown language returns null"); + + // ── fccase.h — Unicode case folding ────────────────────────────────── + // `FcStrDowncase` walks fcCaseFold. ASCII exercises the EVEN_ODD/RANGE + // methods; U+0130 (Latin capital I with dot) is a FULL fold — one char to + // several — which is the method with its own byte pool. + { + FcChar8 *d = FcStrDowncase((const FcChar8 *) "ABC"); + std::printf(" downcase(\"ABC\") = %s\n", d ? (const char *) d : "(null)"); + check(d && std::strcmp((const char *) d, "abc") == 0, + "fccase: ASCII folds through the range methods"); + if (d) FcStrFree(d); + } + { + // U+0410 CYRILLIC CAPITAL A -> U+0430, a two-byte fold. + FcChar8 *d = FcStrDowncase((const FcChar8 *) "\xd0\x90"); + const bool ok = d && std::strcmp((const char *) d, "\xd0\xb0") == 0; + std::printf(" downcase(U+0410) = %s\n", ok ? "U+0430" : "(wrong)"); + check(ok, "…and a Cyrillic capital folds to its lowercase"); + if (d) FcStrFree(d); + } + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif From 1a8f4892c2d21ebbeef5dc78890d3719f6791eb0 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 22:44:05 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20build.mcpp=20=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E5=8C=85=E5=90=AB=20=E2=80=94=E2=80=94llvm=20?= =?UTF-8?q?=E8=85=BF=E6=8A=93=E5=88=B0=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit error: implicit instantiation of undefined template 'std::basic_istringstream' libstdc++ 通过 传递包含了它,libc++ 没有。这正是 validate.yml 注释里 说的「gcc 腿结构上看不见的一类 bug」。 重发 tarball(mcpp3),并在重发前用 mcpp toolchain default llvm 在本地跑过一遍 clang——免得再耗一轮 CI 才发现下一个同类问题。 --- pkgs/f/freedesktop.fontconfig.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkgs/f/freedesktop.fontconfig.lua b/pkgs/f/freedesktop.fontconfig.lua index e725ef0..b430eb5 100644 --- a/pkgs/f/freedesktop.fontconfig.lua +++ b/pkgs/f/freedesktop.fontconfig.lua @@ -66,10 +66,10 @@ package = { linux = { ["2.15.0"] = { url = { - GLOBAL = "https://github.com/mcpplibs/fontconfig/releases/download/v2.15.0/fontconfig-2.15.0-mcpp2.tar.gz", - CN = "https://gitcode.com/mcpp-res/fontconfig/releases/download/2.15.0/fontconfig-2.15.0-mcpp2.tar.gz", + GLOBAL = "https://github.com/mcpplibs/fontconfig/releases/download/v2.15.0/fontconfig-2.15.0-mcpp3.tar.gz", + CN = "https://gitcode.com/mcpp-res/fontconfig/releases/download/2.15.0/fontconfig-2.15.0-mcpp3.tar.gz", }, - sha256 = "a7ac9d403f0035d2c6065237ebed8c6b0b68aab98be948ea2cc907b886be9dc2", + sha256 = "505938aeeafe92257ea2c08d0af0e091d25af3e8d872b0dc82478605cbf99233", }, }, },