#!/bin/bash # ParticleOS ISO Builder - True bootc Native Approach # This script creates a minimal live ISO that contains bootc tools # and can install the ParticleOS OCI image to the target system set -euo pipefail # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_NAME="particleos" VERSION="1.0.0" BUILD_TIMESTAMP=$(date +%y%m%d-%H%M) BUILD_DIR="$SCRIPT_DIR/build" OUTPUT_DIR="$SCRIPT_DIR/output" LOG_DIR="$SCRIPT_DIR/logs/$(date +%y%m%d%H%M)" # bootc-specific variables SYSTEM_IMAGE="particleos-system:latest" LIVE_IMAGE="particleos-live:latest" CONTAINER_NAME="particleos-extract" # apt-cacher-ng configuration APT_CACHER_NG_HOST="192.168.1.79" APT_CACHER_NG_PORT="3142" APT_CACHER_NG_URL="http://$APT_CACHER_NG_HOST:$APT_CACHER_NG_PORT" CACHER_CONTAINER_BUILD_USED="No" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging setup mkdir -p "$LOG_DIR" exec > >(tee -a "$LOG_DIR/build.log") 2>&1 # Utility functions print_header() { echo -e "${BLUE}================================${NC}" echo -e "${BLUE}$1${NC}" echo -e "${BLUE}================================${NC}" } print_status() { echo -e "${BLUE}[INFO]${NC} $1" } print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } print_error() { echo -e "${RED}[ERROR]${NC} $1" exit 1 } # Check if apt-cacher-ng is reachable check_proxy_reachable() { if command -v nc >/dev/null 2>&1; then if nc -z "$APT_CACHER_NG_HOST" "$APT_CACHER_NG_PORT" 2>/dev/null; then return 0 fi fi return 1 } # Check prerequisites check_prerequisites() { print_header "Phase 1: Check Prerequisites" local missing_packages=() local required_packages=( "squashfs-tools" "xorriso" "grub-pc-bin" "grub-efi-amd64-signed" "syslinux-common" "netcat-openbsd" "casper" "podman" ) for pkg in "${required_packages[@]}"; do if ! dpkg -l | grep -q "^ii.*$pkg"; then missing_packages+=("$pkg") fi done if [ ${#missing_packages[@]} -gt 0 ]; then print_status "Installing missing packages: ${missing_packages[*]}" sudo apt update sudo apt install -y "${missing_packages[@]}" || print_error "Failed to install required packages" fi # Check for bootc specifically (since it's not in standard repos) if ! command -v bootc &> /dev/null; then print_error "bootc is not installed. Please install bootc from your local packages first." fi # Check disk space (need at least 10GB) - check root filesystem for podman storage local available_space=$(df / | awk 'NR==2 {print $4}') local required_space=$((10 * 1024 * 1024)) # 10GB in KB if [ "$available_space" -lt "$required_space" ]; then print_error "Insufficient disk space on root filesystem. Need at least 10GB free, have $((available_space / 1024 / 1024))GB" fi # Check network connectivity if ! ping -c 1 archive.ubuntu.com >/dev/null 2>&1; then print_error "Cannot reach Ubuntu archives. Check your internet connection." fi print_success "All prerequisites satisfied" } # Clean build environment clean_build() { print_header "Phase 2: Clean Build Environment" # Safety check: Ensure BUILD_DIR is within SCRIPT_DIR if [[ "$BUILD_DIR" != "$SCRIPT_DIR"* ]]; then print_error "BUILD_DIR must be within SCRIPT_DIR for safety" fi if [ -d "$BUILD_DIR" ]; then print_status "Cleaning existing build directory..." # Force remove any existing containers and images podman container rm -f "$CONTAINER_NAME" 2>/dev/null || true podman image rm -f "$SYSTEM_IMAGE" "$LIVE_IMAGE" 2>/dev/null || true sudo rm -rf "$BUILD_DIR" fi mkdir -p "$BUILD_DIR" "$OUTPUT_DIR" chmod 755 "$OUTPUT_DIR" print_success "Build environment cleaned" } # Create the main ParticleOS system OCI image create_system_image() { print_header "Phase 3: Create ParticleOS System Image" print_status "Creating ParticleOS system OCI image..." # Try to use apt-cacher-ng if available for container build local use_proxy=false if check_proxy_reachable; then print_status "Container build will use apt-cacher-ng: $APT_CACHER_NG_URL" CACHER_CONTAINER_BUILD_USED="Yes" use_proxy=true else print_warning "apt-cacher-ng not reachable. Container build will proceed without proxy." CACHER_CONTAINER_BUILD_USED="No (Fallback to direct)" use_proxy=false fi # Create a Dockerfile for the main system image cat > "$BUILD_DIR/Dockerfile.system" << 'EOF' # Start with Ubuntu 24.04 base FROM ubuntu:24.04 # Set environment ENV DEBIAN_FRONTEND=noninteractive ENV TZ=UTC # Configure apt proxy if available EOF # Add proxy configuration if available if [ "$use_proxy" = true ]; then cat >> "$BUILD_DIR/Dockerfile.system" << EOF RUN echo 'Acquire::http::Proxy "$APT_CACHER_NG_URL";' > /etc/apt/apt.conf.d/01proxy && \\ echo 'Acquire::https::Proxy "$APT_CACHER_NG_URL";' >> /etc/apt/apt.conf.d/01proxy EOF fi # Continue with the system Dockerfile cat >> "$BUILD_DIR/Dockerfile.system" << 'EOF' # Update and install base packages RUN apt update && apt install -y \ systemd \ systemd-sysv \ dbus \ curl \ ca-certificates \ gnupg \ gpgv \ locales \ resolvconf \ && rm -rf /var/lib/apt/lists/* # Configure locales RUN locale-gen en_US.UTF-8 && update-locale LANG=en_US.UTF-8 # Block snapd installation with APT preferences RUN echo 'Package: snapd' > /etc/apt/preferences.d/no-snapd && \ echo 'Pin: release *' >> /etc/apt/preferences.d/no-snapd && \ echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/no-snapd && \ echo '' >> /etc/apt/preferences.d/no-snapd && \ echo 'Package: snap' >> /etc/apt/preferences.d/no-snapd && \ echo 'Pin: release *' >> /etc/apt/preferences.d/no-snapd && \ echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/no-snapd && \ echo '' >> /etc/apt/preferences.d/no-snapd && \ echo 'Package: firefox*' >> /etc/apt/preferences.d/no-snapd && \ echo 'Pin: release o=Ubuntu*' >> /etc/apt/preferences.d/no-snapd && \ echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/no-snapd # Install desktop environment RUN apt update && apt install -y \ plasma-desktop \ plasma-workspace \ sddm \ kwin-x11 \ flatpak \ network-manager \ plasma-nm \ openssh-server \ curl \ wget \ vim \ nano \ htop \ neofetch \ tree \ pulseaudio \ pulseaudio-utils \ fonts-ubuntu \ fonts-noto \ build-essential \ git \ && rm -rf /var/lib/apt/lists/* # Copy and install local packages COPY pkgs/ /tmp/pkgs/ # Install dependencies first, then our custom packages RUN apt update && apt install -y \ libarchive13t64 \ libavahi-client3 \ libavahi-common3 \ libavahi-glib1 \ libcurl3t64-gnutls \ libgpgme11t64 \ libglib2.0-0t64 \ podman \ skopeo \ && rm -rf /var/lib/apt/lists/* # Install ostree and bootc packages from local files (dynamic version detection) RUN for pkg in /tmp/pkgs/libostree-1-1_*.deb; do \ if [ -f "$pkg" ]; then dpkg -i "$pkg"; break; fi; \ done RUN for pkg in /tmp/pkgs/ostree_*.deb; do \ if [ -f "$pkg" ]; then dpkg -i "$pkg"; break; fi; \ done RUN for pkg in /tmp/pkgs/bootc_*.deb; do \ if [ -f "$pkg" ]; then dpkg -i "$pkg"; break; fi; \ done # Install apt-ostree from local packages if available RUN for pkg in /tmp/pkgs/apt-ostree_*.deb; do \ if [ -f "$pkg" ]; then dpkg -i "$pkg"; break; fi; \ done # Install ostree-boot (recommended by bootc) RUN for pkg in /tmp/pkgs/ostree-boot_*.deb; do \ if [ -f "$pkg" ]; then dpkg -i "$pkg"; break; fi; \ done # Remove unwanted packages RUN apt purge -y ubuntu-advantage-tools || true && \ apt purge -y update-notifier || true && \ apt purge -y update-manager || true && \ apt purge -y unattended-upgrades || true && \ apt autoremove -y && \ apt clean # Configure system RUN echo "particleos" > /etc/hostname && \ echo "127.0.0.1 localhost" >> /etc/hosts && \ echo "127.0.1.1 particleos.local particleos" >> /etc/hosts && \ ln -sf /usr/share/zoneinfo/UTC /etc/localtime # Create user RUN useradd -m -s /bin/bash particle && \ usermod -aG sudo particle && \ echo 'particle ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/particle && \ chmod 0440 /etc/sudoers.d/particle # Configure apt-ostree RUN mkdir -p /etc/apt-ostree && \ echo "ref: particleos/desktop/1.0.0" > /etc/apt-ostree/ref # Enable services RUN systemctl enable sddm NetworkManager ssh && \ systemctl disable apt-daily.timer apt-daily-upgrade.timer # Clean up RUN rm -rf /tmp/pkgs /var/lib/apt/lists/* /tmp/* # Set labels for bootc LABEL org.opencontainers.image.title="ParticleOS Desktop" LABEL org.opencontainers.image.description="Atomic Ubuntu Desktop with apt-ostree" LABEL org.opencontainers.image.version="1.0.0" LABEL org.opencontainers.image.vendor="ParticleOS" LABEL org.opencontainers.image.source="https://github.com/particleos/particleos-installer" # Expose ports EXPOSE 22 # Set default command CMD ["/lib/systemd/systemd"] EOF # Copy packages to build context if [ -d "$SCRIPT_DIR/pkgs" ]; then cp -r "$SCRIPT_DIR/pkgs" "$BUILD_DIR/" fi # Copy install script to build context if [ -f "$SCRIPT_DIR/install-particleos" ]; then cp "$SCRIPT_DIR/install-particleos" "$BUILD_DIR/" fi # Build the system image using podman print_status "Building system image..." cd "$BUILD_DIR" podman build -f Dockerfile.system -t "$SYSTEM_IMAGE" . || print_error "Failed to build system image." print_success "System image built: $SYSTEM_IMAGE" } # Create minimal live ISO with bootc tools create_live_iso() { print_header "Phase 4: Create Minimal Live ISO with bootc Tools" print_status "Creating minimal live ISO with bootc installation tools..." # Create a minimal Dockerfile for the live environment cat > "$BUILD_DIR/Dockerfile.live" << 'EOF' # Start with Ubuntu 24.04 base FROM ubuntu:24.04 # Set environment ENV DEBIAN_FRONTEND=noninteractive ENV TZ=UTC # Update and install minimal packages for live environment RUN apt update && apt install -y \ systemd \ systemd-sysv \ dbus \ curl \ ca-certificates \ gnupg \ gpgv \ locales \ resolvconf \ live-boot \ live-config \ casper \ linux-image-generic \ linux-headers-generic \ grub-pc-bin \ grub-efi-amd64-signed \ xorriso \ squashfs-tools \ && rm -rf /var/lib/apt/lists/* # Configure locales RUN locale-gen en_US.UTF-8 && update-locale LANG=en_US.UTF-8 # Copy and install bootc packages COPY pkgs/ /tmp/pkgs/ # Install dependencies first, then our custom packages RUN apt update && apt install -y \ libarchive13t64 \ libavahi-client3 \ libavahi-common3 \ libavahi-glib1 \ libcurl3t64-gnutls \ libgpgme11t64 \ libglib2.0-0t64 \ podman \ skopeo \ && rm -rf /var/lib/apt/lists/* # Install ostree and bootc packages from local files (dynamic version detection) RUN for pkg in /tmp/pkgs/libostree-1-1_*.deb; do \ if [ -f "$pkg" ]; then dpkg -i "$pkg"; break; fi; \ done RUN for pkg in /tmp/pkgs/ostree_*.deb; do \ if [ -f "$pkg" ]; then dpkg -i "$pkg"; break; fi; \ done RUN for pkg in /tmp/pkgs/bootc_*.deb; do \ if [ -f "$pkg" ]; then dpkg -i "$pkg"; break; fi; \ done # Install apt-ostree from local packages if available RUN for pkg in /tmp/pkgs/apt-ostree_*.deb; do \ if [ -f "$pkg" ]; then dpkg -i "$pkg"; break; fi; \ done # Install ostree-boot (recommended by bootc) RUN for pkg in /tmp/pkgs/ostree-boot_*.deb; do \ if [ -f "$pkg" ]; then dpkg -i "$pkg"; break; fi; \ done # Configure system RUN echo "particleos-live" > /etc/hostname && \ echo "127.0.0.1 localhost" >> /etc/hosts && \ echo "127.0.1.1 particleos-live.local particleos-live" >> /etc/hosts && \ ln -sf /usr/share/zoneinfo/UTC /etc/localtime # Create user for live environment RUN useradd -m -s /bin/bash particle && \ usermod -aG sudo particle && \ echo 'particle ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/particle && \ chmod 0440 /etc/sudoers.d/particle # Create casper hook for live boot RUN mkdir -p /etc/casper && \ echo "particleos-live" > /etc/casper/hostname && \ echo "particle" > /etc/casper/username # Create bootc installation script COPY install-particleos /usr/local/bin/install-particleos RUN chmod +x /usr/local/bin/install-particleos # Regenerate initrd to include live-boot modules RUN update-initramfs -u -k all # Clean up RUN rm -rf /tmp/pkgs /var/lib/apt/lists/* /tmp/* # Set labels LABEL org.opencontainers.image.title="ParticleOS Live" LABEL org.opencontainers.image.description="Minimal live environment for ParticleOS installation" LABEL org.opencontainers.image.version="1.0.0" LABEL org.opencontainers.image.vendor="ParticleOS" # Set default command CMD ["/lib/systemd/systemd"] EOF # Build the live image print_status "Building live image..." podman build -f Dockerfile.live -t "$LIVE_IMAGE" . || print_error "Failed to build live image." # Extract the live filesystem print_status "Extracting live filesystem..." podman create --name "$CONTAINER_NAME" "$LIVE_IMAGE" || print_error "Failed to create container." local extract_dir="$BUILD_DIR/extract" mkdir -p "$extract_dir" podman export "$CONTAINER_NAME" | tar -x -C "$extract_dir" || print_error "Failed to extract filesystem." # Remove the temporary container podman container rm "$CONTAINER_NAME" || print_warning "Failed to remove container" # Verify extraction was successful if [ ! -f "$extract_dir/etc/os-release" ]; then print_error "Filesystem extraction failed - /etc/os-release not found" fi print_success "Live filesystem extracted" } # Create the ISO create_iso() { print_header "Phase 5: Create ISO" print_status "Creating ISO from live filesystem..." local extract_dir="$BUILD_DIR/extract" local iso_dir="$BUILD_DIR/iso" # Create ISO directory structure mkdir -p "$iso_dir"/{casper,boot/grub,EFI/BOOT,isolinux} # Find kernel and initrd local kernel_path=$(find "$extract_dir/boot" -maxdepth 1 -name "vmlinuz-*" | sort -V | tail -n 1) local initrd_path=$(find "$extract_dir/boot" -maxdepth 1 -name "initrd.img-*" | sort -V | tail -n 1) if [ -z "$kernel_path" ] || [ -z "$initrd_path" ]; then print_error "Kernel or initrd not found in extracted filesystem" fi # Copy kernel and initrd cp "$kernel_path" "$iso_dir/casper/vmlinuz" || print_error "Failed to copy kernel." cp "$initrd_path" "$iso_dir/casper/initrd" || print_error "Failed to copy initrd." # Create squashfs (exclude boot/grub, not entire boot) print_status "Creating squashfs..." sudo mksquashfs "$extract_dir" "$iso_dir/casper/filesystem.squashfs" -comp xz -e boot/grub || print_error "Failed to create squashfs." # Generate filesystem.manifest print_status "Generating filesystem.manifest..." sudo chroot "$extract_dir" dpkg-query -W --showformat='${Package} ${Version}\n' > "$iso_dir/casper/filesystem.manifest" || print_error "Failed to generate filesystem.manifest." # Create filesystem.size du -sx --block-size=1 "$extract_dir" | cut -f1 > "$iso_dir/casper/filesystem.size" # Create casper.conf cat > "$iso_dir/casper/casper.conf" << 'EOF' # This file is used by casper to configure the live environment # See casper(7) for more information # Default options for the live environment DEFAULT_OPTS="boot=casper quiet splash ---" # Timeout for the boot menu (in seconds) TIMEOUT=30 # Default selection in the boot menu DEFAULT=0 # Enable automatic hardware detection AUTO_DETECT=true # Enable automatic network configuration AUTO_NETWORK=true # Enable automatic user creation AUTO_USER=true # Default username for the live environment DEFAULT_USER=particle # Enable automatic login AUTO_LOGIN=true EOF # Find ISOLINUX files local isolinux_base_dir="" for dir in /usr/lib/ISOLINUX /usr/lib/syslinux /usr/share/syslinux; do if [ -f "$dir/isolinux.bin" ]; then isolinux_base_dir="$dir" break fi done if [ -z "$isolinux_base_dir" ]; then print_error "ISOLINUX files not found" fi # Find syslinux modules directory local syslinux_modules_dir="" if [ -d "/usr/lib/syslinux/modules/bios" ]; then syslinux_modules_dir="/usr/lib/syslinux/modules/bios" elif [ -d "/usr/share/syslinux/modules/bios" ]; then syslinux_modules_dir="/usr/share/syslinux/modules/bios" elif [ -d "$isolinux_base_dir/modules/bios" ]; then syslinux_modules_dir="$isolinux_base_dir/modules/bios" fi # Copy ISOLINUX files with robust module detection cp "$isolinux_base_dir/isolinux.bin" "$iso_dir/isolinux/" || print_error "Failed to copy isolinux.bin" # Copy ldlinux.c32 from modules directory if available, otherwise from base directory if [ -f "$syslinux_modules_dir/ldlinux.c32" ]; then cp "$syslinux_modules_dir/ldlinux.c32" "$iso_dir/isolinux/" || print_error "Failed to copy ldlinux.c32 from modules dir" elif [ -f "$isolinux_base_dir/ldlinux.c32" ]; then cp "$isolinux_base_dir/ldlinux.c32" "$iso_dir/isolinux/" || print_error "Failed to copy ldlinux.c32 from base dir" else print_error "ldlinux.c32 not found in any expected location" fi # Copy other modules if found for module in menu.c32 memdisk mboot.c32 libutil.c32; do if [ -f "$syslinux_modules_dir/$module" ]; then cp "$syslinux_modules_dir/$module" "$iso_dir/isolinux/" || print_warning "Failed to copy $module from modules dir" elif [ -f "$isolinux_base_dir/$module" ]; then cp "$isolinux_base_dir/$module" "$iso_dir/isolinux/" || print_warning "Failed to copy $module from base dir" else print_warning "Syslinux module $module not found, ISOLINUX might have issues" fi done # Copy memtest86+ if available local memtest_paths=( "/boot/memtest86+.bin" "/usr/lib/memtest86+/memtest86+.bin" "/usr/share/memtest86+/memtest86+.bin" ) for memtest_path in "${memtest_paths[@]}"; do if [ -f "$memtest_path" ]; then cp "$memtest_path" "$iso_dir/isolinux/memtest86+.bin" || print_warning "Failed to copy memtest86+.bin" cp "$memtest_path" "$iso_dir/boot/grub/memtest86+.bin" || print_warning "Failed to copy memtest86+.bin to grub" break fi done # Create isolinux.cfg cat > "$iso_dir/isolinux/isolinux.cfg" << EOF UI menu.c32 prompt 0 menu title ParticleOS Live timeout 30 label live menu label ^ParticleOS Live menu default kernel /casper/vmlinuz append boot=casper quiet splash --- label live-nomodeset menu label ParticleOS Live (safe graphics) kernel /casper/vmlinuz append boot=casper quiet splash nomodeset --- label memtest menu label ^Memory test kernel /isolinux/memdisk append initrd=/isolinux/memtest86+.bin label hd menu label ^Boot from first hard disk localboot 0x80 EOF # Create GRUB configuration cat > "$iso_dir/boot/grub/grub.cfg" << EOF set timeout=30 set default=0 menuentry "ParticleOS Live" { linux /casper/vmlinuz boot=casper quiet splash --- initrd /casper/initrd } menuentry "ParticleOS Live (safe graphics)" { linux /casper/vmlinuz boot=casper quiet splash nomodeset --- initrd /casper/initrd } menuentry "Memory test" { linux16 /boot/grub/memtest86+.bin } menuentry "Boot from first hard disk" { set root=(hd0) chainloader +1 } EOF # Create EFI boot image print_status "Creating EFI boot image..." grub-mkimage -o "$iso_dir/EFI/BOOT/bootx64.efi" \ -p "/boot/grub" \ -O x86_64-efi \ boot linux normal configfile part_gpt part_msdos fat squash4 test configfile search loadenv efi_gop efi_uga all_video gfxterm_menu chain iso9660 || print_error "Failed to create EFI boot image." # Create the ISO print_status "Creating ISO with xorriso..." xorriso -as mkisofs \ -o "$OUTPUT_DIR/${PROJECT_NAME}-${BUILD_TIMESTAMP}.iso" \ -J -joliet-long \ -r -V "ParticleOS ${VERSION}" \ -b isolinux/isolinux.bin \ -c isolinux/boot.cat \ -boot-load-size 4 -boot-info-table \ -no-emul-boot \ -eltorito-alt-boot \ -e EFI/BOOT/bootx64.efi \ -no-emul-boot \ -isohybrid-mbr "$isolinux_base_dir/isohdpfx.bin" \ -partition_offset 16 \ -part_like_isohybrid \ "$iso_dir" || print_error "Failed to create ISO with xorriso." print_success "ISO created: $OUTPUT_DIR/${PROJECT_NAME}-${BUILD_TIMESTAMP}.iso" } # Cleanup function cleanup() { print_status "Cleaning up..." # Remove temporary containers and images podman container rm -f "$CONTAINER_NAME" 2>/dev/null || true podman image rm -f "$SYSTEM_IMAGE" "$LIVE_IMAGE" 2>/dev/null || true # Remove build directory if [ -d "$BUILD_DIR" ]; then sudo rm -rf "$BUILD_DIR" fi print_success "Cleanup completed" } # Main execution main() { echo "$(date): Starting ParticleOS ISO build with true bootc approach" echo "$(date): Log directory: $LOG_DIR" echo -e "${GREEN}🚀 ParticleOS ISO Builder (True bootc Native)${NC}" echo "======================================" echo "Project: $PROJECT_NAME" echo "Version: $VERSION" echo "Build Directory: $BUILD_DIR" echo "Tool: True bootc + Minimal Live ISO" echo "" echo -e "${BLUE}🔄 This build creates:${NC}" echo " • OCI system image for deployment" echo " • Minimal live ISO with bootc tools" echo " • Live environment can install OCI image" echo " • True bootc-managed target system" echo "" # Set up signal handlers trap cleanup EXIT trap 'print_error "Build interrupted by user"' INT TERM # Execute build phases check_prerequisites clean_build create_system_image create_live_iso create_iso echo "" echo -e "${GREEN}================================${NC}" echo -e "${GREEN}🎉 Build Completed Successfully!${NC}" echo -e "${GREEN}================================${NC}" echo "" echo -e "${BLUE}📦 Generated Files:${NC}" echo " • System Image: $SYSTEM_IMAGE" echo " • Live Image: $LIVE_IMAGE" echo " • ISO: $OUTPUT_DIR/${PROJECT_NAME}-${BUILD_TIMESTAMP}.iso" echo "" echo -e "${BLUE}📋 Summary of Cacher Usage:${NC}" echo " • Container Build: $CACHER_CONTAINER_BUILD_USED" echo "" echo -e "${BLUE}🚀 Next Steps:${NC}" echo " 1. Test the ISO in QEMU:" echo " qemu-system-x86_64 -m 4G -enable-kvm -cdrom $OUTPUT_DIR/${PROJECT_NAME}-${BUILD_TIMESTAMP}.iso" echo "" echo " 2. Boot from ISO and install:" echo " ./install-particleos /dev/sda" echo "" echo -e "${BLUE}📝 Logs:${NC}" echo " • Build log: $LOG_DIR/build.log" echo "" } # Run main function main "$@"