Cross-build verification & failure-class catalog#
This document catalogs the classes of failure hit during the base→:latest-cross
rebuild campaigns and maps each to the fast check that catches it before a
multi-hour QEMU build. It is the reference for the pre-flight verification
workflow (see “Pre-flight” below).
The guiding principle: every error we debugged interactively should become a check that fails in seconds, not after a 30–60 min emulated build.
Failure classes (from build history)#
# |
Class |
Representative bug(s) |
Fix commit(s) |
Caught early by |
|---|---|---|---|---|
1 |
Script not COPY’d into a stage → sourced fn missing at runtime ( |
|
|
sourced-scripts-present static check ( |
2 |
Relocated native GCC/G++ can’t find |
|
|
compile smoke test (C + C++ |
3 |
Missing dev headers for QEMU source builds |
|
|
same compile smoke test (header presence probe) |
4 |
Cross toolchain artifact wrong-arch / not runnable on host |
|
|
|
5 |
venv/wheel install collision & bad seeding |
apt numpy seeded into venv without dist-info → uv install |
|
torch-venv integrity smoke ( |
6 |
Undefined/typo’d bash function or quoting bug |
|
(dedup passes) |
shellcheck gate ( |
7 |
Include-flag construction bugs |
bare |
|
shellcheck + compile smoke test |
8 |
Dockerfile ARG / mirror / cache drift |
apt not |
|
|
Existing infrastructure (reuse, don’t duplicate)#
Shared helper for class 2/3:
01-core/common.sh→append_cross_idirafter <triplet>already appends-idirafter /usr/include{,/<triplet>}toCPPFLAGS/CFLAGS/CXXFLAGS. Used bybuild-libcamera.sh,build-gstreamer-monorepo.sh. (The torch-venv fix should adopt this — see task #16.)Compiler validation (class 4):
06-packaging/validate-compilers.shemitsARTIFACT COMPILER VERIFICATION PASSED for <arch>; validates GCC 16.2.0 + Clang 22.1.8 chain and per-arch ELF machine type. Extend here for the compile smoke test.Smoke framework:
06-packaging/smoke-common.sh(pass/fail/FAILURES); smoke tests are06-packaging/smoke-<thing>.shandsource smoke-common.sh.Static host verifiers wired into
.githooks/pre-commit:verify-critical-fixes.sh,01-core/verify-arg-consistency.sh,sync_versions.py --check,bash -n. The hook is the home for the new shellcheck gate and the sourced-scripts-present check.
Pre-flight#
Run linux/scripts/preflight.sh before build-cross-chain.sh. It runs every
fast (no-build) check in seconds/minutes so whole failure classes are caught
before a multi-hour QEMU rebuild. All checks run even if one fails; the script
exits non-zero if any did.
Since 2026-08-08 preflight also validates the stage graph itself (slug
stage-graph — parent refs, dockerfile existence, tag resolution, cycles);
previously that ran only at build kickoff. The script-tests slug now prints an
assertion aggregate (“11 suites, 120 assertions”) — a sudden drop in that
number is the alarm it looks like: the harness fails suites that run zero
assertions, and the aggregate makes shrinking coverage visible.
In-image verification gates & their escape hatches (audit round 2)#
The 2026-08-08 audit closed a set of gates that previously could not fail. Each hard gate has ONE explicit, documented escape hatch — set it only for a deliberately reduced image, never to “get the build green”:
Gate |
Where it runs |
Escape hatch / opt-in |
|---|---|---|
riscv64 app-wheelhouse must contain real |
|
|
|
|
unset |
CUDA/cuDNN/TensorRT/NCCL completeness |
|
default is warn-only; |
TVM presence/version per arch |
|
|
ELF architecture of shipped binaries |
|
|
litert / genai / opencv-core produce real artifacts |
|
none — these verify stage-specific files now; genai mirrors its producer’s legitimate cross-build skip |
Check |
Script |
Catches (class) |
|---|---|---|
shellcheck gate |
|
6, 7 |
script COPY coverage |
|
1 |
critical fixes (incl. fix6) |
|
2, 3 (+ prior fixes) |
ARG consistency |
|
8 |
version snapshot |
|
8 |
ubuntu mirror consistency |
|
8 |
runtime path consistency |
|
8 |
Each is runnable standalone (same commands). The pre-commit hook
(.githooks/pre-commit) runs the shellcheck gate (staged files), script COPY
coverage, and critical-fixes checks on every commit.
In-image smoke tests (need a built image, not part of preflight)#
These validate a built/pulled image and also run during the build to fail fast:
Two-environment semantics of
smoke-media.sh(since 2026-08-10): the suite runs TWICE — once inside the media build sandbox (Dockerfile.media, loader NOT yet wired:/opt/ffmpeglibs and/opt/venvare unreachable there) and once at the packaging stage (Dockerfile.package, loader fully configured). In the sandbox run, three gates deliberately DEFER instead of failing: theonnxruntime_genaiPython import (its wheel installs into/opt/venvonly at packaging;smoke-torch-venv.shis the functional gate), the gstlibavplugin load (links the source-built ffmpeg libav* incl. the bundled libtensorflow — gated on ffmpeg-executability), and ffmpeg’s own execution. Auditing coverage by the media-stage log alone therefore UNDER-counts what is enforced — the packaging-stage run is the strict one.
Native source-build header preflight — inside
setup-torch-venv.sh(verify_native_source_headers): compiles tiny C / C++ / jpeglib probes with the same compiler+flags the pip build uses, so a header/sysroot regression (classes 2/3) aborts in <1s instead of after a ~9-min numpy/pillow compile.Torch venv integrity —
06-packaging/smoke-torch-venv.sh: imports numpy/torch/torchvision/PIL/cv2/contourpy (+ torch↔numpy ABI bridge) from/opt/venv(class 5). Wired intothe wrapper-smoke target's smoke set (validate-compilers, smoke-media, smoke-torch-venv, smoke-cross-all-arches); skips cleanly if no venv. Run standalone:VENV=/opt/venv smoke-torch-venv.sh.Runtime-image boot + functional smoke —
06-packaging/smoke-runtime-image.sh <image> <arch>, run per-arch bybuild-runtime-manifest.shagainst the freshly built wrapper. Boots the actual published image and, under binfmt/qemu for the cross arches, runs real workloads on-target:ML imports (
onnxruntime,numpy,torch) +ffmpeg -version(pipefail-guarded so a missing.socan’t pass silently); torch-less sentinel flagged.ML version-pin assertion (fail) — not just importable but the correct versions. Delegates to
smoke-torch-venv.sh(assert-only), which asserts each ML package’s installed version equals one of {uv.lock} ∪ {versions.env pin}: uv.lock is authoritative for the uv-resolved packages (numpy/pillow/contourpy + the amd64/arm64 torch/vision/onnx wheels), versions.env for the ones we build or force-reinstall from a local wheel (riscv64 torch/vision, the source-built onnxruntime whose__version__1.27.0 ≠ its pip dist 1.24.4, ai-edge-litert). Also checks the+cpu/+cu130build variant vsPYTORCH_EXTRAand the OpenCV major. Catches a wrong version silently slipping in (lock drift, a stale local wheel, a floated index) — the class a presence/import check can’t see.Native
/opt.so-closure gate —lddover the ffmpeg/opencv5/libcamera/ vulkan payload; any unresolved soname fails the gate. Generalises the ffmpeg check to the whole native stack (the class that shipped libopencore-amrwb.so.0- broken ffmpeg). Venv Python extensions are excluded (import-time lib paths defeat bareldd; the import checks are their gate).GStreamer plugin health (warn) — lists plugins whose runtime
.sois absent (they degrade gracefully); surfaces app-critical regressions likewebrtcbin2→librice-proto.so.0. GStreamer core pipeline (fail) —videotestsrc ! videoconvert ! fakesink.onnxruntime inference (fail) — runs a tiny embedded Add model and asserts the output (proves the CPU EP executes, not just imports). cv2 encode/decode roundtrip. Application import — the shipped venv must
import orchestr_ant_ion.Native compiler battery compile+link+RUN — an 8-case battery with the image’s
gcc/g++, each running the resulting binary on-target: C hello (stdout), pthreads, libm, libatomic; C++ hello (libstdc++), exceptions+STL (throw/catch +std::sort), std::thread, and-flto. This is what upgrades class 4 from a static ELF/machine check to genuine execution proof: a cross arch’s binary can’t run on the x86_64 build host, so the shipped native GCC (esp. the riscv64--with-isa-spectoolchain) was previously never actually executed — under qemu here it is. The exceptions+STL case is the regression guard for theswap-native-gcc.shwrapper fix (c46da5f): the compiler reaches the runtime image’s system headers via command-line-idirafterwrappers, not an installedspecsfile — a specs file silently drops-lgcc_s/--eh-frame-hdrand makes every throwing C++ program terminate at runtime. GateRUNTIME_COMPILER_SMOKE=0to skip just this;RUNTIME_FUNCTIONAL_SMOKE=0skips all functional checks;RUNTIME_IMAGE_SMOKE=0(inbuild-runtime-manifest.sh) skips the whole runtime-image smoke (e.g. a host without a qemu handler for a foreign arch).
Foreign-arch execution needs QEMU binfmt — registered without sudo#
The foreign-arch smokes above only mean something if the image’s binaries can
actually execute on the build host. That requires a binfmt_misc QEMU handler.
build-runtime-manifest.sh registers it automatically, with no sudo, before the
smoke loop (ensure_foreign_binfmt → linux/scripts/setup-rootless-binfmt.sh).
Opt out with RUNTIME_REGISTER_BINFMT=0 (e.g. a rootful/CI host where qemu is
already registered via docker run --privileged tonistiigi/binfmt or
update-binfmts).
Two dead-ends to know about, because both look like they work and don’t:
nerdctl run --privileged tonistiigi/binfmt --install …(rootless) registers binfmt inside the throwaway container’s own user namespace, which--rmdestroys. It prints “arch OK” but never reaches the namespace where builds/runs happen.BuildKit’s embedded
/dev/.buildkit_qemu_emulatoronly wraps the top-level process of aRUN. The shell starts, but its first child exec (uname,mktemp,gcc,python,ffmpeg) dies withExec format error— so it cannot run any real multi-process smoke.
setup-rootless-binfmt.sh is the working no-sudo path on a rootless
containerd/BuildKit host: buildkitd runs nsenter’d into containerd’s rootlesskit
namespace, so nerdctl run and nerdctl build share one persistent namespace.
The script extracts the static qemu-<arch> emulators from tonistiigi/binfmt,
enters that shared namespace via containerd-rootless-setuptool.sh nsenter (where
the mapped uid-0 does hold CAP_SYS_ADMIN over its own mounts — no host sudo), and
registers each with flags POCF. The F (fix-binary) flag is the crux: the
kernel opens the interpreter fd at registration time, so emulation is inherited into
the nested build/run namespaces where the qemu path isn’t even mounted — which is
exactly what makes child execs work. Register once per boot (or install the
systemd --user unit with --install-service); verify with --verify.
Dedup & factoring notes (2026-07)#
The tree has been through several dedup passes already; remaining duplication is largely deliberate and should not be “fixed”:
cross_build_is_active/install_host_packagesetc. are re-defined as fallbacks in several modules so each can be sourced standalone. Removing them breaks isolated use.build-libcamera.sh’s inline-idirafter /usr/include(generic) plus itsappend_cross_idiraftercall is a native/cross fallback pair, not a copy.The
-idirafterlogic insetup-torch-venv.sh+swap-native-gcc.shduplicates the canonical helperappend_cross_idirafter(01-core/common.sh). It is inlined on purpose: the torch/android stages do not COPYcommon.sh(nor itsload-versions-env.shchain), and pulling that in for six flag lines is not worth the surface on a verified critical path. The copies are cross-referenced to the helper and kept in sync byverify-critical-fixes.shfix6 — the pattern for necessary duplication: guard it, don’t hide it.
Hardening pass (2026-07) — landed + residuals#
Landed (see verify-critical-fixes.sh fix7 for the regression guards):
Build cache — replaced the self-defeating registry
-buildcache(whose--cache-towas gated out byNO_CACHE_EXPORT, so nothing ever cache-hit) with a local buildkit cache + inline cache on push. This is why full base rebuilds no longer recur on the same host.Base cache scope — base RUNs bind-mount only
01-core(+02-toolchainfor shared tooling), so editing a media/android script no longer busts the base image.Supply-chain — the sole floating external base (
ubuntu:26.04) is now digest-pinned by its multi-arch manifest-list digest (UBUNTU_DIGEST).Non-root runtime —
/workspaceischowned tokataglyphis;PYTHONDONTWRITEBYTECODE=1; the build-only fakesudoshim is stripped from the shipped image.Robustness — image-wide
aptretries (80-retries); real-pipe +PIPESTATUSso a failing build’s log tail is flushed and its true exit code returned;parallel-loop.shnames the failed arch.Smokes — every previously-orphaned smoke now runs:
smoke-toolchain(toolchain),smoke-vulkan+smoke-android(android),smoke-vulkan+smoke-torch-venv(package wrapper-smoke), and host-sidesmoke-runtime-image(inbuild-runtime-manifest.sh,RUNTIME_IMAGE_SMOKE=0to skip).Reproducibility (opt-in) —
clone_or_update_repoandbuild-ffmpeg.shaccept a 40-hex commit SHA;OPENCV_COMMIT/OPENCV_CONTRIB_COMMIT/FFMPEG_COMMIT(empty by default = track the bleeding-edge branch) freeze those sources to an immutable commit for a release build.Attestations (opt-in) —
BUILD_ATTEST=1attaches SLSA provenance + SBOM to pushed images.
Residual supply-chain gaps (tracked, not yet closed — each is a known
curl/wget without a checksum; the fix is to route it through
download_verified_file with a new *_SHA256 in versions.env):
Source |
Site |
Note |
|---|---|---|
Flutter SDK tarball |
|
per-arch sha; large |
Android cmdline-tools zip |
|
NDK/build-tools are sdkmanager-verified |
freetype source |
|
swallows failure with |
GStreamer-Android universal |
|
published sha256 available |
|
|
|
Deliberate keep: the -dev header packages in setup-torch-venv.sh land in
the final image. This is a cross-dev container that compiles Python wheels
from source under QEMU at build time, so the headers are load-bearing; splitting
build-deps from runtime-deps would risk the source-build path for marginal size
savings. Left as-is by design.