aboutsummaryrefslogtreecommitdiff
path: root/custom/lib
diff options
context:
space:
mode:
Diffstat (limited to 'custom/lib')
-rw-r--r--custom/lib/btrfs.sh900
-rw-r--r--custom/lib/common.sh173
-rw-r--r--custom/lib/config.sh131
-rw-r--r--custom/lib/disk.sh204
-rw-r--r--custom/lib/zfs.sh359
5 files changed, 0 insertions, 1767 deletions
diff --git a/custom/lib/btrfs.sh b/custom/lib/btrfs.sh
deleted file mode 100644
index 321c05c..0000000
--- a/custom/lib/btrfs.sh
+++ /dev/null
@@ -1,900 +0,0 @@
-#!/usr/bin/env bash
-# btrfs.sh - Btrfs-specific functions for archangel installer
-# Source this file after common.sh, config.sh, disk.sh
-
-#############################
-# Btrfs/LUKS Constants
-#############################
-
-# LUKS settings
-LUKS_MAPPER_NAME="cryptroot"
-
-# Mount options for btrfs subvolumes
-BTRFS_OPTS="noatime,compress=zstd,space_cache=v2,discard=async"
-
-# Subvolume layout (matches ZFS dataset structure)
-# Format: "name:mountpoint:extra_opts"
-BTRFS_SUBVOLS=(
- "@:/::"
- "@home:/home::"
- "@snapshots:/.snapshots::"
- "@var_log:/var/log::"
- "@var_cache:/var/cache::"
- "@tmp:/tmp::nosuid,nodev"
- "@var_tmp:/var/tmp::nosuid,nodev"
- "@media:/media::compress=no"
- "@vms:/vms::nodatacow,compress=no"
- "@var_lib_docker:/var/lib/docker::"
-)
-
-#############################
-# LUKS Functions
-#############################
-
-create_luks_container() {
- local partition="$1"
- local passphrase="$2"
-
- step "Creating LUKS Encrypted Container"
-
- info "Setting up LUKS encryption on $partition..."
-
- # Create LUKS container (-q for batch mode, -d - to read key from stdin)
- echo -n "$passphrase" | cryptsetup -q luksFormat --type luks2 \
- --cipher aes-xts-plain64 --key-size 512 --hash sha512 \
- --iter-time 2000 --pbkdf argon2id \
- -d - "$partition" \
- || error "Failed to create LUKS container"
-
- info "LUKS container created."
-}
-
-open_luks_container() {
- local partition="$1"
- local passphrase="$2"
- local name="${3:-$LUKS_MAPPER_NAME}"
-
- info "Opening LUKS container..."
-
- echo -n "$passphrase" | cryptsetup open "$partition" "$name" -d - \
- || error "Failed to open LUKS container"
-
- info "LUKS container opened as /dev/mapper/$name"
-}
-
-close_luks_container() {
- local name="${1:-$LUKS_MAPPER_NAME}"
-
- cryptsetup close "$name" 2>/dev/null || true
-}
-
-# Testing keyfile for automated LUKS testing
-# When TESTING=yes, we embed a keyfile in initramfs to allow unattended boot
-LUKS_KEYFILE="/etc/cryptroot.key"
-
-setup_luks_testing_keyfile() {
- local passphrase="$1"
- shift
- local partitions=("$@")
-
- [[ "${TESTING:-}" != "yes" ]] && return 0
-
- step "Setting Up Testing Keyfile (TESTING MODE)"
- warn "Adding keyfile to initramfs for automated testing."
- warn "This reduces security - for testing only!"
-
- # Generate random keyfile
- dd if=/dev/urandom of="/mnt${LUKS_KEYFILE}" bs=512 count=4 status=none \
- || error "Failed to generate keyfile"
- chmod 000 "/mnt${LUKS_KEYFILE}"
-
- # Add keyfile to each LUKS partition (slot 1, passphrase stays in slot 0)
- for partition in "${partitions[@]}"; do
- info "Adding keyfile to $partition..."
- echo -n "$passphrase" | cryptsetup luksAddKey "$partition" "/mnt${LUKS_KEYFILE}" -d - \
- || error "Failed to add keyfile to $partition"
- done
-
- info "Testing keyfile configured for ${#partitions[@]} partition(s)."
-}
-
-# Multi-disk LUKS functions
-create_luks_containers() {
- local passphrase="$1"
- shift
- local partitions=("$@")
-
- step "Creating LUKS Encrypted Containers"
-
- local i=0
- for partition in "${partitions[@]}"; do
- info "Setting up LUKS encryption on $partition..."
- echo -n "$passphrase" | cryptsetup -q luksFormat --type luks2 \
- --cipher aes-xts-plain64 --key-size 512 --hash sha512 \
- --iter-time 2000 --pbkdf argon2id \
- -d - "$partition" \
- || error "Failed to create LUKS container on $partition"
- ((++i))
- done
-
- info "Created $i LUKS containers."
-}
-
-open_luks_containers() {
- local passphrase="$1"
- shift
- local partitions=("$@")
-
- step "Opening LUKS Containers"
-
- local i=0
- for partition in "${partitions[@]}"; do
- local name="${LUKS_MAPPER_NAME}${i}"
- [[ $i -eq 0 ]] && name="$LUKS_MAPPER_NAME" # First one has no suffix
- info "Opening LUKS container: $partition -> /dev/mapper/$name"
- echo -n "$passphrase" | cryptsetup open "$partition" "$name" -d - \
- || error "Failed to open LUKS container: $partition"
- ((++i))
- done
-
- info "Opened ${#partitions[@]} LUKS containers."
-}
-
-close_luks_containers() {
- local count="${1:-1}"
-
- for ((i=0; i<count; i++)); do
- local name="${LUKS_MAPPER_NAME}${i}"
- [[ $i -eq 0 ]] && name="$LUKS_MAPPER_NAME"
- cryptsetup close "$name" 2>/dev/null || true
- done
-}
-
-# Get list of opened LUKS mapper devices
-get_luks_devices() {
- local count="$1"
- local devices=()
-
- for ((i=0; i<count; i++)); do
- local name="${LUKS_MAPPER_NAME}${i}"
- [[ $i -eq 0 ]] && name="$LUKS_MAPPER_NAME"
- devices+=("/dev/mapper/$name")
- done
-
- echo "${devices[@]}"
-}
-
-configure_crypttab() {
- local partitions=("$@")
-
- step "Configuring crypttab"
-
- echo "# LUKS encrypted root partitions" > /mnt/etc/crypttab
-
- # Use keyfile if in testing mode, otherwise prompt for passphrase
- local key_source="none"
- if [[ "${TESTING:-}" == "yes" ]]; then
- key_source="$LUKS_KEYFILE"
- info "Testing mode: using keyfile for automatic unlock"
- fi
-
- local i=0
- for partition in "${partitions[@]}"; do
- local uuid
- uuid=$(blkid -s UUID -o value "$partition")
- local name="${LUKS_MAPPER_NAME}${i}"
- [[ $i -eq 0 ]] && name="$LUKS_MAPPER_NAME"
-
- echo "$name UUID=$uuid $key_source luks,discard" >> /mnt/etc/crypttab
- info "crypttab: $name -> UUID=$uuid"
- ((++i))
- done
-
- info "crypttab configured for $i partition(s)"
-}
-
-configure_luks_initramfs() {
- step "Configuring Initramfs for LUKS"
-
- # Backup original
- cp /mnt/etc/mkinitcpio.conf /mnt/etc/mkinitcpio.conf.bak
-
- # Add encrypt hook before filesystems (configure_btrfs_initramfs overwrites
- # this with the final hook list, using sd-encrypt for multi-disk setups)
- sed -i 's/^HOOKS=.*/HOOKS=(base udev microcode modconf kms keyboard keymap consolefont block encrypt filesystems fsck)/' \
- /mnt/etc/mkinitcpio.conf
-
- # Include keyfile in initramfs for testing mode (unattended boot)
- if [[ "${TESTING:-}" == "yes" ]]; then
- info "Testing mode: embedding keyfile in initramfs"
- sed -i "s|^FILES=.*|FILES=($LUKS_KEYFILE)|" /mnt/etc/mkinitcpio.conf
- # If FILES line doesn't exist, add it
- if ! grep -q "^FILES=" /mnt/etc/mkinitcpio.conf; then
- echo "FILES=($LUKS_KEYFILE)" >> /mnt/etc/mkinitcpio.conf
- fi
- fi
-
- # Create crypttab.initramfs for sd-encrypt (used by multi-disk LUKS)
- # sd-encrypt reads this file to open all LUKS devices during initramfs
- if [[ -f /mnt/etc/crypttab ]]; then
- cp /mnt/etc/crypttab /mnt/etc/crypttab.initramfs
- info "Created crypttab.initramfs for sd-encrypt."
- fi
-
- info "Added encrypt hook to initramfs."
-}
-
-configure_luks_grub() {
- local partition="$1"
-
- step "Configuring GRUB for LUKS"
-
- local uuid
- uuid=$(blkid -s UUID -o value "$partition")
-
- # Enable GRUB cryptodisk support (required for encrypted /boot)
- echo "GRUB_ENABLE_CRYPTODISK=y" >> /mnt/etc/default/grub
-
- # Add cryptdevice to GRUB cmdline
- # For testing mode, also add cryptkey parameter for automated unlock
- local cryptkey_param=""
- if [[ "${TESTING:-}" == "yes" ]]; then
- # rootfs: prefix tells encrypt hook the keyfile is in the initramfs
- cryptkey_param="cryptkey=rootfs:$LUKS_KEYFILE "
- info "Testing mode: adding cryptkey parameter for automated unlock"
- fi
-
- sed -i "s|^GRUB_CMDLINE_LINUX=\"|GRUB_CMDLINE_LINUX=\"cryptdevice=UUID=$uuid:$LUKS_MAPPER_NAME:allow-discards ${cryptkey_param}|" \
- /mnt/etc/default/grub
-
- info "GRUB configured with cryptdevice parameter and cryptodisk enabled."
-}
-
-#############################
-# Btrfs Pre-flight
-#############################
-
-btrfs_preflight() {
- step "Checking Btrfs Requirements"
-
- # Check for btrfs-progs
- if ! command_exists mkfs.btrfs; then
- error "btrfs-progs not installed. Cannot create btrfs filesystem."
- fi
- info "btrfs-progs available."
-
- # Check for required tools
- require_command btrfs
- require_command grub-install
-
- info "Btrfs preflight checks passed."
-}
-
-#############################
-# Btrfs Volume Creation
-#############################
-
-# Create btrfs filesystem (single or multi-device)
-# Usage: create_btrfs_volume device1 [device2 ...] [--raid-level level]
-create_btrfs_volume() {
- local devices=()
- local raid_level=""
-
- # Parse arguments
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --raid-level)
- raid_level="$2"
- shift 2
- ;;
- *)
- devices+=("$1")
- shift
- ;;
- esac
- done
-
- step "Creating Btrfs Filesystem"
-
- local num_devices=${#devices[@]}
-
- if [[ $num_devices -eq 1 ]]; then
- # Single device
- info "Formatting ${devices[0]} as btrfs..."
- mkfs.btrfs -f -L "archroot" "${devices[0]}" || error "Failed to create btrfs filesystem"
- info "Btrfs filesystem created on ${devices[0]}"
- else
- # Multi-device RAID
- local data_profile="raid1"
- local meta_profile="raid1"
-
- case "$raid_level" in
- stripe)
- data_profile="raid0"
- meta_profile="raid1" # Always mirror metadata for safety
- info "Creating striped btrfs (RAID0 data, RAID1 metadata) with $num_devices devices..."
- ;;
- mirror)
- data_profile="raid1"
- meta_profile="raid1"
- info "Creating mirrored btrfs (RAID1) with $num_devices devices..."
- ;;
- *)
- # Default to mirror for safety
- data_profile="raid1"
- meta_profile="raid1"
- info "Creating mirrored btrfs (RAID1) with $num_devices devices..."
- ;;
- esac
-
- mkfs.btrfs -f -L "archroot" \
- -d "$data_profile" \
- -m "$meta_profile" \
- "${devices[@]}" || error "Failed to create btrfs filesystem"
-
- info "Btrfs $raid_level filesystem created on ${devices[*]}"
- fi
-}
-
-#############################
-# Subvolume Creation
-#############################
-
-create_btrfs_subvolumes() {
- local partition="$1"
-
- step "Creating Btrfs Subvolumes"
-
- # Mount the raw btrfs volume temporarily
- mount "$partition" /mnt || error "Failed to mount btrfs volume"
-
- # Create each subvolume
- for subvol_spec in "${BTRFS_SUBVOLS[@]}"; do
- IFS=':' read -r name mountpoint extra <<< "$subvol_spec"
- info "Creating subvolume: $name -> $mountpoint"
- btrfs subvolume create "/mnt/$name" || error "Failed to create subvolume $name"
- done
-
- # Unmount raw volume
- umount /mnt
-
- info "Created ${#BTRFS_SUBVOLS[@]} subvolumes."
-}
-
-#############################
-# Btrfs Mount Functions
-#############################
-
-mount_btrfs_subvolumes() {
- local partition="$1"
-
- step "Mounting Btrfs Subvolumes"
-
- # Mount root subvolume first
- info "Mounting @ -> /mnt"
- mount -o "subvol=@,$BTRFS_OPTS" "$partition" /mnt || error "Failed to mount root subvolume"
-
- # Create mount points and mount remaining subvolumes
- for subvol_spec in "${BTRFS_SUBVOLS[@]}"; do
- IFS=':' read -r name mountpoint extra <<< "$subvol_spec"
-
- # Skip root, already mounted
- [[ "$name" == "@" ]] && continue
-
- # Build mount options
- local opts="subvol=$name,$BTRFS_OPTS"
-
- # Apply extra options (override defaults where specified)
- if [[ -n "$extra" ]]; then
- # Handle compress=no by removing compress from opts and not adding it
- if [[ "$extra" == *"compress=no"* ]]; then
- opts=$(echo "$opts" | sed 's/,compress=zstd//')
- fi
- # Handle nodatacow
- if [[ "$extra" == *"nodatacow"* ]]; then
- opts="$opts,nodatacow"
- opts=$(echo "$opts" | sed 's/,compress=zstd//')
- fi
- # Handle nosuid,nodev for tmp
- if [[ "$extra" == *"nosuid"* ]]; then
- opts="$opts,nosuid,nodev"
- fi
- fi
-
- info "Mounting $name -> /mnt$mountpoint"
- mkdir -p "/mnt$mountpoint"
- mount -o "$opts" "$partition" "/mnt$mountpoint" || error "Failed to mount $name"
- done
-
- # Set permissions on tmp directories
- chmod 1777 /mnt/tmp /mnt/var/tmp
-
- info "All subvolumes mounted."
-}
-
-#############################
-# Fstab Generation
-#############################
-
-generate_btrfs_fstab() {
- local partition="$1"
- local efi_partition="$2"
-
- step "Generating fstab"
-
- local uuid
- uuid=$(blkid -s UUID -o value "$partition")
-
- # Start with header
- cat > /mnt/etc/fstab << EOF
-# /etc/fstab - Btrfs subvolume mounts
-# IMPORTANT: Using subvol= NOT subvolid= for snapshot compatibility
-# Generated by archangel installer
-
-EOF
-
- # Add each subvolume
- for subvol_spec in "${BTRFS_SUBVOLS[@]}"; do
- IFS=':' read -r name mountpoint extra <<< "$subvol_spec"
-
- # Build mount options
- local opts="subvol=$name,$BTRFS_OPTS"
-
- # Apply extra options
- if [[ -n "$extra" ]]; then
- if [[ "$extra" == *"compress=no"* ]]; then
- opts=$(echo "$opts" | sed 's/,compress=zstd//')
- fi
- if [[ "$extra" == *"nodatacow"* ]]; then
- opts="$opts,nodatacow"
- opts=$(echo "$opts" | sed 's/,compress=zstd//')
- fi
- if [[ "$extra" == *"nosuid"* ]]; then
- opts="$opts,nosuid,nodev"
- fi
- fi
-
- echo "UUID=$uuid $mountpoint btrfs $opts 0 0" >> /mnt/etc/fstab
- done
-
- # Add EFI partition
- local efi_uuid
- efi_uuid=$(blkid -s UUID -o value "$efi_partition")
- echo "" >> /mnt/etc/fstab
- echo "# EFI System Partition" >> /mnt/etc/fstab
- echo "UUID=$efi_uuid /efi vfat defaults,noatime 0 2" >> /mnt/etc/fstab
-
- info "fstab generated with ${#BTRFS_SUBVOLS[@]} btrfs mounts + EFI"
-}
-
-#############################
-# Snapper Configuration
-#############################
-
-configure_snapper() {
- step "Configuring Snapper"
-
- # Snapper needs D-Bus which isn't available in chroot
- # Create a firstboot service to properly initialize snapper
-
- info "Creating snapper firstboot configuration..."
-
- # Create the firstboot script using echo (more reliable than HEREDOC)
- {
- echo '#!/bin/bash'
- echo '# Snapper firstboot configuration'
- echo 'set -e'
- echo ''
- echo '# Check if snapper is already configured'
- echo 'if snapper list-configs 2>/dev/null | grep -q "^root"; then'
- echo ' exit 0'
- echo 'fi'
- echo ''
- echo 'echo "Configuring snapper for btrfs root..."'
- echo ''
- echo '# Unmount the pre-created @snapshots'
- echo 'umount /.snapshots 2>/dev/null || true'
- echo 'rmdir /.snapshots 2>/dev/null || true'
- echo ''
- echo '# Let snapper create its config'
- echo 'snapper -c root create-config /'
- echo ''
- echo '# Replace snapper .snapshots with our @snapshots'
- echo 'btrfs subvolume delete /.snapshots'
- echo 'mkdir /.snapshots'
- echo 'ROOT_DEV=$(findmnt -n -o SOURCE / | sed "s/\[.*\]//")'
- echo 'mount -o subvol=@snapshots "$ROOT_DEV" /.snapshots'
- echo 'chmod 750 /.snapshots'
- echo ''
- echo '# Configure timeline'
- echo 'snapper -c root set-config "TIMELINE_CREATE=yes"'
- echo 'snapper -c root set-config "TIMELINE_CLEANUP=yes"'
- echo 'snapper -c root set-config "TIMELINE_LIMIT_HOURLY=6"'
- echo 'snapper -c root set-config "TIMELINE_LIMIT_DAILY=7"'
- echo 'snapper -c root set-config "TIMELINE_LIMIT_WEEKLY=2"'
- echo 'snapper -c root set-config "TIMELINE_LIMIT_MONTHLY=1"'
- echo 'snapper -c root set-config "NUMBER_LIMIT=50"'
- echo ''
- echo '# Create genesis snapshot'
- echo 'snapper -c root create -d "genesis"'
- echo ''
- echo '# Update GRUB (config on EFI partition)'
- echo 'grub-mkconfig -o /efi/grub/grub.cfg'
- echo ''
- echo 'echo "Snapper configuration complete!"'
- } > /mnt/usr/local/bin/snapper-firstboot
- chmod +x /mnt/usr/local/bin/snapper-firstboot
-
- # Create systemd service for firstboot
- {
- echo '[Unit]'
- echo 'Description=Snapper First Boot Configuration'
- echo 'After=local-fs.target dbus.service'
- echo 'Wants=dbus.service'
- echo 'ConditionPathExists=!/etc/snapper/.firstboot-done'
- echo ''
- echo '[Service]'
- echo 'Type=oneshot'
- echo 'ExecStart=/usr/local/bin/snapper-firstboot'
- echo 'ExecStartPost=/usr/bin/touch /etc/snapper/.firstboot-done'
- echo 'RemainAfterExit=yes'
- echo ''
- echo '[Install]'
- echo 'WantedBy=multi-user.target'
- } > /mnt/etc/systemd/system/snapper-firstboot.service
-
- # Enable the firstboot service
- arch-chroot /mnt systemctl enable snapper-firstboot.service
-
- # Enable snapper timers
- arch-chroot /mnt systemctl enable snapper-timeline.timer
- arch-chroot /mnt systemctl enable snapper-cleanup.timer
-
- info "Snapper firstboot service configured."
- info "Snapper will be fully configured on first boot."
-}
-
-#############################
-# GRUB Configuration
-#############################
-
-configure_grub() {
- local efi_partition="$1"
-
- step "Configuring GRUB Bootloader"
-
- # Mount EFI partition
- mkdir -p /mnt/efi
- mount "$efi_partition" /mnt/efi
-
- # Configure GRUB defaults for btrfs
- info "Setting GRUB configuration..."
- cat > /mnt/etc/default/grub << 'EOF'
-# GRUB configuration for btrfs root with snapshots
-GRUB_DEFAULT=0
-GRUB_TIMEOUT=5
-GRUB_DISTRIBUTOR="Arch"
-GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3"
-GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200"
-
-# Serial console support (for headless/VM testing)
-GRUB_TERMINAL="console serial"
-GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"
-
-# Disable os-prober (single-boot system)
-GRUB_DISABLE_OS_PROBER=true
-
-# Btrfs: tell GRUB where to find /boot within subvolume
-GRUB_BTRFS_OVERRIDE_BOOT_PARTITION_DETECTION=true
-EOF
-
- # Add LUKS encryption settings if enabled
- if [[ "$NO_ENCRYPT" != "yes" && -n "$LUKS_PASSPHRASE" ]]; then
- echo "" >> /mnt/etc/default/grub
- echo "# LUKS encryption support" >> /mnt/etc/default/grub
- echo "GRUB_ENABLE_CRYPTODISK=y" >> /mnt/etc/default/grub
-
- # For multi-disk LUKS, sd-encrypt reads crypttab.initramfs — no cmdline params needed
- # For single-disk LUKS, the encrypt hook needs cryptdevice= on the cmdline
- local num_luks_disks
- num_luks_disks=$(echo "$DISKS" | tr ',' '\n' | wc -l)
-
- if [[ $num_luks_disks -eq 1 ]]; then
- local luks_part
- luks_part=$(echo "$DISKS" | cut -d',' -f1)2
- if [[ -b "$luks_part" ]]; then
- local uuid
- uuid=$(blkid -s UUID -o value "$luks_part")
- local cryptkey_param=""
- if [[ "${TESTING:-}" == "yes" ]]; then
- cryptkey_param="cryptkey=rootfs:$LUKS_KEYFILE "
- info "Testing mode: adding cryptkey parameter for automated unlock"
- fi
- sed -i "s|^GRUB_CMDLINE_LINUX=\"|GRUB_CMDLINE_LINUX=\"cryptdevice=UUID=$uuid:$LUKS_MAPPER_NAME:allow-discards ${cryptkey_param}|" \
- /mnt/etc/default/grub
- info "Added cryptdevice parameter for LUKS partition."
- fi
- else
- info "Multi-disk LUKS: sd-encrypt reads crypttab.initramfs (no cryptdevice cmdline needed)"
- fi
- fi
-
- # Create grub directory on EFI partition
- # GRUB modules on FAT32 EFI partition avoid btrfs subvolume path issues
- mkdir -p /mnt/efi/grub
-
- # Install GRUB with boot-directory on EFI partition
- info "Installing GRUB to EFI partition..."
- arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/efi \
- --bootloader-id=GRUB --boot-directory=/efi \
- || error "GRUB installation failed"
-
- # Create symlink BEFORE grub-mkconfig (grub-btrfs expects /boot/grub)
- rm -rf /mnt/boot/grub 2>/dev/null || true
- arch-chroot /mnt ln -sfn /efi/grub /boot/grub
-
- # Generate GRUB config (uses /boot/grub symlink -> /efi/grub)
- info "Generating GRUB configuration..."
- arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg \
- || error "Failed to generate GRUB config"
-
- # Sync to ensure grub.cfg is written to FAT32 EFI partition
- sync
-
- # Enable grub-btrfsd for automatic snapshot menu updates
- info "Enabling grub-btrfs daemon..."
- arch-chroot /mnt systemctl enable grub-btrfsd
-
- info "GRUB configured with btrfs snapshot support."
-}
-
-#############################
-# EFI Redundancy (Multi-disk)
-#############################
-
-# Install GRUB to all EFI partitions for redundancy
-install_grub_all_efi() {
- local efi_partitions=("$@")
-
- step "Installing GRUB to All EFI Partitions"
-
- local i=1
- for efi_part in "${efi_partitions[@]}"; do
- # First EFI at /efi (already mounted), subsequent at /efi2, /efi3, etc.
- local chroot_efi_dir="/efi"
- local mount_point="/mnt/efi"
- local bootloader_id="GRUB"
-
- if [[ $i -gt 1 ]]; then
- chroot_efi_dir="/efi${i}"
- mount_point="/mnt/efi${i}"
- bootloader_id="GRUB-disk${i}"
-
- # Mount secondary EFI partitions
- if ! mountpoint -q "$mount_point" 2>/dev/null; then
- mkdir -p "$mount_point"
- mount "$efi_part" "$mount_point" || { warn "Failed to mount $efi_part"; ((++i)); continue; }
- # Also create the directory in chroot for grub-install
- mkdir -p "/mnt${chroot_efi_dir}"
- mount --bind "$mount_point" "/mnt${chroot_efi_dir}"
- fi
- fi
-
- info "Installing GRUB to $efi_part ($bootloader_id)..."
- arch-chroot /mnt grub-install --target=x86_64-efi \
- --efi-directory="$chroot_efi_dir" \
- --bootloader-id="$bootloader_id" \
- --boot-directory=/efi \
- || warn "GRUB install to $efi_part may have failed (continuing)"
-
- ((++i))
- done
-
- info "GRUB installed to ${#efi_partitions[@]} EFI partition(s)."
-}
-
-# Create pacman hook to sync GRUB across all EFI partitions
-create_grub_sync_hook() {
- local efi_partitions=("$@")
-
- step "Creating GRUB Sync Hook"
-
- # Only needed for multi-disk
- if [[ ${#efi_partitions[@]} -lt 2 ]]; then
- info "Single disk - no sync hook needed."
- return
- fi
-
- # Create sync script
- local script_content='#!/bin/bash
-# Sync GRUB to all EFI partitions after grub package update
-# Generated by archangel installer
-
-set -e
-
-EFI_PARTITIONS=('
- for part in "${efi_partitions[@]}"; do
- script_content+="\"$part\" "
- done
- script_content+=')
-
-PRIMARY_EFI="/efi"
-
-sync_grub() {
- local i=0
- for part in "${EFI_PARTITIONS[@]}"; do
- if [[ $i -eq 0 ]]; then
- # Primary - just reinstall GRUB
- grub-install --target=x86_64-efi --efi-directory="$PRIMARY_EFI" \
- --bootloader-id=GRUB --boot-directory=/efi 2>/dev/null || true
- else
- # Secondary - mount, install, unmount
- local mount_point="/tmp/efi-sync-$i"
- mkdir -p "$mount_point"
- mount "$part" "$mount_point" 2>/dev/null || continue
- grub-install --target=x86_64-efi --efi-directory="$mount_point" \
- --bootloader-id="GRUB-disk$((i+1))" --boot-directory=/efi 2>/dev/null || true
- umount "$mount_point" 2>/dev/null || true
- rmdir "$mount_point" 2>/dev/null || true
- fi
- ((++i))
- done
-}
-
-sync_grub
-'
- echo "$script_content" > /mnt/usr/local/bin/grub-sync-efi
- chmod +x /mnt/usr/local/bin/grub-sync-efi
-
- # Create pacman hook
- mkdir -p /mnt/etc/pacman.d/hooks
- cat > /mnt/etc/pacman.d/hooks/99-grub-sync-efi.hook << 'HOOKEOF'
-[Trigger]
-Type = Package
-Operation = Upgrade
-Target = grub
-
-[Action]
-Description = Syncing GRUB to all EFI partitions...
-When = PostTransaction
-Exec = /usr/local/bin/grub-sync-efi
-HOOKEOF
-
- info "GRUB sync hook created for ${#efi_partitions[@]} EFI partitions."
-}
-
-#############################
-# Pacman Snapshot Hook
-#############################
-
-configure_btrfs_pacman_hook() {
- step "Configuring Pacman Snapshot Hook"
-
- # snap-pac handles this automatically when installed
- # Just verify it's set up
- info "snap-pac will create pre/post snapshots for pacman transactions."
- info "Snapshots visible in GRUB menu via grub-btrfs."
-}
-
-#############################
-# Genesis Snapshot
-#############################
-
-create_btrfs_genesis_snapshot() {
- step "Creating Genesis Snapshot"
-
- # Genesis snapshot will be created by snapper-firstboot service on first boot
- # This ensures snapper is properly configured before creating snapshots
-
- info "Genesis snapshot will be created on first boot."
- info "The snapper-firstboot service handles this automatically."
-}
-
-#############################
-# Btrfs Services
-#############################
-
-configure_btrfs_services() {
- step "Configuring System Services"
-
- # Enable standard services
- arch-chroot /mnt systemctl enable NetworkManager
- arch-chroot /mnt systemctl enable avahi-daemon
-
- # Snapper timers (already enabled in configure_snapper)
-
- # grub-btrfsd (already enabled in configure_grub)
-
- info "System services configured."
-}
-
-#############################
-# Btrfs Initramfs
-#############################
-
-configure_btrfs_initramfs() {
- step "Configuring Initramfs for Btrfs"
-
- # Backup original
- cp /mnt/etc/mkinitcpio.conf /mnt/etc/mkinitcpio.conf.bak
-
- # Remove archiso drop-in if present
- if [[ -f /mnt/etc/mkinitcpio.conf.d/archiso.conf ]]; then
- info "Removing archiso drop-in config..."
- rm -f /mnt/etc/mkinitcpio.conf.d/archiso.conf
- fi
-
- # Create proper linux-lts preset
- info "Creating linux-lts preset..."
- cat > /mnt/etc/mkinitcpio.d/linux-lts.preset << 'EOF'
-# mkinitcpio preset file for linux-lts
-
-PRESETS=(default fallback)
-
-ALL_kver="/boot/vmlinuz-linux-lts"
-
-default_image="/boot/initramfs-linux-lts.img"
-
-fallback_image="/boot/initramfs-linux-lts-fallback.img"
-fallback_options="-S autodetect"
-EOF
-
- # Configure hooks for btrfs
- # Include encrypt hook if LUKS is enabled, btrfs hook if multi-device
- local num_disks=${#SELECTED_DISKS[@]}
- local luks_enabled="no"
- [[ "$NO_ENCRYPT" != "yes" && -n "$LUKS_PASSPHRASE" ]] && luks_enabled="yes"
-
- if [[ $num_disks -gt 1 && "$luks_enabled" == "yes" ]]; then
- # Multi-disk LUKS: use sd-encrypt (reads crypttab.initramfs to open ALL devices)
- # The traditional encrypt hook only supports a single cryptdevice
- info "Multi-device LUKS: using sd-encrypt for multi-device LUKS unlock"
- sed -i "s/^HOOKS=.*/HOOKS=(base systemd microcode modconf kms keyboard sd-vconsole block sd-encrypt btrfs filesystems fsck)/" \
- /mnt/etc/mkinitcpio.conf
- elif [[ $num_disks -gt 1 ]]; then
- info "Multi-device btrfs: adding btrfs hook for device assembly"
- sed -i "s/^HOOKS=.*/HOOKS=(base udev microcode modconf kms keyboard keymap consolefont block btrfs filesystems fsck)/" \
- /mnt/etc/mkinitcpio.conf
- elif [[ "$luks_enabled" == "yes" ]]; then
- sed -i "s/^HOOKS=.*/HOOKS=(base udev microcode modconf kms keyboard keymap consolefont block encrypt filesystems fsck)/" \
- /mnt/etc/mkinitcpio.conf
- else
- sed -i "s/^HOOKS=.*/HOOKS=(base udev microcode modconf kms keyboard keymap consolefont block filesystems fsck)/" \
- /mnt/etc/mkinitcpio.conf
- fi
-
- # Regenerate initramfs
- info "Regenerating initramfs..."
- arch-chroot /mnt mkinitcpio -P
-
- info "Initramfs configured for btrfs."
-}
-
-#############################
-# Btrfs Cleanup
-#############################
-
-btrfs_cleanup() {
- step "Cleaning Up Btrfs"
-
- # Unmount in reverse order
- info "Unmounting subvolumes..."
-
- # Sync all filesystems before unmounting (important for FAT32 EFI partition)
- sync
-
- # Unmount EFI first
- umount /mnt/efi 2>/dev/null || true
-
- # Unmount all btrfs subvolumes (reverse order)
- for ((i=${#BTRFS_SUBVOLS[@]}-1; i>=0; i--)); do
- IFS=':' read -r name mountpoint extra <<< "${BTRFS_SUBVOLS[$i]}"
- [[ "$name" == "@" ]] && continue
- umount "/mnt$mountpoint" 2>/dev/null || true
- done
-
- # Unmount root last
- umount /mnt 2>/dev/null || true
-
- info "Btrfs cleanup complete."
-}
diff --git a/custom/lib/common.sh b/custom/lib/common.sh
deleted file mode 100644
index 0f02e37..0000000
--- a/custom/lib/common.sh
+++ /dev/null
@@ -1,173 +0,0 @@
-#!/usr/bin/env bash
-# common.sh - Shared functions for archangel installer
-# Source this file: source "$(dirname "$0")/lib/common.sh"
-
-#############################
-# Output Functions
-#############################
-
-# Colors (optional, gracefully degrade if not supported)
-if [[ -t 1 ]]; then
- RED='\033[0;31m'
- GREEN='\033[0;32m'
- YELLOW='\033[0;33m'
- BLUE='\033[0;34m'
- BOLD='\033[1m'
- NC='\033[0m' # No Color
-else
- RED=''
- GREEN=''
- YELLOW=''
- BLUE=''
- BOLD=''
- NC=''
-fi
-
-info() { echo -e "${GREEN}[INFO]${NC} $1"; }
-warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
-error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
-step() { echo ""; echo -e "${BOLD}==> $1${NC}"; }
-prompt() { echo -e "${BLUE}$1${NC}"; }
-
-# Log to file if LOG_FILE is set
-log() {
- local msg
- msg="[$(date +'%Y-%m-%d %H:%M:%S')] $1"
- if [[ -n "$LOG_FILE" ]]; then
- echo "$msg" >> "$LOG_FILE"
- fi
-}
-
-#############################
-# Validation Functions
-#############################
-
-require_root() {
- if [[ $EUID -ne 0 ]]; then
- error "This script must be run as root"
- fi
-}
-
-command_exists() {
- command -v "$1" &>/dev/null
-}
-
-require_command() {
- command_exists "$1" || error "Required command not found: $1"
-}
-
-#############################
-# FZF Prompts
-#############################
-
-# Check if fzf is available
-has_fzf() {
- command_exists fzf
-}
-
-# Generic fzf selection
-# Usage: result=$(fzf_select "prompt" "option1" "option2" ...)
-fzf_select() {
- local prompt="$1"
- shift
- local options=("$@")
-
- if has_fzf; then
- printf '%s\n' "${options[@]}" | fzf --prompt="$prompt " --height=15 --reverse
- else
- # Fallback to simple select
- PS3="$prompt "
- select opt in "${options[@]}"; do
- if [[ -n "$opt" ]]; then
- echo "$opt"
- break
- fi
- done
- fi
-}
-
-# Multi-select with fzf
-# Usage: readarray -t results < <(fzf_multi "prompt" "opt1" "opt2" ...)
-fzf_multi() {
- local prompt="$1"
- shift
- local options=("$@")
-
- if has_fzf; then
- printf '%s\n' "${options[@]}" | fzf --prompt="$prompt " --height=20 --reverse --multi
- else
- # Fallback: just return all options (user must edit)
- printf '%s\n' "${options[@]}"
- fi
-}
-
-#############################
-# Filesystem Selection
-#############################
-
-# Select filesystem type (ZFS or Btrfs)
-# Sets global FILESYSTEM variable
-select_filesystem() {
- step "Select Filesystem"
-
- local options=(
- "ZFS - Built-in encryption, best data integrity (recommended)"
- "Btrfs - Copy-on-write, LUKS encryption, GRUB snapshot boot"
- )
-
- local selected
- selected=$(fzf_select "Filesystem:" "${options[@]}")
-
- case "$selected" in
- ZFS*)
- FILESYSTEM="zfs"
- info "Selected: ZFS"
- ;;
- Btrfs*)
- FILESYSTEM="btrfs"
- info "Selected: Btrfs"
- ;;
- *)
- error "No filesystem selected"
- ;;
- esac
-}
-
-#############################
-# Disk Utilities
-#############################
-
-# Get disk size in human-readable format
-get_disk_size() {
- local disk="$1"
- lsblk -dno SIZE "$disk" 2>/dev/null | tr -d ' '
-}
-
-# Get disk model
-get_disk_model() {
- local disk="$1"
- lsblk -dno MODEL "$disk" 2>/dev/null | tr -d ' ' | head -c 20
-}
-
-# Check if disk is in use (mounted or has holders)
-disk_in_use() {
- local disk="$1"
- [[ -n "$(lsblk -no MOUNTPOINT "$disk" 2>/dev/null | grep -v '^$')" ]] && return 0
- [[ -n "$(ls /sys/block/"$(basename "$disk")"/holders/ 2>/dev/null)" ]] && return 0
- return 1
-}
-
-# List available disks (not in use)
-list_available_disks() {
- local disks=()
- for disk in /dev/nvme[0-9]n[0-9] /dev/sd[a-z] /dev/vd[a-z]; do
- [[ -b "$disk" ]] || continue
- disk_in_use "$disk" && continue
- local size
- size=$(get_disk_size "$disk")
- local model
- model=$(get_disk_model "$disk")
- disks+=("$disk ($size, $model)")
- done
- printf '%s\n' "${disks[@]}"
-}
diff --git a/custom/lib/config.sh b/custom/lib/config.sh
deleted file mode 100644
index 358a5f4..0000000
--- a/custom/lib/config.sh
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/usr/bin/env bash
-# config.sh - Configuration and argument handling for archangel installer
-# Source this file after common.sh
-
-#############################
-# Global Config Variables
-#############################
-
-CONFIG_FILE=""
-UNATTENDED=false
-
-# These get populated by config file or interactive prompts
-FILESYSTEM="" # "zfs" or "btrfs"
-HOSTNAME=""
-TIMEZONE=""
-LOCALE=""
-KEYMAP=""
-SELECTED_DISKS=()
-RAID_LEVEL=""
-WIFI_SSID=""
-WIFI_PASSWORD=""
-ENCRYPTION_ENABLED=false
-ZFS_PASSPHRASE=""
-LUKS_PASSPHRASE=""
-ROOT_PASSWORD=""
-SSH_ENABLED=false
-SSH_KEY=""
-
-#############################
-# Argument Parsing
-#############################
-
-parse_args() {
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --config-file)
- if [[ -n "$2" && ! "$2" =~ ^- ]]; then
- CONFIG_FILE="$2"
- shift 2
- else
- error "--config-file requires a path argument"
- fi
- ;;
- --help|-h)
- show_usage
- exit 0
- ;;
- *)
- error "Unknown option: $1 (use --help for usage)"
- ;;
- esac
- done
-}
-
-show_usage() {
- cat <<EOF
-Usage: archangel [OPTIONS]
-
-Arch Linux installer with ZFS/Btrfs support and snapshot management.
-
-Options:
- --config-file PATH Use config file for unattended installation
- --help, -h Show this help message
-
-Without --config-file, runs in interactive mode.
-See /root/archangel.conf.example for a config template.
-EOF
-}
-
-#############################
-# Config File Loading
-#############################
-
-load_config() {
- local config_path="$1"
-
- if [[ ! -f "$config_path" ]]; then
- error "Config file not found: $config_path"
- fi
-
- info "Loading config from: $config_path"
-
- # Source the config file (it's just key=value pairs)
- # shellcheck disable=SC1090
- source "$config_path"
-
- # Convert DISKS from comma-separated string to array
- if [[ -n "$DISKS" ]]; then
- IFS=',' read -ra SELECTED_DISKS <<< "$DISKS"
- fi
-
- UNATTENDED=true
- info "Running in unattended mode"
-}
-
-check_config() {
- # Only use config when explicitly specified with --config-file
- # This prevents accidental disk destruction from an unnoticed config file
- if [[ -n "$CONFIG_FILE" ]]; then
- load_config "$CONFIG_FILE"
- fi
-}
-
-#############################
-# Config Validation
-#############################
-
-validate_config() {
- local errors=0
-
- [[ -z "$HOSTNAME" ]] && { warn "HOSTNAME not set"; ((errors++)); }
- [[ -z "$TIMEZONE" ]] && { warn "TIMEZONE not set"; ((errors++)); }
- [[ ${#SELECTED_DISKS[@]} -eq 0 ]] && { warn "No disks selected"; ((errors++)); }
- [[ -z "$ROOT_PASSWORD" ]] && { warn "ROOT_PASSWORD not set"; ((errors++)); }
-
- # Validate disks exist
- for disk in "${SELECTED_DISKS[@]}"; do
- [[ -b "$disk" ]] || { warn "Disk not found: $disk"; ((errors++)); }
- done
-
- # Validate timezone
- if [[ -n "$TIMEZONE" && ! -f "/usr/share/zoneinfo/$TIMEZONE" ]]; then
- warn "Invalid timezone: $TIMEZONE"
- ((errors++))
- fi
-
- if [[ $errors -gt 0 ]]; then
- error "Config validation failed with $errors error(s)"
- fi
- info "Config validation passed"
-}
diff --git a/custom/lib/disk.sh b/custom/lib/disk.sh
deleted file mode 100644
index 2e7deb3..0000000
--- a/custom/lib/disk.sh
+++ /dev/null
@@ -1,204 +0,0 @@
-#!/usr/bin/env bash
-# disk.sh - Disk partitioning functions for archangel installer
-# Source this file after common.sh
-
-#############################
-# Partition Disks
-#############################
-
-# Partition a single disk for ZFS/Btrfs installation
-# Creates: EFI partition (512M) + root partition (rest)
-# Uses global FILESYSTEM variable to determine partition type
-partition_disk() {
- local disk="$1"
- local efi_size="${2:-512M}"
-
- # Determine root partition type based on filesystem
- local root_type="BF00" # ZFS (Solaris root)
- if [[ "$FILESYSTEM" == "btrfs" ]]; then
- root_type="8300" # Linux filesystem
- fi
-
- info "Partitioning $disk..."
-
- # Wipe existing partition table
- sgdisk --zap-all "$disk" || error "Failed to wipe $disk"
-
- # Create EFI partition (512M, type EF00)
- sgdisk -n 1:0:+${efi_size} -t 1:EF00 -c 1:"EFI" "$disk" || error "Failed to create EFI partition on $disk"
-
- # Create root partition (rest of disk)
- sgdisk -n 2:0:0 -t 2:$root_type -c 2:"ROOT" "$disk" || error "Failed to create root partition on $disk"
-
- # Notify kernel of partition changes
- partprobe "$disk" 2>/dev/null || true
- sleep 1
-
- info "Partitioned $disk: EFI=${efi_size}, ROOT=remainder"
-}
-
-# Partition multiple disks (for RAID configurations)
-partition_disks() {
- local efi_size="${1:-512M}"
- shift
- local disks=("$@")
-
- for disk in "${disks[@]}"; do
- partition_disk "$disk" "$efi_size"
- done
-}
-
-#############################
-# Partition Helpers
-#############################
-
-# Get EFI partition path for a disk
-get_efi_partition() {
- local disk="$1"
- if [[ "$disk" =~ nvme ]]; then
- echo "${disk}p1"
- else
- echo "${disk}1"
- fi
-}
-
-# Get root partition path for a disk
-get_root_partition() {
- local disk="$1"
- if [[ "$disk" =~ nvme ]]; then
- echo "${disk}p2"
- else
- echo "${disk}2"
- fi
-}
-
-# Get all root partitions from disk array
-get_root_partitions() {
- local disks=("$@")
- local parts=()
- for disk in "${disks[@]}"; do
- parts+=("$(get_root_partition "$disk")")
- done
- printf '%s\n' "${parts[@]}"
-}
-
-# Get all EFI partitions from disk array
-get_efi_partitions() {
- local disks=("$@")
- local parts=()
- for disk in "${disks[@]}"; do
- parts+=("$(get_efi_partition "$disk")")
- done
- printf '%s\n' "${parts[@]}"
-}
-
-#############################
-# EFI Partition Management
-#############################
-
-# Format EFI partition
-format_efi() {
- local partition="$1"
- local label="${2:-EFI}"
-
- info "Formatting EFI partition: $partition"
- mkfs.fat -F32 -n "$label" "$partition" || error "Failed to format EFI: $partition"
-}
-
-# Format all EFI partitions
-format_efi_partitions() {
- local disks=("$@")
- local first=true
-
- for disk in "${disks[@]}"; do
- local efi
- efi=$(get_efi_partition "$disk")
- if $first; then
- format_efi "$efi" "EFI"
- first=false
- else
- format_efi "$efi" "EFI2"
- fi
- done
-}
-
-# Mount EFI partition
-mount_efi() {
- local partition="$1"
- local mount_point="${2:-/mnt/efi}"
-
- mkdir -p "$mount_point"
- mount "$partition" "$mount_point" || error "Failed to mount EFI at $mount_point"
- info "Mounted EFI: $partition -> $mount_point"
-}
-
-#############################
-# Disk Selection (Interactive)
-#############################
-
-# Interactive disk selection using fzf
-select_disks() {
- local available
- available=$(list_available_disks)
-
- if [[ -z "$available" ]]; then
- error "No available disks found"
- fi
-
- step "Select installation disk(s)"
- prompt "Use Tab to select multiple disks for RAID, Enter to confirm"
-
- local selected
- if has_fzf; then
- selected=$(echo "$available" | fzf --multi --prompt="Select disk(s): " --height=15 --reverse)
- else
- echo "$available"
- read -rp "Enter disk path(s) separated by space: " selected
- fi
-
- if [[ -z "$selected" ]]; then
- error "No disk selected"
- fi
-
- # Extract just the device paths (remove size/model info)
- SELECTED_DISKS=()
- while IFS= read -r line; do
- local disk
- disk=$(echo "$line" | cut -d' ' -f1)
- SELECTED_DISKS+=("$disk")
- done <<< "$selected"
-
- info "Selected disks: ${SELECTED_DISKS[*]}"
-}
-
-#############################
-# RAID Level Selection
-#############################
-
-select_raid_level() {
- local num_disks=${#SELECTED_DISKS[@]}
-
- if [[ $num_disks -eq 1 ]]; then
- RAID_LEVEL=""
- info "Single disk - no RAID"
- return
- fi
-
- step "Select RAID level"
-
- local options=()
- options+=("mirror - Mirror data across disks (recommended)")
-
- if [[ $num_disks -ge 3 ]]; then
- options+=("raidz1 - Single parity, lose 1 disk capacity")
- fi
- if [[ $num_disks -ge 4 ]]; then
- options+=("raidz2 - Double parity, lose 2 disks capacity")
- fi
-
- local selected
- selected=$(fzf_select "RAID level:" "${options[@]}")
- RAID_LEVEL=$(echo "$selected" | cut -d' ' -f1)
-
- info "Selected RAID level: $RAID_LEVEL"
-}
diff --git a/custom/lib/zfs.sh b/custom/lib/zfs.sh
deleted file mode 100644
index feda91d..0000000
--- a/custom/lib/zfs.sh
+++ /dev/null
@@ -1,359 +0,0 @@
-#!/usr/bin/env bash
-# zfs.sh - ZFS-specific functions for archangel installer
-# Source this file after common.sh, config.sh, disk.sh
-
-#############################
-# ZFS Constants
-#############################
-
-POOL_NAME="${POOL_NAME:-zroot}"
-ASHIFT="${ASHIFT:-12}"
-COMPRESSION="${COMPRESSION:-zstd}"
-
-#############################
-# ZFS Pre-flight
-#############################
-
-zfs_preflight() {
- # Check ZFS module
- if ! lsmod | grep -q zfs; then
- info "Loading ZFS module..."
- modprobe zfs || error "Failed to load ZFS module. Is zfs-linux-lts installed?"
- fi
- info "ZFS module loaded successfully."
-}
-
-#############################
-# ZFS Pool Creation
-#############################
-
-create_zfs_pool() {
- local encryption="${1:-true}"
- local passphrase="$2"
-
- step "Creating ZFS Pool"
-
- # Destroy existing pool if present
- if zpool list "$POOL_NAME" &>/dev/null; then
- warn "Pool $POOL_NAME already exists. Destroying..."
- zpool destroy -f "$POOL_NAME"
- fi
-
- # Get root partitions
- local zfs_parts=()
- for disk in "${SELECTED_DISKS[@]}"; do
- zfs_parts+=("$(get_root_partition "$disk")")
- done
-
- # Build pool configuration based on RAID level
- local pool_config
- if [[ "$RAID_LEVEL" == "stripe" ]]; then
- pool_config="${zfs_parts[*]}"
- info "Creating striped pool with ${#zfs_parts[@]} disks (NO redundancy)..."
- warn "Data loss will occur if ANY disk fails!"
- elif [[ -n "$RAID_LEVEL" ]]; then
- pool_config="$RAID_LEVEL ${zfs_parts[*]}"
- info "Creating $RAID_LEVEL pool with ${#zfs_parts[@]} disks..."
- else
- pool_config="${zfs_parts[0]}"
- info "Creating single-disk pool..."
- fi
-
- # Base pool options
- local pool_opts=(
- -f
- -o ashift="$ASHIFT"
- -o autotrim=on
- -O acltype=posixacl
- -O atime=off
- -O canmount=off
- -O compression="$COMPRESSION"
- -O dnodesize=auto
- -O normalization=formD
- -O relatime=on
- -O xattr=sa
- -O mountpoint=none
- -R /mnt
- )
-
- # Create pool (with or without encryption)
- if [[ "$encryption" == "false" ]]; then
- warn "Creating pool WITHOUT encryption"
- zpool create "${pool_opts[@]}" "$POOL_NAME" $pool_config
- else
- info "Creating encrypted pool..."
- echo "$passphrase" | zpool create "${pool_opts[@]}" \
- -O encryption=aes-256-gcm \
- -O keyformat=passphrase \
- -O keylocation=prompt \
- "$POOL_NAME" $pool_config
- fi
-
- info "ZFS pool created successfully."
- zpool status "$POOL_NAME"
-}
-
-#############################
-# ZFS Dataset Creation
-#############################
-
-create_zfs_datasets() {
- step "Creating ZFS Datasets"
-
- # Root dataset container
- zfs create -o mountpoint=none -o canmount=off "$POOL_NAME/ROOT"
-
- # Calculate reservation (20% of pool, capped 5-20G)
- local pool_size_bytes
- pool_size_bytes=$(zpool get -Hp size "$POOL_NAME" | awk '{print $3}')
- local pool_size_gb=$((pool_size_bytes / 1024 / 1024 / 1024))
- local reserve_gb=$((pool_size_gb / 5))
- [[ $reserve_gb -gt 20 ]] && reserve_gb=20
- [[ $reserve_gb -lt 5 ]] && reserve_gb=5
-
- # Main root filesystem
- zfs create -o mountpoint=/ -o canmount=noauto -o reservation=${reserve_gb}G "$POOL_NAME/ROOT/default"
- zfs mount "$POOL_NAME/ROOT/default"
-
- # Home
- zfs create -o mountpoint=/home "$POOL_NAME/home"
- zfs create -o mountpoint=/root "$POOL_NAME/home/root"
-
- # Media - compression off for already-compressed files
- zfs create -o mountpoint=/media -o compression=off "$POOL_NAME/media"
-
- # VMs - 64K recordsize for VM disk images
- zfs create -o mountpoint=/vms -o recordsize=64K "$POOL_NAME/vms"
-
- # Var datasets
- zfs create -o mountpoint=/var -o canmount=off "$POOL_NAME/var"
- zfs create -o mountpoint=/var/log "$POOL_NAME/var/log"
- zfs create -o mountpoint=/var/cache "$POOL_NAME/var/cache"
- zfs create -o mountpoint=/var/lib -o canmount=off "$POOL_NAME/var/lib"
- zfs create -o mountpoint=/var/lib/pacman "$POOL_NAME/var/lib/pacman"
- zfs create -o mountpoint=/var/lib/docker "$POOL_NAME/var/lib/docker"
-
- # Temp directories - excluded from snapshots
- zfs create -o mountpoint=/var/tmp -o com.sun:auto-snapshot=false "$POOL_NAME/var/tmp"
- zfs create -o mountpoint=/tmp -o com.sun:auto-snapshot=false "$POOL_NAME/tmp"
- chmod 1777 /mnt/tmp /mnt/var/tmp
-
- info "Datasets created:"
- zfs list -r "$POOL_NAME" -o name,mountpoint,compression
-}
-
-#############################
-# ZFSBootMenu Configuration
-#############################
-
-configure_zfsbootmenu() {
- step "Configuring ZFSBootMenu"
-
- # Ensure hostid exists
- if [[ ! -f /etc/hostid ]]; then
- zgenhostid
- fi
- local host_id
- host_id=$(hostid)
-
- # Copy hostid to installed system
- cp /etc/hostid /mnt/etc/hostid
-
- # Create ZFSBootMenu directory on EFI
- mkdir -p /mnt/efi/EFI/ZBM
-
- # Download ZFSBootMenu release EFI binary
- info "Downloading ZFSBootMenu..."
- local zbm_url="https://get.zfsbootmenu.org/efi"
- if ! curl -fsSL -o /mnt/efi/EFI/ZBM/zfsbootmenu.efi "$zbm_url"; then
- error "Failed to download ZFSBootMenu"
- fi
- info "ZFSBootMenu binary installed."
-
- # Set kernel command line on the ROOT PARENT dataset
- local cmdline="rw loglevel=3"
-
- # Add AMD GPU workarounds if needed
- if lspci | grep -qi "amd.*display\|amd.*vga"; then
- info "AMD GPU detected - adding workaround parameters"
- cmdline="$cmdline amdgpu.pg_mask=0 amdgpu.cwsr_enable=0"
- fi
-
- zfs set org.zfsbootmenu:commandline="$cmdline" "$POOL_NAME/ROOT"
- info "Kernel command line set on $POOL_NAME/ROOT"
-
- # Set bootfs property
- zpool set bootfs="$POOL_NAME/ROOT/default" "$POOL_NAME"
- info "Default boot filesystem set to $POOL_NAME/ROOT/default"
-
- # Create EFI boot entries for each disk
- local zbm_cmdline="spl_hostid=0x${host_id} zbm.timeout=3 zbm.prefer=${POOL_NAME} zbm.import_policy=hostid"
-
- for i in "${!SELECTED_DISKS[@]}"; do
- local disk="${SELECTED_DISKS[$i]}"
- local label="ZFSBootMenu"
- if [[ ${#SELECTED_DISKS[@]} -gt 1 ]]; then
- label="ZFSBootMenu-disk$((i+1))"
- fi
-
- info "Creating EFI boot entry: $label on $disk"
- efibootmgr --create \
- --disk "$disk" \
- --part 1 \
- --label "$label" \
- --loader '\EFI\ZBM\zfsbootmenu.efi' \
- --unicode "$zbm_cmdline" \
- --quiet
- done
-
- # Set as primary boot option
- local bootnum
- bootnum=$(efibootmgr | grep "ZFSBootMenu" | head -1 | grep -oP 'Boot\K[0-9A-F]+')
- if [[ -n "$bootnum" ]]; then
- local current_order
- current_order=$(efibootmgr | grep "BootOrder" | cut -d: -f2 | tr -d ' ')
- efibootmgr --bootorder "$bootnum,$current_order" --quiet
- info "ZFSBootMenu set as primary boot option"
- fi
-
- info "ZFSBootMenu configuration complete."
-}
-
-#############################
-# ZFS Services
-#############################
-
-configure_zfs_services() {
- step "Configuring ZFS Services"
-
- arch-chroot /mnt systemctl enable zfs.target
- arch-chroot /mnt systemctl disable zfs-import-cache.service
- arch-chroot /mnt systemctl enable zfs-import-scan.service
- arch-chroot /mnt systemctl enable zfs-mount.service
- arch-chroot /mnt systemctl enable zfs-import.target
-
- # Disable cachefile - we use zfs-import-scan
- zpool set cachefile=none "$POOL_NAME"
- rm -f /mnt/etc/zfs/zpool.cache
-
- info "ZFS services configured."
-}
-
-#############################
-# Pacman Snapshot Hook
-#############################
-
-configure_zfs_pacman_hook() {
- step "Configuring Pacman Snapshot Hook"
-
- mkdir -p /mnt/etc/pacman.d/hooks
-
- cat > /mnt/etc/pacman.d/hooks/zfs-snapshot.hook << EOF
-[Trigger]
-Operation = Upgrade
-Operation = Install
-Operation = Remove
-Type = Package
-Target = *
-
-[Action]
-Description = Creating ZFS snapshot before pacman transaction...
-When = PreTransaction
-Exec = /usr/local/bin/zfs-pre-snapshot
-EOF
-
- cat > /mnt/usr/local/bin/zfs-pre-snapshot << 'EOF'
-#!/bin/bash
-POOL="zroot"
-DATASET="$POOL/ROOT/default"
-TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
-SNAPSHOT_NAME="pre-pacman_$TIMESTAMP"
-
-if zfs snapshot "$DATASET@$SNAPSHOT_NAME"; then
- echo "Created snapshot: $DATASET@$SNAPSHOT_NAME"
-else
- echo "Warning: Failed to create snapshot" >&2
-fi
-EOF
-
- chmod +x /mnt/usr/local/bin/zfs-pre-snapshot
- info "Pacman hook configured."
-}
-
-#############################
-# ZFS Tools
-#############################
-
-install_zfs_tools() {
- step "Installing ZFS Management Tools"
-
- # Copy ZFS management scripts
- cp /usr/local/bin/zfssnapshot /mnt/usr/local/bin/zfssnapshot
- cp /usr/local/bin/zfsrollback /mnt/usr/local/bin/zfsrollback
- chmod +x /mnt/usr/local/bin/zfssnapshot
- chmod +x /mnt/usr/local/bin/zfsrollback
-
- info "ZFS management scripts installed: zfssnapshot, zfsrollback"
-}
-
-#############################
-# EFI Sync (Multi-disk)
-#############################
-
-sync_zfs_efi_partitions() {
- local efi_parts=()
- for disk in "${SELECTED_DISKS[@]}"; do
- efi_parts+=("$(get_efi_partition "$disk")")
- done
-
- # Skip if only one disk
- [[ ${#efi_parts[@]} -le 1 ]] && return
-
- step "Syncing EFI partitions for redundancy"
-
- for ((i=1; i<${#efi_parts[@]}; i++)); do
- local secondary="${efi_parts[$i]}"
- local tmp_mount="/tmp/efi_sync_$$"
-
- mkdir -p "$tmp_mount"
- mount "$secondary" "$tmp_mount"
- rsync -a /mnt/efi/ "$tmp_mount/"
- umount "$tmp_mount"
- rmdir "$tmp_mount"
-
- info "Synced EFI to $secondary"
- done
-}
-
-#############################
-# Genesis Snapshot
-#############################
-
-create_zfs_genesis_snapshot() {
- step "Creating Genesis Snapshot"
-
- local snapshot_name="genesis"
- zfs snapshot -r "$POOL_NAME@$snapshot_name"
-
- info "Genesis snapshot created: $POOL_NAME@$snapshot_name"
- info "You can restore to this point anytime with: zfsrollback $snapshot_name"
-}
-
-#############################
-# ZFS Cleanup
-#############################
-
-zfs_cleanup() {
- step "Cleaning up ZFS"
-
- # Unmount all ZFS datasets
- zfs unmount -a 2>/dev/null || true
-
- # Unmount EFI
- umount /mnt/efi 2>/dev/null || true
-
- # Export pool (important for clean import on boot)
- zpool export "$POOL_NAME"
-
- info "ZFS pool exported cleanly."
-}