Give every config array entry its own image, and build arm64 audio once

Follow-up to the tools/ build system: the kiosk and amd64 audio-endpoint
builders left their ISO in the live-build tree under live-build's fixed
filename, and never moved it to build-output/. Configure two thin clients and
build-all would build both into the same path — the second silently overwriting
the first, leaving one ISO carrying the second room's hostname and no sign the
first was ever produced.

core_publish_image() now moves each finished image to
build-output/smarthome-<hostname>-<pairid>.iso, so one array entry produces one
artifact. Verified: two thin clients now yield two distinct ISOs, and the
per-type builders still refuse with the real list when the hostname is omitted
or wrong.

The arm64 audio endpoint is the deliberate exception and is now handled as one:
Pi Imager sets hostname/Wi-Fi per unit at flash time, so a single generic .img
serves every room and build-all builds it once regardless of how many arm64
entries are listed. amd64 has no equivalent for a generic x86 ISO, so it bakes
the hostname in and does need one per room (project-plan Phase 15.5).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
digest-per-person-and-agendas
Amir Alexander Abdelbaki 2026-07-31 13:25:38 +02:00
parent ea82ee70ad
commit bbeabde97e
11 changed files with 94 additions and 7 deletions

View File

@ -94,6 +94,24 @@ Building one image is supported but unusual: the images are a set that has to ag
with itself, which is why `build-all.sh` is the default and a failure in one image
doesn't abandon the rest.
### One entry, one image
Kiosks and audio endpoints come from **arrays** in the config, and every entry gets its
own artifact in `build-output/`, named `smarthome-<hostname>-<pairid>.iso`. That naming
is load-bearing rather than cosmetic: live-build always writes the same filename into
the same per-host tree, so two thin clients would otherwise have the second silently
overwrite the first — leaving one ISO carrying the second room's hostname and nothing
to indicate the first was lost.
Per-type builders take a hostname when several of that type exist, and refuse with the
list of real ones if you omit it or get it wrong.
**The one deliberate exception is the arm64 audio endpoint.** Raspberry Pi Imager sets
hostname and Wi-Fi per unit at flash time, so one generic `.img` serves every room and
`build-all.sh` builds it once no matter how many arm64 entries are listed. amd64 has no
equivalent tool for a generic x86 ISO, so it bakes the hostname in and genuinely needs
one ISO per room — see `docs/project-plan.md` Phase 15.5.
## The ISOs contain secrets
This is deliberate — burning everything in is what makes installation unattended, with

View File

@ -137,18 +137,35 @@ if [[ "$MODE" == "all" || "$MODE" == "kiosks" ]]; then
run_build "${ktype}${khost}" "$builder" "$khost"
done
# The two audio-endpoint architectures are asymmetric on purpose, and it changes how
# many images each needs (docs/project-plan.md Phase 15.5):
# amd64 — no x86 equivalent of Pi Imager's OS Customisation exists, so the hostname
# is baked in at build time. One ISO PER ROOM.
# arm64 — Raspberry Pi Imager sets hostname/Wi-Fi per unit at flash time, so one
# generic .img serves every room. Built ONCE no matter how many are listed.
arm64_done="false"
for row in "${AUDIO_ROWS[@]:-}"; do
[[ -n "$row" ]] || continue
IFS=$'\t' read -r aarch ahost _ <<< "$row"
# Two genuinely different toolchains, not one builder with a flag: live-build for
# the amd64 mini-PC, rpi-image-gen for the arm64 Pi.
case "$aarch" in
amd64) builder="${SCRIPT_DIR}/build-audio-endpoint-iso-amd64.sh" ;;
arm64) builder="${SCRIPT_DIR}/build-audio-endpoint-image-arm64.sh" ;;
*) core_warn "Unknown audio endpoint arch '${aarch}' — skipping ${ahost}"
FAILED+=("audio/${ahost} (bad arch)"); continue ;;
amd64)
run_build "audio endpoint (amd64) — ${ahost}" \
"${SCRIPT_DIR}/build-audio-endpoint-iso-amd64.sh" "$ahost"
;;
arm64)
if [[ "$arm64_done" == "true" ]]; then
core_log "arm64 audio image already built — ${ahost} flashes the same .img (hostname set in Pi Imager)"
continue
fi
run_build "audio endpoint (arm64, generic image)" \
"${SCRIPT_DIR}/build-audio-endpoint-image-arm64.sh" "$ahost"
arm64_done="true"
;;
*)
core_warn "Unknown audio endpoint arch '${aarch}' — skipping ${ahost}"
FAILED+=("audio/${ahost} (bad arch)")
;;
esac
run_build "audio endpoint (${aarch}) — ${ahost}" "$builder" "$ahost"
done
fi

View File

