wrolpi/debian-live-config/build.sh
Roland e01795ad7e
Debian 13 (trixie) rebase: docker, debian-live-config, and pi-gen builds.
All three build targets move from Debian 12/bookworm to Debian 13/trixie
for fresh installs; existing Debian 12 systems remain supported in place.

Docker: api/help on python:3.13-trixie, controller on 3.13-slim-trixie,
app on node:20-trixie (matches trixie's nodejs 20), samba/zim on
debian:13.  Full backend suite passes on Python 3.13 in the api container.

debian-live-config: --distribution trixie; postgres teardown for the
SQLite cutover (pg_dropcluster hook block removed, wrolpi-bootstrap.sh no
longer creates a cluster or calls the deleted initialize_api_db.sh — the
API creates wrolpi.db itself on first start).  README documents the
SQLite layout and drops Etcher recommendations.

pi-gen: upstream has no trixie branch — the arm64 branch now defaults to
RELEASE=trixie; KEYBOARD_KEYMAP=en_US is rejected by trixie's
console-setup (ckbcomp validates), use 'us'; stop stamping a Postgres
.pgpass into /etc/skel.

Package lists (both targets): postgresql-15/postgresql-doc/libpq-dev
deleted; libtiff5-dev renamed to libtiff-dev; software-properties-common
and cpufrequtils removed from trixie (Controller CPU throttle falls back
to sysfs, dead cpufreq-set sudoers rule removed); locales and gnupg added
(no longer arrive transitively under --apt-recommends false); sqlite3 CLI
added for reset_tags.sh.

help.sh: replace the Postgres service/port/socket/cluster/psql checks
(all false FAILEDs after the SQLite cutover) with SQLite checks:
wrolpi.db exists, PRAGMA quick_check, file_group table + row count, run
as the wrolpi user so root never creates the -shm sidecar.

Both build scripts take a WROLPI_BUILD_DIR override so development builds
can run beside the release pipeline's build trees; the sudo re-exec
passes it through explicitly (a plain sudo would drop it).

test_common.py: the truncate tests hard-coded sys.getsizeof results from
the 3.11 object layout (str/list shrank in 3.12); assert the contract
(fits budget, prefix, monotonic in budget) so they pass on 3.11 and 3.13.

Verified: trixie ISO (3.7G) and pi-gen desktop image (4.3G) both built
from scratch and passed their sanity checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:43:08 -06:00

186 lines
7.5 KiB
Bash
Executable file

#!/bin/bash
# Build the WROLPi Debian Live + installer ISO.
#
# Output: an iso-hybrid image that boots into a full WROLPi system on any
# x86 PC. Users can run WROLPi directly off the USB (persistence partition
# created on first boot) or install to disk via the Calamares launcher on
# the desktop. Calamares wiring lands in phase 2; phase 1 ships the
# live-only flow.
#
# https://live-team.pages.debian.net/live-manual/html/live-manual/index.en.html
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# Overridable so a development build can run beside the release pipeline's
# build tree without colliding with it.
BUILD_DIR="${WROLPI_BUILD_DIR:-/var/tmp/wrolpi-build-debian}"
Help() {
echo "Build WROLPi Debian Live ISO image."
echo
echo "Syntax: build.sh [-h] [-b BRANCH]"
echo "options:"
echo "h Print this help."
echo "b Build from this git BRANCH (default: 'release')."
echo
echo "Set WROLPI_BUILD_DIR to build somewhere other than the default"
echo "(${BUILD_DIR}), e.g. beside the release pipeline's tree."
echo
}
BRANCH="release"
while getopts ":hb:" option; do
case $option in
h) Help; exit ;;
b) BRANCH="${OPTARG}" ;;
*) echo "Error: Invalid option"; exit 1 ;;
esac
done
# Get version from the target git branch (not local checkout) so output
# filename matches actual content. Strip whitespace so stray newlines or
# spaces in version.txt do not corrupt the lb config metadata strings.
VERSION=$(curl -sL "https://raw.githubusercontent.com/lrnselfreliance/wrolpi/${BRANCH}/wrolpi/version.txt" | tr -d '[:space:]')
if [ -z "${VERSION}" ]; then
echo "ERROR: Could not fetch version from branch '${BRANCH}'"
exit 1
fi
echo "Building WROLPi version: ${VERSION} from branch: ${BRANCH}"
# Re-execute this script if it wasn't called with sudo. Pass BUILD_DIR
# through explicitly — a plain `sudo "$0"` drops WROLPI_BUILD_DIR from the
# environment and the build would silently land on the default path.
if [ $EUID != 0 ]; then
sudo WROLPI_BUILD_DIR="${BUILD_DIR}" "$0" "$@"
exit $?
fi
# Clear out old builds. Strip immutable flag from blob files first — the
# chroot hook applies chattr +i to /opt/wrolpi-blobs/*; without removing
# that here, rm -rf leaves them behind and the next lb build's
# chroot_includes_after_packages rsync fails with "Operation not permitted"
# trying to overwrite them.
if [ -d "${BUILD_DIR}" ]; then
find "${BUILD_DIR}" -path '*/opt/wrolpi-blobs/*' -type f -exec chattr -i {} + 2>/dev/null || true
rm -rf "${BUILD_DIR}"
fi
mkdir -p "${BUILD_DIR}"
cd "${BUILD_DIR}" || (echo "Work directory must exist" && exit 1)
set -e
# Live + Debian Installer. The installer is the stock Debian d-i with
# `--debian-installer live`, which copies the running squashfs onto the
# target disk (much faster than a network-based install) and presents the
# usual graphical/text installer flow. GRUB gets "Install" + "Graphical
# install" + "Live" entries. noswap prevents the live system from
# activating swap partitions found on the host machine's internal disks.
lb config \
--binary-images iso-hybrid \
--mode debian \
--architectures amd64 \
--linux-flavours amd64 \
--distribution trixie \
--debian-installer live \
--debian-installer-gui true \
--archive-areas "main contrib non-free non-free-firmware" \
--updates true \
--security true \
--apt-recommends false \
--firmware-binary true \
--firmware-chroot true \
--win32-loader false \
--checksums sha512 \
--clean \
--color \
--linux-packages "linux-image linux-headers" \
--memtest memtest86+ \
--bootappend-live "boot=live components quiet splash noswap persistence persistence-storage=filesystem persistence-media=removable persistence-label=persistence live-config.username=wrolpi live-config.user-fullname=WROLPi live-config.hostname=wrolpi" \
--iso-volume "WROLPi v${VERSION}" \
--iso-application "WROLPi v${VERSION}" \
--iso-preparer WROLPi \
--iso-publisher https://wrolpi.org
rsync -a "${SCRIPT_DIR}/config" "${BUILD_DIR}/"
# Write branch config for hook script.
mkdir -p "${BUILD_DIR}/config/includes.chroot/opt/wrolpi-blobs"
echo "${BRANCH}" > "${BUILD_DIR}/config/includes.chroot/opt/wrolpi-branch"
# Cache directory for downloaded blobs (persists across builds).
BLOB_CACHE="${SCRIPT_DIR}/files"
mkdir -p "${BLOB_CACHE}"
# Download map fonts (skip if cached).
if [ ! -f "${BLOB_CACHE}/map-fonts.tar.gz" ]; then
echo "Downloading map fonts..."
mkdir -p /tmp/map-fonts-dl
curl -fsSL https://github.com/protomaps/basemaps-assets/archive/refs/heads/main.tar.gz \
| tar -xz --strip-components=1 -C /tmp/map-fonts-dl basemaps-assets-main/fonts
tar -czf "${BLOB_CACHE}/map-fonts.tar.gz" -C /tmp/map-fonts-dl fonts
rm -rf /tmp/map-fonts-dl
fi
[[ -f "${BLOB_CACHE}/map-fonts.tar.gz" && -s "${BLOB_CACHE}/map-fonts.tar.gz" ]] || \
(echo "ERROR: Failed to download map fonts!" && exit 1)
# Download map overview (skip if cached).
if [ ! -f "${BLOB_CACHE}/map-overview.pmtiles" ]; then
echo "Downloading map overview..."
curl -fsSL https://wrolpi.nyc3.cdn.digitaloceanspaces.com/maps/map-overview.pmtiles \
-o "${BLOB_CACHE}/map-overview.pmtiles"
fi
[[ -f "${BLOB_CACHE}/map-overview.pmtiles" && -s "${BLOB_CACHE}/map-overview.pmtiles" ]] || \
(echo "ERROR: Failed to download map overview!" && exit 1)
# Copy cached blobs into the live image.
cp "${BLOB_CACHE}/map-fonts.tar.gz" "${BUILD_DIR}/config/includes.chroot/opt/wrolpi-blobs/"
cp "${BLOB_CACHE}/map-overview.pmtiles" "${BUILD_DIR}/config/includes.chroot/opt/wrolpi-blobs/"
# pipefail so an lb build failure isn't masked by tee's success exit code.
set -o pipefail
time nice -n 18 lb build 2>&1 | tee "${SCRIPT_DIR}/build.log"
set +o pipefail
grep "9999-wrolpi.hook.chroot completed" "${SCRIPT_DIR}/build.log" >/dev/null 2>&1 || (echo "build hook failed!" && exit 1)
# Copy the freshly built ISO directly to its final name. Take the single ISO
# out of BUILD_DIR (which is wiped each build, so it holds exactly one) rather
# than globbing SCRIPT_DIR -- a leftover ISO from a previous build would
# otherwise make a `*.iso` glob match multiple files and break the move.
DEST="${SCRIPT_DIR}/WROLPi-v${VERSION}-amd64.iso"
shopt -s nullglob
BUILT_ISOS=("${BUILD_DIR}"/*.iso)
shopt -u nullglob
if [ "${#BUILT_ISOS[@]}" -eq 0 ]; then
echo "Build failed. No ISOs were found!"
exit 1
fi
rm -f "${DEST}"
cp "${BUILT_ISOS[0]}" "${DEST}"
chmod 644 "${DEST}"
chown "${SUDO_UID:-1000}:${SUDO_GID:-1000}" "${DEST}"
# Sanity-check the resulting ISO before declaring success, so a truncated or
# corrupt build never ships.
MIN_ISO_BYTES=$((1024 * 1024 * 1024)) # 1 GiB floor; a real ISO is ~3 GiB.
ISO_BYTES=$(stat -c%s "${DEST}")
if [ "${ISO_BYTES}" -lt "${MIN_ISO_BYTES}" ]; then
echo "ERROR: ISO is only ${ISO_BYTES} bytes (< ${MIN_ISO_BYTES}); build likely truncated." >&2
exit 1
fi
# The ISO 9660 primary volume descriptor carries the "CD001" magic at byte
# offset 32769. It is present even in iso-hybrid images, where `file` reports
# the leading MBR instead.
if [ "$(dd if="${DEST}" bs=1 skip=32769 count=5 2>/dev/null)" != "CD001" ]; then
echo "ERROR: ${DEST} is not a valid ISO 9660 image (missing CD001 magic)." >&2
exit 1
fi
# Cross-check with blkid when available.
if command -v blkid >/dev/null 2>&1; then
ISO_FSTYPE=$(blkid -o value -s TYPE "${DEST}" 2>/dev/null || true)
if [ -n "${ISO_FSTYPE}" ] && [ "${ISO_FSTYPE}" != "iso9660" ]; then
echo "ERROR: blkid reports filesystem '${ISO_FSTYPE}', expected iso9660." >&2
exit 1
fi
fi
echo "ISO sanity checks passed ($(numfmt --to=iec "${ISO_BYTES}" 2>/dev/null || echo "${ISO_BYTES} bytes"), iso9660)."
echo "Build has completed. ISO output ${DEST}"