SmartestHome/tools/build-audio-endpoint-image-...

167 lines
8.4 KiB
Bash
Executable File

#!/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 <path-to-image>.img /dev/<sd-card-device>"
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)"