aboutsummaryrefslogtreecommitdiff
path: root/custom
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-01-22 23:21:18 -0600
committerCraig Jennings <c@cjennings.net>2026-01-22 23:21:18 -0600
commit0ffe7a85a1b024b88e4ddc3305c5f805edd6e8e1 (patch)
treeccd6c610630cce9eef268ab692999cdfe3bb5a1b /custom
parent197a8036af21232276cfbd9624d9eeeebe722df6 (diff)
downloadarchangel-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')
-rw-r--r--custom/40_zfs_snapshots13
-rw-r--r--custom/grub-zfs-snap160
-rwxr-xr-xcustom/install-archzfs227
-rwxr-xr-xcustom/zfs-snap-prune208
-rwxr-xr-xcustom/zfsrollback10
-rwxr-xr-xcustom/zfssnapshot9
-rw-r--r--custom/zz-grub-zfs-snap.hook22
7 files changed, 102 insertions, 547 deletions
diff --git a/custom/40_zfs_snapshots b/custom/40_zfs_snapshots
deleted file mode 100644
index 5215289..0000000
--- a/custom/40_zfs_snapshots
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-# /etc/grub.d/40_zfs_snapshots
-# GRUB configuration generator for ZFS snapshot boot entries
-#
-# This script is called by grub-mkconfig to generate menu entries
-# for booting into ZFS snapshots.
-
-set -e
-
-# Only run if grub-zfs-snap is installed
-if [[ -x /usr/local/bin/grub-zfs-snap ]]; then
- /usr/local/bin/grub-zfs-snap --generate
-fi
diff --git a/custom/grub-zfs-snap b/custom/grub-zfs-snap
deleted file mode 100644
index 27f2c1f..0000000
--- a/custom/grub-zfs-snap
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/bin/bash
-# grub-zfs-snap - Generate GRUB menu entries for ZFS snapshots
-#
-# This script scans ZFS snapshots and generates GRUB submenu entries
-# allowing boot into previous system states.
-#
-# Usage: grub-zfs-snap [--generate]
-# --generate Output GRUB menu entries (called by /etc/grub.d/40_zfs_snapshots)
-# (no args) Regenerate GRUB config
-#
-# Installation:
-# 1. Copy this script to /usr/local/bin/grub-zfs-snap
-# 2. Copy 40_zfs_snapshots to /etc/grub.d/
-# 3. Run: grub-mkconfig -o /boot/grub/grub.cfg
-
-set -e
-
-# Configuration
-POOL_NAME="${POOL_NAME:-zroot}"
-ROOT_DATASET="${ROOT_DATASET:-ROOT/default}"
-MAX_SNAPSHOTS="${MAX_SNAPSHOTS:-10}"
-
-# Get full dataset path
-FULL_DATASET="${POOL_NAME}/${ROOT_DATASET}"
-
-# Colors for terminal output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-NC='\033[0m'
-
-info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; }
-error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
-
-# Check if running as root
-check_root() {
- if [[ $EUID -ne 0 ]]; then
- error "This script must be run as root"
- fi
-}
-
-# Get kernel and initramfs paths
-get_kernel_info() {
- # Find the LTS kernel
- KERNEL=$(ls /boot/vmlinuz-linux-lts 2>/dev/null || ls /boot/vmlinuz-linux 2>/dev/null | head -1)
- INITRD=$(ls /boot/initramfs-linux-lts.img 2>/dev/null || ls /boot/initramfs-linux.img 2>/dev/null | head -1)
-
- if [[ -z "$KERNEL" ]] || [[ -z "$INITRD" ]]; then
- error "Could not find kernel or initramfs"
- fi
-
- # Get just the filenames
- KERNEL_FILE=$(basename "$KERNEL")
- INITRD_FILE=$(basename "$INITRD")
-}
-
-# Get GRUB root device hint
-get_grub_root() {
- # Get the EFI partition device
- local efi_dev=$(findmnt -n -o SOURCE /boot 2>/dev/null | head -1)
- if [[ -z "$efi_dev" ]]; then
- # Fallback - assume first partition of first disk
- echo "hd0,gpt1"
- return
- fi
-
- # Convert to GRUB device notation
- # This is simplified - may need adjustment for complex setups
- local disk=$(echo "$efi_dev" | sed 's/[0-9]*$//')
- local part=$(echo "$efi_dev" | grep -o '[0-9]*$')
-
- # Get disk index (simplified - assumes /dev/sda or /dev/vda style)
- echo "hd0,gpt${part}"
-}
-
-# Generate GRUB menu entries for snapshots
-generate_entries() {
- get_kernel_info
-
- local grub_root=$(get_grub_root)
- local hostid=$(hostid)
-
- # Get list of snapshots, sorted by creation time (newest first)
- local snapshots=$(zfs list -H -t snapshot -o name,creation -s creation -r "$FULL_DATASET" 2>/dev/null | \
- grep "^${FULL_DATASET}@" | \
- tac | \
- head -n "$MAX_SNAPSHOTS")
-
- if [[ -z "$snapshots" ]]; then
- return
- fi
-
- # Generate submenu
- cat << 'SUBMENU_START'
-submenu 'ZFS Snapshots' --class zfs {
-SUBMENU_START
-
- while IFS=$'\t' read -r snapshot creation; do
- local snap_name="${snapshot##*@}"
- local snap_date=$(echo "$creation" | awk '{print $1, $2, $3, $4, $5}')
-
- cat << ENTRY
- menuentry '${snap_name} (${snap_date})' --class zfs {
- insmod part_gpt
- insmod fat
- insmod zfs
- search --no-floppy --fs-uuid --set=root $(grub-probe --target=fs_uuid /boot)
- echo 'Loading Linux kernel...'
- linux /${KERNEL_FILE} root=ZFS=${snapshot} ro spl.spl_hostid=0x${hostid}
- echo 'Loading initial ramdisk...'
- initrd /${INITRD_FILE}
- }
-ENTRY
- done <<< "$snapshots"
-
- echo "}"
-}
-
-# Regenerate GRUB configuration
-regenerate_grub() {
- check_root
- info "Regenerating GRUB configuration..."
- grub-mkconfig -o /boot/grub/grub.cfg
- info "Done. ZFS snapshots added to GRUB menu."
-}
-
-# List current snapshots
-list_snapshots() {
- echo "ZFS Snapshots for ${FULL_DATASET}:"
- echo ""
- zfs list -H -t snapshot -o name,creation,used -s creation -r "$FULL_DATASET" 2>/dev/null | \
- grep "^${FULL_DATASET}@" | \
- tac | \
- head -n "$MAX_SNAPSHOTS" | \
- while IFS=$'\t' read -r name creation used; do
- local snap_name="${name##*@}"
- printf " %-30s %s (used: %s)\n" "$snap_name" "$creation" "$used"
- done
-}
-
-# Main
-case "${1:-}" in
- --generate)
- generate_entries
- ;;
- --list)
- list_snapshots
- ;;
- "")
- regenerate_grub
- ;;
- *)
- echo "Usage: $0 [--generate|--list]"
- echo ""
- echo "Options:"
- echo " --generate Output GRUB menu entries (for /etc/grub.d/)"
- echo " --list List available snapshots"
- echo " (no args) Regenerate GRUB configuration"
- exit 1
- ;;
-esac
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
diff --git a/custom/zfs-snap-prune b/custom/zfs-snap-prune
deleted file mode 100755
index 762ff99..0000000
--- a/custom/zfs-snap-prune
+++ /dev/null
@@ -1,208 +0,0 @@
-#!/bin/bash
-# zfs-snap-prune - Prune old ZFS snapshots with hybrid retention policy
-#
-# Retention Policy:
-# - Always keep the N most recent snapshots (default: 20)
-# - Delete snapshots beyond N only if older than MAX_AGE (default: 180 days)
-# - Never delete genesis snapshot
-#
-# Usage:
-# zfs-snap-prune [OPTIONS]
-#
-# Options:
-# --dry-run Show what would be deleted without deleting
-# --verbose Show decision for every snapshot
-# --quiet Suppress non-error output
-# --test Use mock data from stdin instead of real ZFS
-# --help Show this help message
-#
-# Environment variables:
-# POOL_NAME - ZFS pool name (default: zroot)
-# ROOT_DATASET - Root dataset path (default: ROOT/default)
-# KEEP_COUNT - Number of recent snapshots to always keep (default: 20)
-# MAX_AGE_DAYS - Delete older snapshots beyond KEEP_COUNT (default: 180)
-# NOW_OVERRIDE - Override current timestamp for testing (epoch seconds)
-
-set -e
-
-# Configuration (can be overridden by environment)
-POOL_NAME="${POOL_NAME:-zroot}"
-ROOT_DATASET="${ROOT_DATASET:-ROOT/default}"
-KEEP_COUNT="${KEEP_COUNT:-20}"
-MAX_AGE_DAYS="${MAX_AGE_DAYS:-180}"
-
-FULL_DATASET="${POOL_NAME}/${ROOT_DATASET}"
-
-# Flags
-DRY_RUN=false
-VERBOSE=false
-QUIET=false
-TEST_MODE=false
-
-# Colors for output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m'
-
-usage() {
- sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
- exit 0
-}
-
-info() {
- [[ "$QUIET" == "true" ]] && return
- echo -e "${GREEN}[INFO]${NC} $1"
-}
-
-verbose() {
- [[ "$VERBOSE" != "true" ]] && return
- echo -e "${BLUE}[VERBOSE]${NC} $1"
-}
-
-warn() {
- [[ "$QUIET" == "true" ]] && return
- echo -e "${YELLOW}[WARN]${NC} $1"
-}
-
-error() {
- echo -e "${RED}[ERROR]${NC} $1" >&2
- exit 1
-}
-
-# Parse arguments
-while [[ $# -gt 0 ]]; do
- case "$1" in
- --dry-run)
- DRY_RUN=true
- shift
- ;;
- --verbose)
- VERBOSE=true
- shift
- ;;
- --quiet)
- QUIET=true
- shift
- ;;
- --test)
- TEST_MODE=true
- shift
- ;;
- --help|-h)
- usage
- ;;
- *)
- error "Unknown option: $1"
- ;;
- esac
-done
-
-# Check if running as root (skip in test mode)
-if [[ "$TEST_MODE" != "true" ]] && [[ $EUID -ne 0 ]]; then
- error "This script must be run as root"
-fi
-
-# Get current timestamp (can be overridden for testing)
-NOW="${NOW_OVERRIDE:-$(date +%s)}"
-MAX_AGE_SECONDS=$((MAX_AGE_DAYS * 24 * 60 * 60))
-CUTOFF_TIME=$((NOW - MAX_AGE_SECONDS))
-
-info "Pruning snapshots for ${FULL_DATASET}"
-info "Policy: Keep ${KEEP_COUNT} recent, delete if older than ${MAX_AGE_DAYS} days"
-[[ "$DRY_RUN" == "true" ]] && info "DRY RUN - no changes will be made"
-
-# Get snapshots - either from ZFS or stdin (test mode)
-# Expected format: snapshot_name<TAB>creation_date_string
-# Example: zroot/ROOT/default@pre-pacman_2025-01-15 Wed Jan 15 10:30 2025
-if [[ "$TEST_MODE" == "true" ]]; then
- # Read mock data from stdin
- SNAPSHOTS=$(cat | tac)
-else
- # Query real ZFS - sorted by creation (oldest first), then reversed for newest first
- SNAPSHOTS=$(zfs list -H -t snapshot -o name,creation -s creation -r "$FULL_DATASET" 2>/dev/null | \
- grep "^${FULL_DATASET}@" | \
- tac) || true
-fi
-
-if [[ -z "$SNAPSHOTS" ]]; then
- info "No snapshots found"
- exit 0
-fi
-
-# Count snapshots
-TOTAL=$(echo "$SNAPSHOTS" | wc -l)
-info "Found ${TOTAL} snapshots"
-
-# Track results
-DELETED=0
-KEPT=0
-POSITION=0
-
-# Process each snapshot
-while IFS=$'\t' read -r snapshot creation_str; do
- [[ -z "$snapshot" ]] && continue
-
- POSITION=$((POSITION + 1))
- SNAP_NAME="${snapshot##*@}"
-
- # Parse creation time
- if [[ "$TEST_MODE" == "true" ]]; then
- # In test mode, creation_str is epoch seconds
- SNAP_TIME="$creation_str"
- else
- # In real mode, parse date string
- SNAP_TIME=$(date -d "$creation_str" +%s 2>/dev/null || echo "0")
- fi
-
- AGE_DAYS=$(( (NOW - SNAP_TIME) / 86400 ))
-
- # Decision logic
- if [[ $POSITION -le $KEEP_COUNT ]]; then
- # Always keep the first KEEP_COUNT snapshots (most recent)
- verbose "KEEP: ${SNAP_NAME} (position ${POSITION}/${KEEP_COUNT}, ${AGE_DAYS} days old) - within keep count"
- KEPT=$((KEPT + 1))
- elif [[ "$SNAP_NAME" == "genesis" ]]; then
- # Never delete genesis
- verbose "KEEP: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old) - genesis protected"
- KEPT=$((KEPT + 1))
- elif [[ $SNAP_TIME -ge $CUTOFF_TIME ]]; then
- # Not old enough to delete
- verbose "KEEP: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old) - younger than ${MAX_AGE_DAYS} days"
- KEPT=$((KEPT + 1))
- else
- # Delete: beyond keep count AND older than max age
- if [[ "$DRY_RUN" == "true" ]]; then
- info "WOULD DELETE: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old)"
- DELETED=$((DELETED + 1))
- elif [[ "$TEST_MODE" == "true" ]]; then
- # Test mode: simulate deletion (don't actually call zfs)
- verbose "DELETE: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old)"
- DELETED=$((DELETED + 1))
- else
- verbose "DELETE: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old)"
- if zfs destroy "$snapshot" 2>/dev/null; then
- DELETED=$((DELETED + 1))
- else
- warn "Failed to delete ${snapshot}"
- fi
- fi
- fi
-done <<< "$SNAPSHOTS"
-
-# Summary
-info "Summary: ${KEPT} kept, ${DELETED} deleted"
-
-# Regenerate GRUB menu if we deleted anything (skip in dry-run and test modes)
-if [[ $DELETED -gt 0 ]] && [[ "$DRY_RUN" != "true" ]] && [[ "$TEST_MODE" != "true" ]]; then
- if [[ -x /usr/local/bin/grub-zfs-snap ]]; then
- info "Regenerating GRUB menu..."
- /usr/local/bin/grub-zfs-snap
- fi
-fi
-
-# Exit with special code for testing (number of deleted)
-if [[ "$TEST_MODE" == "true" ]]; then
- echo "RESULT:kept=${KEPT},deleted=${DELETED}"
-fi
diff --git a/custom/zfsrollback b/custom/zfsrollback
index ee858f6..d73e0e8 100755
--- a/custom/zfsrollback
+++ b/custom/zfsrollback
@@ -171,14 +171,8 @@ done
echo ""
if [ $failed -eq 0 ]; then
echo "Rollback complete."
-
- # Update GRUB boot menu if grub-zfs-snap is available
- # (destroyed snapshots need to be removed from menu)
- if command -v grub-zfs-snap &> /dev/null; then
- echo ""
- echo "Updating GRUB boot menu..."
- grub-zfs-snap
- fi
+ echo ""
+ echo "Note: ZFSBootMenu auto-detects snapshots - no menu regeneration needed."
else
echo "Rollback completed with $failed failure(s)"
exit 1
diff --git a/custom/zfssnapshot b/custom/zfssnapshot
index 1fa7e3b..749ea5a 100755
--- a/custom/zfssnapshot
+++ b/custom/zfssnapshot
@@ -101,10 +101,5 @@ done
echo ""
echo "Snapshot complete. Verify with: zfs list -t snapshot | grep $snapshot_name"
-
-# Update GRUB boot menu if grub-zfs-snap is available
-if command -v grub-zfs-snap &> /dev/null; then
- echo ""
- echo "Updating GRUB boot menu..."
- grub-zfs-snap
-fi
+echo ""
+echo "To boot from this snapshot: reboot and press Ctrl+D at ZFSBootMenu"
diff --git a/custom/zz-grub-zfs-snap.hook b/custom/zz-grub-zfs-snap.hook
deleted file mode 100644
index 8153b84..0000000
--- a/custom/zz-grub-zfs-snap.hook
+++ /dev/null
@@ -1,22 +0,0 @@
-[Trigger]
-Type = Package
-Operation = Upgrade
-Operation = Install
-Operation = Remove
-Target = linux-lts
-Target = linux-lts-headers
-Target = zfs-dkms
-Target = zfs-utils
-Target = grub
-
-[Trigger]
-Type = Path
-Operation = Install
-Operation = Upgrade
-Operation = Remove
-Target = usr/lib/modules/*/vmlinuz
-
-[Action]
-Description = Updating GRUB with ZFS snapshots...
-When = PostTransaction
-Exec = /usr/local/bin/grub-zfs-snap