#!/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