546 lines
18 KiB
Bash
Executable File
546 lines
18 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# EcoOS Installer
|
|
# Installs EcoOS from live USB to disk
|
|
#
|
|
|
|
set -e
|
|
|
|
# Configuration
|
|
TIMEOUT=10
|
|
HOSTNAME="ecoos"
|
|
USERNAME="ecouser"
|
|
SQUASHFS_PATH="/run/live/medium/live/filesystem.squashfs"
|
|
|
|
# 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..."
|
|
|
|
# Wipe existing partition table
|
|
wipefs -a "$disk" >/dev/null 2>&1 || true
|
|
|
|
# Create GPT partition table
|
|
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%
|
|
|
|
# Wait for partitions to appear
|
|
sleep 2
|
|
partprobe "$disk"
|
|
sleep 1
|
|
|
|
# Determine partition names (nvme vs sd)
|
|
if [[ "$disk" == *"nvme"* ]]; then
|
|
EFI_PART="${disk}p1"
|
|
ROOT_PART="${disk}p2"
|
|
else
|
|
EFI_PART="${disk}1"
|
|
ROOT_PART="${disk}2"
|
|
fi
|
|
|
|
log "Created partitions: EFI=$EFI_PART, Root=$ROOT_PART"
|
|
}
|
|
|
|
# Format partitions
|
|
format_partitions() {
|
|
log "Formatting partitions..."
|
|
|
|
mkfs.fat -F 32 -n "EFI" "$EFI_PART"
|
|
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
|
|
|
|
mkdir -p /mnt/target/boot/efi
|
|
mount "$EFI_PART" /mnt/target/boot/efi
|
|
|
|
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 efi_uuid=$(blkid -s UUID -o value "$EFI_PART")
|
|
|
|
# Create fstab
|
|
cat > /mnt/target/etc/fstab << EOF
|
|
# EcoOS fstab
|
|
UUID=$root_uuid / ext4 defaults,noatime 0 1
|
|
UUID=$efi_uuid /boot/efi vfat umask=0077 0 1
|
|
EOF
|
|
|
|
# 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"
|
|
}
|
|
|
|
# Install bootloader
|
|
install_bootloader() {
|
|
log "Installing GRUB bootloader..."
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# Disable casper-related services
|
|
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
|
|
|
|
# Install GRUB
|
|
chroot /mnt/target grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=EcoOS --recheck
|
|
|
|
# Generate GRUB config
|
|
chroot /mnt/target update-grub
|
|
|
|
# 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
|
|
umount /mnt/target/boot/efi
|
|
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 "$@"
|