211 lines
7.9 KiB
Bash
Executable File
211 lines
7.9 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
|
|
|
|
# Both architectures now produce ONE PRE-NAMED IMAGE PER ROOM, so every device in
|
|
# the household is identified by the build rather than by something typed in
|
|
# afterwards. arm64 used to be the exception (one generic .img, hostname set in
|
|
# Raspberry Pi Imager at flash time); baking it in costs a full rpi-image-gen run per
|
|
# room, which is the tradeoff `build.arm64_prebake: false` exists to undo.
|
|
arm64_generic_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 [[ "$CORE_ARM64_PREBAKE" != "true" ]]; then
|
|
# Generic mode: one image for every arm64 room, named in Pi Imager.
|
|
if [[ "$arm64_generic_done" == "true" ]]; then
|
|
core_log "arm64 generic image already built — ${ahost} flashes the same .img (name it in Pi Imager)"
|
|
continue
|
|
fi
|
|
run_build "audio endpoint (arm64, generic image)" \
|
|
"${SCRIPT_DIR}/build-audio-endpoint-image-arm64.sh" "$ahost"
|
|
arm64_generic_done="true"
|
|
else
|
|
run_build "audio endpoint (arm64) — ${ahost}" \
|
|
"${SCRIPT_DIR}/build-audio-endpoint-image-arm64.sh" "$ahost"
|
|
fi
|
|
;;
|
|
*)
|
|
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 ]]
|