#!/usr/bin/env bash # # Smart Home Steam-TV-Box ISO Builder # Target: builds a Debian live ISO on a Debian/Ubuntu build machine # # Drives `lb config && lb build` over hosts/steam-tv-box/live-build/ to produce the # living-room gaming + media image: # - greetd autologin straight into a kiosk Sway session (no greeter UI) # - boots into STEAM BIG PICTURE and nothing else # - native, local gaming: steam-installer + i386 multiarch + the full Mesa/Vulkan # stack (this box has a real GPU; it is NOT the thin client's Steam Link streaming) # - Prism Launcher (Minecraft), registered as a non-Steam game so it runs *through* # Steam with Steam Input and the Steam Controller API live # - a media session — Firefox (uBlock Origin + SponsorBlock), Spotify, mpv — that is # launched LAZILY, the first time somebody leaves Big Picture # - steamtv-agent (Python, systemd) — HA MQTT-discovery entities + swaymsg control # - wayvnc for administration (password mandatory, fails closed) # # This script is also the single point that keeps configs/ (the human-edited source of # truth, reviewed in git) in sync with live-build/config/includes.chroot/ (the # generated tree that actually gets baked into the image). Never hand-edit anything # under includes.chroot — it is wiped and regenerated on every run. The same now goes # for live-build/config/preseed.cfg, which is generated from configs/installer/. # # Run as: sudo -E tools/build-steam-tv-box-iso.sh [hostname] # # Configuration comes from CoreSystemConfig.json — see tools/README.md. set -euo pipefail # --------------------------------------------------------------------------- # CONFIGURATION — comes from CoreSystemConfig.json, NOT from this file. # # sudo -E tools/build-steam-tv-box-iso.sh # the only steam-tv-box # sudo -E tools/build-steam-tv-box-iso.sh # a specific one, if several # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/coreconfig.sh source "${SCRIPT_DIR}/lib/coreconfig.sh" core_select_kiosk "steam-tv-box" "${1:-}" # CORE_KIOSK_DEBIAN_RELEASE is the per-kiosk `debian_release` override, falling back to # household.debian_release when the kiosk does not set one. It exists mostly for this # image: the household default is bookworm, whose Mesa (22.3) predates most of the # driver work current titles depend on. Every other host here is a browser and a # Python agent and does not care what Mesa it has; this one is the only machine in the # project that renders anything demanding, and it is also the only one where being a # release behind cannot be fixed later by editing a config file. Set # "debian_release": "trixie" on this kiosk — see CoreSystemConfig.json.template. DEBIAN_RELEASE="$CORE_KIOSK_DEBIAN_RELEASE" KIOSK_USERNAME="$CORE_KIOSK_USERNAME" IMAGE_HOSTNAME="$CORE_KIOSK_HOSTNAME" KEYBOARD_LAYOUT="$CORE_KEYBOARD_LAYOUT" ENABLE_INSTALLER="$CORE_KIOSK_ENABLE_INSTALLER" MQTT_BROKER_HOST="$CORE_MQTT_BROKER_HOST" MQTT_BROKER_PORT="$CORE_MQTT_BROKER_PORT" MQTT_USERNAME="$CORE_MQTT_USERNAME" MQTT_PASSWORD="$CORE_MQTT_PASSWORD" SSH_AUTHORIZED_KEY="$CORE_SSH_AUTHORIZED_KEY" STEAMTV_NAME="$CORE_KIOSK_FRIENDLY_NAME" # The HA area this device physically sits in, from the kiosk's `room` in # CoreSystemConfig.json. The agent publishes it as suggested_area so HA files the # device in the right room by itself — see docs/rooms-and-endpoints.md. STEAMTV_ROOM="${CORE_KIOSK_ROOM:-}" HA_URL="$CORE_HA_URL" # Which GPU is in this machine: amd | intel | nvidia. Read by 0300-steam.hook.chroot # (to install the NVIDIA driver, or deliberately not to) and by kiosk-session (the # wlroots cursor workaround). Guessing this wrong on an AMD box by installing the # NVIDIA driver actively breaks it, which is why it is configuration and not detection. GPU_VENDOR="${CORE_KIOSK_GPU_VENDOR:-amd}" ENABLE_CEC="${CORE_KIOSK_ENABLE_CEC:-true}" # --------------------------------------------------------------------------- # Paths # --------------------------------------------------------------------------- HOST_DIR="${CORE_REPO_ROOT}/hosts/steam-tv-box" CONFIGS_DIR="${HOST_DIR}/configs" AGENT_DIR="${HOST_DIR}/agent" LIVE_BUILD_DIR="${HOST_DIR}/live-build" INCLUDES="${LIVE_BUILD_DIR}/config/includes.chroot" PACKAGE_LIST="${LIVE_BUILD_DIR}/config/package-lists/steam-tv-box.list.chroot" # --------------------------------------------------------------------------- # Sanity checks # --------------------------------------------------------------------------- if [[ $EUID -ne 0 ]]; then echo "Warning: not running as root. 'lb build' needs root to bootstrap and chroot," echo " and will fail partway through. Re-run with: sudo $0" echo " Continuing anyway so you can at least regenerate includes.chroot..." fi if ! grep -qi "debian\|ubuntu" /etc/os-release; then echo "Warning: live-build targets a Debian/Ubuntu build host. Proceeding anyway..." fi if ! command -v lb &> /dev/null; then if [[ $EUID -eq 0 ]]; then echo "--- Installing live-build ---" apt-get update apt-get install -y live-build else echo "live-build is not installed and this script is not running as root." >&2 echo " Install it first: sudo apt-get install live-build" >&2 exit 1 fi else echo "--- live-build already installed, skipping ---" fi if [[ ! -f "$PACKAGE_LIST" ]]; then echo "Missing package list: $PACKAGE_LIST" >&2 exit 1 fi case "$GPU_VENDOR" in amd|intel|nvidia) ;; *) echo "gpu_vendor is '${GPU_VENDOR}' — must be one of: amd, intel, nvidia." >&2 echo " Fix it on this kiosk in CoreSystemConfig.json and re-run." >&2 exit 1 ;; esac if [[ "$DEBIAN_RELEASE" == "bookworm" ]]; then echo "Note: building this image against bookworm. Its Mesa is 22.3, which is old for" echo " a machine whose job is running games locally — expect missing Vulkan features" echo " and poor performance in anything recent. Set \"debian_release\": \"trixie\" on" echo " this kiosk in CoreSystemConfig.json unless you have a specific reason not to." fi echo echo "=== Smart Home Steam-TV-Box ISO Builder ===" echo "Debian release : $DEBIAN_RELEASE" echo "Kiosk user : $KIOSK_USERNAME" echo "Image hostname : $IMAGE_HOSTNAME" echo "GPU vendor : $GPU_VENDOR" echo "MQTT broker : ${MQTT_BROKER_HOST}:${MQTT_BROKER_PORT}" echo "Home Assistant : $HA_URL" echo "HDMI-CEC : $ENABLE_CEC" echo "Debian installer : $ENABLE_INSTALLER" echo "Keyboard layout : $KEYBOARD_LAYOUT" echo # --------------------------------------------------------------------------- # 1. Regenerate includes.chroot from configs/ and agent/ # --------------------------------------------------------------------------- echo "--- Regenerating $INCLUDES ---" rm -rf "$INCLUDES" mkdir -p \ "$INCLUDES/etc/greetd" \ "$INCLUDES/etc/wayvnc" \ "$INCLUDES/etc/steamtv-agent" \ "$INCLUDES/etc/firefox/policies" \ "$INCLUDES/etc/steamtv-firefox" \ "$INCLUDES/usr/local/bin" \ "$INCLUDES/opt/steamtv-agent" \ "$INCLUDES/home/${KIOSK_USERNAME}/.config/sway" \ "$INCLUDES/home/${KIOSK_USERNAME}/.config/mpv" \ "$INCLUDES/home/${KIOSK_USERNAME}/.ssh" # @KIOSK_USERNAME@ and @KEYBOARD_LAYOUT@ are the only templated tokens in the configs. # Everything else the hooks need is read at build time from /etc/steamtv-agent/ # config.env (written below), which live-build copies in via chroot_local-includes # *before* it runs chroot_local-hooks — that ordering is what lets the hooks be plain # scripts with no outer-shell variables of their own. subst() { sed -e "s/@KIOSK_USERNAME@/${KIOSK_USERNAME}/g" \ -e "s/@KEYBOARD_LAYOUT@/${KEYBOARD_LAYOUT}/g" "$1" > "$2" } subst "${CONFIGS_DIR}/greetd/config.toml" "$INCLUDES/etc/greetd/config.toml" subst "${CONFIGS_DIR}/wayvnc/config" "$INCLUDES/etc/wayvnc/config" subst "${CONFIGS_DIR}/sway/config" "$INCLUDES/home/${KIOSK_USERNAME}/.config/sway/config" subst "${AGENT_DIR}/steamtv-agent.service" "$INCLUDES/opt/steamtv-agent/steamtv-agent.service" install -m 0755 "${CONFIGS_DIR}/greetd/kiosk-session" "$INCLUDES/usr/local/bin/kiosk-session" install -m 0755 "${CONFIGS_DIR}/wayvnc/start-wayvnc" "$INCLUDES/usr/local/bin/start-wayvnc" # Bound to the remote's power/sleep buttons — see the Sway config's remote-control # section for why those turn the display off rather than the machine. install -m 0755 "${CONFIGS_DIR}/sway/display-toggle" "$INCLUDES/usr/local/bin/display-toggle" # The session: the two halves of the leave-Big-Picture detection, the launchers they # call, and the Steam shortcut registration that puts Prism inside Steam Input. install -m 0755 "${CONFIGS_DIR}/session/steam-session" "$INCLUDES/usr/local/bin/steam-session" install -m 0755 "${CONFIGS_DIR}/session/steam-big-picture" "$INCLUDES/usr/local/bin/steam-big-picture" install -m 0755 "${CONFIGS_DIR}/session/steam-shortcut-prism" "$INCLUDES/usr/local/bin/steam-shortcut-prism" install -m 0755 "${CONFIGS_DIR}/session/session-watcher" "$INCLUDES/usr/local/bin/session-watcher" install -m 0755 "${CONFIGS_DIR}/session/media-session" "$INCLUDES/usr/local/bin/media-session" install -m 0755 "${CONFIGS_DIR}/session/media-player" "$INCLUDES/usr/local/bin/media-player" install -m 0755 "${CONFIGS_DIR}/session/spotify-launch" "$INCLUDES/usr/local/bin/spotify-launch" install -m 0755 "${CONFIGS_DIR}/session/prism-launch" "$INCLUDES/usr/local/bin/prism-launch" # fleet-bootstrap: fetch this machine's published monitoring script and run it. Opt-in — # with no /etc/fleet-bootstrap.conf it exits 0 and does nothing. install -m 0755 "${CORE_REPO_ROOT}/tools/fleet-bootstrap.sh" "$INCLUDES/usr/local/bin/fleet-bootstrap" install -m 0644 "${CONFIGS_DIR}/mpv/mpv.conf" "$INCLUDES/home/${KIOSK_USERNAME}/.config/mpv/mpv.conf" # Console (VT/TTY) keymap — separate from Sway's own xkb_layout above, since greetd # briefly owns the console before Sway starts. mkdir -p "$INCLUDES/etc/default" cat > "$INCLUDES/etc/default/keyboard" < "$INCLUDES/home/${KIOSK_USERNAME}/.ssh/authorized_keys" chmod 600 "$INCLUDES/home/${KIOSK_USERNAME}/.ssh/authorized_keys" echo " Baked an SSH authorized_keys entry for ${KIOSK_USERNAME}." else echo " No SSH_AUTHORIZED_KEY set — SSH admin access will not be possible on this image." fi # --------------------------------------------------------------------------- # 2. Runtime config, read by steamtv-agent, the sway session wrapper, and the hooks # --------------------------------------------------------------------------- echo "--- Writing /etc/steamtv-agent/config.env into includes.chroot ---" cat > "$INCLUDES/etc/steamtv-agent/config.env" < "${LIVE_BUILD_DIR}/config/preseed.cfg" # --------------------------------------------------------------------------- # 4. lb config # --------------------------------------------------------------------------- cd "$LIVE_BUILD_DIR" chmod +x config/hooks/normal/*.hook.chroot if [[ -e .build ]]; then echo "--- Previous build found, running 'lb clean' (package cache is kept) ---" lb clean fi INSTALLER_MODE="none" if [[ "$ENABLE_INSTALLER" == "true" ]]; then INSTALLER_MODE="live" fi echo "--- Running lb config ---" # live-build auto-discovers config/package-lists/*.list.chroot and # config/hooks/normal/*.hook.chroot; there is no flag to point at them individually. # # --archive-areas: contrib is REQUIRED — steam-installer lives there and # 0300-steam.hook.chroot fails without it, producing an image with no Steam on it. # non-free is required for the NVIDIA driver (and is harmless on an AMD/Intel box, # since nothing from it is installed unless gpu_vendor says so). lb config \ --distribution "$DEBIAN_RELEASE" \ --architectures amd64 \ --linux-flavours amd64 \ --archive-areas "main contrib non-free non-free-firmware" \ --binary-images iso-hybrid \ --debian-installer "$INSTALLER_MODE" \ --iso-application "SmartestHome Steam TV Box" \ --iso-publisher "SmartestHome" \ --iso-volume "smarthome-steam-tv-box" \ --memtest none \ --bootappend-live "boot=live components quiet splash noautologin username=${KIOSK_USERNAME} hostname=${IMAGE_HOSTNAME}" # noautologin: live-config would otherwise autologin its own account on tty1 and fight # greetd for the VT (0200-greetd.hook.chroot masks getty@tty1 as a belt-and-braces # second line of defence). # --------------------------------------------------------------------------- # 5. lb build # --------------------------------------------------------------------------- echo "--- Running lb build (this takes a while and needs network) ---" echo " Longer than the other images: the i386 multiarch stack and Steam's" echo " dependency chain are a few hundred extra packages." 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}" ISO_PATH="$(core_publish_image "$ISO_PATH" "steam-tv-box" "$IMAGE_HOSTNAME")" echo echo "=== Done ===" echo "ISO written to:" echo " ${ISO_PATH}" echo echo "What's in it:" echo " greetd : autologin as '${KIOSK_USERNAME}' straight into Sway on vt1" echo " Boots to : Steam Big Picture, and nothing else" echo " Steam : NATIVE (steam-installer + i386 multiarch) — games run on this" echo " machine. This is not the thin client's Steam Link streaming." echo " GPU stack : Mesa/Vulkan (${GPU_VENDOR}), 64- and 32-bit" echo " Prism Launcher : Flathub, registered as a non-Steam game so it launches THROUGH" echo " Steam with Steam Input / the Steam Controller API active" echo " Media session : Firefox (uBlock Origin + SponsorBlock), Spotify, mpv —" echo " started the FIRST TIME you leave Big Picture, not at boot" echo " Sway : workspaces 1:steam / 2:games / 3:web / 4:media / 5:music" echo " wayvnc : 0.0.0.0:5900, auth REQUIRED, refuses to start without a password" echo " steamtv-agent : system service, MQTT ${MQTT_BROKER_HOST}:${MQTT_BROKER_PORT}" echo " Audio output : HA select, persisted to audio-config.json" echo " Keyboard layout : ${KEYBOARD_LAYOUT} (console + Sway)" echo " Maintenance shell: Super+Shift+Ctrl+M opens a floating terminal locally" echo echo "Next steps:" echo " 1. Write the ISO to a USB stick:" echo " sudo dd if=${ISO_PATH} of=/dev/sdX bs=4M status=progress oflag=sync" echo " (double-check /dev/sdX with 'lsblk' first — dd will happily eat the wrong disk)" if [[ "$ENABLE_INSTALLER" != "true" ]]; then echo " 2. NOTE: enable_installer is false, so this boots live — games would live in RAM" echo " and vanish on reboot. For a real gaming box set \"enable_installer\": true on" echo " this kiosk in CoreSystemConfig.json and rebuild, then install to the disk." else echo " 2. Boot the target machine and run the installer. Give /home its own large" echo " partition (or mount a games disk at /home/${KIOSK_USERNAME}/Games) — a Steam" echo " library and a few modpacks are hundreds of gigabytes." fi echo " 3. Confirm it lands in Big Picture with no login prompt, and that a gamepad is" echo " recognised (that is steam-devices' udev rules doing their job)." echo " 4. Check the box is actually accelerated before blaming any game — from the" echo " maintenance shell (Super+Shift+Ctrl+M):" echo " vulkaninfo | head -n 20 # should name your GPU, not llvmpipe" echo " glxinfo -B # same" echo " 5. SET THE WAYVNC PASSWORD — wayvnc will NOT be running until you do:" echo " sudo sh -c 'openssl rand -base64 24 > /etc/wayvnc/wayvnc-password'" echo " sudo chmod 600 /etc/wayvnc/wayvnc-password" echo " sudo chown ${KIOSK_USERNAME}:${KIOSK_USERNAME} /etc/wayvnc/wayvnc-password" echo " swaymsg reload" echo " 6. Log into Steam on the TV. Until you do, Prism cannot be registered as a" echo " non-Steam game (there is no user profile to register it in) and will fall" echo " back to launching directly, without Steam Input. After the first login," echo " restart the session (or reboot) — steam-session registers the shortcut on" echo " the way in, before Steam starts, because Steam overwrites shortcuts.vdf on" echo " exit. Then confirm 'Prism Launcher' is in the Big Picture library and that" echo " the pad works inside Minecraft." echo " 7. Leave Big Picture and confirm the media session appears: Firefox on 3:web," echo " mpv on 4:media, Spotify on 5:music. Then go back into a game and confirm" echo " they are NOT killed (that is deliberate — see configs/session/media-session)." echo " 8. Confirm steamtv-agent connected and registered:" echo " systemctl status steamtv-agent" echo " In Home Assistant, a '${STEAMTV_NAME}' device should appear under the MQTT" echo " integration with: Session (sensor), Mode (select), Screen (select), Launch" echo " buttons for Steam/Prism/browser/player/Spotify, Stop media apps (button)," echo " Audio output (select), Display (switch), Volume (number), Playback state" echo " (sensor) and the transport buttons." echo " 9. Set the audio output. On a TV box the right sink is usually HDMI, and if the" echo " set was off at boot WirePlumber may have defaulted to a headphone jack with" echo " nothing in it — the classic 'the game has no sound' report. Pick it in HA's" echo " 'Audio output' select; the choice persists across reboots." echo echo "Then pull the power on the container host and re-check: the box must still boot" echo "into Big Picture and play a game. steamtv-agent connects asynchronously and will" echo "simply keep retrying." echo echo "Rebuilding later: edit hosts/steam-tv-box/configs/* or agent/*, then re-run this" echo "script — includes.chroot is regenerated from them every time."