#!/bin/bash # # EcoOS Installer # Installs EcoOS from live USB/SD to disk # Supports: x86_64 (UEFI), ARM64 (UEFI), Raspberry Pi (native boot) # set -e # Configuration TIMEOUT=10 HOSTNAME="ecoos" USERNAME="ecouser" SQUASHFS_PATH="/run/live/medium/live/filesystem.squashfs" # Detect architecture detect_architecture() { local arch=$(uname -m) local is_rpi="no" # Check if running on Raspberry Pi if [ -f /sys/firmware/devicetree/base/model ]; then local model=$(cat /sys/firmware/devicetree/base/model 2>/dev/null || echo "") if [[ "$model" == *"Raspberry Pi"* ]]; then is_rpi="yes" fi fi case "$arch" in x86_64) ARCH_TYPE="amd64" BOOT_TYPE="uefi" ;; aarch64) if [ "$is_rpi" = "yes" ]; then ARCH_TYPE="rpi" BOOT_TYPE="rpi" else ARCH_TYPE="arm64" BOOT_TYPE="uefi" fi ;; *) ARCH_TYPE="unknown" BOOT_TYPE="unknown" ;; esac export ARCH_TYPE BOOT_TYPE } # Call architecture detection early detect_architecture # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color log() { echo -e "${GREEN}[EcoOS]${NC} $1" >&2 } warn() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2 } error() { echo -e "${RED}[ERROR]${NC} $1" >&2 exit 1 } # Get the device the live system is running from get_live_device() { local live_dev="" # Find the device backing /run/live/medium if mountpoint -q /run/live/medium 2>/dev/null; then live_dev=$(findmnt -n -o SOURCE /run/live/medium | sed 's/[0-9]*$//') fi # Also check /cdrom for older casper if [ -z "$live_dev" ] && mountpoint -q /cdrom 2>/dev/null; then live_dev=$(findmnt -n -o SOURCE /cdrom | sed 's/[0-9]*$//') fi echo "$live_dev" } # List available disks (excluding live media and loop devices) list_disks() { local live_dev=$(get_live_device) local disks=() for disk in /sys/block/sd* /sys/block/nvme* /sys/block/vd*; do [ -e "$disk" ] || continue local name=$(basename "$disk") local dev="/dev/$name" # Skip if this is the live media if [ "$dev" = "$live_dev" ]; then continue fi # Skip removable devices (USB sticks) - but allow if it's the only option local removable=$(cat "$disk/removable" 2>/dev/null || echo "0") # Get size in GB local size_bytes=$(cat "$disk/size" 2>/dev/null || echo "0") local size_gb=$((size_bytes * 512 / 1024 / 1024 / 1024)) # Skip disks smaller than 10GB if [ "$size_gb" -lt 10 ]; then continue fi # Get model local model=$(cat "$disk/device/model" 2>/dev/null | tr -d '\n' || echo "Unknown") disks+=("$dev|$size_gb|$model|$removable") done printf '%s\n' "${disks[@]}" } # Select disk with timeout # All UI output goes to stderr so stdout only returns the device path select_disk() { local disks mapfile -t disks < <(list_disks) if [ ${#disks[@]} -eq 0 ]; then error "No suitable disks found for installation" fi echo "" >&2 echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" >&2 echo -e "${BLUE}║${NC} ${GREEN}EcoOS Disk Installation${NC} ${BLUE}║${NC}" >&2 echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}" >&2 echo "" >&2 echo "Available disks:" >&2 echo "" >&2 local i=1 local default_disk="" local default_idx=1 local max_size=0 for disk_info in "${disks[@]}"; do IFS='|' read -r dev size model removable <<< "$disk_info" local marker="" if [ "$size" -gt "$max_size" ]; then max_size=$size default_disk=$dev default_idx=$i fi printf " ${YELLOW}%d)${NC} %-12s %4d GB %s\n" "$i" "$dev" "$size" "$model" >&2 ((i++)) done echo "" >&2 echo -e "Default: ${GREEN}$default_disk${NC} (largest disk)" >&2 echo "" >&2 echo -e "${YELLOW}WARNING: Selected disk will be COMPLETELY ERASED!${NC}" >&2 echo "" >&2 # Countdown with input check local selected="" local remaining=$TIMEOUT while [ $remaining -gt 0 ]; do printf "\rSelect disk [1-%d] or press Enter for default (%ds remaining): " "${#disks[@]}" "$remaining" >&2 if read -t 1 -n 1 input; then if [ -z "$input" ]; then # Enter pressed - use default selected=$default_idx break elif [[ "$input" =~ ^[0-9]$ ]] && [ "$input" -ge 1 ] && [ "$input" -le ${#disks[@]} ]; then selected=$input echo "" >&2 break else echo "" >&2 warn "Invalid selection. Please enter 1-${#disks[@]}" remaining=$TIMEOUT fi fi ((remaining--)) done if [ -z "$selected" ]; then selected=$default_idx echo "" >&2 log "Timeout - auto-selecting default disk" fi # Get selected disk local idx=$((selected - 1)) IFS='|' read -r TARGET_DISK size model removable <<< "${disks[$idx]}" echo "" >&2 log "Selected: $TARGET_DISK ($size GB - $model)" echo "" >&2 # Final confirmation with shorter timeout echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}" >&2 echo -e "${RED}║ ALL DATA ON $TARGET_DISK WILL BE PERMANENTLY DESTROYED! ║${NC}" >&2 echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}" >&2 echo "" >&2 local confirm_timeout=10 printf "Press 'y' to confirm, any other key to cancel (%ds): " "$confirm_timeout" >&2 if read -t $confirm_timeout -n 1 confirm; then echo "" >&2 if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then error "Installation cancelled by user" fi else echo "" >&2 log "Auto-confirming installation..." fi # Only this goes to stdout - the actual device path echo "$TARGET_DISK" } # Partition the disk partition_disk() { local disk=$1 log "Partitioning $disk for $ARCH_TYPE ($BOOT_TYPE boot)..." # Wipe existing partition table wipefs -a "$disk" >/dev/null 2>&1 || true if [ "$BOOT_TYPE" = "rpi" ]; then # Raspberry Pi uses MBR partition table log "Creating MBR partition table for Raspberry Pi..." parted -s "$disk" mklabel msdos # Create boot partition (FAT32, 256MB) parted -s "$disk" mkpart primary fat32 1MiB 257MiB parted -s "$disk" set 1 boot on # Create root partition (rest of disk) parted -s "$disk" mkpart primary ext4 257MiB 100% else # x86_64 and generic ARM64 use GPT with EFI log "Creating GPT partition table with EFI..." parted -s "$disk" mklabel gpt # Create EFI partition (512MB) parted -s "$disk" mkpart ESP fat32 1MiB 513MiB parted -s "$disk" set 1 esp on # Create root partition (rest of disk) parted -s "$disk" mkpart root ext4 513MiB 100% fi # Wait for partitions to appear sleep 2 partprobe "$disk" sleep 1 # Determine partition names (nvme vs sd vs mmcblk) if [[ "$disk" == *"nvme"* ]] || [[ "$disk" == *"mmcblk"* ]]; then BOOT_PART="${disk}p1" ROOT_PART="${disk}p2" else BOOT_PART="${disk}1" ROOT_PART="${disk}2" fi # For UEFI systems, BOOT_PART is the EFI partition if [ "$BOOT_TYPE" = "uefi" ]; then EFI_PART="$BOOT_PART" fi log "Created partitions: Boot=$BOOT_PART, Root=$ROOT_PART" } # Format partitions format_partitions() { log "Formatting partitions..." if [ "$BOOT_TYPE" = "rpi" ]; then mkfs.fat -F 32 -n "boot" "$BOOT_PART" else mkfs.fat -F 32 -n "EFI" "$EFI_PART" fi mkfs.ext4 -F -L "EcoOS" "$ROOT_PART" log "Partitions formatted" } # Mount partitions mount_partitions() { log "Mounting partitions..." mkdir -p /mnt/target mount "$ROOT_PART" /mnt/target if [ "$BOOT_TYPE" = "rpi" ]; then # Raspberry Pi mounts boot at /boot mkdir -p /mnt/target/boot mount "$BOOT_PART" /mnt/target/boot else # UEFI systems mount EFI at /boot/efi mkdir -p /mnt/target/boot/efi mount "$EFI_PART" /mnt/target/boot/efi fi log "Partitions mounted at /mnt/target" } # Copy system files copy_system() { log "Copying system files (this may take several minutes)..." # Check if squashfs exists if [ ! -f "$SQUASHFS_PATH" ]; then # Try alternative paths (including casper paths for Ubuntu) for path in /run/live/medium/live/filesystem.squashfs \ /cdrom/casper/filesystem.squashfs \ /cdrom/live/filesystem.squashfs \ /isodevice/casper/filesystem.squashfs \ /lib/live/mount/medium/live/filesystem.squashfs \ /rofs/../casper/filesystem.squashfs; do if [ -f "$path" ]; then SQUASHFS_PATH="$path" break fi done fi # If still not found, try to find it if [ ! -f "$SQUASHFS_PATH" ]; then local found=$(find /cdrom /run /media -name "filesystem.squashfs" 2>/dev/null | head -1) if [ -n "$found" ]; then SQUASHFS_PATH="$found" fi fi if [ ! -f "$SQUASHFS_PATH" ]; then error "Cannot find filesystem.squashfs" fi log "Extracting from $SQUASHFS_PATH..." # Extract squashfs unsquashfs -f -d /mnt/target "$SQUASHFS_PATH" log "System files copied" } # Configure the installed system configure_system() { log "Configuring system..." # Get UUIDs local root_uuid=$(blkid -s UUID -o value "$ROOT_PART") local boot_uuid=$(blkid -s UUID -o value "$BOOT_PART") # Create fstab based on boot type if [ "$BOOT_TYPE" = "rpi" ]; then cat > /mnt/target/etc/fstab << EOF # EcoOS fstab - Raspberry Pi UUID=$root_uuid / ext4 defaults,noatime 0 1 UUID=$boot_uuid /boot vfat defaults 0 2 EOF else cat > /mnt/target/etc/fstab << EOF # EcoOS fstab UUID=$root_uuid / ext4 defaults,noatime 0 1 UUID=$boot_uuid /boot/efi vfat umask=0077 0 1 EOF fi # Set hostname echo "$HOSTNAME" > /mnt/target/etc/hostname cat > /mnt/target/etc/hosts << EOF 127.0.0.1 localhost 127.0.1.1 $HOSTNAME ::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters EOF # Ensure seat group exists (for seatd) if ! grep -q "^seat:" /mnt/target/etc/group; then chroot /mnt/target groupadd seat 2>/dev/null || true fi # Ensure render group exists (for GPU access) if ! grep -q "^render:" /mnt/target/etc/group; then chroot /mnt/target groupadd render 2>/dev/null || true fi # Ensure ecouser exists and is configured # Groups: video,render (GPU), audio, input (devices), sudo, seat (seatd) if ! grep -q "^$USERNAME:" /mnt/target/etc/passwd; then chroot /mnt/target useradd -m -s /bin/bash -c "EcoOS User" -G video,render,audio,input,sudo,adm,cdrom,plugdev,seat "$USERNAME" else # Add to required groups if user already exists chroot /mnt/target usermod -a -G video,render,seat "$USERNAME" 2>/dev/null || true # Set the GECOS/full name field if missing chroot /mnt/target chfn -f "EcoOS User" "$USERNAME" 2>/dev/null || true fi # Create Sway config directory for ecouser mkdir -p /mnt/target/home/$USERNAME/.config/sway if [ -f /mnt/target/etc/sway/config ]; then cp /mnt/target/etc/sway/config /mnt/target/home/$USERNAME/.config/sway/config fi chroot /mnt/target chown -R $USERNAME:$USERNAME /home/$USERNAME/.config 2>/dev/null || true # Set a default password (ecouser:ecouser) - should be changed on first boot echo "$USERNAME:ecouser" | chroot /mnt/target chpasswd # Enable sudo for ecouser echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /mnt/target/etc/sudoers.d/ecouser chmod 440 /mnt/target/etc/sudoers.d/ecouser # Remove live-boot packages marker if present rm -f /mnt/target/etc/live 2>/dev/null || true # Enable systemd-networkd for static IP (more reliable than NetworkManager) chroot /mnt/target systemctl enable systemd-networkd.service 2>/dev/null || true chroot /mnt/target systemctl enable systemd-networkd-wait-online.service 2>/dev/null || true # Disable NetworkManager to avoid conflicts chroot /mnt/target systemctl disable NetworkManager.service 2>/dev/null || true chroot /mnt/target systemctl mask NetworkManager.service 2>/dev/null || true # Create network config for systemd-networkd (DHCP for QEMU/VMs) mkdir -p /mnt/target/etc/systemd/network cat > /mnt/target/etc/systemd/network/10-wired.network << 'NETEOF' [Match] Name=ens* enp* eth* [Network] DHCP=yes NETEOF # Fix critical directory permissions - /etc must be world-readable # for systemd-networkd and other services to read their config files # This is CRITICAL - squashfs may have wrong permissions from Docker build log "Fixing /etc permissions..." # Fix /etc and all subdirectories recursively find /mnt/target/etc -type d -exec chmod 755 {} \; # Fix critical files that must be world-readable for file in passwd group hosts hostname profile bash.bashrc environment \ shells nsswitch.conf resolv.conf machine-id ld.so.conf; do if [ -f "/mnt/target/etc/$file" ]; then chmod 644 "/mnt/target/etc/$file" fi done # Shadow files should be root-only readable chmod 640 /mnt/target/etc/shadow 2>/dev/null || true chmod 640 /mnt/target/etc/gshadow 2>/dev/null || true # Sudoers files need specific permissions chmod 440 /mnt/target/etc/sudoers 2>/dev/null || true find /mnt/target/etc/sudoers.d -type f -exec chmod 440 {} \; 2>/dev/null || true # Network config chmod 644 /mnt/target/etc/systemd/network/10-wired.network log "systemd-networkd enabled for networking" # Enable other services for installed system chroot /mnt/target systemctl enable eco-daemon.service 2>/dev/null || true chroot /mnt/target systemctl enable seatd.service 2>/dev/null || true chroot /mnt/target systemctl enable ssh.service 2>/dev/null || true chroot /mnt/target systemctl enable debug-network.service 2>/dev/null || true log "System configured" } # Configure Raspberry Pi boot files configure_rpi_boot() { log "Configuring Raspberry Pi boot files..." local root_uuid=$(blkid -s UUID -o value "$ROOT_PART") # Copy kernel and initrd to boot partition local kernel=$(ls /mnt/target/boot/vmlinuz-* 2>/dev/null | sort -V | tail -1) local initrd=$(ls /mnt/target/boot/initrd.img-* 2>/dev/null | sort -V | tail -1) if [ -n "$kernel" ]; then cp "$kernel" /mnt/target/boot/vmlinuz log "Copied kernel: $(basename $kernel)" fi if [ -n "$initrd" ]; then cp "$initrd" /mnt/target/boot/initrd.img log "Copied initrd: $(basename $initrd)" fi # Copy device tree blobs local dtb_dir=$(ls -d /mnt/target/usr/lib/linux-image-*-raspi 2>/dev/null | tail -1) if [ -d "$dtb_dir/broadcom" ]; then cp "$dtb_dir/broadcom"/*.dtb /mnt/target/boot/ 2>/dev/null || true log "Copied device tree blobs" fi if [ -d "$dtb_dir/overlays" ]; then mkdir -p /mnt/target/boot/overlays cp -r "$dtb_dir/overlays"/* /mnt/target/boot/overlays/ 2>/dev/null || true log "Copied device tree overlays" fi # Copy Pi firmware files if [ -d /mnt/target/usr/lib/raspi-firmware ]; then cp /mnt/target/usr/lib/raspi-firmware/*.bin /mnt/target/boot/ 2>/dev/null || true cp /mnt/target/usr/lib/raspi-firmware/*.elf /mnt/target/boot/ 2>/dev/null || true cp /mnt/target/usr/lib/raspi-firmware/*.dat /mnt/target/boot/ 2>/dev/null || true log "Copied Raspberry Pi firmware" fi # Create config.txt cat > /mnt/target/boot/config.txt << 'EOF' # EcoOS Raspberry Pi Configuration # Supports Pi 3, 4, and 5 # Enable 64-bit mode arm_64bit=1 # Kernel and initrd kernel=vmlinuz initramfs initrd.img followkernel # Enable serial console for debugging enable_uart=1 # GPU/display settings dtoverlay=vc4-kms-v3d gpu_mem=256 # USB and power settings (Pi 4/5) max_usb_current=1 # Audio dtparam=audio=on # Camera/display interfaces camera_auto_detect=1 display_auto_detect=1 # Pi 5 specific (ignored on older models) [pi5] dtoverlay=dwc2,dr_mode=host EOF # Create cmdline.txt with root UUID echo "console=serial0,115200 console=tty1 root=UUID=$root_uuid rootfstype=ext4 fsck.repair=yes rootwait quiet splash" > /mnt/target/boot/cmdline.txt log "Raspberry Pi boot configured" } # Install bootloader install_bootloader() { log "Installing bootloader for $ARCH_TYPE ($BOOT_TYPE)..." # Mount necessary filesystems for chroot mount --bind /dev /mnt/target/dev mount --bind /dev/pts /mnt/target/dev/pts mount --bind /proc /mnt/target/proc mount --bind /sys /mnt/target/sys mount --bind /run /mnt/target/run # Disable casper-related services (common to all architectures) log "Disabling live boot services..." chroot /mnt/target systemctl disable casper.service 2>/dev/null || true chroot /mnt/target systemctl disable casper-md5check.service 2>/dev/null || true chroot /mnt/target systemctl mask casper.service 2>/dev/null || true chroot /mnt/target systemctl mask casper-md5check.service 2>/dev/null || true # Remove casper initramfs hooks to prevent live-boot behavior log "Removing casper initramfs hooks..." rm -rf /mnt/target/usr/share/initramfs-tools/scripts/casper 2>/dev/null || true rm -rf /mnt/target/usr/share/initramfs-tools/scripts/casper-premount 2>/dev/null || true rm -rf /mnt/target/usr/share/initramfs-tools/scripts/casper-bottom 2>/dev/null || true rm -f /mnt/target/usr/share/initramfs-tools/hooks/casper 2>/dev/null || true rm -f /mnt/target/etc/initramfs-tools/conf.d/casper.conf 2>/dev/null || true # Regenerate initramfs without casper hooks log "Regenerating initramfs..." chroot /mnt/target update-initramfs -u -k all # Ensure proper boot target chroot /mnt/target systemctl set-default multi-user.target 2>/dev/null || true if [ "$BOOT_TYPE" = "rpi" ]; then # Raspberry Pi uses native bootloader (no GRUB) configure_rpi_boot else # UEFI systems use GRUB log "Installing GRUB bootloader..." # Fix GRUB default config - remove casper/live boot parameters and add serial console if [ -f /mnt/target/etc/default/grub ]; then # Remove any boot=casper or live-related parameters sed -i 's/boot=casper//g' /mnt/target/etc/default/grub # Update GRUB_CMDLINE_LINUX_DEFAULT with serial console for debugging sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT="console=tty1 console=ttyS0,115200n8"/' /mnt/target/etc/default/grub # If line doesn't exist, add it if ! grep -q "GRUB_CMDLINE_LINUX_DEFAULT" /mnt/target/etc/default/grub; then echo 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty1 console=ttyS0,115200n8"' >> /mnt/target/etc/default/grub fi # Enable serial terminal in GRUB echo 'GRUB_TERMINAL="console serial"' >> /mnt/target/etc/default/grub echo 'GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"' >> /mnt/target/etc/default/grub fi # Install GRUB based on architecture if [ "$ARCH_TYPE" = "amd64" ]; then chroot /mnt/target grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=EcoOS --recheck elif [ "$ARCH_TYPE" = "arm64" ]; then chroot /mnt/target grub-install --target=arm64-efi --efi-directory=/boot/efi --bootloader-id=EcoOS --recheck fi # Generate GRUB config chroot /mnt/target update-grub fi # Cleanup mounts (use lazy unmount for stubborn mounts, reverse order) sync umount -l /mnt/target/run 2>/dev/null || true umount -l /mnt/target/sys 2>/dev/null || true umount -l /mnt/target/proc 2>/dev/null || true umount -l /mnt/target/dev/pts 2>/dev/null || true umount -l /mnt/target/dev 2>/dev/null || true log "Bootloader installed" } # Cleanup and reboot cleanup_and_reboot() { log "Cleaning up..." # Sync disks sync # Unmount based on boot type if [ "$BOOT_TYPE" = "rpi" ]; then umount /mnt/target/boot else umount /mnt/target/boot/efi fi umount /mnt/target echo "" echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ EcoOS Installation Complete! ║${NC}" echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}" echo "" echo "The system will reboot in 10 seconds..." echo "Remove the USB drive when the screen goes blank." echo "" sleep 10 reboot } # Main installation flow main() { clear echo "" echo -e "${GREEN}" echo " ███████╗ ██████╗ ██████╗ ██████╗ ███████╗" echo " ██╔════╝██╔════╝██╔═══██╗██╔═══██╗██╔════╝" echo " █████╗ ██║ ██║ ██║██║ ██║███████╗" echo " ██╔══╝ ██║ ██║ ██║██║ ██║╚════██║" echo " ███████╗╚██████╗╚██████╔╝╚██████╔╝███████║" echo " ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝" echo -e "${NC}" echo " System Installer v1.0" echo "" # Check if running as root if [ "$(id -u)" -ne 0 ]; then error "This script must be run as root" fi # Select target disk TARGET_DISK=$(select_disk) # Partition disk partition_disk "$TARGET_DISK" # Format partitions format_partitions # Mount partitions mount_partitions # Copy system copy_system # Configure system configure_system # Install bootloader install_bootloader # Cleanup and reboot cleanup_and_reboot } # Run main function main "$@"