diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | PLAN-zfsbootmenu-implementation.org | 712 | ||||
| -rwxr-xr-x | build.sh | 17 | ||||
| -rw-r--r-- | custom/40_zfs_snapshots | 13 | ||||
| -rw-r--r-- | custom/grub-zfs-snap | 160 | ||||
| -rwxr-xr-x | custom/install-archzfs | 227 | ||||
| -rwxr-xr-x | custom/zfs-snap-prune | 208 | ||||
| -rwxr-xr-x | custom/zfsrollback | 10 | ||||
| -rwxr-xr-x | custom/zfssnapshot | 9 | ||||
| -rw-r--r-- | custom/zz-grub-zfs-snap.hook | 22 | ||||
| -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.org | 365 | ||||
| -rw-r--r-- | docs/session-context.org | 52 |
13 files changed, 1232 insertions, 564 deletions
@@ -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 @@ -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 |
