#!/bin/bash # https://github.com/RPI-Distro/pi-gen 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-pi-gen}" Help() { echo "Build WROLPi Raspberry Pi OS 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. VERSION=$(curl -sL "https://raw.githubusercontent.com/lrnselfreliance/wrolpi/${BRANCH}/wrolpi/version.txt") 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 CHROOTFS="${BUILD_DIR}/work/WROLPi/stage0/rootfs" cleanup() { [ -f /var/tmp/arm64.zip ] && rm /var/tmp/arm64.zip # Only unmount if it is actually a mountpoint for mp in \ "${CHROOTFS}/dev/pts" \ "${CHROOTFS}/sys" \ "${CHROOTFS}/proc" \ "${CHROOTFS}/run" \ "${CHROOTFS}/tmp" \ "${CHROOTFS}/dev"; do if mountpoint -q "${mp}" 2>/dev/null; then umount "${mp}" 2>/dev/null || umount -l "${mp}" 2>/dev/null fi done # Remove immutable attribute from protected files before cleanup chattr -i "${BUILD_DIR}"/work/*/stage*/rootfs/opt/wrolpi-blobs/* 2>/dev/null || : # Final safety net – remove the whole build tree rm -rf "${BUILD_DIR}" } trap cleanup EXIT set -e set -x # Cache directory for downloaded blobs (persists across builds). BLOB_CACHE="${SCRIPT_DIR}/stage2/04-wrolpi/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) # Clear leftovers from a previous build before unpacking. The EXIT trap # normally cleans up, but a SIGTERM/kill (e.g. `systemctl stop`) skips it and # leaves BUILD_DIR behind -- the `mv` below would then nest the new tree inside # the stale one and break the build. Strip the immutable flag off blob files # first (the chroot sets chattr +i), or rm -rf cannot remove them. chattr -i "${BUILD_DIR}"/work/*/stage*/rootfs/opt/wrolpi-blobs/* 2>/dev/null || : rm -rf "${BUILD_DIR}" /var/tmp/pi-gen-arm64 /var/tmp/arm64.zip # Get the latest pi-gen code. The `arm64` branch builds trixie (Debian 13). wget https://github.com/RPi-Distro/pi-gen/archive/refs/heads/arm64.zip -O /var/tmp/arm64.zip unzip /var/tmp/arm64.zip -d /var/tmp mv /var/tmp/pi-gen-arm64 "${BUILD_DIR}" # Copy the configuration and build files into the pi-gen directory. cp "${SCRIPT_DIR}/config.txt" "${BUILD_DIR}/config.txt" echo "export WROLPI_BRANCH=${BRANCH}" >> "${BUILD_DIR}/config.txt" rsync -a "${SCRIPT_DIR}"/stage2/* "${BUILD_DIR}/stage2/" # We only need to build the Lite and Desktop images. rm -f "${BUILD_DIR}"/stage*/EXPORT* #echo 'IMG_SUFFIX="-lite"' > "${BUILD_DIR}/stage2/EXPORT_IMAGE" echo 'IMG_SUFFIX="-desktop"' > "${BUILD_DIR}/stage5/EXPORT_IMAGE" # Build the images. (cd "${BUILD_DIR}" && time nice -n 18 "${BUILD_DIR}"/build.sh -c "${BUILD_DIR}"/config.txt | \ tee "${SCRIPT_DIR}"/build.log) grep "00-run-chroot.sh completed" "${SCRIPT_DIR}/build.log" >/dev/null 2>&1 || (echo "script 0 failed!" && exit 1) grep "03-run-chroot.sh completed" "${SCRIPT_DIR}/build.log" >/dev/null 2>&1 || (echo "script 3 failed!" && exit 1) grep "03-run-chroot.sh completed" "${SCRIPT_DIR}/build.log" >/dev/null 2>&1 || (echo "script 3 failed!" && exit 1) grep "04-run-chroot.sh completed" "${SCRIPT_DIR}/build.log" >/dev/null 2>&1 || (echo "script 4 failed!" && exit 1) # Move the built images out of the build directory. #mv "${BUILD_DIR}"/deploy/*lite*xz "${SCRIPT_DIR}"/WROLPi-v"${VERSION}"-aarch64-lite.img.xz mv "${BUILD_DIR}"/deploy/*desktop*xz "${SCRIPT_DIR}"/WROLPi-v"${VERSION}"-aarch64-desktop.img.xz chmod 644 "${SCRIPT_DIR}"/*xz chown -R 1000:1000 "${SCRIPT_DIR}" set +x # Sanity-check the resulting image before declaring success, so a truncated or # corrupt build never ships. IMG="${SCRIPT_DIR}/WROLPi-v${VERSION}-aarch64-desktop.img.xz" MIN_IMG_BYTES=$((1024 * 1024 * 1024)) # 1 GiB floor; compressed is ~5 GiB. MIN_UNCOMPRESSED_BYTES=$((2 * 1024 * 1024 * 1024)) # 2 GiB floor; real is ~8 GiB. IMG_BYTES=$(stat -c%s "${IMG}") if [ "${IMG_BYTES}" -lt "${MIN_IMG_BYTES}" ]; then echo "ERROR: image is only ${IMG_BYTES} bytes (< ${MIN_IMG_BYTES}); build likely truncated." >&2 exit 1 fi # Full decompression test: catches any corruption in the .xz stream. if ! xz -t "${IMG}"; then echo "ERROR: ${IMG} failed xz integrity check." >&2 exit 1 fi # Confirm the uncompressed image is a sane size (field 5 of the robot totals). UNCOMPRESSED_BYTES=$(xz --robot -l "${IMG}" 2>/dev/null | awk -F'\t' '/^totals/{print $5}') if [ -n "${UNCOMPRESSED_BYTES}" ] && [ "${UNCOMPRESSED_BYTES}" -lt "${MIN_UNCOMPRESSED_BYTES}" ]; then echo "ERROR: uncompressed image is only ${UNCOMPRESSED_BYTES} bytes (< ${MIN_UNCOMPRESSED_BYTES})." >&2 exit 1 fi echo "Image sanity checks passed ($(numfmt --to=iec "${IMG_BYTES}" 2>/dev/null || echo "${IMG_BYTES} bytes") compressed, xz OK)." echo "Build completed successfully"