. "$BST_LIB" # BST-TIMEOUT: 300 # the probe is a 2.5 MB wasm module; staging it dominates # # libsoc is a C library over four generic Linux kernel interfaces -- GPIO # (/sys/class/gpio), I2C (/dev/i2c-N), SPI (/dev/spidevN.M) and PWM # (/sys/class/pwm) -- plus two layers that touch no peripheral at all: an # INI-ish reader (conffile_*) and the board-config lookup built on it # (libsoc_board_*). # # THIS MACHINE HAS NONE OF THE FOUR. That is the ticket's premise, and it is # established here by probe rather than asserted: the cases below report what # /sys and /dev actually carry and adjudicate from that, so a future runtime # that grows a controller reports a different reason instead of going red. A # machine with GPIO is a HEALTHIER machine and a suite that reds on it is wrong. # # The port ships -dev + -staticdev only: seven headers, libsoc.pc, libsoc.a -- # no program, and this image has no C compiler. A shell test could therefore say # nothing beyond "the files are present", which is not coverage for a library # whose failure mode is answering WRONGLY (a parser that mis-splits a pair, a # lookup that returns the wrong pin). So the two device-free layers are driven # for real through data/libsoc-probe, a C program cross-linked during the port # against this very archive (source: src/libsoc_probe.c + src/libsoc_pwm_probe.c). # # What keeps the artifact half from being a file listing is the completeness # check: it reads EVERY function the INSTALLED headers declare and requires each # one in the archive, so a partial compile, a visibility mistake or a # header/library skew all fail it. The assertion tracks the headers rather than a # list written here, which is what makes it survive a version bump. # # --------------------------------------------------------------------------- # WHY THE SYMBOL LOOKUP IS `grep -x`, NOT `grep -o` # # The batched form the older suites use, `grep -oFf names flat`, prints the # matched SUBSTRING, so a name that is a prefix of another can be reported found # whether or not it exists on its own. libsoc has two such pairs -- conffile_get # inside conffile_get_int, and libsoc_gpio_callback_interrupt inside # ..._interrupt_cancel. `-x` requires the whole line to match, which is exactly # the exact-symbol question, and it closes the hole by construction. # # Be precise about what that buys, because it was measured rather than assumed: # on THIS corpus the two forms give the same verdict. `sort -u` over `-o` output # collapses duplicates, and where both the short and the long name are patterns # grep reports the longest at that position, so the shorter one is never emitted # on its own. Injecting a declared-but-absent `conffile_ge` into the header # reddens sym-complete under BOTH forms (declared=49 found=48). So the anchoring # is NOT what sym-complete is resting on here -- sym-no-prefix-inflation below # is, and it goes red the moment the anchor is dropped. `-x` is kept as the # correct-by-construction form for a future corpus where it does bite; the # self-check is what pins it today. # # Text assertions are `case` globs against files read once into shell variables # -- no fork per case. ~110 cases as check_out+cat would be ~220 process spawns, # and on this runtime the forking IS the cost. # --------------------------------------------------------------------------- incd=/usr/include pc=/usr/lib/pkgconfig/libsoc.pc lib=/usr/lib/libsoc.a nl=' ' # hasfile -- ok iff is a non-empty regular file. # nofile -- ok iff does not exist. # has -- ok iff contains the literal . # hasnot -- ok iff it does not. # hasline -- ok iff carries as a WHOLE line. hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "absent or empty: $2"; fi; } nofile() { if [ ! -e "$2" ]; then bst_ok "$1"; else bst_notok "$1" "present: $2"; fi; } has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2" ;; esac; } hasnot() { case "$3" in *"$2"*) bst_notok "$1" "present: $2" ;; *) bst_ok "$1" ;; esac; } hasline() { case "$nl$3$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "not in archive: $2" ;; esac; } # --- the seven installed headers --------------------------------------------- # # lib/Makefile.am's include_HEADERS lists exactly these. libsoc_file.h exists in # the source tree and is NOT installed, so it is asserted absent: board.c # includes it, which means the archive exports file_* symbols whose declarations # a consumer cannot obtain. That is upstream's packaging, pinned here so a # version bump that changes it is noticed. for h in board conffile debug gpio i2c pwm spi; do hasfile "libsoc/header-$h" "$incd/libsoc_$h.h" done nofile libsoc/header-file-not-installed "$incd/libsoc_file.h" # --- each header declares its own layer -------------------------------------- # # Read once, asserted by glob. This is the header side of the skew check: the # completeness check below proves the archive carries what the headers declare, # and these prove the headers still declare the layer they are named for. hgpio=$(cat "$incd/libsoc_gpio.h" 2>/dev/null) has libsoc/decl-gpio-request 'libsoc_gpio_request' "$hgpio" has libsoc/decl-gpio-free 'libsoc_gpio_free' "$hgpio" has libsoc/decl-gpio-direction 'libsoc_gpio_set_direction' "$hgpio" has libsoc/decl-gpio-level 'libsoc_gpio_set_level' "$hgpio" has libsoc/decl-gpio-edge 'libsoc_gpio_set_edge' "$hgpio" has libsoc/decl-gpio-interrupt 'libsoc_gpio_wait_interrupt' "$hgpio" hi2c=$(cat "$incd/libsoc_i2c.h" 2>/dev/null) has libsoc/decl-i2c-init 'libsoc_i2c_init' "$hi2c" has libsoc/decl-i2c-write 'libsoc_i2c_write' "$hi2c" has libsoc/decl-i2c-read 'libsoc_i2c_read' "$hi2c" hspi=$(cat "$incd/libsoc_spi.h" 2>/dev/null) has libsoc/decl-spi-init 'libsoc_spi_init' "$hspi" has libsoc/decl-spi-rw 'libsoc_spi_rw' "$hspi" has libsoc/decl-spi-mode 'libsoc_spi_set_mode' "$hspi" hpwm=$(cat "$incd/libsoc_pwm.h" 2>/dev/null) has libsoc/decl-pwm-request 'libsoc_pwm_request' "$hpwm" has libsoc/decl-pwm-enabled 'libsoc_pwm_set_enabled' "$hpwm" has libsoc/decl-pwm-period 'libsoc_pwm_set_period' "$hpwm" hboard=$(cat "$incd/libsoc_board.h" 2>/dev/null) has libsoc/decl-board-init 'libsoc_board_init' "$hboard" has libsoc/decl-board-gpio-id 'libsoc_board_gpio_id' "$hboard" # libsoc_board.h pulls libsoc_conffile.h in, which is what makes board_config's # `conffile *conf` member complete for a consumer including only the board header. has libsoc/decl-board-includes-conffile '#include "libsoc_conffile.h"' "$hboard" hconf=$(cat "$incd/libsoc_conffile.h" 2>/dev/null) has libsoc/decl-conffile-load 'conffile_load' "$hconf" has libsoc/decl-conffile-get 'conffile_get' "$hconf" has libsoc/decl-conffile-get-int 'conffile_get_int' "$hconf" hdbg=$(cat "$incd/libsoc_debug.h" 2>/dev/null) has libsoc/decl-debug-set 'libsoc_set_debug' "$hdbg" has libsoc/decl-debug-get 'libsoc_get_debug' "$hdbg" # The two enum sets that make libsoc_gpio.h and libsoc_pwm.h mutually exclusive # in one translation unit: both define LS_SHARED/LS_GREEDY/LS_WEAK at file scope. # Asserted as a MEASURED property of the shipped headers, not as a wish -- it is # why src/ carries two probe translation units, and a reader who changes that # needs to see the constraint stated where the headers are. has libsoc/enum-collision-gpio 'LS_SHARED' "$hgpio" has libsoc/enum-collision-pwm 'LS_SHARED' "$hpwm" # --- pkg-config --------------------------------------------------------------- # # Note the module is `soc`, not `libsoc`: a consumer writes PKG_CHECK_MODULES # ([SOC],[soc]) and links -lsoc. Getting this wrong is the commonest way a # downstream port of ours would fail to find a library that is right there. hasfile libsoc/pc-present "$pc" pct=$(cat "$pc" 2>/dev/null) has libsoc/pc-name 'Name: soc' "$pct" has libsoc/pc-version 'Version: 0.8.2' "$pct" has libsoc/pc-libs '-lsoc' "$pct" has libsoc/pc-libdir '-L${libdir}' "$pct" has libsoc/pc-cflags '-I${includedir}' "$pct" has libsoc/pc-prefix 'prefix=/usr' "$pct" # --- the archive -------------------------------------------------------------- hasfile libsoc/archive-present "$lib" check_eq libsoc/archive-format '!' "$(head -c 7 "$lib" 2>/dev/null)" # --- completeness: every declared entry point is in the archive --------------- # # Flatten the archive's printable runs to one token per line first: a raw # `grep -o` over binary data reports "Binary file matches" instead of the # matches, which would silently under-count and read as a partial compile. tmp=${TMPDIR:-/tmp} flat=$tmp/libsoc-flat.$$ namef=$tmp/libsoc-names.$$ fakef=$tmp/libsoc-fake.$$ ( tr -c '[:print:]' '\n' < "$lib" > "$flat" 2>/dev/null ) # Declarations start at column 0 in every installed header; the doc comments do # not. That distinction matters here: libsoc_board.h's comment block for # libsoc_board_gpio_id still calls it `libsoc_gpio_id`, a name that has not # existed since the board layer was introduced, and a comment-blind extraction # would demand it of the archive and fail. grep -hoE '^[a-zA-Z_][a-zA-Z_0-9 *]*\**(libsoc|conffile)_[a-z_0-9]+[[:space:]]*\(' "$incd"/libsoc_*.h 2>/dev/null \ | sed 's/(.*//' | awk '{print $NF}' | sed 's/^\**//' \ | sort -u > "$namef" # -x: the WHOLE LINE must equal the name. See the header note. found=$(grep -xFf "$namef" "$flat" 2>/dev/null | sort -u) ndecl=$(grep -c . "$namef"); nfound=$(printf '%s' "$found" | grep -c .) # The floor guards the check itself: headers that lost their declarations, or an # extraction that silently matched nothing, would otherwise pass vacuously with # zero names to look up. 0.8.2 declares 48. if [ "$ndecl" -ge 40 ] && [ "$nfound" -eq "$ndecl" ]; then bst_ok libsoc/sym-complete else bst_notok libsoc/sym-complete "declared=$ndecl found=$nfound"; fi # Matcher self-check: names that do not exist, each one character short of one # that does. `-x` must reject every one of them; a matcher that matched any # would be reporting presence from a prefix, which is the failure the whole-line # form exists to prevent. printf '%s\n' conffile_ge libsoc_gpio_reques libsoc_i2c_ini libsoc_pwm_fre \ libsoc_board_ini libsoc_spi_writ libsoc_set_debu > "$fakef" fake=$(grep -xFf "$fakef" "$flat" 2>/dev/null | sort -u) nfake=$(printf '%s' "$fake" | grep -c .) if [ "$nfake" -eq 0 ]; then bst_ok libsoc/sym-no-prefix-inflation else bst_notok libsoc/sym-no-prefix-inflation "matcher claimed $nfake non-existent names: $(_bst_1line "$fake")"; fi # A named sample across all six layers as well, so a failure names the layer # that broke rather than only reporting a count. for s in libsoc_gpio_request libsoc_gpio_free libsoc_gpio_set_direction \ libsoc_gpio_get_direction libsoc_gpio_set_level libsoc_gpio_get_level \ libsoc_gpio_set_edge libsoc_gpio_get_edge libsoc_gpio_wait_interrupt \ libsoc_gpio_callback_interrupt libsoc_gpio_callback_interrupt_cancel \ libsoc_i2c_init libsoc_i2c_free libsoc_i2c_write libsoc_i2c_read \ libsoc_i2c_set_timeout \ libsoc_spi_init libsoc_spi_free libsoc_spi_set_mode libsoc_spi_get_mode \ libsoc_spi_set_speed libsoc_spi_get_speed libsoc_spi_set_bits_per_word \ libsoc_spi_get_bits_per_word libsoc_spi_write libsoc_spi_read libsoc_spi_rw \ libsoc_pwm_request libsoc_pwm_free libsoc_pwm_set_enabled \ libsoc_pwm_get_enabled libsoc_pwm_set_polarity libsoc_pwm_get_polarity \ libsoc_pwm_set_duty_cycle libsoc_pwm_get_duty_cycle \ libsoc_pwm_set_period libsoc_pwm_get_period \ libsoc_board_init libsoc_board_free libsoc_board_gpio_id \ conffile_load conffile_free conffile_get conffile_get_int \ libsoc_debug libsoc_warn libsoc_set_debug libsoc_get_debug; do hasline "libsoc/sym-$s" "$s" "$found" done rm -f "$flat" "$namef" "$fakef" # --- the configuration this port declares ------------------------------------- # # The recipe builds with the DEFAULT (empty) PACKAGECONFIG and its default # BOARD, which is the ticket's instruction: no python bindings (that option # pulls python3, blocked behind BRINTOS-1476), no board config files, and no # /etc/libsoc.conf symlink. These are not style assertions -- they are what # makes libsoc_board_init() fall through to its probe path, which is the # behaviour the probe adjudicates below. nofile libsoc/no-boardconfigs /usr/share/libsoc nofile libsoc/no-etc-conf /etc/libsoc.conf set -- /usr/lib/python*/site-packages/libsoc* if [ ! -e "$1" ]; then bst_ok libsoc/no-python-bindings else bst_notok libsoc/no-python-bindings "present: $1"; fi # --- static-only guards ------------------------------------------------------- # # The distro appends --disable-shared at class-target scope, so libtool builds # only the archive. A stray .so or .la would mean that selection was lost. noglob_ok() { # ok iff nothing matched the glob already expanded into "$@" _id=$1; shift if [ ! -e "$1" ]; then bst_ok "$_id"; else bst_notok "$_id" "present: $1"; fi } set -- /usr/lib/libsoc.so*; noglob_ok libsoc/no-shared "$@" set -- /usr/lib/libsoc.la; noglob_ok libsoc/no-libtool "$@" # test/ is not in SUBDIRS, so none of upstream's five test programs is built or # installed -- they must never reach the image under any name. for t in board_test gpio_test i2c_test pwm_test spi_test; do nofile "libsoc/no-$t" "/usr/bin/$t" done # --- the probe ---------------------------------------------------------------- # # do_install ships data/ 0644 and the rootfs is read-only at browser runtime, so # the payload arrives non-executable and cannot be made executable where it # sits. Copy to tmpfs and set the bit there. Never assert -x on the shipped # path: that passes against a hand-placed copy and fails against every image # that ever built the package. hasfile libsoc/probe-shipped "$BST_DATA/libsoc-probe" probe=$tmp/bst-libsoc-probe.$$ if cp "$BST_DATA/libsoc-probe" "$probe" 2>/dev/null && chmod +x "$probe" 2>/dev/null; then out=$("$probe" 2>&1); rc=$? rm -f "$probe" else rm -f "$probe" out=''; rc=127 bst_notok libsoc/probe-stage "cannot stage $BST_DATA/libsoc-probe" fi if [ "$rc" -eq 0 ] && [ -n "$out" ]; then bst_ok libsoc/probe-ran elif bst_execfail "$out"; then bst_unrun libsoc/probe-ran "$out" else bst_notok libsoc/probe-ran "rc=$rc out:'$(_bst_1line "$out")'" fi # p -- ok iff the probe printed exactly that line. Every case # below is named, so a probe that printed nothing still emits `not ok` lines # rather than contributing nothing to the count. p() { case "$nl$out$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "want line '$2'" ;; esac; } # pget -- echo the value the probe printed for , or the empty string. # One fork; used only where a case has to BRANCH on what was measured. pget() { printf '%s\n' "$out" | sed -n "s/^$1=//p" | head -n 1; } p libsoc/probe-complete 'probe=complete' p libsoc/probe-fixture 'fixture=written' # --- conffile: asserted for real, no device involved -------------------------- # # The probe writes its own fixture and reads it back through the shipped # archive. Every value below is the parser's answer, not a file listing. p libsoc/cf-load 'cf-load=nonnull' p libsoc/cf-value 'cf-model=brintOS-wasm32-probe' p libsoc/cf-value-with-slashes 'cf-dtfile=/nonexistent/device-tree/model' p libsoc/cf-missing-key 'cf-missing-key=DEFAULTED' p libsoc/cf-missing-section 'cf-missing-section=DEFAULTED' p libsoc/cf-comment-skipped 'cf-comment-not-parsed=DEFAULTED' p libsoc/cf-section-isolated 'cf-section-isolated=DEFAULTED' # "P9_11 = 30" -- both halves trimmed, and the value read as an integer. p libsoc/cf-int-trimmed 'cf-int-p9=30' p libsoc/cf-int-plain 'cf-int-p8=66' p libsoc/cf-int-negative 'cf-int-neg=-5' p libsoc/cf-int-nonnumeric 'cf-int-nonnumeric=-99' p libsoc/cf-int-missing 'cf-int-missing=-7' p libsoc/cf-free 'cf-free=returned' # An unopenable path: conffile_load warns and still returns an allocated EMPTY # conffile rather than NULL, so a caller that only checks for NULL believes it # loaded a config. Pinned as the behaviour this build has -- every lookup # against it must default rather than fault. p libsoc/cf-absent-not-null 'cf-load-absent=nonnull' p libsoc/cf-absent-defaults 'cf-absent-get=DEFAULTED' p libsoc/cf-absent-defaults-int 'cf-absent-get-int=-3' p libsoc/cf-absent-free 'cf-absent-free=returned' # --- board: asserted for real, through the LIBSOC_CONF override --------------- # # board.c's _get_conf_file() honours $LIBSOC_CONF ahead of the compiled-in # /etc/libsoc.conf, which is what makes the board layer reachable on a machine # that ships no board config at all. p libsoc/bd-setenv 'bd-setenv=ok' p libsoc/bd-init 'bd-init=nonnull' p libsoc/bd-gpio-id 'bd-gpio-id-p9=30' p libsoc/bd-gpio-id-2 'bd-gpio-id-p8=66' # libsoc_board_gpio_id returns unsigned, so the -1 the reader defaults an # unknown pin to reaches the caller as UINT_MAX. Asserting the value a caller # actually sees, rather than the one the doc comment implies. p libsoc/bd-gpio-id-unknown 'bd-gpio-id-unknown=4294967295' p libsoc/bd-free 'bd-free=returned' # With LIBSOC_CONF naming a file that does not exist, board_init falls through # to _probe(), which walks /usr/share/libsoc. This image installs neither, so it # must return NULL rather than a half-built board_config. Adjudicated against # what the probe measured, so an image that later ships board files reports a # skip instead of going red. bddir=$(pget iface-board-datadir) if [ "$bddir" = absent ]; then p libsoc/bd-init-absent 'bd-init-absent=null' else bst_skip libsoc/bd-init-absent \ "/usr/share/libsoc is $bddir on this image, so board_init's probe path can legitimately find a match" fi # --- debug level: asserted for real ------------------------------------------- # # Built --disable-debug these two would instead print a warning and # libsoc_get_debug() would fall off the end of a non-void function. The # round-trip is what establishes the recipe's default PACKAGECONFIG (which # leaves --enable-debug on) really took. p libsoc/dbg-enable 'dbg-set-1=1' p libsoc/dbg-disable 'dbg-set-0=0' # --- the four kernel interfaces: probed, never assumed ------------------------ # # THE ONE THING THE SHIPPED ARTIFACT CANNOT ANSWER is whether a request against # a real controller succeeds. That needs a peripheral bus, and this image has # none of the four. Each interface gets two cases: that libsoc REFUSES CLEANLY # when the interface is absent (a real assertion, and non-invasive -- there is # nothing to mutate), and the against-a-device case, which genuinely cannot run # here and is a skip carrying the probe's own evidence. # # Where an interface IS present the probe declines to call the opener: exporting # a GPIO or PWM channel this suite picked would be a side effect on somebody # else's hardware. Both branches are exercised in practice -- the build host has # /sys/class/gpio, six /dev/i2c-* nodes and /sys/class/pwm, and takes the # declined branch; the guest takes the absent branch. gpiocls=$(pget iface-gpio-class); gpioexp=$(pget iface-gpio-export) i2cn=$(pget iface-i2c-nodes); spin=$(pget iface-spi-nodes) pwmcls=$(pget iface-pwm-class); pwmchip=$(pget iface-pwm-chip0) etcconf=$(pget iface-board-etcconf) probefacts="gpio:/sys/class/gpio=$gpiocls export=$gpioexp i2c:/dev/i2c-*=$i2cn spi:/dev/spidev*=$spin pwm:/sys/class/pwm=$pwmcls pwmchip0=$pwmchip board:/usr/share/libsoc=$bddir /etc/libsoc.conf=$etcconf" # GPIO. libsoc_gpio_request opens /sys/class/gpio/export for write; with no such # node file_open fails and the request must return NULL rather than hand back a # gpio whose value_fd is -1. if [ "$gpioexp" = absent ]; then p libsoc/gpio-refuses-without-class 'gpio-request=null' else bst_skip libsoc/gpio-refuses-without-class \ "/sys/class/gpio/export exists, so the probe declined to export a pin on real hardware -- $probefacts" fi bst_skip libsoc/gpio-against-controller \ "no GPIO controller behind the virtio interface, so there is no pin to drive -- $probefacts" # I2C. libsoc_i2c_init file_valid()s /dev/i2c-N before opening it, so with no # node it must return NULL from the error label having freed its allocation. if [ "$i2cn" = 0 ]; then p libsoc/i2c-refuses-without-node 'i2c-init=null' else bst_skip libsoc/i2c-refuses-without-node \ "$i2cn /dev/i2c-* node(s) exist, so the probe declined to open a real bus -- $probefacts" fi bst_skip libsoc/i2c-against-device \ "no I2C controller behind the virtio interface, so there is no device to address -- $probefacts" # SPI. Same shape as I2C: file_valid() on /dev/spidevN.M ahead of the open. if [ "$spin" = 0 ]; then p libsoc/spi-refuses-without-node 'spi-init=null' else bst_skip libsoc/spi-refuses-without-node \ "$spin /dev/spidev* node(s) exist, so the probe declined to open a real bus -- $probefacts" fi bst_skip libsoc/spi-against-device \ "no SPI controller behind the virtio interface, so there is no device to clock -- $probefacts" # PWM. libsoc_pwm_request writes the channel number to # /sys/class/pwm/pwmchipN/export; with no chip the write fails and it returns # NULL. Driven from the probe's second translation unit -- see the enum note above. if [ "$pwmchip" = absent ]; then p libsoc/pwm-refuses-without-chip 'pwm-request=null' else bst_skip libsoc/pwm-refuses-without-chip \ "/sys/class/pwm/pwmchip0 exists, so the probe declined to export a channel on real hardware -- $probefacts" fi bst_skip libsoc/pwm-against-controller \ "no PWM controller behind the virtio interface, so there is no channel to enable -- $probefacts"