diff options
| author | Craig Jennings <c@cjennings.net> | 2026-02-23 11:54:25 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-02-23 11:54:25 -0600 |
| commit | 6631521d9f45b73f9b4df76db9148d82af8d57b9 (patch) | |
| tree | 344efeff361b1c8953bd4f56d304a8ec70637899 /custom/lib | |
| parent | c7608c8ba7757fa49ca02c068a4b0473e7f825dc (diff) | |
| download | archangel-6631521d9f45b73f9b4df76db9148d82af8d57b9.tar.gz archangel-6631521d9f45b73f9b4df76db9148d82af8d57b9.zip | |
refactor: rename custom/ to installer/ for clarity
The custom/ directory name was an archiso implementation detail. Renamed
to installer/ which clearly communicates that this directory contains the
installer scripts and utilities that ship on the ISO.
Updated all references in build.sh, Makefile, test-install.sh, and README.
Diffstat (limited to 'custom/lib')
| -rw-r--r-- | custom/lib/btrfs.sh | 900 | ||||
| -rw-r--r-- | custom/lib/common.sh | 173 | ||||
| -rw-r--r-- | custom/lib/config.sh | 131 | ||||
| -rw-r--r-- | custom/lib/disk.sh | 204 | ||||
| -rw-r--r-- | custom/lib/zfs.sh | 359 |
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." -} |
