#!/usr/bin/env bash # # Builds the arm64 headless audio-endpoint image (Raspberry Pi + HiFiBerry # Amp2) with rpi-image-gen. Read hosts/audio-endpoint/README.md before running # this — in particular the callout that rpi-image-gen's exact config/layer # schema (config/audio-endpoint.yaml) was not hands-on validated while writing # this script; a real build may need that file adjusted first. # # Output: a real bootable .img. Each physical unit then gets its own # hostname/Wi-Fi via Raspberry Pi Imager's OS Customisation dialog when # flashing — see the README's "Per-room identity" section. This script builds # ONE generic image, not one per room. set -euo pipefail # --------------------------------------------------------------------------- # CONFIGURATION — edit if needed # --------------------------------------------------------------------------- RPI_IMAGE_GEN_SRC="${RPI_IMAGE_GEN_SRC:-/opt/smart-home/src/rpi-image-gen}" RPI_IMAGE_GEN_REPO="https://github.com/raspberrypi/rpi-image-gen.git" # --------------------------------------------------------------------------- # Paths # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/coreconfig.sh source "${SCRIPT_DIR}/lib/coreconfig.sh" core_select_audio_endpoint arm64 "${1:-}" # This script now lives in tools/, so the host directory it drives is addressed from # the repo root rather than relative to the script. AUDIO_ENDPOINT_DIR="${CORE_REPO_ROOT}/hosts/audio-endpoint" RPI_IMAGE_GEN_DIR="${AUDIO_ENDPOINT_DIR}/rpi-image-gen" SHARED_CONFIGS_DIR="$AUDIO_ENDPOINT_DIR/configs" BUILD_CONFIG_TEMPLATE="$RPI_IMAGE_GEN_DIR/config/audio-endpoint.yaml" # Per-endpoint build config, generated below. Kept out of the tracked config/ dir so a # build never leaves a room-specific file lying where the template belongs. if [[ "${CORE_ARM64_PREBAKE:-true}" == "true" ]]; then BUILD_CONFIG="$RPI_IMAGE_GEN_DIR/config/generated-${CORE_AUDIO_HOSTNAME}.yaml" IMAGE_INSTANCE="$CORE_AUDIO_HOSTNAME" else # Generic mode: build the template unchanged, name each unit in Pi Imager. BUILD_CONFIG="$BUILD_CONFIG_TEMPLATE" IMAGE_INSTANCE="generic" fi # --------------------------------------------------------------------------- # Sanity checks # --------------------------------------------------------------------------- for f in spotify-connect.service spotify-connect-start; do if [ ! -f "$SHARED_CONFIGS_DIR/$f" ]; then echo "Missing $SHARED_CONFIGS_DIR/$f — is this script running from a full checkout?" >&2 exit 1 fi done if [ ! -f "$BUILD_CONFIG_TEMPLATE" ]; then echo "Missing $BUILD_CONFIG_TEMPLATE" >&2 exit 1 fi if ! command -v git >/dev/null 2>&1; then echo "git is required to fetch rpi-image-gen. Install it first: sudo apt-get install git" >&2 exit 1 fi # --------------------------------------------------------------------------- # 0. Generate this room's build config — PRE-NAMED, not generic. # # The arm64 image used to be built once and handed to Raspberry Pi Imager, whose OS # Customisation dialog set hostname/Wi-Fi per physical unit at flash time. That worked, # but it made this the ONE image in the household where identity is typed in by hand # after the build — exactly the step CoreSystemConfig.json exists to remove. It also # failed quietly when mistyped: spotify-connect-start reads $(hostname) at service # start, so a typo doesn't error, it just puts a Spotify Connect device with the wrong # room name on the network. # # So the hostname is baked in here instead, per endpoint, same as every other image. # The cost is honest and worth stating: one full rpi-image-gen run PER ROOM rather than # one for all of them, and on an x86 build host that means qemu-emulated arm64 # bootstrapping each time. Set "arm64_prebake": false in the config's build section to # go back to a single generic image and do it in Imager. # --------------------------------------------------------------------------- if [[ "${CORE_ARM64_PREBAKE:-true}" == "true" ]]; then core_log "Generating build config for ${CORE_AUDIO_HOSTNAME}" python3 - "$BUILD_CONFIG_TEMPLATE" "$BUILD_CONFIG" "$CORE_AUDIO_HOSTNAME" <<'PYEOF' import re, sys template, dest, hostname = sys.argv[1], sys.argv[2], sys.argv[3] text = open(template).read() # Only the hostname value is rewritten. Deliberately a targeted substitution rather # than a YAML load/dump: this file is full of explanatory comments about an unverified # schema, and round-tripping it through a YAML library would silently strip every one # of them. It also avoids a PyYAML dependency in the build path. new, n = re.subn(r"(?m)^(hostname:\n(?:\s+#.*\n)*\s+set:\s*).*$", lambda m: m.group(1) + hostname, text) if n != 1: sys.exit(f"error: expected exactly one 'hostname:/set:' block in {template}, found {n}. " "The template's shape changed — fix this substitution rather than shipping " "an image named after the wrong room.") header = (f"# GENERATED by tools/build-audio-endpoint-image-arm64.sh for '{hostname}'.\n" f"# Do not edit — edit config/audio-endpoint.yaml (the template) or\n" f"# CoreSystemConfig.json, and rebuild.\n") open(dest, "w").write(header + new) PYEOF else core_log "arm64_prebake is false — building the generic image (name each unit in Pi Imager)" fi # --------------------------------------------------------------------------- # 1. Fetch rpi-image-gen if not already present # --------------------------------------------------------------------------- if [ ! -d "$RPI_IMAGE_GEN_SRC" ]; then echo "--- Cloning rpi-image-gen into $RPI_IMAGE_GEN_SRC ---" git clone --depth 1 "$RPI_IMAGE_GEN_REPO" "$RPI_IMAGE_GEN_SRC" else echo "--- rpi-image-gen already present at $RPI_IMAGE_GEN_SRC, skipping clone ---" fi # --------------------------------------------------------------------------- # 2. Build # --------------------------------------------------------------------------- echo "--- Running rpi-image-gen build ---" echo " Config: $BUILD_CONFIG" cd "$RPI_IMAGE_GEN_SRC" # UNVERIFIED: assumes a ./rpi-image-gen entry point at the repo root, matching # common conventions for this class of tool (e.g. pi-gen's own build.sh). If # the real tool exposes a different entry point (a pip-installed console # script, a Python module, etc.), fix this one line — everything else in this # script and config/audio-endpoint.yaml is unaffected either way. ./rpi-image-gen build -c "$BUILD_CONFIG" # Publish alongside every other image. The search is best-effort because # rpi-image-gen's output path is part of the unverified surface — if it finds nothing, # core_publish_image says so and the image is still wherever the tool put it. # Newest .img wins, rather than `-newer $BUILD_CONFIG`: a build that finishes in the # same filesystem timestamp granularity as the config write is not strictly "newer", # and that comparison silently found nothing rather than failing visibly. IMG="$(find "$RPI_IMAGE_GEN_SRC/work" -name '*.img' -printf '%T@ %p\n' 2>/dev/null \ | sort -rn | head -1 | cut -d' ' -f2-)" if [[ -n "$IMG" ]]; then IMG="$(core_publish_image "$IMG" "audio-endpoint" "$IMAGE_INSTANCE")" else core_warn "No .img found under $RPI_IMAGE_GEN_SRC/work — check rpi-image-gen's own output above for where it wrote the image; this wrapper could not locate it." fi echo echo "=== Done (if rpi-image-gen's invocation above matched the real tool) ===" echo echo "Next steps:" echo " 1. Flash with Raspberry Pi Imager (GUI) or:" echo " rpi-imager --cli .img /dev/" echo " 2. The hostname is ALREADY SET to '${CORE_AUDIO_HOSTNAME}' in this image — it is" echo " built for one room, not generic, so there is nothing to type into Imager's" echo " OS Customisation dialog for it. That hostname is also the Spotify Connect" echo " device name (spotify-connect-start reads \$(hostname) at service start)." echo " STILL DO set Wi-Fi and an SSH key in Imager if this unit needs them —" echo " those are not baked in, see README.md's 'Per-room identity' section." echo " 3. Attach the HiFiBerry Amp2 HAT and passive speakers before first boot." echo " 4. First boot checklist (see README.md for the full unverified list):" echo " systemctl status spotify-connect" echo " # confirm the room's hostname shows up as a Spotify Connect device" echo " aplay -l # confirm the HiFiBerry is card 0 (onboard audio disabled)"