Remove deprecated one-shot scripts and update first-boot configuration for improved provisioning</message>

<message>Delete obsolete one-shot scripts for setting screen rotation and wallpaper, as well as related Python and shell scripts. Update the first-boot configuration to streamline the provisioning process by removing references to these scripts. This cleanup enhances maintainability and focuses on the essential steps required for the first boot experience, ensuring a more efficient setup for users.
This commit is contained in:
nearxos
2026-02-23 16:15:47 +02:00
parent 2d6e5aa009
commit 25bf710c67
51 changed files with 650 additions and 1192 deletions

View File

@@ -1,448 +1,178 @@
#!/bin/bash
# Revision: 2
# First-boot: packages, Chromium kiosk, rpd-labwc + touch, reTerminal DM drivers.
# Run by cloud-init (user-data-remote-gnss.example). Run as root.
# Optional: copy first-boot.conf.example to first-boot.conf and edit; it is loaded
# from the script dir, /tmp/first-boot.conf, or /etc/cm4-provisioning/first-boot.conf.
# Set ENABLE_STEP_01=0 .. ENABLE_STEP_13=0 in config to disable a step (default: all enabled).
# Revision: 4
# First-boot provisioning for reTerminal DM — two-phase process.
# Phase 1 (cloud-init): hostname, packages, reTerminal DM drivers → reboot
# Phase 2 (systemd oneshot): kiosk, theme, LightDM, rotation → reboot
#
# Step implementations are downloaded from FILE_SERVER/steps/ and sourced.
# All fileserver assets live in cloud-init/fileserver/ in the repo.
#
# REQUIRES first-boot.conf — looked up in order:
# 1. <script-dir>/first-boot.conf
# 2. /tmp/first-boot.conf (cloud-init runcmd downloads it)
# 3. /etc/cm4-provisioning/first-boot.conf
# 4. /var/lib/cm4-provisioning/first-boot.conf (persisted for phase 2)
# Copy first-boot.conf.example → first-boot.conf and fill in all values.
# Disable steps: ENABLE_STEP_01=0 .. ENABLE_STEP_13=0
set -e
export DEBIAN_FRONTEND=noninteractive
# --- Defaults (overridden by first-boot.conf if present) ---
# ── Phase tracking ──────────────────────────────────────────────────────
STATE_DIR="/var/lib/cm4-provisioning"
PHASE_FILE="$STATE_DIR/first-boot-phase"
PERSISTENT_SCRIPT="$STATE_DIR/first-boot.sh"
PERSISTENT_CONF="$STATE_DIR/first-boot.conf"
PHASE2_SERVICE="cm4-first-boot-phase2"
STEPS_DIR="/tmp/first-boot-steps"
LOGFILE="/var/log/first-boot.log"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" 2>/dev/null && pwd)"
FILE_SERVER="${FILE_SERVER:-http://10.20.50.1:5000/files/first-boot}"
HOSTNAME="${HOSTNAME:-guard}"
PI_USER="${PI_USER:-pi}"
LOGFILE="${LOGFILE:-/var/log/first-boot.log}"
PLYMOUTH_DIR="${PLYMOUTH_DIR:-/usr/share/plymouth/themes/custom}"
WALLPAPER_PATH="${WALLPAPER_PATH:-/usr/share/rpd-wallpaper/splash.png}"
PACKAGES="${PACKAGES:-git chromium wmctrl openssh-server swaybg wlr-randr maliit-keyboard xinput-calibrator python3-gi python3-gi-cairo}"
LIGHTDM_SESSION="${LIGHTDM_SESSION:-rpd-labwc}"
GTK_THEME_NAME="${GTK_THEME_NAME:-PiXnoir}"
WALLPAPER_MODE="${WALLPAPER_MODE:-crop}"
DSI_ROTATE="${DSI_ROTATE:-90}"
SWIOTLB_SIZE="${SWIOTLB_SIZE:-65536}"
RETERMINAL_DEVICE="${RETERMINAL_DEVICE:-reTerminal-DM}"
RETERMINAL_REPO_URL="${RETERMINAL_REPO_URL:-https://github.com/Seeed-Studio/seeed-linux-dtoverlays}"
ONESHOT_SCRIPTS="${ONESHOT_SCRIPTS:-}"
# --- Load config file first (first found); then defaults apply only to unset vars ---
# ── Load config (required) ──────────────────────────────────────────────
FIRST_BOOT_CONF=""
for _f in "$SCRIPT_DIR/first-boot.conf" /tmp/first-boot.conf /etc/cm4-provisioning/first-boot.conf; do
for _f in "$SCRIPT_DIR/first-boot.conf" /tmp/first-boot.conf \
/etc/cm4-provisioning/first-boot.conf "$PERSISTENT_CONF"; do
if [[ -f "$_f" ]]; then
# shellcheck source=first-boot.conf.example
set -a && source "$_f" && set +a
FIRST_BOOT_CONF="$_f"
break
fi
done
# Step enable flags (1 = run, 0 = skip). Default all enabled; only set if not already set by config.
ENABLE_STEP_01="${ENABLE_STEP_01:-1}"
ENABLE_STEP_02="${ENABLE_STEP_02:-1}"
ENABLE_STEP_03="${ENABLE_STEP_03:-1}"
ENABLE_STEP_04="${ENABLE_STEP_04:-1}"
ENABLE_STEP_05="${ENABLE_STEP_05:-1}"
ENABLE_STEP_06="${ENABLE_STEP_06:-1}"
ENABLE_STEP_07="${ENABLE_STEP_07:-1}"
ENABLE_STEP_08="${ENABLE_STEP_08:-1}"
ENABLE_STEP_09="${ENABLE_STEP_09:-1}"
ENABLE_STEP_10="${ENABLE_STEP_10:-1}"
ENABLE_STEP_11="${ENABLE_STEP_11:-1}"
ENABLE_STEP_12="${ENABLE_STEP_12:-1}"
ENABLE_STEP_13="${ENABLE_STEP_13:-1}"
# --- Derived paths ---
PI_HOME="/home/$PI_USER"
AUTOSTART="$PI_HOME/.config/autostart"
# Portal base URL for first-boot status API (derived from FILE_SERVER, e.g. http://10.20.50.1:5000)
if [[ "$FILE_SERVER" =~ ^(https?://[^/]+) ]]; then
PORTAL_BASE="${BASH_REMATCH[1]}"
else
PORTAL_BASE=""
if [[ -z "$FIRST_BOOT_CONF" ]]; then
echo "[$(date -Iseconds)] ERROR: first-boot.conf not found. Searched:" >&2
echo " $SCRIPT_DIR/first-boot.conf" >&2
echo " /tmp/first-boot.conf" >&2
echo " /etc/cm4-provisioning/first-boot.conf" >&2
echo " $PERSISTENT_CONF" >&2
exit 1
fi
# --- Logging ---
# Step enable flags (default: all enabled)
for _n in 01 02 03 04 05 06 07 08 09 10 11 12 13; do
eval "ENABLE_STEP_${_n}=\"\${ENABLE_STEP_${_n}:-1}\""
done
# ── Derived ─────────────────────────────────────────────────────────────
PI_HOME="/home/$PI_USER"
AUTOSTART="$PI_HOME/.config/autostart"
[[ "$FILE_SERVER" =~ ^(https?://[^/]+) ]] && PORTAL_BASE="${BASH_REMATCH[1]}" || PORTAL_BASE=""
[[ -f "$PHASE_FILE" ]] && CURRENT_PHASE="$(cat "$PHASE_FILE")" || CURRENT_PHASE="1"
# ── Logging ─────────────────────────────────────────────────────────────
log() { echo "[$(date -Iseconds)] $*"; }
exec > >(tee -a "$LOGFILE") 2>&1
log "=== first-boot.sh started ==="
[[ -n "$FIRST_BOOT_CONF" ]] && log "Config loaded from $FIRST_BOOT_CONF" || log "Using built-in defaults (no config file found)"
log "FILE_SERVER=$FILE_SERVER PI_USER=$PI_USER HOSTNAME=$HOSTNAME LOGFILE=$LOGFILE"
log "=== first-boot.sh phase $CURRENT_PHASE ==="
log "Config: $FIRST_BOOT_CONF"
log "FILE_SERVER=$FILE_SERVER PI_USER=$PI_USER HOSTNAME=$HOSTNAME"
# --- Report status to portal (no-op if PORTAL_BASE empty; failures ignored) ---
# ── Portal status API ──────────────────────────────────────────────────
report_status() {
local phase="$1" message="$2" step="$3" step_name="$4" ip="$5"
[[ -z "$PORTAL_BASE" ]] && return 0
local json="{\"phase\":\"${phase}\",\"message\":\"${message}\",\"step\":\"${step}\",\"step_name\":\"${step_name}\",\"hostname\":\"${HOSTNAME}\",\"ip\":\"${ip}\"}"
curl -s -X POST -H "Content-Type: application/json" -d "$json" "${PORTAL_BASE}/api/first-boot-status" || true
curl -s -X POST -H "Content-Type: application/json" \
-d "{\"phase\":\"$1\",\"message\":\"$2\",\"step\":\"$3\",\"step_name\":\"$4\",\"hostname\":\"$HOSTNAME\",\"ip\":\"$5\"}" \
"${PORTAL_BASE}/api/first-boot-status" || true
}
report_status "started" "First-boot started" "" "" ""
report_status "started" "Phase $CURRENT_PHASE started" "" "" ""
# --- Helper: run step if enabled (accepts "1" or "0"; strips CR/LF/whitespace) ---
run_step() {
local n="$1" name="$2"
local enable_var="ENABLE_STEP_${n}" val
val="${!enable_var}"
val="${val//[^01]/}"
val="${val:0:1}"
if [[ "$val" == "1" ]]; then
log "--- Step $n: $name ---"
"step_${n}_${name}" || return $?
report_status "running" "Step $n: $name completed" "$n" "$name" ""
else
log "--- Step $n: $name (disabled) ---"
fi
}
# --- Helper: install one-shot from FILE_SERVER ---
# ── Helpers (available to step scripts) ─────────────────────────────────
install_oneshot() {
local name="$1"
log "Installing one-shot: $name"
if curl -fsSL "${FILE_SERVER}/${name}.sh" -o "$PI_HOME/${name}.sh"; then
log "Downloaded ${name}.sh to $PI_HOME/${name}.sh"
else
log "WARNING: Could not download ${name}.sh"; return 1
fi
if curl -fsSL "${FILE_SERVER}/${name}.desktop" -o "$AUTOSTART/${name}.desktop"; then
log "Downloaded ${name}.desktop to $AUTOSTART/${name}.desktop"
else
log "WARNING: Could not download ${name}.desktop"; return 1
fi
chmod 755 "$PI_HOME/${name}.sh" && chmod 644 "$AUTOSTART/${name}.desktop"
curl -fsSL "${FILE_SERVER}/${name}.sh" -o "$PI_HOME/${name}.sh" || { log "WARNING: ${name}.sh download failed"; return 1; }
curl -fsSL "${FILE_SERVER}/${name}.desktop" -o "$AUTOSTART/${name}.desktop" || { log "WARNING: ${name}.desktop download failed"; return 1; }
chmod 755 "$PI_HOME/${name}.sh"
chmod 644 "$AUTOSTART/${name}.desktop"
chown "$PI_USER:$PI_USER" "$PI_HOME/${name}.sh" "$AUTOSTART/${name}.desktop"
log "One-shot $name installed (will run at first login and then remove itself)"
log "One-shot $name installed"
}
# --- Step 01: Hostname and /etc/hosts ---
step_01_hostname() {
echo "$HOSTNAME" > /etc/hostname
hostnamectl set-hostname "$HOSTNAME" 2>/dev/null || true
if ! grep -q "127.0.1.1[[:space:]]*$HOSTNAME" /etc/hosts 2>/dev/null; then
sed -i "/127.0.1.1[[:space:]].*$/d" /etc/hosts
echo "127.0.1.1 $HOSTNAME" >> /etc/hosts
fi
log "Hostname set to $HOSTNAME; /etc/hosts updated"
}
# --- Step 02: Packages ---
step_02_packages() {
log "Running apt-get update ..."
apt-get update -qq
log "Installing: $PACKAGES"
apt-get install -y -qq $PACKAGES
log "Packages installed successfully"
}
# --- Step 03: reTerminal DM drivers (Seeed) ---
step_03_reterminal_drivers() {
if [[ -z "$RETERMINAL_REPO_URL" ]]; then
log "Skipping reTerminal drivers (RETERMINAL_REPO_URL not set)"
# ── Download, source, and run a step ────────────────────────────────────
run_step() {
local n="$1" name="$2"
local val; eval "val=\"\${ENABLE_STEP_${n}}\""
val="${val//[^01]/}"
if [[ "${val:0:1}" != "1" ]]; then
log "--- Step $n: $name (disabled) ---"
return 0
fi
REPO_DIR="/tmp/seeed-linux-dtoverlays"
log "Cloning seeed-linux-dtoverlays to $REPO_DIR ..."
git clone --depth 1 "$RETERMINAL_REPO_URL" "$REPO_DIR"
log "Running reTerminal.sh --device $RETERMINAL_DEVICE from $REPO_DIR ..."
if ( cd "$REPO_DIR" && "$REPO_DIR/scripts/reTerminal.sh" --device "$RETERMINAL_DEVICE" ); then
log "reTerminal DM drivers installed (reboot will apply)"
else
log "WARNING: reTerminal.sh failed (see log above). Display/touch may still work; you can retry later with: cd $REPO_DIR && sudo ./scripts/reTerminal.sh --device $RETERMINAL_DEVICE"
fi
rm -rf "$REPO_DIR"
log "--- Step $n: $name ---"
local f="$STEPS_DIR/${n}-${name}.sh"
curl -fsSL "${FILE_SERVER}/steps/${n}-${name}.sh" -o "$f" \
|| { log "ERROR: could not download step ${n}-${name}.sh"; return 1; }
source "$f"
"step_${n}_${name}"
report_status "running" "Step $n: $name done" "$n" "$name" ""
}
# --- Step 04: Kiosk files from file server ---
step_04_kiosk_files() {
log "Creating $AUTOSTART"
mkdir -p "$AUTOSTART" "$PI_HOME/Desktop" "$PI_HOME/.local/share/applications"
log "Downloading start-chromium.sh from ${FILE_SERVER}/start-chromium.sh"
curl -fsSL "${FILE_SERVER}/start-chromium.sh" -o "$PI_HOME/start-chromium.sh"
log "Downloading chromium-kiosk.desktop from ${FILE_SERVER}/chromium-kiosk.desktop"
curl -fsSL "${FILE_SERVER}/chromium-kiosk.desktop" -o "$AUTOSTART/chromium-kiosk.desktop"
chmod 755 "$PI_HOME/start-chromium.sh" && chmod 644 "$AUTOSTART/chromium-kiosk.desktop"
chown -R "$PI_USER:$PI_USER" "$PI_HOME/start-chromium.sh" "$AUTOSTART/chromium-kiosk.desktop"
# Launcher icon: desktop + application menu (start Chromium when killed)
if curl -fsSL "${FILE_SERVER}/chromium-kiosk-launcher.desktop" -o "$PI_HOME/chromium-kiosk-launcher.desktop" 2>/dev/null; then
sed "s|/home/pi|$PI_HOME|g" "$PI_HOME/chromium-kiosk-launcher.desktop" > "$PI_HOME/Desktop/Chromium Kiosk.desktop"
cp "$PI_HOME/chromium-kiosk-launcher.desktop" "$PI_HOME/.local/share/applications/"
sed -i "s|/home/pi|$PI_HOME|g" "$PI_HOME/.local/share/applications/chromium-kiosk-launcher.desktop"
chmod 755 "$PI_HOME/Desktop/Chromium Kiosk.desktop"
chmod 644 "$PI_HOME/.local/share/applications/chromium-kiosk-launcher.desktop"
chown "$PI_USER:$PI_USER" "$PI_HOME/Desktop/Chromium Kiosk.desktop" "$PI_HOME/.local/share/applications/chromium-kiosk-launcher.desktop"
log "Chromium kiosk launcher icon installed on desktop and in application menu"
fi
# 5 taps in top-right corner close Chromium (Python + Gtk)
if curl -fsSL "${FILE_SERVER}/five-tap-close-chromium.py" -o "$PI_HOME/five-tap-close-chromium.py" 2>/dev/null; then
chmod 755 "$PI_HOME/five-tap-close-chromium.py"
if curl -fsSL "${FILE_SERVER}/five-tap-close-chromium.desktop" -o "$AUTOSTART/five-tap-close-chromium.desktop" 2>/dev/null; then
sed -i "s|/home/pi|$PI_HOME|g" "$AUTOSTART/five-tap-close-chromium.desktop"
chmod 644 "$AUTOSTART/five-tap-close-chromium.desktop"
chown "$PI_USER:$PI_USER" "$PI_HOME/five-tap-close-chromium.py" "$AUTOSTART/five-tap-close-chromium.desktop"
log "5-tap top-right close Chromium installed (autostart)"
fi
fi
log "Kiosk files installed under $PI_HOME and $AUTOSTART"
}
# ── Phase transitions ──────────────────────────────────────────────────
phase1_reboot() {
mkdir -p "$STATE_DIR"
cp "$(readlink -f "${BASH_SOURCE[0]:-.}")" "$PERSISTENT_SCRIPT"
chmod 755 "$PERSISTENT_SCRIPT"
[[ -n "$FIRST_BOOT_CONF" && -f "$FIRST_BOOT_CONF" && "$FIRST_BOOT_CONF" != "$PERSISTENT_CONF" ]] \
&& cp "$FIRST_BOOT_CONF" "$PERSISTENT_CONF"
echo "2" > "$PHASE_FILE"
# --- Step 05: Boot splash and wallpaper ---
step_05_splash_wallpaper() {
log "Creating $PLYMOUTH_DIR and /usr/share/rpd-wallpaper"
mkdir -p "$PLYMOUTH_DIR" /usr/share/rpd-wallpaper
if curl -fsSL "${FILE_SERVER}/splash.png" -o "$PLYMOUTH_DIR/splash.png"; then
log "Downloaded splash.png; copying to $WALLPAPER_PATH"
cp "$PLYMOUTH_DIR/splash.png" "$WALLPAPER_PATH"
chmod 644 "$PLYMOUTH_DIR/splash.png" "$WALLPAPER_PATH"
if curl -fsSL "${FILE_SERVER}/custom.plymouth" -o "$PLYMOUTH_DIR/custom.plymouth" \
&& curl -fsSL "${FILE_SERVER}/custom.script" -o "$PLYMOUTH_DIR/custom.script"; then
chmod 644 "$PLYMOUTH_DIR/custom.plymouth" "$PLYMOUTH_DIR/custom.script"
log "Plymouth theme files (custom.plymouth, custom.script) installed"
else
log "WARNING: Could not download custom.plymouth/custom.script; boot splash theme may be incomplete"
fi
grep -q '^Theme=custom' /etc/plymouth/plymouthd.conf 2>/dev/null || printf '%s\n' '[Daemon]' 'Theme=custom' >> /etc/plymouth/plymouthd.conf
log "Running update-initramfs (may take a moment) ..."
update-initramfs -u -k all 2>/dev/null || true
mkdir -p /etc/lightdm/lightdm.conf.d
curl -fsSL "${FILE_SERVER}/99-wallpaper.conf" -o /etc/lightdm/lightdm.conf.d/99-wallpaper.conf 2>/dev/null || log "WARNING: Could not download 99-wallpaper.conf"
for PROFILE in LXDE-pi default; do
PCMANFM_DESKTOP="$PI_HOME/.config/pcmanfm/$PROFILE/desktop-items-0.conf"
mkdir -p "$(dirname "$PCMANFM_DESKTOP")"
if [[ ! -f "$PCMANFM_DESKTOP" ]]; then
printf '%s\n' '[*]' "wallpaper=$WALLPAPER_PATH" "wallpaper_mode=$WALLPAPER_MODE" 'wallpaper_common=1' 'show_trash=0' > "$PCMANFM_DESKTOP"
else
grep -q '^wallpaper=' "$PCMANFM_DESKTOP" && sed -i "s|^wallpaper=.*|wallpaper=$WALLPAPER_PATH|" "$PCMANFM_DESKTOP" || echo "wallpaper=$WALLPAPER_PATH" >> "$PCMANFM_DESKTOP"
grep -q '^wallpaper_mode=' "$PCMANFM_DESKTOP" && sed -i "s/^wallpaper_mode=.*/wallpaper_mode=$WALLPAPER_MODE/" "$PCMANFM_DESKTOP" || echo "wallpaper_mode=$WALLPAPER_MODE" >> "$PCMANFM_DESKTOP"
grep -q '^show_trash=' "$PCMANFM_DESKTOP" && sed -i 's/^show_trash=.*/show_trash=0/' "$PCMANFM_DESKTOP" || echo 'show_trash=0' >> "$PCMANFM_DESKTOP"
fi
chown -R "$PI_USER:$PI_USER" "$(dirname "$PCMANFM_DESKTOP")"
done
log "Set desktop wallpaper via pcmanfm config (LXDE-pi and default)"
log "Splash and wallpaper set from file server"
else
log "WARNING: Could not download splash.png"
fi
# Optional: taskbar start button icon from file server (start-here.png)
if curl -fsSL "${FILE_SERVER}/start-here.png" -o /tmp/start-here.png; then
PIXTRIX_PLACES="/usr/share/icons/PiXtrix"
if [[ -d "$PIXTRIX_PLACES/32x32/places" ]]; then
for s in 16 24 32 48 64 96; do
DEST="$PIXTRIX_PLACES/${s}x${s}/places/start-here.png"
if [[ -d "$(dirname "$DEST")" ]]; then
rm -f "$DEST"
if command -v convert >/dev/null 2>&1; then
convert /tmp/start-here.png -resize "${s}x${s}" "$DEST" 2>/dev/null && true
else
cp /tmp/start-here.png "$DEST"
fi
chmod 644 "$DEST"
fi
done
log "Taskbar start button icon (start-here.png) installed from file server"
else
log "WARNING: PiXtrix theme places dir not found; skipped taskbar icon"
fi
rm -f /tmp/start-here.png
fi
}
# --- Step 06: LightDM session ---
step_06_lightdm() {
mkdir -p /etc/lightdm/lightdm.conf.d
if curl -fsSL "${FILE_SERVER}/99-default-session.conf" -o /etc/lightdm/lightdm.conf.d/99-default-session.conf 2>/dev/null; then
log "99-default-session.conf installed"
else
log "WARNING: Could not download 99-default-session.conf"
fi
if [[ -f /etc/lightdm/lightdm.conf ]]; then
sed -i "s/^user-session=.*/user-session=$LIGHTDM_SESSION/" /etc/lightdm/lightdm.conf
sed -i "s/^autologin-session=.*/autologin-session=$LIGHTDM_SESSION/" /etc/lightdm/lightdm.conf
log "Patched /etc/lightdm/lightdm.conf to use $LIGHTDM_SESSION"
fi
# Delay LightDM on first boot after provisioning so reTerminal DM DSI panel has time to init (avoids black screen on first reboot)
mkdir -p /etc/systemd/system/lightdm.service.d
cat > /etc/systemd/system/cm4-await-display.service << 'AWAITSVC'
cat > "/etc/systemd/system/${PHASE2_SERVICE}.service" <<PHASE2SVC
[Unit]
Description=Wait for reTerminal DM display on first boot after provisioning
Before=lightdm.service
DefaultDependencies=no
Description=reTerminal DM first-boot phase 2
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'if [ -f /var/lib/cm4-provisioning/await-display ]; then echo "Waiting 18s for DSI panel..."; sleep 18; rm -f /var/lib/cm4-provisioning/await-display; fi'
ExecStart=$PERSISTENT_SCRIPT
StandardOutput=journal+console
TimeoutStartSec=600
[Install]
WantedBy=graphical.target
AWAITSVC
printf '%s\n' '[Unit]' 'After=cm4-await-display.service' > /etc/systemd/system/lightdm.service.d/99-await-display.conf
WantedBy=multi-user.target
PHASE2SVC
systemctl daemon-reload
systemctl enable cm4-await-display.service 2>/dev/null || true
log "Installed cm4-await-display.service (delays LightDM on first reboot after provisioning)"
}
systemctl enable "${PHASE2_SERVICE}.service"
# --- Step 07: Maliit on-screen keyboard ---
step_07_maliit() {
mkdir -p "$AUTOSTART" "$PI_HOME/.config"
curl -fsSL "${FILE_SERVER}/maliit-keyboard.desktop" -o "$AUTOSTART/maliit-keyboard.desktop" 2>/dev/null && log "maliit-keyboard.desktop installed" || log "WARNING: Could not download maliit-keyboard.desktop"
}
# --- Step 08: Dark theme (GTK + taskbar) ---
step_08_dark_theme() {
# Use Adwaita-dark if PiXnoir is not installed (e.g. some Raspberry Pi OS images)
local theme_name="$GTK_THEME_NAME"
if [[ ! -d "/usr/share/themes/${theme_name}" ]]; then
if [[ -d /usr/share/themes/Adwaita-dark ]]; then
theme_name="Adwaita-dark"
log "PiXnoir not found; using Adwaita-dark for dark theme"
fi
fi
GTK_SETTINGS="$PI_HOME/.config/gtk-3.0/settings.ini"
mkdir -p "$(dirname "$GTK_SETTINGS")"
# PiXtrix holds the custom start-here icon (installed in step 05); set so taskbar shows it
local icon_theme="PiXtrix"
[[ ! -d /usr/share/icons/PiXtrix ]] && icon_theme=""
if [[ ! -f "$GTK_SETTINGS" ]]; then
{ printf '%s\n' '[Settings]' 'gtk-application-prefer-dark-theme=1' "gtk-theme-name=$theme_name"
[[ -n "$icon_theme" ]] && echo "gtk-icon-theme-name=$icon_theme"
} > "$GTK_SETTINGS"
else
grep -q '^gtk-application-prefer-dark-theme=' "$GTK_SETTINGS" && sed -i 's/^gtk-application-prefer-dark-theme=.*/gtk-application-prefer-dark-theme=1/' "$GTK_SETTINGS" || echo 'gtk-application-prefer-dark-theme=1' >> "$GTK_SETTINGS"
grep -q '^gtk-theme-name=' "$GTK_SETTINGS" && sed -i "s/^gtk-theme-name=.*/gtk-theme-name=$theme_name/" "$GTK_SETTINGS" || echo "gtk-theme-name=$theme_name" >> "$GTK_SETTINGS"
if [[ -n "$icon_theme" ]]; then
grep -q '^gtk-icon-theme-name=' "$GTK_SETTINGS" && sed -i "s/^gtk-icon-theme-name=.*/gtk-icon-theme-name=$icon_theme/" "$GTK_SETTINGS" || echo "gtk-icon-theme-name=$icon_theme" >> "$GTK_SETTINGS"
fi
fi
log "Set dark theme ($theme_name) in gtk-3.0/settings.ini" && [[ -n "$icon_theme" ]] && log "Icon theme $icon_theme (custom start-here from step 05)"
# Ensure .profile exports prefer-dark for Libadwaita/GTK4 apps (session dark mode)
if [[ -f "$PI_HOME/.profile" ]] && ! grep -q 'ADW_DEBUG_COLOR_SCHEME\|COLOR_SCHEME.*prefer-dark' "$PI_HOME/.profile" 2>/dev/null; then
echo '' >> "$PI_HOME/.profile"
echo '# Prefer dark mode for GTK4/Libadwaita apps (cm4-provisioning first-boot)' >> "$PI_HOME/.profile"
echo 'export ADW_DEBUG_COLOR_SCHEME=prefer-dark' >> "$PI_HOME/.profile"
chown "$PI_USER:$PI_USER" "$PI_HOME/.profile"
log "Added prefer-dark export to .profile"
fi
# User GTK CSS: force start menu dropdown and all menus/popovers to full dark (matches Adwaita-dark palette)
if curl -fsSL "${FILE_SERVER}/gtk.css" -o "$PI_HOME/.config/gtk-3.0/gtk.css" 2>/dev/null; then
log "gtk.css installed (dark menu/popover override)"
fi
# Deploy dark taskbar (wf-panel-pi) so panel is dark and matches theme
local panel_conf="$PI_HOME/.config/wf-panel-pi"
mkdir -p "$panel_conf"
if curl -fsSL "${FILE_SERVER}/wf-panel-pi.ini" -o "$panel_conf/wf-panel-pi.ini" 2>/dev/null; then
sed -i "s|/home/pi|$PI_HOME|g" "$panel_conf/wf-panel-pi.ini"
if curl -fsSL "${FILE_SERVER}/panel-theme.css" -o "$panel_conf/panel-theme.css" 2>/dev/null; then
log "Taskbar theme (wf-panel-pi.ini, panel-theme.css) installed"
else
log "WARNING: Could not download panel-theme.css"
fi
else
log "WARNING: Could not download wf-panel-pi.ini (taskbar will use defaults)"
fi
chown -R "$PI_USER:$PI_USER" "$PI_HOME/.config"
}
# --- Step 09: Re-apply splash and Plymouth theme ---
step_09_reapply_splash() {
CFG_PATH="/boot/firmware/config.txt"
[[ -f /boot/firmware/config.txt ]] || CFG_PATH="/boot/config.txt"
if [[ -f "$CFG_PATH" ]] && grep -q '^disable_splash=1' "$CFG_PATH"; then
sed -i 's/^disable_splash=1$/disable_splash=0/' "$CFG_PATH"
log "Set disable_splash=0 so Plymouth splash is shown"
fi
if [[ -f /etc/plymouth/plymouthd.conf ]]; then
sed -i '/^Theme=/d' /etc/plymouth/plymouthd.conf
sed -i '/^\[Daemon\]$/d' /etc/plymouth/plymouthd.conf
grep -q '^\[Daemon\]' /etc/plymouth/plymouthd.conf || echo '[Daemon]' >> /etc/plymouth/plymouthd.conf
echo 'Theme=custom' >> /etc/plymouth/plymouthd.conf
log "Plymouth theme set to custom only"
fi
log "Running update-initramfs to apply Plymouth theme ..."
update-initramfs -u -k all 2>/dev/null || true
}
# --- Step 10: Kernel cmdline (swiotlb + DSI rotation) ---
step_10_cmdline() {
CMDLINE_PATH="/boot/firmware/cmdline.txt"
[[ -f "$CMDLINE_PATH" ]] || CMDLINE_PATH="/boot/cmdline.txt"
if [[ -f "$CMDLINE_PATH" ]]; then
if [[ -n "$SWIOTLB_SIZE" ]] && ! grep -q 'swiotlb=' "$CMDLINE_PATH"; then
sed -i "s/rootwait/rootwait swiotlb=$SWIOTLB_SIZE/" "$CMDLINE_PATH"
log "Added swiotlb=$SWIOTLB_SIZE to kernel cmdline (vc4-drm / DSI)"
fi
if [[ -n "$DSI_ROTATE" ]] && ! grep -q 'video=DSI-1:rotate=' "$CMDLINE_PATH"; then
sed -i "s/\$/ video=DSI-1:rotate=$DSI_ROTATE/" "$CMDLINE_PATH"
log "Added video=DSI-1:rotate=$DSI_ROTATE to kernel cmdline (DSI rotation)"
fi
fi
}
# --- Step 11: One-shot scripts ---
step_11_oneshots() {
if [[ -n "$DSI_ROTATE" ]]; then
log "Rotation is set via kernel cmdline (video=DSI-1:rotate=$DSI_ROTATE)"
# Install set-rotation-at-login to write ~/.config/kanshi/config at every login (same as Control Center)
if curl -fsSL "${FILE_SERVER}/set-rotation-at-login.sh" -o "$PI_HOME/set-rotation-at-login.sh" 2>/dev/null; then
chmod 755 "$PI_HOME/set-rotation-at-login.sh"
chown "$PI_USER:$PI_USER" "$PI_HOME/set-rotation-at-login.sh"
if curl -fsSL "${FILE_SERVER}/set-rotation-at-login.desktop" -o /tmp/set-rotation-at-login.desktop 2>/dev/null; then
sed "s|/home/pi|$PI_HOME|g" /tmp/set-rotation-at-login.desktop > "$AUTOSTART/set-rotation-at-login.desktop"
chown "$PI_USER:$PI_USER" "$AUTOSTART/set-rotation-at-login.desktop"
log "Installed set-rotation-at-login (re-applies rotation from cmdline every login)"
fi
rm -f /tmp/set-rotation-at-login.desktop
fi
fi
if [[ -n "$ONESHOT_SCRIPTS" ]]; then
for _name in $ONESHOT_SCRIPTS; do
install_oneshot "$_name" || true
done
else
log "No one-shot scripts configured (ONESHOT_SCRIPTS empty)"
fi
}
# --- Step 12: Log file permissions ---
step_12_log_permissions() {
chmod 666 "$LOGFILE"
log "Log file $LOGFILE is now appendable by user $PI_USER for one-shot scripts"
}
# --- Step 13: Reboot ---
step_13_reboot() {
DEVICE_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
report_status "done" "First-boot complete" "13" "reboot" "$DEVICE_IP"
log "Device IP: ${DEVICE_IP:-unknown}"
# Flag for cm4-await-display.service: on next boot delay LightDM so DSI panel can init (avoids black screen)
mkdir -p /var/lib/cm4-provisioning
touch /var/lib/cm4-provisioning/await-display
log "=== first-boot.sh finished, rebooting ==="
local ip; ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
report_status "phase1-done" "Phase 1 done, rebooting" "03" "reterminal_drivers" "$ip"
log "=== Phase 1 done — rebooting to activate drivers ==="
reboot
}
# --- Main: run steps in order ---
run_step 01 hostname
run_step 02 packages
run_step 03 reterminal_drivers
run_step 04 kiosk_files
run_step 05 splash_wallpaper
run_step 06 lightdm
run_step 07 maliit
run_step 08 dark_theme
run_step 09 reapply_splash
run_step 10 cmdline
run_step 11 oneshots
run_step 12 log_permissions
run_step 13 reboot
phase2_cleanup() {
systemctl disable "${PHASE2_SERVICE}.service" 2>/dev/null || true
rm -f "/etc/systemd/system/${PHASE2_SERVICE}.service"
systemctl daemon-reload
rm -f "$PHASE_FILE" "$PERSISTENT_SCRIPT" "$PERSISTENT_CONF"
log "Phase 2 cleanup done"
}
# If reboot was disabled, still report done and device IP so the portal shows completion
_step13_val="${ENABLE_STEP_13:-1}"
_step13_val="${_step13_val//[^01]/}"
_step13_val="${_step13_val:0:1}"
if [[ "$_step13_val" != "1" ]]; then
# ── Main ────────────────────────────────────────────────────────────────
mkdir -p "$STEPS_DIR"
if [[ "$CURRENT_PHASE" == "1" ]]; then
log "=== PHASE 1: hostname, packages, reTerminal drivers ==="
run_step 01 hostname
run_step 02 packages
run_step 03 reterminal_drivers
phase1_reboot
elif [[ "$CURRENT_PHASE" == "2" ]]; then
log "=== PHASE 2: kiosk, theme, display config ==="
run_step 04 kiosk_files
run_step 05 splash_wallpaper
run_step 06 lightdm
run_step 07 maliit
run_step 08 dark_theme
run_step 09 reapply_splash
run_step 10 cmdline
run_step 11 oneshots
run_step 12 log_permissions
phase2_cleanup
run_step 13 reboot
# Only reached if step 13 (reboot) is disabled
DEVICE_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
report_status "done" "First-boot complete (reboot disabled)" "13" "reboot" "$DEVICE_IP"
log "Device IP: ${DEVICE_IP:-unknown}"
log "=== first-boot.sh finished (reboot disabled) ==="
else
log "ERROR: Unknown phase '$CURRENT_PHASE'"; exit 1
fi