Files
Nomarchy/installer/install.sh
Bernardo Magri c66f0b19cd 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.
2026-04-26 19:44:34 +01:00

1463 lines
48 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -e
# Nomarchy TTY Installer
# Golden path: BTRFS + LUKS2 encryption
#
# This is a minimal, single-path installer designed for TTY-only environments.
# For a customized installation, manually set up your disk and use the generated
# flake configuration as a starting point.
# Load the hardware-detection database — resolved relative to this script so it
# works whether we're invoked from /etc/install.sh on the live ISO or straight
# from a checkout.
_NOMARCHY_INSTALL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=hardware-db.sh
source "$_NOMARCHY_INSTALL_DIR/hardware-db.sh"
# Colors and styling
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Installer state
NOMARCHY_REPO=""
NOMARCHY_REV=""
HOSTNAME=""
TARGET_DRIVE=""
USERNAME=""
LUKS_PASSWORD=""
USER_PASSWORD=""
TIMEZONE="UTC"
KEYMAP_LAYOUT=""
KEYMAP_VARIANT=""
LOCALE=""
FORM_FACTOR=""
HARDWARE_MODULES=""
NOMARCHY_HW_OPTS=""
SELECTED_PROFILES=""
# "" = not yet answered; "true"/"false" set by configure_impermanence.
ENABLE_IMPERMANENCE=""
# Curated short lists for the keymap/locale prompts. "Other…" drops the user
# into the full `localectl` list via gum filter.
COMMON_KEYMAPS=(
us de fr gb es it pt br se no fi dk nl pl ru ua ch jp kr cn ar
)
COMMON_LOCALES=(
en_US.UTF-8 en_GB.UTF-8 de_DE.UTF-8 fr_FR.UTF-8 es_ES.UTF-8
it_IT.UTF-8 pt_BR.UTF-8 pt_PT.UTF-8 nl_NL.UTF-8 sv_SE.UTF-8
ja_JP.UTF-8 zh_CN.UTF-8 ko_KR.UTF-8
)
# CLI flags
DRY_RUN="false"
RESUME="false"
# Resumable answers — passwords are NEVER persisted; the user re-enters them.
STATE_FILE="/tmp/nomarchy-install.state.sh"
# ============================================================================
# CLI / PERSISTENCE
# ============================================================================
usage() {
cat <<USAGE
Usage: install.sh [--dry-run] [--resume] [-h|--help]
--dry-run Generate the flake into a tmpdir and run \`nix flake check\`.
Doesn't touch the disk, doesn't run nixos-install.
--resume Reuse answers from a previous interrupted run
(saved at $STATE_FILE — passwords excluded).
-h, --help Print this message.
USAGE
}
parse_args() {
while (( $# > 0 )); do
case "$1" in
--dry-run) DRY_RUN="true" ;;
--resume) RESUME="true" ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
shift
done
}
# Persist non-secret answers so an interrupted install can pick up where it
# left off. Uses `declare -p` so each line is a self-contained `declare --`
# statement that `source` re-establishes verbatim.
save_state() {
declare -p \
TARGET_DRIVE USERNAME HOSTNAME TIMEZONE \
KEYMAP_LAYOUT KEYMAP_VARIANT LOCALE FORM_FACTOR \
ENABLE_IMPERMANENCE HARDWARE_MODULES NOMARCHY_HW_OPTS \
SELECTED_PROFILES NOMARCHY_REV \
> "$STATE_FILE"
}
load_state() {
if [[ "$RESUME" == "true" ]] && [[ -f "$STATE_FILE" ]]; then
# shellcheck disable=SC1090
source "$STATE_FILE"
info "Resumed from $STATE_FILE"
fi
}
# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================
# Helper to run commands via nix run
nrun() {
local pkg="$1"
shift
nix run --extra-experimental-features "nix-command flakes" "nixpkgs#$pkg" -- "$@"
}
header() {
clear
nrun gum style \
--foreground 212 --border-foreground 212 --border double \
--align center --width 60 --margin "1 2" --padding "2 4" \
"NOMARCHY INSTALLER" "Nomarchy Distribution"
echo ""
}
section() {
echo ""
nrun gum style --foreground 14 --bold "━━━ $1 ━━━"
echo ""
}
success() {
nrun gum style --foreground 10 "$1"
}
error() {
nrun gum style --foreground 9 "$1"
}
info() {
nrun gum style --foreground 12 "$1"
}
# ============================================================================
# STEP 1: ENVIRONMENT CHECK
# ============================================================================
check_environment() {
section "Environment Check"
# Check for root
if [[ $EUID -ne 0 ]]; then
error "This installer must be run as root (use sudo)"
exit 1
fi
success "Running as root"
# Confirm we booted via UEFI. The disko config lays out a GPT + ESP and
# we install systemd-boot/grub-efi; on a BIOS-booted host that doesn't
# work and would partial-install before failing.
if [[ ! -d /sys/firmware/efi ]]; then
error "This installer requires a UEFI system (no /sys/firmware/efi)."
info "Reboot in UEFI mode (disable CSM/legacy in firmware setup) and try again."
exit 1
fi
success "UEFI boot detected"
# Find Nomarchy repo
if [[ -d "/etc/nomarchy" ]]; then
NOMARCHY_REPO="/etc/nomarchy"
elif [[ -d "$(dirname "$0")/.." ]] && [[ -f "$(dirname "$0")/../flake.nix" ]]; then
NOMARCHY_REPO="$(realpath "$(dirname "$0")/..")"
fi
if [[ -z "$NOMARCHY_REPO" ]]; then
error "Nomarchy repository not found"
exit 1
fi
success "Found Nomarchy at $NOMARCHY_REPO"
# Capture the exact commit we're installing from. The generated flake
# pins `nomarchy.url` to this revision so the installed system can't
# silently drift onto a newer (possibly breaking) main.
if command -v git >/dev/null 2>&1 && [[ -d "$NOMARCHY_REPO/.git" ]]; then
NOMARCHY_REV=$(git -C "$NOMARCHY_REPO" rev-parse HEAD 2>/dev/null || echo "")
fi
if [[ -n "$NOMARCHY_REV" ]]; then
success "Pinning Nomarchy to $NOMARCHY_REV"
else
info "Could not determine Nomarchy revision; downstream flake will track main."
fi
# Check internet
gum spin --spinner dot --title "Checking internet connection..." -- sleep 1
while ! ping -c 1 -W 2 1.1.1.1 &>/dev/null; do
error "No internet connection"
local choice
choice=$(gum choose "Open Network Manager (nmtui)" "Retry" "Exit")
case "$choice" in
*nmtui*) nmtui ;;
*Exit*) exit 1 ;;
esac
done
success "Internet connection verified"
}
# ============================================================================
# STEP 2: DISK SELECTION
# ============================================================================
select_disk() {
section "Disk Selection"
if [[ -n "$TARGET_DRIVE" ]]; then
success "Resumed: $TARGET_DRIVE"
return
fi
# Build a richer drive table than the bare `NAME SIZE` lsblk default.
# Columns: NAME, SIZE, TYPE (NVMe/USB/SSD/HDD), VENDOR, MODEL, SERIAL.
# Empty fields render as "--" so column -t can still align them.
local raw rows=""
raw=$(lsblk -d -n -p -o NAME,SIZE,ROTA,TRAN,VENDOR,MODEL,SERIAL 2>/dev/null \
| grep -vE '^(/dev/(loop|ram|zram|sr))')
while IFS= read -r line; do
[[ -z "$line" ]] && continue
# NAME and SIZE are reliably whitespace-free; ROTA/TRAN are short
# tokens; VENDOR/MODEL/SERIAL can carry internal spaces. Pull the
# first four fields off the front, treat the rest as the
# vendor/model/serial trio split via the original lsblk column
# widths — easier to just re-query each device for clean values.
local dev size rota tran
read -r dev size rota tran _ <<<"$line"
local type vendor model serial
case "$tran" in
nvme) type="NVMe" ;;
usb) type="USB" ;;
sata|scsi)
if [[ "$rota" == "1" ]]; then type="HDD"; else type="SSD"; fi
;;
*)
if [[ "$rota" == "1" ]]; then type="HDD"; else type="SSD"; fi
;;
esac
vendor=$(lsblk -d -n -o VENDOR "$dev" 2>/dev/null | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//')
model=$(lsblk -d -n -o MODEL "$dev" 2>/dev/null | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//')
serial=$(lsblk -d -n -o SERIAL "$dev" 2>/dev/null | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//')
[[ -z "$vendor" ]] && vendor="--"
[[ -z "$model" ]] && model="--"
[[ -z "$serial" ]] && serial="--"
# Tab-separated for column -t -s, then collapse internal whitespace
# in MODEL so multi-space brand strings don't break alignment.
rows+=$(printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
"$dev" "$size" "$type" "$vendor" "${model//$'\t'/ }" "$serial")
rows+=$'\n'
done <<<"$raw"
if [[ -z "$rows" ]]; then
error "No installable drives found."
exit 1
fi
info "Available drives:"
echo ""
{
printf 'NAME\tSIZE\tTYPE\tVENDOR\tMODEL\tSERIAL\n'
printf '%s' "$rows"
} | column -t -s $'\t'
echo ""
# gum choose gets the same aligned rows so the picker reads like the table.
local picker
picker=$(printf '%s' "$rows" | column -t -s $'\t')
local 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"
exit 1
fi
if [[ "$DRY_RUN" != "true" ]]; then
echo ""
gum style --foreground 9 --bold "⚠ WARNING: All data on $TARGET_DRIVE will be DESTROYED!"
echo ""
if ! gum confirm "Are you sure you want to use $TARGET_DRIVE?"; then
error "Aborted"
exit 1
fi
fi
success "Selected: $TARGET_DRIVE"
save_state
}
# ============================================================================
# STEP 3: LUKS PASSPHRASE
# ============================================================================
get_luks_passphrase() {
if [[ "$DRY_RUN" == "true" ]]; then
info "Dry run: skipping LUKS passphrase prompt."
LUKS_PASSWORD="dryrun-not-used"
return
fi
# Already set this session (review-edit loop iterated). Don't re-prompt;
# password isn't persisted across runs but is held in memory until
# execute_installation unsets it.
[[ -n "${LUKS_PASSWORD:-}" ]] && return
section "Disk Encryption"
info "Your disk will be encrypted with LUKS2."
info "Enter a strong passphrase (you'll need this at every boot)."
echo ""
local pass1 pass2
while true; do
pass1=$(gum input --password --placeholder "Enter LUKS passphrase")
[[ -z "$pass1" ]] && continue
pass2=$(gum input --password --placeholder "Confirm passphrase")
if [[ "$pass1" == "$pass2" ]]; then
LUKS_PASSWORD="$pass1"
break
else
error "Passphrases do not match. Try again."
fi
done
success "Encryption passphrase set"
}
# ============================================================================
# STEP 4: USER CONFIGURATION
# ============================================================================
configure_user() {
section "User Configuration"
if [[ -z "$USERNAME" ]]; then
USERNAME=$(nrun gum input --placeholder "Enter username (lowercase, no spaces)")
if [[ -z "$USERNAME" ]] || [[ ! "$USERNAME" =~ ^[a-z][a-z0-9_-]*$ ]]; then
error "Invalid username"
exit 1
fi
fi
success "Username: $USERNAME"
if [[ -z "$HOSTNAME" ]]; then
HOSTNAME=$(nrun gum input --value "nomarchy" --placeholder "Hostname for this machine")
if [[ -z "$HOSTNAME" ]] || [[ ! "$HOSTNAME" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
error "Invalid hostname (use lowercase letters, digits, and hyphens only)"
exit 1
fi
fi
success "Hostname: $HOSTNAME"
if [[ "$DRY_RUN" == "true" ]]; then
info "Dry run: skipping user password prompt."
USER_PASSWORD="dryrun-not-used"
save_state
return
fi
# User password (can be same as LUKS or different)
info "Set a password for your user account"
local pass1 pass2
while true; do
pass1=$(nrun gum input --password --placeholder "Enter user password")
[[ -z "$pass1" ]] && continue
pass2=$(nrun gum input --password --placeholder "Confirm user password")
if [[ "$pass1" == "$pass2" ]]; then
USER_PASSWORD="$pass1"
break
else
error "Passwords do not match. Try again."
fi
done
success "User password set"
save_state
}
# ============================================================================
# STEP 5a: KEYBOARD & LANGUAGE
# ============================================================================
# Curated short list first ("Other…" drops into the full localectl list).
# Applied IMMEDIATELY to the running session so the rest of the install types
# correctly — TTY uses loadkeys, Hyprland uses hyprctl. Both best-effort.
select_keymap_locale() {
section "Keyboard & Language"
if [[ -z "$KEYMAP_LAYOUT" ]]; then
local choice
choice=$(printf '%s\n' "${COMMON_KEYMAPS[@]}" "Other…" \
| nrun gum choose --header "Keyboard layout")
if [[ "$choice" == "Other…" ]]; then
KEYMAP_LAYOUT=$(localectl list-x11-keymap-layouts 2>/dev/null \
| nrun gum filter --placeholder "Search keyboard layout…")
else
KEYMAP_LAYOUT="$choice"
fi
[[ -z "$KEYMAP_LAYOUT" ]] && KEYMAP_LAYOUT="us"
fi
success "Keyboard layout: $KEYMAP_LAYOUT"
# Variant — optional. Only prompt if the layout actually has variants.
if [[ -z "$KEYMAP_VARIANT" ]]; then
local variants
variants=$(localectl list-x11-keymap-variants "$KEYMAP_LAYOUT" 2>/dev/null || true)
if [[ -n "$variants" ]]; then
local v
v=$(printf '(none)\n%s\n' "$variants" \
| nrun gum filter --placeholder "Variant (optional)" --value "(none)")
[[ "$v" == "(none)" || -z "$v" ]] || KEYMAP_VARIANT="$v"
fi
fi
[[ -n "$KEYMAP_VARIANT" ]] && success "Variant: $KEYMAP_VARIANT" || success "Variant: (default)"
# Apply to the live session, best-effort.
loadkeys "$KEYMAP_LAYOUT" 2>/dev/null || true
if [[ -n "${WAYLAND_DISPLAY:-}" ]] && command -v hyprctl >/dev/null 2>&1; then
hyprctl keyword input:kb_layout "$KEYMAP_LAYOUT" >/dev/null 2>&1 || true
hyprctl keyword input:kb_variant "$KEYMAP_VARIANT" >/dev/null 2>&1 || true
fi
if [[ -z "$LOCALE" ]]; then
local choice
choice=$(printf '%s\n' "${COMMON_LOCALES[@]}" "Other…" \
| nrun gum choose --header "Language / locale")
if [[ "$choice" == "Other…" ]]; then
LOCALE=$(localectl list-locales 2>/dev/null \
| nrun gum filter --placeholder "Search locale…")
else
LOCALE="$choice"
fi
[[ -z "$LOCALE" ]] && LOCALE="en_US.UTF-8"
fi
success "Locale: $LOCALE"
save_state
}
# ============================================================================
# STEP 5: TIMEZONE
# ============================================================================
select_timezone() {
section "Timezone"
if [[ -n "$TIMEZONE" ]] && [[ "$TIMEZONE" != "UTC" ]]; then
success "Resumed: $TIMEZONE"
return
fi
local timezones
timezones=$(timedatectl list-timezones 2>/dev/null || echo "UTC")
TIMEZONE=$(echo "$timezones" | gum filter --placeholder "Search timezone...")
[[ -z "$TIMEZONE" ]] && TIMEZONE="UTC"
success "Timezone: $TIMEZONE"
save_state
}
# ============================================================================
# STEP 6: HARDWARE VENDOR
# ============================================================================
select_hardware() {
section "Hardware Configuration"
if [[ -n "$HARDWARE_MODULES" ]]; then
success "Resumed hardware modules"
return
fi
local dmi_vendor dmi_product detect_output
dmi_vendor=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "Unknown")
dmi_product=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "Unknown")
info "DMI: $dmi_vendor / $dmi_product"
echo ""
# Auto-detect CPU, GPU, chassis, and known model from hardware-db.sh.
detect_output=$(nomarchy_detect_hw || true)
echo "Auto-detected:"
nomarchy_hw_summary <<< "$detect_output"
echo ""
# Collect modules + nomarchy options from the detector output.
local modules=() hw_opts=()
while IFS= read -r line; do
case "$line" in
"MODULE "*) modules+=("${line#MODULE }") ;;
"OPT "*) hw_opts+=("${line#OPT }") ;;
esac
done <<< "$detect_output"
# Let the user accept, extend, or replace the detection.
local choice
choice=$(nrun gum choose --header "Hardware configuration" \
"Accept detected modules" \
"Add an extra nixos-hardware module" \
"Pick from the manual list (override)")
case "$choice" in
"Add an extra nixos-hardware module")
local extra
extra=$(nrun gum input --placeholder "e.g. asus-zephyrus-ga401 (no 'nixos-hardware.' prefix)")
[[ -n "$extra" ]] && modules+=("$extra")
;;
"Pick from the manual list (override)")
modules=()
hw_opts=()
_select_hardware_manual modules hw_opts
;;
esac
# De-duplicate while preserving order.
local seen="" uniq_mods=() m
for m in "${modules[@]}"; do
if [[ ":$seen:" != *":$m:"* ]]; then
uniq_mods+=("$m")
seen="$seen:$m"
fi
done
# Emit a list the heredoc in generate_flake_config splats into
# hardware-selection.nix's imports. The heredoc already indents the first
# line by 4 spaces — we add real newlines + 4 spaces (via $'\n ') for
# subsequent lines so every entry lines up.
HARDWARE_MODULES=""
for m in "${uniq_mods[@]}"; do
[[ -z "$HARDWARE_MODULES" ]] || HARDWARE_MODULES+=$'\n '
HARDWARE_MODULES+="inputs.nixos-hardware.nixosModules.${m}"
done
# Same treatment for nomarchy.hardware.* toggles.
NOMARCHY_HW_OPTS=""
local o
for o in "${hw_opts[@]}"; do
# opt is e.g. `isFramework=true` → `nomarchy.hardware.isFramework = true;`
local key="${o%%=*}" val="${o#*=}"
NOMARCHY_HW_OPTS+="nomarchy.hardware.${key} = ${val};"$'\n '
done
success "Hardware configuration set (${#uniq_mods[@]} module$([[ ${#uniq_mods[@]} -eq 1 ]] || echo s))"
save_state
}
# Manual fallback menu, kept for odd hardware the DB doesn't recognise yet.
# Writes into the two arrays named by its arguments (bash 4.3+ nameref).
_select_hardware_manual() {
local -n _mods_ref="$1"
local -n _opts_ref="$2"
local vendor
vendor=$(nrun gum choose --header "Pick vendor" \
"Framework" "Dell" "Lenovo" "Apple (T2 Mac)" "Microsoft Surface" "ASUS" "System76" "Other...")
case "$vendor" in
Framework)
local model
model=$(nrun gum choose \
"framework-16-7040-amd" \
"framework-13-amd-ai-300-series" \
"framework-13-7040-amd" \
"framework-13-13th-gen-intel" \
"framework-13-12th-gen-intel" \
"framework-13-11th-gen-intel")
_mods_ref+=("$model")
_opts_ref+=("isFramework=true")
;;
Dell)
local model
model=$(nrun gum choose \
"dell-xps-15-9500" "dell-xps-15-9510" "dell-xps-15-9520" \
"dell-xps-13-9310" "dell-xps-13-9370" "dell-xps-13-9380" \
"dell-precision-5530" "dell-latitude-7480")
_mods_ref+=("$model")
[[ "$model" == *xps* ]] && _opts_ref+=("isXPS=true")
;;
Lenovo)
local model
model=$(nrun gum choose \
"lenovo-thinkpad-x1-carbon-gen11" "lenovo-thinkpad-x1-carbon-gen10" \
"lenovo-thinkpad-x1-carbon-gen9" "lenovo-thinkpad-x1-extreme" \
"lenovo-thinkpad-t14-amd-gen3" "lenovo-thinkpad-t14-amd-gen2" \
"lenovo-thinkpad-t480" "lenovo-thinkpad-l13")
_mods_ref+=("$model")
;;
"Apple (T2 Mac)")
_mods_ref+=("apple-t2")
_opts_ref+=("isT2Mac=true")
;;
"Microsoft Surface")
local model
model=$(nrun gum choose \
"microsoft-surface-pro-9" "microsoft-surface-pro-8" "microsoft-surface-pro-7" \
"microsoft-surface-laptop-5" "microsoft-surface-laptop-4" "microsoft-surface-laptop-3")
_mods_ref+=("$model")
;;
ASUS)
local model
model=$(nrun gum choose \
"asus-zephyrus-ga403" "asus-zephyrus-ga402" "asus-zephyrus-ga401" \
"asus-zephyrus-ga503" "asus-rog-strix-g513" "asus-zenbook-ux")
_mods_ref+=("$model")
;;
System76)
_mods_ref+=("system76")
;;
"Other...")
local custom
custom=$(nrun gum input --placeholder "e.g. asus-zephyrus-ga401 (no 'nixos-hardware.' prefix)")
[[ -n "$custom" ]] && _mods_ref+=("$custom")
;;
esac
}
# ============================================================================
# STEP 6b: FORM FACTOR (LAPTOP / DESKTOP)
# ============================================================================
# Auto-detects via /sys/class/power_supply/BAT* — same signal hardware-db.sh
# uses to pick common-pc-laptop vs common-pc. The user can flip the answer
# (e.g. a desktop with a UPS that exposes a BAT*, or a laptop that doesn't).
confirm_form_factor() {
section "Form Factor"
if [[ -n "$FORM_FACTOR" ]]; then
success "Resumed: $FORM_FACTOR"
return
fi
local default="desktop"
if compgen -G "/sys/class/power_supply/BAT*" >/dev/null; then
default="laptop"
fi
info "Auto-detected: $default"
if nrun gum confirm "Treat this machine as a $default?"; then
FORM_FACTOR="$default"
else
FORM_FACTOR=$([[ "$default" == "laptop" ]] && echo desktop || echo laptop)
fi
success "Form factor: $FORM_FACTOR"
save_state
}
# ============================================================================
# STEP 7: IMPERMANENCE (OPTIONAL)
# ============================================================================
configure_impermanence() {
section "Impermanence (Optional)"
if [[ "$RESUME" == "true" ]] && [[ "$ENABLE_IMPERMANENCE" != "" ]]; then
success "Resumed: impermanence = $ENABLE_IMPERMANENCE"
return
fi
info "Impermanence erases your root filesystem on every boot."
info "Only explicitly persisted files survive reboots."
info "This provides a clean, reproducible system."
echo ""
if gum confirm "Enable Impermanence?"; then
ENABLE_IMPERMANENCE="true"
success "Impermanence enabled"
else
info "Impermanence disabled (traditional persistent root)"
fi
save_state
}
# ============================================================================
# STEP 8: SOFTWARE PROFILES (OPTIONAL)
# ============================================================================
select_profiles() {
section "Software Profiles"
if [[ -n "$SELECTED_PROFILES" ]]; then
success "Resumed selected profiles"
return
fi
info "Pick optional software profiles to include in your configuration."
info "Multiple selection allowed (Space to select, Enter to confirm)."
echo ""
SELECTED_PROFILES=$(nrun gum choose --no-limit --header "Select Profiles" \
"Dev (VSCode, Git, Lazygit, Zed, Docker)" \
"Gaming (Steam, Gamemode, Lutris, Heroic)" \
"Office (LibreOffice, Thunderbird, Obsidian, Zotero)" \
"Media (VLC, OBS Studio, GIMP, Inkscape, Spotify)" \
"CLI Utils (ripgrep, fd, bat, eza, zoxide, fzf)")
if [[ -z "$SELECTED_PROFILES" ]]; then
info "No profiles selected (minimal install)"
else
# Count selected profiles by number of lines
local count
count=$(echo "$SELECTED_PROFILES" | grep -c "^" || echo 0)
success "Selected $count profile(s)"
fi
save_state
}
# ============================================================================
# STEP 9: REVIEW & CONFIRM
# ============================================================================
review_configuration() {
section "Review Configuration"
echo " Drive: $TARGET_DRIVE (BTRFS + LUKS2)"
echo " Hostname: $HOSTNAME"
echo " Username: $USERNAME"
echo " Keymap: $KEYMAP_LAYOUT${KEYMAP_VARIANT:+ ($KEYMAP_VARIANT)}"
echo " Locale: $LOCALE"
echo " Timezone: $TIMEZONE"
echo " Form factor: $FORM_FACTOR"
echo " Impermanence: $ENABLE_IMPERMANENCE"
echo " Profiles: ${SELECTED_PROFILES:-None}"
echo " Nomarchy rev: ${NOMARCHY_REV:-main (unpinned)}"
echo ""
if [[ "$DRY_RUN" == "true" ]]; then
info "Dry run: skipping destructive confirmation."
return 0
fi
nrun gum style --foreground 9 "This will DESTROY all data on $TARGET_DRIVE"
echo ""
local action
action=$(nrun gum choose --header "Choose:" \
"Continue with installation" \
"Edit a field" \
"Abort")
case "$action" in
"Continue with installation") return 0 ;;
"Edit a field") edit_fields; return 2 ;;
*) error "Aborted"; exit 1 ;;
esac
}
# Multi-select clear: each cleared field is re-prompted on the next loop
# iteration in main() because every prompt short-circuits when its var is
# non-empty. Hardware clears the cached `nixos-hardware` modules and per-host
# option lines together so they stay consistent. Passwords are never offered
# here — get_luks_passphrase short-circuits when LUKS_PASSWORD is already set,
# so editing a field doesn't re-ask for the LUKS passphrase.
edit_fields() {
section "Edit Fields"
local choices
choices=$(nrun gum choose --no-limit --header "Pick fields to re-enter (space to select, enter to confirm):" \
"Drive ($TARGET_DRIVE)" \
"User and host ($USERNAME @ $HOSTNAME)" \
"Keymap and locale ($KEYMAP_LAYOUT / $LOCALE)" \
"Timezone ($TIMEZONE)" \
"Hardware ($HARDWARE_MODULES)" \
"Form factor ($FORM_FACTOR)" \
"Impermanence ($ENABLE_IMPERMANENCE)" \
"Profiles (${SELECTED_PROFILES:-none})" \
"Nomarchy rev (${NOMARCHY_REV:-main})")
[[ -z "$choices" ]] && return
while IFS= read -r f; do
case "$f" in
"Drive"*) TARGET_DRIVE="" ;;
"User and host"*) USERNAME=""; HOSTNAME="" ;;
"Keymap"*) KEYMAP_LAYOUT=""; KEYMAP_VARIANT=""; LOCALE="" ;;
"Timezone"*) TIMEZONE="" ;;
"Hardware"*) HARDWARE_MODULES=""; NOMARCHY_HW_OPTS="" ;;
"Form factor"*) FORM_FACTOR="" ;;
"Impermanence"*) ENABLE_IMPERMANENCE="" ;;
"Profiles"*) SELECTED_PROFILES="" ;;
"Nomarchy rev"*) NOMARCHY_REV="" ;;
esac
done <<<"$choices"
}
# ============================================================================
# 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 (04 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
return
fi
section "Installing Nomarchy"
# 9.1 Partition with disko
info "Partitioning disk(s)..."
for d in $TARGET_DRIVE; do
prewipe_target_drive "$d"
done
local disko_file tmp_disko
tmp_disko=$(mktemp --suffix=.nix)
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
# to root and shred the file (overwrite) on the way out, even though
# it's already in RAM — defense in depth.
local luks_key="/dev/shm/nomarchy-luks.key"
install -m 600 /dev/null "$luks_key"
printf '%s' "$LUKS_PASSWORD" > "$luks_key"
disko --mode disko "$tmp_disko"
shred -u "$luks_key" 2>/dev/null || rm -f "$luks_key"
unset LUKS_PASSWORD
success "Disk partitioned"
# 9.2 Generate hardware config
info "Generating hardware configuration..."
mkdir -p /mnt/etc/nixos
nixos-generate-config --root /mnt
success "Hardware configuration generated"
# 9.3 Generate flake configuration
info "Creating system configuration..."
generate_flake_config
success "Configuration generated"
# 9.4 Resolve inputs once, here, and lock them. First boot then consumes
# the same flake.lock and doesn't re-resolve a newer upstream.
info "Resolving flake inputs (this pins nomarchy, nixpkgs, etc.)..."
(
cd /mnt/etc/nixos
nix --extra-experimental-features "nix-command flakes" flake lock >/dev/null
)
success "flake.lock written"
# 9.5 Initialize git repo so `nix` treats /etc/nixos as a flake worktree.
info "Initializing git repository..."
(
cd /mnt/etc/nixos
nrun git git init -q
nrun git git add .
nrun git git config user.name "Nomarchy Installer"
nrun git git config user.email "installer@nomarchy"
nrun git git commit -qm "Initial Nomarchy configuration"
)
success "Git repository initialized"
# 9.6 Handle impermanence
if [[ "$ENABLE_IMPERMANENCE" == "true" ]]; then
info "Setting up impermanence..."
mkdir -p /mnt/persist/etc
mv /mnt/etc/nixos /mnt/persist/etc/
mkdir -p /mnt/etc
ln -s /persist/etc/nixos /mnt/etc/nixos
success "Impermanence configured"
fi
# 9.7 Install the Nomarchy system from the freshly-generated flake.
info "Running nixos-install (this will take a while)..."
nixos-install --flake "/mnt/etc/nixos#$HOSTNAME" --no-root-passwd
success "Nomarchy installed"
# 9.8 Activate Home Manager for $USERNAME inside the new system so the
# user's first login already has Nomarchy's dotfiles. `home-manager
# switch` must run as the target user with a real $HOME, so we use
# `runuser` (sudo -u keeps the caller's HOME → files land in /root).
info "Activating Home Manager for $USERNAME..."
if nixos-enter --root /mnt -- bash -c "
set -e
install -d -o '$USERNAME' -g users -m 0755 '/home/$USERNAME'
runuser -u '$USERNAME' -- env HOME='/home/$USERNAME' \
nix --extra-experimental-features 'nix-command flakes' \
run 'home-manager/release-25.11' -- switch \
--flake '/etc/nixos#$USERNAME' --impure
"; then
success "Home Manager activated"
else
error "Home Manager activation failed (non-fatal)."
info "Run \`nomarchy-env-update\` after the first login to retry."
fi
# 9.9 Pre-flight: catch evaluation errors in the freshly-installed
# configuration *now*, while we can still fix them with `vi`, instead of
# at the user's first post-reboot rebuild.
info "Verifying configuration evaluates (nixos-rebuild dry-build)..."
if nixos-enter --root /mnt -- bash -c "
nixos-rebuild dry-build --flake /etc/nixos#$HOSTNAME 2>&1 | tail -20
"; then
success "Configuration evaluates cleanly"
else
error "Pre-flight rebuild check failed."
info "The system is installed; fix /etc/nixos before rebooting if possible."
fi
success "Installation complete!"
rm -f "$STATE_FILE"
}
# ----------------------------------------------------------------------------
# Dry run: generate the flake into a tmpdir and parse-check it. Doesn't
# touch the disk; useful while iterating on the installer or to validate a
# saved state file before actually committing to the install.
# ----------------------------------------------------------------------------
execute_dry_run() {
section "Dry Run"
local tmp
tmp=$(mktemp -d -t nomarchy-dryrun.XXXXXX)
info "Generating configuration in $tmp"
# Mock /mnt so the existing generator writes into the tmpdir.
local fake_root="$tmp/mnt"
mkdir -p "$fake_root/etc/nixos"
ln -snf "$fake_root" "$tmp/.mntlink"
# generate_flake_config writes to /mnt/etc/nixos directly. We can't
# easily re-target without a refactor — bind-mount instead so the
# absolute paths in the function still resolve to our tmpdir.
mount --bind "$fake_root" /mnt 2>/dev/null || true
if [[ ! -d /mnt/etc/nixos ]]; then
mkdir -p /mnt/etc/nixos
fi
# Stub hardware-configuration.nix — `nixos-generate-config` requires
# actually-mounted target filesystems, so we provide a syntactically
# valid placeholder for parse-checking only.
cat > /mnt/etc/nixos/hardware-configuration.nix <<'EOF'
{ ... }: { boot.loader.systemd-boot.enable = true; fileSystems."/" = { device = "/dev/null"; fsType = "tmpfs"; }; }
EOF
generate_flake_config
info "Running \`nix-instantiate --parse\` on each generated file..."
local f rc=0
for f in flake.nix hardware-selection.nix system.nix home.nix; do
if nix-instantiate --parse "/mnt/etc/nixos/$f" >/dev/null 2>&1; then
success " $f"
else
error " $f failed to parse"
rc=1
fi
done
info "Generated files:"
ls -1 /mnt/etc/nixos/
umount /mnt 2>/dev/null || true
info "Output kept at $fake_root for inspection."
if (( rc != 0 )); then
error "Dry run reported parse errors."
exit "$rc"
fi
success "Dry run OK — no disk touched."
}
# ============================================================================
# GENERATE FLAKE CONFIGURATION
# ============================================================================
generate_flake_config() {
local impermanence_opt=""
[[ "$ENABLE_IMPERMANENCE" == "true" ]] && impermanence_opt="nomarchy.system.impermanence.enable = true;"
local PROFILE_SYSTEM_OPTS=""
local PROFILE_HOME_PACKAGES=""
while IFS= read -r profile; do
[[ -z "$profile" ]] && continue
case "$profile" in
"Dev "*)
PROFILE_SYSTEM_OPTS+=$'\n # Dev profile\n nomarchy.system.virtualization.docker.enable = true;'
PROFILE_HOME_PACKAGES+=$'\n vscode\n zed-editor\n lazygit\n gh\n docker-compose'
;;
"Gaming "*)
PROFILE_SYSTEM_OPTS+=$'\n # Gaming profile\n programs.steam.enable = true;\n programs.gamemode.enable = true;'
PROFILE_HOME_PACKAGES+=$'\n steam\n lutris\n heroic'
;;
"Office "*)
PROFILE_HOME_PACKAGES+=$'\n libreoffice\n thunderbird\n obsidian\n zotero'
;;
"Media "*)
PROFILE_HOME_PACKAGES+=$'\n vlc\n obs-studio\n gimp\n inkscape\n spotify'
;;
"CLI Utils "*)
PROFILE_HOME_PACKAGES+=$'\n ripgrep\n fd\n bat\n eza\n zoxide\n fzf'
;;
esac
done <<< "$SELECTED_PROFILES"
# Pin the upstream Nomarchy flake to the exact commit we're installing
# from so the first post-reboot `nixos-rebuild` doesn't silently pull a
# newer main. Fall back to tracking main if we couldn't resolve a SHA.
# Upstream lives on the self-hosted Gitea at git.bemagri.xyz; flakes
# consume it via the `git+https://` URL form.
local nomarchy_url
if [[ -n "$NOMARCHY_REV" ]]; then
nomarchy_url="git+https://git.bemagri.xyz/bernardo/Nomarchy.git?rev=$NOMARCHY_REV"
else
nomarchy_url="git+https://git.bemagri.xyz/bernardo/Nomarchy.git"
fi
# flake.nix — the generator uses a non-quoted heredoc so $HOSTNAME,
# $USERNAME, and $nomarchy_url expand inline.
cat > /mnt/etc/nixos/flake.nix <<FLAKE_EOF
{
description = "My Nomarchy Configuration";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
nomarchy.url = "$nomarchy_url";
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
};
# Two-track Nomarchy workflow:
# * System changes → sudo nixos-rebuild switch --flake /etc/nixos#$HOSTNAME
# * Dotfiles/themes → nomarchy-env-update (home-manager switch, no rebuild)
#
# Both consume the same \`pkgs\` below so overlays and allowUnfree stay in
# sync across the two paths.
outputs = { self, nixpkgs, nomarchy, home-manager, nixos-hardware, ... }@inputs:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ nomarchy.overlays.default ];
config.allowUnfree = true;
};
in
{
nixosConfigurations.$HOSTNAME = nixpkgs.lib.nixosSystem {
inherit pkgs;
specialArgs = { inputs = nomarchy.inputs // inputs; };
modules = [
./hardware-configuration.nix
./hardware-selection.nix
nomarchy.nixosModules.system
./system.nix
];
};
# Standalone Home Manager — \`home-manager switch --flake /etc/nixos#$USERNAME\`
# (which is what \`nomarchy-env-update\` runs). Kept separate from the
# NixOS config so dotfile/theme iterations don't rebuild the system.
homeConfigurations.$USERNAME = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
extraSpecialArgs = { inputs = nomarchy.inputs // inputs; };
modules = [
nomarchy.nixosModules.home
./home.nix
{
home.username = "$USERNAME";
home.homeDirectory = "/home/$USERNAME";
home.stateVersion = "25.11";
}
];
};
};
}
FLAKE_EOF
# hardware-selection.nix
cat > /mnt/etc/nixos/hardware-selection.nix << EOF
{ inputs, ... }:
{
imports = [
$HARDWARE_MODULES
];
$NOMARCHY_HW_OPTS
}
EOF
# system.nix — curated system-level options. Uncomment what you want and
# run \`sudo nixos-rebuild switch --flake /etc/nixos#$HOSTNAME\` to apply.
# XKB variant is optional — only emit when the user picked one.
local xkb_variant_line=""
if [[ -n "$KEYMAP_VARIANT" ]]; then
xkb_variant_line=" variant = \"$KEYMAP_VARIANT\";"
fi
cat > /mnt/etc/nixos/system.nix << EOF
{ pkgs, ... }:
{
networking.hostName = "$HOSTNAME";
time.timeZone = "$TIMEZONE";
# Keyboard & language — set by the installer.
console.keyMap = "$KEYMAP_LAYOUT";
i18n.defaultLocale = "$LOCALE";
services.xserver.xkb = {
layout = "$KEYMAP_LAYOUT";
$xkb_variant_line
};
# Physical form factor — gates UI affordances (battery widget, etc).
nomarchy.system.formFactor = "$FORM_FACTOR";
$impermanence_opt
$PROFILE_SYSTEM_OPTS
# Compressed RAM swap. Near-free memory headroom on small machines and
# harmless on big ones — kernel only uses it under pressure. Disable if
# you've enabled disk swap or hibernation.
zramSwap.enable = true;
# System-wide packages. Most tools belong in home.nix instead — only put
# things here that need to be available to all users or to root (e.g. CLI
# tools used by sudo, system admin utilities).
environment.systemPackages = with pkgs; [
home-manager
# --- CLI tools useful as root ---
# wget
# curl
# rsync
# htop
# tree
# tmux
];
services.displayManager.autoLogin.enable = true;
services.displayManager.autoLogin.user = "$USERNAME";
users.users."$USERNAME" = {
isNormalUser = true;
initialPassword = "$USER_PASSWORD";
extraGroups = [ "networkmanager" "wheel" "video" "audio" "render" ];
};
# --- Optional system services ---
# Uncomment to enable. Some require extra groups on your user (see below).
# Containers / virtualization
# virtualisation.docker.enable = true; # adds "docker" group
# virtualisation.libvirtd.enable = true; # adds "libvirtd" group — needed for virt-manager
# Networking / sync
# services.tailscale.enable = true;
# services.syncthing = {
# enable = true;
# user = "$USERNAME";
# dataDir = "/home/$USERNAME";
# };
# Printing
# services.printing.enable = true;
# Flatpak (alternative app delivery)
# services.flatpak.enable = true;
# xdg.portal.enable = true;
# Gaming (system-level — pairs with home.packages.steam)
# programs.steam.enable = true;
# programs.gamemode.enable = true;
# --- Optional Nomarchy modules ---
# Each line is a one-shot toggle for a Tier 1 system feature.
# BTRFS timeline snapshots of /. Auto-skips on non-BTRFS.
# nomarchy.system.snapper.enable = true;
# Suspend-then-hibernate on lid/idle/power-key. Requires disk swap.
# nomarchy.system.hibernation.enable = true;
# nomarchy.system.hibernation.idleMinutes = 30;
# Rootless Podman with \`docker\` compatibility.
# nomarchy.system.containers.enable = true;
# libvirt + virt-manager + OVMF.
# nomarchy.system.virtualization.libvirt.enable = true;
# GNOME Keyring auto-unlock + gcr SSH agent (default: on).
# nomarchy.system.keyring.enable = false;
# fcitx5 input method (CJK / IME).
# nomarchy.system.inputMethod.enable = true;
# voxtype voice-typing wiring (you must install voxtype yourself).
# nomarchy.system.voxtype.enable = true;
system.stateVersion = "25.11";
}
EOF
# home.nix — curated app menu. Uncomment what you want and run
# `nomarchy-env-update` to apply.
#
# NB: not heredoc-quoted — we expand $FORM_FACTOR. Any literal `$` or
# backtick in the body must be escaped.
cat > /mnt/etc/nixos/home.nix << EOF
{ pkgs, ... }:
{
# Physical form factor — mirrors nomarchy.system.formFactor in system.nix.
# Gates UI affordances like the waybar battery widget.
nomarchy.formFactor = "$FORM_FACTOR";
# User-level packages (Home Manager).
#
# Nomarchy already ships a minimal desktop (firefox, thunar, mpv, imv, mako,
# hyprlock, swww, wl-clipboard, grim, slurp, rofi-wayland, etc.). The list
# below is a menu of extras — uncomment what you want and run
# \`nomarchy-env-update\`.
home.packages = with pkgs; [
# --- Enabled by default ---
btop # Resource monitor (TUI)
fastfetch # System info at login
chromium # Secondary browser
$PROFILE_HOME_PACKAGES
# --- Editors & dev ---
# vscode
# jetbrains.idea-community
# neovide
# zed-editor
# lazygit
# gh # GitHub CLI
# docker-compose
# postman
# dbeaver-bin
# --- Productivity ---
# obsidian
# libreoffice
# thunderbird
# zathura # PDF viewer
# zotero
# xournalpp
# --- Media ---
# vlc
# obs-studio
# gimp
# inkscape
# kdenlive
# spotify
# audacity
# yt-dlp
# --- Comms ---
# discord
# telegram-desktop
# signal-desktop
# slack
# zoom-us
# --- Security ---
# keepassxc
# bitwarden-desktop
# _1password-gui
# --- Gaming ---
# steam
# lutris
# heroic
# --- CLI / utilities ---
# ripgrep
# fd
# bat
# eza
# zoxide
# fzf
# httpie
# tldr
];
# --- Optional Nomarchy app modules ---
# opencode AI coding CLI integration (deploys ~/.config/opencode/opencode.json).
# The \`opencode\` package itself is not installed automatically — add it to
# \`home.packages\` above if you want it on PATH.
# nomarchy.apps.opencode.enable = true;
# Extra Home Manager modules go here (program configs, services, etc.).
}
EOF
}
# ============================================================================
# FINISH
# ============================================================================
finish() {
header
nrun gum style --foreground 10 --bold --align center "INSTALLATION COMPLETE!"
echo ""
echo "Nomarchy has been successfully installed."
echo ""
echo "Next steps:"
echo " 1. Remove the installation media"
echo " 2. Reboot your computer"
echo " 3. Log in as: $USERNAME (host: $HOSTNAME)"
echo " 4. Your configuration lives at /etc/nixos/"
echo ""
echo "Rebuild commands:"
echo " • System: sudo nixos-rebuild switch --flake /etc/nixos#$HOSTNAME"
echo " • Dotfiles: nomarchy-env-update (runs home-manager switch)"
echo ""
echo "Tip: run 'nomarchy-themes-prebuild' once to pre-cache every theme"
echo " variant. Theme switches after that are instant (no rebuild)."
echo ""
if nrun gum confirm "Reboot now?"; then
reboot
fi
}
# ============================================================================
# MAIN
# ============================================================================
main() {
parse_args "$@"
header
load_state
check_environment
# Loop: prompts (each skips when its var is set) → review → Continue|Edit.
# On Edit, edit_fields() clears the chosen vars and the next iteration
# re-prompts only those. Continue breaks. --resume lands here with vars
# already loaded, so the first pass goes straight to review.
while true; do
select_disk
get_luks_passphrase
configure_user
select_keymap_locale
select_timezone
select_hardware
confirm_form_factor
configure_impermanence
select_profiles
if review_configuration; then break; fi
done
execute_installation
# Skip the reboot prompt on a dry run — nothing to reboot into.
if [[ "$DRY_RUN" == "true" ]]; then
info "Dry run finished."
exit 0
fi
finish
}
main "$@"