#!/bin/sh # fontconfig-drive.sh -- drive the shipped fontconfig command-line tools over a # two-font fixture and print the result as tagged, path-normalised sections. # # ONE DRIVER, TWO MACHINES. This same file produces the target's output and the # x86-64 reference in data/fontconfig-native.txt, so the two sides cannot drift: # a case that changed on one machine only would have to have been typed twice. # It is POSIX sh (busybox ash on the target) and is run with `sh `, so the # 0644 mode do_install gives every data/ payload is fine -- there is nothing to # chmod and nothing to copy out of the read-only rootfs. # # usage: sh fontconfig-drive.sh [bindir] # # must be writable and is created; , when given, prefixes every # fc-* call, which is how the reference is taken from a natively-built 2.15.0 # rather than from whatever fontconfig the build host happens to carry (Ubuntu's # is 2.13, which writes a UUID-named cache-7 and would not be a control at all). # # WHY EVERYTHING IS NORMALISED, AND EXACTLY WHAT IS REMOVED. Four things below # are machine- or run-dependent BY CONSTRUCTION, and comparing them would make # every run red for a reason that has nothing to do with the port: # # * THE WORKDIR PATH. It carries a pid on both machines (a fixed /tmp/ # is forbidden on a shared build host), and fontconfig prints the font path # in almost every output. -> @W@ # * THE CACHE FILE NAME. fontconfig 2.15 names a cache - # .cache-; the md5 follows the workdir and the arch follows # fcarch.h, which reads endianness, sizeof(void*) and alignof(double) -- # le32d8 on wasm32 against le64 on x86-64. -> @CACHE@ # * THE TOOL'S OWN argv[0]. fc-cache prefixes its status line with the path it # was invoked as, which is an install prefix on the reference machine and a # bare name on the target. -> the bare tool name. # * THE conf.avail DIRECTORY. fc-conflist prints absolute paths under the # install prefix. -> @CONFAVAIL@, which keeps the FILE LIST -- a real # statement about what the package ships -- and drops only the prefix. # # Nothing else is touched. Every family, style, spacing, charset range, lang # set, fontversion, foundry and format below is compared byte for byte. # # WHY THE FIXTURE IS TWO FONTS. fc-match and fc-list can only be shown to CHOOSE # when there is something to choose between: against a one-font directory they # return that font whatever they were asked, and every match case would pass on a # build whose matcher did nothing at all. 8x16.pcf is a bitmap, monospaced, # non-scalable "Fixed" (foundry Sony); vera.ttf is scalable proportional # TrueType "Bitstream Vera Sans" (foundry Bits). # # THE PROCESS COUNT IS A CORRECTNESS PROPERTY OF THIS FILE, NOT A PERFORMANCE # ONE. 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 whole suite is then lost with cases=0 -- not a red, no result at all. # Measured here: the first cut of this driver wedged the guest on one of its two # gate runs. So every avoidable fork is removed rather than left as tidiness: # # * ONE sed FOR THE WHOLE TRANSCRIPT. The body below runs inside a single # `{ ... } | norm`, so normalisation costs one process instead of the # twenty-one it cost when each section piped through its own. # * NO `$(command)` WHERE printf WILL DO. A command substitution is a fork on # top of the exec it wraps, so the match/pattern loops print the tool's # output directly rather than capturing it first. # * BUILTINS FOR STRING WORK. ${f##*/} rather than basename, `set --` rather # than `wc -l | tr -d ' '`. # # The fc-* invocations themselves are the deliverable and are NOT trimmed for # this -- a control removed to make a suite complete is a suite that measures # less. Everything around them is. # # WHY EVERY LIST IS SORTED, AND WHY THAT IS NOT PAPERING OVER ANYTHING. fc-list, # fc-scan and fc-cat walk a directory, so their order is readdir order and is not # a property of the port. Measured before relying on it: building the fixture # with the two file names in the opposite alphabetical order changes NO answer in # the match sections, so the sort hides an irrelevant difference and not a real # one. LC_ALL=C makes the sort itself the same on both machines. set -u LC_ALL=C; export LC_ALL W=${1:?workdir}; PCF=${2:?pcf}; TTF=${3:?ttf}; BIN=${4:-} fc () { cmd=$1; shift; if [ -n "$BIN" ]; then "$BIN/$cmd" "$@"; else "$cmd" "$@"; fi; } mkdir -p "$W/fonts" "$W/cache" "$W/pristine" || exit 1 cp "$PCF" "$W/fonts/8x16.pcf" || exit 1 cp "$TTF" "$W/fonts/vera.ttf" || exit 1 cp "$PCF" "$W/pristine/8x16.pcf" || exit 1 cp "$TTF" "$W/pristine/vera.ttf" || exit 1 # A MINIMAL fonts.conf, NOT THE IMAGE'S. /etc/fonts/fonts.conf names # /usr/share/fonts and the xdg cache, so a suite that used it would report # whatever fonts the image happens to carry and would go red the day one is # added. This config names exactly the fixture directory and exactly one cache # directory, both under $W, so the result is a property of the port and of these # two font files and of nothing else. It also deliberately loads NO conf.d: # without the generic-family aliases a desktop config carries, `fc-match sans` is # not a meaningful question here, and the matches below ask about concrete # properties instead. cat > "$W/fonts.conf" < $W/fonts $W/cache XML FONTCONFIG_FILE=$W/fonts.conf; export FONTCONFIG_FILE # Everything below writes through norm(), so no section can forget to normalise. # NOTE the [lb]e rather than \(le\|be\): with | as the s/// delimiter, GNU sed # reads \| as an escaped delimiter and the alternation silently never matches -- # which is exactly how the first cut of this file shipped a cache name into the # reference unnormalised. norm () { sed -e "s|$W|@W@|g" \ -e 's|[0-9a-f]\{32\}-[lb]e[0-9d]*\.cache-[0-9]*|@CACHE@|g' \ -e 's|^[^ :]*/\(fc-[a-z]*\):|\1:|' \ -e 's|[^ ]*/conf\.avail/|@CONFAVAIL@/|g' } sect () { echo "==FC-BEGIN $1=="; } esec () { echo "==FC-END $1=="; } emit () { # emit -- stdout+stderr of the command n=$1; shift sect "$n" "$@" 2>&1 esec "$n" } # EVERYTHING FROM HERE TO THE CLOSING BRACE GOES THROUGH ONE norm. It has to be # a group rather than per-section pipes: see the process-count note above. { # --- what this fontconfig IS ------------------------------------------------- emit version fc fc-cache --version # --- the cache the port writes ----------------------------------------------- # fc-cache runs BEFORE anything reads, so every later section reads a cache this # build produced rather than falling back to scanning the directory. Its success # is NOT taken from its 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. The cache file's # existence, in cache-files below, is the assertion. emit cache-build fc fc-cache -f -v "$W/fonts" # THE CACHE NAME IS NORMALISED HERE RATHER THAN BY norm, BECAUSE THIS SECTION IS # SORTED. norm runs on the whole transcript at the end, so a plain `ls | sort` # would order the RAW names -- and the raw cache name begins with an md5 of the # workdir path, whose first character is a digit or a hex letter depending on the # run. `CACHEDIR.TAG` therefore sorts before it or after it at random. Caught by # diffing the refactor's output against the reference it was supposed not to # change; the case/echo form costs no fork and is order-stable. sect cache-files for f in "$W/cache"/*; do f=${f##*/} case "$f" in *-[lb]e*.cache-*) echo "@CACHE@" ;; *) echo "$f" ;; esac done | sort esec cache-files # The lock file must be GONE once fc-cache returns. A leftover .LCK is what a # crashed or half-refused atomic write leaves behind, and it makes every later # fc-cache on this directory wait ten minutes before deciding the lock is stale. lck=0 for f in "$W/cache"/*.LCK; do [ -e "$f" ] && lck=$((lck+1)); done sect cache-lockfile printf 'lck=%s\n' "$lck" esec cache-lockfile # DELIBERATELY EXCLUDED from the reference comparison -- it is the one value the # two machines MUST disagree about. The suite asserts it against the literal # wasm32 answer instead, so a build whose fcarch.h derivation moved is caught # rather than normalised away. sect arch for f in "$W/cache"/*.cache-*; do f=${f##*/} echo "${f#*-}" # ${f#*-}, not ${f##*-}: strip the md5 up to the FIRST # hyphen, since the suffix itself contains one done | sort esec arch # --- reading a font file directly -------------------------------------------- emit query-pcf fc fc-query "$W/fonts/8x16.pcf" emit query-ttf fc fc-query "$W/fonts/vera.ttf" # fc-scan on the DIRECTORY, one line per font so the sort is meaningful. Sorting # fc-scan's raw multi-line dump would interleave the two patterns' fields and # turn a per-font disagreement into an unreadable diff. sect scan-dir fc fc-scan -f '%{file}|%{family}|%{style}|%{fontformat}|%{scalable}\n' "$W/fonts" 2>&1 | sort esec scan-dir # --- reading through the cache ------------------------------------------------- # The two halves are emitted separately because fc-cat -v prints a three-line # header and then one very long line per font; sorting them together is # deterministic but unreadable, and the header is the part that carries the paths. fc fc-cat -v "$W/fonts" > "$W/.cat" 2>&1 sect cat-header grep -E '^(Directory|Cache):' "$W/.cat" | sort esec cat-header sect cat-fonts grep '^"' "$W/.cat" | sort esec cat-fonts sect list-default fc fc-list 2>&1 | sort esec list-default sect list-format fc fc-list -f '%{family}|%{style}|%{spacing}|%{scalable}|%{outline}|%{fontformat}|%{foundry}|%{fontversion}\n' 2>&1 | sort esec list-format sect list-props fc fc-list : family style spacing scalable outline pixelsize charset 2>&1 | sort esec list-props # --- matching ------------------------------------------------------------------ # WHICH OF THESE ACTUALLY DISCRIMINATE, MEASURED RATHER THAN ASSUMED. The # obvious reading is that every pair below picks the font carrying the asked-for # value. It does not, and the difference is worth stating because a comment that # claimed otherwise would be describing a test that does not exist: # # DISCRIMINATE (each pair returns a different font): # the two family names, :scalable=true/false, :fontformat=PCF/TrueType, # :foundry=Bits # DO NOT (both arms return Bitstream Vera Sans): # :outline=true/false, :spacing=110 # # That is fontconfig's own weighting, not a defect and not this port's doing: # measured deterministic and INDEPENDENT OF FONT-SET ORDER (rebuilding the # fixture with the file names reversed gives the identical table), and it will be # identical on the target for the same reason every other value here is. The # non-discriminating pairs are kept precisely because they are compared byte for # byte: if wasm32 answered them differently from x86-64, THAT is a port defect, # and it is exactly the kind that a suite asserting only the "obvious" answers # would never see. sect match for p in \ 'Fixed' \ 'Bitstream Vera Sans' \ ':scalable=true' \ ':scalable=false' \ ':outline=true' \ ':outline=false' \ ':spacing=110' \ ':fontformat=PCF' \ ':fontformat=TrueType' \ ':foundry=Bits' \ ':foundry=Sony' do printf '%s => ' "$p" fc fc-match -f '%{family}/%{style}/%{fontformat}\n' "$p" 2>&1 done esec match # fc-match -s sorts the WHOLE font set by how well it matches, so it reports the # loser as well as the winner -- a strictly stronger statement than the best # match alone, and the one that notices a matcher that has stopped ordering while # still returning something plausible. sect match-sorted for p in ':scalable=true' ':scalable=false' ':fontformat=PCF' ':fontformat=TrueType'; do printf '%s =>\n' "$p" fc fc-match -s -f ' %{family}/%{fontformat}\n' "$p" 2>&1 done esec match-sorted sect pattern for p in 'Fixed-12:bold' 'Bitstream Vera Sans-10:italic' ':lang=ru' ':lang=en' ':charset=41'; do printf '%s =>\n' "$p" fc fc-pattern "$p" 2>&1 done esec pattern # --- the configuration the tools are reading ----------------------------------- # The + line is the config actually loaded; every - line is a conf.avail file # available and not loaded. # # DELIBERATELY EXCLUDED from the reference comparison, for the same class of # reason as `arch` and found the same way -- by measuring rather than assuming. # The reference comes from fontconfig-native, and a NATIVE SYSROOT IS NOT A # ROOTFS: it stages the binaries and headers a later recipe needs and does not # stage ${datadir}/fontconfig/conf.avail at all, so its fc-conflist prints the # `+` line and nothing else, while the target's prints forty-one entries the # package really does ship. Comparing them would be comparing two different # questions. The suite asserts the `+` line exactly (it is what makes every # other section a property of the port rather than of the image) and bounds the # available set from below against the filesystem instead. # # Worth recording that this is the ONLY section that differs between two # independently built x86-64 fontconfigs -- a hand ./configure against freetype # 2.11.1 and Yocto's fontconfig-native against 2.13.2 agree byte for byte on # every family, charset, lang set, cache rendering, match and control below. sect conflist fc fc-conflist 2>&1 | sort esec conflist sect validate for f in "$W/fonts/8x16.pcf" "$W/fonts/vera.ttf"; do printf '%s rc=' "${f##*/}" fc fc-validate "$f" >/dev/null 2>&1; echo "$?" done esec validate # --- negative controls ---------------------------------------------------------- # Each asks fontconfig to REFUSE something, because every case above asks it to # accept, and a build that accepted everything would pass all of them. sect negative # 1. a file that is not a font at all echo 'this is not a font' > "$W/notafont" fc fc-query "$W/notafont" >/dev/null 2>&1; echo "notafont rc=$?" # 2. a TRUNCATED copy of a real font -- the header parses, the tables do not. # Distinct from (1): it separates "rejects garbage" from "validates what it # reads", and only the second is a statement about the port. dd if="$W/pristine/vera.ttf" of="$W/trunc.ttf" bs=1 count=200 2>/dev/null fc fc-query "$W/trunc.ttf" >/dev/null 2>&1; echo "truncated rc=$?" # 3. a font file that does not exist fc fc-query "$W/absent.ttf" >/dev/null 2>&1; echo "absent rc=$?" # 4. an EMPTY directory must yield an empty scan -- the control for every # positive scan above, which would otherwise also pass if fc-scan reported # the same two fonts regardless of what it was pointed at. mkdir -p "$W/emptydir" fc fc-scan "$W/emptydir" 2>/dev/null | awk 'END{print "emptydir lines=" NR}' # 5. a pattern nothing satisfies must still MATCH something -- fontconfig always # returns a best effort -- but must not claim the property it was refused. printf 'impossible => ' fc fc-match -f '%{family}/%{fontformat}\n' ':fontformat=Type 1' 2>&1 esec negative # --- is the CACHE what is being read? A pair, each half the other's control ----- # This is the only question fc-cache's exit status cannot answer, and the reason # it matters here is the link() note above: a cache that was never written leaves # every fc-list correct anyway, because fontconfig silently falls back to scanning # the directory. Every section above would stay green on a port where fc-cache # does nothing at all. # # The corruption is written IN PLACE with conv=notrunc, which is load-bearing: it # leaves the directory's own mtime untouched, and FcCacheTimeValid compares the # cache's checksum against exactly that mtime. Creating, deleting or renaming a # file in the directory would invalidate the cache and measure the opposite thing. sect cache-authoritative dd if=/dev/zero of="$W/fonts/8x16.pcf" bs=1 count=64 conv=notrunc 2>/dev/null dd if=/dev/zero of="$W/fonts/vera.ttf" bs=1 count=64 conv=notrunc 2>/dev/null # Both font files are now unparseable. fc-list must STILL report both, because # it is reading the cache and never opens them. fc fc-list 2>&1 | sort esec cache-authoritative # ...and with the cache deleted and rebuilt against those same corrupt files, # fc-list must now report NOTHING. Without this half the section above would # also pass on a build that had memorised the fonts some other way, and with it # the pair says: the earlier answer came from the cache, and the cache is the # only thing that made it possible. sect cache-rebuilt-on-corrupt rm -f "$W/cache"/*.cache-* 2>/dev/null fc fc-cache -f "$W/fonts" >/dev/null 2>&1 fc fc-list 2>/dev/null | awk 'END{print "lines=" NR}' esec cache-rebuilt-on-corrupt # Restore, rebuild, and confirm the fixture is back -- so a reader can tell a red # above from a driver that simply left the tree broken. sect cache-restored cp "$W/pristine/8x16.pcf" "$W/fonts/8x16.pcf" cp "$W/pristine/vera.ttf" "$W/fonts/vera.ttf" rm -f "$W/cache"/*.cache-* 2>/dev/null fc fc-cache -f "$W/fonts" >/dev/null 2>&1 fc fc-list 2>&1 | sort esec cache-restored echo "==FC-DRIVE-DONE==" } | norm