#!/usr/bin/env bash # ============================================================================= # vendor.sh - Pre-download dependencies for air-gapped installation # Run on a build/staging machine that has internet access. # Output: wheelhouse/ (pip wheel + sdist), debs/ (apt .deb) → transfer to target Pi 4. # # wheel/deb are architecture-dependent. The defaults are explicitly pinned to the target # (Raspberry Pi OS Bookworm 64-bit, aarch64, Python 3.11), so even downloading on an x86_64 # build machine yields the correct aarch64 wheel (independent of the build host arch). Still, .deb # files are safest downloaded on the target OS/arch. Adjust the target via env vars if needed: # TARGET_PLATFORM (default manylinux2014_aarch64), TARGET_PYVER (default 311), CAM # ============================================================================= set -euo pipefail cd "$(dirname "$0")" TARGET_PLATFORM="${TARGET_PLATFORM:-manylinux2014_aarch64}" TARGET_PYVER="${TARGET_PYVER:-311}" TARGET_ABI="${TARGET_ABI:-cp${TARGET_PYVER}}" echo "[vendor] (1/2) Python wheel → wheelhouse/ (platform=${TARGET_PLATFORM}, py=${TARGET_PYVER})" mkdir -p wheelhouse python3 -m pip download -r requirements.txt -d wheelhouse \ --only-binary=:all: \ --platform "${TARGET_PLATFORM}" \ --python-version "${TARGET_PYVER}" \ --implementation cp \ --abi "${TARGET_ABI}" \ || { echo "[ERROR] Target wheel download failed (check TARGET_PLATFORM/TARGET_PYVER)"; exit 1; } python3 -m pip download -r requirements.txt -d wheelhouse --no-binary=:all: \ || echo "[WARN] sdist fallback partially failed (safe to ignore if all binary wheels are present)" WHEEL_COUNT="$(ls -A wheelhouse 2>/dev/null | wc -l)" echo " wheelhouse file count: ${WHEEL_COUNT}" [ "${WHEEL_COUNT}" -gt 0 ] || { echo "[ERROR] wheelhouse is empty"; exit 1; } echo "[vendor] (2/2) apt .deb → debs/" mkdir -p debs CAM="${CAM:-}" if [ -z "${CAM}" ]; then if apt-cache show rpicam-apps >/dev/null 2>&1; then CAM=rpicam-apps; else CAM=libcamera-apps; fi fi # Include nftables and unattended-upgrades too → harden_pi.sh can run offline in an air-gapped network. BASE_PKGS="python3-rpi-lgpio python3-requests python3-venv nftables unattended-upgrades ${CAM}" DEPS="$(apt-cache depends --recurse --no-recommends --no-suggests \ --no-conflicts --no-breaks --no-replaces --no-enhances ${BASE_PKGS} \ | awk '/^\w/{print $1}' | sort -u)" ( cd debs && apt-get download ${BASE_PKGS} ${DEPS} ) 2>/dev/null \ || echo "[WARN] Some .deb downloads failed (may be virtual packages / transitive deps — required packages are verified below)" MISS=0 for p in ${BASE_PKGS}; do if ! ls debs/"${p}"_*.deb >/dev/null 2>&1; then echo "[ERROR] Missing required package .deb: ${p}" MISS=1 fi done [ "${MISS}" = 0 ] || { echo "[ERROR] Missing required .deb — you must run vendor.sh on the target arch (aarch64 Bookworm)."; exit 1; } echo " debs file count: $(ls debs/*.deb 2>/dev/null | wc -l) (all required packages confirmed)" # Real architecture check — validate the target (arm64/all) via the .deb's internal Architecture field, not the filename. # (If the host is amd64, apt-get download fetches amd64 .debs, and on the target Pi dpkg # breaks with "wrong architecture". A filename-only check cannot catch this.) BAD_ARCH=0 for deb in debs/*.deb; do [ -e "$deb" ] || continue arch="$(dpkg-deb --field "$deb" Architecture 2>/dev/null || echo '?')" case "$arch" in arm64|all) : ;; *) echo "[ERROR] arch mismatch: $(basename "$deb") = ${arch} (not arm64/all)"; BAD_ARCH=1 ;; esac done if [ "${BAD_ARCH}" != 0 ]; then echo "[ERROR] There are .deb files that are not the target (aarch64/arm64). Host arch=$(dpkg --print-architecture 2>/dev/null || echo unknown)." >&2 echo " You must run vendor.sh on an arm64 Pi (or arm64 chroot/qemu)." >&2 exit 1 fi echo " arch check passed (all .deb = arm64/all)" echo "[vendor] Done. Transfer wheelhouse/ and debs/ to the target Pi 4, then run install.sh."