Files
shawn 39bac0b0cd Build: DietPi OS minimization
- dietpi-postinstall.sh: first-boot setup from DietPi minimal with only
  needed packages (Python, JACK, cmake, GPIO libs, I2C tools)
- minimize.sh: removes unused packages, systemd services, and kernel
  modules with --dry-run and --aggressive modes; saves before/after profiles
- build-minimal-image.sh: produces flashable SD card image from scratch
  (download DietPi + provision) or from a running SD card device
- baseline-footprint.md: documents before/after RAM, disk, boot time
  targets and verification checklist

Closes t_38b86d31 — DietPi OS minimization
2026-06-08 00:12:40 -04:00

292 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────────────────────
# Pi Multi-FX Pedal — Minimal SD Card Image Builder
#
# Takes a stripped DietPi installation (after dietpi-postinstall.sh + minimize.sh)
# and produces a minimal, flashable SD card image (compressed .img.gz).
#
# This script runs on the build host (x86 or RPi) and produces an image that
# can be written directly to an SD card for deployment.
#
# Usage:
# sudo ./build-minimal-image.sh [--source-device /dev/sdX] [--output ./pedal-image.img.gz]
# sudo ./build-minimal-image.sh --from-scratch # Download DietPi + build from zero
#
# Options:
# --source-device DEV Build image FROM a running SD card (e.g. /dev/mmcblk0)
# --from-scratch Download a fresh DietPi image and provision it
# --output PATH Output image path (default: ./pi-multifx-pedal-<date>.img.gz)
# --no-compress Skip gzip compression (saves time for testing)
# --help Show this help
#
# Dependencies:
# - pishrink (auto-installed if missing)
# - rsync, fdisk, parted, losetup (standard Linux tools)
# - wget, unzip (for --from-scratch)
#
# Target: < 500 MB compressed image, < 60 MB idle RAM on boot
# ──────────────────────────────────────────────────────────────────────────────
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
OUTPUT_DIR="$(pwd)"
COMPRESS=true
SOURCE_DEVICE=""
FROM_SCRATCH=false
# ── Colour helpers ───────────────────────────────────────────────────────────
info() { printf "\e[34m[INFO] %s\e[0m\n" "$*"; }
ok() { printf "\e[32m[ OK ] %s\e[0m\n" "$*"; }
warn() { printf "\e[33m[WARN] %s\e[0m\n" "$*"; }
err() { printf "\e[31m[FAIL] %s\e[0m\n" "$*"; }
# ── Parse args ───────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--source-device) SOURCE_DEVICE="$2"; shift 2 ;;
--output) OUTPUT_DIR="$(dirname "$2")"; shift 2 ;;
--from-scratch) FROM_SCRATCH=true; shift ;;
--no-compress) COMPRESS=false; shift ;;
--help) grep "^#" "$0" | grep -v "^#!/" | sed 's/^# \?//'; exit 0 ;;
*) err "Unknown option: $1"; exit 1 ;;
esac
done
if [[ $EUID -ne 0 ]]; then
err "This script must be run as root (sudo ./build-minimal-image.sh)"
exit 1
fi
# ── Determine output filename ────────────────────────────────────────────────
DATE_TAG=$(date +%Y%m%d)
OUTPUT_IMG="${OUTPUT_DIR}/pi-multifx-pedal-${DATE_TAG}.img"
OUTPUT_FINAL="${OUTPUT_IMG}${COMPRESS:+".gz"}"
info "========== Pi Multi-FX Pedal — Image Builder =========="
echo ""
# ══════════════════════════════════════════════════════════════════════════════
# Mode 1: Build from scratch (download DietPi + provision)
# ══════════════════════════════════════════════════════════════════════════════
if [[ "$FROM_SCRATCH" == true ]]; then
info "Mode: from-scratch — downloading DietPi image..."
WORKDIR="$(mktemp -d -t pedal-image-XXXXXX)"
trap 'rm -rf "$WORKDIR"' EXIT
# ── Download latest DietPi for RPi 4 ──────────────────────────────
DIETPI_URL="https://dietpi.com/downloads/images/DietPi_RPi-ARMv8-Bookworm.img.xz"
DIETPI_IMG="${WORKDIR}/DietPi_RPi-ARMv8-Bookworm.img"
MOUNT_DIR="${WORKDIR}/mnt"
BOOT_DIR="${WORKDIR}/boot"
if [[ ! -f "$DIETPI_IMG" ]]; then
info "Downloading DietPi for RPi 4B (ARMv8 Bookworm)..."
wget -q --show-progress "$DIETPI_URL" -O "${DIETPI_IMG}.xz"
info "Extracting..."
unxz -f "${DIETPI_IMG}.xz"
ok "DietPi image ready: $(ls -lh "$DIETPI_IMG" | awk '{print $5}')"
fi
# ── Expand the image to 4 GB (room for Python deps + NAM models) ──
info "Expanding image to 4 GB..."
ORIG_SIZE=$(stat -c%s "$DIETPI_IMG" 2>/dev/null || stat -f%z "$DIETPI_IMG" 2>/dev/null)
dd if=/dev/zero bs=1M count=$((4096 - ORIG_SIZE / 1048576)) >> "$DIETPI_IMG" 2>/dev/null
# Fix partition table — expand last partition to fill new space
PART_START=$(fdisk -l "$DIETPI_IMG" 2>/dev/null | grep "^${DIETPI_IMG}" | tail -1 | awk '{print $2}')
if [[ -z "$PART_START" ]]; then
# Try parsed differently
PART_INFO=$(fdisk -l "$DIETPI_IMG" 2>/dev/null | tail -2 | head -1)
PART_START=$(echo "$PART_INFO" | awk '{print $2}')
fi
if [[ -n "$PART_START" ]]; then
# fdisk: delete last partition, recreate with same start, larger end
printf "d\nn\np\n\n${PART_START}\n\nw\n" | fdisk "$DIETPI_IMG" > /dev/null 2>&1 || true
ok "Partition table expanded"
fi
# ── Mount image ──────────────────────────────────────────────────────
mkdir -p "$MOUNT_DIR" "$BOOT_DIR"
LOOP_DEV=$(losetup --find --show --partscan "$DIETPI_IMG")
ok "Loop device: $LOOP_DEV"
# Wait for partitions to appear
sleep 2
# Mount root partition (usually p2 for DietPi) and boot (p1)
ROOT_PART="${LOOP_DEV}p2"
BOOT_PART="${LOOP_DEV}p1"
if [[ ! -b "$ROOT_PART" ]]; then
ROOT_PART="${LOOP_DEV}p2"
BOOT_PART="${LOOP_DEV}p1"
fi
mount "$ROOT_PART" "$MOUNT_DIR"
mount "$BOOT_PART" "$BOOT_DIR"
ok "Image mounted at $MOUNT_DIR"
# ── Copy project scripts into the image ────────────────────────────
info "Staging project files..."
mkdir -p "$MOUNT_DIR/opt/pi-multifx-pedal/scripts" \
"$MOUNT_DIR/opt/pi-multifx-pedal/src" \
"$MOUNT_DIR/opt/pi-multifx-pedal/docs" \
"$MOUNT_DIR/opt/pi-multifx-pedal/presets"
# Use a bind mount so we access the project root cleanly
# (copying via rsync preserves scripts for chroot execution)
rsync -a --delete \
--exclude='.git' \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='.venv' \
--exclude='venv' \
--exclude='hardware/gerber' \
"$PROJECT_DIR/" "$MOUNT_DIR/opt/pi-multifx-pedal/"
ok "Project files staged"
# ── Chroot and run postinstall ────────────────────────────────────
info "Running postinstall inside chroot..."
# Bind mount required filesystems for chroot
mount --bind /dev "$MOUNT_DIR/dev"
mount --bind /proc "$MOUNT_DIR/proc"
mount --bind /sys "$MOUNT_DIR/sys"
# Copy DNS config for apt inside chroot
cp /etc/resolv.conf "$MOUNT_DIR/etc/resolv.conf" 2>/dev/null || true
# Run postinstall
chroot "$MOUNT_DIR" /bin/bash -c "
cd /opt/pi-multifx-pedal
bash scripts/dietpi-postinstall.sh --unattended
" || warn "Postinstall exited with non-zero (some steps may need reboot inside chroot)"
# Run minimize
chroot "$MOUNT_DIR" /bin/bash -c "
cd /opt/pi-multifx-pedal
bash scripts/minimize.sh --aggressive
" || true
# ── Cleanup chroot ────────────────────────────────────────────────
umount "$MOUNT_DIR/dev" 2>/dev/null || true
umount "$MOUNT_DIR/proc" 2>/dev/null || true
umount "$MOUNT_DIR/sys" 2>/dev/null || true
rm -f "$MOUNT_DIR/etc/resolv.conf"
# ── Clean apt caches inside image ─────────────────────────────────
chroot "$MOUNT_DIR" apt-get clean -qq 2>/dev/null || true
rm -rf "$MOUNT_DIR/var/cache/apt/archives/"* 2>/dev/null || true
rm -rf "$MOUNT_DIR/var/log/"*.log 2>/dev/null || true
rm -rf "$MOUNT_DIR/tmp/"* 2>/dev/null || true
# ── Sync and unmount ──────────────────────────────────────────────
sync
umount "$MOUNT_DIR" 2>/dev/null || true
umount "$BOOT_DIR" 2>/dev/null || true
losetup -d "$LOOP_DEV" 2>/dev/null || true
info "Image built from scratch: $DIETPI_IMG"
cp "$DIETPI_IMG" "$OUTPUT_IMG"
ok "Copied to $OUTPUT_IMG"
# ══════════════════════════════════════════════════════════════════════════════
# Mode 2: Build from a running SD card device (e.g. /dev/mmcblk0)
# ══════════════════════════════════════════════════════════════════════════════
elif [[ -n "$SOURCE_DEVICE" ]]; then
info "Mode: from-device — imaging $SOURCE_DEVICE..."
if [[ ! -b "$SOURCE_DEVICE" ]]; then
err "Not a block device: $SOURCE_DEVICE"
exit 1
fi
# ── Verify we're not imaging our own boot device ───────────────────
ROOT_DEV=$(findmnt -n -o SOURCE / 2>/dev/null | sed 's/[0-9]*$//' || true)
if [[ "$SOURCE_DEVICE" == "$ROOT_DEV" ]]; then
err "Refusing to image the device we're booted from ($SOURCE_DEVICE)"
err "Boot from a different SD card or use --from-scratch"
exit 1
fi
info "Reading $SOURCE_DEVICE..."
dd if="$SOURCE_DEVICE" of="$OUTPUT_IMG" bs=4M status=progress
sync
ok "Raw image created: $OUTPUT_IMG"
# ── Run pishrink to shrink the image ───────────────────────────────
info "Shrinking image with PiShrink..."
if ! command -v pishrink &>/dev/null; then
info "Installing PiShrink..."
wget -q https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh -O /tmp/pishrink.sh
chmod +x /tmp/pishrink.sh
PISHRINK="/tmp/pishrink.sh"
else
PISHRINK="pishrink"
fi
$PISHRINK -v "$OUTPUT_IMG" -p 2>/dev/null || {
warn "PiShrink encountered issues, trying without verbose..."
$PISHRINK "$OUTPUT_IMG" || warn "PiShrink failed — image is raw (unshrunk)"
}
ok "Image shrunk"
# ══════════════════════════════════════════════════════════════════════════════
# Mode 3: No source specified — build from current system
# ══════════════════════════════════════════════════════════════════════════════
else
warn "No --source-device or --from-scratch specified."
warn ""
warn "If you want to build from your CURRENT running system:"
warn " sudo ./build-minimal-image.sh --source-device /dev/mmcblk0"
warn ""
warn "If you want to build from a fresh DietPi download:"
warn " sudo ./build-minimal-image.sh --from-scratch"
echo ""
err "Choose one of the above."
exit 1
fi
# ══════════════════════════════════════════════════════════════════════════════
# Final compression
# ══════════════════════════════════════════════════════════════════════════════
if [[ "$COMPRESS" == true ]] && [[ -f "$OUTPUT_IMG" ]]; then
info "Compressing image (gzip -9)..."
gzip -9 -f "$OUTPUT_IMG"
FINAL_FILE="${OUTPUT_IMG}.gz"
ok "Compressed: $FINAL_FILE"
elif [[ -f "$OUTPUT_IMG" ]]; then
FINAL_FILE="$OUTPUT_IMG"
fi
# ── Summary ──────────────────────────────────────────────────────────────────
echo ""
info "========== Build Complete =========="
echo ""
echo " Output: $FINAL_FILE"
if [[ -f "$FINAL_FILE" ]]; then
SIZE=$(stat -c%s "$FINAL_FILE" 2>/dev/null || stat -f%z "$FINAL_FILE" 2>/dev/null)
echo " Size: $(numfmt --to=iec-i "$SIZE") ($(numfmt --to=si "$SIZE"))"
fi
echo ""
echo " Write to SD card:"
echo " gunzip -c $FINAL_FILE | sudo dd of=/dev/mmcblk0 bs=4M status=progress"
echo " sync"
echo ""
echo " First-boot instructions:"
echo " 1. Insert SD card, power on"
echo " 2. The pedal service starts automatically (multi-fx-pedal.target)"
echo " 3. Check status: systemctl status pi-multifx-pedal.service"
echo " 4. Verify JACK: jack_wait -c"
echo ""