SmartestHome/tools/build-all.sh

206 lines
7.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Build EVERY image this household needs, from one CoreSystemConfig.json.
#
# This is the normal way to use tools/. The individual builders still exist and still
# work, but rebuilding one image on its own is the unusual case: the images are a set
# that has to agree with itself, and the whole reason a kiosk knows the container
# host's address is that both were generated in the same pass from the same file.
# Building the set is therefore the default, and building one is the exception.
#
# sudo -E tools/build-all.sh # the core pair + every configured kiosk
# sudo -E tools/build-all.sh --core # just the twinned pair
# sudo -E tools/build-all.sh --kiosks # just the kiosks
# sudo -E tools/build-all.sh --dry-run # validate + list what would be built
#
# Every image is stamped with the same pair ID, so a drawer full of USB sticks can be
# checked against each other later: same ID means they were built from the same
# config and agree on every address and token.
#
# WHAT COMES OUT CONTAINS SECRETS. Wi-Fi PSK, service tokens, MQTT and HA credentials
# are burnt into these images — that is the point (nothing to configure post-install),
# and it makes every ISO a credential. .gitignore keeps them out of the repo; wiping
# old USB sticks is on you.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/coreconfig.sh
source "${SCRIPT_DIR}/lib/coreconfig.sh"
MODE="all"
DRY_RUN="false"
for arg in "$@"; do
case "$arg" in
--core) MODE="core" ;;
--kiosks) MODE="kiosks" ;;
--dry-run) DRY_RUN="true" ;;
-h|--help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) core_die "Unknown argument '$arg'. Try --help." ;;
esac
done
core_load
[[ "$DRY_RUN" == "true" ]] || core_require_root "$@"
PAIR_ID="$(core_pair_id)"
# Which kiosks are configured, and which builder each needs.
mapfile -t KIOSK_ROWS < <(python3 - "$CORE_CONFIG_PATH" <<'PY'
import json, sys
for k in json.load(open(sys.argv[1])).get("kiosks", []):
print(f"{k['type']}\t{k['hostname']}\t{k.get('friendly_name','')}")
PY
)
# Audio endpoints, with the architecture each one needs. Both architectures are built
# when both are configured — they're two different toolchains producing two different
# artifacts (an amd64 ISO and an arm64 .img), not one image that runs on both.
mapfile -t AUDIO_ROWS < <(python3 - "$CORE_CONFIG_PATH" <<'PY'
import json, sys
for a in json.load(open(sys.argv[1])).get("audio_endpoints", []):
print(f"{a['arch']}\t{a['hostname']}\t{a.get('friendly_name','')}")
PY
)
cat <<EOF
============================================================================
SmartestHome — build all
============================================================================
Config : ${CORE_CONFIG_PATH}
Pair ID : ${PAIR_ID}
Subnet : ${CORE_SUBNET_PREFIX}.0/24
Core pair (twinned — each knows the other's address by derivation):
container host ${CORE_CONTAINER_HOST_IP} ${CORE_CONTAINER_HOST_NAME}
LLM host ${CORE_LLM_HOST_IP} ${CORE_LLM_HOST_NAME}
the link ${CORE_OLLAMA_HOST}
Kiosks + audio endpoints (all pointed at ${CORE_CONTAINER_HOST_IP} by derivation):
EOF
if [[ ${#KIOSK_ROWS[@]} -eq 0 && ${#AUDIO_ROWS[@]} -eq 0 ]]; then
echo " (none configured)"
fi
for row in "${KIOSK_ROWS[@]:-}"; do
[[ -n "$row" ]] || continue
IFS=$'\t' read -r ktype khost kname <<< "$row"
printf ' %-18s %-24s %s\n' "$ktype" "$khost" "$kname"
done
for row in "${AUDIO_ROWS[@]:-}"; do
[[ -n "$row" ]] || continue
IFS=$'\t' read -r aarch ahost aname <<< "$row"
printf ' %-18s %-24s %s\n' "audio/${aarch}" "$ahost" "$aname"
done
echo "
Building : ${MODE}$([[ "$DRY_RUN" == "true" ]] && echo " (DRY RUN — nothing will be built)")
============================================================================"
if [[ "$DRY_RUN" == "true" ]]; then
echo "
Config is valid and the above is what would be built. Re-run without --dry-run.
"
exit 0
fi
BUILT=()
FAILED=()
run_build() {
local label="$1"; shift
core_log "Building ${label}"
# One image failing must not abandon the rest: an ISO build is long, and losing an
# hour of successful builds because the last one hit a mirror timeout would be a
# poor trade. Failures are collected and reported together at the end.
if "$@"; then
BUILT+=("$label")
else
core_warn "${label} FAILED — continuing with the rest"
FAILED+=("$label")
fi
}
if [[ "$MODE" == "all" || "$MODE" == "core" ]]; then
run_build "container host" "${SCRIPT_DIR}/build-container-host-iso.sh"
run_build "LLM host" "${SCRIPT_DIR}/build-llm-host-iso.sh"
fi
if [[ "$MODE" == "all" || "$MODE" == "kiosks" ]]; then
for row in "${KIOSK_ROWS[@]}"; do
IFS=$'\t' read -r ktype khost _ <<< "$row"
builder="${SCRIPT_DIR}/build-${ktype}-iso.sh"
if [[ ! -x "$builder" ]]; then
core_warn "No builder for kiosk type '${ktype}' (${builder}) — skipping ${khost}"
FAILED+=("${ktype}/${khost} (no builder)")
continue
fi
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"
case "$aarch" in
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
done
fi
OUTPUT_DIR="${CORE_REPO_ROOT}/${CORE_BUILD_OUTPUT_DIR}"
echo "
============================================================================
Build all — done (pair ${PAIR_ID})
============================================================================"
if [[ ${#BUILT[@]} -gt 0 ]]; then
echo " Built:"
for b in "${BUILT[@]}"; do echo "$b"; done
fi
if [[ ${#FAILED[@]} -gt 0 ]]; then
echo " FAILED:"
for f in "${FAILED[@]}"; do echo "$f"; done
fi
echo "
Images in: ${OUTPUT_DIR}
"
ls -lh "$OUTPUT_DIR" 2>/dev/null | tail -n +2 | awk '{printf " %-52s %s\n", $9, $5}' || true
cat <<EOF
Install the container host FIRST — the kiosks and the LLM host are all
clients of it, and every one of them was built expecting it at
${CORE_CONTAINER_HOST_IP}.
Two things still need a human, because neither exists at build time:
1. HA_TOKEN — a Long-Lived Access Token from Home Assistant's own UI, which
can't be created until HA is running. Put it in CoreSystemConfig.json and
rebuild, or edit identity.env on the container host.
2. TRUSTED_ENTITY_PREFIXES — the real entity_id prefixes your Private BLE
Device setup produces. The default is a guess and it is the highest-risk
unknown in Phase 6.
EOF
[[ ${#FAILED[@]} -eq 0 ]]