. "$BST_LIB" # nanopb 0.4.8 GENERATOR -- the code-generator half of nanopb, as meta-oe ships # it from dynamic-layers/meta-python/recipes-devtools/nanopb. The runtime half # is measured by 92-nanopb.test beside this file; the two are a pair, and this # suite is what makes the pair complete: it asserts the target can turn its own # .proto into the .pb.c/.pb.h that the runtime's descriptors are made of. # # --------------------------------------------------------------------------- # WHY A BYTE-FOR-BYTE COMPARISON IS LEGITIMATE HERE # # CODE GENERATION IS A PURE FILE TRANSFORM AND nanopb MAKES IT A DETERMINISTIC # ONE. --no-timestamp is the DEFAULT in 0.4.8 (optparser sets notimestamp=True), # so the emitted preamble carries the version string and no clock reading; there # is no path, no hostname and no ordering left to vary. Regenerating the same # corpus twice produces identical bytes, and that was checked before these # fixtures were committed rather than assumed. # # So the expected files here are a true oracle, not a convenience. They were # produced from THIS corpus by THIS generator, driven by the SAME protoc the # image ships (25.8.0) and the SAME python-protobuf it ships (4.25.8, pure # Python), on x86_64. Every byte is required to match on wasm32: nothing in the # transform reads the word size, the endianness or the ABI. A single byte of # disagreement is a defect in the generator, in python-protobuf, or in the # interpreter running them -- there is no tolerance band in this file. # # WHAT THE CORPUS IS FOR. Each construct is present because it drives a # different arm of nanopb_generator.py, not for coverage's own sake: # - scalars of every wire type (varint, zigzag, fixed32, fixed64, length- # delimited) -- the field-descriptor emitter; # - a nested message (Reading.Axis) -- the name-mangling arm; # - an IMPORTED message and an IMPORTED enum (from nanopbgen_common.proto) -- # the arm that has to resolve a symbol across files, which is the one that # fails first when protoc and the generator disagree about a descriptor; # - a oneof -- the union emitter, which produces which_source + an anonymous # union rather than plain members; # - repeated -- the count-plus-array emitter; # - a .options file bounding label/payload/host/samples -- WITHOUT which the # generator falls back to pb_callback_t for all four and the struct is a # different shape entirely. That fallback is silent, which is exactly why # the STATIC-allocation cases below are named separately from the byte # comparison: if the .options file is ever not read, the diff is enormous # and says nothing, while gen/static-label says what actually happened. # # --------------------------------------------------------------------------- # THE CASE THAT EARNS THIS SUITE ITS KEEP IS gen/import. # # generator/proto/__init__.py imported pkg_resources at module scope while using # it only inside a has_grpcio_protoc() branch. pkg_resources comes from # setuptools, which nanopb does not depend on and this image does not carry, so # the generator died at import before reading a .proto -- a failure no build # would ever catch, since nothing about the recipe compiles or links. The port # patches the import to be lazy. gen/import is the regression guard, and # gen/no-setuptools records the precondition that makes it meaningful: if # setuptools ever lands on this image the guard stops proving anything, and that # case turns it from a silent pass into a visible skip. # --------------------------------------------------------------------------- # A root prefix, empty on the target and set only when this suite is validated # NATIVELY before it ships -- which is how the expectations here were checked in # the first place, against the very .deb the port produces. The runner never sets # it, so on the image these are exactly the absolute paths. Same escape hatch, # and same reason, as 92-nanopb.test's $BST_NANOPB_ROOT. R="${BST_NANOPB_GEN_ROOT:-}" GEN=$R/usr/bin/nanopb_generator.py SITE=$R/usr/lib/python3.12/site-packages/nanopb INCDIR=$R/usr/include/nanopb if [ ! -f "$GEN" ]; then bst_skip nanopb-gen/present "nanopb-generator not installed on this image" exit 0 fi bst_ok nanopb-gen/present require python3 nanopb-gen/python3 require protoc nanopb-gen/protoc # --- the package is whole ---------------------------------------------------- check nanopb-gen/wrapper-py test -f "$GEN" check nanopb-gen/wrapper-plain test -f "$R/usr/bin/nanopb_generator" check nanopb-gen/wrapper-plugin test -f "$R/usr/bin/protoc-gen-nanopb" check nanopb-gen/mod-generator test -f "$SITE/generator/nanopb_generator.py" check nanopb-gen/mod-utils test -f "$SITE/generator/proto/_utils.py" check nanopb-gen/mod-nanopb-pb2 test -f "$SITE/generator/proto/nanopb_pb2.py" check nanopb-gen/mod-nanopb-proto test -f "$SITE/generator/proto/nanopb.proto" # The shipped nanopb_pb2.py must not look STALE against the nanopb.proto beside # it. load_nanopb_pb2() compares their mtimes and, if the .py is older, tries to # regenerate it INTO site-packages -- a write into the rootfs on every run, which # on a read-only or virtiofs root fails and falls through to a temp-dir rebuild # that costs a protoc invocation per run. Yocto normalises both to # SOURCE_DATE_EPOCH, so they are equal and the shipped file is used as-is. _p_src=$(stat -c %Y "$SITE/generator/proto/nanopb.proto" 2>/dev/null) _p_dst=$(stat -c %Y "$SITE/generator/proto/nanopb_pb2.py" 2>/dev/null) if [ -n "$_p_src" ] && [ -n "$_p_dst" ] && [ "$_p_dst" -ge "$_p_src" ]; then bst_ok nanopb-gen/pb2-not-stale else bst_notok nanopb-gen/pb2-not-stale "nanopb_pb2.py ($_p_dst) older than nanopb.proto ($_p_src); every run will try to rebuild it into $SITE" fi # --- the import path --------------------------------------------------------- # The precondition that makes gen/import meaningful. A pass here says setuptools # is genuinely absent, so the next case is testing what it claims to test. if python3 -c 'import pkg_resources' >/dev/null 2>&1; then bst_skip nanopb-gen/no-setuptools "pkg_resources is importable on this image, so nanopb-gen/import no longer proves the lazy-import fix" else bst_ok nanopb-gen/no-setuptools fi check nanopb-gen/import python3 -c 'import nanopb.generator.nanopb_generator' # google.protobuf must resolve, and must resolve to the PURE-PYTHON codec: the # upb accelerator is a CPython extension, deliberately not built for this arm. check nanopb-gen/protobuf-import python3 -c 'import google.protobuf.descriptor_pb2' check_out nanopb-gen/protobuf-pure python \ python3 -c 'from google.protobuf.internal import api_implementation as a; print(a.Type())' check nanopb-gen/no-upb-so sh -c "! ls '$R/usr/lib/python3.12/site-packages/google/_upb'/*.so >/dev/null 2>&1" check_out nanopb-gen/version nanopb-0.4.8 python3 "$GEN" --version # --- generate the corpus ----------------------------------------------------- WORK=${TMPDIR:-/tmp}/nanopb-gen.$$ mkdir -p "$WORK" cp "$BST_DATA/nanopbgen_common.proto" "$WORK/nanopbgen_common.proto" 2>/dev/null cp "$BST_DATA/nanopbgen_main.proto" "$WORK/nanopbgen_main.proto" 2>/dev/null cp "$BST_DATA/nanopbgen_main.options" "$WORK/nanopbgen_main.options" 2>/dev/null if [ ! -f "$WORK/nanopbgen_main.proto" ]; then bst_notok nanopb-gen/corpus-staged "could not stage the .proto corpus into $WORK" exit 0 fi bst_ok nanopb-gen/corpus-staged # --- IS protoc ALIVE ON THIS IMAGE? ONE LIVE PROBE DECIDES THE OTHER HALF ----- # Everything below this point reaches protoc: nanopb_generator.py has no .proto # parser of its own -- invoke_protoc() shells out to `protoc` for every file it # reads -- and the plugin path is protoc invoking US. So a protoc that cannot # run takes the whole transform with it, and the cases would otherwise report a # missing .pb.h as though the EMITTER had failed. # # protoc is blocked by brintos/hwjs-cc#82: the coroutinize pass's CTOR-DRIVE # lowering drives a coroutinized .init_array constructor once and then executes # a bare `unreachable`, so protoc traps in __wasm_call_ctors before reaching # main and every invocation -- `protoc --version` included -- dies with SIGILL # having printed nothing. 83-protobuf.test carries the same probe over the same # defect, and measured the same 132 there. # # Guarded on a LIVE run rather than pinned to "expected red": when #82 lands # this branch stops being taken and the byte-exact comparison below runs with no # edit to this file. Deliberately not bst_execfail -- the binary stages and # starts; it is neither missing nor an ENOMEM staging refusal, and labelling it # UNRUN-* would attribute it to the wrong platform mechanism. _pv=$(protoc --version 2>&1); _prc=$? PROTOC_OK=1 PROTOC_WHY= if [ "$_prc" -ne 0 ]; then PROTOC_OK=0 PROTOC_WHY="blocked by brintos/hwjs-cc#82 -- protoc traps before main (rc=$_prc$([ -n "$_pv" ] && echo ", said: $(_bst_1line "$_pv")")); nanopb_generator.py reads every .proto THROUGH protoc, so the transform never runs. Not a nanopb defect: gen/import, gen/protobuf-import and gen/version above all pass, which is the generator and its protobuf binding loading and answering" printf 'BST-DIAG nanopb-gen/protoc-blocked rc=%s size=%s bytes (hwjs-cc#82)\n' \ "$_prc" "$(wc -c < $R/usr/bin/protoc 2>/dev/null)" fi if [ "$PROTOC_OK" = 0 ]; then for _c in run emit-nanopbgen_common.pb.h emit-nanopbgen_common.pb.c \ emit-nanopbgen_main.pb.h emit-nanopbgen_main.pb.c \ nested import-msg import-enum oneof repeated \ static-label static-payload static-host static-samples alloc-static do bst_skip "nanopb-gen/$_c" "$PROTOC_WHY" done H=$WORK/nanopbgen_main.pb.h C=$WORK/nanopbgen_main.pb.c else _gout=$(cd "$WORK" && python3 "$GEN" nanopbgen_common.proto nanopbgen_main.proto 2>&1); _grc=$? if [ "$_grc" -eq 0 ]; then bst_ok nanopb-gen/run elif bst_execfail "$_gout"; then bst_unrun nanopb-gen/run "$_gout" else bst_notok nanopb-gen/run "rc=$_grc $(_bst_1line "$_gout")"; fi # --- byte for byte against the oracle ---------------------------------------- for f in nanopbgen_common.pb.h nanopbgen_common.pb.c nanopbgen_main.pb.h nanopbgen_main.pb.c; do if [ ! -f "$WORK/$f" ]; then bst_notok "nanopb-gen/emit-$f" "the generator produced no $f" elif cmp -s "$WORK/$f" "$BST_DATA/$f.expected"; then bst_ok "nanopb-gen/emit-$f" else bst_notok "nanopb-gen/emit-$f" "differs from the committed oracle: $(cd "$WORK" && diff "$f" "$BST_DATA/$f.expected" 2>&1 | head -3 | tr '\n' ' ')" fi done # --- the constructs, named individually -------------------------------------- # Each of these is implied by the byte comparison above and is asserted anyway: # a diff of a 4.5 KB header says "it changed", these say WHAT changed, which is # the difference between a red anyone can act on and a red nobody can. H=$WORK/nanopbgen_main.pb.h C=$WORK/nanopbgen_main.pb.c check nanopb-gen/nested grep -q 'bstgen_Reading_Axis' "$H" check nanopb-gen/import-msg grep -q 'bstgen_Stamp stamp' "$H" check nanopb-gen/import-enum grep -q 'bstgen_Severity level' "$H" check nanopb-gen/oneof grep -q 'which_source' "$H" check nanopb-gen/repeated grep -q 'samples_count' "$H" # The four .options bounds. A miss here means the .options file was not read and # the fields became pb_callback_t -- see the header comment. check nanopb-gen/static-label grep -q 'char label\[16\]' "$H" check nanopb-gen/static-payload grep -q 'PB_BYTES_ARRAY_T(24)' "$H" check nanopb-gen/static-host grep -q 'char host\[12\]' "$H" check nanopb-gen/static-samples grep -q 'uint32_t samples\[8\]' "$H" # ...and NO field in this corpus may be allocated as a callback. With the # .options file read, every one of the thirteen is STATIC; drop the file and # label/payload/samples/host each become # X(a, CALLBACK, SINGULAR, STRING, label, 2) # The anchor is the `X(a, CALLBACK,` line form, not the bare word: the emitter # also writes `#define bstgen_Reading_CALLBACK pb_default_field_callback` # unconditionally, so a match on CALLBACK alone reds a correct header -- and a # pattern that expects the TYPE straight after `CALLBACK,` never matches at all, # because the rule (SINGULAR/REPEATED/ONEOF) sits between them. This case was # written that second way first and passed the no-options mutation arm it exists # to catch. check nanopb-gen/alloc-static sh -c "! grep -qE '^X\\(a, *CALLBACK,' '$H'" fi # --- agreement with the runtime headers this image ships --------------------- # The generated code is only useful if the runtime beside it defines what the # code names. This walks the nanopb identifiers the emitted files actually use # and asserts each is defined in /usr/include/nanopb -- which is what catches a # generator and a runtime that have drifted to different nanopb versions, the # one failure this pair is uniquely exposed to. if [ ! -d "$INCDIR" ]; then bst_skip nanopb-gen/runtime-agree "nanopb-runtime-dev headers absent; nothing to agree with" else _miss= for sym in PB_BIND pb_msgdesc_t pb_field_iter_t PB_LTYPE_UVARINT PB_LTYPE_SVARINT \ PB_LTYPE_FIXED32 PB_LTYPE_FIXED64 PB_LTYPE_STRING PB_LTYPE_BYTES \ PB_LTYPE_SUBMESSAGE PB_BYTES_ARRAY_T pb_size_t pb_byte_t; do grep -rq "$sym" "$INCDIR" 2>/dev/null || _miss="$_miss $sym" done if [ -z "$_miss" ]; then bst_ok nanopb-gen/runtime-agree else bst_notok nanopb-gen/runtime-agree "generated code names nanopb symbols the installed headers do not define:$_miss"; fi # The generated .pb.c's one include must be the header the runtime installs. if [ "$PROTOC_OK" = 0 ]; then bst_skip nanopb-gen/include-path "$PROTOC_WHY" else check nanopb-gen/include-path grep -q '#include "nanopbgen_main.pb.h"' "$C" fi check nanopb-gen/pb-h-present test -f "$INCDIR/pb.h" fi # --- the plugin entry point -------------------------------------------------- # protoc-gen-nanopb is copied verbatim from the source tree, where # `from nanopb_generator import *` means the module beside it. Installed, that # resolves to /usr/bin/nanopb_generator.py -- the CONFIGURED wrapper in the same # bindir -- which re-exports main_cli/main_plugin. It therefore works by a # coincidence of layout rather than by design, which is precisely why it is # asserted rather than assumed: a future recipe that stops installing the # configured wrapper into ${bindir} would break `protoc --nanopb_out=` silently. if [ "$PROTOC_OK" = 0 ]; then bst_skip nanopb-gen/plugin "$PROTOC_WHY" bst_skip nanopb-gen/plugin-agrees "$PROTOC_WHY" else _pout=$(cd "$WORK" && rm -f plugin_out.pb.h plugin_out.pb.c 2>/dev/null; mkdir -p plug && \ protoc --plugin=protoc-gen-nanopb=$R/usr/bin/protoc-gen-nanopb \ --nanopb_out=plug -I. nanopbgen_common.proto nanopbgen_main.proto 2>&1); _prc=$? if [ "$_prc" -eq 0 ] && [ -f "$WORK/plug/nanopbgen_main.pb.h" ]; then bst_ok nanopb-gen/plugin elif bst_execfail "$_pout"; then bst_unrun nanopb-gen/plugin "$_pout" else bst_notok nanopb-gen/plugin "rc=$_prc $(_bst_1line "$_pout")"; fi # The plugin path and the CLI path must agree byte for byte -- they are the same # emitter reached two ways, so a difference is a defect in the wrapper layout # rather than a legitimate variation. if [ -f "$WORK/plug/nanopbgen_main.pb.h" ] && [ -f "$WORK/nanopbgen_main.pb.h" ]; then if cmp -s "$WORK/plug/nanopbgen_main.pb.h" "$WORK/nanopbgen_main.pb.h"; then bst_ok nanopb-gen/plugin-agrees else bst_notok nanopb-gen/plugin-agrees "protoc --nanopb_out= and the CLI emitted different headers" fi else bst_skip nanopb-gen/plugin-agrees "one of the two headers was not produced" fi fi # --- a malformed .proto is refused ------------------------------------------- # Measured off-target with the same generator and protoc: protoc reports # `Missing field number` and nanopb_generator.py exits 1 with no file written. # The second half matters more than the status: a generator that emitted a # truncated .pb.c on a parse error would hand a consumer code that compiles and # is wrong. BAD=$WORK/bad mkdir -p "$BAD" cp "$BST_DATA/nanopbgen_bad.proto" "$BAD/nanopbgen_bad.proto" 2>/dev/null if [ -f "$BAD/nanopbgen_bad.proto" ]; then bst_ok nanopb-gen/bad-staged if [ "$PROTOC_OK" = 0 ]; then bst_skip nanopb-gen/bad-rc "$PROTOC_WHY" bst_skip nanopb-gen/bad-no-output "$PROTOC_WHY" else _bout=$(cd "$BAD" && python3 "$GEN" nanopbgen_bad.proto 2>&1); _brc=$? check_eq nanopb-gen/bad-rc 1 "$_brc" check nanopb-gen/bad-no-output sh -c "! test -f '$BAD/nanopbgen_bad.pb.c'" fi else bst_skip nanopb-gen/bad-staged "nanopbgen_bad.proto absent from $BST_DATA" bst_skip nanopb-gen/bad-rc "fixture absent" bst_skip nanopb-gen/bad-no-output "fixture absent" fi rm -rf "$WORK" 2>/dev/null