# tools — the build system Every build and setup script for this project, driven by **one** config file at the repo root. ```sh cp CoreSystemConfig.json.template CoreSystemConfig.json $EDITOR CoreSystemConfig.json tools/validate-config.py # check it before you commit to a long build sudo -E tools/build-all.sh # build everything ``` That's the whole workflow. ## Why this exists Before it, the same facts lived in six places. The container host's IP was written into four ISO builders; `IDENTITY_TOKEN` had to match by hand across three; every service URL was a hand-typed string with a port in it. Any one of them could be wrong, and the symptom was always the same and always late: an image that boots fine and then can't reach something, discovered after a 40-minute build and a reboot. Two changes fix that class of bug: 1. **Nothing is written twice.** No script in this directory contains an IP address, a port or a token. They read `CoreSystemConfig.json`. 2. **Anything derivable is derived.** You give the subnet prefix once and one last octet per host. Every address and every service URL is computed from those. ## The twinned pair The container host and the LLM host are built as a matched set, and the twinning is mechanical rather than a matter of remembering: ``` network.subnet_prefix 192.168.30 container_host.ip_last_octet 12 -> 192.168.30.12 llm_host.ip_last_octet 13 -> 192.168.30.13 ...so the container host's OLLAMA_HOST is http://192.168.30.13:11434 because it was COMPUTED from the LLM host's octet in the same build, not because someone typed the same address into two files. ``` Move the LLM host to `.21` and rebuild: the container host's Ollama URL follows on its own. Change the subnet prefix and *both* halves move, along with every kiosk's service URLs. **Neither image can be built pointing at an address the other one isn't using.** Both halves are stamped with the same `SMARTHOME_PAIR_ID` in `/etc/smarthome-build`, so two USB sticks found in a drawer months later can be checked against each other. The ID is a hash of the config's *meaning*, not its bytes — reformatting the JSON doesn't change it, moving a host does. ## The validator `validate-config.py` runs before every build and refuses to start on an error, so a mistake costs seconds instead of an hour. It's stdlib-only on purpose: it has to be able to run on a fresh checkout before anything is installed. What it catches, beyond missing and malformed values: | Check | Why it matters | |---|---| | **Duplicate ports** | Two services on one port means a container that silently fails to bind, or whichever won last boot answering. Miserable to diagnose from the symptom — and it's how `music_assistant` (default 8095) collides with `pantry_vision`, which Compose's own check never catches because Music Assistant runs `network_mode: host` | | Both core hosts on one address | The twinning's single assumption | | A host colliding with the gateway | — | | Duplicate kiosk hostnames | They identify devices on the network and in HA | | Placeholder or low-entropy tokens | Caught *before* the length check, so padding `changeme` out to 32 characters doesn't sneak past | | A private key pasted where the public key goes | — | | A kiosk that needs a disabled service | A door panel built against `enable.identity: false` builds fine and fails at runtime | | Wi-Fi SSID without a PSK, MQTT user without a password | — | | `192.168.0.x` / `192.168.1.x` subnets (warning) | Collides with typical café and hotel LANs, which breaks a WireGuard split tunnel routing that range — see `docs/network-integration.md` §2.1 | Warnings print but don't block. Errors block and nothing is written. ## What's here | Script | Builds | |---|---| | **`build-all.sh`** | **Everything. The normal entry point** — `--core`, `--kiosks`, `--dry-run` | | `build-core-pair.sh` | The twinned container host + LLM host | | `build-container-host-iso.sh` | Just the container host | | `build-llm-host-iso.sh` | Just the LLM host | | `build-door-panel-iso.sh` etc. | One kiosk; takes a hostname when several of a type are configured | | `build-audio-endpoint-iso-amd64.sh` | An amd64 audio endpoint (mini PC, live-build → `.iso`) | | `build-audio-endpoint-image-arm64.sh` | An arm64 audio endpoint (Pi + HiFiBerry, rpi-image-gen → `.img`) | | `setup-container-host.sh` | The container-host setup itself, run by its ISO's first-boot unit (or by hand) | | `setup-llm-host.sh` | Same, for the LLM host | | `validate-config.py` | Check the config | | `config-export.py` | Config → shell variables, deriving URLs. Where the twinning happens | | `lib/coreconfig.sh` | The loader every builder sources | Building one image is supported but unusual: the images are a set that has to agree with itself, which is why `build-all.sh` is the default and a failure in one image doesn't abandon the rest. ## The ISOs contain secrets This is deliberate — burning everything in is what makes installation unattended, with no env files to edit on a freshly-booted host. It also means **every ISO is a credential**: Wi-Fi PSK, service tokens, MQTT and HA credentials, all readable by anyone holding the stick. `.gitignore` covers `CoreSystemConfig.json` and `build-output/`, so neither can be committed by accident. Wiping old USB sticks is on you. ## Two things can't be burnt in Neither exists at build time, so both need a human afterwards: 1. **`HA_TOKEN`** — a Long-Lived Access Token from Home Assistant's own UI, which doesn't exist until HA has been started and an account created. Put it in the config 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 (Developer Tools → States). The shipped default is a guess and it's the highest-risk unknown in Phase 6. ## Adding a service or a kiosk - **A new port**: add it to `ports` and reference it in `config-export.py`'s derived URLs. The duplicate check covers it from then on. - **Another audio endpoint**: add an entry to `audio_endpoints` with its `arch`. `build-all.sh` builds every entry, so listing both an `amd64` and an `arm64` one gets you both — they're separate toolchains producing different artifacts, not one image that runs on both. - **Another kiosk of an existing type**: add an entry to `kiosks` with its own hostname. `build-all.sh` picks it up; the per-type builder takes the hostname as an argument. - **A new kiosk type**: add it to `KIOSK_TYPES` in `validate-config.py` and add a `build--iso.sh`. `build-all.sh` finds it by naming convention. ## Manual verification still outstanding 1. **No ISO has ever been built with this.** `lb build` needs live-build, root, and a long network fetch; none of that has been run. What *has* been tested is everything up to that point: config validation and URL derivation (43 checks), and the builders' generated artifacts — env files, preseed, network config, `/etc/hosts`, first-boot units, build stamps — produced by the real code paths with only `lb` stubbed (44 checks). The `lb config`/`lb build` invocations themselves are unverified. 2. **The preseed files are written from Debian's documented shape, not tested.** An unattended install that gets a preseed key wrong typically stops at an interactive prompt rather than failing loudly, so budget for a monitor on the first install. 3. **`partman-auto/disk` erases the configured disk without confirmation.** That is what unattended means, and it's why `install_disk` is worth double-checking against the actual machine you boot it on. 4. **Static addressing assumes `eth0`.** Debian's predictable interface naming may well call it `enp3s0` on your hardware, in which case `/etc/network/interfaces.d/smarthome` needs the real name. 5. **The kiosk builders' migration is untested end-to-end.** Their config blocks now read from `CoreSystemConfig.json`, but the body of each script is unchanged from when it worked with hand-edited constants — so the risk is confined to the mapping, not to image contents.