@ -126,6 +126,10 @@ lb build
ISO_PATH="$(find "$LIVE_BUILD_DIR" -maxdepth 1 -name '*.iso' -print -quit)"
ISO_PATH="${ISO_PATH:-${LIVE_BUILD_DIR}/live-image-amd64.hybrid.iso}"
# Per-room name: unlike the arm64 image below, amd64 bakes the hostname in at build
# time (there is no x86 equivalent of Pi Imager's OS Customisation), so each room is a
# genuinely different ISO and they must not share a filename.
ISO_PATH="$(core_publish_image "$ISO_PATH" "audio-endpoint" "$IMAGE_HOSTNAME")"
echo
echo "=== Done ==="

View File

@ -242,6 +242,9 @@ lb build
ISO_PATH="$(find "$LIVE_BUILD_DIR" -maxdepth 1 -name '*.iso' -print -quit)"
ISO_PATH="${ISO_PATH:-${LIVE_BUILD_DIR}/live-image-amd64.hybrid.iso}"
# Published under this kiosk's own hostname — live-build reuses one filename per host
# tree, so two of the same kiosk type would otherwise overwrite each other.
ISO_PATH="$(core_publish_image "$ISO_PATH" "door-panel" "$IMAGE_HOSTNAME")"
echo
echo "=== Done ==="

View File

@ -244,6 +244,9 @@ lb build
ISO_PATH="$(find "$LIVE_BUILD_DIR" -maxdepth 1 -name '*.iso' -print -quit)"
ISO_PATH="${ISO_PATH:-${LIVE_BUILD_DIR}/live-image-amd64.hybrid.iso}"
# Published under this kiosk's own hostname — live-build reuses one filename per host
# tree, so two of the same kiosk type would otherwise overwrite each other.
ISO_PATH="$(core_publish_image "$ISO_PATH" "kitchen-display" "$IMAGE_HOSTNAME")"
echo
echo "=== Done ==="

View File

@ -328,6 +328,9 @@ lb build
ISO_PATH="$(find "$LIVE_BUILD_DIR" -maxdepth 1 -name '*.iso' -print -quit)"
ISO_PATH="${ISO_PATH:-${LIVE_BUILD_DIR}/live-image-amd64.hybrid.iso}"
# Published under this kiosk's own hostname — live-build reuses one filename per host
# tree, so two of the same kiosk type would otherwise overwrite each other.
ISO_PATH="$(core_publish_image "$ISO_PATH" "thin-client" "$IMAGE_HOSTNAME")"
echo
echo "=== Done ==="

View File

@ -255,6 +255,9 @@ lb build
ISO_PATH="$(find "$LIVE_BUILD_DIR" -maxdepth 1 -name '*.iso' -print -quit)"
ISO_PATH="${ISO_PATH:-${LIVE_BUILD_DIR}/live-image-amd64.hybrid.iso}"
# Published under this kiosk's own hostname — live-build reuses one filename per host
# tree, so two of the same kiosk type would otherwise overwrite each other.
ISO_PATH="$(core_publish_image "$ISO_PATH" "touch-panel" "$IMAGE_HOSTNAME")"
echo
echo "=== Done ==="

View File

@ -173,6 +173,42 @@ SMARTHOME_LLM_HOST=${CORE_LLM_HOST_IP}
EOF
}
# Move a finished image out of its live-build tree into build-output/, named for the
# thing it actually is.
#
# THIS IS LOad-BEARING FOR MULTI-INSTANCE BUILDS, not cosmetic. live-build always
# writes the same filename (live-image-amd64.hybrid.iso) into the same per-host tree,
# so building two thin clients — a living-room one and a bedroom one — would have the
# second silently overwrite the first, leaving one ISO carrying the second room's
# hostname and no indication the first was ever lost. Publishing under
# <role>-<hostname>-<pairid> makes each config entry produce its own artifact.
core_publish_image() {
local src="$1" role="$2" instance="${3:-}"
local out_dir="${CORE_REPO_ROOT}/${CORE_BUILD_OUTPUT_DIR}"
mkdir -p "$out_dir"
# Hostnames usually already carry the role ("thin-client-bedroom"), so appending the
# role verbatim gives "thin-client-thin-client-bedroom". Use the hostname alone when
# it already starts with the role.
local name="smarthome-${role}"
if [[ -n "$instance" ]]; then
if [[ "$instance" == "$role"* ]]; then
name="smarthome-${instance}"
else
name="smarthome-${role}-${instance}"
fi
fi
local dest="${out_dir}/${name}-$(core_pair_id).${src##*.}"
if [[ ! -f "$src" ]]; then
core_warn "Expected an image at ${src} but found none — nothing published."
return 1
fi
mv "$src" "$dest"
core_log "Image: ${dest}"
echo "$dest"
}
# Guard for the ISO builders: `lb build` needs root, and finding that out after the
# config phase has already written files is worse than finding out now.
core_require_root() {