From b31f5dca7cc3fb306c3a4d2e2cd743a36f48dfd0 Mon Sep 17 00:00:00 2001 From: commander Date: Tue, 7 Apr 2026 15:44:53 -0400 Subject: [PATCH] feat: complete ISO build pipeline (375 lines, 6 hooks) --- scripts/build-unified.sh | 375 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 scripts/build-unified.sh diff --git a/scripts/build-unified.sh b/scripts/build-unified.sh new file mode 100644 index 0000000..475c65f --- /dev/null +++ b/scripts/build-unified.sh @@ -0,0 +1,375 @@ +#!/bin/bash +# ═══════════════════════════════════════════════════════════════ +# Alfred Linux v4.0 — Unified ISO Build Script +# Usage: ./build-unified.sh +# build-number: b1, b2, b3, b4, b5, b6, rc +# +# Each build is cumulative: +# b1: Branding (Plymouth, GRUB, wallpapers, neofetch, LightDM) +# b2: b1 + Alfred Browser (replace Firefox) +# b3: b2 + Alfred IDE (code-server + Commander extension) +# b4: b3 + Voice (Kokoro TTS + welcome greeting) +# b5: b4 + Meilisearch (local search engine) +# b6: b5 + Calamares (graphical installer) +# rc: b6 (release candidate — same as b6, final test) +# ═══════════════════════════════════════════════════════════════ + +set -uo pipefail + +BUILD_NUM="${1:-}" +ENABLE_UEFI=false +if [[ "${2:-}" == "--uefi" ]]; then + ENABLE_UEFI=true +fi +if [[ -z "$BUILD_NUM" ]]; then + echo "Usage: $0 " + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +BUILD_DIR="$PROJECT_DIR/build" +OUTPUT_DIR="$PROJECT_DIR/iso-output" +DATE=$(date +%Y%m%d) +VERSION="4.0-${BUILD_NUM}" +ISO_NAME="alfred-linux-${VERSION}-amd64-${DATE}.iso" + +echo "" +echo " ╔═══════════════════════════════════════════════════╗" +echo " ║ Alfred Linux — ISO Build System v2.0 ║" +echo " ║ Build: ${VERSION} ║" +echo " ║ ISO: ${ISO_NAME} ║" +echo " ╚═══════════════════════════════════════════════════╝" +echo "" + +if [[ $EUID -ne 0 ]]; then + echo "ERROR: Must run as root (sudo)." + exit 1 +fi + +for cmd in lb debootstrap mksquashfs xorriso; do + if ! command -v "$cmd" &>/dev/null; then + echo "ERROR: '$cmd' not found." + exit 1 + fi +done + +# ── Determine which hooks and packages to include ── +declare -a HOOKS_TO_INCLUDE=() +PKG_LIST="alfred.list.chroot" # default (includes firefox-esr) + +case $BUILD_NUM in + b1) + HOOKS_TO_INCLUDE=("0100-alfred-customize.hook.chroot") + PKG_LIST="alfred.list.chroot" + ;; + b2) + HOOKS_TO_INCLUDE=("0100-alfred-customize.hook.chroot" "0200-alfred-browser.hook.chroot") + PKG_LIST="alfred-b2.list.chroot" + ;; + b3) + HOOKS_TO_INCLUDE=("0100-alfred-customize.hook.chroot" "0200-alfred-browser.hook.chroot" "0300-alfred-ide.hook.chroot") + PKG_LIST="alfred-b2.list.chroot" + ;; + b4) + HOOKS_TO_INCLUDE=("0100-alfred-customize.hook.chroot" "0200-alfred-browser.hook.chroot" "0300-alfred-ide.hook.chroot" "0400-alfred-voice.hook.chroot") + PKG_LIST="alfred-b2.list.chroot" + ;; + b5) + HOOKS_TO_INCLUDE=("0100-alfred-customize.hook.chroot" "0200-alfred-browser.hook.chroot" "0300-alfred-ide.hook.chroot" "0400-alfred-voice.hook.chroot" "0500-alfred-search.hook.chroot") + PKG_LIST="alfred-b2.list.chroot" + ;; + b6|rc|rc2|rc3) + HOOKS_TO_INCLUDE=("0100-alfred-customize.hook.chroot" "0200-alfred-browser.hook.chroot" "0300-alfred-ide.hook.chroot" "0400-alfred-voice.hook.chroot" "0500-alfred-search.hook.chroot" "0600-alfred-installer.hook.chroot") + PKG_LIST="alfred-b2.list.chroot" + ;; + rc4|rc5) + HOOKS_TO_INCLUDE=("0100-alfred-customize.hook.chroot" "0200-alfred-browser.hook.chroot" "0300-alfred-ide.hook.chroot" "0400-alfred-voice.hook.chroot" "0500-alfred-search.hook.chroot" "0600-alfred-installer.hook.chroot" "0700-alfred-welcome.hook.chroot" "0710-alfred-update.hook.chroot" "0800-alfred-store.hook.chroot" "0900-alfred-voice-v2.hook.chroot") + PKG_LIST="alfred-b2.list.chroot" + ;; + rc6) + HOOKS_TO_INCLUDE=("0100-alfred-customize.hook.chroot" "0150-alfred-hardware.hook.chroot" "0200-alfred-browser.hook.chroot" "0300-alfred-ide.hook.chroot" "0400-alfred-voice.hook.chroot" "0500-alfred-search.hook.chroot" "0600-alfred-installer.hook.chroot" "0700-alfred-welcome.hook.chroot" "0710-alfred-update.hook.chroot" "0800-alfred-store.hook.chroot" "0900-alfred-voice-v2.hook.chroot") + PKG_LIST="alfred-b2.list.chroot" + ;; + rc7) + HOOKS_TO_INCLUDE=("0100-alfred-customize.hook.chroot" "0150-alfred-hardware.hook.chroot" "0160-alfred-security.hook.chroot" "0200-alfred-browser.hook.chroot" "0300-alfred-ide.hook.chroot" "0400-alfred-voice.hook.chroot" "0500-alfred-search.hook.chroot" "0600-alfred-installer.hook.chroot" "0700-alfred-welcome.hook.chroot" "0710-alfred-update.hook.chroot" "0800-alfred-store.hook.chroot" "0900-alfred-voice-v2.hook.chroot") + PKG_LIST="alfred-b2.list.chroot" + ;; + rc8|rc9|rc10) + HOOKS_TO_INCLUDE=("0100-alfred-customize.hook.chroot" "0150-alfred-hardware.hook.chroot" "0160-alfred-security.hook.chroot" "0165-alfred-network-hardening.hook.chroot" "0170-alfred-fde.hook.chroot" "0200-alfred-browser.hook.chroot" "0300-alfred-ide.hook.chroot" "0400-alfred-voice.hook.chroot" "0500-alfred-search.hook.chroot" "0600-alfred-installer.hook.chroot" "0700-alfred-welcome.hook.chroot" "0710-alfred-update.hook.chroot" "0800-alfred-store.hook.chroot" "0900-alfred-voice-v2.hook.chroot") + PKG_LIST="alfred-b2.list.chroot" + ;; + *) + echo "ERROR: Unknown build number: $BUILD_NUM" + echo "Valid: b1, b2, b3, b4, b5, b6, rc, rc6, rc7, rc8" + exit 1 + ;; +esac + +echo "[BUILD] Hooks: ${HOOKS_TO_INCLUDE[*]}" +echo "[BUILD] Package list: $PKG_LIST" + +# ── Setup ── +echo "[BUILD] Setting up build directory..." +if [[ -d "$BUILD_DIR" ]]; then + echo "[BUILD] Removing previous build directory..." + rm -rf "$BUILD_DIR" +fi +mkdir -p "$BUILD_DIR" +cd "$BUILD_DIR" + +echo "[BUILD] Configuring live-build for Debian Trixie..." +# NOTE: --security false because live-build v3 (Ubuntu) hardcodes the old +# "/updates" URL format which is wrong for Bookworm (needs "trixie-security"). +# We add the correct security repo in our 0010 hook instead. +lb config \ + --mode debian \ + --distribution trixie \ + --architectures amd64 \ + --binary-images iso-hybrid \ + --bootloader syslinux \ + --apt-indices false \ + --memtest none \ + --firmware-chroot false \ + --security false \ + --initsystem systemd \ + --linux-packages "linux-image" \ + --linux-flavours "amd64" \ + --parent-mirror-bootstrap "http://deb.debian.org/debian" \ + --parent-mirror-chroot "http://deb.debian.org/debian" \ + --parent-mirror-binary "http://deb.debian.org/debian" \ + --mirror-bootstrap "http://deb.debian.org/debian" \ + --mirror-chroot "http://deb.debian.org/debian" \ + --mirror-binary "http://deb.debian.org/debian" \ + --iso-application "Alfred Linux" \ + --iso-publisher "GoSiteMe" \ + --iso-volume "Alfred Linux 3.0" + +# Copy selected package list +echo "[BUILD] Installing package list ($PKG_LIST)..." +cp "$PROJECT_DIR/config/package-lists/$PKG_LIST" config/package-lists/alfred.list.chroot + +# Copy selected hooks +# NOTE: live-build v3 (Ubuntu) uses config/hooks/*.chroot, NOT config/hooks/live/ +echo "[BUILD] Installing build hooks..." +mkdir -p config/hooks +for hook in "${HOOKS_TO_INCLUDE[@]}"; do + if [[ -f "$PROJECT_DIR/config/hooks/live/$hook" ]]; then + cp "$PROJECT_DIR/config/hooks/live/$hook" config/hooks/ + echo " ✓ $hook" + else + echo " ✗ MISSING: $hook" + fi +done +chmod +x config/hooks/*.hook.chroot 2>/dev/null || true + +# Security repo fix hook (always included) +cat > config/hooks/0010-fix-security-repo.hook.chroot << 'HOOKEOF' +#!/bin/bash +# Fix Debian Trixie security repo — live-build v3 generates the wrong URL format +# It creates "trixie/updates" but Trixie uses "trixie-security" +apt-get install -y --no-install-recommends gnupg 2>/dev/null || true + +# Remove any broken security sources that use the old format +sed -i '/bookworm\/updates/d' /etc/apt/sources.list 2>/dev/null || true +rm -f /etc/apt/sources.list.d/security.list 2>/dev/null + +# Write correct security and updates sources +echo "deb http://deb.debian.org/debian-security trixie-security main" > /etc/apt/sources.list.d/security.list +echo "deb http://deb.debian.org/debian trixie-updates main" > /etc/apt/sources.list.d/updates.list +apt-get update -qq 2>/dev/null || apt-get update 2>/dev/null || true +HOOKEOF +chmod +x config/hooks/0010-fix-security-repo.hook.chroot + +# DNS failsafe hook — ensures DNS works for all subsequent hooks +cat > config/hooks/0011-fix-dns.hook.chroot << 'DNSEOF' +#!/bin/bash +# Ensure DNS resolution works inside chroot for package downloads +# /etc/resolv.conf may be a broken symlink to systemd-resolved +rm -f /etc/resolv.conf 2>/dev/null || true +cat > /etc/resolv.conf << 'RESOLV' +nameserver 9.9.9.9 +nameserver 1.1.1.1 +nameserver 8.8.8.8 +RESOLV +echo "[DNS-FIX] Wrote resolvers to /etc/resolv.conf" +# Verify +getent hosts deb.debian.org && echo "[DNS-FIX] DNS working" || echo "[DNS-FIX] WARNING: DNS still broken" +DNSEOF +chmod +x config/hooks/0011-fix-dns.hook.chroot + +# ── Hook: Force non-interactive dpkg (prevents conffile prompts killing builds) ── +cat > config/hooks/0012-fix-dpkg-noninteractive.hook.chroot << 'DPKGEOF' +#!/bin/bash +set +e +echo "[DPKG-FIX] Configuring non-interactive dpkg..." + +# Force dpkg to keep existing config files when packages update them +cat > /etc/dpkg/dpkg.cfg.d/force-confold << 'EOF' +force-confold +force-confdef +EOF + +# Set non-interactive frontend for apt/dpkg +export DEBIAN_FRONTEND=noninteractive +echo 'DEBIAN_FRONTEND=noninteractive' >> /etc/environment + +# Fix any half-configured packages now +DEBIAN_FRONTEND=noninteractive dpkg --configure -a 2>/dev/null || true + +echo "[DPKG-FIX] COMPLETE" +DPKGEOF +chmod +x config/hooks/0012-fix-dpkg-noninteractive.hook.chroot + +# Stage .deb packages in includes.chroot/tmp/ (for b2+) +# NOTE: Do NOT use packages.chroot/ — live-build creates a local apt repo from it, +# which requires the full 'gpg' binary that isn't in the minimal debootstrap chroot. +# Instead, stage .debs in /tmp/ and let hooks install them with dpkg -i. +if [[ "$BUILD_NUM" != "b1" ]]; then + if [[ -d "$PROJECT_DIR/config/packages.chroot" ]]; then + mkdir -p config/includes.chroot/tmp + cp "$PROJECT_DIR/config/packages.chroot/"*.deb config/includes.chroot/tmp/ 2>/dev/null || true + echo "[BUILD] .deb packages staged in /tmp/: $(ls config/includes.chroot/tmp/*.deb 2>/dev/null | wc -l)" + fi +fi + +# Copy includes.chroot (extension tarballs etc for b3+) +if [[ -d "$PROJECT_DIR/config/includes.chroot" ]]; then + mkdir -p config/includes.chroot + cp -r "$PROJECT_DIR/config/includes.chroot/"* config/includes.chroot/ 2>/dev/null || true +fi + +# For b3+: stage Alfred Commander extension tarball +if [[ "$BUILD_NUM" =~ ^(b[3-6]|rc)$ ]]; then + if [[ -f "$PROJECT_DIR/assets/alfred-commander-1.0.1.tar.gz" ]]; then + mkdir -p config/includes.chroot/tmp + cp "$PROJECT_DIR/assets/alfred-commander-1.0.1.tar.gz" config/includes.chroot/tmp/ + echo "[BUILD] Staged Commander extension for b3+ build" + fi +fi + +# Fix bootloader +echo "[BUILD] Fixing bootloader config (isolinux)..." +mkdir -p config/bootloaders/isolinux +for f in install.cfg isolinux.cfg live.cfg.in menu.cfg stdmenu.cfg; do + if [[ -f /usr/share/live/build/bootloaders/isolinux/$f ]]; then + cp /usr/share/live/build/bootloaders/isolinux/$f config/bootloaders/isolinux/ + fi +done +# NOTE: splash.svg.in deliberately excluded — live-build's sed processing of it +# breaks when multiple kernel versions exist (@LINUX_VERSIONS@ contains newlines). +cp /usr/lib/ISOLINUX/isolinux.bin config/bootloaders/isolinux/isolinux.bin +cp /usr/lib/syslinux/modules/bios/vesamenu.c32 config/bootloaders/isolinux/vesamenu.c32 +for extra in ldlinux.c32 libutil.c32 libcom32.c32; do + [[ -f /usr/lib/syslinux/modules/bios/$extra ]] && cp /usr/lib/syslinux/modules/bios/$extra config/bootloaders/isolinux/ +done + +# Create branded live.cfg (overrides the template that produces empty labels) +# NOTE: We use /live/vmlinuz but ALSO add a chroot hook to ensure symlinks exist. +# live-build normally renames vmlinuz-X.Y.Z to vmlinuz, but some builds skip this. +cat > config/bootloaders/isolinux/live.cfg << 'LIVECFG' +label live-amd64 + menu label ^Alfred Linux 3.0 (Live) + menu default + kernel /live/vmlinuz + append initrd=/live/initrd.img boot=live config quiet splash + +label live-amd64-failsafe + menu label ^Alfred Linux 3.0 (Safe Mode) + kernel /live/vmlinuz + append initrd=/live/initrd.img boot=live config nomodeset vga=normal +LIVECFG +# Remove .in templates so live-build uses our pre-built config (not sed-processed templates) +rm -f config/bootloaders/isolinux/live.cfg.in +rm -f config/bootloaders/isolinux/splash.svg.in + +# ── Safety net: late-stage chroot hook copies kernel to generic names ── +# Hook numbered 9999 so it runs LAST, after all packages and initramfs updates. +# Uses cp (not symlinks) so live-build's binary_linux-image reliably finds them. +cat > config/hooks/9999-fix-kernel-names.hook.chroot << 'KERNHOOK' +#!/bin/bash +echo "[Alfred] Creating generic kernel copies in /boot/ (late-stage)..." +cd /boot + +# Always overwrite with latest versioned kernel +KERNEL=$(ls -1 vmlinuz-* 2>/dev/null | sort -V | tail -1) +if [[ -n "$KERNEL" ]]; then + rm -f vmlinuz + cp "$KERNEL" vmlinuz + echo "[Alfred] /boot/vmlinuz copied from $KERNEL" +else + echo "[Alfred] WARNING: No vmlinuz-* found in /boot!" +fi + +INITRD=$(ls -1 initrd.img-* 2>/dev/null | sort -V | tail -1) +if [[ -n "$INITRD" ]]; then + rm -f initrd.img + cp "$INITRD" initrd.img + echo "[Alfred] /boot/initrd.img copied from $INITRD" +else + echo "[Alfred] WARNING: No initrd.img-* found in /boot!" +fi + +ls -lh /boot/vmlinuz /boot/initrd.img 2>/dev/null +KERNHOOK +chmod +x config/hooks/9999-fix-kernel-names.hook.chroot + +echo "[BUILD] Bootloader fix applied: $(ls config/bootloaders/isolinux/ | wc -l) files" + +# Copy skeleton files +echo "[BUILD] Installing skeleton files..." +if [[ -d "$PROJECT_DIR/config/includes.chroot" ]]; then + mkdir -p config/includes.chroot + cp -r "$PROJECT_DIR/config/includes.chroot/"* config/includes.chroot/ 2>/dev/null || true +fi + +# Build! +echo "" +echo "[BUILD] ════════════════════════════════════" +echo "[BUILD] Starting ISO build: $VERSION" +echo "[BUILD] Time: $(date)" +echo "[BUILD] ════════════════════════════════════" +echo "" +export DEBIAN_FRONTEND=noninteractive +lb build 2>&1 | tee "$BUILD_DIR/build.log" +BUILD_RC=${PIPESTATUS[0]} +echo "[BUILD] lb build exited with code: $BUILD_RC" +if [ $BUILD_RC -ne 0 ]; then + echo "[BUILD] WARNING: lb build returned non-zero ($BUILD_RC) — checking if ISO was still created..." +fi + +# Move output +ISO_FILE=$(find . -maxdepth 1 -name "*.iso" -o -name "*.hybrid.iso" | head -1) +if [[ -n "$ISO_FILE" ]]; then + mkdir -p "$OUTPUT_DIR" + mv "$ISO_FILE" "$OUTPUT_DIR/$ISO_NAME" + cd "$OUTPUT_DIR" + sha256sum "$ISO_NAME" > "${ISO_NAME}.sha256" + + # Optional: Add UEFI boot support + if [[ "$ENABLE_UEFI" == "true" ]]; then + echo "[BUILD] Adding UEFI boot support..." + "$SCRIPT_DIR/add-uefi.sh" "$OUTPUT_DIR/$ISO_NAME" + # Regenerate checksum after UEFI modification + sha256sum "$ISO_NAME" > "${ISO_NAME}.sha256" + fi + SIZE=$(du -h "$ISO_NAME" | cut -f1) + echo "" + echo " ╔═══════════════════════════════════════════════════╗" + echo " ║ BUILD COMPLETE — ${VERSION} ║" + echo " ║ ISO: $ISO_NAME" + echo " ║ Size: $SIZE" + echo " ║ SHA256: $(cat "${ISO_NAME}.sha256" | cut -d' ' -f1 | head -c 16)..." + echo " ║ Time: $(date)" + echo " ╚═══════════════════════════════════════════════════╝" + echo "" +else + echo "" + echo " ╔═══════════════════════════════════════════════════╗" + echo " ║ BUILD FAILED — ${VERSION} ║" + echo " ║ Check: $BUILD_DIR/build.log ║" + echo " ╚═══════════════════════════════════════════════════╝" + echo "" + tail -30 "$BUILD_DIR/build.log" + exit 1 +fi