# shellcheck shell=bash # # Shared config loader for every build script in tools/. Source it, call # `core_load`, and every CORE_* variable is in scope. # # source "$(dirname "${BASH_SOURCE[0]}")/lib/coreconfig.sh" # core_load # core values only # core_load --kiosk door-panel # + that kiosk's own values # # WHY THIS EXISTS: before it, every builder carried its own copy of the container # host's IP, the MQTT port, the identity token and half a dozen URLs. Six scripts, six # chances to typo one of them, and the symptom was always the same — a kiosk that # boots fine and then can't reach something, discovered after a 40-minute ISO build. # Now no build script contains an address at all; they all read one file, and # validate-config.py has already refused the build if that file is wrong. # # Nothing here is exported to child processes on purpose: build scripts substitute # these into generated files explicitly, so it's always visible in the script which # value went where. CORE_REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" CORE_CONFIG_PATH="${CORE_CONFIG_PATH:-${CORE_REPO_ROOT}/CoreSystemConfig.json}" CORE_TOOLS_DIR="${CORE_REPO_ROOT}/tools" core_die() { echo -e "\033[1;31m[error]\033[0m $*" >&2; exit 1; } core_log() { echo -e "\n\033[1;34m==>\033[0m $*"; } core_warn() { echo -e "\033[1;33m[warn]\033[0m $*" >&2; } core_load() { if [[ ! -f "$CORE_CONFIG_PATH" ]]; then core_die "No CoreSystemConfig.json found at $CORE_CONFIG_PATH Create one from the template and fill it in: cp ${CORE_REPO_ROOT}/CoreSystemConfig.json.template ${CORE_REPO_ROOT}/CoreSystemConfig.json \$EDITOR ${CORE_REPO_ROOT}/CoreSystemConfig.json Then check it with: ${CORE_TOOLS_DIR}/validate-config.py (Set CORE_CONFIG_PATH to build from a different file — useful for a second household or a test config.)" fi command -v python3 >/dev/null 2>&1 || core_die "python3 is required to read CoreSystemConfig.json" # VALIDATE BEFORE ANYTHING ELSE. An ISO build is long and mostly unattended; the # entire value of the validator evaporates if it runs after 40 minutes of debootstrap # rather than before it. Warnings print but don't stop the build. core_log "Validating $(basename "$CORE_CONFIG_PATH")" if ! python3 "${CORE_TOOLS_DIR}/validate-config.py" "$CORE_CONFIG_PATH"; then core_die "CoreSystemConfig.json has errors (above) — fix them before building. Nothing was built and nothing was written." fi local exported if ! exported="$(python3 "${CORE_TOOLS_DIR}/config-export.py" "$CORE_CONFIG_PATH" "$@")"; then # config-export.py emits a shell `echo ... >&2; exit 1` on an unknown kiosk, so # evaluating its output is what surfaces that message. eval "$exported" core_die "Could not read $CORE_CONFIG_PATH" fi # Every value is shlex.quote'd on the Python side, so a password containing quotes, # spaces or semicolons survives this intact rather than becoming shell syntax. eval "$exported" } # Load the one kiosk this builder is for. # core_select_kiosk door-panel # the only door-panel in the config # core_select_kiosk thin-client living # a specific one, when there are several # # Taking the hostname as an argument (rather than a per-script constant) is what lets # one builder produce several images: two thin clients in two rooms are two entries in # CoreSystemConfig.json, not two copies of a script with one line changed — which is # how the addresses drifted apart in the first place. core_select_kiosk() { local want_type="$1" want_host="${2:-}" local matches matches="$(python3 - "$CORE_CONFIG_PATH" "$want_type" <<'PY' import json, sys cfg = json.load(open(sys.argv[1])) print("\n".join(k["hostname"] for k in cfg.get("kiosks", []) if k.get("type") == sys.argv[2])) PY )" if [[ -z "$matches" ]]; then core_die "No kiosk of type '${want_type}' in $(basename "$CORE_CONFIG_PATH"). Add one to the \"kiosks\" list and re-run." fi if [[ -z "$want_host" ]]; then if [[ "$(wc -l <<< "$matches")" -gt 1 ]]; then core_die "Several '${want_type}' kiosks are configured — say which one: $(sed 's/^/ /' <<< "$matches") e.g. $0 $(head -1 <<< "$matches")" fi want_host="$matches" elif ! grep -qx "$want_host" <<< "$matches"; then core_die "'${want_host}' is not a configured ${want_type}. Available: $(sed 's/^/ /' <<< "$matches")" fi core_load --kiosk "$want_host" } # Same idea as core_select_kiosk, for the headless audio endpoints — selected by # architecture, since those are two genuinely different builders (live-build for the # amd64 mini-PC, rpi-image-gen for the arm64 Pi) rather than one with a flag. core_select_audio_endpoint() { local want_arch="$1" want_host="${2:-}" local matches matches="$(python3 - "$CORE_CONFIG_PATH" "$want_arch" <<'PY' import json, sys cfg = json.load(open(sys.argv[1])) print("\n".join(a["hostname"] for a in cfg.get("audio_endpoints", []) if a.get("arch") == sys.argv[2])) PY )" if [[ -z "$matches" ]]; then core_die "No ${want_arch} audio endpoint in $(basename "$CORE_CONFIG_PATH"). Add one to the \"audio_endpoints\" list and re-run." fi if [[ -z "$want_host" ]]; then if [[ "$(wc -l <<< "$matches")" -gt 1 ]]; then core_die "Several ${want_arch} audio endpoints are configured — say which one: $(sed 's/^/ /' <<< "$matches")" fi want_host="$matches" elif ! grep -qx "$want_host" <<< "$matches"; then core_die "'${want_host}' is not a configured ${want_arch} audio endpoint. Available: $(sed 's/^/ /' <<< "$matches")" fi core_load --audio-endpoint "$want_host" } # Deterministic identifier for a matched set of images, printed at build time and # written into every image as /etc/smarthome-build. Two ISOs built from the same # config carry the same PAIR_ID; if you ever end up holding a container-host ISO and # an llm-host ISO and can't remember whether they agree on addresses and tokens, # compare this and you know. core_pair_id() { local hash hash="$(python3 - "$CORE_CONFIG_PATH" <<'PY' import hashlib, json, sys # Hash the SEMANTIC content, not the bytes: reformatting the JSON or reordering keys # must not change the pair ID, because it didn't change what gets built. cfg = json.loads(open(sys.argv[1]).read()) def strip(node): if isinstance(node, dict): return {k: strip(v) for k, v in sorted(node.items()) if not k.startswith("_")} if isinstance(node, list): return [strip(v) for v in node] return node print(hashlib.sha256(json.dumps(strip(cfg), sort_keys=True).encode()).hexdigest()[:12]) PY )" echo "$hash" } # Written into every image so a booted machine can say what it was built from. core_write_build_stamp() { local dest="$1" role="$2" mkdir -p "$(dirname "$dest")" cat > "$dest" <-- 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() { [[ $EUID -eq 0 ]] || core_die "This needs root (lb build does). Re-run with: sudo -E $0 $*" }