Monorepo for Aesthetic.Computer aesthetic.computer
at main 936 lines 34 kB view raw
1#!/bin/bash 2# make-alpine-kiosk.sh — Create an Alpine Linux kiosk USB with a bundled piece 3# 4# Builds a minimal Alpine rootfs from scratch using apk.static, producing a 5# ~400MB bootable image (vs ~4GB Fedora). Same piece injection / FEDAC-PIECE 6# partition layout as the Fedora script for oven compatibility. 7# 8# Boot flow: 9# Power on → GRUB (instant) → linux-lts kernel → OpenRC → Cage → Chromium 10# → bundled piece (offline, no WiFi needed) 11# 12# Usage: 13# sudo bash fedac/scripts/make-alpine-kiosk.sh <piece-code> [<device>] [options] 14# 15# Options: 16# --image <path> Build a bootable disk image file (.img) 17# --image-size <g> Image size in GiB (default: 1) 18# --base-image Build base image without a specific piece (placeholder only) 19# --density <n> Default pack/runtime density query value (default: 8) 20# --work-base <dir> Directory for large temp work files (default: /tmp) 21# --no-eject Don't eject when done 22# --yes Skip confirmation prompts 23# 24# Requirements: 25# squashfs-tools (mksquashfs), curl, parted, e2fsprogs, dosfstools 26 27set -euo pipefail 28 29RED='\033[0;31m' 30GREEN='\033[0;32m' 31YELLOW='\033[1;33m' 32CYAN='\033[0;36m' 33PINK='\033[38;5;205m' 34NC='\033[0m' 35 36SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 37FEDAC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" 38OVERLAY_DIR="$FEDAC_DIR/overlays/kiosk" 39REPO_ROOT="$(cd "$FEDAC_DIR/.." && pwd)" 40 41# Alpine 3.21 (edge for latest Chromium) 42ALPINE_VERSION="3.21" 43ALPINE_MIRROR="https://dl-cdn.alpinelinux.org/alpine" 44ALPINE_REPO_MAIN="${ALPINE_MIRROR}/v${ALPINE_VERSION}/main" 45ALPINE_REPO_COMMUNITY="${ALPINE_MIRROR}/v${ALPINE_VERSION}/community" 46APK_STATIC_URL="${ALPINE_MIRROR}/v${ALPINE_VERSION}/main/x86_64/apk-tools-static-2.14.6-r3.apk" 47APK_CACHE_DIR="${HOME}/.cache/alpine-apk" 48 49PACK_DENSITY_DEFAULT="8" 50 51usage() { 52 echo -e "${PINK}Alpine Kiosk Piece USB Creator${NC}" 53 echo "" 54 echo "Creates a minimal Alpine Linux USB that boots into a fullscreen piece (offline)." 55 echo "" 56 echo "Usage: sudo $0 <piece-code> [<device>] [options]" 57 echo "" 58 echo " <piece-code> Piece code (e.g., notepat, \$cow)" 59 echo " <device> Optional USB block device (e.g., /dev/sdb)" 60 echo "" 61 echo "Options:" 62 echo " --image <path> Build a bootable disk image file (.img)" 63 echo " --image-size <g> Image size in GiB (default: 1)" 64 echo " --base-image Build base image (placeholder piece)" 65 echo " --density <n> Runtime density (default: 8)" 66 echo " --work-base <dir> Temp directory (default: /tmp)" 67 echo " --no-eject Don't eject the USB when done" 68 echo " --yes Skip confirmation prompts" 69 echo " --help Show this help" 70 exit 1 71} 72 73cleanup() { 74 echo -e "\n${YELLOW}Cleaning up mounts...${NC}" 75 for mp in "${EFI_MOUNT:-}" "${LIVE_MOUNT:-}" "${PIECE_MOUNT_TMP:-}"; do 76 [ -n "$mp" ] && mountpoint -q "$mp" 2>/dev/null && umount "$mp" 2>/dev/null || true 77 done 78 if [ -n "${LOOP_DEV:-}" ]; then 79 losetup -d "$LOOP_DEV" 2>/dev/null || true 80 LOOP_DEV="" 81 fi 82} 83trap cleanup EXIT 84 85# ── Parse args ── 86PIECE_CODE="" 87DEVICE="" 88IMAGE_PATH="" 89IMAGE_SIZE_GB="1" 90WORK_BASE="/tmp" 91DO_EJECT=true 92SKIP_CONFIRM=false 93BASE_IMAGE_MODE=false 94PACK_DENSITY="$PACK_DENSITY_DEFAULT" 95 96while [ $# -gt 0 ]; do 97 case "$1" in 98 --image) IMAGE_PATH="$2"; shift 2 ;; 99 --image-size) IMAGE_SIZE_GB="$2"; shift 2 ;; 100 --density) PACK_DENSITY="$2"; shift 2 ;; 101 --work-base) WORK_BASE="$2"; shift 2 ;; 102 --base-image) BASE_IMAGE_MODE=true; shift ;; 103 --no-eject) DO_EJECT=false; shift ;; 104 --yes) SKIP_CONFIRM=true; shift ;; 105 --help|-h) usage ;; 106 /dev/*) DEVICE="$1"; shift ;; 107 *) if [ -z "$PIECE_CODE" ]; then PIECE_CODE="$1"; shift; else echo -e "${RED}Unknown arg: $1${NC}"; usage; fi ;; 108 esac 109done 110 111if [ "$BASE_IMAGE_MODE" = true ]; then 112 [ -n "$PIECE_CODE" ] || PIECE_CODE="__base__" 113else 114 [ -n "$PIECE_CODE" ] || { echo -e "${RED}Error: No piece code specified${NC}"; usage; } 115fi 116[ -n "$DEVICE" ] || [ -n "$IMAGE_PATH" ] || { 117 echo -e "${RED}Error: Specify a USB device and/or --image path${NC}" 118 usage 119} 120[ -z "$DEVICE" ] || [ -b "$DEVICE" ] || { echo -e "${RED}Error: $DEVICE is not a block device${NC}"; exit 1; } 121[ -z "$IMAGE_PATH" ] || [[ "$IMAGE_SIZE_GB" =~ ^[0-9]+$ ]] || { 122 echo -e "${RED}Error: --image-size must be an integer GiB value${NC}" 123 exit 1 124} 125[[ "$PACK_DENSITY" =~ ^[1-9][0-9]*$ ]] || { 126 echo -e "${RED}Error: --density must be a positive integer${NC}" 127 exit 1 128} 129[ -d "$WORK_BASE" ] || mkdir -p "$WORK_BASE" 130 131if [ "$(id -u)" -ne 0 ]; then 132 echo -e "${RED}Error: Must run as root (sudo)${NC}" 133 exit 1 134fi 135 136# Safety: refuse system disks 137if [ -n "$DEVICE" ]; then 138 case "$DEVICE" in 139 /dev/nvme*|/dev/vda|/dev/xvda) 140 echo -e "${RED}REFUSED: $DEVICE looks like a system disk.${NC}" 141 exit 1 142 ;; 143 /dev/sda) 144 RM_FLAG="$(lsblk -dn -o RM "$DEVICE" 2>/dev/null || echo "")" 145 if [ "$RM_FLAG" != "1" ]; then 146 echo -e "${RED}REFUSED: $DEVICE looks like a system disk.${NC}" 147 exit 1 148 fi 149 ;; 150 esac 151fi 152 153# Ensure required tools are installed 154if ! command -v mksquashfs &>/dev/null; then 155 echo -e " Installing squashfs-tools..." 156 apt-get install -y squashfs-tools >/dev/null 2>&1 || true 157fi 158 159# Check tools 160for tool in mksquashfs curl parted mkfs.vfat mkfs.ext4 losetup; do 161 command -v "$tool" &>/dev/null || { echo -e "${RED}Error: $tool not found${NC}"; exit 1; } 162done 163 164echo -e "${PINK}╔═══════════════════════════════════════════════╗${NC}" 165echo -e "${PINK}║ Alpine Kiosk Piece USB Creator ║${NC}" 166echo -e "${PINK}║ Piece: ${CYAN}${PIECE_CODE}${PINK} → offline bootable USB ║${NC}" 167echo -e "${PINK}╚═══════════════════════════════════════════════╝${NC}" 168echo "" 169 170WORK_DIR=$(mktemp -d "$WORK_BASE/alpine-kiosk-XXXX") 171echo -e "Work dir: $WORK_DIR" 172echo "" 173 174OWN_ROOTFS=true 175ROOTFS_DIR="$WORK_DIR/rootfs" 176EFI_MOUNT="" 177LIVE_MOUNT="" 178PIECE_MOUNT_TMP="" 179LOOP_DEV="" 180 181# ══════════════════════════════════════════ 182# Step 1: Fetch piece bundle 183# ══════════════════════════════════════════ 184BUNDLE_PATH="$WORK_DIR/piece.html" 185 186if [ "$BASE_IMAGE_MODE" = true ]; then 187 echo -e "${CYAN}[1/6] Base image mode — generating placeholder piece...${NC}" 188 cat > "$BUNDLE_PATH" << 'PLACEHOLDER_EOF' 189<!DOCTYPE html><html><head><meta charset="utf-8"><title>FedOS Base</title></head> 190<body style="background:#000;color:#fff;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;font-family:monospace"> 191<p>No piece loaded. Build with aesthetic.computer/os</p> 192</body></html> 193PLACEHOLDER_EOF 194else 195 echo -e "${CYAN}[1/6] Fetching piece bundle from oven...${NC}" 196 OVEN_URL="https://oven.aesthetic.computer/pack-html" 197 if [[ "$PIECE_CODE" == \$* ]]; then 198 FETCH_URL="${OVEN_URL}?code=${PIECE_CODE}&density=${PACK_DENSITY}" 199 else 200 FETCH_URL="${OVEN_URL}?piece=${PIECE_CODE}&density=${PACK_DENSITY}" 201 fi 202 echo -e " Fetching: ${FETCH_URL}" 203 HTTP_CODE=$(curl -sSL -w '%{http_code}' -o "$BUNDLE_PATH" "$FETCH_URL" 2>/dev/null) 204 if [ "$HTTP_CODE" != "200" ]; then 205 echo -e "${RED}Error: Oven returned HTTP ${HTTP_CODE}${NC}" 206 exit 1 207 fi 208 BUNDLE_SIZE=$(stat -c%s "$BUNDLE_PATH") 209 echo -e " ${GREEN}Bundle fetched: $(numfmt --to=iec $BUNDLE_SIZE)${NC}" 210fi 211 212# ══════════════════════════════════════════ 213# Step 2: Bootstrap Alpine rootfs 214# ══════════════════════════════════════════ 215echo -e "${CYAN}[2/6] Building Alpine rootfs...${NC}" 216 217# 2a. Get apk.static (runs on any Linux host) 218mkdir -p "$APK_CACHE_DIR" 219APK_STATIC="$APK_CACHE_DIR/apk.static" 220if [ ! -x "$APK_STATIC" ]; then 221 echo -e " Downloading apk-tools-static..." 222 APK_TMP="$APK_CACHE_DIR/apk-tools-static.apk" 223 curl -sSL -o "$APK_TMP" "$APK_STATIC_URL" 224 # apk.static is inside the .apk at sbin/apk.static (it's a tar.gz) 225 tar -xzf "$APK_TMP" -C "$APK_CACHE_DIR" sbin/apk.static 2>/dev/null || \ 226 tar -xf "$APK_TMP" -C "$APK_CACHE_DIR" sbin/apk.static 2>/dev/null 227 mv "$APK_CACHE_DIR/sbin/apk.static" "$APK_STATIC" 228 rmdir "$APK_CACHE_DIR/sbin" 2>/dev/null || true 229 rm -f "$APK_TMP" 230 chmod +x "$APK_STATIC" 231 echo -e " ${GREEN}apk.static ready${NC}" 232fi 233 234# 2b. Bootstrap the rootfs 235mkdir -p "$ROOTFS_DIR" 236 237echo -e " Installing Alpine base + kiosk packages..." 238$APK_STATIC \ 239 --root "$ROOTFS_DIR" \ 240 --initdb \ 241 --allow-untrusted \ 242 --no-progress \ 243 --repository "$ALPINE_REPO_MAIN" \ 244 --repository "$ALPINE_REPO_COMMUNITY" \ 245 add \ 246 alpine-base \ 247 busybox \ 248 openrc \ 249 linux-lts \ 250 linux-firmware-intel \ 251 linux-firmware-amdgpu \ 252 mesa-dri-gallium \ 253 mesa-egl \ 254 mesa-gl \ 255 mesa-gbm \ 256 libdrm \ 257 chromium \ 258 cage \ 259 wlroots \ 260 pipewire \ 261 wireplumber \ 262 pipewire-pulse \ 263 alsa-lib \ 264 alsa-plugins-pulse \ 265 eudev \ 266 dbus \ 267 font-noto \ 268 ttf-dejavu \ 269 python3 \ 270 seatd \ 271 xwayland 272 273echo -e " ${GREEN}Alpine packages installed${NC}" 274 275# 2c. Configure Alpine repos inside rootfs 276mkdir -p "$ROOTFS_DIR/etc/apk" 277cat > "$ROOTFS_DIR/etc/apk/repositories" << EOF 278${ALPINE_REPO_MAIN} 279${ALPINE_REPO_COMMUNITY} 280EOF 281 282# 2d. Configure hostname and basic system 283echo "fedos" > "$ROOTFS_DIR/etc/hostname" 284cat > "$ROOTFS_DIR/etc/hosts" << EOF 285127.0.0.1 localhost fedos 286::1 localhost fedos 287EOF 288 289# 2e. Create kiosk user 290echo -e " Creating kioskuser..." 291chroot "$ROOTFS_DIR" /usr/sbin/adduser -D -s /bin/sh -h /home/kioskuser kioskuser 2>/dev/null || true 292chroot "$ROOTFS_DIR" /usr/sbin/addgroup kioskuser video 2>/dev/null || true 293chroot "$ROOTFS_DIR" /usr/sbin/addgroup kioskuser audio 2>/dev/null || true 294chroot "$ROOTFS_DIR" /usr/sbin/addgroup kioskuser input 2>/dev/null || true 295chroot "$ROOTFS_DIR" /usr/sbin/addgroup kioskuser seat 2>/dev/null || true 296 297# 2f. Configure inittab for autologin on tty1 298cat > "$ROOTFS_DIR/etc/inittab" << 'EOF' 299::sysinit:/sbin/openrc sysinit 300::sysinit:/sbin/openrc boot 301::wait:/sbin/openrc default 302tty1::respawn:/bin/login -f kioskuser 303tty2::askfirst:/bin/login 304::ctrlaltdel:/sbin/reboot 305::shutdown:/sbin/openrc shutdown 306EOF 307 308# 2g. Profile script — auto-start kiosk session on tty1 309mkdir -p "$ROOTFS_DIR/home/kioskuser" 310cat > "$ROOTFS_DIR/home/kioskuser/.profile" << 'PROFILEEOF' 311# Auto-start kiosk session on tty1 (match /dev/tty1, /dev/console, or unknown tty) 312# On some EFI hardware busybox init reports /dev/console instead of /dev/tty1 313_tty=$(tty 2>/dev/null) 314case "$_tty" in 315 /dev/tty1|/dev/console|"not a tty"|"") 316 exec /usr/local/bin/kiosk-session.sh 317 ;; 318esac 319PROFILEEOF 320chown -R 1000:1000 "$ROOTFS_DIR/home/kioskuser" 2>/dev/null || true 321 322# 2h. fstab — tmpfs mounts required for read-only SquashFS root 323cat > "$ROOTFS_DIR/etc/fstab" << 'EOF' 324# SquashFS root is read-only; writable layers via tmpfs 325tmpfs /tmp tmpfs nosuid,nodev,mode=1777 0 0 326tmpfs /run tmpfs nosuid,nodev,mode=0755 0 0 327tmpfs /var/log tmpfs nosuid,nodev,mode=0755 0 0 328tmpfs /var/tmp tmpfs nosuid,nodev,mode=1777 0 0 329tmpfs /var/cache tmpfs nosuid,nodev,mode=0755 0 0 330EOF 331 332# Symlinks that Alpine expects (some scripts use /var/run instead of /run) 333ln -sf /run "$ROOTFS_DIR/var/run" 334ln -sf /run/lock "$ROOTFS_DIR/var/lock" 335 336echo -e " ${GREEN}Alpine rootfs configured${NC}" 337 338# ══════════════════════════════════════════ 339# Step 3: Inject kiosk config 340# ══════════════════════════════════════════ 341echo -e "${CYAN}[3/6] Injecting kiosk config...${NC}" 342 343# 3a. Place the piece bundle 344mkdir -p "$ROOTFS_DIR/usr/local/share/kiosk" 345cp "$BUNDLE_PATH" "$ROOTFS_DIR/usr/local/share/kiosk/piece.html" 346echo -e " ${GREEN}Piece bundle installed${NC}" 347 348KIOSK_PIECE_URL="http://localhost:8080/piece.html?density=${PACK_DENSITY}" 349 350# 3b. Kiosk session script (adapted for Alpine/OpenRC) 351cat > "$ROOTFS_DIR/usr/local/bin/kiosk-session.sh" << 'SESSEOF' 352#!/bin/sh 353# Alpine Kiosk Session — Cage + Chromium, PipeWire audio 354# Write logs to persistent FEDAC-PIECE partition 355PIECE_LOG="" 356PIECE_DEV=$(blkid -L FEDAC-PIECE 2>/dev/null || true) 357if [ -n "$PIECE_DEV" ]; then 358 mkdir -p /mnt/piece 359 mount "$PIECE_DEV" /mnt/piece 2>/dev/null || true 360 if mountpoint -q /mnt/piece 2>/dev/null && touch /mnt/piece/.writetest 2>/dev/null; then 361 rm -f /mnt/piece/.writetest 362 PIECE_LOG="/mnt/piece/kiosk.log" 363 fi 364fi 365LOG="${PIECE_LOG:-/tmp/kiosk.log}" 366exec > "$LOG" 2>&1 367echo "[kiosk] $(date) — kiosk-session.sh starting (Alpine)" 368echo "[kiosk] log file: $LOG" 369 370export XDG_SESSION_TYPE=wayland 371export XDG_RUNTIME_DIR="/run/user/$(id -u)" 372mkdir -p "$XDG_RUNTIME_DIR" 373chmod 0700 "$XDG_RUNTIME_DIR" 374 375# seatd socket — required for Cage to get DRM/input access 376export LIBSEAT_BACKEND=seatd 377if [ -S /run/seatd.sock ]; then 378 export SEATD_SOCK=/run/seatd.sock 379fi 380 381# Wayland/DRM env for Cage 382export WLR_LIBINPUT_NO_DEVICES=1 383 384# Wait for DRM device (GPU) 385echo "[kiosk] waiting for /dev/dri/card0..." 386for i in $(seq 1 30); do 387 [ -e /dev/dri/card0 ] && break 388 sleep 0.5 389done 390if [ -e /dev/dri/card0 ]; then 391 echo "[kiosk] DRM device ready: $(ls -la /dev/dri/)" 392else 393 echo "[kiosk] WARNING: /dev/dri/card0 not found after 15s" 394 ls -la /dev/dri/ 2>&1 || echo "[kiosk] /dev/dri does not exist" 395fi 396 397# Link piece files from FEDAC-PIECE if available 398if [ -f /mnt/piece/piece.html ]; then 399 for f in /mnt/piece/*.html; do 400 [ -f "$f" ] && ln -sf "$f" "/usr/local/share/kiosk/$(basename "$f")" 401 done 402 echo "[kiosk] linked piece files from FEDAC-PIECE partition" 403 ls -la /usr/local/share/kiosk/*.html 2>&1 404fi 405 406# Start PipeWire audio stack 407if command -v pipewire >/dev/null 2>&1; then 408 pipewire & 409 sleep 0.2 410 command -v wireplumber >/dev/null 2>&1 && wireplumber & 411 command -v pipewire-pulse >/dev/null 2>&1 && pipewire-pulse & 412fi 413 414echo "[kiosk] XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" 415echo "[kiosk] KIOSK_PIECE_URL=__KIOSK_PIECE_URL__" 416 417# Wait for piece server (port 8080) 418for i in $(seq 1 30); do 419 if python3 -c "import socket; s=socket.socket(); s.settimeout(0.5); s.connect(('127.0.0.1',8080)); s.close()" 2>/dev/null; then 420 echo "[kiosk] piece server ready (attempt $i)" 421 break 422 fi 423 echo "[kiosk] waiting for piece server... ($i/30)" 424 sleep 0.5 425done 426 427# Fallback: if OpenRC service isn't running, start piece server ourselves 428if ! python3 -c "import socket; s=socket.socket(); s.settimeout(0.5); s.connect(('127.0.0.1',8080)); s.close()" 2>/dev/null; then 429 echo "[kiosk] piece server not ready after 15s — starting fallback" 430 python3 /usr/local/bin/kiosk-piece-server.py & 431 sleep 1 432fi 433 434echo "[kiosk] launching cage + chromium" 435echo "[kiosk] LIBSEAT_BACKEND=$LIBSEAT_BACKEND SEATD_SOCK=${SEATD_SOCK:-unset}" 436echo "[kiosk] DRI devices: $(ls /dev/dri/ 2>&1)" 437 438# Check for DRM device — if missing, show diagnostic on console instead of silent fail 439if [ ! -e /dev/dri/card0 ]; then 440 echo "[kiosk] FATAL: no DRM device (/dev/dri/card0) — cage cannot start" >&2 441 # Restore console output so user can see the error 442 exec >/dev/console 2>&1 443 echo "" 444 echo "=== KIOSK ERROR: No GPU/DRM device found ===" 445 echo "cage requires /dev/dri/card0 to run." 446 echo "Check: ls -la /dev/dri/ lspci | grep -i vga dmesg | grep -i drm" 447 echo "Log: $LOG" 448 echo "" 449 # Drop to shell instead of silent exit/respawn loop 450 exec /bin/sh 451fi 452 453# Cage launches a single Wayland app fullscreen with black background 454cage -s -- chromium-browser \ 455 --no-first-run \ 456 --disable-translate \ 457 --disable-infobars \ 458 --disable-suggestions-service \ 459 --disable-save-password-bubble \ 460 --disable-session-crashed-bubble \ 461 --disable-component-update \ 462 --no-default-browser-check \ 463 --autoplay-policy=no-user-gesture-required \ 464 --kiosk \ 465 --disable-pinch \ 466 --overscroll-history-navigation=0 \ 467 --enable-features=OverlayScrollbar \ 468 --force-device-scale-factor=1 \ 469 --disable-background-networking \ 470 --disable-sync \ 471 --metrics-recording-only \ 472 --disable-default-apps \ 473 --mute-audio=false \ 474 --no-sandbox \ 475 --disable-gpu-sandbox \ 476 --enable-gpu-rasterization \ 477 --enable-zero-copy \ 478 --ignore-gpu-blocklist \ 479 --enable-features=VaapiVideoDecoder,VaapiVideoEncoder \ 480 "__KIOSK_PIECE_URL__" 481CAGE_EXIT=$? 482 483# If cage exits (crash or missing Wayland support), show error on console 484echo "[kiosk] cage exited with code $CAGE_EXIT" 485exec >/dev/console 2>&1 486echo "" 487echo "=== KIOSK: cage exited (code $CAGE_EXIT) ===" 488echo "Log: $LOG" 489echo "Restarting in 5s... (Ctrl+C for shell)" 490sleep 5 491SESSEOF 492 493# Replace placeholder URLs 494sed -i "s|__KIOSK_PIECE_URL__|${KIOSK_PIECE_URL}|g" "$ROOTFS_DIR/usr/local/bin/kiosk-session.sh" 495chmod +x "$ROOTFS_DIR/usr/local/bin/kiosk-session.sh" 496echo -e " ${GREEN}Kiosk session script installed${NC}" 497 498# 3c. Install piece server (Python HTTP + volume API) 499cp "$OVERLAY_DIR/kiosk-piece-server.py" "$ROOTFS_DIR/usr/local/bin/kiosk-piece-server.py" 500chmod +x "$ROOTFS_DIR/usr/local/bin/kiosk-piece-server.py" 501 502# Adapt piece server for Alpine: use kioskuser instead of liveuser 503sed -i 's/USER = "liveuser"/USER = "kioskuser"/' "$ROOTFS_DIR/usr/local/bin/kiosk-piece-server.py" 504 505# 3d. OpenRC service for piece server (instead of systemd) 506cat > "$ROOTFS_DIR/etc/init.d/kiosk-piece-server" << 'OPENRCEOF' 507#!/sbin/openrc-run 508description="FedOS Kiosk Piece Server" 509command="/usr/bin/python3" 510command_args="/usr/local/bin/kiosk-piece-server.py" 511pidfile="/run/${RC_SVCNAME}.pid" 512command_background=true 513 514depend() { 515 need localmount 516 after bootmisc 517} 518OPENRCEOF 519chmod +x "$ROOTFS_DIR/etc/init.d/kiosk-piece-server" 520 521# 3e. Enable OpenRC services in proper runlevels 522# sysinit: hardware/device setup (must run first) 523for svc in devfs dmesg mdev hwdrivers; do 524 chroot "$ROOTFS_DIR" /sbin/rc-update add "$svc" sysinit 2>/dev/null || true 525done 526# boot: filesystem and system setup 527# Alpine eudev provides: udev, udev-trigger, udev-settle, udev-postmount 528for svc in bootmisc hostname modules localmount udev udev-trigger udev-settle; do 529 chroot "$ROOTFS_DIR" /sbin/rc-update add "$svc" boot 2>/dev/null || { 530 echo " (service $svc not available, skipping)" 531 } 532done 533# default: user services (seatd for DRM access, dbus for PipeWire) 534for svc in seatd dbus kiosk-piece-server; do 535 chroot "$ROOTFS_DIR" /sbin/rc-update add "$svc" default 2>/dev/null || true 536done 537 538# 3f. Configure seatd — Alpine uses /etc/conf.d/seatd 539mkdir -p "$ROOTFS_DIR/etc/conf.d" 540cat > "$ROOTFS_DIR/etc/conf.d/seatd" << 'SEATDEOF' 541# Run seatd as root, allow "seat" group 542SEATD_ARGS="-g seat" 543SEATDEOF 544 545# 3g. Kernel modules — Alpine uses /etc/modules (one module per line) 546cat > "$ROOTFS_DIR/etc/modules" << EOF 547# GPU drivers 548i915 549amdgpu 550# Sound 551snd_hda_intel 552snd_hda_codec_hdmi 553# Input 554uinput 555# Squashfs (backup, usually built-in) 556squashfs 557EOF 558 559echo -e " ${GREEN}Kiosk config injected${NC}" 560 561# ── 3h. Ensure squashfs is in the initramfs ── 562# Alpine's linux-lts kernel includes squashfs built-in (CONFIG_SQUASHFS=y). 563# mkinitfs already has a squashfs feature file — just make sure it's enabled. 564echo -e " Enabling squashfs in initramfs..." 565if [ -f "$ROOTFS_DIR/etc/mkinitfs/mkinitfs.conf" ]; then 566 if ! grep -q 'squashfs' "$ROOTFS_DIR/etc/mkinitfs/mkinitfs.conf"; then 567 sed -i '/^features=/ s/"$/ squashfs"/' "$ROOTFS_DIR/etc/mkinitfs/mkinitfs.conf" 568 fi 569fi 570# Regenerate initramfs 571KVER=$(ls "$ROOTFS_DIR/lib/modules/" 2>/dev/null | head -1) 572if [ -n "$KVER" ]; then 573 mount -t proc none "$ROOTFS_DIR/proc" 2>/dev/null || true 574 mount -t sysfs none "$ROOTFS_DIR/sys" 2>/dev/null || true 575 chroot "$ROOTFS_DIR" mkinitfs -o /boot/initramfs-lts "$KVER" 576 umount "$ROOTFS_DIR/proc" 2>/dev/null || true 577 umount "$ROOTFS_DIR/sys" 2>/dev/null || true 578 echo -e " ${GREEN}Initramfs regenerated${NC}" 579fi 580 581# ── 3i. Replace /init in initramfs with our findroot script ── 582# Alpine's nlplug-findfs can't resolve PARTUUID/LABEL for raw SquashFS 583# partitions. We extract the initramfs, rename the original /init to 584# /init.alpine as backup, and replace /init with our findroot script 585# that scans devices and mounts the squashfs root. 586# (Appending a separate cpio archive after gzip doesn't work reliably 587# on all kernels/hardware.) 588INITRD_FILE="$ROOTFS_DIR/boot/initramfs-lts" 589if [ -f "$INITRD_FILE" ]; then 590 INJECT_DIR=$(mktemp -d /tmp/alpine-initrd-inject-XXXX) 591 592 # Extract existing initramfs 593 echo -e " Extracting initramfs for injection..." 594 mkdir -p "$INJECT_DIR/root" 595 (cd "$INJECT_DIR/root" && zcat "$INITRD_FILE" | cpio -id 2>/dev/null) 596 597 # Create /sysroot mount point (Alpine uses /newroot, we need /sysroot) 598 mkdir -p "$INJECT_DIR/root/sysroot" 599 600 # Rename original Alpine init as backup 601 if [ -f "$INJECT_DIR/root/init" ]; then 602 mv "$INJECT_DIR/root/init" "$INJECT_DIR/root/init.alpine" 603 echo -e " Renamed original /init → /init.alpine" 604 fi 605 606 # Replace /init with our findroot script 607 cat > "$INJECT_DIR/root/init" << 'FINDROOTEOF' 608#!/bin/sh 609mount -t proc proc /proc 2>/dev/null 610mount -t sysfs sysfs /sys 2>/dev/null 611mount -t devtmpfs dev /dev 2>/dev/null 612# Trigger device discovery 613mdev -s 2>/dev/null; sleep 1 614# Try partition 2 of all common disk types in order 615for dev in /dev/sda2 /dev/vda2 /dev/nvme0n1p2 /dev/mmcblk0p2 /dev/hda2; do 616 if [ -b "$dev" ] && mount -t squashfs -o ro "$dev" /sysroot 2>/dev/null; then 617 # Mount essential tmpfs dirs before init (root is read-only) 618 mount -t tmpfs -o nosuid,nodev,mode=0755 tmpfs /sysroot/run 2>/dev/null 619 mount -t tmpfs -o nosuid,nodev,mode=1777 tmpfs /sysroot/tmp 2>/dev/null 620 mount -t tmpfs -o nosuid,nodev,mode=0755 tmpfs /sysroot/var/log 2>/dev/null 621 mount -t tmpfs -o nosuid,nodev,mode=1777 tmpfs /sysroot/var/tmp 2>/dev/null 622 mount -t tmpfs -o nosuid,nodev,mode=0755 tmpfs /sysroot/var/cache 2>/dev/null 623 # OpenRC needs writable /run/openrc for state tracking 624 mkdir -p /sysroot/run/openrc 625 exec switch_root /sysroot /sbin/init 626 fi 627done 628echo "findroot: could not find squashfs root partition" 629exec /bin/sh 630FINDROOTEOF 631 chmod +x "$INJECT_DIR/root/init" 632 633 # Repack as a single gzip cpio archive 634 echo -e " Repacking initramfs with findroot as /init..." 635 (cd "$INJECT_DIR/root" && find . | cpio -o -H newc --quiet | gzip -1 > "$INITRD_FILE") 636 rm -rf "$INJECT_DIR" 637 echo -e " ${GREEN}Replaced /init in initramfs with findroot${NC}" 638fi 639 640# ══════════════════════════════════════════ 641# Step 4: Build SquashFS + prepare boot files 642# ══════════════════════════════════════════ 643echo -e "${CYAN}[4/6] Building SquashFS image...${NC}" 644 645# 4a. Copy kernel and initramfs out before SquashFS compression 646KERNEL_SRC=$(ls "$ROOTFS_DIR/boot/vmlinuz-"*lts* 2>/dev/null | head -1) 647INITRD_SRC=$(ls "$ROOTFS_DIR/boot/initramfs-"*lts* 2>/dev/null | head -1) 648 649if [ -z "$KERNEL_SRC" ]; then 650 echo -e "${RED}Error: No kernel found in rootfs${NC}" 651 ls "$ROOTFS_DIR/boot/" 2>&1 652 exit 1 653fi 654 655cp "$KERNEL_SRC" "$WORK_DIR/vmlinuz" 656echo -e " Kernel: $(basename $KERNEL_SRC)" 657 658if [ -n "$INITRD_SRC" ] && [ -f "$INITRD_SRC" ]; then 659 cp "$INITRD_SRC" "$WORK_DIR/initramfs" 660 echo -e " Initramfs: $(basename $INITRD_SRC)" 661else 662 # Generate a minimal initramfs if not provided 663 echo -e " ${YELLOW}No initramfs found, generating...${NC}" 664 if command -v mkinitfs >/dev/null 2>&1; then 665 KVER=$(basename "$KERNEL_SRC" | sed 's/vmlinuz-//') 666 chroot "$ROOTFS_DIR" mkinitfs -o /boot/initramfs "$KVER" 2>/dev/null || true 667 INITRD_SRC=$(ls "$ROOTFS_DIR/boot/initramfs"* 2>/dev/null | head -1) 668 [ -f "$INITRD_SRC" ] && cp "$INITRD_SRC" "$WORK_DIR/initramfs" 669 fi 670 if [ ! -f "$WORK_DIR/initramfs" ]; then 671 echo -e "${RED}Error: Could not generate initramfs${NC}" 672 exit 1 673 fi 674fi 675 676# 4b. Remove boot directory from rootfs (kernel/initramfs go on EFI partition) 677rm -rf "$ROOTFS_DIR/boot" 678mkdir -p "$ROOTFS_DIR/boot" 679 680# 4c. Clean up to minimize image size 681rm -rf "$ROOTFS_DIR/var/cache/apk"/* 682rm -rf "$ROOTFS_DIR/usr/share/doc"/* 683rm -rf "$ROOTFS_DIR/usr/share/man"/* 684rm -rf "$ROOTFS_DIR/usr/share/info"/* 685find "$ROOTFS_DIR/usr/share/locale" -mindepth 1 -maxdepth 1 ! -name 'en*' -exec rm -rf {} + 2>/dev/null || true 686 687ROOTFS_SIZE=$(du -sm "$ROOTFS_DIR" | awk '{print $1}') 688echo -e " Rootfs size before compression: ${ROOTFS_SIZE}MB" 689 690# 4d. Build SquashFS 691# SquashFS is built into Alpine's linux-lts kernel (CONFIG_SQUASHFS=y). 692# Root device is found at boot by /findroot (injected into initramfs). 693SFS_PATH="$WORK_DIR/rootfs.squashfs" 694mksquashfs "$ROOTFS_DIR/" "$SFS_PATH" -comp lz4 -Xhc -noappend -no-progress -quiet 695SFS_SIZE=$(stat -c%s "$SFS_PATH") 696echo -e " ${GREEN}SquashFS built: $(numfmt --to=iec $SFS_SIZE)${NC}" 697 698# ══════════════════════════════════════════ 699# Step 5: Build image and/or flash USB 700# ══════════════════════════════════════════ 701echo -e "${CYAN}[5/6] Building output media...${NC}" 702 703build_target() { 704 local target="$1" 705 local label="$2" 706 707 # Unmount existing partitions 708 for part in "${target}"*; do 709 umount "$part" 2>/dev/null || true 710 done 711 712 # Wipe and partition: EFI + ROOT (SquashFS) + PIECE 713 echo -e " Creating partition table on ${label}..." 714 wipefs -a "$target" >/dev/null 2>&1 715 716 local dev_bytes 717 dev_bytes=$(blockdev --getsize64 "$target" 2>/dev/null || stat -c%s "$target" 2>/dev/null) 718 local dev_mib=$((dev_bytes / 1048576)) 719 720 # EFI: 128MB (kernel + initramfs + grub), ROOT: bulk, PIECE: 20MB at end 721 local efi_end=129 # 1MiB start + 128MiB 722 local piece_start=$((dev_mib - 20)) 723 724 parted -s "$target" mklabel gpt 725 parted -s "$target" mkpart '"EFI"' fat32 1MiB "${efi_end}MiB" 726 parted -s "$target" set 1 esp on 727 parted -s "$target" mkpart '"ROOT"' "${efi_end}MiB" "${piece_start}MiB" 728 parted -s "$target" mkpart '"PIECE"' ext4 "${piece_start}MiB" 100% 729 730 sleep 2 731 partprobe "$target" 2>/dev/null || true 732 sleep 2 733 734 # Detect partition names 735 local p1="${target}1" 736 local p2="${target}2" 737 local p3="${target}3" 738 [ -b "$p1" ] || p1="${target}p1" 739 [ -b "$p2" ] || p2="${target}p2" 740 [ -b "$p3" ] || p3="${target}p3" 741 742 # Format 743 mkfs.vfat -F 32 -n BOOT "$p1" >/dev/null 744 # p2 (ROOT) gets raw SquashFS written later — no filesystem format needed 745 mkfs.ext4 -L FEDAC-PIECE -q "$p3" 746 echo -e " ${GREEN}${label}: partitions created (EFI + ROOT + PIECE)${NC}" 747 748 # Write piece files to FEDAC-PIECE partition 749 PIECE_MOUNT_TMP=$(mktemp -d /tmp/alpine-piece-XXXX) 750 mount "$p3" "$PIECE_MOUNT_TMP" 751 cp "$BUNDLE_PATH" "$PIECE_MOUNT_TMP/piece.html" 752 chmod 777 "$PIECE_MOUNT_TMP" 753 sync 754 umount "$PIECE_MOUNT_TMP" 755 rmdir "$PIECE_MOUNT_TMP" 756 PIECE_MOUNT_TMP="" 757 echo -e " ${GREEN}${label}: piece.html written to FEDAC-PIECE${NC}" 758 759 # Write SquashFS rootfs directly to ROOT partition (raw) 760 echo -e " Writing SquashFS rootfs to ROOT partition..." 761 dd if="$SFS_PATH" of="$p2" bs=4M conv=fsync 2>/dev/null 762 echo -e " ${GREEN}${label}: SquashFS rootfs written${NC}" 763 764 # Set up EFI partition 765 EFI_MOUNT=$(mktemp -d /tmp/alpine-efi-XXXX) 766 mount "$p1" "$EFI_MOUNT" 767 768 # Copy kernel and initramfs 769 mkdir -p "$EFI_MOUNT/boot" 770 cp "$WORK_DIR/vmlinuz" "$EFI_MOUNT/boot/vmlinuz" 771 cp "$WORK_DIR/initramfs" "$EFI_MOUNT/boot/initramfs" 772 773 # Install GRUB EFI 774 mkdir -p "$EFI_MOUNT/EFI/BOOT" 775 776 # Try to get grub from rootfs or host 777 if [ -f "$ROOTFS_DIR/usr/lib/grub/x86_64-efi/monolithic/grubx64.efi" ]; then 778 cp "$ROOTFS_DIR/usr/lib/grub/x86_64-efi/monolithic/grubx64.efi" "$EFI_MOUNT/EFI/BOOT/BOOTX64.EFI" 779 elif command -v grub2-mkimage >/dev/null 2>&1; then 780 grub2-mkimage -O x86_64-efi -o "$EFI_MOUNT/EFI/BOOT/BOOTX64.EFI" \ 781 normal linux fat part_gpt efi_gop efi_uga search search_label all_video gzio 2>/dev/null || true 782 elif command -v grub-mkimage >/dev/null 2>&1; then 783 grub-mkimage -O x86_64-efi -o "$EFI_MOUNT/EFI/BOOT/BOOTX64.EFI" \ 784 normal linux fat part_gpt efi_gop efi_uga search search_label all_video gzio 2>/dev/null || true 785 fi 786 787 # If we still don't have a GRUB EFI binary, try copying from host 788 if [ ! -f "$EFI_MOUNT/EFI/BOOT/BOOTX64.EFI" ]; then 789 for candidate in \ 790 /boot/efi/EFI/fedora/grubx64.efi \ 791 /usr/lib/grub/x86_64-efi/monolithic/grubx64.efi \ 792 /usr/share/grub/x86_64-efi/grubx64.efi; do 793 if [ -f "$candidate" ]; then 794 cp "$candidate" "$EFI_MOUNT/EFI/BOOT/BOOTX64.EFI" 795 break 796 fi 797 done 798 fi 799 800 if [ ! -f "$EFI_MOUNT/EFI/BOOT/BOOTX64.EFI" ]; then 801 echo -e "${RED}Warning: No GRUB EFI binary found — image may not boot${NC}" 802 fi 803 804 # GRUB config — Alpine uses direct kernel boot (no live image) 805 cat > "$EFI_MOUNT/EFI/BOOT/grub.cfg" << GRUBEOF 806set default=0 807set timeout=0 808insmod all_video 809insmod gzio 810insmod part_gpt 811insmod fat 812set gfxmode=auto 813set gfxpayload=keep 814terminal_input console 815terminal_output gfxterm 816menuentry "FedOS Alpine" { 817 linux /boot/vmlinuz rdinit=/init console=tty0 ro quiet loglevel=0 mitigations=off 818 initrd /boot/initramfs 819} 820GRUBEOF 821 822 # Also put grub.cfg where some firmware looks 823 mkdir -p "$EFI_MOUNT/EFI/fedora" 824 cp "$EFI_MOUNT/EFI/BOOT/grub.cfg" "$EFI_MOUNT/EFI/fedora/grub.cfg" 825 826 sync 827 umount "$EFI_MOUNT" 828 rmdir "$EFI_MOUNT" 829 EFI_MOUNT="" 830 echo -e " ${GREEN}${label}: EFI configured${NC}" 831} 832 833if [ -n "$IMAGE_PATH" ]; then 834 echo -e " Building disk image: ${GREEN}${IMAGE_PATH}${NC} (${IMAGE_SIZE_GB}GiB)" 835 truncate -s "${IMAGE_SIZE_GB}G" "$IMAGE_PATH" 836 LOOP_DEV=$(losetup --find --show --partscan "$IMAGE_PATH") 837 build_target "$LOOP_DEV" "Disk image" 838 losetup -d "$LOOP_DEV" 839 LOOP_DEV="" 840 echo -e " ${GREEN}Disk image ready${NC}" 841fi 842 843if [ -n "$DEVICE" ]; then 844 echo "" 845 echo -e "USB target: ${YELLOW}$DEVICE${NC}" 846 lsblk "$DEVICE" 2>/dev/null || true 847 echo "" 848 849 if [ "$SKIP_CONFIRM" = false ]; then 850 echo -e "${RED}ALL DATA ON $DEVICE WILL BE DESTROYED${NC}" 851 read -p "Continue? [y/N] " confirm 852 [ "$confirm" = "y" ] || [ "$confirm" = "Y" ] || { echo "Aborted."; exit 0; } 853 fi 854 855 if [ -n "$IMAGE_PATH" ]; then 856 echo -e " Flashing image to USB..." 857 dd if="$IMAGE_PATH" of="$DEVICE" bs=4M status=progress conv=fsync 858 sync 859 echo -e " ${GREEN}USB flashed from image${NC}" 860 else 861 build_target "$DEVICE" "USB" 862 fi 863fi 864 865# ══════════════════════════════════════════ 866# Step 6: Finalize 867# ══════════════════════════════════════════ 868echo -e "${CYAN}[6/6] Finalizing...${NC}" 869sync 870 871# Generate manifest for base images (used by oven /os endpoint) 872if [ "$BASE_IMAGE_MODE" = true ] && [ -n "$IMAGE_PATH" ] && [ -f "$IMAGE_PATH" ]; then 873 MANIFEST_PATH="${IMAGE_PATH%.img}-manifest.json" 874 IMG_SIZE=$(stat -c%s "$IMAGE_PATH") 875 IMG_SHA256=$(sha256sum "$IMAGE_PATH" | awk '{print $1}') 876 # Get FEDAC-PIECE partition offset/size from GPT table (robust, no fdisk guessing). 877 PIECE_META=$(python3 -c " 878import subprocess 879out = subprocess.check_output(['parted', '-s', '-m', '$IMAGE_PATH', 'unit', 'B', 'print'], text=True) 880part3 = None 881for raw in out.splitlines(): 882 line = raw.strip() 883 if not line or not line[0].isdigit() or ':' not in line: 884 continue 885 cols = line.split(':') 886 if len(cols) < 4: 887 continue 888 num = cols[0] 889 start = int(cols[1].rstrip('B')) 890 size = int(cols[3].rstrip('B')) 891 name = cols[5].rstrip(';').strip().upper() if len(cols) > 5 else '' 892 if name == 'PIECE': 893 print(f'{start}:{size}') 894 raise SystemExit(0) 895 if num == '3': 896 part3 = (start, size) 897if part3: 898 print(f'{part3[0]}:{part3[1]}') 899" 2>/dev/null || true) 900 PIECE_OFFSET=${PIECE_META%%:*} 901 PIECE_SIZE=${PIECE_META##*:} 902 [ -n "${PIECE_OFFSET:-}" ] || PIECE_OFFSET="0" 903 [ -n "${PIECE_SIZE:-}" ] || PIECE_SIZE=$((20 * 1024 * 1024)) 904 cat > "$MANIFEST_PATH" << MANIFEST_EOF 905{ 906 "version": "$(date +%Y-%m-%d)", 907 "flavor": "alpine", 908 "alpine": "${ALPINE_VERSION}", 909 "piecePartitionOffset": ${PIECE_OFFSET}, 910 "piecePartitionSize": ${PIECE_SIZE}, 911 "totalSize": ${IMG_SIZE}, 912 "sha256": "${IMG_SHA256}" 913} 914MANIFEST_EOF 915 echo -e " ${GREEN}Manifest written: ${MANIFEST_PATH}${NC}" 916fi 917 918if [ "$DO_EJECT" = true ] && [ -n "$DEVICE" ]; then 919 eject "$DEVICE" 2>/dev/null || true 920 echo -e " ${GREEN}Ejected${NC}" 921fi 922 923# Clean up work dir 924if [ "$OWN_ROOTFS" = true ]; then 925 echo -e " Cleaning work dir..." 926 rm -rf "$WORK_DIR" 927fi 928 929echo "" 930echo -e "${GREEN}╔═══════════════════════════════════════════════╗${NC}" 931echo -e "${GREEN}║ Alpine Kiosk Build Ready ║${NC}" 932echo -e "${GREEN}║ Piece: ${CYAN}${PIECE_CODE}${GREEN} (offline, ~400MB) ║${NC}" 933echo -e "${GREEN}╚═══════════════════════════════════════════════╝${NC}" 934[ -n "$IMAGE_PATH" ] && echo -e " Image: ${GREEN}${IMAGE_PATH}${NC}" 935[ -n "$DEVICE" ] && echo -e " USB: ${GREEN}${DEVICE}${NC}" 936echo ""