167 lines
8.4 KiB
Bash
Executable File
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)"
|