. "$BST_LIB" # BST-TIMEOUT: 300 # five execs of small wasm modules (K9 adds ~10 ms of sleeps); the runner's own default # # of 60 is enough on an idle guest and not on a loaded one, # # and 300 is what the ticket gates and the nightly pass, so # # this marker costs them nothing. # # libffi 3.4.6 (poky meta/recipes-support) on wasm32-hwjs -- ffi_call and # closures WORK, each within a generated set of signatures, and this suite keeps # both statements true. # # WHAT IS SHIPPED. libffi's machine-dependent layer is this port's own: # upstream's wasm32 backend is Emscripten-only (it reaches an indirect call # through the JS runtime's dynCall and builds a closure by compiling a fresh # module and growing the function table). Since BRINTOS-2269 ffi_call reaches # its target through a generated table of statically typed trampolines, one per # wasm signature, and a signature outside the table is refused by ffi_prep_cif # with a status. Since BRINTOS-2663 a closure's callable is a slot function # from a generated pool keyed by wasm signature, bound by ffi_prep_closure_loc # and handed back by ffi_closure_free; outside the pool, or with every slot of # a signature bound, prep refuses with a status. The guest cannot make a new # function-table entry at run time (linux-in-the-browser#180), so a pool of # existing ones is what a closure gets. Everything else is upstream's core. # # SO THERE ARE THREE KINDS OF CASE HERE. # # 1. The core must answer CORRECTLY. A library that packaged the right # symbols and laid structures out wrongly would satisfy every consumer's # configure and corrupt every consumer's data, so the layout arithmetic is # pinned hardest. # # 2. ffi_call must return the RIGHT VALUE through every lowering the backend # performs -- small integers, i64, f32, f64, long double, a struct by value # and by hidden-pointer return, a single-member struct, the variadic buffer, # a callee in libc.so that parks in the kernel mid-call. Each is a literal # the x86-64 control also produces. # # 3. A closure must return the RIGHT VALUE whichever way it is reached -- # directly, through ffi_call, and from inside libc.so (qsort), including a # closure that parks in the kernel with libc.so's frame beneath it -- and # its pool must hold the documented number and give slots back on free. # # EVERY EXPECTED VALUE IS PINNED FROM AN x86-64 CONTROL, not guessed: the same # libffi 3.4.6 tarball, configured with this recipe's own switches # (--disable-builddir --disable-exec-static-tramp, static), built natively with # gcc 13.3.0 against glibc 2.39, and driven through the SAME probe source on # 2026-08-24. Where a case says "control", that is what it means. # # THREE THINGS DIFFER FROM THE CONTROL AND EACH IS A PROPERTY OF THE ARM, not a # defect. They are asserted at their wasm32 values with the control's recorded # beside them, because an unexplained difference is how a real regression gets # read as "oh, that one is just different": # # - pointer size, and ffi_type_pointer with it: 4 here, 8 on x86-64. # - FFI_DEFAULT_ABI: FFI_WASM32 (1) here, FFI_UNIX64 (2) on x86-64 -- two # unrelated enums, so only the wasm32 value means anything below. # - FFI_TYPE_COMPLEX: fatal here and accepted there. x86-64 has a complex # lowering; this arm has none, and upstream's own prep_cif.c answers that # with a bare abort() -- guarded on FFI_TARGET_HAS_COMPLEX_TYPE, which # wasm32 does not define -- BEFORE this port's machdep is reached, so # there is no status for a caller to read and no message on the way out. # It is therefore a red witness (`abort-complex` below) rather than a # status case, and the probe does not prepare one: doing so killed the # probe and took its other thirty cases with it, which is how this was # found. # # Two families are absent from ffi.h on this arm entirely, and an x86-64 header # declares both -- which is why the ABI manifest below is shorter than a native # one and why neither shows up as a missing symbol: # # - ffi_type_complex_float/_double/_longdouble, gated on # FFI_TARGET_HAS_COMPLEX_TYPE, which wasm32 does not define; # - ffi_call_go and ffi_prep_go_closure, gated on FFI_GO_CLOSURES, which # wasm32/ffitarget.h leaves commented out. LX=/usr/libexec/libffi-tests ABI=$LX/ffi-abi.txt # --------------------------------------------------------- the deliverable --- # Named by path rather than probed with `require`: libffi ships no program, so # there is no tool whose absence could produce a skip, and an image that failed # to install the archive must go RED rather than quiet. check libffi/archive test -f /usr/lib/libffi.a check libffi/header test -f /usr/include/ffi.h check libffi/ffitarget test -f /usr/include/ffitarget.h check libffi/pkgconfig test -f /usr/lib/pkgconfig/libffi.pc # THE VERSION A CONSUMER CONFIGURES AGAINST. glib's meson asks for # `dependency('libffi', version: '>=3.0.0')` and every other consumer asks # something similar, so the number pkg-config reports is load-bearing: a .pc # that reported nothing, or reported the wrong version, fails those configures # with a message about libffi being missing rather than about the version. if command -v pkg-config >/dev/null 2>&1; then check_eq libffi/modversion 3.4.6 "$(pkg-config --modversion libffi 2>&1)" # -lffi must be advertised AND must resolve; the resolving half is # 99-pkgconfig-link-surface.test's whole subject, so this only pins the name. check_out libffi/pc-libs "-lffi" pkg-config --libs libffi # A consumer asks pkg-config where the header is. Here the answer is the # compiler's own default include directory, so pkg-config strips it and # --cflags is legitimately EMPTY; "include" was the x86-64 control's answer, # whose prefix is not /usr. Pin what makes the empty answer correct, so a # .pc that lost its Cflags line -- or points somewhere the compiler does not # search -- is still a red. check_eq libffi/pc-includedir /usr/include "$(pkg-config --variable=includedir libffi 2>&1)" check_eq libffi/pc-cflags "" "$(pkg-config --cflags libffi 2>&1)" else bst_skip libffi/modversion "pkg-config not installed on this image" bst_skip libffi/pc-libs "pkg-config not installed on this image" bst_skip libffi/pc-cflags "pkg-config not installed on this image" bst_skip libffi/pc-includedir "pkg-config not installed on this image" fi # ------------------------------------------------------------ ABI surface --- # ffi.h declares 33 names on this arm and libffi.a defines every one of them -- # the recipe proves that with llvm-nm at build time and fails the build if one # is missing, which is a stronger place for that check than here. # # What this suite can add is the SECOND fact, the one the archive cannot show: # four of those names are recorded NOT LINKABLE. Whether a program reaching # the raw-closure entry points links depends on the rest of the program: the # coroutinize pass lowers ffi_translate_args' alloca in a small program and # refuses it as an opaque escape in the generated all-names one (hwjs-cc#15). # ffi_closure_alloc, a fifth until closures.c took its heap branch on wasm, # links. # # The manifest and the shipped libffi-abi-check come from the same generator run # but are different artifacts, so comparing their counts catches a packaging # step that shipped a stale one. check libffi/abi-manifest test -f "$ABI" if [ ! -f "$ABI" ]; then bst_notok libffi/abi-linkable "no manifest at $ABI" bst_notok libffi/abi-unlinkable "no manifest at $ABI" elif [ ! -x $LX/libffi-abi-check ]; then bst_notok libffi/abi-linkable "$LX/libffi-abi-check is not installed" bst_notok libffi/abi-unlinkable "no check program to compare the manifest with" else want=$(awk '$3=="linkable"' "$ABI" | wc -l | tr -d ' ') aout=$($LX/libffi-abi-check 2>&1); arc=$? if bst_execfail "$aout"; then bst_unrun libffi/abi-linkable "$aout" else afield() { printf '%s\n' "$aout" | awk -v t="$1" '$1==t {print $2; exit}'; } check_eq libffi/abi-linkable "$want" "$(afield R0-abi-linkable)" check_eq libffi/abi-nonnull "$want" "$(afield R0-abi-nonnull)" check_eq libffi/abi-rc 0 "$arc" check_eq libffi/abi-unlinkable-count 4 "$(afield R0-abi-unlinkable)" fi # THE UNLINKABLE SET IS PINNED BY NAME, not merely counted. check_eq libffi/abi-unlinkable \ 'ffi_prep_java_raw_closure ffi_prep_java_raw_closure_loc ffi_prep_raw_closure ffi_prep_raw_closure_loc' \ "$(awk '$3=="unlinkable"{printf "%s%s", s, $1; s=" "} END{print ""}' "$ABI" | tr ' ' '\n' | sort | tr '\n' ' ' | sed 's/ $//')" fi # ---------------------------------------------------------------- the core --- # libffi-probe drives the portable half in process. Absent probe is a RED, not # a skip: it ships from the same recipe as the archive, so its absence means the # port did not install what it built. if [ ! -x $LX/libffi-probe ]; then bst_notok libffi/probe-present "$LX/libffi-probe is not installed -- libffi-tests missing from the image" else pout=$($LX/libffi-probe 2>&1); prc=$? if bst_execfail "$pout"; then bst_unrun libffi/probe-run "$pout" else field() { printf '%s\n' "$pout" | awk -v t="$1" '$1==t {print $2; exit}'; } line() { printf '%s\n' "$pout" | awk -v t="$1" '$1==t {$1=""; sub(/^ /,""); print; exit}'; } check_eq libffi/probe-rc 0 "$prc" case "$pout" in *ALL-LIBFFI-OK*) bst_ok libffi/probe-complete ;; *) bst_notok libffi/probe-complete "no ALL-LIBFFI-OK: $(_bst_1line "$pout")" ;; esac # --- the arm's own constants ------------------------------------------- # FFI_DEFAULT_ABI is FFI_WASM32 (1). This is the whole ticket in one # number: ffitarget.h describes FFI_WASM32 as "raw, no structures, varargs, # or closures (not implemented!)" and makes it the default whenever # __EMSCRIPTEN__ is undefined, which here it always is. The control reads 2 # (FFI_UNIX64) from an unrelated enum. check_eq libffi/default-abi 1 "$(field R0-default-abi)" # 0..3: FFI_FIRST_ABI, FFI_WASM32, FFI_WASM32_EMSCRIPTEN, FFI_LAST_ABI. check_eq libffi/abi-range '0..3' "$(field R0-abi-range)" check_eq libffi/sizeof-ptr 4 "$(field R0-sizeof-ptr)" # control: 8 check_eq libffi/closures 1 "$(field R0-closures)" # --- the exported type descriptors ------------------------------------- # Three fields each -- size/alignment/type-code -- and they are the entire # input to the layout algorithm, so a wrong one here is a wrong answer # everywhere downstream. Every fixed-width entry matches the control # exactly; only `pointer` differs, and it differs by the word size. for t in void:1/1/0 uint8:1/1/5 sint8:1/1/6 uint16:2/2/7 sint16:2/2/8 \ uint32:4/4/9 sint32:4/4/10 uint64:8/8/11 sint64:8/8/12 \ float:4/4/2 double:8/8/3 longdouble:16/16/4; do n=${t%%:*}; want=${t#*:} check_eq "libffi/type-$n" "$want" \ "$(printf '%s\n' "$pout" | awk -v n="$n" '$1=="R1-type" && $2==n{print $3}')" done # pointer is 4/4/14 here and 8/8/14 on the control -- the word size, and the # only descriptor difference between the two arms check_eq libffi/type-pointer '4/4/14' \ "$(printf '%s\n' "$pout" | awk '$1=="R1-type" && $2=="pointer"{print $3}')" # --- ffi_prep_cif: the call every consumer makes ------------------------ # 0 is FFI_OK. A backend that refused FFI_DEFAULT_ABI would fail here and # break every consumer at configure time rather than at call time, which is # the failure this port exists to avoid. check_eq libffi/prep-status 0 "$(field R2-prep-status)" check_eq libffi/prep-nargs 3 "$(field R2-nargs)" check_eq libffi/prep-rtype 11 "$(field R2-rtype)" # nfixedargs is the wasm32 arm's FFI_EXTRA_CIF_FIELDS member; the control # has no such field and prints n/a. A backend that left it unset would hand # the caller whatever the stack held. check_eq libffi/prep-nfixedargs 3 "$(field R2-nfixedargs)" check_eq libffi/prep-void-status 0 "$(field R2b-void-status)" check_eq libffi/prep-void-nfixedargs 0 "$(field R2b-nfixedargs)" # --- ffi_prep_cif_var ---------------------------------------------------- # With FFI_TARGET_SPECIFIC_VARIADIC, prep_cif.c returns # ffi_prep_cif_machdep_var's status and never calls ffi_prep_cif_machdep, # so the backend's variadic hook must both keep nfixedargs and choose the # call trampoline itself -- the second half is what call-variadic and # call-variadic-longdouble below exercise. check_eq libffi/var-status 0 "$(field R3-var-status)" check_eq libffi/var-nargs 4 "$(field R3-nargs)" check_eq libffi/var-nfixedargs 2 "$(field R3-nfixedargs)" # --- struct layout: the portable core's real work ----------------------- # {uint8, uint32, uint8, double}. Padding comes from each member's own # alignment, so this is arch-invariant for fixed-width members and MUST # equal the control, which read the same. check_eq libffi/offsets-status 0 "$(field R4-offsets-status)" check_eq libffi/offsets '0|4|8|16 size=24 align=8' "$(line R4-offsets)" # all-one-byte members: no padding anywhere, and the case where an # implementation that always rounded up to the word would show check_eq libffi/offsets-packed '0|1|2 size=3 align=1' "$(line R4b-offsets)" # NESTED: the inner struct's COMPUTED alignment, not its first member's, # decides where it lands. An implementation that flattened the element list # would put the third member at 24 by a different route and still pass # everything above; it could not reproduce inner=16/8. check_eq libffi/offsets-nested '0|8|24 size=32 align=8 inner=16/8' "$(line R5-offsets)" check_eq libffi/struct-return-status 0 "$(field R5b-struct-return-status)" check_eq libffi/struct-return-size '8 align=4' "$(line R5b-struct-size)" # --- the refusals ------------------------------------------------------- # A backend is only predictable if it says no in the same places a real backend # would. 2 is FFI_BAD_ABI, 1 is FFI_BAD_TYPEDEF. # # The ABI bounds are refused by the PORTABLE CORE before this port sees # them, and read the same on the control. check_eq libffi/reject-abi-first 2 "$(field R6-abi-first)" check_eq libffi/reject-abi-last 2 "$(field R6-abi-last)" check_eq libffi/reject-abi-bogus 2 "$(field R6-abi-bogus)" # FFI_TYPE_COMPLEX is not prepared from inside the probe on this arm: it # is fatal here (see the header), so the probe prints the reason in the # place the control prints a status, and `abort-complex` below is what # actually witnesses the death. Asserted rather than allowed to vanish -- # an absent pair would read the same as a probe that lost them. check_eq libffi/complex-rtype-unprobed 'n/a-aborts-upstream' "$(field R6-complex-rtype)" check_eq libffi/complex-arg-unprobed 'n/a-aborts-upstream' "$(field R6-complex-arg)" # --- the closure allocator -------------------------------------------- # Links and allocates. code=data is closures.c's heap branch answering: # the "code" pointer is the allocation itself, not a callable table index, # which is why ffi_prep_closure_loc must refuse rather than pretend. The # x86-64 control answers code=distinct (its mmap-exec alias). check_eq libffi/closure-alloc 'nonnull code=data' "$(line R7-closure-alloc)" # --- the raw API -------------------------------------------------------- # No consumer on this image today; asserted because it is PACKAGED, and a # symbol that ships is a symbol someone can call. The round trip exercises # the same word-size arithmetic ffi_raw_size reports, so a wrong # FFI_SIZEOF_ARG shows up as a corrupted value rather than only as a wrong # total. check_eq libffi/raw-roundtrip 'deadbeef|-2.5|ptr-ok' "$(line R9-roundtrip)" fi fi # ---------------------------------------------------------------- ffi_call --- # libffi-call drives ffi_call through every lowering the backend performs and # prints each result. Every literal below is what the same source prints on the # x86-64 control (BRINTOS-2269), except C12: a signature outside the trampoline # table, which this backend refuses at prep with FFI_BAD_TYPEDEF (1) and which # the control prints the same value for by construction. C13 asserts only that # nanosleep in libc.so returned 0 through ffi_call -- the elapsed time is not # gated, because sleeps return early on this platform (brintos/linux#48). if [ ! -x $LX/libffi-call ]; then bst_notok libffi/call-present "$LX/libffi-call is not installed -- libffi-tests missing from the image" else bst_ok libffi/call-present kout=$($LX/libffi-call 2>&1); krc=$? if bst_execfail "$kout"; then bst_unrun libffi/call-run "$kout" else kline() { printf '%s\n' "$kout" | awk -v t="$1" '$1==t {$1=""; sub(/^ /,""); print; exit}'; } check_eq libffi/call-rc 0 "$krc" check_eq libffi/call-prep 0 "$(kline C1-prep)" check_eq libffi/call-uint32 42 "$(kline C1-uint32)" check_eq libffi/call-ret-sint8 -5 "$(kline C2-sint8)" check_eq libffi/call-ret-uint8 250 "$(kline C2-uint8)" check_eq libffi/call-ret-sint16 -300 "$(kline C2-sint16)" check_eq libffi/call-ret-uint16 65000 "$(kline C2-uint16)" check_eq libffi/call-int64 4294967289 "$(kline C3-int64)" check_eq libffi/call-float-double 3.7500 "$(kline C4-double)" check_eq libffi/call-ret-float 2.5000 "$(kline C5-float)" check_eq libffi/call-longdouble 3.7500 "$(kline C6-longdouble)" check_eq libffi/call-struct-arg 60 "$(kline C7-struct-arg)" check_eq libffi/call-struct-ret '2|3|1' "$(kline C7-struct-ret)" check_eq libffi/call-single-member 2.5000 "$(kline C8-wrapped)" check_eq libffi/call-var-prep 0 "$(kline C9-prep)" check_eq libffi/call-variadic '-3|1234567890123|0.50 rc=21' "$(kline C9-variadic)" check_eq libffi/call-sum16 136 "$(kline C10-sum16)" check_eq libffi/call-mixed8 32.5000 "$(kline C11-mixed8)" check_eq libffi/call-uncovered-prep 1 "$(kline C12-uncovered-prep)" # A long double in the variadic buffer sits at 16-byte alignment, as # clang's va_arg reads it. check_eq libffi/call-variadic-longdouble '7|2.125' "$(kline C14-variadic-longdouble)" check_eq libffi/call-two-f64-of-eleven 49 "$(kline C15-two-f64-of-eleven)" case "$(kline C13-nanosleep)" in 'rc=0 '*) bst_ok libffi/call-parks-in-libc ;; *) bst_notok libffi/call-parks-in-libc "want rc=0, got: $(kline C13-nanosleep)" ;; esac # Every cif the probe prepares must be accepted; a refusal prints # `FAIL P-prep` and skips that call rather than aborting the probe. case "$kout" in *CALL-FAILURES*) check_eq libffi/call-no-refused-prep "" "$(printf '%s\n' "$kout" | grep -E '^FAIL .*-prep')" ;; *) bst_notok libffi/call-no-refused-prep "the probe did not reach its summary line" ;; esac case "$kout" in *ALL-LIBFFI-CALL-OK*) bst_ok libffi/call-complete ;; *) bst_notok libffi/call-complete "$(printf '%s\n' "$kout" | grep -E '^FAIL|CALL-FAILURES' | tr '\n' ' ')" ;; esac fi fi # --------------------------------------------------------------- closures --- # libffi-closure binds closures and calls them. Every literal is what the same # source prints against upstream libffi on the x86-64 control (BRINTOS-2663), # except K11/K12, K13's address, K14, K15 and K16's kept callable, which measure this backend's pool -- 16 slots # for an all-i32 signature, then FFI_BAD_TYPEDEF (1); double(double, double, # double) outside the pool, refused at prep with 1; and a re-prep into a full # pool refused while the closure keeps its old callable -- and which the # control prints by construction. K13 rebinds at a NEW address (a spare # allocation takes the freed block) and asserts the address differs, so it # passes only if ffi_closure_free released the slot. K16: with a slot freed # below it, a re-prep to the same signature keeps its callable rather than # moving to the free slot, as upstream keeps it. K8 and K9 are the consumer's shape: qsort in libc.so # calls the closure, and in K9 every comparison parks in nanosleep with qsort's # frame beneath it. if [ ! -x $LX/libffi-closure ]; then bst_notok libffi/closure-present "$LX/libffi-closure is not installed -- libffi-tests missing from the image" else bst_ok libffi/closure-present kout=$($LX/libffi-closure 2>&1); krc=$? if bst_execfail "$kout"; then bst_unrun libffi/closure-run "$kout" else kline() { printf '%s\n' "$kout" | awk -v t="$1" '$1==t {$1=""; sub(/^ /,""); print; exit}'; } check_eq libffi/closure-rc 0 "$krc" check_eq libffi/closure-prep 0 "$(kline K1-prep)" check_eq libffi/closure-direct 107 "$(kline K2-direct)" check_eq libffi/closure-ffi-call 98 "$(kline K3-ffi-call)" check_eq libffi/closure-ret-short -300 "$(kline K4-short)" check_eq libffi/closure-double 2.5000 "$(kline K5-double)" check_eq libffi/closure-float 3.2500 "$(kline K6-float)" check_eq libffi/closure-int64 -3703703670369 "$(kline K7-int64)" check_eq libffi/closure-qsort '-7,-7,0,3,8,19,42,1000' "$(kline K8-qsort)" check_eq libffi/closure-qsort-called yes "$(kline K8-compared)" check_eq libffi/closure-qsort-parks '1,2,3,4,5' "$(kline K9-qsort-parks)" check_eq libffi/closure-struct-arg 456 "$(kline K10-struct-arg)" check_eq libffi/closure-pool-vi 16 "$(kline K11-pool-vi)" check_eq libffi/closure-pool-called 136 "$(kline K11-called-sum)" check_eq libffi/closure-pool-refused 1 "$(kline K12-exhausted-status)" check_eq libffi/closure-rebind 0 "$(kline K13-rebind-after-free)" check_eq libffi/closure-rebind-new-address yes "$(kline K13-distinct-address)" check_eq libffi/closure-reprep-refused 1 "$(kline K15-refused-reprep)" check_eq libffi/closure-reprep-kept -7 "$(kline K15-old-callable)" check_eq libffi/closure-same-sig-reprep 0 "$(kline K16-same-sig-reprep)" check_eq libffi/closure-callable-kept yes "$(kline K16-callable-kept)" check_eq libffi/closure-uncovered 1 "$(kline K14-uncovered-prep)" case "$kout" in *ALL-LIBFFI-CLOSURE-OK*) bst_ok libffi/closure-complete ;; *) bst_notok libffi/closure-complete "$(printf '%s\n' "$kout" | grep -E '^FAIL|CLOSURE-FAILURES' | tr '\n' ' ')" ;; esac fi fi # ------------------------------------------------------- THE RED WITNESS --- # It asserts the absence of a message. This death is upstream's, not this # port's: prep_cif.c answers FFI_TYPE_COMPLEX with a bare abort() on an arm # without FFI_TARGET_HAS_COMPLEX_TYPE, before this port's machdep runs. So the # case that would assert a diagnostic asserts SILENCE instead -- a consumer # carrying a complex descriptor over from another target gets no status and no # line, and that is the fact worth pinning. The day this arm gains complex # support the program stops linking (its own #error) and the probe's guard # restores the status cases. if [ ! -x $LX/libffi-abort-complex ]; then bst_notok libffi/abort-complex-present "$LX/libffi-abort-complex is not installed" bst_notok libffi/abort-complex-staged "no program to run" bst_notok libffi/abort-complex-died "no program to run" bst_notok libffi/abort-complex-silent "no program to run" bst_notok libffi/abort-complex-rc "no program to run" else bst_ok libffi/abort-complex-present cout=$($LX/libffi-abort-complex 2>&1); crc=$? if bst_execfail "$cout"; then bst_unrun libffi/abort-complex-staged "$cout" bst_unrun libffi/abort-complex-died "$cout" bst_unrun libffi/abort-complex-silent "$cout" bst_unrun libffi/abort-complex-rc "$cout" else case "$cout" in *PREP-COMPLEX-STAGED*) bst_ok libffi/abort-complex-staged ;; *) bst_notok libffi/abort-complex-staged "never reached the cif: $(_bst_1line "$cout")" ;; esac # THE RED WITNESS: ffi_prep_cif must never return for a complex type here case "$cout" in *RETURNED-FROM-PREP-CIF-COMPLEX*) bst_notok libffi/abort-complex-died \ "ffi_prep_cif RETURNED for a complex type -- complex has an answer on this arm now, so retire the witness, the probe guard and this block together: $(_bst_1line "$cout")" ;; *) bst_ok libffi/abort-complex-died ;; esac # ...and it says nothing on the way out, unlike this port's own aborts case "$cout" in *libffi-wasm32-hwjs*) bst_notok libffi/abort-complex-silent \ "a diagnostic appeared -- this death is upstream's bare abort() and carries none, so something else aborted: $(_bst_1line "$cout")" ;; *) bst_ok libffi/abort-complex-silent ;; esac if [ "$crc" -ne 0 ]; then bst_ok libffi/abort-complex-rc else bst_notok libffi/abort-complex-rc "exited 0 -- a caller would read that as success" fi fi fi