. "$BST_LIB" # spice-protocol 0.14.4 -- the SPICE remote-desktop protocol definitions # (meta-openembedded meta-networking recipes-support/spice). 13 headers and one # .pc, no executable code whatsoever. See spice-protocol_%.bbappend for what the # port adds and why. # # --------------------------------------------------------------------------- # WHAT CAN AND CANNOT BE ASSERTED ABOUT A PACKAGE THAT IS PURE DEFINITIONS # # There is nothing to run. The whole deliverable is bytes that other recipes # compile against, and this image ships no C compiler, so a shell suite on its # own can say exactly one thing: the files arrived. That is worth asserting and # it is nowhere near enough -- a header set can be complete, installed, and # still describe a different wire layout than the peer at the other end of a # SPICE connection, and nothing here would notice until a SPICE client is # ported. # # So the port builds an ABI PROBE at recipe time and this suite drives it. The # recipe generates one probe from the tree it is about to package and compiles # it TWICE -- once with ${BUILD_CC} for the x86-64 build host, once with ${CC} # for wasm32 -- shipping the build host's run beside the target binary as # spice-protocol-abi.ref. Section 5 compares the two. # # UNLIKE xorgproto, THERE IS NO ORACLE IN THE PACKAGE. xorgproto states each # structure's wire size itself (`#define sz_`), so its probe can judge # without a reference. spice-protocol states nothing of the kind, which is why # the reference is a second real build of the same headers rather than a # constant: it must come from the SAME source as the target probe, or a version # bump makes the two disagree for a reason that has nothing to do with this # port. # # THE COMPARISON IS SPLIT, AND THE SPLIT IS READ OFF THE DECLARATION. Under GCC # and clang the only thing that packs these structures is SPICE_ATTR_PACKED -- # start-packed.h's `#pragma pack` arm is MinGW/MSVC-only -- so a type carrying # that attribute is by construction a wire form whose layout the protocol fixes, # and it must be identical on every architecture. Those lines, plus every enum # constant (which is a protocol number), are asserted as EXACT EQUALITY: 999 of # them, nothing tolerated. Ordinary host-side structures beside them carry no # such contract and are handled separately in section 6. # # THE COUNTS ARE ASSERTED AT BUILD TIME, NOT HERE, and the division is not # arbitrary. In the recipe ${D}${includedir}/spice-1 holds this package's files # and nothing else, so `13 headers` is exact and the bbappend makes a short # install fatal. What this suite asserts instead is the build-time census file, # the presence of each named header, and the probe's own coverage totals -- each # of which stays true as the image grows. INC=/usr/include/spice-1 PCD=/usr/share/pkgconfig ST=/usr/libexec/spice-protocol-tests SURF=/usr/share/spice-protocol/INSTALL-SURFACE DROPS=/usr/share/spice-protocol/ABI-DROPS # The complete installed header set, measured 2026-08-22 against 0.14.4 and # taken from spice/meson.build's install_headers() list -- which is a literal # thirteen-name array, so this is the DECLARED set and not merely what happened # to arrive. SP_HDRS="barrier.h end-packed.h enums.h ipc_ring.h macros.h protocol.h \ qxl_dev.h qxl_windows.h start-packed.h stats.h stream-device.h types.h \ vd_agent.h" work=${TMPDIR:-/tmp}/bst-spice-protocol.$$ rm -rf "$work"; mkdir -p "$work" # hasfile -- ok iff is a non-empty regular file. Fork-free. hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "missing or empty: $2"; fi } # --------------------------------------------------------------------------- # 0. THE CANARY, which is not an assertion about spice-protocol at all. # # The guest console drops a suite's FIRST BST result line before the host parser # sees it -- brintos/linux#96. The harness computes its pass count as # total-minus-failures, so THE DROPPED LINE IS COUNTED AS A PASS, and a suite # whose first case is a genuine `not ok` loses the one line that mattered. # So the first line out of this file asserts something already true. # Remove this case when #96 is fixed. bst_ok spice-protocol/suite-canary # --------------------------------------------------------------------------- # 1. PAYLOAD -- the directory, every declared header, and the metadata. check_rc spice-protocol/dir-spice-1 0 test -d "$INC" check_rc spice-protocol/dir-spice 0 test -d "$INC/spice" # Each header by name, from the declared set. A loop rather than thirteen # hasfile lines so that a header ADDED upstream without this list being updated # shows up in hdr-count below rather than passing unnoticed. missing= nh=0 for h in $SP_HDRS; do nh=$((nh + 1)) [ -s "$INC/spice/$h" ] || missing="$missing $h" done if [ -n "$missing" ]; then bst_notok spice-protocol/hdrs-all-present "absent from $INC/spice:$missing" else bst_ok spice-protocol/hdrs-all-present fi check_eq spice-protocol/hdr-declared-set 13 "$nh" # The directory holds the declared set and NOTHING ELSE. spice-1/spice is this # package's alone -- unlike /usr/include, which is shared -- so a stray file here # is either a packaging fault or a header nobody declared, and both are worth a # red. This is also the case that notices an upstream bump adding a header. check_eq spice-protocol/hdr-count 13 \ "$(ls "$INC/spice" 2>/dev/null | wc -l | tr -d ' ')" hasfile spice-protocol/pc-file "$PCD/spice-protocol.pc" hasfile spice-protocol/surface-file "$SURF" hasfile spice-protocol/drops-file "$DROPS" hasfile spice-protocol/probe-shipped "$ST/spice-abi-probe" hasfile spice-protocol/abi-ref-shipped "$ST/spice-protocol-abi.ref" # THE .pc LIVES UNDER ${datadir}, NOT ${libdir}, and that is asserted rather # than incidental. upstream's meson.build computes install_dir from prefix + # datadir deliberately: the payload is architecture-independent data and so is # its metadata. A .pc that moved to /usr/lib/pkgconfig would still be found by # pkg-config and every other case here would still pass, while the package had # quietly become arch-specific metadata. check_rc spice-protocol/pc-not-in-libdir 1 test -e /usr/lib/pkgconfig/spice-protocol.pc # --------------------------------------------------------------------------- # 2. THE BUILD-TIME CENSUS, read back on target. # # The recipe counted the tree at install time, where it is unambiguously this # package's, compared it byte-for-byte against a native x86-64 build, and # refused to package a short one. Reading the file here is what carries those # measurements onto the image: a rootfs assembled from a stale .deb would # present the old numbers. surf=$(cat "$SURF" 2>/dev/null) check_eq spice-protocol/surface-headers 13 \ "$(printf '%s\n' "$surf" | sed -n 's/.*[ ]headers=\([0-9][0-9]*\).*/\1/p')" check_eq spice-protocol/surface-checks 1064 \ "$(printf '%s\n' "$surf" | sed -n 's/.*[ ]checks=\([0-9][0-9]*\).*/\1/p')" check_eq spice-protocol/surface-enums 605 \ "$(printf '%s\n' "$surf" | sed -n 's/.*[ ]enums=\([0-9][0-9]*\).*/\1/p')" check_eq spice-protocol/surface-packed 394 \ "$(printf '%s\n' "$surf" | sed -n 's/.*[ ]packed=\([0-9][0-9]*\).*/\1/p')" check_eq spice-protocol/surface-asserts 999 \ "$(printf '%s\n' "$surf" | sed -n 's/.*[ ]asserts=\([0-9][0-9]*\).*/\1/p')" # WHAT THE GENERATOR COULD NOT COVER, asserted as its own case. Coverage is # DISCOVERED -- every candidate check is screened by compiling it -- so the # exclusions are the other half of the coverage claim, and a generator that # started dropping more would shrink the probe on BOTH arms at once and leave # section 5 comparing two equally-diminished runs. The recipe's floors are the # primary guard; this is the one that makes the exclusions readable on target # rather than only in a build log rm_work has deleted. # # Nine at 0.14.4, and all nine are conditionally-declared members of the four # VDAgentClipboard* structures -- `__reserved` and `selection` on each, plus # `serial` on the Grab. They sit inside #if blocks upstream does not enable. check_eq spice-protocol/abi-drops 9 "$(wc -l < "$DROPS" | tr -d ' ')" # --------------------------------------------------------------------------- # 3. THE .pc -- generated by meson, so read rather than assumed. require pkg-config spice-protocol/pkg-config-present check_rc spice-protocol/pc-exists 0 pkg-config --exists spice-protocol # --validate parses the file and resolves its Requires without emitting flags. # A .pc that --exists but does not validate is the shape that hides best: the # consumer fails at configure with a message about a package it never named. pkg-config --validate spice-protocol >"$work/v.out" 2>&1 vrc=$? if [ "$vrc" -eq 0 ] && [ ! -s "$work/v.out" ]; then bst_ok spice-protocol/pc-validate else bst_notok spice-protocol/pc-validate \ "rc=$vrc $(_bst_1line "$(cat "$work/v.out")")" fi # THE IDENTITY PIN. Unlike xorgproto -- whose PV is a bundle tag appearing in # none of the installed bytes -- spice-protocol's .pc carries a real version: # meson.project_version() is '0.14.4' in the tree at SRCREV 6f453a77, which is # the v0.14.4 tag, so PV, the tag and the .pc all agree and --modversion is a # genuine identity pin. An upstream bump is then a changed number here rather # than a silent pass. check_eq spice-protocol/pc-modversion 0.14.4 \ "$(pkg-config --modversion spice-protocol 2>/dev/null)" # --cflags has to name a directory that actually holds the headers. A .pc whose # prefix arithmetic moved would answer with a path that is simply not there, and # every check above would still pass. # # PKG_CONFIG_ALLOW_SYSTEM_CFLAGS is set for the same reason 57-xorgproto sets # it: pkg-config suppresses an -I naming a directory already on the compiler's # system include path. /usr/include/spice-1 is NOT such a directory, so the flag # is emitted either way here -- but asking for the flag pkg-config COMPUTED # rather than the one it decided to print is what makes this case about the # .pc's arithmetic instead of about pkg-config's output policy. # # The trailing space is pkg-config's own -- it emits one after the last flag -- # and is trimmed rather than tolerated, because the value is used as a path # below and a trailing blank fails the test for an unrelated reason. cf=$(PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --cflags spice-protocol 2>/dev/null \ | sed 's/[[:space:]]*$//') check_eq spice-protocol/pc-cflags "-I$INC" "$cf" d=${cf#-I} check_rc spice-protocol/pc-cflags-dir-real 0 test -f "$d/spice/protocol.h" # The .pc must not name a libdir or an architecture. The recipe refuses to # package one that does; this is the same assertion made on the image, which is # where a rootfs assembled from a stale .deb would show a different answer. if grep -qE '^libdir=|/usr/lib|wasm32|@[A-Za-z_]*@' "$PCD/spice-protocol.pc"; then bst_notok spice-protocol/pc-arch-neutral \ "spice-protocol.pc names a libdir, an arch token or an unsubstituted variable: $(_bst_1line "$(cat "$PCD/spice-protocol.pc")")" else bst_ok spice-protocol/pc-arch-neutral fi # --------------------------------------------------------------------------- # 4. THE PACKAGE, as the image's own database sees it. # # ${PN} is ALLOW_EMPTY and carries nothing, so what an image actually installs # is -dev; asserting that here is what distinguishes a rootfs that took the # headers from one that took only the empty parent. if dpkg -s spice-protocol-dev >/dev/null 2>&1; then bst_ok spice-protocol/pkg-dev-installed else bst_notok spice-protocol/pkg-dev-installed "dpkg does not know spice-protocol-dev" fi check_eq spice-protocol/pkg-version 0.14.4-r0 \ "$(dpkg -s spice-protocol-dev 2>/dev/null | sed -n 's/^Version: //p')" # --------------------------------------------------------------------------- # 5. THE WIRE ABI -- the target run against the build host's. # # do_install installs the payload with -m 0644 and the rootfs is read-only at # runtime, so the probe arrives non-executable and cannot be made executable # where it sits. Copy it to /tmp, which is a mode=1777 tmpfs with no noexec. probe=$work/spice-abi-probe if cp "$ST/spice-abi-probe" "$probe" 2>/dev/null && chmod +x "$probe" 2>/dev/null; then # The probe prints and exits 0 whatever it finds -- it reports, it does not # judge -- so a non-zero rc HERE means the binary did not run: a trap, a # missing import, an exec refusal. On this arm that is a real and common # failure and is what this case is for. "$probe" > "$work/abi.out" 2>"$work/abi.err" rc=$? if [ "$rc" -ne 0 ]; then bst_notok spice-protocol/abi-probe-runs "rc=$rc $(_bst_1line "$(cat "$work/abi.err")")" else bst_ok spice-protocol/abi-probe-runs fi tgt_total=$(sed -n 's/^SP-TOTAL //p' "$work/abi.out") ref_total=$(sed -n 's/^SP-TOTAL //p' "$ST/spice-protocol-abi.ref") # The probe reached its end. Without this, a run truncated part-way would # present a short line set that happened to match a short reference. if [ -n "$tgt_total" ]; then bst_ok spice-protocol/abi-probe-complete else bst_notok spice-protocol/abi-probe-complete \ "no SP-TOTAL line: $(_bst_1line "$(cat "$work/abi.out")")" fi # The two runs must cover the SAME set, or every comparison below is between # different questions. check_eq spice-protocol/abi-total-agrees "$ref_total" "$tgt_total" # Coverage floor, bounded from below and dated 2026-08-22. It is the same # number the recipe refuses to build under; asserting it again here is what # catches a rootfs carrying a probe from an older, thinner build. nlines=$(grep -c '^SP-' "$work/abi.out" 2>/dev/null) if [ -n "$nlines" ] && [ "$nlines" -ge 1066 ]; then bst_ok spice-protocol/abi-coverage else bst_notok spice-protocol/abi-coverage \ "probe printed ${nlines:-0} SP- lines, expected not less than 1066" fi # THE ASSERTION THIS SUITE EXISTS FOR. # # Every packed-type figure and every enum constant, byte-identical between # this arm and an x86-64 build of the same headers. A packed type is a SPICE # wire form whose layout the protocol fixes; an enum constant is a protocol # number -- a message opcode, a channel type, an error code. Neither may vary # by architecture, so this is exact equality with nothing tolerated and # nothing pinned: the expected values come from the reference the recipe # emitted from the same headers, not from a constant in this file. # # A line only the target reports is an arm-specific layout fault. A line only # the reference reports means this arm silently AGREES where x86-64 differs, # which is just as much a divergence, and the same comparison catches it. # # The recipe already asserted these with _Static_assert at build time and # refuses to package a violation, so a red here means something the build # could not see: a stale probe, a stale reference, or a runtime that does not # reproduce its own compile-time layout. All three are worth knowing. grep -E '^SP-(SIZE P|OFF P|ENUM) ' "$work/abi.out" | sort > "$work/tgt.wire" grep -E '^SP-(SIZE P|OFF P|ENUM) ' "$ST/spice-protocol-abi.ref" | sort > "$work/ref.wire" check_eq spice-protocol/abi-wire-count 999 \ "$(wc -l < "$work/tgt.wire" | tr -d ' ')" if cmp -s "$work/tgt.wire" "$work/ref.wire"; then bst_ok spice-protocol/abi-wire-matches-buildhost else bst_notok spice-protocol/abi-wire-matches-buildhost \ "wasm32 and the build host disagree on the SPICE wire layout: $(_bst_1line "$(diff "$work/ref.wire" "$work/tgt.wire" 2>&1 | head -20)")" fi # --------------------------------------------------------------------------- # 6. THE UNPACKED TYPES, where a difference is expected rather than wrong. # # These carry no protocol contract, so one holding a pointer or a long # legitimately differs between the LP64 build host and this ILP32 arm. # Measured 2026-08-22, exactly one type does: QXLReleaseInfoExt, which stores a # `QXLReleaseInfo *info` beside a uint32_t and is declared OUTSIDE the packed # region -- 8 bytes here against 16 there, with group_id at 4 against 8. # # The set is asserted by name rather than merely tolerated. It rots on an # upstream bump that adds another pointer-bearing structure -- and that is the # intended cost, the same trade 57-xorgproto makes for its ten padding cases: # a suite that states what it tolerates is worth more than one that tolerates # silently, and the red asks a person to look rather than hiding a real # divergence among an expected one. grep -E '^SP-(SIZE N|OFF N) ' "$work/abi.out" | sort > "$work/tgt.plain" grep -E '^SP-(SIZE N|OFF N) ' "$ST/spice-protocol-abi.ref" | sort > "$work/ref.plain" diff "$work/ref.plain" "$work/tgt.plain" 2>/dev/null \ | sed -n 's/^[<>] SP-[A-Z]* N \([^ ]*\).*/\1/p' | sort -u > "$work/plain.diff" if [ "$(cat "$work/plain.diff" | tr '\n' ' ' | sed 's/ *$//')" = \ "QXLReleaseInfoExt QXLReleaseInfoExt.group_id" ]; then bst_ok spice-protocol/abi-plain-known-differences else bst_notok spice-protocol/abi-plain-known-differences \ "unpacked types differing across architectures changed: $(_bst_1line "$(cat "$work/plain.diff")")" fi # THE PRECONDITION THAT MAKES SECTION 6 READ THE WAY IT DOES. # # QXLReleaseInfoExt differs because this arm is ILP32 and the build host LP64. # Assert that rather than assume it: on an arm where pointers were 8 bytes the # difference would vanish, abi-plain-known-differences would go red, and # nothing would say why. This case says why. mdl=$(sed -n 's/^SP-MODEL //p' "$work/abi.out") check_eq spice-protocol/abi-data-model "ptr=4 long=4 int=4 short=2" "$mdl" else bst_notok spice-protocol/abi-probe-stage "cannot stage $ST/spice-abi-probe" fi rm -rf "$work"