diff options
| author | Craig Jennings <c@cjennings.net> | 2026-01-22 23:21:18 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-01-22 23:21:18 -0600 |
| commit | 0ffe7a85a1b024b88e4ddc3305c5f805edd6e8e1 (patch) | |
| tree | ccd6c610630cce9eef268ab692999cdfe3bb5a1b /custom/install-archzfs | |
| parent | 197a8036af21232276cfbd9624d9eeeebe722df6 (diff) | |
| download | archangel-0ffe7a85a1b024b88e4ddc3305c5f805edd6e8e1.tar.gz archangel-0ffe7a85a1b024b88e4ddc3305c5f805edd6e8e1.zip | |
Replace GRUB with ZFSBootMenu bootloader
This is a major change that replaces the GRUB bootloader with ZFSBootMenu,
providing native ZFS boot environment support.
Key changes:
- EFI partition reduced from 1GB to 512MB (only holds ZFSBootMenu)
- EFI now mounts at /efi instead of /boot
- Kernel and initramfs live on ZFS root (enables snapshot boot with matching kernel)
- Downloads pre-built ZFSBootMenu EFI binary from get.zfsbootmenu.org
- Creates EFI boot entries for all disks in multi-disk configurations
- Syncs ZFSBootMenu to all EFI partitions for redundancy
- Sets org.zfsbootmenu:commandline on zroot/ROOT for kernel cmdline inheritance
- Sets bootfs pool property for default boot environment
- AMD GPU workarounds (pg_mask, cwsr_enable) added to kernel cmdline when AMD detected
Deleted GRUB snapshot tooling (no longer needed):
- custom/grub-zfs-snap
- custom/40_zfs_snapshots
- custom/zz-grub-zfs-snap.hook
- custom/zfs-snap-prune
Updated helper scripts:
- zfssnapshot: removed grub-zfs-snap call, shows ZFSBootMenu tip
- zfsrollback: removed grub-zfs-snap call, notes auto-detection
Tested configurations:
- Single disk installation
- 2-disk mirror (mirror-0)
- 3-disk RAIDZ1 (raidz1-0)
- All boot correctly with ZFSBootMenu
Diffstat (limited to 'custom/install-archzfs')
| -rwxr-xr-x | custom/install-archzfs | 227 |
1 files changed, 98 insertions, 129 deletions
diff --git a/custom/install-archzfs b/custom/install-archzfs index 567a213..a17aad5 100755 --- a/custom/install-archzfs +++ b/custom/install-archzfs @@ -693,7 +693,7 @@ show_summary() { else echo " ZFS Pool: $POOL_NAME (encrypted)" fi - echo " Boot: EFI on all disks (redundant)" + echo " Boot: ZFSBootMenu on all disks (redundant)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" @@ -717,8 +717,9 @@ partition_disks() { wipefs -af "$disk" sgdisk --zap-all "$disk" - # Create partitions: 1G EFI + rest for ZFS - sgdisk -n 1:0:+1G -t 1:ef00 -c 1:"EFI" "$disk" + # Create partitions: 512M EFI + rest for ZFS + # EFI only needs to hold ZFSBootMenu binary (~64MB) - 512MB is plenty + sgdisk -n 1:0:+512M -t 1:ef00 -c 1:"EFI" "$disk" sgdisk -n 2:0:0 -t 2:bf00 -c 2:"ZFS" "$disk" # Determine partition names (handle nvme/mmcblk naming) @@ -860,10 +861,11 @@ create_datasets() { mount_efi() { step "Mounting EFI Partition" - mkdir -p /mnt/boot - # Mount primary (first) EFI partition - mount "${EFI_PARTS[0]}" /mnt/boot - info "Primary EFI partition ${EFI_PARTS[0]} mounted at /mnt/boot" + # EFI partition mounts at /efi - only holds ZFSBootMenu binary + # /boot is a directory on ZFS root (kernels live on ZFS for snapshot safety) + mkdir -p /mnt/efi + mount "${EFI_PARTS[0]}" /mnt/efi + info "EFI partition ${EFI_PARTS[0]} mounted at /mnt/efi" } install_base() { @@ -898,8 +900,6 @@ EOF linux-firmware \ zfs-dkms \ zfs-utils \ - grub \ - freetype2 \ efibootmgr \ networkmanager \ avahi \ @@ -922,10 +922,10 @@ EOF configure_system() { step "Configuring System" - # fstab (only for EFI) + # fstab (only for EFI - /boot is on ZFS root) info "Generating fstab..." - echo "# /boot - EFI System Partition" > /mnt/etc/fstab - echo "UUID=$(blkid -s UUID -o value "${EFI_PARTS[0]}") /boot vfat defaults,noatime 0 2" >> /mnt/etc/fstab + echo "# /efi - EFI System Partition (ZFSBootMenu binary)" > /mnt/etc/fstab + echo "UUID=$(blkid -s UUID -o value "${EFI_PARTS[0]}") /efi vfat defaults,noatime 0 2" >> /mnt/etc/fstab # Timezone info "Setting timezone to $TIMEZONE..." @@ -1097,8 +1097,8 @@ EOF arch-chroot /mnt mkinitcpio -P } -configure_bootloader() { - step "Configuring GRUB Bootloader" +configure_zfsbootmenu() { + step "Configuring ZFSBootMenu" # Ensure hostid exists BEFORE reading it # This is critical: hostid command returns a value even without /etc/hostid, @@ -1111,76 +1111,80 @@ configure_bootloader() { local host_id host_id=$(hostid) - # Configure GRUB defaults - cat > /mnt/etc/default/grub << EOF -GRUB_DEFAULT=0 -GRUB_TIMEOUT=5 -GRUB_DISTRIBUTOR="Arch Linux (ZFS)" -GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3" -GRUB_CMDLINE_LINUX="spl.spl_hostid=0x$host_id" -GRUB_PRELOAD_MODULES="part_gpt part_msdos zfs" -GRUB_TERMINAL_OUTPUT="console" -GRUB_DISABLE_OS_PROBER=true -GRUB_GFXMODE=auto -GRUB_GFXPAYLOAD_LINUX=keep -GRUB_FONT=/boot/grub/fonts/DejaVuSansMono32.pf2 -EOF - - # Install GRUB to each EFI partition for boot redundancy - info "Installing GRUB to ${#EFI_PARTS[@]} EFI partition(s)..." - - for i in "${!EFI_PARTS[@]}"; do - local efi_part="${EFI_PARTS[$i]}" - local bootloader_id="GRUB" - if [[ ${#EFI_PARTS[@]} -gt 1 ]]; then - bootloader_id="GRUB-disk$((i+1))" - fi - - # Unmount current boot if mounted, mount this EFI partition - umount /mnt/boot 2>/dev/null || true - mount "$efi_part" /mnt/boot - - # Create directories and font - mkdir -p /mnt/boot/grub/fonts - arch-chroot /mnt grub-mkfont -s 32 -o /boot/grub/fonts/DejaVuSansMono32.pf2 \ - /usr/share/fonts/TTF/DejaVuSansMono.ttf 2>/dev/null || true + # Copy hostid to installed system (ZFS uses this for pool ownership) + cp /etc/hostid /mnt/etc/hostid - # Install GRUB - info "Installing GRUB to $efi_part (bootloader-id: $bootloader_id)..." - arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot \ - --bootloader-id="$bootloader_id" --recheck + # Create ZFSBootMenu directory on EFI + mkdir -p /mnt/efi/EFI/ZBM - # Generate configuration - arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg - done + # Download ZFSBootMenu release EFI binary + # Using the bundled release which includes everything needed + 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." - # Remount primary EFI for rest of installation - umount /mnt/boot 2>/dev/null || true - mount "${EFI_PARTS[0]}" /mnt/boot + # Set kernel command line on the ROOT PARENT dataset + # This allows inheritance to all boot environments (future-proofing) + # ZFSBootMenu reads org.zfsbootmenu:commandline property + local cmdline="rw loglevel=3" - if [[ ${#EFI_PARTS[@]} -gt 1 ]]; then - info "GRUB installed to all ${#EFI_PARTS[@]} disks for boot redundancy." + # Add any AMD GPU workarounds if needed (detect Strix Halo etc) + 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 -} -configure_grub_zfs_snap() { - step "Configuring ZFS Snapshot Boot Entries" + # Set on ROOT parent so all boot environments inherit it + zfs set org.zfsbootmenu:commandline="$cmdline" "$POOL_NAME/ROOT" + info "Kernel command line set on $POOL_NAME/ROOT (inherited by children)" - # Install grub-zfs-snap script - info "Installing grub-zfs-snap..." - cp /usr/local/bin/grub-zfs-snap /mnt/usr/local/bin/grub-zfs-snap - chmod +x /mnt/usr/local/bin/grub-zfs-snap + # Set bootfs property - tells ZFSBootMenu which dataset to boot by default + 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 + # ZFSBootMenu EFI parameters (passed via --unicode): + # spl_hostid=0x... - Required for pool import + # zbm.timeout=3 - Seconds before auto-boot (-1 = always show menu) + # zbm.prefer=POOLNAME - Preferred pool to boot from + # zbm.import_policy=hostid - How to handle pool imports + 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 - # Install GRUB generator - cp /usr/local/share/grub-zfs-snap/40_zfs_snapshots /mnt/etc/grub.d/40_zfs_snapshots - chmod +x /mnt/etc/grub.d/40_zfs_snapshots + # Determine partition number (always 1 - first partition is EFI) + local part_num=1 + + info "Creating EFI boot entry: $label on $disk" + efibootmgr --create \ + --disk "$disk" \ + --part "$part_num" \ + --label "$label" \ + --loader '\EFI\ZBM\zfsbootmenu.efi' \ + --unicode "$zbm_cmdline" \ + --quiet + done - # Install pacman hook for auto-regeneration - mkdir -p /mnt/etc/pacman.d/hooks - cp /usr/local/share/grub-zfs-snap/zz-grub-zfs-snap.hook /mnt/etc/pacman.d/hooks/ + # Get the boot entry number and set as first in boot order + local bootnum + bootnum=$(efibootmgr | grep "ZFSBootMenu" | head -1 | grep -oP 'Boot\K[0-9A-F]+') + if [[ -n "$bootnum" ]]; then + # Get current boot order, prepend our entry + 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 "ZFS snapshots will appear in GRUB boot menu." - info "Run 'grub-zfs-snap' to manually regenerate after creating snapshots." + info "ZFSBootMenu configuration complete." } configure_zfs_services() { @@ -1198,9 +1202,7 @@ configure_zfs_services() { arch-chroot /mnt systemctl enable zfs-mount.service arch-chroot /mnt systemctl enable zfs-import.target - # Copy hostid to installed system (ZFS uses this for pool ownership) - # Note: hostid is generated in configure_bootloader, so it always exists here - cp /etc/hostid /mnt/etc/hostid + # Note: hostid and bootfs are already set by configure_zfsbootmenu() # Disable cachefile - we use zfs-import-scan which doesn't need it # Also remove any existing cachefile since zfs-import-scan has a condition @@ -1208,9 +1210,6 @@ configure_zfs_services() { zpool set cachefile=none "$POOL_NAME" rm -f /mnt/etc/zfs/zpool.cache - # Set bootfs - zpool set bootfs="$POOL_NAME/ROOT/default" "$POOL_NAME" - # Enable other services arch-chroot /mnt systemctl enable NetworkManager arch-chroot /mnt systemctl enable avahi-daemon @@ -1251,10 +1250,6 @@ else echo "Warning: Failed to create snapshot" >&2 fi -# Prune old snapshots (runs quietly, non-blocking) -if [[ -x /usr/local/bin/zfs-snap-prune ]]; then - /usr/local/bin/zfs-snap-prune --quiet & -fi EOF chmod +x /mnt/usr/local/bin/zfs-pre-snapshot @@ -1262,49 +1257,17 @@ EOF info "Pacman hook configured." } -configure_snapshot_retention() { - step "Configuring Snapshot Retention" +configure_zfs_tools() { + step "Installing ZFS Management Tools" # Copy ZFS management scripts - cp /usr/local/bin/zfs-snap-prune /mnt/usr/local/bin/zfs-snap-prune 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/zfs-snap-prune chmod +x /mnt/usr/local/bin/zfssnapshot chmod +x /mnt/usr/local/bin/zfsrollback - # Create systemd service for pruning - cat > /mnt/etc/systemd/system/zfs-snap-prune.service << 'EOF' -[Unit] -Description=Prune old ZFS snapshots -After=zfs.target - -[Service] -Type=oneshot -ExecStart=/usr/local/bin/zfs-snap-prune --quiet -EOF - - # Create systemd timer for daily pruning - cat > /mnt/etc/systemd/system/zfs-snap-prune.timer << 'EOF' -[Unit] -Description=Daily ZFS snapshot pruning - -[Timer] -OnCalendar=daily -Persistent=true -RandomizedDelaySec=1h - -[Install] -WantedBy=timers.target -EOF - - # Enable the timer - arch-chroot /mnt systemctl enable zfs-snap-prune.timer - - info "ZFS management scripts installed: zfssnapshot, zfsrollback, zfs-snap-prune" - info "Snapshot retention configured (daily pruning enabled)." - info "Policy: Keep 20 recent, delete if older than 180 days" - info "Genesis snapshot is always preserved." + info "ZFS management scripts installed: zfssnapshot, zfsrollback" + info "Note: Install sanoid via archsetup for automated snapshot retention." } copy_archsetup() { @@ -1326,7 +1289,6 @@ sync_efi_partitions() { step "Syncing EFI Partitions for Redundancy" - local primary_efi="${EFI_PARTS[0]}" local temp_mount="/mnt/efi_sync" for i in "${!EFI_PARTS[@]}"; do @@ -1335,13 +1297,14 @@ sync_efi_partitions() { fi local efi_part="${EFI_PARTS[$i]}" - info "Syncing to EFI partition $((i+1)): $efi_part" + info "Syncing ZFSBootMenu to EFI partition $((i+1)): $efi_part" mkdir -p "$temp_mount" mount "$efi_part" "$temp_mount" - # Sync all content from primary EFI (mounted at /mnt/boot) to secondary - rsync -a --delete /mnt/boot/ "$temp_mount/" + # Copy ZFSBootMenu binary to secondary EFI partitions + mkdir -p "$temp_mount/EFI/ZBM" + cp /mnt/efi/EFI/ZBM/zfsbootmenu.efi "$temp_mount/EFI/ZBM/" umount "$temp_mount" done @@ -1420,7 +1383,7 @@ cleanup() { ZFS_PASSPHRASE="" info "Unmounting filesystems..." - umount /mnt/boot 2>/dev/null || true + umount /mnt/efi 2>/dev/null || true info "Exporting ZFS pool..." zpool export "$POOL_NAME" @@ -1443,11 +1406,18 @@ print_summary() { echo " ZFS Pool: $POOL_NAME (encrypted)" fi echo "" - echo "ZFS Features:" + echo "ZFSBootMenu Features:" + echo " - Boot from any snapshot (Ctrl+D at boot menu)" echo " - Genesis snapshot: pristine post-install state" echo " - Pre-pacman snapshots for safe upgrades" echo " - Sanoid/syncoid configured by archsetup" echo "" + echo "Boot Menu Keys (at ZFSBootMenu):" + echo " Enter - Boot selected environment" + echo " e - Edit kernel command line" + echo " Ctrl+D - Show snapshot selector" + echo " Ctrl+R - Recovery shell" + echo "" echo "Useful Commands:" echo " List snapshots: zfs list -t snapshot" echo " Manual snapshot: zfs snapshot zroot/home@my-backup" @@ -1485,11 +1455,10 @@ main() { configure_wifi configure_ssh configure_initramfs - configure_bootloader - configure_grub_zfs_snap + configure_zfsbootmenu configure_zfs_services configure_pacman_hook - configure_snapshot_retention + configure_zfs_tools copy_archsetup sync_efi_partitions create_genesis_snapshot |
