Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
Shell
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777#!/bin/bash# make-kiosk-piece-usb.sh — Create a FedAC kiosk USB with a bundled KidLisp piece## Pipeline: fetch piece bundle from oven → inject into Fedora live rootfs → flash to USB## Boot flow:# Power on → GRUB (instant) → PALS Plymouth splash → mutter (no desktop)# → Firefox fullscreen → bundled piece (offline, no WiFi needed)## Usage:# sudo bash fedac/scripts/make-kiosk-piece-usb.sh <piece-code> [<device>] [options]## Examples:# sudo bash fedac/scripts/make-kiosk-piece-usb.sh roz /dev/sdb# sudo bash fedac/scripts/make-kiosk-piece-usb.sh '$cow' --image /tmp/fedac-roz.img# sudo bash fedac/scripts/make-kiosk-piece-usb.sh roz /dev/sdb --image /tmp/fedac-roz.img## Options:# --rootfs <dir> Use existing extracted rootfs (skip ISO download + extract)# --iso <path> Use local ISO instead of downloading# --bundle <path> Use local HTML bundle instead of fetching from oven# --rebuild-bundle Rebuild local bundle with in-repo bundler before use# --image <path> Build a bootable disk image file (.img)# --image-size <g> Image size in GiB (default: 16)# --base-image Build base image without a specific piece (placeholder only)# --density <n> Default pack/runtime density query value (default: 8)# --work-base <dir> Directory for large temp work files (default: /tmp)# --no-eject Don't eject when done# --yes Skip confirmation prompts## Requirements:# erofs-utils (mkfs.erofs), curl, dracut (for initrd rebuild)# ~5GB free temp space (or less if using --rootfs)
set -euo pipefail
RED='\033[0;31m'GREEN='\033[0;32m'YELLOW='\033[1;33m'CYAN='\033[0;36m'PINK='\033[38;5;205m'NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"FEDAC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"OVERLAY_DIR="$FEDAC_DIR/overlays/kiosk"REPO_ROOT="$(cd "$FEDAC_DIR/.." && pwd)"
# Fedora 43 WorkstationFEDORA_VERSION="43"FEDORA_RELEASE="43-1.6"FEDORA_ISO_NAME="Fedora-Workstation-Live-${FEDORA_RELEASE}.x86_64.iso"FEDORA_URL="https://dl.fedoraproject.org/pub/fedora/linux/releases/${FEDORA_VERSION}/Workstation/x86_64/iso/${FEDORA_ISO_NAME}"FEDORA_SHA256="2a4a16c009244eb5ab2198700eb04103793b62407e8596f30a3e0cc8ac294d77"CACHE_DIR="${HOME}/Downloads"
# Oven pack/bundle endpoints (pack is preferred; bundle kept as fallback)OVEN_PACK_URL="https://oven.aesthetic.computer/pack-html"OVEN_BUNDLE_URL="https://oven.aesthetic.computer/bundle-html"PACK_DENSITY_DEFAULT="8"
usage() { echo -e "${PINK}FedAC Kiosk Piece USB Creator${NC}" echo "" echo "Creates a live USB that boots into a fullscreen KidLisp piece (offline)." echo "" echo "Usage: sudo $0 <piece-code> [<device>] [options]" echo "" echo " <piece-code> KidLisp piece code (e.g., roz, \$cow)" echo " <device> Optional USB block device (e.g., /dev/sdb)" echo "" echo "Options:" echo " --rootfs <dir> Use existing extracted rootfs directory" echo " --iso <path> Use existing ISO instead of downloading" echo " --bundle <path> Use local HTML bundle (skip oven fetch)" echo " --rebuild-bundle Rebuild local bundle with in-repo bundler before use" echo " --density <n> Default pack/runtime density query value (default: 8)" echo " --no-eject Don't eject the USB when done" echo " --yes Skip confirmation prompts" echo " --help Show this help" exit 1}
cleanup() { echo -e "\n${YELLOW}Cleaning up mounts...${NC}" for mp in "${ISO_MOUNT:-}" "${ROOTFS_MOUNT:-}" "${EFI_MOUNT:-}" "${LIVE_MOUNT:-}"; do [ -n "$mp" ] && mountpoint -q "$mp" 2>/dev/null && umount "$mp" 2>/dev/null || true done if [ -n "${LOOP_DEV:-}" ]; then losetup -d "$LOOP_DEV" 2>/dev/null || true LOOP_DEV="" fi}trap cleanup EXIT
# ── Parse args ──PIECE_CODE=""DEVICE=""ROOTFS_DIR=""ISO_PATH=""LOCAL_BUNDLE_PATH=""IMAGE_PATH=""IMAGE_SIZE_GB="16"WORK_BASE="/tmp"DO_EJECT=trueSKIP_CONFIRM=falseBASE_IMAGE_MODE=falsePACK_DENSITY="$PACK_DENSITY_DEFAULT"REBUILD_BUNDLE=false
while [ $# -gt 0 ]; do case "$1" in --rootfs) ROOTFS_DIR="$2"; shift 2 ;; --iso) ISO_PATH="$2"; shift 2 ;; --bundle) LOCAL_BUNDLE_PATH="$2"; shift 2 ;; --rebuild-bundle) REBUILD_BUNDLE=true; shift ;; --image) IMAGE_PATH="$2"; shift 2 ;; --image-size) IMAGE_SIZE_GB="$2"; shift 2 ;; --density) PACK_DENSITY="$2"; shift 2 ;; --work-base) WORK_BASE="$2"; shift 2 ;; --base-image) BASE_IMAGE_MODE=true; shift ;; --no-eject) DO_EJECT=false; shift ;; --yes) SKIP_CONFIRM=true; shift ;; --help|-h) usage ;; /dev/*) DEVICE="$1"; shift ;; *) if [ -z "$PIECE_CODE" ]; then PIECE_CODE="$1"; shift; else echo -e "${RED}Unknown arg: $1${NC}"; usage; fi ;; esacdone
if [ "$BASE_IMAGE_MODE" = true ]; then [ -n "$PIECE_CODE" ] || PIECE_CODE="__base__"else [ -n "$PIECE_CODE" ] || { echo -e "${RED}Error: No piece code specified${NC}"; usage; }fi[ -n "$DEVICE" ] || [ -n "$IMAGE_PATH" ] || { echo -e "${RED}Error: Specify a USB device and/or --image path${NC}" usage}[ -z "$DEVICE" ] || [ -b "$DEVICE" ] || { echo -e "${RED}Error: $DEVICE is not a block device${NC}"; exit 1; }[ -z "$IMAGE_PATH" ] || [[ "$IMAGE_SIZE_GB" =~ ^[0-9]+$ ]] || { echo -e "${RED}Error: --image-size must be an integer GiB value${NC}" exit 1}[ -z "$LOCAL_BUNDLE_PATH" ] || [ -f "$LOCAL_BUNDLE_PATH" ] || { echo -e "${RED}Error: --bundle '$LOCAL_BUNDLE_PATH' not found${NC}" exit 1}[[ "$PACK_DENSITY" =~ ^[1-9][0-9]*$ ]] || { echo -e "${RED}Error: --density must be a positive integer${NC}" exit 1}[ -d "$WORK_BASE" ] || mkdir -p "$WORK_BASE"[ -d "$WORK_BASE" ] || { echo -e "${RED}Error: --work-base '$WORK_BASE' is not accessible${NC}" exit 1}
if [ "$(id -u)" -ne 0 ]; then echo -e "${RED}Error: Must run as root (sudo)${NC}" exit 1fi
# Safety: refuse system disksif [ -n "$DEVICE" ]; then case "$DEVICE" in /dev/nvme*|/dev/vda|/dev/xvda) echo -e "${RED}REFUSED: $DEVICE looks like a system disk.${NC}" exit 1 ;; /dev/sda) RM_FLAG="$(lsblk -dn -o RM "$DEVICE" 2>/dev/null || echo "")" if [ "$RM_FLAG" != "1" ]; then echo -e "${RED}REFUSED: $DEVICE looks like a system disk.${NC}" exit 1 fi echo -e "${YELLOW}Warning: allowing /dev/sda because lsblk reports RM=1 (removable).${NC}" ;; esacfi
# Check toolsfor tool in mkfs.erofs curl; do command -v "$tool" &>/dev/null || { echo -e "${RED}Error: $tool not found${NC}"; exit 1; }doneif [ -n "$IMAGE_PATH" ]; then for tool in losetup; do command -v "$tool" &>/dev/null || { echo -e "${RED}Error: $tool not found${NC}"; exit 1; } donefi
echo -e "${PINK}╔═══════════════════════════════════════════════╗${NC}"echo -e "${PINK}║ FedAC Kiosk Piece USB Creator ║${NC}"echo -e "${PINK}║ Piece: ${CYAN}${PIECE_CODE}${PINK} → offline bootable USB ║${NC}"echo -e "${PINK}╚═══════════════════════════════════════════════╝${NC}"echo ""
WORK_DIR=$(mktemp -d "$WORK_BASE/fedac-kiosk-XXXX")echo -e "Work dir: $WORK_DIR"echo ""
# Build bundle with local in-repo bundler (fallback when oven is unreachable).build_local_bundle() { local piece_code="$1" local output_path="$2" local bundler_script="$FEDAC_DIR/scripts/build-local-piece-bundle.mjs"
if ! command -v node >/dev/null 2>&1; then echo -e "${RED}Error: node is required for local bundle rebuild${NC}" return 1 fi if [ ! -f "$bundler_script" ]; then echo -e "${RED}Error: local bundler script not found: $bundler_script${NC}" return 1 fi
echo -e " ${YELLOW}Building bundle locally (oven unavailable or rebuild requested)...${NC}" node "$bundler_script" "$piece_code" --out "$output_path" --density "$PACK_DENSITY" --nocompress --keeplabel}
# ══════════════════════════════════════════# Step 1: Fetch piece bundle from oven# ══════════════════════════════════════════BUNDLE_PATH="$WORK_DIR/piece.html"
if [ "$BASE_IMAGE_MODE" = true ]; then echo -e "${CYAN}[1/6] Base image mode — generating placeholder piece...${NC}" cat > "$BUNDLE_PATH" << 'PLACEHOLDER_EOF'<!DOCTYPE html><html><head><meta charset="utf-8"><title>FedAC Base</title></head><body style="background:#000;color:#fff;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;font-family:monospace"><p>No piece loaded. Build with aesthetic.computer/os</p></body></html>PLACEHOLDER_EOF echo -e " ${GREEN}Placeholder piece.html generated${NC}"elif [ -n "$LOCAL_BUNDLE_PATH" ]; then if [ "$REBUILD_BUNDLE" = true ]; then echo -e "${CYAN}[1/6] Rebuilding local bundle: ${LOCAL_BUNDLE_PATH}${NC}" build_local_bundle "$PIECE_CODE" "$LOCAL_BUNDLE_PATH" else echo -e "${CYAN}[1/6] Using local bundle: ${LOCAL_BUNDLE_PATH}${NC}" fi cp "$LOCAL_BUNDLE_PATH" "$BUNDLE_PATH" BUNDLE_SIZE=$(stat -c%s "$BUNDLE_PATH") echo -e " ${GREEN}Local bundle copied: $(numfmt --to=iec $BUNDLE_SIZE)${NC}"else # Always use local bundler first (ensures keeplabel and latest code). # Falls back to oven only if local build fails (e.g., no node available). echo -e "${CYAN}[1/6] Building piece bundle locally: ${PIECE_CODE}...${NC}" if build_local_bundle "$PIECE_CODE" "$BUNDLE_PATH"; then BUNDLE_SIZE=$(stat -c%s "$BUNDLE_PATH") echo -e " ${GREEN}Local bundle built: $(numfmt --to=iec $BUNDLE_SIZE)${NC}" else echo -e "${YELLOW}Local bundle build failed — falling back to oven...${NC}"
HTTP_CODE="000" USED_ENDPOINT="" USED_MODE=""
if [[ "$PIECE_CODE" == \$* ]]; then BUNDLE_MODES=(code piece) else BUNDLE_MODES=(piece code) fi
for endpoint in "$OVEN_PACK_URL" "$OVEN_BUNDLE_URL"; do for mode in "${BUNDLE_MODES[@]}"; do BUNDLE_URL="${endpoint}?${mode}=${PIECE_CODE}&nocompress=1&density=${PACK_DENSITY}&keeplabel=1" echo -e " URL: $BUNDLE_URL" HTTP_CODE="$(curl -s --connect-timeout 8 --max-time 25 --retry 1 --retry-delay 1 \ -o "$BUNDLE_PATH" -w "%{http_code}" "$BUNDLE_URL" || echo "000")" if [ "$HTTP_CODE" = "200" ]; then USED_ENDPOINT="$endpoint" USED_MODE="$mode" break 2 fi echo -e " ${YELLOW}${mode} via ${endpoint##*/} failed (HTTP ${HTTP_CODE})${NC}" done done
if [ "$HTTP_CODE" != "200" ]; then echo -e "${RED}Failed to build bundle locally and from oven.${NC}" echo " Check that '$PIECE_CODE' is valid and Node/oven are available." exit 1 fi BUNDLE_SIZE=$(stat -c%s "$BUNDLE_PATH") echo -e " ${GREEN}Bundle fetched: $(numfmt --to=iec $BUNDLE_SIZE)${NC}" echo -e " ${GREEN}Source: ${USED_ENDPOINT##*/} (${USED_MODE}), density=${PACK_DENSITY}${NC}" fifi
# Quick sanity check — bundle should contain HTMLif ! head -1 "$BUNDLE_PATH" | grep -qi "<!DOCTYPE\|<html"; then echo -e "${YELLOW}Warning: Bundle doesn't look like HTML. First line:${NC}" head -1 "$BUNDLE_PATH"fi
# ══════════════════════════════════════════# Step 2: Get or prepare rootfs# ══════════════════════════════════════════echo -e "${CYAN}[2/6] Preparing rootfs...${NC}"
OWN_ROOTFS=falseif [ -n "$ROOTFS_DIR" ]; then echo -e " Using existing rootfs: ${GREEN}$ROOTFS_DIR${NC}" if [ ! -d "$ROOTFS_DIR/usr" ]; then echo -e "${RED}Error: $ROOTFS_DIR doesn't look like a rootfs (no /usr)${NC}" exit 1 fielse # Need to extract from ISO OWN_ROOTFS=true ROOTFS_DIR="$WORK_DIR/rootfs"
if [ -z "$ISO_PATH" ]; then ISO_PATH="${CACHE_DIR}/${FEDORA_ISO_NAME}" if [ -f "$ISO_PATH" ]; then echo -e " Found cached ISO: ${GREEN}$ISO_PATH${NC}" else echo -e " Downloading Fedora ${FEDORA_VERSION} ISO (~2.6 GB)..." mkdir -p "$CACHE_DIR" curl -L --progress-bar -o "$ISO_PATH" "$FEDORA_URL" fi fi
echo -e " Extracting rootfs from ISO (takes a few minutes)..." ISO_MOUNT=$(mktemp -d /tmp/fedac-iso-XXXX) mount -o loop,ro "$ISO_PATH" "$ISO_MOUNT"
SQUASH_IMG="$ISO_MOUNT/LiveOS/squashfs.img" [ -f "$SQUASH_IMG" ] || { echo -e "${RED}No LiveOS/squashfs.img in ISO${NC}"; exit 1; }
# Mount LiveOS image when possible; otherwise extract directly. ROOTFS_MOUNT=$(mktemp -d /tmp/fedac-rootfs-mount-XXXX) mkdir -p "$ROOTFS_DIR" if mount -o loop,ro "$SQUASH_IMG" "$ROOTFS_MOUNT" 2>/tmp/fedac-rootfs-mount.err; then # Copy rootfs to work dir echo -e " Copying rootfs (this takes a while)..." cp -a "$ROOTFS_MOUNT"/* "$ROOTFS_DIR"/
umount "$ROOTFS_MOUNT" rmdir "$ROOTFS_MOUNT" ROOTFS_MOUNT="" else MOUNT_ERR="$(cat /tmp/fedac-rootfs-mount.err 2>/dev/null || true)" rm -f /tmp/fedac-rootfs-mount.err echo -e " ${YELLOW}Mounting LiveOS image failed (${MOUNT_ERR})${NC}" echo -e " ${CYAN}Falling back to direct extraction...${NC}" rmdir "$ROOTFS_MOUNT" 2>/dev/null || true ROOTFS_MOUNT=""
IMG_INFO="$(file -b "$SQUASH_IMG" 2>/dev/null || true)" if echo "$IMG_INFO" | grep -qi "erofs"; then command -v fsck.erofs >/dev/null 2>&1 || { echo -e "${RED}Error: fsck.erofs not found for EROFS extraction${NC}" exit 1 } fsck.erofs --extract="$ROOTFS_DIR" --force --overwrite "$SQUASH_IMG" >/tmp/fedac-erofs-extract.log 2>&1 tail -n 20 /tmp/fedac-erofs-extract.log || true rm -f /tmp/fedac-erofs-extract.log else command -v unsquashfs >/dev/null 2>&1 || { echo -e "${RED}Error: unsquashfs not found for squashfs extraction${NC}" exit 1 } unsquashfs -f -d "$ROOTFS_DIR" "$SQUASH_IMG" fi fi
umount "$ISO_MOUNT" rmdir "$ISO_MOUNT" ISO_MOUNT="" echo -e " ${GREEN}Rootfs extracted${NC}"fi
# ══════════════════════════════════════════# Step 3: Inject kiosk config into rootfs# ══════════════════════════════════════════echo -e "${CYAN}[3/6] Injecting kiosk config...${NC}"
# 3a. Place the piece bundlemkdir -p "$ROOTFS_DIR/usr/local/share/kiosk"cp "$BUNDLE_PATH" "$ROOTFS_DIR/usr/local/share/kiosk/piece.html"echo -e " ${GREEN}Piece bundle installed${NC}"
# Install kiosk shell wrapper (HUD overlay for volume/battery)SHELL_HTML="$FEDAC_DIR/overlays/kiosk/kiosk-shell.html"if [ -f "$SHELL_HTML" ]; then cp "$SHELL_HTML" "$ROOTFS_DIR/usr/local/share/kiosk/kiosk-shell.html" sed -i "s|__DENSITY__|${PACK_DENSITY}|g" "$ROOTFS_DIR/usr/local/share/kiosk/kiosk-shell.html" echo -e " ${GREEN}Kiosk shell (HUD overlay) installed${NC}"fi
# Use piece server (localhost:8080) so the shell wrapper can access system APIs.# The piece server serves kiosk-shell.html at / which iframes piece.html.# kiosk-session.sh waits for the piece server before launching Firefox.KIOSK_PIECE_URL="http://localhost:8080/"
# NOTE: Wayland compositor runtime is installed AFTER the rootfs strip step# (step 3i) so package removals cannot accidentally remove launcher deps.
# Volume key daemon is pure Python — no external packages needed.
# 3b. Kiosk session scriptcat > "$ROOTFS_DIR/usr/local/bin/kiosk-session.sh" << 'SESSEOF'#!/bin/bash# FedAC Kiosk Session - Wayland primary (Weston), Cage fallback.# Write logs to persistent FEDAC-PIECE partition (readable from another machine)# FEDAC-PIECE is auto-mounted by systemd (mnt-piece.mount unit) — no blkid/mount needed here.PIECE_LOG=""if mountpoint -q /mnt/piece 2>/dev/null && touch /mnt/piece/.writetest 2>/dev/null; then rm -f /mnt/piece/.writetest PIECE_LOG="/mnt/piece/kiosk.log"fiLOG="${PIECE_LOG:-/tmp/kiosk.log}"TTY_DEV="$(tty 2>/dev/null || true)"[ -n "$TTY_DEV" ] || TTY_DEV="/dev/tty1"if [ -w "$TTY_DEV" ]; then exec > >(tee -a "$LOG" "$TTY_DEV") 2>&1else exec > "$LOG" 2>&1fiecho "[kiosk] $(date) — kiosk-session.sh starting"echo "[kiosk] log file: $LOG"export XDG_SESSION_TYPE=waylandexport XDG_RUNTIME_DIR="/run/user/$(id -u)"export MOZ_ENABLE_WAYLAND=1export MOZ_WEBRENDER=0export MOZ_X11_EGL=0export GDK_BACKEND=waylandexport LIBGL_ALWAYS_SOFTWARE=1export MESA_LOADER_DRIVER_OVERRIDE=llvmpipeexport MOZ_DBUS_REMOTE=0export NO_AT_BRIDGE=1export CLUTTER_DISABLE_ANIMATIONS=1export WLR_RENDERER=pixmanexport WLR_NO_HARDWARE_CURSORS=1PROFILE="/home/liveuser/.mozilla/firefox/kiosk"WESTON_BIN="$(command -v weston 2>/dev/null || true)"CAGE_BIN="$(command -v cage 2>/dev/null || true)"WAYLAND_LOG="${LOG%.log}-wayland.log"FIREFOX_LOG="${LOG%.log}-firefox.log"
echo "[kiosk] checking binaries: weston=${WESTON_BIN:-MISSING} cage=${CAGE_BIN:-MISSING} firefox=$(command -v firefox 2>/dev/null || echo MISSING)"echo "[kiosk] wayland log: $WAYLAND_LOG"echo "[kiosk] firefox log: $FIREFOX_LOG"
mkdir -p "$PROFILE/chrome"cat > "$PROFILE/user.js" << 'USERJSEOF'user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);user_pref("browser.sessionstore.resume_from_crash", false);user_pref("browser.tabs.drawInTitlebar", false);user_pref("browser.tabs.inTitlebar", 0);user_pref("browser.startup.homepage", "__KIOSK_PIECE_URL__");user_pref("browser.startup.page", 1);user_pref("browser.fullscreen.autohide", false);user_pref("full-screen-api.warning.timeout", 0);user_pref("full-screen-api.transition.timeout", 0);user_pref("media.autoplay.default", 0);user_pref("media.autoplay.blocking_policy", 0);user_pref("media.autoplay.block-webaudio", false);user_pref("layers.acceleration.disabled", true);user_pref("gfx.webrender.force-disabled", true);user_pref("accessibility.typeaheadfind", false);user_pref("accessibility.typeaheadfind.manual", false);user_pref("accessibility.typeaheadfind.linksonly", false);// Block notification prompts, allow everything else (kiosk = trust all)user_pref("permissions.default.desktop-notification", 2);user_pref("permissions.default.geo", 1);user_pref("permissions.default.camera", 1);user_pref("permissions.default.microphone", 1);user_pref("permissions.default.xr", 1);user_pref("dom.webnotifications.enabled", false);user_pref("dom.push.enabled", false);user_pref("dom.push.connection.enabled", false);// MIDI: allow without prompting (including SysEx)user_pref("midi.prompt_on_sysex_access", false);user_pref("midi.testing", true);user_pref("dom.webmidi.enabled", true);// Suppress ALL info bars, notification bars, and popup promptsuser_pref("browser.uitour.enabled", false);user_pref("browser.discovery.enabled", false);user_pref("extensions.getAddons.showPane", false);user_pref("browser.protections_panel.infoMessage.seen", true);user_pref("browser.contentblocking.introCount", 99);user_pref("browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", false);user_pref("browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", false);user_pref("security.insecure_connection_text.enabled", false);user_pref("security.insecure_connection_text.pbmode.enabled", false);user_pref("security.insecure_field_warning.contextual.enabled", false);// Pre-grant permissions for localhost so no prompt appearsuser_pref("permissions.manager.defaultsUrl", "");USERJSEOF
# Pre-populate permissions.sqlite — grant all permissions for localhost:8080# This prevents ANY permission popups from appearing on first load.python3 -c "import sqlite3, osdb = '$PROFILE/permissions.sqlite'conn = sqlite3.connect(db)c = conn.cursor()c.execute('CREATE TABLE IF NOT EXISTS moz_perms (id INTEGER PRIMARY KEY, origin TEXT, type TEXT, permission INTEGER, expireType INTEGER, expireTime INTEGER, modificationTime INTEGER)')# permission=1=ALLOW, 2=DENY; for kiosk we deny notifications, allow autoplayperms = [ ('http://localhost:8080', 'desktop-notification', 2, 0, 0), ('http://localhost:8080', 'autoplay-media', 1, 0, 0), ('http://localhost:8080', 'geo', 1, 0, 0), ('http://localhost:8080', 'camera', 1, 0, 0), ('http://localhost:8080', 'microphone', 1, 0, 0), ('http://localhost:8080', 'midi', 1, 0, 0), ('http://localhost:8080', 'midi-sysex', 1, 0, 0), ('http://localhost:8080', 'popup', 1, 0, 0), ('http://localhost:8080', 'persistent-storage', 1, 0, 0),]import timets = int(time.time() * 1000)for origin, ptype, perm, exp_type, exp_time in perms: c.execute('INSERT INTO moz_perms (origin, type, permission, expireType, expireTime, modificationTime) VALUES (?, ?, ?, ?, ?, ?)', (origin, ptype, perm, exp_type, exp_time, ts))conn.commit()conn.close()"echo "[kiosk] pre-populated permissions.sqlite for localhost:8080"
cat > "$PROFILE/chrome/userChrome.css" << 'CHROMEEOF'/* FedAC Kiosk: hide ALL browser chrome, toolbars, and notification bars */#TabsToolbar,#titlebar,#toolbar-menubar,#nav-bar,#PersonalToolbar,#navigator-toolbox { visibility: collapse !important; min-height: 0 !important; max-height: 0 !important; height: 0 !important; overflow: hidden !important;}/* Kill all notification/info bars — the thin line at top */notification, .notificationbox-stack,#notification-popup-box,.notification-anchor-icon,#PopupAutoComplete,.popup-notification-panel,#appMenu-notification-popup,#identity-popup,#tracking-protection-icon-container,hbox.infobar,.infobar,notification[value] { display: none !important; height: 0 !important; max-height: 0 !important; opacity: 0 !important;}/* Ensure no top border/line from the browser frame */#main-window, #browser { border-top: none !important;}CHROMEEOF
# Wait briefly for mounted piece files (mount can be late on some hardware).for i in $(seq 1 20); do [ -f /mnt/piece/piece.html ] && break [ "$i" = "1" ] && echo "[kiosk] waiting for /mnt/piece/piece.html..." sleep 0.5done
# Check for piece on FEDAC-PIECE partition (already mounted rw above for logging).# Symlink ALL html files — the oven injects piece.html (shell) + piece-app.html (bundle),# and the shell iframe needs piece-app.html resolvable from the same directory.if [ -f /mnt/piece/piece.html ]; then for f in /mnt/piece/*.html; do [ -f "$f" ] && ln -sf "$f" "/usr/local/share/kiosk/$(basename "$f")" done echo "[kiosk] linked piece files from FEDAC-PIECE partition" ls -la /usr/local/share/kiosk/*.html 2>&1else echo "[kiosk] WARNING: /mnt/piece/piece.html missing — using bundled rootfs fallback"fi
# Paint PALS logo to framebuffer (visible during compositor startup)if [ -x /usr/local/bin/pals-fb-splash ]; then /usr/local/bin/pals-fb-splash 2>/dev/null || truefi
# Ensure user audio stack is available.mkdir -p "$XDG_RUNTIME_DIR"if command -v systemctl >/dev/null 2>&1; then systemctl --user start pipewire.service pipewire-pulse.service wireplumber.service >/dev/null 2>&1 || truefiif [ ! -S "$XDG_RUNTIME_DIR/pulse/native" ] && command -v pipewire >/dev/null 2>&1; then echo "[kiosk] pulse socket missing; starting pipewire fallback daemons" pipewire >> "$LOG" 2>&1 & sleep 0.2 command -v wireplumber >/dev/null 2>&1 && wireplumber >> "$LOG" 2>&1 & command -v pipewire-pulse >/dev/null 2>&1 && pipewire-pulse >> "$LOG" 2>&1 &fi
echo "[kiosk] XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"echo "[kiosk] KIOSK_PIECE_URL=__KIOSK_PIECE_URL__"ls -la /usr/local/share/kiosk/ 2>&1 || echo "[kiosk] /usr/local/share/kiosk/ missing"
# Wait for piece server (port 8080) — with socket activation this should be instant.for i in $(seq 1 30); do 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 echo "[kiosk] piece server ready" break fi [ "$i" -ge 5 ] && echo "[kiosk] waiting for piece server... ($i/30)" sleep 0.5done
# Fallback: if systemd service isn't running, start piece server ourselvesif ! python3 -c "import socket; s=socket.socket(); s.settimeout(0.5); s.connect(('127.0.0.1',8080)); s.close()" 2>/dev/null; then echo "[kiosk] piece server not ready after 15s — starting fallback" python3 /usr/local/bin/kiosk-piece-server.py & sleep 1fi
# Build the Firefox launch wrapper once and run it inside a Wayland compositor.cat > /tmp/fedac-firefox-kiosk.sh << 'WLEOF'#!/bin/shexec firefox --kiosk --no-remote --new-instance \ --profile "__PROFILE__" \ "__KIOSK_PIECE_URL__"WLEOFsed -i "s|__PROFILE__|$PROFILE|g" /tmp/fedac-firefox-kiosk.shchmod +x /tmp/fedac-firefox-kiosk.sh
WESTON_SHELL=""for cand in \ /usr/lib64/libweston-*/kiosk-shell.so \ /usr/lib/libweston-*/kiosk-shell.so \ /usr/lib64/libweston-*/fullscreen-shell.so \ /usr/lib/libweston-*/fullscreen-shell.sodo [ -f "$cand" ] && WESTON_SHELL="$cand" && breakdoneecho "[kiosk] weston shell: ${WESTON_SHELL:-default}"
launch_with_weston() { local socket="fedac-wayland" local args=(--backend=drm --socket="$socket" --idle-time=0) [ -n "$WESTON_SHELL" ] && args+=(--shell="$WESTON_SHELL")
rm -f "$XDG_RUNTIME_DIR/$socket" "$WESTON_BIN" "${args[@]}" >> "$WAYLAND_LOG" 2>&1 & local weston_pid="$!"
for i in $(seq 1 60); do [ -S "$XDG_RUNTIME_DIR/$socket" ] && break sleep 0.1 done if [ ! -S "$XDG_RUNTIME_DIR/$socket" ]; then echo "[kiosk] weston socket did not appear" kill "$weston_pid" >/dev/null 2>&1 || true wait "$weston_pid" >/dev/null 2>&1 || true return 1 fi
WAYLAND_DISPLAY="$socket" /tmp/fedac-firefox-kiosk.sh >> "$FIREFOX_LOG" 2>&1 local code="$?" kill "$weston_pid" >/dev/null 2>&1 || true wait "$weston_pid" >/dev/null 2>&1 || true return "$code"}
launch_with_cage() { "$CAGE_BIN" /tmp/fedac-firefox-kiosk.sh >> "$WAYLAND_LOG" 2>&1}
if [ -n "$WESTON_BIN" ] || [ -n "$CAGE_BIN" ]; then while true; do if [ -n "$WESTON_BIN" ]; then echo "[kiosk] launching firefox in Wayland mode (weston)" launch_with_weston CODE="$?" echo "[kiosk] weston/firefox exited (code=$CODE); restarting in 2s" else echo "[kiosk] launching firefox in Wayland mode (cage)" launch_with_cage CODE="$?" echo "[kiosk] cage/firefox exited (code=$CODE); restarting in 2s" fi tail -n 40 "$FIREFOX_LOG" 2>/dev/null || true tail -n 40 "$WAYLAND_LOG" 2>/dev/null || true sleep 2 donefi
echo "[kiosk] FATAL: no working launcher (weston/cage)"echo "[kiosk] staying on tty for debug; log=$LOG"while true; do sleep 60; doneSESSEOFsed -i "s|__KIOSK_PIECE_URL__|$KIOSK_PIECE_URL|g" "$ROOTFS_DIR/usr/local/bin/kiosk-session.sh"chmod +x "$ROOTFS_DIR/usr/local/bin/kiosk-session.sh"echo -e " ${GREEN}Kiosk session script installed${NC}"
# Root preflight: mount FEDAC-PIECE and link any injected html files before# tty1 autologin starts Firefox.cat > "$ROOTFS_DIR/usr/local/bin/fedac-piece-sync.sh" << 'PIECESYNCEOF'#!/bin/bashset -euo pipefailLOG="/tmp/fedac-piece-sync.log"log() { echo "[piece-sync] $(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG"; }
mkdir -p /mnt/piece /usr/local/share/kiosk
if ! mountpoint -q /mnt/piece 2>/dev/null; then if mount /dev/disk/by-label/FEDAC-PIECE /mnt/piece 2>/dev/null || mount -L FEDAC-PIECE /mnt/piece 2>/dev/null; then log "mounted FEDAC-PIECE" else log "mount failed" fifi
if [ -f /mnt/piece/piece.html ]; then linked=0 shopt -s nullglob for f in /mnt/piece/*.html; do ln -sf "$f" "/usr/local/share/kiosk/$(basename "$f")" linked=$((linked + 1)) done shopt -u nullglob log "linked ${linked} html file(s)"else log "piece.html missing on /mnt/piece"fiPIECESYNCEOFchmod +x "$ROOTFS_DIR/usr/local/bin/fedac-piece-sync.sh"echo -e " ${GREEN}Piece sync preflight installed${NC}"
# 3c. Disable GDM and force tty1 autologin -> kiosk-session.sh# This path is resilient on Fedora live images even when graphical.target isn't reached.mkdir -p "$ROOTFS_DIR/etc/systemd/system/getty@tty1.service.d"
# Mask GDM so it never starts and switch to multi-user (no graphical desktop)ln -sf /dev/null "$ROOTFS_DIR/etc/systemd/system/gdm.service"ln -sf /dev/null "$ROOTFS_DIR/etc/systemd/system/display-manager.service"# Force multi-user.target as default — prevents GNOME desktop flash on bootln -sf /usr/lib/systemd/system/multi-user.target "$ROOTFS_DIR/etc/systemd/system/default.target"
# Disable SELinux — the erofs live rootfs lacks correct xattr labels, causing# pam_unix to fail with "User not known to the underlying authentication module".# For an offline kiosk, SELinux provides no benefit and adds boot complexity.if [ -f "$ROOTFS_DIR/etc/selinux/config" ]; then sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' "$ROOTFS_DIR/etc/selinux/config" echo -e " ${GREEN}SELinux disabled in rootfs config${NC}"fi
cat > "$ROOTFS_DIR/etc/systemd/system/getty@tty1.service.d/autologin.conf" << 'AGEOF'[Service]ExecStartPre=-/usr/local/bin/fedac-piece-sync.shExecStart=ExecStart=-/sbin/agetty --noissue --nonewline --autologin liveuser --noclear %I $TERMType=idleAGEOF
# Some minimal roots don't ship a liveuser account; create it for tty1 autologin.if ! chroot "$ROOTFS_DIR" /usr/bin/getent passwd liveuser >/dev/null 2>&1; then chroot "$ROOTFS_DIR" /usr/sbin/useradd -m -s /bin/bash liveuser >/dev/null 2>&1 || truefichroot "$ROOTFS_DIR" /usr/sbin/usermod -s /bin/bash liveuser >/dev/null 2>&1 || truechroot "$ROOTFS_DIR" /usr/bin/passwd -d liveuser >/dev/null 2>&1 || true# Unlock root account so emergency shell is accessible for debugging.# passwd -d sets empty password, but sulogin needs the shadow entry unlocked too.chroot "$ROOTFS_DIR" /usr/bin/passwd -d root >/dev/null 2>&1 || truesed -i 's|^root:[^:]*:|root::|' "$ROOTFS_DIR/etc/shadow"
# Ensure PAM allows passwordless autologin for liveuser.# Fedora 43 pam_unix.so rejects empty-password accounts unless "nullok" is present.# Use authselect (Fedora's PAM manager) so the change survives boot-time regeneration,# then also patch the files directly as a belt-and-suspenders fallback.if chroot "$ROOTFS_DIR" /usr/bin/authselect current >/dev/null 2>&1; then # Get current profile and add with-nullok feature CURRENT_PROFILE=$(chroot "$ROOTFS_DIR" /usr/bin/authselect current -r 2>/dev/null || echo "sssd") chroot "$ROOTFS_DIR" /usr/bin/authselect select "$CURRENT_PROFILE" with-nullok --force >/dev/null 2>&1 || true echo -e " ${GREEN}authselect: enabled with-nullok on profile $CURRENT_PROFILE${NC}"fi# Belt-and-suspenders: also patch the files directly in case authselect isn't available.for pamfile in "$ROOTFS_DIR/etc/pam.d/system-auth" "$ROOTFS_DIR/etc/pam.d/password-auth"; do if [ -f "$pamfile" ]; then sed -i '/pam_unix\.so/ { /nullok/! s/pam_unix\.so/pam_unix.so nullok/ }' "$pamfile" fidoneecho -e " ${GREEN}PAM nullok configured for passwordless autologin${NC}"
# 3d. Hardware volume key daemon — pure Python, reads raw Linux input events.# No actkbd or external packages needed. Runs as root, talks to liveuser's PipeWire.cat > "$ROOTFS_DIR/usr/local/bin/kiosk-volume-keyd" << 'VKDEOF'#!/usr/bin/python3"""FedAC kiosk volume key daemon.
Reads raw input_event structs from all keyboard /dev/input/event* devices,intercepts KEY_MUTE (113), KEY_VOLUMEDOWN (114), KEY_VOLUMEUP (115),and adjusts the PipeWire/PulseAudio/ALSA volume for liveuser.
Zero external dependencies — stdlib only."""import glob, os, select, struct, subprocess, sys
# Linux input_event: struct timeval (16 bytes on 64-bit), __u16 type, __u16 code, __s32 valueEVENT_SIZE = struct.calcsize("llHHi")EV_KEY = 0x01KEY_DOWN = 1 # value=1 is key press
KEY_MUTE = 113KEY_VOLUMEDOWN = 114KEY_VOLUMEUP = 115
USER = "liveuser"UID = NoneRUNTIME_DIR = None
def find_uid(): global UID, RUNTIME_DIR try: import pwd UID = pwd.getpwnam(USER).pw_uid except (KeyError, ImportError): UID = 1000 RUNTIME_DIR = f"/run/user/{UID}"
def run_as_user(cmd): """Run a command as liveuser with their audio env.""" env = os.environ.copy() env["XDG_RUNTIME_DIR"] = RUNTIME_DIR env["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path={RUNTIME_DIR}/bus" try: subprocess.run( ["runuser", "-u", USER, "--"] + cmd, env=env, timeout=5, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) except Exception: pass
def vol_up(): run_as_user(["wpctl", "set-volume", "-l", "1.5", "@DEFAULT_AUDIO_SINK@", "5%+"])
def vol_down(): run_as_user(["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%-"])
def vol_mute(): run_as_user(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"])
ACTIONS = {KEY_MUTE: vol_mute, KEY_VOLUMEDOWN: vol_down, KEY_VOLUMEUP: vol_up}
def is_keyboard(evdev_path): """Check if device has EV_KEY capability (bit 1 in capabilities/ev).""" sysfs = f"/sys/class/input/{os.path.basename(evdev_path)}/device/capabilities/ev" try: caps = int(open(sysfs).read().strip(), 16) return bool(caps & (1 << EV_KEY)) except Exception: return False
def main(): find_uid() # Open all keyboard-capable input devices fds = {} for path in sorted(glob.glob("/dev/input/event*")): if is_keyboard(path): try: fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) fds[fd] = path except OSError: pass
if not fds: print("kiosk-volume-keyd: no keyboard devices found", file=sys.stderr) sys.exit(1)
print(f"kiosk-volume-keyd: monitoring {len(fds)} device(s)", file=sys.stderr)
poll = select.poll() for fd in fds: poll.register(fd, select.POLLIN)
while True: try: events = poll.poll(5000) except (KeyboardInterrupt, SystemExit): break for fd, mask in events: if mask & select.POLLIN: try: data = os.read(fd, EVENT_SIZE * 16) except OSError: continue for off in range(0, len(data) - EVENT_SIZE + 1, EVENT_SIZE): _sec, _usec, ev_type, code, value = struct.unpack_from("llHHi", data, off) if ev_type == EV_KEY and value == KEY_DOWN and code in ACTIONS: ACTIONS[code]()
for fd in fds: os.close(fd)
if __name__ == "__main__": main()VKDEOFchmod +x "$ROOTFS_DIR/usr/local/bin/kiosk-volume-keyd"
# Systemd service for the volume key daemoncat > "$ROOTFS_DIR/etc/systemd/system/kiosk-volume-keyd.service" << 'SVCEOF'[Unit]Description=FedAC kiosk volume key daemonAfter=systemd-udevd.service
[Service]Type=simpleExecStart=/usr/local/bin/kiosk-volume-keydRestart=on-failureRestartSec=2
[Install]WantedBy=multi-user.targetSVCEOFln -sf /etc/systemd/system/kiosk-volume-keyd.service "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/kiosk-volume-keyd.service"echo -e " ${GREEN}Volume keys mapped (kiosk-volume-keyd)${NC}"
# 3e. Piece server — serves kiosk HTML files + volume API on port 8080.cp "$OVERLAY_DIR/kiosk-piece-server.py" "$ROOTFS_DIR/usr/local/bin/kiosk-piece-server.py"chmod +x "$ROOTFS_DIR/usr/local/bin/kiosk-piece-server.py"cp "$OVERLAY_DIR/kiosk-piece-server.service" "$ROOTFS_DIR/etc/systemd/system/kiosk-piece-server.service"cp "$OVERLAY_DIR/kiosk-piece-server.socket" "$ROOTFS_DIR/etc/systemd/system/kiosk-piece-server.socket"# Enable socket activation — systemd binds port 8080 immediately at boot,# then starts the service on first connection (eliminates "waiting for piece server" delay).mkdir -p "$ROOTFS_DIR/etc/systemd/system/sockets.target.wants"ln -sf /etc/systemd/system/kiosk-piece-server.socket "$ROOTFS_DIR/etc/systemd/system/sockets.target.wants/kiosk-piece-server.socket"# Also keep direct service enable as fallback (service starts on its own too)mkdir -p "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants"ln -sf /etc/systemd/system/kiosk-piece-server.service "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/kiosk-piece-server.service"echo -e " ${GREEN}Piece server installed + socket-activated (port 8080)${NC}"
# 3e2. Auto-mount FEDAC-PIECE partition at boot via systemd mount unit.# The kiosk session runs as liveuser who can't mount — systemd mounts it as root.mkdir -p "$ROOTFS_DIR/mnt/piece"cat > "$ROOTFS_DIR/etc/systemd/system/mnt-piece.mount" << 'MOUNTEOF'[Unit]Description=Mount FEDAC-PIECE partitionAfter=systemd-udev-settle.service local-fs-pre.targetWants=systemd-udev-settle.serviceBefore=local-fs.target kiosk-piece-server.service
[Mount]What=/dev/disk/by-label/FEDAC-PIECEWhere=/mnt/pieceType=ext4Options=rw,noatime,nofail,x-systemd.device-timeout=30s
[Install]WantedBy=local-fs.targetMOUNTEOFmkdir -p "$ROOTFS_DIR/etc/systemd/system/local-fs.target.wants"ln -sf /etc/systemd/system/mnt-piece.mount "$ROOTFS_DIR/etc/systemd/system/local-fs.target.wants/mnt-piece.mount"echo -e " ${GREEN}FEDAC-PIECE auto-mount unit installed${NC}"
mkdir -p "$ROOTFS_DIR/home/liveuser"cat > "$ROOTFS_DIR/home/liveuser/.bash_profile" << 'BPROFEOF'#!/bin/bashTTY="$(tty 2>/dev/null || true)"if [ -z "${DISPLAY:-}" ] && { [ "${XDG_VTNR:-}" = "1" ] || [ "$TTY" = "/dev/tty1" ]; }; then [ -n "$TTY" ] && echo "[fedac] starting kiosk session on $TTY" >"$TTY" 2>/dev/null || true exec /usr/local/bin/kiosk-session.shfiBPROFEOFchmod 644 "$ROOTFS_DIR/home/liveuser/.bash_profile"chown liveuser:liveuser "$ROOTFS_DIR/home/liveuser/.bash_profile" 2>/dev/null || trueecho -e " ${GREEN}GDM masked, tty1 autologin configured${NC}"
# 3d. Slim livesys-gnome — no GDM config, just cleanup + schema compilecat > "$ROOTFS_DIR/usr/libexec/livesys/sessions.d/livesys-gnome" << 'LSEOF'#!/usr/bin/sh# livesys-gnome: FedAC Kiosk — minimal live session cleanup
# Suppress GNOME initial setup / welcome screensmkdir -p ~liveuser/.configecho yes > ~liveuser/.config/gnome-initial-setup-donerm -f /etc/xdg/autostart/gnome-initial-setup*.desktoprm -f /etc/xdg/autostart/org.gnome.Tour.desktoprm -f ~liveuser/.config/autostart/org.fedoraproject.welcome-screen.desktoprm -f ~liveuser/.config/autostart/fedora-welcome.desktop
# Compile glib schemas (mutter reads these at startup)glib-compile-schemas /usr/share/glib-2.0/schemas 2>/dev/null || true
chown -R liveuser:liveuser ~liveuser/ 2>/dev/null || trueLSEOFchmod +x "$ROOTFS_DIR/usr/libexec/livesys/sessions.d/livesys-gnome"echo -e " ${GREEN}livesys-gnome trimmed (no GDM)${NC}"
# 3e. Disable unnecessary services for faster bootfor svc in \ abrtd abrt-journal-core abrt-oops abrt-vmcore abrt-xorg \ avahi-daemon cups cups-browsed \ flatpak-system-update \ ModemManager \ sssd sssd-kcm \ thermald tuned \ rsyslog \ mdmonitor mcelog smartd atd \ NetworkManager-wait-online \ gnome-session gnome-shell gnome-session-manager \ gnome-initial-setup gnome-initial-setup-first-login \ gnome-software-service; do ln -sf /dev/null "$ROOTFS_DIR/etc/systemd/system/${svc}.service" 2>/dev/null || truedone# Also mask any graphical session targets that could pull in a desktopfor target in graphical.target; do ln -sf /dev/null "$ROOTFS_DIR/etc/systemd/system/${target}" 2>/dev/null || truedoneecho -e " ${GREEN}Unnecessary services + graphical.target masked${NC}"
# Blacklist iwlwifi — ThinkPad BIOS has WGDS but no WRDS ACPI table,# causing iwlwifi to crash on boot. Kiosk is offline so WiFi isn't needed.mkdir -p "$ROOTFS_DIR/etc/modprobe.d"cat > "$ROOTFS_DIR/etc/modprobe.d/fedac-blacklist.conf" << 'BLEOF'# FedAC kiosk: disable WiFi (iwlwifi crashes on WGDS without WRDS)blacklist iwlwifiblacklist iwlmvmblacklist iwldvmBLEOFecho -e " ${GREEN}iwlwifi blacklisted (offline kiosk, avoids WRDS/WGDS crash)${NC}"
# 3f. Firefox policies (no default tabs, no welcome)mkdir -p "$ROOTFS_DIR/usr/lib64/firefox/distribution"cat > "$ROOTFS_DIR/usr/lib64/firefox/distribution/policies.json" << 'FPEOF'{ "policies": { "OverrideFirstRunPage": "", "OverridePostUpdatePage": "", "Homepage": { "URL": "__KIOSK_PIECE_URL__", "StartPage": "homepage" }, "NewTabPage": false, "DisableProfileImport": true, "DontCheckDefaultBrowser": true, "NoDefaultBookmarks": true, "DisableTelemetry": true, "DisableFirefoxStudies": true, "DisablePocket": true, "DisplayBookmarksToolbar": "never", "DisplayMenuBar": "never", "SearchBar": "none", "Permissions": { "Notifications": { "BlockNewRequests": true, "Locked": true }, "Location": { "Allow": ["http://localhost:8080"], "BlockNewRequests": false }, "Camera": { "Allow": ["http://localhost:8080"], "BlockNewRequests": false }, "Microphone": { "Allow": ["http://localhost:8080"], "BlockNewRequests": false }, "Autoplay": { "Default": "allow-audio-video", "Allow": ["http://localhost:8080"], "Locked": true } }, "PopupBlocking": { "Default": true }, "FirefoxSuggest": { "WebSuggestions": false, "SponsoredSuggestions": false, "ImproveSuggest": false } }}FPEOFsed -i "s|__KIOSK_PIECE_URL__|$KIOSK_PIECE_URL|g" "$ROOTFS_DIR/usr/lib64/firefox/distribution/policies.json"
cat > "$ROOTFS_DIR/usr/lib64/firefox/defaults/pref/autoconfig.js" << 'ACEOF'pref("general.config.filename", "firefox.cfg");pref("general.config.obscure_value", 0);ACEOF
cat > "$ROOTFS_DIR/usr/lib64/firefox/firefox.cfg" << 'CFGEOF'// FedAC Kiosk Firefox configdefaultPref("browser.sessionstore.resume_from_crash", false);defaultPref("browser.startup.homepage_override.mstone", "ignore");defaultPref("browser.startup.page", 1);defaultPref("browser.startup.homepage", "__KIOSK_PIECE_URL__");defaultPref("browser.startup.blankWindow", false);defaultPref("browser.display.background_color", "#000000");defaultPref("browser.tabs.warnOnClose", false);defaultPref("browser.tabs.drawInTitlebar", false);defaultPref("browser.tabs.inTitlebar", 0);defaultPref("browser.shell.checkDefaultBrowser", false);defaultPref("datareporting.policy.dataSubmissionEnabled", false);defaultPref("toolkit.telemetry.reportingpolicy.firstRun", false);defaultPref("browser.newtabpage.enabled", false);defaultPref("browser.aboutwelcome.enabled", false);defaultPref("toolkit.legacyUserProfileCustomizations.stylesheets", true);defaultPref("accessibility.typeaheadfind", false);defaultPref("accessibility.typeaheadfind.manual", false);defaultPref("accessibility.typeaheadfind.linksonly", false);defaultPref("gfx.webrender.force-disabled", true);defaultPref("gfx.webrender.software", false);defaultPref("layers.acceleration.disabled", true);// Allow Web Audio API autoplay (kiosk — no user gesture needed)defaultPref("media.autoplay.default", 0);defaultPref("media.autoplay.blocking_policy", 0);defaultPref("media.autoplay.allow-extension-background-pages", true);// Suppress notification prompts, allow hardware access (kiosk = trust all)defaultPref("permissions.default.desktop-notification", 2);defaultPref("permissions.default.geo", 1);defaultPref("permissions.default.camera", 1);defaultPref("permissions.default.microphone", 1);defaultPref("dom.webnotifications.enabled", false);defaultPref("dom.push.enabled", false);// MIDI: allow without prompting (including SysEx)defaultPref("midi.prompt_on_sysex_access", false);defaultPref("midi.testing", true);defaultPref("dom.webmidi.enabled", true);defaultPref("browser.urlbar.suggest.searches", false);defaultPref("security.insecure_connection_text.enabled", false);defaultPref("security.insecure_connection_text.pbmode.enabled", false);defaultPref("browser.contentblocking.introCount", 99);CFGEOFsed -i "s|__KIOSK_PIECE_URL__|$KIOSK_PIECE_URL|g" "$ROOTFS_DIR/usr/lib64/firefox/firefox.cfg"echo -e " ${GREEN}Firefox policies installed${NC}"
# 3g. Plymouth PALS logoPALS_LOGO="$REPO_ROOT/fedac/plymouth/pals.png"if [ -f "$PALS_LOGO" ]; then cp "$PALS_LOGO" "$ROOTFS_DIR/usr/share/plymouth/themes/spinner/watermark.png" # Center the logo and set black background sed -i 's/^WatermarkVerticalAlignment=.*/WatermarkVerticalAlignment=.5/' \ "$ROOTFS_DIR/usr/share/plymouth/themes/spinner/spinner.plymouth" cat > "$ROOTFS_DIR/etc/plymouth/plymouthd.conf" << 'PLYEOF'[Daemon]Theme=spinnerShowDelay=0PLYEOF echo -e " ${GREEN}PALS Plymouth theme installed${NC}"fi
# 3h. dconf kiosk settingsmkdir -p "$ROOTFS_DIR/etc/dconf/db/local.d" "$ROOTFS_DIR/etc/dconf/profile"cp "$OVERLAY_DIR/00-kiosk" "$ROOTFS_DIR/etc/dconf/db/local.d/00-kiosk" 2>/dev/null || truecp "$OVERLAY_DIR/dconf-profile-user" "$ROOTFS_DIR/etc/dconf/profile/user" 2>/dev/null || true
echo -e " ${GREEN}All kiosk config injected${NC}"
# ══════════════════════════════════════════# Step 3i: Strip bloat from rootfs (kiosk only needs browser + Wayland + audio)# ══════════════════════════════════════════echo -e "${CYAN} Stripping unnecessary packages from rootfs...${NC}"ROOTFS_BEFORE=$(du -sb "$ROOTFS_DIR" 2>/dev/null | awk '{print $1}')
# Mount /dev, /proc, /sys for chroot dnf operationsmount --bind /dev "$ROOTFS_DIR/dev" 2>/dev/null || truemount --bind /proc "$ROOTFS_DIR/proc" 2>/dev/null || truemount --bind /sys "$ROOTFS_DIR/sys" 2>/dev/null || true
# Remove large unnecessary packages via dnf (resolves dependencies cleanly)STRIP_PKGS=( # Office suite (~300MB) "libreoffice*" # Java (~248MB) "java-*-openjdk*" # Virtualization (~110MB) "qemu*" "edk2-ovmf" "gnome-boxes" "libvirt*" "podman*" # LLVM (~145MB) "llvm-libs" # GNOME apps we don't need (~150MB+) "gnome-software*" "gnome-maps" "gnome-weather" "gnome-contacts" "gnome-calendar" "gnome-clocks" "gnome-connections" "gnome-characters" "gnome-font-viewer" "gnome-logs" "gnome-calculator" "gnome-text-editor" "gnome-tour" "gnome-photos" "gnome-music" "gnome-user-docs" "simple-scan" "baobab" "totem*" "cheese" "rhythmbox" "shotwell" "evince*" "yelp*" "loupe" "snapshot" "gnome-console" # Crash reporter (~30MB) "abrt*" "gnome-abrt" # IBus input methods (~156MB) — kiosk doesn't need multilingual input "ibus*" # WebKit (not needed, Firefox is our browser) (~180MB) "webkit2gtk*" "webkitgtk*" "javascriptcoregtk*" # Container tools "buildah" "skopeo" "toolbox" "containers-common*" # Evolution data server (email/calendar backend) "evolution*" # Flatpak "flatpak*" # Printing "cups*" # Misc heavy packages "unicode-ucd" "cldr-emoji-annotation" "anaconda*" "livecd-tools" "liveinst*" "mediawriter")chroot "$ROOTFS_DIR" /usr/bin/dnf -y remove "${STRIP_PKGS[@]}" 2>&1 | tail -5 || true
# Remove non-English locales (~220MB)echo -e " Stripping non-English locales..."find "$ROOTFS_DIR/usr/share/locale" -mindepth 1 -maxdepth 1 -type d \ ! -name "en" ! -name "en_US" ! -name "en_GB" -exec rm -rf {} + 2>/dev/null || true
# Remove docs, man pages, help (~250MB)rm -rf "$ROOTFS_DIR/usr/share/doc"rm -rf "$ROOTFS_DIR/usr/share/man"rm -rf "$ROOTFS_DIR/usr/share/help"rm -rf "$ROOTFS_DIR/usr/share/gtk-doc"rm -rf "$ROOTFS_DIR/usr/share/info"
# Remove Firefox langpacks (~43MB) — kiosk is English-onlyrm -rf "$ROOTFS_DIR/usr/lib64/firefox/langpacks" 2>/dev/null || truechroot "$ROOTFS_DIR" /usr/bin/dnf -y remove "firefox-langpacks" 2>&1 | tail -1 || true
# Remove all langpacks except English (~238MB)chroot "$ROOTFS_DIR" /usr/bin/dnf -y remove "glibc-all-langpacks" 2>&1 | tail -1 || truechroot "$ROOTFS_DIR" /usr/bin/dnf -y install "glibc-langpack-en" 2>&1 | tail -1 || true
# Remove CJK fonts (~57MB) — not needed for kioskrm -rf "$ROOTFS_DIR/usr/share/fonts/google-noto-serif-cjk-vf-fonts" 2>/dev/null || truerm -rf "$ROOTFS_DIR/usr/share/fonts/google-noto-sans-cjk-vf-fonts" 2>/dev/null || true
# Trim firmware — keep only common wifi + GPU, remove exotic/unused (~300MB savings)FIRMWARE_KEEP="intel|iwlwifi|amd|nvidia|ath|rtl|brcm|realtek|i915|radeon|amdgpu|mediatek"for fw_dir in "$ROOTFS_DIR/usr/lib/firmware"/*/; do fw_name=$(basename "$fw_dir") if ! echo "$fw_name" | grep -qiE "$FIRMWARE_KEEP"; then rm -rf "$fw_dir" 2>/dev/null || true fidone# Remove large individual firmware blobs we don't needrm -rf "$ROOTFS_DIR/usr/lib/firmware/liquidio" 2>/dev/null || truerm -rf "$ROOTFS_DIR/usr/lib/firmware/netronome" 2>/dev/null || truerm -rf "$ROOTFS_DIR/usr/lib/firmware/mellanox" 2>/dev/null || truerm -rf "$ROOTFS_DIR/usr/lib/firmware/dpaa2" 2>/dev/null || truerm -rf "$ROOTFS_DIR/usr/lib/firmware/qcom" 2>/dev/null || truerm -rf "$ROOTFS_DIR/usr/lib/firmware/cnm" 2>/dev/null || truerm -rf "$ROOTFS_DIR/usr/lib/firmware/qed" 2>/dev/null || truerm -rf "$ROOTFS_DIR/usr/lib/firmware/mrvl" 2>/dev/null || truerm -rf "$ROOTFS_DIR/usr/lib/firmware/cxgb4" 2>/dev/null || true
# Clean dnf cache inside rootfschroot "$ROOTFS_DIR" /usr/bin/dnf clean all 2>/dev/null || truerm -rf "$ROOTFS_DIR/var/cache/dnf" 2>/dev/null || truerm -rf "$ROOTFS_DIR/var/log/"* 2>/dev/null || true
# Unmount chroot bindsumount "$ROOTFS_DIR/sys" 2>/dev/null || trueumount "$ROOTFS_DIR/proc" 2>/dev/null || trueumount "$ROOTFS_DIR/dev" 2>/dev/null || true
ROOTFS_AFTER=$(du -sb "$ROOTFS_DIR" 2>/dev/null | awk '{print $1}')SAVED=$(( (ROOTFS_BEFORE - ROOTFS_AFTER) / 1048576 ))echo -e " ${GREEN}Stripped ${SAVED}MB from rootfs ($(numfmt --to=iec $ROOTFS_BEFORE) → $(numfmt --to=iec $ROOTFS_AFTER))${NC}"
# ── Install Wayland runtime required by kiosk-session.sh ───────────────echo -e " Installing Wayland runtime into rootfs (post-strip)..."# Re-mount chroot binds for dnfmount --bind /dev "$ROOTFS_DIR/dev" 2>/dev/null || truemount --bind /proc "$ROOTFS_DIR/proc" 2>/dev/null || truemount --bind /sys "$ROOTFS_DIR/sys" 2>/dev/null || truemount -t tmpfs tmpfs "$ROOTFS_DIR/tmp" 2>/dev/null || truecp /etc/resolv.conf "$ROOTFS_DIR/etc/resolv.conf" 2>/dev/null || trueif ! grep -q '^nameserver ' "$ROOTFS_DIR/etc/resolv.conf" 2>/dev/null; then cat > "$ROOTFS_DIR/etc/resolv.conf" << 'RSVEOF'nameserver 1.1.1.1nameserver 8.8.8.8RSVEOFelif grep -q '127\.0\.0\.53' "$ROOTFS_DIR/etc/resolv.conf" 2>/dev/null; then grep -q '^nameserver 1.1.1.1$' "$ROOTFS_DIR/etc/resolv.conf" 2>/dev/null || echo "nameserver 1.1.1.1" >> "$ROOTFS_DIR/etc/resolv.conf" grep -q '^nameserver 8.8.8.8$' "$ROOTFS_DIR/etc/resolv.conf" 2>/dev/null || echo "nameserver 8.8.8.8" >> "$ROOTFS_DIR/etc/resolv.conf"fi
DNF_INSTALL_FAILED=falseif [ -x "$ROOTFS_DIR/usr/bin/dnf" ]; then if ! chroot "$ROOTFS_DIR" /usr/bin/dnf -y install \ weston cage; then DNF_INSTALL_FAILED=true fielif command -v dnf >/dev/null 2>&1; then if ! dnf -y --installroot="$ROOTFS_DIR" --releasever="$FEDORA_VERSION" install \ weston cage; then DNF_INSTALL_FAILED=true fielse DNF_INSTALL_FAILED=truefi
umount "$ROOTFS_DIR/tmp" "$ROOTFS_DIR/sys" "$ROOTFS_DIR/proc" "$ROOTFS_DIR/dev" 2>/dev/null || trueif [ "$DNF_INSTALL_FAILED" = true ]; then echo -e " ${RED}ERROR: Failed to install Wayland runtime into rootfs${NC}" echo -e " ${RED}Check network/DNS on the build machine, then re-run.${NC}" exit 1fi
# ── Verify critical binaries survived the strip ──────────────────────echo -e " Verifying critical binaries..."MISSING=""for bin in firefox pipewire weston; do OK=false [ -x "$ROOTFS_DIR/usr/bin/$bin" ] && OK=true if [ "$OK" = true ]; then echo -e " ${GREEN}✓ $bin${NC}" else echo -e " ${RED}✗ $bin MISSING${NC}" MISSING="$MISSING $bin" fidoneif [ -x "$ROOTFS_DIR/usr/bin/cage" ]; then echo -e " ${GREEN}✓ cage${NC}"else echo -e " ${YELLOW}• cage missing (weston-only fallback)${NC}"fiif [ -n "$MISSING" ]; then echo -e " ${RED}ERROR: Missing critical binaries:${MISSING}${NC}" echo -e " ${RED}Aborting build to avoid flashing a non-booting kiosk image.${NC}" exit 1fi
# ══════════════════════════════════════════# Step 4: Build SquashFS + initrd# ══════════════════════════════════════════echo -e "${CYAN}[4/6] Building SquashFS image...${NC}"
EROFS_PATH="$WORK_DIR/squashfs.img"if command -v mksquashfs >/dev/null 2>&1; then mksquashfs "$ROOTFS_DIR/" "$EROFS_PATH" -comp zstd -Xcompression-level 3 -noappendelse echo -e "${RED}Error: mksquashfs not found. Install squashfs-tools.${NC}" exit 1fiEROFS_SIZE=$(stat -c%s "$EROFS_PATH")echo -e " ${GREEN}SquashFS built: $(numfmt --to=iec $EROFS_SIZE)${NC}"
# ── Framebuffer splash: paint PALS logo to /dev/fb0 ──────────────────# Pre-convert PALS PNG → raw BGRA for fast framebuffer blitting.# This is used both in the initrd (pre-Plymouth) and in kiosk-session.sh.PALS_FB_DIR="$ROOTFS_DIR/usr/local/share/pals-fb"mkdir -p "$PALS_FB_DIR"
if [ -f "$REPO_ROOT/fedac/plymouth/pals.png" ]; then python3 - "$REPO_ROOT/fedac/plymouth/pals.png" "$PALS_FB_DIR/logo.bgra" << 'PYEOF'import struct, sys, zlib
def read_png(path): """Minimal PNG reader → RGBA pixels. No PIL needed.""" with open(path, 'rb') as f: sig = f.read(8) assert sig == b'\x89PNG\r\n\x1a\n', "Not a PNG" chunks = {} idat = b'' while True: hdr = f.read(8) if len(hdr) < 8: break length = struct.unpack('>I', hdr[:4])[0] ctype = hdr[4:8] data = f.read(length) f.read(4) # crc if ctype == b'IHDR': w, h, bd, ct = struct.unpack('>IIBB', data[:10]) chunks['IHDR'] = (w, h, bd, ct) elif ctype == b'IDAT': idat += data elif ctype == b'IEND': break w, h, bd, ct = chunks['IHDR'] raw = zlib.decompress(idat) # ct=6 is RGBA, ct=2 is RGB bpp = 4 if ct == 6 else 3 stride = 1 + w * bpp # filter byte + pixel data pixels = bytearray(w * h * 4) prev_row = bytearray(w * bpp) for y in range(h): off = y * stride filt = raw[off] row = bytearray(raw[off+1:off+1+w*bpp]) if filt == 1: # Sub for i in range(bpp, len(row)): row[i] = (row[i] + row[i-bpp]) & 0xFF elif filt == 2: # Up for i in range(len(row)): row[i] = (row[i] + prev_row[i]) & 0xFF elif filt == 3: # Average for i in range(len(row)): a = row[i-bpp] if i >= bpp else 0 row[i] = (row[i] + (a + prev_row[i]) // 2) & 0xFF elif filt == 4: # Paeth for i in range(len(row)): a = row[i-bpp] if i >= bpp else 0 b = prev_row[i] c = prev_row[i-bpp] if i >= bpp else 0 p = a + b - c pa, pb, pc = abs(p-a), abs(p-b), abs(p-c) pr = a if pa <= pb and pa <= pc else (b if pb <= pc else c) row[i] = (row[i] + pr) & 0xFF for x in range(w): si = x * bpp r, g, b_val = row[si], row[si+1], row[si+2] a = row[si+3] if bpp == 4 else 255 di = (y * w + x) * 4 pixels[di:di+4] = bytes([r, g, b_val, a]) prev_row = row return w, h, bytes(pixels)
src, dst = sys.argv[1], sys.argv[2]w, h, rgba = read_png(src)# Convert RGBA → BGRA for Linux framebufferbgra = bytearray(len(rgba))for i in range(0, len(rgba), 4): bgra[i] = rgba[i+2] # B bgra[i+1] = rgba[i+1] # G bgra[i+2] = rgba[i] # R bgra[i+3] = rgba[i+3] # Awith open(dst, 'wb') as f: f.write(struct.pack('<II', w, h)) # 8-byte header: width, height (LE) f.write(bgra)print(f"Converted {w}x{h} PNG → BGRA ({len(bgra)+8} bytes)")PYEOFfi
# Framebuffer splash script (used in initrd hook AND kiosk-session.sh)cat > "$ROOTFS_DIR/usr/local/bin/pals-fb-splash" << 'FBEOF'#!/usr/bin/python3"""Paint PALS logo centered on dark purple to /dev/fb0."""import os, struct, sys
LOGO_PATH = "/usr/local/share/pals-fb/logo.bgra"BG_COLOR = (0x2e, 0x0a, 0x1a, 0xFF) # BGRA for #1a0a2e
def main(): if not os.path.exists("/dev/fb0"): return try: sz = open("/sys/class/graphics/fb0/virtual_size").read().strip().split(",") sw, sh = int(sz[0]), int(sz[1]) except Exception: return bpp_bits = 32 try: bpp_bits = int(open("/sys/class/graphics/fb0/bits_per_pixel").read().strip()) except Exception: pass if bpp_bits != 32: return stride = sw * 4
# Fill screen with dark purple row = bytes(BG_COLOR) * sw with open("/dev/fb0", "wb") as fb: for _ in range(sh): fb.write(row)
# Blit PALS logo centered if not os.path.exists(LOGO_PATH): return with open(LOGO_PATH, "rb") as f: lw, lh = struct.unpack("<II", f.read(8)) logo_data = f.read()
ox = max(0, (sw - lw) // 2) oy = max(0, (sh - lh) // 2)
with open("/dev/fb0", "r+b") as fb: for y in range(lh): if oy + y >= sh: break fb.seek((oy + y) * stride + ox * 4) src_off = y * lw * 4 row_data = logo_data[src_off:src_off + lw * 4] # Alpha-blend each pixel over background blended = bytearray(len(row_data)) for x in range(lw): if ox + x >= sw: break pi = x * 4 b, g, r, a = row_data[pi], row_data[pi+1], row_data[pi+2], row_data[pi+3] if a == 255: blended[pi:pi+4] = bytes([b, g, r, 255]) elif a == 0: blended[pi:pi+4] = bytes(BG_COLOR) else: inv = 255 - a blended[pi] = (b * a + BG_COLOR[0] * inv + 127) // 255 blended[pi+1] = (g * a + BG_COLOR[1] * inv + 127) // 255 blended[pi+2] = (r * a + BG_COLOR[2] * inv + 127) // 255 blended[pi+3] = 255 fb.write(blended[:min(lw, sw - ox) * 4])
if __name__ == "__main__": main()FBEOFchmod +x "$ROOTFS_DIR/usr/local/bin/pals-fb-splash"
# Create dracut module for early fb splash (runs before Plymouth)DRACUT_MOD="$ROOTFS_DIR/usr/lib/dracut/modules.d/00pals-fb"mkdir -p "$DRACUT_MOD"cat > "$DRACUT_MOD/module-setup.sh" << 'DRACEOF'#!/bin/bashcheck() { return 0; }depends() { return 0; }install() { inst_simple /usr/local/bin/pals-fb-splash inst_simple /usr/local/share/pals-fb/logo.bgra inst_hook pre-trigger 00 "$moddir/pals-fb-hook.sh"}DRACEOFcat > "$DRACUT_MOD/pals-fb-hook.sh" << 'HOOKEOF'#!/bin/sh# Paint PALS logo to framebuffer before Plymouth starts.# Runs in initrd with minimal environment.if [ -x /usr/local/bin/pals-fb-splash ] && [ -e /dev/fb0 ]; then /usr/local/bin/pals-fb-splash 2>/dev/null &fiHOOKEOFchmod +x "$DRACUT_MOD/module-setup.sh" "$DRACUT_MOD/pals-fb-hook.sh"echo -e " ${GREEN}Framebuffer splash installed (initrd + runtime)${NC}"
# Build initrd with PALS Plymouth theme + fb splashecho -e " Building initrd with PALS Plymouth + fb splash..."KVER=$(ls "$ROOTFS_DIR/lib/modules/" | head -1)
mount --bind /proc "$ROOTFS_DIR/proc"mount --bind /sys "$ROOTFS_DIR/sys"mount --bind /dev "$ROOTFS_DIR/dev"
chroot "$ROOTFS_DIR" dracut --force --no-hostonly \ --add "dmsquash-live plymouth pals-fb" \ /tmp/live-initrd.img "$KVER" 2>/dev/null || true
cp "$ROOTFS_DIR/tmp/live-initrd.img" "$WORK_DIR/initrd"cp "$ROOTFS_DIR/boot/vmlinuz-$KVER" "$WORK_DIR/linux"rm -f "$ROOTFS_DIR/tmp/live-initrd.img"
umount "$ROOTFS_DIR/proc" "$ROOTFS_DIR/sys" "$ROOTFS_DIR/dev"echo -e " ${GREEN}Initrd built with PALS theme + fb splash${NC}"
build_target() { local target="$1" local label="$2"
# Unmount existing partitions for part in "${target}"*; do umount "$part" 2>/dev/null || true done
# Wipe and partition: 512MB FAT32 EFI + main ext4 + 20MB FEDAC-PIECE echo -e " Creating partition table on ${label}..." wipefs -a "$target" >/dev/null 2>&1 # Compute LIVE partition end to avoid negative offsets (which confuse # parted on some versions / loop devices). local dev_bytes dev_bytes=$(blockdev --getsize64 "$target" 2>/dev/null || stat -c%s "$target" 2>/dev/null) local dev_mib=$((dev_bytes / 1048576)) local live_end=$((dev_mib - 20)) # leave 20 MiB for PIECE partition
parted -s "$target" mklabel gpt parted -s "$target" mkpart '"EFI"' fat32 1MiB 513MiB parted -s "$target" set 1 esp on parted -s "$target" mkpart '"LIVE"' ext4 513MiB "${live_end}MiB" parted -s "$target" mkpart '"PIECE"' ext4 "${live_end}MiB" 100%
sleep 2 partprobe "$target" 2>/dev/null || true sleep 2
# Detect partition names local p1="${target}1" local p2="${target}2" local p3="${target}3" [ -b "$p1" ] || p1="${target}p1" [ -b "$p2" ] || p2="${target}p2" [ -b "$p3" ] || p3="${target}p3"
# Format mkfs.vfat -F 32 -n BOOT "$p1" >/dev/null mkfs.ext4 -L FEDAC-LIVE -q "$p2" mkfs.ext4 -L FEDAC-PIECE -q "$p3" echo -e " ${GREEN}${label}: partitions created (EFI + LIVE + PIECE)${NC}"
# Write piece.html to FEDAC-PIECE partition local PIECE_MOUNT PIECE_MOUNT=$(mktemp -d /tmp/fedac-piece-XXXX) mount "$p3" "$PIECE_MOUNT" cp "$BUNDLE_PATH" "$PIECE_MOUNT/piece.html" [ -f "$FEDAC_DIR/overlays/kiosk/kiosk-shell.html" ] && \ sed "s|__DENSITY__|${PACK_DENSITY}|g" "$FEDAC_DIR/overlays/kiosk/kiosk-shell.html" > "$PIECE_MOUNT/kiosk-shell.html" chmod 777 "$PIECE_MOUNT" sync umount "$PIECE_MOUNT" rmdir "$PIECE_MOUNT" echo -e " ${GREEN}${label}: piece.html written to FEDAC-PIECE${NC}"
# Flash EROFS LIVE_MOUNT=$(mktemp -d /tmp/fedac-live-XXXX) mount "$p2" "$LIVE_MOUNT" mkdir -p "$LIVE_MOUNT/LiveOS" cp "$EROFS_PATH" "$LIVE_MOUNT/LiveOS/squashfs.img" sync umount "$LIVE_MOUNT" rmdir "$LIVE_MOUNT" LIVE_MOUNT="" echo -e " ${GREEN}${label}: EROFS written${NC}"
# Set up EFI partition EFI_MOUNT=$(mktemp -d /tmp/fedac-efi-XXXX) mount "$p1" "$EFI_MOUNT"
mkdir -p "$EFI_MOUNT/EFI/BOOT" # Use shimx64.efi as BOOTX64.EFI for Secure Boot support if [ -f "$ROOTFS_DIR/boot/efi/EFI/fedora/shimx64.efi" ]; then cp "$ROOTFS_DIR/boot/efi/EFI/fedora/shimx64.efi" "$EFI_MOUNT/EFI/BOOT/BOOTX64.EFI" # GRUB must sit alongside the shim so it can chain-load it cp "$ROOTFS_DIR/boot/efi/EFI/fedora/grubx64.efi" "$EFI_MOUNT/EFI/BOOT/grubx64.efi" elif [ -f "$ROOTFS_DIR/boot/efi/EFI/fedora/grubx64.efi" ]; then cp "$ROOTFS_DIR/boot/efi/EFI/fedora/grubx64.efi" "$EFI_MOUNT/EFI/BOOT/BOOTX64.EFI" elif [ -f "$ROOTFS_DIR/usr/lib/grub/x86_64-efi/monolithic/grubx64.efi" ]; then cp "$ROOTFS_DIR/usr/lib/grub/x86_64-efi/monolithic/grubx64.efi" "$EFI_MOUNT/EFI/BOOT/BOOTX64.EFI" fi
mkdir -p "$EFI_MOUNT/loader" cp "$WORK_DIR/linux" "$EFI_MOUNT/loader/linux" cp "$WORK_DIR/initrd" "$EFI_MOUNT/loader/initrd"
cat > "$EFI_MOUNT/EFI/BOOT/grub.cfg" << 'GRUBEOF'set default=0set timeout=0insmod all_videomenuentry "FedAC Kiosk" { linux /loader/linux quiet root=live:LABEL=FEDAC-LIVE rd.live.image rd.live.overlay.nouserconfirmprompt selinux=0 initrd /loader/initrd}GRUBEOF
mkdir -p "$EFI_MOUNT/EFI/fedora" cp "$EFI_MOUNT/EFI/BOOT/grub.cfg" "$EFI_MOUNT/EFI/fedora/grub.cfg"
sync umount "$EFI_MOUNT" rmdir "$EFI_MOUNT" EFI_MOUNT="" echo -e " ${GREEN}${label}: EFI configured${NC}"}
# ══════════════════════════════════════════# Step 5: Build image and/or flash USB# ══════════════════════════════════════════echo -e "${CYAN}[5/6] Building output media...${NC}"
if [ -n "$IMAGE_PATH" ]; then echo -e " Building disk image: ${GREEN}${IMAGE_PATH}${NC} (${IMAGE_SIZE_GB}GiB)" truncate -s "${IMAGE_SIZE_GB}G" "$IMAGE_PATH" LOOP_DEV=$(losetup --find --show --partscan "$IMAGE_PATH") build_target "$LOOP_DEV" "Disk image" losetup -d "$LOOP_DEV" LOOP_DEV="" echo -e " ${GREEN}Disk image ready${NC}"fi
if [ -n "$DEVICE" ]; then echo "" echo -e "USB target: ${YELLOW}$DEVICE${NC}" lsblk "$DEVICE" 2>/dev/null || true echo ""
if [ "$SKIP_CONFIRM" = false ]; then echo -e "${RED}ALL DATA ON $DEVICE WILL BE DESTROYED${NC}" read -p "Continue? [y/N] " confirm [ "$confirm" = "y" ] || [ "$confirm" = "Y" ] || { echo "Aborted."; exit 0; } fi
if [ -n "$IMAGE_PATH" ]; then echo -e " Flashing image to USB..." dd if="$IMAGE_PATH" of="$DEVICE" bs=4M status=progress conv=fsync sync echo -e " ${GREEN}USB flashed from image${NC}" else build_target "$DEVICE" "USB" fifi
# ══════════════════════════════════════════# Step 6: Finalize# ══════════════════════════════════════════echo -e "${CYAN}[6/6] Finalizing...${NC}"sync
# Generate manifest for base images (used by oven /os endpoint)if [ "$BASE_IMAGE_MODE" = true ] && [ -n "$IMAGE_PATH" ] && [ -f "$IMAGE_PATH" ]; then MANIFEST_PATH="${IMAGE_PATH%.img}-manifest.json" IMG_SIZE=$(stat -c%s "$IMAGE_PATH") IMG_SHA256=$(sha256sum "$IMAGE_PATH" | awk '{print $1}') # Get FEDAC-PIECE partition offset/size from GPT table (robust, no fdisk guessing). PIECE_META=$(python3 -c "import subprocessout = subprocess.check_output(['parted', '-s', '-m', '$IMAGE_PATH', 'unit', 'B', 'print'], text=True)part3 = Nonefor raw in out.splitlines(): line = raw.strip() if not line or not line[0].isdigit() or ':' not in line: continue cols = line.split(':') if len(cols) < 4: continue num = cols[0] start = int(cols[1].rstrip('B')) size = int(cols[3].rstrip('B')) name = cols[5].rstrip(';').strip().upper() if len(cols) > 5 else '' if name == 'PIECE': print(f'{start}:{size}') raise SystemExit(0) if num == '3': part3 = (start, size)if part3: print(f'{part3[0]}:{part3[1]}')" 2>/dev/null || true) PIECE_OFFSET=${PIECE_META%%:*} PIECE_SIZE=${PIECE_META##*:} [ -n "${PIECE_OFFSET:-}" ] || PIECE_OFFSET="0" [ -n "${PIECE_SIZE:-}" ] || PIECE_SIZE=$((20 * 1024 * 1024)) cat > "$MANIFEST_PATH" << MANIFEST_EOF{ "version": "$(date +%Y-%m-%d)", "fedora": "${FEDORA_RELEASE}", "piecePartitionOffset": ${PIECE_OFFSET}, "piecePartitionSize": ${PIECE_SIZE}, "totalSize": ${IMG_SIZE}, "sha256": "${IMG_SHA256}"}MANIFEST_EOF echo -e " ${GREEN}Manifest written: ${MANIFEST_PATH}${NC}"fi
if [ "$DO_EJECT" = true ] && [ -n "$DEVICE" ]; then eject "$DEVICE" 2>/dev/null || true echo -e " ${GREEN}Ejected${NC}"fi
# Clean up work dir (but not external rootfs)if [ "$OWN_ROOTFS" = true ]; then echo -e " Cleaning work dir..." rm -rf "$WORK_DIR"else rm -f "$WORK_DIR/piece.html" "$WORK_DIR/squashfs.img" "$WORK_DIR/initrd" "$WORK_DIR/linux" rmdir "$WORK_DIR" 2>/dev/null || truefi
echo ""echo -e "${GREEN}╔═══════════════════════════════════════════════╗${NC}"echo -e "${GREEN}║ FedAC Kiosk Build Ready ║${NC}"echo -e "${GREEN}║ Piece: ${CYAN}${PIECE_CODE}${GREEN} (offline) ║${NC}"echo -e "${GREEN}╚═══════════════════════════════════════════════╝${NC}"echo ""if [ -n "$IMAGE_PATH" ]; then echo -e "Image:" echo -e " ${CYAN}${IMAGE_PATH}${NC}" if [ "$BASE_IMAGE_MODE" = true ]; then echo -e "Manifest:" echo -e " ${CYAN}${MANIFEST_PATH}${NC}" echo "" echo -e "Upload to DO Spaces for oven /os endpoint:" echo -e " ${YELLOW}aws s3 cp ${IMAGE_PATH} s3://releases-aesthetic-computer/os/fedac-base-latest.img --endpoint-url https://sfo3.digitaloceanspaces.com --acl public-read${NC}" echo -e " ${YELLOW}aws s3 cp ${MANIFEST_PATH} s3://releases-aesthetic-computer/os/fedac-base-manifest.json --endpoint-url https://sfo3.digitaloceanspaces.com --acl public-read${NC}" fifiif [ -n "$DEVICE" ]; then echo -e "USB:" echo -e " ${CYAN}${DEVICE}${NC}"fiecho ""echo -e "Boot flow:"echo -e " Power on → GRUB (instant) → ${PINK}PALS splash${NC}"echo -e " → ${PINK}Firefox fullscreen → ${PIECE_CODE}${NC} (offline)"echo ""echo "Plug into target machine → boot from USB → done."