wrolpi/repair.sh
lrnselfreliance 944d95c717
Bake the app build into images; rebuild on-device only when stale (#536)
* Bake the app build into images; rebuild on-device only when stale

Previously the Raspberry Pi image shipped without app/build/ and compiled
the React bundle on first boot via repair.sh -- a multi-minute, memory-hungry
step that can OOM a 2GB Pi.  The Debian Live ISO already baked the build in,
so the two paths had drifted.

Add scripts/build_app.sh as the single source of truth: it hashes the build
inputs (src/, public/, package.json, package-lock.json, tsconfig.json) and
runs `npm run build` (with GENERATE_SOURCEMAP=false) only when build/ is
missing or those inputs changed, then writes build/.build-stamp.

Wire all three call sites to it:
- pi-gen 03-run-chroot.sh now builds after npm install, baking build/ into
  the Pi image like the Debian hook already does.
- debian-live 9999-wrolpi.hook.chroot uses it so the baked build is stamped.
- repair.sh uses it, so first-boot/manual-repair/install/upgrade all become
  no-ops when the shipped build is current.

Result: fresh images ship a stamped build/, so a weak Pi never compiles on
boot or repair; an on-device rebuild happens only when an upgrade actually
changes app/.  build_app.sh never runs npm install, so repair.sh keeps its
offline guarantee.

* build_app.sh: rebuild when built output is incomplete, not just missing

Addresses review feedback: a matching stamp only proves the inputs are
unchanged, not that the output survived.  repair.sh exists to fix corruption,
so a build/ with index.html + stamp but a missing JS/CSS asset must still
rebuild.  Verify every file referenced by build/asset-manifest.json exists
(and that the manifest itself parses); rebuild otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:19:25 -06:00

215 lines
8.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# This script will attempt to repair a WROLPi installation. It will not use internet.
cd /opt/wrolpi || (echo "Cannot repair. /opt/wrolpi does not exist" && exit 1)
# Re-execute this script if it wasn't called with sudo.
if [ $EUID != 0 ]; then
sudo "$0" "$@"
exit $?
fi
rpi=false
if (grep 'Raspberry Pi' /proc/cpuinfo >/dev/null); then
rpi=true
fi
set -e
set -x
# Stop services if they are running.
systemctl stop wrolpi-controller.service || :
systemctl stop wrolpi-api.service || :
systemctl stop wrolpi-app.service || :
systemctl stop wrolpi-kiwix.service || :
systemctl stop caddy || :
# Clear Python bytecode cache from virtual environments to fix potential corruption.
echo "Clearing Python bytecode cache..."
find /opt/wrolpi/venv -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || :
find /opt/wrolpi/venv -name "*.pyc" -delete 2>/dev/null || :
find /opt/wrolpi/controller/venv -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || :
find /opt/wrolpi/controller/venv -name "*.pyc" -delete 2>/dev/null || :
find /opt/wrolpi-help/venv -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || :
find /opt/wrolpi-help/venv -name "*.pyc" -delete 2>/dev/null || :
# Create the WROLPi user
grep wrolpi: /etc/passwd || useradd -md /home/wrolpi wrolpi -s "$(command -v bash)"
[ -f /home/wrolpi/.pgpass ] || cat >/home/wrolpi/.pgpass <<'EOF'
127.0.0.1:5432:wrolpi:wrolpi:wrolpi
EOF
chmod 0600 /home/wrolpi/.pgpass
# Reset any inadvertent changes to the WROLPi repo. Restore ownership in case this repair script fails.
git config --global --add safe.directory /opt/wrolpi
git reset HEAD --hard
chown -R wrolpi:wrolpi /opt/wrolpi
# Scripts are run by systemd services (e.g. wrolpi-cert-renew.service) which fail with
# "Permission denied" if the executable bit was lost.
chmod +x /opt/wrolpi/*.sh /opt/wrolpi/scripts/*.sh
# Copy configs to system.
mkdir -p /etc/caddy
cp /opt/wrolpi/etc/raspberrypios/Caddyfile /etc/caddy/Caddyfile
mkdir -p /var/www
cp /opt/wrolpi/etc/raspberrypios/50x.html /var/www/50x.html
# Generate certificate for HTTPS.
/opt/wrolpi/scripts/generate_certificates.sh
# Install map fonts from blob (needed for MapLibre map labels).
MAP_FONTS_BLOB=/opt/wrolpi-blobs/map-fonts.tar.gz
if [ -f "${MAP_FONTS_BLOB}" ]; then
mkdir -p /opt/wrolpi/modules/map/static/fonts
tar -xzf "${MAP_FONTS_BLOB}" -C /opt/wrolpi/modules/map/static/
fi
# Start Caddy quickly so user can access Controller from React UI
systemctl start caddy || :
# WROLPi needs a few privileged commands.
cp /opt/wrolpi/etc/raspberrypios/90-wrolpi /etc/sudoers.d/90-wrolpi
chmod 0440 /etc/sudoers.d/90-wrolpi
# Verify this new file is valid.
visudo -c -f /etc/sudoers.d/90-wrolpi
# Update the Portable bootstrap on boxes installed from the Live USB; it is
# otherwise frozen at image-build time.
LIVE_INCLUDES=/opt/wrolpi/debian-live-config/config/includes.chroot
if [ -f /usr/local/sbin/wrolpi-bootstrap.sh ]; then
cp "${LIVE_INCLUDES}/usr/local/sbin/wrolpi-bootstrap.sh" /usr/local/sbin/wrolpi-bootstrap.sh
chmod 0755 /usr/local/sbin/wrolpi-bootstrap.sh
if [ -f /etc/systemd/system/wrolpi-bootstrap.service ]; then
cp "${LIVE_INCLUDES}/etc/systemd/system/wrolpi-bootstrap.service" /etc/systemd/system/wrolpi-bootstrap.service
fi
fi
# Install the systemd services
cp /opt/wrolpi/etc/raspberrypios/wrolpi*.service /etc/systemd/system/
cp /opt/wrolpi/etc/raspberrypios/wrolpi-*.timer /etc/systemd/system/
cp /opt/wrolpi/etc/raspberrypios/wrolpi.target /etc/systemd/system/
systemctl enable wrolpi-controller.service
systemctl enable wrolpi-api.service
systemctl enable wrolpi-app.service
systemctl enable wrolpi-kiwix.service
systemctl enable wrolpi-help.service
systemctl enable wrolpi-mounts.service
systemctl enable wrolpi-cert-renew.timer
# Repair Controller (offline-safe - no pip install)
repair_controller() {
echo "Checking WROLPi Controller..."
# Check venv exists
if [ ! -f /opt/wrolpi/controller/venv/bin/python ]; then
echo "ERROR: Controller venv missing!"
echo "Run install.sh or upgrade.sh (requires internet) to reinstall."
return 1
fi
# Check venv is functional
if ! /opt/wrolpi/controller/venv/bin/python --version > /dev/null 2>&1; then
echo "ERROR: Controller venv is corrupted!"
echo "Run install.sh or upgrade.sh (requires internet) to reinstall."
return 1
fi
# Check systemd service is installed
if [ ! -f /etc/systemd/system/wrolpi-controller.service ]; then
echo "Controller systemd service not installed, copying..."
cp /opt/wrolpi/etc/raspberrypios/wrolpi-controller.service /etc/systemd/system/
systemctl daemon-reload
fi
# Ensure service is enabled
if ! systemctl is-enabled wrolpi-controller > /dev/null 2>&1; then
echo "Enabling Controller service..."
systemctl enable wrolpi-controller
fi
echo "Controller repair completed"
}
# Run Controller repair if venv exists (skip on fresh install)
if [ -d /opt/wrolpi/controller/venv ]; then
repair_controller || echo "Controller repair failed, continuing..."
fi
# Start Controller so user can monitor repair in UI.
systemctl start wrolpi-controller
/usr/bin/systemctl daemon-reload
# WROLPi needs a few privileged commands.
cp /opt/wrolpi/etc/raspberrypios/90-wrolpi /etc/sudoers.d/90-wrolpi
chmod 0440 /etc/sudoers.d/90-wrolpi
# Verify this new file is valid.
visudo -c -f /etc/sudoers.d/90-wrolpi
# WROLPi uses SQLite inside the media directory; the API creates/migrates the database at
# startup. Never fatal: the media drive may not be mounted yet.
(cd /opt/wrolpi && /opt/wrolpi/venv/bin/python3 /opt/wrolpi/main.py db upgrade) || \
echo "WARNING: db upgrade failed (media directory unavailable?); the API will retry at startup."
# Create the media directory. This should be mounted by the maintainer.
[ -d /media/wrolpi ] || mkdir /media/wrolpi
# Create config directory if external drive is mounted, and is empty.
if grep -qs /media/wrolpi /proc/mounts && [ -z "$(ls -A /media/wrolpi)" ] && [ ! -d /media/wrolpi/config ]; then
mkdir /media/wrolpi/config
fi
# Build the frontend app -- only if build/ is missing or the app source changed.
# The shipped image already contains a current build/, so on a healthy install
# (including a weak Pi) this is a no-op; it only rebuilds after an upgrade that
# changed app/, or if build/ went missing. Stays offline: it never npm-installs.
/opt/wrolpi/scripts/build_app.sh
# Change owner of the media directory, ignore any errors because the drive may be fat/exfat/etc.
chown wrolpi:wrolpi /media/wrolpi 2>/dev/null || echo "Ignoring failure to change media directory permissions."
# Ensure the config directory is owned by wrolpi so the API can write config files.
[ -d /media/wrolpi/config ] && chown -R wrolpi:wrolpi /media/wrolpi/config 2>/dev/null || :
# On Portable (live-boot) systems the Postgres cluster lives under config/postgresql,
# bind-mounted over /var/lib/postgresql — so the config chown above just swept it. Restore
# ownership only when that bind is active; on installed systems Postgres lives on the root
# filesystem and any config/postgresql directory is inert (dead weight or a user's backup).
if mountpoint -q /var/lib/postgresql 2>/dev/null && id postgres >/dev/null 2>&1; then
chown -R postgres:postgres /var/lib/postgresql /etc/postgresql 2>/dev/null || :
fi
# Remove immutable flag from blob files before chown (may not exist or may not be set).
chattr -i /opt/wrolpi-blobs/* 2>/dev/null || :
chown -R wrolpi:wrolpi /home/wrolpi /opt/wrolpi*
# Protect blob files from accidental modification or deletion.
# These files are critical for initializing WROLPi.
if [ -d /opt/wrolpi-blobs ] && ls /opt/wrolpi-blobs/* >/dev/null 2>&1; then
chown -R root:root /opt/wrolpi-blobs
chmod 444 /opt/wrolpi-blobs/*
chattr +i /opt/wrolpi-blobs/*
fi
# Copy MOTD once the repair has been successful.
cp /opt/wrolpi/etc/raspberrypios/motd/30-wrolpi.motd /etc/update-motd.d/30-wrolpi
chmod +x /etc/update-motd.d/*
# Copy desktop shortcuts to existing users (new users get them from /etc/skel).
if [[ ${rpi} == true ]]; then
for user_home in /home/wrolpi /home/pi; do
if [ -d "${user_home}/Desktop" ]; then
cp /opt/wrolpi/etc/raspberrypios/*.desktop "${user_home}/Desktop/"
chown -R "$(basename ${user_home})":"$(basename ${user_home})" "${user_home}/Desktop/"
fi
done
# Update skeleton for future users.
mkdir -p /etc/skel/Desktop
cp /opt/wrolpi/etc/raspberrypios/*.desktop /etc/skel/Desktop/
fi
systemctl restart wrolpi-help
systemctl start wrolpi.target
set +x
echo "Repair has completed. You may run /opt/wrolpi/help.sh to check system status."