aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--PLAN-zfsbootmenu-implementation.org712
-rwxr-xr-xbuild.sh17
-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
-rw-r--r--docs/2026-01-22-ratio-amd-gpu-freeze-fix-instructions.org (renamed from inbox/instructions.txt)0
-rw-r--r--docs/research-sandreas-zarch.org365
-rw-r--r--docs/session-context.org52
13 files changed, 1232 insertions, 564 deletions
diff --git a/.gitignore b/.gitignore
index dce47fe..57e156e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ profile/
zfs-packages/
vm/
test-logs/
+reference-repos/
diff --git a/PLAN-zfsbootmenu-implementation.org b/PLAN-zfsbootmenu-implementation.org
new file mode 100644
index 0000000..6733c26
--- /dev/null
+++ b/PLAN-zfsbootmenu-implementation.org
@@ -0,0 +1,712 @@
+#+TITLE: ZFSBootMenu Implementation Plan
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2026-01-22
+
+* Overview
+
+Replace GRUB bootloader with ZFSBootMenu in the archzfs installation script. ZFSBootMenu provides native ZFS snapshot booting, eliminates kernel/snapshot version mismatch issues, and simplifies the boot architecture.
+
+* Why ZFSBootMenu?
+
+| Feature | GRUB | ZFSBootMenu |
+|-----------------+-----------------------------------+------------------------------|
+| Snapshot boot | Custom scripts (grub-zfs-snap) | Native, built-in |
+| Kernel location | Separate /boot partition | On ZFS with root |
+| Rollback safety | Can mismatch kernel/snapshot | Kernel travels with snapshot |
+| Boot menu | Regenerate with grub-mkconfig | Auto-discovers datasets |
+| EFI size needed | ~1GB (kernels + GRUB) | ~64MB (single binary) |
+| Complexity | High (scripts, hooks, generators) | Low (single binary + config) |
+
+* Current Architecture (GRUB)
+
+#+begin_example
+EFI Partition (1GB, /boot):
+├── EFI/
+│ └── GRUB/
+│ └── grubx64.efi
+├── grub/
+│ ├── grub.cfg
+│ └── fonts/
+├── vmlinuz-linux-lts
+├── initramfs-linux-lts.img
+└── initramfs-linux-lts-fallback.img
+
+ZFS Pool:
+└── zroot/
+ └── ROOT/
+ └── default (mountpoint=/)
+#+end_example
+
+* Target Architecture (ZFSBootMenu)
+
+#+begin_example
+EFI Partition (512MB, /efi):
+└── EFI/
+ └── ZBM/
+ └── zfsbootmenu.efi
+
+ZFS Pool:
+└── zroot/
+ └── ROOT/ (org.zfsbootmenu:commandline set here)
+ └── default (mountpoint=/) (bootfs property points here)
+ └── boot/ <-- regular directory, NOT a dataset!
+ ├── vmlinuz-linux-lts
+ ├── initramfs-linux-lts.img
+ └── initramfs-linux-lts-fallback.img
+#+end_example
+
+*Key insight from research:* /boot is a regular directory inside ROOT/default,
+NOT a separate ZFS dataset. This ensures:
+1. Snapshots of ROOT/default include the matching kernel
+2. Rolling back a snapshot also rolls back to the kernel that was installed at that time
+3. ZFSBootMenu can find the kernel at the expected path
+
+* Files to Modify
+
+** custom/install-archzfs
+Primary installation script - most changes here.
+
+** build.sh
+ISO build script - remove GRUB snapshot tooling.
+
+** custom/grub-zfs-snap (DELETE)
+No longer needed - ZFSBootMenu handles natively.
+
+** custom/40_zfs_snapshots (DELETE)
+GRUB generator - no longer needed.
+
+** custom/zz-grub-zfs-snap.hook (DELETE)
+Pacman hook for GRUB - no longer needed.
+
+** custom/zfssnapshot
+Update to remove grub-zfs-snap call (ZFSBootMenu auto-detects).
+
+** custom/zfsrollback
+Update to remove grub-zfs-snap call.
+
+* Implementation Steps
+
+** Step 1: Update partition_disks()
+
+Location: custom/install-archzfs, lines 707-750
+
+Changes:
+- Reduce EFI partition from 1GB to 512MB
+- Update comments to reflect new purpose
+
+#+begin_src bash
+# Change this line:
+sgdisk -n 1:0:+1G -t 1:ef00 -c 1:"EFI" "$disk"
+
+# To:
+sgdisk -n 1:0:+512M -t 1:ef00 -c 1:"EFI" "$disk"
+#+end_src
+
+** Step 2: Update create_datasets()
+
+Location: custom/install-archzfs, lines 817-859
+
+*CRITICAL: DO NOT create a separate /boot dataset!*
+
+From research (PandaScience, sandreas/zarch, ZFSBootMenu docs):
+ZFSBootMenu expects /boot to be a *regular directory* inside the root dataset,
+NOT a separate ZFS dataset. The kernels must live at the path /boot/* within
+the root filesystem for ZFSBootMenu to find them.
+
+Changes:
+- Do NOT create a /boot dataset
+- The /boot directory will be created automatically by pacstrap when installing the kernel
+- This ensures snapshots of ROOT/default include the matching kernel
+
+#+begin_src bash
+# DO NOT ADD THIS - it's WRONG:
+# zfs create -o mountpoint=/boot "$POOL_NAME/ROOT/default/boot"
+
+# /boot is just a regular directory inside ROOT/default
+# mkinitcpio puts kernel/initramfs there automatically
+#+end_src
+
+Note: With ZFSBootMenu, kernels live ON the root ZFS dataset (not EFI partition).
+When you snapshot ROOT/default, the kernel is included in the snapshot.
+
+** Step 3: Replace mount_efi()
+
+Location: custom/install-archzfs, lines 861-867
+
+Changes:
+- Rename to mount_filesystems()
+- Mount EFI at /efi instead of /boot
+- /boot is already mounted as ZFS dataset
+
+#+begin_src bash
+mount_filesystems() {
+ step "Mounting Filesystems"
+
+ # EFI partition - only holds ZFSBootMenu binary
+ mkdir -p /mnt/efi
+ mount "${EFI_PARTS[0]}" /mnt/efi
+ info "EFI partition ${EFI_PARTS[0]} mounted at /mnt/efi"
+
+ # /boot is a directory inside the ZFS root dataset (created by pacstrap)
+ # No separate mount needed - it's part of the root filesystem
+}
+#+end_src
+
+** Step 4: Update install_base()
+
+Location: custom/install-archzfs, lines 869-920
+
+Changes:
+- Remove: grub, freetype2 (GRUB font support)
+- Keep: efibootmgr (needed for EFI boot entries)
+
+#+begin_src bash
+# Remove these from pacstrap:
+# grub \
+# freetype2 \
+
+# Keep efibootmgr
+#+end_src
+
+** Step 5: Update configure_system() fstab
+
+Location: custom/install-archzfs, lines 926-929
+
+Changes:
+- Mount EFI at /efi instead of /boot
+
+#+begin_src bash
+# Change:
+echo "UUID=$(blkid -s UUID -o value "${EFI_PARTS[0]}") /boot vfat defaults,noatime 0 2"
+
+# To:
+echo "UUID=$(blkid -s UUID -o value "${EFI_PARTS[0]}") /efi vfat defaults,noatime 0 2"
+#+end_src
+
+** Step 6: Update configure_initramfs()
+
+Location: custom/install-archzfs, lines 1021-1098
+
+Changes:
+- Update preset to use /boot (now on ZFS)
+- No changes to hooks - ZFS hook still needed
+
+The preset file paths remain the same (/boot/vmlinuz-linux-lts, etc.) but /boot is now on ZFS instead of EFI partition.
+
+** Step 7: Replace configure_bootloader() with configure_zfsbootmenu()
+
+Location: custom/install-archzfs, lines 1100-1164
+
+Delete the entire GRUB function and replace with:
+
+#+begin_src bash
+configure_zfsbootmenu() {
+ step "Configuring ZFSBootMenu"
+
+ # Ensure hostid exists and get value
+ # CRITICAL: Must be done BEFORE pool creation ideally, but we do it here too
+ if [[ ! -f /etc/hostid ]]; then
+ zgenhostid
+ fi
+ local host_id=$(hostid)
+
+ # Copy hostid to installed system (ZFS uses this for pool ownership)
+ cp /etc/hostid /mnt/etc/hostid
+
+ # Create ZFSBootMenu directory on EFI
+ mkdir -p /mnt/efi/EFI/ZBM
+
+ # Download ZFSBootMenu release EFI binary
+ # Using the bundled release which includes everything needed
+ # (Alternative: build from AUR with generate-zbm, but this is simpler)
+ 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
+ # This allows inheritance to all boot environments (future-proofing)
+ # ZFSBootMenu reads org.zfsbootmenu:commandline property
+ local cmdline="rw loglevel=3"
+
+ # 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
+
+ # 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)"
+
+ # 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
+
+ # 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
+
+ # Get the boot entry number and set as first in boot order
+ local 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=$(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."
+}
+#+end_src
+
+** Step 8: Delete configure_grub_zfs_snap()
+
+Location: custom/install-archzfs, lines 1166-1184
+
+Delete the entire function - ZFSBootMenu handles snapshot menus natively.
+
+** Step 9: Update sync_efi_partitions()
+
+Location: custom/install-archzfs, lines 1285-1315
+
+Changes:
+- Sync ZFSBootMenu binary instead of GRUB
+- Create EFI boot entries for secondary disks
+
+#+begin_src bash
+sync_efi_partitions() {
+ [[ ${#EFI_PARTS[@]} -le 1 ]] && return
+
+ step "Syncing EFI Partitions for Redundancy"
+
+ for i in "${!EFI_PARTS[@]}"; do
+ [[ $i -eq 0 ]] && continue
+
+ local efi_part="${EFI_PARTS[$i]}"
+ local temp_mount="/mnt/efi_sync"
+
+ info "Syncing ZFSBootMenu to EFI partition $((i+1)): $efi_part"
+
+ mkdir -p "$temp_mount"
+ mount "$efi_part" "$temp_mount"
+
+ # Copy ZFSBootMenu binary
+ mkdir -p "$temp_mount/EFI/ZBM"
+ cp /mnt/efi/EFI/ZBM/zfsbootmenu.efi "$temp_mount/EFI/ZBM/"
+
+ umount "$temp_mount"
+ done
+
+ rmdir "$temp_mount" 2>/dev/null || true
+ info "All EFI partitions synchronized."
+}
+#+end_src
+
+** Step 10: Update cleanup()
+
+Location: custom/install-archzfs, lines 1379-1393
+
+Changes:
+- Unmount /mnt/efi instead of /mnt/boot
+
+#+begin_src bash
+# Change:
+umount /mnt/boot 2>/dev/null || true
+
+# To:
+umount /mnt/efi 2>/dev/null || true
+#+end_src
+
+** Step 11: Update print_summary()
+
+Location: custom/install-archzfs, lines 1395-1424
+
+Changes:
+- Update bootloader references from GRUB to ZFSBootMenu
+- Update useful commands section
+
+#+begin_src bash
+# Update the "ZFS Features" section:
+echo "ZFS Features:"
+echo " - ZFSBootMenu: boot from any snapshot"
+echo " - Genesis snapshot: pristine post-install state"
+echo " - Pre-pacman snapshots for safe upgrades"
+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"
+#+end_src
+
+** Step 12: Update build.sh
+
+Location: build.sh
+
+Changes:
+- Remove grub-zfs-snap file copies (lines ~375-380)
+- Remove grub-zfs-snap permissions (line ~408)
+- Keep bootloader configs for live ISO (still uses GRUB/syslinux)
+
+#+begin_src bash
+# DELETE these lines from build.sh:
+
+# Copy grub-zfs-snap to ISO
+cp custom/grub-zfs-snap profile/airootfs/usr/local/bin/grub-zfs-snap
+mkdir -p profile/airootfs/usr/local/share/grub-zfs-snap
+cp custom/40_zfs_snapshots profile/airootfs/usr/local/share/grub-zfs-snap/
+cp custom/zz-grub-zfs-snap.hook profile/airootfs/usr/local/share/grub-zfs-snap/
+
+# And from file_permissions:
+["usr/local/bin/grub-zfs-snap"]="0:0:755"
+#+end_src
+
+** Step 13: Update zfssnapshot and zfsrollback
+
+Location: custom/zfssnapshot, custom/zfsrollback
+
+Changes:
+- Remove calls to grub-zfs-snap
+- ZFSBootMenu auto-detects snapshots, no regeneration needed
+
+#+begin_src bash
+# DELETE from zfssnapshot (around line 107):
+grub-zfs-snap 2>/dev/null || true
+
+# DELETE from zfsrollback (around line 177):
+grub-zfs-snap 2>/dev/null || true
+#+end_src
+
+** Step 14: Delete GRUB-specific files
+
+Files to delete from custom/:
+- custom/grub-zfs-snap
+- custom/40_zfs_snapshots
+- custom/zz-grub-zfs-snap.hook
+
+#+begin_src bash
+rm custom/grub-zfs-snap
+rm custom/40_zfs_snapshots
+rm custom/zz-grub-zfs-snap.hook
+#+end_src
+
+** Step 15: Update main() function call order
+
+Location: custom/install-archzfs, main() around line 1443
+
+Changes:
+- Replace configure_bootloader with configure_zfsbootmenu
+- Remove configure_grub_zfs_snap call
+
+#+begin_src bash
+# Change this sequence:
+ configure_initramfs
+ configure_bootloader # <- rename
+ configure_grub_zfs_snap # <- delete
+ configure_zfs_services
+
+# To:
+ configure_initramfs
+ configure_zfsbootmenu # <- new function
+ configure_zfs_services
+#+end_src
+
+* Testing Plan
+
+** Test Environment
+
+- QEMU VM with UEFI firmware (OVMF)
+- Multiple test scenarios for different disk configurations
+- Existing test script: scripts/test-vm.sh
+
+** Test 1: Single Disk Install
+
+#+begin_src bash
+# Start VM
+./scripts/test-vm.sh
+
+# In VM, run installer
+install-archzfs
+
+# Select single disk
+# Complete installation
+# Reboot
+#+end_src
+
+*Validation Points:*
+- [ ] EFI partition is 512MB (not 1GB)
+- [ ] /efi contains only EFI/ZBM/zfsbootmenu.efi
+- [ ] /boot is a directory (NOT a dataset): =zfs list= should NOT show zroot/ROOT/default/boot
+- [ ] Kernel files exist in /boot/ (=ls /boot/vmlinuz*=)
+- [ ] ZFSBootMenu menu appears on boot
+- [ ] Can boot into installed system
+- [ ] After login: =zfs get org.zfsbootmenu:commandline zroot/ROOT= shows cmdline (set on parent)
+- [ ] After login: =zpool get bootfs zroot= shows zroot/ROOT/default
+
+** Test 2: Mirror Install (2 disks)
+
+#+begin_src bash
+# Create second virtual disk
+qemu-img create -f qcow2 test-disk2.qcow2 50G
+
+# Modify test-vm.sh to add second disk
+# -drive file=test-disk2.qcow2,if=virtio
+
+# Run installer, select both disks, choose mirror
+#+end_src
+
+*Validation Points:*
+- [ ] Both disks have EFI partitions
+- [ ] ZFSBootMenu binary exists on both EFI partitions
+- [ ] EFI boot entries exist for both disks (efibootmgr -v)
+- [ ] Can boot from either disk (test by removing first disk)
+- [ ] ZFS pool shows mirror topology (zpool status)
+
+** Test 3: RAIDZ1 Install (3 disks)
+
+*Validation Points:*
+- [ ] All three disks have EFI partitions with ZFSBootMenu
+- [ ] Three EFI boot entries created
+- [ ] ZFS pool shows raidz1 topology
+
+** Test 4: Snapshot Boot
+
+#+begin_src bash
+# After installation and first boot:
+
+# Create a test file
+echo "original" > /root/test.txt
+
+# Create a snapshot
+zfs snapshot zroot/ROOT/default@test-snap
+
+# Modify the file
+echo "modified" > /root/test.txt
+
+# Reboot, at ZFSBootMenu press Ctrl+D
+# Select zroot/ROOT/default@test-snap
+# Boot from snapshot
+#+end_src
+
+*Validation Points:*
+- [ ] ZFSBootMenu shows snapshot selector with Ctrl+D
+- [ ] Snapshot appears in list
+- [ ] Booting from snapshot shows original file content
+- [ ] Kernel version matches (no mismatch errors)
+
+** Test 5: Kernel Update Scenario
+
+#+begin_src bash
+# Simulate kernel update (or actually do one)
+pacman -Syu
+
+# Reboot
+#+end_src
+
+*Validation Points:*
+- [ ] New kernel is on ZFS (included in future snapshots)
+- [ ] ZFSBootMenu detects and boots new kernel
+- [ ] No manual regeneration needed (unlike GRUB)
+
+** Test 6: Recovery Shell
+
+#+begin_src bash
+# At ZFSBootMenu, press Ctrl+R
+#+end_src
+
+*Validation Points:*
+- [ ] Recovery shell accessible
+- [ ] ZFS pool is importable from recovery
+- [ ] Can manually mount and chroot if needed
+
+** Test 7: Encrypted Pool
+
+#+begin_src bash
+# Run installer with encryption enabled
+# Enter passphrase when prompted
+#+end_src
+
+*Validation Points:*
+- [ ] ZFSBootMenu prompts for passphrase
+- [ ] Pool unlocks successfully
+- [ ] System boots normally after passphrase entry
+
+* Validation Checklist (All Tests)
+
+** Pre-Installation
+- [ ] Live ISO boots successfully
+- [ ] ZFS module loads (lsmod | grep zfs)
+
+** Partitioning
+- [ ] EFI partition is 512MB
+- [ ] ZFS partition uses remaining space
+- [ ] Partition table is GPT
+
+** Filesystem Layout
+- [ ] /efi is vfat, mounted from EFI partition
+- [ ] /boot is a directory inside ROOT/default (NOT a separate dataset)
+- [ ] Verify: =zfs list= should NOT show a zroot/ROOT/default/boot dataset
+- [ ] Kernel/initramfs exist in /boot/ (on the ZFS root filesystem)
+
+** ZFSBootMenu
+- [ ] zfsbootmenu.efi exists at /efi/EFI/ZBM/
+- [ ] EFI boot entry points to ZFSBootMenu
+- [ ] org.zfsbootmenu:commandline property set on root dataset
+- [ ] hostid included in cmdline (spl.spl_hostid=0x...)
+
+** Boot Process
+- [ ] ZFSBootMenu menu appears
+- [ ] Countdown timer works
+- [ ] Default boot entry is correct
+- [ ] Boot completes successfully
+- [ ] All ZFS datasets mount correctly
+
+** Multi-Disk (if applicable)
+- [ ] All EFI partitions contain zfsbootmenu.efi
+- [ ] All disks have EFI boot entries
+- [ ] Can boot from any disk
+
+** Snapshots
+- [ ] Genesis snapshot created
+- [ ] Ctrl+D shows snapshot selector
+- [ ] Can boot from snapshot
+- [ ] Snapshot includes matching kernel
+
+** Services
+- [ ] zfs-import-scan.service enabled
+- [ ] zfs-mount.service enabled
+- [ ] NetworkManager starts
+- [ ] SSH accessible (if enabled)
+
+* Rollback Plan
+
+If ZFSBootMenu implementation fails:
+
+1. Keep GRUB version in a git branch before changes
+2. ISO still boots with GRUB (live environment unchanged)
+3. Can install GRUB manually from live environment:
+ #+begin_src bash
+ pacstrap /mnt grub
+ arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot
+ arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg
+ #+end_src
+
+* Research Findings (from comparable projects)
+
+This plan incorporates best practices from these open-source Arch+ZFS installers:
+
+** eoli3n/archiso-zfs + arch-config
+- ZFSBootMenu built from source with =generate-zbm= (we use pre-built binary for simplicity)
+- Uses file-based encryption key (=/etc/zfs/zroot.key=) embedded in initramfs to avoid double passphrase prompt
+- Sets =org.zfsbootmenu:commandline= on ROOT parent for inheritance to all boot environments
+- Minimal dataset layout: ROOT, data/home
+
+** PandaScience/arch-on-zfs
+- Uses rEFInd (not ZFSBootMenu), but documents the "smart dataset layout":
+ - =system/= for root, =userdata/= for home, =nobackup/= for cache/tmp
+- Emphasizes =canmount=noauto= on root dataset (we already do this)
+- Recommends =/boot= as directory inside root, NOT separate dataset
+
+** sandreas/zarch
+- Downloads ZFSBootMenu binary from =https://get.zfsbootmenu.org/latest.EFI= (adopted)
+- Uses efibootmgr =--unicode= parameter for ZFSBootMenu cmdline (adopted)
+- ZFSBootMenu parameters: =spl_hostid=, =zbm.timeout=, =zbm.prefer=, =zbm.import_policy= (adopted)
+- Uses zrepl for time-based snapshots (we use pacman hooks - complementary approach)
+
+** danboid/ALEZ
+- Two-pool design (bpool + rpool) for GRUB compatibility - NOT needed with ZFSBootMenu
+- systemd-boot ZFS entry uses =zfs=POOL/ROOT/default= parameter
+- Pool export/reimport pattern for clean state
+
+** danfossi/Arch-ZFS-Root-Installation-Script
+- Uses =compatibility=grub2= pool option for GRUB - NOT needed with ZFSBootMenu
+- Good partition suffix helper for nvme/mmcblk naming (we already have this)
+- Separate bpool for boot - NOT needed with ZFSBootMenu
+
+** Key Lessons Adopted
+
+1. *DO NOT create separate /boot dataset* - must be directory inside root
+2. *Set commandline on ROOT parent* - inherited by all boot environments
+3. *Use =--unicode= for ZFSBootMenu parameters* - spl_hostid, zbm.timeout, zbm.prefer
+4. *Download pre-built EFI binary* - simpler than building from AUR
+5. *Copy hostid to installed system* - required for pool import
+6. *Set bootfs pool property* - tells ZFSBootMenu default boot target
+
+** Optional Enhancement: File-Based Encryption Key
+
+From eoli3n: To avoid entering passphrase twice (ZFSBootMenu + initramfs):
+
+#+begin_src bash
+# During pool creation, use keylocation=file instead of prompt
+echo "$ZFS_PASSPHRASE" > /mnt/etc/zfs/zroot.key
+chmod 000 /mnt/etc/zfs/zroot.key
+
+# Add to mkinitcpio FILES
+echo 'FILES+=(/etc/zfs/zroot.key)' >> /mnt/etc/mkinitcpio.conf.d/zfs-key.conf
+
+# Change keylocation
+zfs set keylocation=file:///etc/zfs/zroot.key zroot
+#+end_src
+
+Trade-off: Simpler UX (one passphrase) but key is in initramfs on ZFS.
+*Current plan uses prompt-based* - user enters passphrase at ZFSBootMenu.
+
+* References
+
+** Official Documentation
+- ZFSBootMenu Documentation: https://docs.zfsbootmenu.org/
+- ZFSBootMenu GitHub: https://github.com/zbm-dev/zfsbootmenu
+- ZFSBootMenu man page: https://docs.zfsbootmenu.org/en/latest/man/zfsbootmenu.7.html
+- Arch Wiki ZFS: https://wiki.archlinux.org/title/ZFS
+
+** Researched Projects
+- eoli3n/archiso-zfs: https://github.com/eoli3n/archiso-zfs
+- eoli3n/arch-config: https://github.com/eoli3n/arch-config
+- PandaScience/arch-on-zfs: https://github.com/PandaScience/arch-on-zfs
+- sandreas/zarch: https://github.com/sandreas/zarch
+- danboid/ALEZ: https://github.com/danboid/ALEZ
+- danfossi/Arch-ZFS-Root-Installation-Script: https://github.com/danfossi/Arch-ZFS-Root-Installation-Script
+
+** Guides
+- Florian Esser's ZFSBootMenu Guide: https://florianesser.ch/posts/20220714-arch-install-zbm/
+- Arch Wiki ZFSBootMenu: https://wiki.archlinux.org/title/User:Kayvlim/Install_UEFI_and_BIOS_compatible_Arch_Linux_with_Encrypted_ZFS_and_ZFSBootMenu
+
+* Implementation Order
+
+1. Create git branch: =git checkout -b zfsbootmenu=
+2. Delete GRUB files (Step 14)
+3. Update build.sh (Step 12)
+4. Update install-archzfs (Steps 1-11, 15)
+5. Update helper scripts (Step 13)
+6. Build new ISO
+7. Run Test 1 (single disk)
+8. Fix any issues
+9. Run Tests 2-7
+10. Merge to main when all tests pass
diff --git a/build.sh b/build.sh
index 337564f..acfecd6 100755
--- a/build.sh
+++ b/build.sh
@@ -372,17 +372,6 @@ cp "$CUSTOM_DIR/install-archzfs" "$PROFILE_DIR/airootfs/usr/local/bin/"
cp "$CUSTOM_DIR/install-claude" "$PROFILE_DIR/airootfs/usr/local/bin/"
cp "$CUSTOM_DIR/archsetup-zfs" "$PROFILE_DIR/airootfs/usr/local/bin/"
-# Copy grub-zfs-snap for ZFS snapshot boot entries
-info "Copying grub-zfs-snap..."
-cp "$CUSTOM_DIR/grub-zfs-snap" "$PROFILE_DIR/airootfs/usr/local/bin/"
-mkdir -p "$PROFILE_DIR/airootfs/usr/local/share/grub-zfs-snap"
-cp "$CUSTOM_DIR/40_zfs_snapshots" "$PROFILE_DIR/airootfs/usr/local/share/grub-zfs-snap/"
-cp "$CUSTOM_DIR/zz-grub-zfs-snap.hook" "$PROFILE_DIR/airootfs/usr/local/share/grub-zfs-snap/"
-
-# Copy zfs-snap-prune for snapshot retention
-info "Copying zfs-snap-prune..."
-cp "$CUSTOM_DIR/zfs-snap-prune" "$PROFILE_DIR/airootfs/usr/local/bin/"
-
# Copy zfssnapshot and zfsrollback for ZFS management
info "Copying zfssnapshot and zfsrollback..."
cp "$CUSTOM_DIR/zfssnapshot" "$PROFILE_DIR/airootfs/usr/local/bin/"
@@ -409,12 +398,6 @@ if grep -q "file_permissions=" "$PROFILE_DIR/profiledef.sh"; then
/)/ i\ ["/usr/local/bin/archsetup-zfs"]="0:0:755"
}' "$PROFILE_DIR/profiledef.sh"
sed -i '/^file_permissions=(/,/)/ {
- /)/ i\ ["/usr/local/bin/grub-zfs-snap"]="0:0:755"
- }' "$PROFILE_DIR/profiledef.sh"
- sed -i '/^file_permissions=(/,/)/ {
- /)/ i\ ["/usr/local/bin/zfs-snap-prune"]="0:0:755"
- }' "$PROFILE_DIR/profiledef.sh"
- sed -i '/^file_permissions=(/,/)/ {
/)/ i\ ["/usr/local/bin/zfssnapshot"]="0:0:755"
}' "$PROFILE_DIR/profiledef.sh"
sed -i '/^file_permissions=(/,/)/ {
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
diff --git a/inbox/instructions.txt b/docs/2026-01-22-ratio-amd-gpu-freeze-fix-instructions.org
index d6b8461..d6b8461 100644
--- a/inbox/instructions.txt
+++ b/docs/2026-01-22-ratio-amd-gpu-freeze-fix-instructions.org
diff --git a/docs/research-sandreas-zarch.org b/docs/research-sandreas-zarch.org
new file mode 100644
index 0000000..55bc77b
--- /dev/null
+++ b/docs/research-sandreas-zarch.org
@@ -0,0 +1,365 @@
+#+TITLE: Research: sandreas/zarch ZFSBootMenu Installation
+#+DATE: 2026-01-22
+#+AUTHOR: Research Notes
+
+* Overview
+
+This document summarizes research on the [[https://github.com/sandreas/zarch][sandreas/zarch]] GitHub repository for
+Arch Linux ZFS installation. The project uses ZFSBootMenu, native encryption,
+and automatic snapshots via zrepl.
+
+* Project Philosophy
+
+sandreas/zarch is described as a "single, non-modular file with some minor
+config profiles" - the author explicitly avoids a "modular multi-script beast."
+This contrasts with our more modular approach but offers useful patterns.
+
+** Key Features
+- ZFSBootMenu as bootloader (not GRUB)
+- Native ZFS encryption (AES-256-GCM)
+- Automatic snapshots via zrepl
+- EFI-only (no BIOS support)
+- Profile-based configuration
+
+* ZFSBootMenu Installation
+
+** Download and Install
+#+begin_src bash
+# Create EFI directory
+mkdir -p /efi/EFI/ZBM
+
+# Download latest ZFSBootMenu EFI binary
+wget -c https://get.zfsbootmenu.org/latest.EFI -O /efi/EFI/ZBM/ZFSBOOTMENU.EFI
+
+# Or use curl variant
+curl -o /boot/efi/EFI/ZBM/VMLINUZ.EFI -L https://get.zfsbootmenu.org/efi
+#+end_src
+
+** EFI Boot Entry Registration
+#+begin_src bash
+efibootmgr --disk $DISK --part 1 \
+ --create \
+ --label "ZFSBootMenu" \
+ --loader '\EFI\ZBM\ZFSBOOTMENU.EFI' \
+ --unicode "spl_hostid=$(hostid) zbm.timeout=3 zbm.prefer=zroot zbm.import_policy=hostid" \
+ --verbose
+#+end_src
+
+** Key ZFSBootMenu Parameters
+| Parameter | Purpose |
+|------------------------+------------------------------------------------|
+| zbm.timeout=N | Seconds to wait before auto-booting default |
+| zbm.prefer=POOL | Preferred pool for default boot environment |
+| zbm.import_policy | Pool import strategy (hostid recommended) |
+| zbm.skip | Skip menu and boot default immediately |
+| zbm.show | Force menu display |
+| spl_hostid=0xXXXXXXXX | Host ID for pool import validation |
+
+** Kernel Command Line for Boot Environments
+#+begin_src bash
+# Set inherited command line on ROOT dataset
+zfs set org.zfsbootmenu:commandline="quiet loglevel=0" zroot/ROOT
+
+# Set pool bootfs property
+zpool set bootfs=zroot/ROOT/arch zroot
+#+end_src
+
+* Dataset Layout
+
+** zarch Dataset Structure
+#+begin_example
+$POOL mountpoint=none
+$POOL/ROOT mountpoint=none (container for boot environments)
+$POOL/ROOT/arch mountpoint=/, canmount=noauto (active root)
+$POOL/home mountpoint=/home (shared across boot environments)
+#+end_example
+
+** Comparison: Our archzfs Dataset Structure
+#+begin_example
+zroot mountpoint=none, canmount=off
+zroot/ROOT mountpoint=none, canmount=off
+zroot/ROOT/default mountpoint=/, canmount=noauto, reservation=5-20G
+zroot/home mountpoint=/home
+zroot/home/root mountpoint=/root
+zroot/media mountpoint=/media, compression=off
+zroot/vms mountpoint=/vms, recordsize=64K
+zroot/var mountpoint=/var, canmount=off
+zroot/var/log mountpoint=/var/log
+zroot/var/cache mountpoint=/var/cache
+zroot/var/lib mountpoint=/var/lib, canmount=off
+zroot/var/lib/pacman mountpoint=/var/lib/pacman
+zroot/var/lib/docker mountpoint=/var/lib/docker
+zroot/var/tmp mountpoint=/var/tmp, auto-snapshot=false
+zroot/tmp mountpoint=/tmp, auto-snapshot=false
+#+end_example
+
+** Key Differences
+- zarch: Minimal dataset layout (ROOT, home)
+- archzfs: Fine-grained datasets with workload-specific tuning
+- archzfs: Separate /var/log, /var/cache, /var/lib/docker
+- archzfs: recordsize=64K for VM storage
+- archzfs: compression=off for media (already compressed)
+
+* ZFS Pool Creation
+
+** zarch Pool Creation (with encryption)
+#+begin_src bash
+zpool create -f \
+ -o ashift=12 \
+ -O compression=lz4 \
+ -O acltype=posixacl \
+ -O xattr=sa \
+ -O relatime=off \
+ -O atime=off \
+ -O encryption=aes-256-gcm \
+ -O keylocation=prompt \
+ -O keyformat=passphrase \
+ -o autotrim=on \
+ -m none \
+ $POOL ${DISK}-part2
+#+end_src
+
+** Our archzfs Pool Creation (with encryption)
+#+begin_src bash
+zpool create -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 encryption=aes-256-gcm \
+ -O keyformat=passphrase \
+ -O keylocation=prompt \
+ -O mountpoint=none \
+ -R /mnt \
+ "$POOL_NAME" $pool_config
+#+end_src
+
+** Key Differences
+| Option | zarch | archzfs | Notes |
+|-----------------+-------------------+-----------------------+---------------------------------|
+| compression | lz4 | zstd (configurable) | zstd better ratio, more CPU |
+| atime | off | off | Same |
+| relatime | off | on | archzfs uses relatime instead |
+| dnodesize | (default) | auto | Better extended attribute perf |
+| normalization | (default) | formD | Unicode consistency |
+
+* Snapshot Automation
+
+** zarch: zrepl Configuration
+
+zarch uses zrepl for automated snapshots with this retention grid:
+
+#+begin_example
+1x1h(keep=4) | 24x1h(keep=1) | 7x1d(keep=1) | 4x1w(keep=1) | 12x4w(keep=1) | 1x53w(keep=1)
+#+end_example
+
+This means:
+- Keep 4 snapshots within the last hour
+- Keep 1 snapshot per hour for 24 hours
+- Keep 1 snapshot per day for 7 days
+- Keep 1 snapshot per week for 4 weeks
+- Keep 1 snapshot per 4 weeks for 12 periods (48 weeks)
+- Keep 1 snapshot per year
+
+#+begin_src yaml
+# Example zrepl.yml structure
+jobs:
+ - name: snapjob
+ type: snap
+ filesystems:
+ "zroot<": true
+ snapshotting:
+ type: periodic
+ interval: 15m
+ prefix: zrepl_
+ pruning:
+ keep:
+ - type: grid
+ grid: 1x1h(keep=all) | 24x1h | 14x1d
+ regex: "^zrepl_.*"
+ - type: regex
+ negate: true
+ regex: "^zrepl_.*"
+#+end_src
+
+** archzfs: Pacman Hook Approach
+
+Our approach uses pre-transaction snapshots:
+#+begin_src bash
+# /etc/pacman.d/hooks/zfs-snapshot.hook
+[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
+#+end_src
+
+** Comparison: Snapshot Approaches
+| Feature | zrepl (zarch) | Pacman Hook (archzfs) |
+|-------------------+--------------------------+------------------------------|
+| Trigger | Time-based (15 min) | Event-based (pacman) |
+| Retention | Complex grid policy | Manual or sanoid |
+| Granularity | High (frequent) | Package transaction focused |
+| Recovery Point | ~15 minutes | Last package operation |
+| Storage overhead | Higher (more snapshots) | Lower (fewer snapshots) |
+
+** Alternative: sanoid (mentioned in archzfs)
+Sanoid provides similar functionality to zrepl with simpler configuration:
+#+begin_src ini
+# /etc/sanoid/sanoid.conf
+[zroot/ROOT/default]
+use_template = production
+recursive = yes
+
+[template_production]
+frequently = 0
+hourly = 24
+daily = 7
+weekly = 4
+monthly = 12
+yearly = 1
+autosnap = yes
+autoprune = yes
+#+end_src
+
+* EFI and Boot Partition Strategy
+
+** zarch: 512MB EFI, ZFSBootMenu
+- Single 512MB EFI partition (type EF00)
+- ZFSBootMenu EFI binary downloaded from upstream
+- No GRUB, no separate boot partition on ZFS
+- Kernel/initramfs stored on ZFS root (ZFSBootMenu reads them)
+
+** archzfs: 1GB EFI, GRUB with ZFS Support
+- 1GB EFI partition per disk
+- GRUB with ZFS module for pool access
+- Redundant EFI partitions synced via rsync
+- Boot files in EFI partition (not ZFS)
+
+** Trade-offs
+
+| Aspect | ZFSBootMenu | GRUB + ZFS |
+|---------------------+--------------------------------+------------------------------|
+| Boot environment | Native (designed for ZFS) | Requires ZFS module |
+| Snapshot booting | Built-in, interactive | Custom GRUB menu entries |
+| Encryption | Prompts for key automatically | More complex setup |
+| EFI space needed | Minimal (~512MB) | Larger (kernel/initramfs) |
+| Complexity | Simpler (single binary) | More moving parts |
+| Recovery | Can browse/rollback at boot | Requires grub.cfg regen |
+
+* Pacman Hooks and Systemd Services
+
+** zarch Services
+#+begin_example
+zfs-import-cache
+zfs-import.target
+zfs-mount
+zfs-zed
+zfs.target
+set-locale-once.service (custom first-boot locale config)
+#+end_example
+
+** archzfs Services
+#+begin_example
+zfs.target
+zfs-import-scan.service (instead of cache-based)
+zfs-mount.service
+zfs-import.target
+NetworkManager
+avahi-daemon
+sshd
+#+end_example
+
+** Key Difference: Import Method
+- zarch: Uses zfs-import-cache (requires cachefile)
+- archzfs: Uses zfs-import-scan (scans with blkid, no cachefile needed)
+
+The scan method is simpler and more portable (works if moving disks between
+systems).
+
+* mkinitcpio Configuration
+
+** zarch Approach
+#+begin_src bash
+sed -i '/^HOOKS=/s/block filesystems/block zfs filesystems/g' /etc/mkinitcpio.conf
+#+end_src
+
+** archzfs Approach
+#+begin_src bash
+HOOKS=(base udev microcode modconf kms keyboard keymap consolefont block zfs filesystems)
+#+end_src
+
+** Important Notes
+- Both use busybox-based udev (not systemd hook)
+- archzfs explicitly removes autodetect to ensure all storage drivers included
+- archzfs removes fsck (ZFS doesn't use it)
+- archzfs includes microcode early loading
+
+* Useful Patterns to Consider
+
+** 1. Profile-Based Configuration
+zarch uses a profile directory system:
+#+begin_example
+default/
+ archpkg.txt # Official packages
+ aurpkg.txt # AUR packages
+ services.txt # Services to enable
+ zarch.conf # Core configuration
+ custom-chroot.sh # Custom post-install
+#+end_example
+
+This allows maintaining multiple configurations (desktop, server, VM) cleanly.
+
+** 2. ZFSBootMenu for Simpler Boot
+For future consideration:
+- Native ZFS boot environment support
+- Interactive snapshot selection at boot
+- Simpler encryption key handling
+- Smaller EFI partition needs
+
+** 3. zrepl for Time-Based Snapshots
+For systems needing frequent snapshots beyond pacman transactions:
+- 15-minute intervals for development machines
+- Complex retention policies
+- Replication to remote systems
+
+** 4. AUR Helper Installation Pattern
+#+begin_src bash
+# Build yay as regular user, install as root
+su -c "git clone https://aur.archlinux.org/yay-bin.git" "$USER_NAME"
+arch-chroot -u "$USER_NAME" /mnt makepkg -D /home/$USER_NAME/yay-bin -s
+pacman -U --noconfirm yay-bin-*.pkg.tar.*
+#+end_src
+
+* References
+
+- [[https://github.com/sandreas/zarch][sandreas/zarch GitHub Repository]]
+- [[https://zfsbootmenu.org/][ZFSBootMenu Official Site]]
+- [[https://docs.zfsbootmenu.org/en/latest/][ZFSBootMenu Documentation]]
+- [[https://zrepl.github.io/][zrepl Documentation]]
+- [[https://wiki.archlinux.org/title/ZFS][Arch Wiki: ZFS]]
+- [[https://github.com/acrion/zfs-autosnap][zfs-autosnap - Pre-upgrade Snapshots]]
+- [[https://aur.archlinux.org/packages/pacman-zfs-hook][pacman-zfs-hook AUR Package]]
+- [[https://florianesser.ch/posts/20220714-arch-install-zbm/][Guide: Install Arch Linux on encrypted zpool with ZFSBootMenu]]
+
+* Action Items for archzfs
+
+Based on this research, potential improvements:
+
+1. [ ] Consider adding ZFSBootMenu as alternative bootloader option
+2. [ ] Evaluate zrepl for systems needing frequent time-based snapshots
+3. [ ] Document the grub-zfs-snap vs ZFSBootMenu trade-offs
+4. [ ] Consider profile-based configuration for different use cases
+5. [ ] Add sanoid configuration to archsetup for automated snapshot retention
diff --git a/docs/session-context.org b/docs/session-context.org
new file mode 100644
index 0000000..2cf29bd
--- /dev/null
+++ b/docs/session-context.org
@@ -0,0 +1,52 @@
+#+TITLE: Session Context
+#+DATE: 2026-01-22
+
+* Session: Thursday 2026-01-22 21:37 CST - ongoing
+
+** Current Task
+Creating implementation plan to replace GRUB with ZFSBootMenu in install-archzfs.
+
+** Status
+Plan written and updated with research findings.
+
+** Work Completed This Session
+
+1. Read protocols.org and NOTES.org
+2. Ran session startup workflow
+3. Found inbox item: instructions.txt (AMD GPU fix guide from earlier session)
+4. Created detailed ZFSBootMenu implementation plan
+5. Researched 5 comparable open-source projects:
+ - eoli3n/archiso-zfs + arch-config
+ - PandaScience/arch-on-zfs
+ - sandreas/zarch
+ - danboid/ALEZ
+ - danfossi/Arch-ZFS-Root-Installation-Script
+6. Updated plan with best practices from research
+
+** Key Corrections from Research
+
+CRITICAL: The original plan incorrectly proposed creating a /boot dataset.
+All researched projects agree: /boot must be a DIRECTORY inside ROOT/default,
+NOT a separate ZFS dataset. This ensures snapshots include the kernel.
+
+Other improvements adopted:
+- Set org.zfsbootmenu:commandline on ROOT parent (not ROOT/default) for inheritance
+- Add ZFSBootMenu EFI parameters: zbm.timeout, zbm.prefer, zbm.import_policy
+- Copy hostid to installed system
+- Set bootfs pool property
+
+** Files Created/Modified
+
+- PLAN-zfsbootmenu-implementation.org - Main implementation plan (project root)
+- docs/session-context.org - This file
+
+** Inbox Status
+
+1 item pending: instructions.txt (AMD GPU fix guide)
+- Recommendation: file to docs/2026-01-22-ratio-amd-gpu-freeze-fix-instructions.org
+
+** Next Steps
+
+1. File inbox item (instructions.txt)
+2. Decide whether to implement the ZFSBootMenu plan now or later
+3. If implementing: create git branch, follow plan steps, test