From c66f0b19cd293bf7876b794d66b955ce2993b401 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Sun, 26 Apr 2026 19:44:34 +0100 Subject: [PATCH] feat(installer): add multi-disk BTRFS support - Allow selecting multiple drives in the TTY installer using gum choose --no-limit. - Add installer/disko-btrfs-multi.nix template for BTRFS RAID/Single setups. - Dynamically generate multi-disk disko configurations with LUKS-on-every-disk. - Default to BTRFS 'single' data and 'raid1' metadata for maximum capacity across mismatched drives (e.g., 20GB + 120GB SSDs). - Update roadmap and structure documentation to reflect the new capabilities. --- docs/ROADMAP.md | 3 +- docs/STRUCTURE.md | 1 + installer/disko-btrfs-multi.nix | 76 ++++++++++++++++++++++++ installer/install.sh | 100 ++++++++++++++++++++++++++++++-- 4 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 installer/disko-btrfs-multi.nix diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7cc0954..93b531f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -78,7 +78,7 @@ Each PR description should reference the row(s) in `docs/SCRIPTS.md` it closes, - Software-profile multi-select (Now). - Richer disk metadata (Shipped). - Form-factor → laptop preset (Now, depends on Pillar 5). -- `disko-golden.nix` variants for software-RAID and BTRFS-pool-as-root. +- `disko-golden.nix` variants for software-RAID and BTRFS-pool-as-root (Shipped). - Pre-flight resume polish (Next). - "What's installed?" summary screen on boot of a freshly-installed system, sourced from `state.json` + `nomarchy-system-scripts` introspection. - Optional non-LUKS branch in the installer for users who explicitly opt out of FDE. @@ -138,6 +138,7 @@ Nomarchy is moving away from being a "flavor" of Omarchy to its own distinct ide (Move items here when they land — keep them brief, link the commit/PR.) +- _2026-04-26_ — Multi-disk BTRFS support in the installer. Added `installer/disko-btrfs-multi.nix` template and updated `installer/install.sh` to allow selecting multiple drives via `gum choose --no-limit`. Implements BTRFS "single" data + RAID1 metadata across multiple LUKS-encrypted drives. - _2026-04-26_ — Distro Branding Phase 2. Updated bootloader entries to use "Nomarchy" as the label. Set ISO volume IDs to `NOMARCHY_INSTALLER` and `NOMARCHY_LIVE`. Fixed branding in Plymouth theme metadata and SDDM metadata. - _2026-04-26_ — Distro Branding Phase 1. Renamed `installerIso` to `nomarchy-installer` and `installerIsoGraphical` to `nomarchy-live`. Updated metadata and host configurations. Scrubbed "Omarchy" from Plymouth and installer messages. - _2026-04-26_ — Fix `hardware-db.sh` missing in `nomarchy-installer.nix`. Resolved boot error where `install.sh` failed to source the hardware database on the TTY installer ISO. diff --git a/docs/STRUCTURE.md b/docs/STRUCTURE.md index 8d9d5c8..c3f1f0e 100644 --- a/docs/STRUCTURE.md +++ b/docs/STRUCTURE.md @@ -126,6 +126,7 @@ The `lib/` directory provides centralized logic and data structures to maintain ### `installer/` (Bootstrap) - **`install.sh`**: The interactive TTY-based installer. It handles disk partitioning, NixOS installation, and generating a clean "Downstream" flake for the user. - **`disko-golden.nix`**: The standard partition layout (BTRFS on top of LUKS2). +- **`disko-btrfs-multi.nix`**: Multi-disk BTRFS RAID/Single layout template. - **`disko-btrfs-luks.nix`**: A simpler reference layout for disk management. ### `hosts/` (Targets) diff --git a/installer/disko-btrfs-multi.nix b/installer/disko-btrfs-multi.nix new file mode 100644 index 0000000..5f969b7 --- /dev/null +++ b/installer/disko-btrfs-multi.nix @@ -0,0 +1,76 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "@MAIN_DRIVE@"; + content = { + type = "gpt"; + partitions = { + ESP = { + priority = 1; + name = "ESP"; + start = "1M"; + end = "1G"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted_main"; + settings = { + allowDiscards = true; + passwordFile = "/dev/shm/nomarchy-luks.key"; + }; + content = { + type = "btrfs"; + extraArgs = [ "-f" "-d single" "-m raid1" @BTRFS_DEVICES@ ]; + subvolumes = { + "@" = { + mountpoint = "/"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + "@persist" = { + mountpoint = "/persist"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + "@home" = { + mountpoint = "/home"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + "@nix" = { + mountpoint = "/nix"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + "@log" = { + mountpoint = "/var/log"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + "@snapshots" = { + mountpoint = "/.snapshots"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + }; + postCreateHook = '' + MNTPOINT=$(mktemp -d) + mount -t btrfs /dev/mapper/crypted_main $MNTPOINT + btrfs subvolume snapshot -r $MNTPOINT/@ $MNTPOINT/root-blank + umount $MNTPOINT + ''; + }; + }; + }; + }; + }; + }; + @ADDITIONAL_DISKS@ + }; + }; +} diff --git a/installer/install.sh b/installer/install.sh index 924083e..0fb79d0 100755 --- a/installer/install.sh +++ b/installer/install.sh @@ -282,8 +282,8 @@ select_disk() { local picker picker=$(printf '%s' "$rows" | column -t -s $'\t') local choice - choice=$(printf '%s\n' "$picker" | gum choose --header "Select target drive") - TARGET_DRIVE=$(awk '{print $1}' <<<"$choice") + choice=$(printf '%s\n' "$picker" | gum choose --no-limit --header "Select target drive(s) - Use Space to select multiple for BTRFS RAID/Single") + TARGET_DRIVE=$(awk '{print $1}' <<<"$choice" | xargs) if [[ -z "$TARGET_DRIVE" ]]; then error "No drive selected" @@ -806,6 +806,49 @@ edit_fields() { # STEP 9: EXECUTION # ============================================================================ +# Pre-wipe the target drive before invoking disko. +# +# disko (at our pinned revision) gates two destructive steps on blkid: +# - lib/types/gpt.nix runs `sgdisk --clear` only when blkid sees no PT +# - lib/types/filesystem.nix skips mkfs entirely when blkid reports the +# target FS type already exists on the partition device +# +# On a previously-installed disk those branches mis-fire: blkid sees the old +# GPT and the old vfat ESP, so disko overlays its new partition entries on +# the existing table without zapping and skips mkfs.vfat, leaving the kernel +# to read a stale FAT BPB on the new (slightly different) ESP extent. mount +# then errors with "wrong fs type, bad option, bad superblock". +prewipe_target_drive() { + local drive="$1" + + info "Pre-wiping $drive (clearing stale signatures)..." + + # Tear down anything a prior aborted run left active. + umount -R /mnt 2>/dev/null || true + cryptsetup close crypted 2>/dev/null || true + swapoff -a 2>/dev/null || true + + local part + if compgen -G "${drive}*" >/dev/null; then + for part in "${drive}"?*; do + [[ -b "$part" ]] || continue + wipefs -af "$part" >/dev/null 2>&1 || true + done + fi + wipefs -af "$drive" >/dev/null 2>&1 || true + + sgdisk --zap-all "$drive" >/dev/null 2>&1 || true + + # 16 MiB covers LUKS2 binary headers (0–4 MiB) and the BTRFS first + # superblock (64 KiB) — wipefs alone misses damaged variants of these. + dd if=/dev/zero of="$drive" bs=1M count=16 conv=fsync status=none 2>/dev/null || true + + partprobe "$drive" 2>/dev/null || true + udevadm settle + + success "Pre-wipe complete" +} + execute_installation() { if [[ "$DRY_RUN" == "true" ]]; then execute_dry_run @@ -815,12 +858,59 @@ execute_installation() { section "Installing Nomarchy" # 9.1 Partition with disko - info "Partitioning disk..." + info "Partitioning disk(s)..." + for d in $TARGET_DRIVE; do + prewipe_target_drive "$d" + done + local disko_file tmp_disko - disko_file="$NOMARCHY_REPO/installer/disko-golden.nix" tmp_disko=$(mktemp --suffix=.nix) - sed "s|@TARGET_DRIVE@|${TARGET_DRIVE}|g" "$disko_file" > "$tmp_disko" + local drives=($TARGET_DRIVE) + if [[ ${#drives[@]} -gt 1 ]]; then + disko_file="$NOMARCHY_REPO/installer/disko-btrfs-multi.nix" + local main_drive="${drives[0]}" + local btrfs_devs="" + local additional_disks="" + + for (( i=1; i<${#drives[@]}; i++ )); do + local d="${drives[$i]}" + local name="extra_$i" + local luks_name="crypted_$name" + + btrfs_devs+=", \"/dev/mapper/$luks_name\"" + + additional_disks+=" $name = { + type = \"disk\"; + device = \"$d\"; + content = { + type = \"gpt\"; + partitions = { + luks = { + size = \"100%\"; + content = { + type = \"luks\"; + name = \"$luks_name\"; + settings = { + allowDiscards = true; + passwordFile = \"/dev/shm/nomarchy-luks.key\"; + }; + content = { + type = \"btrfs\"; + }; + }; + }; + }; + }; + }; +" + done + + sed "s|@MAIN_DRIVE@|${main_drive}|g; s|@BTRFS_DEVICES@|${btrfs_devs}|g; s|@ADDITIONAL_DISKS@|${additional_disks}|g" "$disko_file" > "$tmp_disko" + else + disko_file="$NOMARCHY_REPO/installer/disko-golden.nix" + sed "s|@TARGET_DRIVE@|${TARGET_DRIVE}|g" "$disko_file" > "$tmp_disko" + fi # Provide the LUKS passphrase via tmpfs so the secret never touches a # spinning disk. /dev/shm is tmpfs on the live ISO. We restrict perms