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..b430eb5 --- /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-mcpp3.tar.gz", + CN = "https://gitcode.com/mcpp-res/fontconfig/releases/download/2.15.0/fontconfig-2.15.0-mcpp3.tar.gz", + }, + sha256 = "505938aeeafe92257ea2c08d0af0e091d25af3e8d872b0dc82478605cbf99233", + }, + }, + }, + + 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