BetterFrame/deploy/scripts/setup-pi-kiosk.sh

192 lines
7.7 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# Single-host BetterFrame installer/updater for Raspberry Pi 5 (Bookworm+).
#
# What it does (every step idempotent — safe to re-run for updates):
# 1. clone or `git pull` the BetterFrame repo into the invoking user's home
# 2. install OS deps: Docker + compose, GTK4/GStreamer/WebKit dev libs,
# cage + seatd, rust toolchain (via rustup) if missing
# 3. bring up the Docker stack (server + Angie proxy + Node-RED) via
# `docker compose up -d --build`
# 4. build the Rust kiosk binary as the invoking user, install to
# /opt/betterframe/kiosk/betterframe-kiosk
# 5. provision the unprivileged `bfkiosk` user + cage PAM stack + systemd
# unit, disable any display manager, set multi-user.target as default
# 6. enable + start the kiosk service
#
# Re-run after every git change to pull + rebuild + redeploy.
#
# Usage: sudo ./deploy/scripts/setup-pi-kiosk.sh
#
# Env overrides:
# BF_HOME=/path/to/repo override repo location (default: $HOME/betterframe)
# BF_REPO_URL=git@… override clone URL (default: github)
# SKIP_BUILD=1 skip kiosk cargo build (expects existing binary)
# SKIP_DOCKER=1 skip `docker compose up -d --build`
# SKIP_KIOSK=1 server-only host (no kiosk service / cage / tty1)
# SKIP_SERVER=1 kiosk-only host (no Docker stack)
set -euo pipefail
if [ "$EUID" -ne 0 ]; then
echo "error: must run as root (sudo $0)" >&2
exit 1
fi
REPO_URL="${BF_REPO_URL:-https://github.com/BetterCorp/BetterFrame.git}"
BUILD_USER="${SUDO_USER:-$(id -un)}"
if [ "${BUILD_USER}" = "root" ]; then
echo "error: refuses to run as root without SUDO_USER set. Use 'sudo ...' from a normal user." >&2
exit 1
fi
USER_HOME="$(getent passwd "${BUILD_USER}" | cut -d: -f6)"
BF_HOME="${BF_HOME:-${USER_HOME}/betterframe}"
run_as_user() {
# Run in a login shell so ~/.cargo/env, ~/.profile etc. are sourced.
sudo -u "${BUILD_USER}" -H -i sh -c "$1"
}
# ----------------------------------------------------------------------------
# 1. Base packages
# ----------------------------------------------------------------------------
echo "==> Installing base packages"
apt-get update
apt-get install -y --no-install-recommends \
git ca-certificates curl gnupg lsb-release sudo
# ----------------------------------------------------------------------------
# 2. Clone or update repo
# ----------------------------------------------------------------------------
if [ -d "${BF_HOME}/.git" ]; then
echo "==> Updating repo at ${BF_HOME}"
run_as_user "git -C '${BF_HOME}' fetch --prune origin && git -C '${BF_HOME}' pull --ff-only"
else
echo "==> Cloning ${REPO_URL}${BF_HOME}"
install -d -o "${BUILD_USER}" -g "${BUILD_USER}" "$(dirname "${BF_HOME}")"
run_as_user "git clone '${REPO_URL}' '${BF_HOME}'"
fi
REPO_ROOT="${BF_HOME}"
# ----------------------------------------------------------------------------
# 3. Docker engine + compose plugin
# ----------------------------------------------------------------------------
if [ "${SKIP_SERVER:-0}" != "1" ] && [ "${SKIP_DOCKER:-0}" != "1" ]; then
if ! command -v docker >/dev/null 2>&1; then
echo "==> Installing Docker (via convenience script)"
curl -fsSL https://get.docker.com | sh
else
echo "==> Docker already installed: $(docker --version)"
fi
if ! docker compose version >/dev/null 2>&1; then
apt-get install -y --no-install-recommends docker-compose-plugin
fi
# Let BUILD_USER use docker without sudo (idempotent).
if ! id -nG "${BUILD_USER}" | tr ' ' '\n' | grep -qx docker; then
usermod -aG docker "${BUILD_USER}"
echo " added ${BUILD_USER} to docker group (re-login required for that user)"
fi
systemctl enable --now docker
fi
# ----------------------------------------------------------------------------
# 4. Kiosk runtime deps (GTK/GStreamer/WebKit + cage + seatd)
# ----------------------------------------------------------------------------
if [ "${SKIP_KIOSK:-0}" != "1" ]; then
echo "==> Installing kiosk runtime deps"
apt-get install -y --no-install-recommends \
cage seatd \
libgtk-4-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
libwebkitgtk-6.0-dev pkg-config build-essential \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-libav \
v4l-utils wlr-randr
systemctl enable --now seatd
fi
# ----------------------------------------------------------------------------
# 5. Rust toolchain (kiosk build prerequisite)
# ----------------------------------------------------------------------------
if [ "${SKIP_KIOSK:-0}" != "1" ] && [ "${SKIP_BUILD:-0}" != "1" ]; then
if ! run_as_user "command -v cargo >/dev/null 2>&1"; then
echo "==> Installing rustup as ${BUILD_USER}"
run_as_user "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal"
fi
fi
# ----------------------------------------------------------------------------
# 6. Build + start Docker stack
# ----------------------------------------------------------------------------
if [ "${SKIP_SERVER:-0}" != "1" ] && [ "${SKIP_DOCKER:-0}" != "1" ]; then
echo "==> Building + starting Docker stack"
run_as_user "cd '${REPO_ROOT}' && docker compose -f deploy/docker/docker-compose.yml up -d --build"
fi
# ----------------------------------------------------------------------------
# 7. Build + install kiosk binary
# ----------------------------------------------------------------------------
if [ "${SKIP_KIOSK:-0}" != "1" ]; then
BIN_SRC="${REPO_ROOT}/kiosk/target/release/betterframe-kiosk"
BIN_DST_DIR="/opt/betterframe/kiosk"
BIN_DST="${BIN_DST_DIR}/betterframe-kiosk"
if [ "${SKIP_BUILD:-0}" != "1" ]; then
echo "==> Building kiosk binary (release)"
run_as_user "cd '${REPO_ROOT}' && cargo build --release --manifest-path kiosk/Cargo.toml"
fi
if [ ! -f "${BIN_SRC}" ]; then
echo "error: kiosk binary not found at ${BIN_SRC}." >&2
exit 1
fi
install -d -m 755 "${BIN_DST_DIR}"
install -m 755 "${BIN_SRC}" "${BIN_DST}"
echo " installed → ${BIN_DST}"
# --------------------------------------------------------------------------
# 8. bfkiosk user + PAM + systemd unit
# --------------------------------------------------------------------------
echo "==> Provisioning bfkiosk user"
if ! id -u bfkiosk >/dev/null 2>&1; then
useradd -m -s /usr/sbin/nologin bfkiosk
fi
for grp in video render input audio seat; do
if getent group "$grp" >/dev/null; then
usermod -a -G "$grp" bfkiosk
fi
done
echo "==> Disabling display managers + getty@tty1"
for dm in lightdm gdm gdm3 sddm; do
systemctl disable --now "${dm}.service" 2>/dev/null || true
done
systemctl set-default multi-user.target
systemctl disable --now getty@tty1.service 2>/dev/null || true
echo "==> Installing PAM + systemd unit"
install -m 644 "${REPO_ROOT}/deploy/pam.d/cage" /etc/pam.d/cage
install -m 644 "${REPO_ROOT}/deploy/systemd/betterframe-kiosk.service" \
/etc/systemd/system/betterframe-kiosk.service
if [ ! -e /etc/default/betterframe-kiosk ]; then
cat > /etc/default/betterframe-kiosk <<'EOF'
# Runtime env for betterframe-kiosk. Edit and `systemctl restart betterframe-kiosk`.
# BETTERFRAME_SERVER=http://192.168.1.10
EOF
fi
systemctl daemon-reload
systemctl enable betterframe-kiosk.service
# Restart picks up new binary on re-run.
systemctl restart betterframe-kiosk.service || true
fi
echo "==> Done."
if [ "${SKIP_SERVER:-0}" != "1" ] && [ "${SKIP_DOCKER:-0}" != "1" ]; then
echo " Server stack: http://$(hostname -I | awk '{print $1}')/"
fi
if [ "${SKIP_KIOSK:-0}" != "1" ]; then
echo " Kiosk service: systemctl status betterframe-kiosk"
fi