. "$BST_LIB" # BST-TIMEOUT: 300 # measured 76s over 71 cases (2026-08-18, builder at load ~2), # # so ~4x headroom; the 60s default is for shell suites and # # this one drives 42 fc-* processes # fontconfig 2.15.0 -- font DISCOVERY, MATCHING and CACHING, as poky ships it # from meta/recipes-graphics/fontconfig plus our bbappend. It rasterises nothing: # freetype (57-freetype, already on this image) turns an outline into pixels, and # this library is the layer that decides WHICH font that should have been. # # --------------------------------------------------------------------------- # WHAT IS BEING MEASURED, AND WHY A PRESENCE TEST WOULD NOT BE COVERAGE # # Unlike most library ports here, fontconfig ships nine executables, so this # suite needs no cross-linked probe -- it drives the shipped binaries, which is # what BRINTOS-1032 asked for. But "fc-list runs and prints something" is a very # weak statement about a matcher: fontconfig's failure mode is answering the # WRONG font, not crashing, and a port whose charset parsing, cache serialisation # or match weighting had gone wrong would still print two plausible lines. # # So every answer is compared BYTE FOR BYTE against data/fontconfig-native.txt -- # the same two fonts, driven by the same script, through a natively-built # fontconfig 2.15.0 and freetype 2.13.2 on x86-64. The script is # data/fontconfig-drive.sh and BOTH machines run THAT FILE, so the two sides # cannot drift: a case that changed on one machine only would have to have been # typed twice. Its header carries what is normalised away and why. # # THE EVIDENCE IS LAYERED. Read a red in the order the sections run, because the # first section to go red localises the defect: # # query-pcf/query-ttf FreeType metadata + fontconfig's own charset and lang # derivation, straight off the file -- no cache involved # scan-dir the same, reached by walking a directory # cache-build/-files fc-cache wrote a cache at all (see the link() note) # cat-* the cache DESERIALISES to what was put in it # list-* the pattern formatter and the cache reader together # match/-sorted the match algorithm and its weighting # negative refusals: garbage, truncation, absence, empty dir # cache-authoritative reads really go through the cache, not a rescan # # A build where query-* agrees but cat-* does not has a cache serialisation # problem -- the 32-bit intptr_t offsets fontconfig writes into the cache are the # obvious suspect on this arm. One where cat-* agrees but match does not has a # matcher problem and nothing else. # # THE ANCHORED CASES CONSULT NO CONTROL, because an oracle can be wrong. They are # properties of the FONT FILES (Vera's family name and 131072 fontversion, the # PCF's "Fixed"/Sony/spacing=110) or of THIS ARM (the le32d8 cache suffix), true # by construction rather than by measurement, so a reference captured from an # already-broken build could not make them pass. # # TWO SECTIONS ARE EXCLUDED FROM THE BYTE COMPARISON, AND ONLY THESE TWO: # # arch -- the cache format suffix, which the two machines MUST disagree # about. fcarch.h derives it from endianness, sizeof(void*) and # alignof(double): x86-64 is le64; wasm32 is little-endian with # 4-byte pointers and 8-byte double alignment, so le32d8. It is # asserted below against the literal wasm32 answer instead, so a # build whose fcarch derivation moved is caught rather than # normalised away. # conflist -- because a NATIVE SYSROOT IS NOT A ROOTFS. fontconfig-native # stages the binaries a later recipe needs and does not stage # ${datadir}/fontconfig/conf.avail at all, so the reference's # fc-conflist prints only its `+` line where the target's prints # that plus forty-one `- conf.avail` entries. # Asserted below against the target filesystem instead. # # THE ORACLE WAS CROSS-CHECKED RATHER THAN TRUSTED. The reference was generated # twice on x86-64 from two independent builds -- a hand ./configure against the # distro's freetype 2.11.1, and Yocto's fontconfig-native against the freetype # 2.13.2 this image ships -- and every section but `conflist` came out byte for # byte identical. The shipped reference is the second, so the target and the # reference share a FreeType version; the first exists to say that none of these # values is an artifact of one build's configuration. # --------------------------------------------------------------------------- # A root prefix, empty on the target and set only when this suite is validated # NATIVELY under dash on the build host before it ships. The runner never sets # it, so on the image these are exactly the absolute paths. R="${BST_FONTCONFIG_ROOT:-}" BINDIR=$R/usr/bin FIXTURE=$R/usr/libexec/fontconfig-tests/8x16.pcf VERA=$BST_DATA/freetype-vera.ttf DRIVE=$BST_DATA/fontconfig-drive.sh NATIVE=$BST_DATA/fontconfig-native.txt ARCHIVE=$R/usr/lib/libfontconfig.a PCFILE=$(bst_local "$R/usr/lib/pkgconfig/fontconfig.pc") # tmpfs copy: a `done < "$PCFILE"` runs in this shell (brintos/linux#73) if [ ! -x "$BINDIR/fc-query" ]; then bst_skip fontconfig/present "fontconfig-utils not installed on this image" exit 0 fi bst_ok fontconfig/present # --- the packages are whole --------------------------------------------------- # EVERY ASSERTION IN THIS BLOCK IS A SHELL BUILTIN, WHICH IS NOT A STYLE CHOICE. # `check test -f X` wraps its command in $(...), so it forks even for a # builtin; twenty-odd of those are twenty-odd processes, and on this platform the # process count is a correctness property -- see the note above the driver call # below. `if [ ... ]` costs nothing, and bst_ok/bst_notok are printf. ck () { if [ $2 "$3" ]; then bst_ok "$1"; else bst_notok "$1" "absent: $3"; fi; } # All nine drivers, not just the six BRINTOS-1032 names: fc-conflist, fc-pattern # and fc-validate come out of the same PACKAGES split and their absence would # mean the split had moved under us. for t in fc-cache fc-cat fc-conflist fc-list fc-match fc-pattern fc-query fc-scan fc-validate; do ck "fontconfig/bin-$t" -x "$BINDIR/$t" done ck fontconfig/staticlib -f "$ARCHIVE" ck fontconfig/pkgconfig -f "$PCFILE" ck fontconfig/hdr-fc -f "$R/usr/include/fontconfig/fontconfig.h" ck fontconfig/hdr-fcpriv -f "$R/usr/include/fontconfig/fcprivate.h" ck fontconfig/etc-conf -f "$R/etc/fonts/fonts.conf" ck fontconfig/etc-confd -d "$R/etc/fonts/conf.d" ck fontconfig/dtd -f "$R/usr/share/xml/fontconfig/fonts.dtd" ck fontconfig/confavail -d "$R/usr/share/fontconfig/conf.avail" # The libexec fc-cache hard link the stock recipe makes for fontcache.bbclass. # Nothing on this image inherits that class, but the link is what the recipe # installs and its absence would mean do_install:append had stopped running. ck fontconfig/libexec-fccache -f "$R/usr/libexec/fc-cache" # THE CACHE DIRECTORY THE bbappend SHIPS. fontconfig would mkdir it on first # write, so its absence is silent on a writable rootfs and total on a read-only # one -- which is exactly why it is packaged rather than left to runtime. ck fontconfig/cachedir -d "$R/var/cache/fontconfig" # ${PN} is not empty and must not be: it carries /etc/fonts, without which every # tool above runs against a default configuration naming no font directory. # Shape, not absence: this arm converts to shared libraries per recipe # (BRINTOS-1740), so a .so here is a conversion, not a policy breach. bst_lib_shape fontconfig/no-shared "$R/usr/lib/libfontconfig.so*" # `read` in a loop rather than `check_out ... cat`: one open, no fork. _pc=0 if [ -f "$PCFILE" ]; then while IFS= read -r l; do case "$l" in *-lfontconfig*) _pc=1 ;; esac; done < "$PCFILE" fi if [ "$_pc" -eq 1 ]; then bst_ok fontconfig/pc-libs else bst_notok fontconfig/pc-libs "no -lfontconfig in $PCFILE"; fi # --- the fixtures ship --------------------------------------------------------- # `test -f`, never `test -x`: do_install installs data/ with 0644, so a payload # that is executable where it sits was placed by hand and the assertion would # invert the one thing this suite exists to establish. ck fontconfig/fixture-pcf -f "$FIXTURE" ck fontconfig/fixture-ttf -f "$VERA" ck fontconfig/drive-present -f "$DRIVE" ck fontconfig/native-present -f "$NATIVE" if [ ! -f "$FIXTURE" ] || [ ! -f "$VERA" ] || [ ! -f "$DRIVE" ] || [ ! -f "$NATIVE" ]; then bst_notok fontconfig/drive-run "fixtures missing; cannot drive the tools" exit 0 fi # --- run the driver ------------------------------------------------------------- # /tmp is a mode=1777 tmpfs mounted from inittab and is the one place on this # image guaranteed writable. The cache directory the port ships (/var/cache/ # fontconfig) is deliberately NOT used here: the suite must measure the port, not # whatever a previous run left on the rootfs. tmp=${TMPDIR:-/tmp} W=$tmp/bst-fontconfig.$$ OUT=$tmp/bst-fontconfig-out.$$ PATH="$BINDIR:$PATH"; export PATH sh "$DRIVE" "$W" "$FIXTURE" "$VERA" "$BINDIR" > "$OUT" 2>&1 rc=$? if [ "$rc" -eq 0 ]; then bst_ok fontconfig/drive-rc else bst_notok fontconfig/drive-rc "rc=$rc"; fi # --- ONE awk DOES THE WHOLE COMPARISON -------------------------------------------- # It reads the reference and the transcript, compares every section, extracts every # anchored value, and emits the BST result lines itself. # # THAT IS A CORRECTNESS DECISION, NOT A TIDINESS ONE. The first cut of this file # did the same work in shell -- an `awk` per section extraction, a `cmp` per # section, a `printf|sed|head` per anchored value -- which came to roughly 120 # processes on top of the driver's own. On this platform a guest that runs enough # coroutinized frames through exec eventually traps at a park with an orphaned # handle (brintos/hwjs-cc#69, with brintos/hardwarejs#28 leaving the lane # unreclaimed), and the suite is then lost with cases=0 -- no red, no result at # all. That is what happened on this suite's first gate run. The same lesson is # recorded for 88-civetweb, which went from wedging 2 runs in 3 at 62 command # substitutions to 3 in 3 at 33. # # The comparison itself is unchanged: byte for byte, one case per section, with # the first differing line as the detail so a red names the answer that moved. awk -v want_arch="le32d8.cache-9" ' function emit(id, ok, detail) { if (ok) printf "BST ok %s\n", id else printf "BST not ok %s : %s\n", id, substr(detail, 1, 100) } function firstdiff(a, b, xa, xb, na, nb, i, n) { na = split(a, xa, "\n"); nb = split(b, xb, "\n") n = (na > nb) ? na : nb for (i = 1; i <= n; i++) if (xa[i] != xb[i]) return "line " i ": want <" xb[i] "> got <" xa[i] ">" return "differ but no differing line -- trailing newline?" } function val(sec, name, xa, na, i, pfx) { # a "\tname: value" line na = split(ACT[sec], xa, "\n"); pfx = name ": " for (i = 1; i <= na; i++) { s = xa[i]; sub(/^[ \t]+/, "", s) if (index(s, pfx) == 1) return substr(s, length(pfx) + 1) } return "" } function line_after(sec, pfx, xa, na, i) { # a "" line, remainder na = split(ACT[sec], xa, "\n") for (i = 1; i <= na; i++) if (index(xa[i], pfx) == 1) return substr(xa[i], length(pfx) + 1) return "" } FNR == NR { if (index($0, "==FC-BEGIN ") == 1) { r = substr($0, 12, length($0) - 13); next } if (index($0, "==FC-END ") == 1) { r = ""; next } if (r != "") REF[r] = REF[r] $0 "\n" next } { if (index($0, "==FC-BEGIN ") == 1) { a = substr($0, 12, length($0) - 13); next } if (index($0, "==FC-END ") == 1) { a = ""; next } if ($0 == "==FC-DRIVE-DONE==") { done = 1; next } if (a != "") ACT[a] = ACT[a] $0 "\n" } END { # A driver that died half way leaves a truncated transcript whose sections all # compare equal-and-empty, which reads as a full green. The end marker makes # that impossible, so it is checked before anything is believed. emit("fontconfig/drive-complete", done, "no ==FC-DRIVE-DONE== in the transcript") n = split("version cache-build cache-files cache-lockfile query-pcf query-ttf " \ "scan-dir cat-header cat-fonts list-default list-format list-props " \ "match match-sorted pattern validate negative cache-authoritative " \ "cache-rebuilt-on-corrupt cache-restored", S, " ") # THE REFERENCE MUST NOT BE EMPTY WHERE IT IS COMPARED. Two empty strings # compare equal, so a reference that lost a section -- or a section name typed # one way in the driver and another here -- would be a silent green forever. missing = "" for (i = 1; i <= n; i++) if (REF[S[i]] == "") missing = missing " " S[i] emit("fontconfig/ref-sections", missing == "", "empty in the reference:" missing) for (i = 1; i <= n; i++) { k = S[i] emit("fontconfig/" k, ACT[k] == REF[k], firstdiff(ACT[k], REF[k])) } # --- anchored: true by construction, no control consulted ------------------- # THE CACHE FORMAT SUFFIX. le32d8 and nothing else: wasm32 is little-endian # with 4-byte pointers and alignof(double) 8, which is what makes it le32d8 # rather than the le32d4 a 32-bit ARM produces. This is the one value the # reference cannot supply -- it says le64 -- and a build answering le64 here # has had SIZEOF_VOID_P mis-derived at configure time, with a cache layout to # match. arch = ACT["arch"]; sub(/\n$/, "", arch) emit("fontconfig/arch-le32d8", arch == want_arch, "want <" want_arch "> got <" arch ">") ver = ACT["version"]; sub(/\n$/, "", ver) emit("fontconfig/version-2150", ver == "fontconfig version 2.15.0", "got <" ver ">") # PROPERTIES OF THE TWO FONT FILES -- facts about what shipped, so they hold # whatever the reference says. # Indexed loops, not `for (i in P)`: awk leaves the iteration order of an # associative array unspecified, and a transcript whose case order changes # between runs is one nobody can diff against the last one. np = split("family:\"Fixed\"(s) foundry:\"Sony\"(s) spacing:110(i)(s) " \ "scalable:False(s) fontformat:\"PCF\"(s) pixelsize:16(f)(s)", P, " ") for (i = 1; i <= np; i++) { split(P[i], kv, ":"); f = kv[1]; w = substr(P[i], length(f) + 2) g = val("query-pcf", f) emit("fontconfig/pcf-" f, g == w, "want <" w "> got <" g ">") } nq = split("family:\"Bitstream_Vera_Sans\"(s) foundry:\"Bits\"(s) scalable:True(s) " \ "fontformat:\"TrueType\"(s) fontversion:131072(i)(s)", Q, " ") for (i = 1; i <= nq; i++) { split(Q[i], kv, ":"); f = kv[1]; w = substr(Q[i], length(f) + 2) gsub(/_/, " ", w) g = val("query-ttf", f) emit("fontconfig/ttf-" f, g == w, "want <" w "> got <" g ">") } # THE CACHE WAS WRITTEN AT ALL. Anchored rather than left to the section # compare, because this stands in for fc-cache exit status: FcAtomicLock # hard-links a temp file onto .LCK and returns false on any link() # error outside {EPERM,ENOTSUP,EACCES} -- and fc-cache STILL EXITS 0 when that # happens, writing nothing. On a substrate without a working link() this is # the case that goes red, and measured by mutation it is the ONLY one: # replacing fc-cache with `exit 0` leaves list-*, cat-* and match green, # because fontconfig silently falls back to scanning the directory. c = split(ACT["cache-files"], xa, "\n"); ncache = 0 for (i = 1; i <= c; i++) if (xa[i] == "@CACHE@") ncache++ emit("fontconfig/cache-written", ncache == 1, "cache files written: " ncache ", want 1") # THE MATCHER CHOSE. Named separately from the `match` section compare because # a reference is only an oracle: these four are true of the fixture itself -- # ask for the family only one font has, or the format only one font is in, and # fontconfig must return that font. A matcher returning a constant would pass # the section compare if the reference had been captured from it. split("Fixed|Fixed/Regular/PCF" \ "\036" "Bitstream Vera Sans|Bitstream Vera Sans/Roman/TrueType" \ "\036" ":fontformat=PCF|Fixed/Regular/PCF" \ "\036" ":scalable=false|Fixed/Regular/PCF", M, "\036") split("match-family-pcf match-family-ttf match-format-pcf match-scalable-no", MN, " ") for (i = 1; i <= 4; i++) { j = index(M[i], "|"); pat = substr(M[i], 1, j - 1); w = substr(M[i], j + 1) g = line_after("match", pat " => ") emit("fontconfig/" MN[i], g == w, "want <" w "> got <" g ">") } # THE ONLY CONFIG LOADED IS THE FIXTURE CONFIG, which is what makes every # section above a property of the port rather than of whatever /etc/fonts # holds. A `+` line naming anything else means FONTCONFIG_FILE stopped taking # effect and the whole comparison is measuring the image configuration. g = "+ " line_after("conflist", "+ ") emit("fontconfig/conf-loaded", g == "+ @W@/fonts.conf: No description", "got <" g ">") # THE PACKAGE SHIPS ITS conf.avail SET. Bounded from BELOW rather than pinned: # the count is a property of the fontconfig release and rises on a version # bump, so an exact number would go false for a reason that is not a defect. # 41 is what 2.15.0 ships, measured 2026-08-18. c = split(ACT["conflist"], xa, "\n"); navail = 0 for (i = 1; i <= c; i++) if (index(xa[i], "- @CONFAVAIL@/") == 1) navail++ emit("fontconfig/conf-avail-count", navail >= 41, "fc-conflist offers " navail " conf.avail entries, want >= 41") } ' "$NATIVE" "$OUT" # ...and the conf.avail files are really on the filesystem, not merely named. # fc-conflist reads the directory, so the two would agree even if the package had # shipped an empty one -- which is the case this pair separates. ck fontconfig/conf-avail-file -f "$R/usr/share/fontconfig/conf.avail/30-metric-aliases.conf" # THE FIXTURE IS THE FIXTURE. Vera is pinned by digest because it is SHARED with # 57-freetype, whose reference was captured against this exact file: a fixture # swapped under either suite would otherwise make the OTHER one red for a reason # its own reader could not see. The one unavoidable fork down here. _d=$(sha256sum "$VERA" 2>/dev/null) case "$_d" in c4c45690b345435b2cba52ecabe275f05e49b389b39fe68ad03afbb551288d3d*) bst_ok fontconfig/vera-digest ;; *) bst_notok fontconfig/vera-digest "want c4c45690..288d3d got '$_d'" ;; esac rm -rf "$W" "$OUT" 2>/dev/null