#!/usr/bin/env bash set -euo pipefail # Automated Btrfs install using the flake in this repo. # Defaults remain environment-driven for headless use. # Interactive prompts are shown unless --non-interactive is set. NON_INTERACTIVE=0 if [[ "${1:-}" == "--non-interactive" ]]; then NON_INTERACTIVE=1 shift fi DISK=${DISK:-/dev/vda} # full-disk target ROOT_PART=${ROOT_PART:-} # optional existing root partition BOOT_PART=${BOOT_PART:-} # optional existing boot (FAT) partition HOSTNAME=${HOSTNAME:-nixos} USE_LUKS=${USE_LUKS:-0} # 1 to encrypt root SWAP_GB=${SWAP_GB:-0} # swap size in GB, 0 disables SCRIPT_DIR=$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd) REPO_ROOT=$(cd -- "${SCRIPT_DIR}/.." && pwd) has_cmd() { command -v "$1" >/dev/null 2>&1; } prompt_select() { local title="$1" prompt="$2" choices=("${@:3}") if (( NON_INTERACTIVE )); then echo "${choices[0]}" return fi if has_cmd dialog; then dialog --no-cancel --menu "$prompt" 15 60 6 "${choices[@]}" 2> /tmp/choice cat /tmp/choice elif has_cmd whiptail; then whiptail --notags --menu "$prompt" 15 60 6 "${choices[@]}" 2> /tmp/choice || true cat /tmp/choice else echo "$prompt" select c in "${choices[@]}"; do echo "$c"; break; done fi } prompt_yes_no() { local prompt="$1" default="${2:-n}" if (( NON_INTERACTIVE )); then [[ "$default" =~ ^[Yy]$ ]] && echo "yes" || echo "no" return fi if has_cmd dialog; then if dialog --yesno "$prompt" 8 60; then echo "yes"; else echo "no"; fi elif has_cmd whiptail; then if whiptail --yesno "$prompt" 8 60; then echo "yes"; else echo "no"; fi else read -rp "$prompt [y/N]: " ans [[ "${ans:-$default}" =~ ^[Yy]$ ]] && echo "yes" || echo "no" fi } prompt_input() { local prompt="$1" default="$2" if (( NON_INTERACTIVE )); then echo "$default" return fi if has_cmd dialog; then dialog --inputbox "$prompt" 8 60 "$default" 2>/tmp/input && cat /tmp/input || echo "$default" elif has_cmd whiptail; then whiptail --inputbox "$prompt" 8 60 "$default" 2>/tmp/input && cat /tmp/input || echo "$default" else read -rp "$prompt [$default]: " ans echo "${ans:-$default}" fi } ls_disks() { lsblk -dpno NAME,SIZE,TYPE | awk '$3=="disk"{print $1" "$2}' } if (( ! NON_INTERACTIVE )); then mapfile -t disks < <(ls_disks) if ((${#disks[@]})); then disk_choices=() for d in "${disks[@]}"; do disk_choices+=("$d" "") done DISK=$(prompt_select "disk" "Select target disk (will be wiped)" "${disk_choices[@]}") fi target_mode=$(prompt_select "mode" "Partitioning mode" \ "full-disk" "Use entire disk (wipes!)" \ "existing" "Use existing partitions") if [[ "$target_mode" == "existing" ]]; then ROOT_PART=$(prompt_input "Root partition (will be formatted btrfs)" "${ROOT_PART:-/dev/vda2}") BOOT_PART=$(prompt_input "Boot EFI partition (FAT32)" "${BOOT_PART:-/dev/vda1}") fi USE_LUKS=$(prompt_yes_no "Encrypt root with LUKS?" "n") [[ "$USE_LUKS" == "yes" ]] && USE_LUKS=1 || USE_LUKS=0 SWAP_GB=$(prompt_input "Swap size in GB (0 = none)" "${SWAP_GB}") HOSTNAME=$(prompt_input "Hostname" "${HOSTNAME}") fi echo "[0/8] Summary:" echo " Disk: ${DISK}" if [[ -n "$ROOT_PART" ]]; then echo " Root partition: $ROOT_PART" echo " Boot partition: $BOOT_PART" else echo " Mode: full disk wipe" fi echo " LUKS: $([ "$USE_LUKS" -eq 1 ] && echo enabled || echo disabled)" echo " Swap: ${SWAP_GB}G" echo " Hostname: ${HOSTNAME}" if (( NON_INTERACTIVE )); then echo "Proceeding in non-interactive mode." else if [[ "$(prompt_yes_no "Proceed with these settings? This will destroy data." "n")" != "yes" ]]; then echo "Aborted." exit 1 fi fi part_boot="" part_root="" mapper_root="" echo "[1/8] Partitioning/selection" if [[ -z "$ROOT_PART" ]]; then printf "label: gpt\n,550M,U,boot\n,,L,cryptroot\n" | sfdisk "${DISK}" part_boot="${DISK}1" part_root="${DISK}2" else part_boot="${BOOT_PART}" part_root="${ROOT_PART}" fi echo "[2/8] Formatting boot" mkfs.fat -F 32 "${part_boot}" echo "[3/8] Preparing root" if [[ "$USE_LUKS" -eq 1 ]]; then pass=${LUKS_PASSPHRASE:-} if [[ -z "$pass" && $NON_INTERACTIVE -eq 0 ]]; then pass=$(prompt_input "LUKS passphrase" "") fi if [[ -z "$pass" ]]; then echo "LUKS passphrase required for encrypted install" exit 1 fi printf "%s" "$pass" | cryptsetup luksFormat "${part_root}" - printf "%s" "$pass" | cryptsetup open "${part_root}" cryptroot - mapper_root="/dev/mapper/cryptroot" else mapper_root="${part_root}" fi mkfs.btrfs -f "${mapper_root}" echo "[4/8] Creating Btrfs subvolumes" mkdir -p /mnt mount "${mapper_root}" /mnt btrfs subvolume create /mnt/root btrfs subvolume create /mnt/home btrfs subvolume create /mnt/nix umount /mnt echo "[5/8] Mounting subvolumes" mount -o compress=zstd,subvol=root "${mapper_root}" /mnt mkdir -p /mnt/{home,nix,boot} mount -o compress=zstd,subvol=home "${mapper_root}" /mnt/home mount -o compress=zstd,subvol=nix "${mapper_root}" /mnt/nix mount "${part_boot}" /mnt/boot if [[ "${SWAP_GB}" -gt 0 ]]; then echo "[5b/8] Creating swapfile (${SWAP_GB}G)" mkdir -p /mnt/swap chattr +C /mnt/swap || true fallocate -l "${SWAP_GB}G" /mnt/swap/swapfile chmod 600 /mnt/swap/swapfile mkswap /mnt/swap/swapfile swapon /mnt/swap/swapfile fi echo "[6/8] Copying flake to /etc/nixos" if command -v rsync >/dev/null 2>&1; then rsync -a --exclude '.git' --exclude 'result' --exclude 'node_modules' "${REPO_ROOT}/" /mnt/etc/nixos/ else (cd "${REPO_ROOT}" && tar -cf - --exclude=.git --exclude=result --exclude=node_modules .) | (cd /mnt/etc/nixos && tar -xf -) fi echo "[7/8] Generating hardware config" hardware_file="/mnt/etc/nixos/hosts/${HOSTNAME}-hardware.nix" mkdir -p /mnt/etc/nixos/hosts nixos-generate-config --root /mnt --show-hardware-config > "${hardware_file}" if [[ "$USE_LUKS" -eq 1 || "${SWAP_GB}" -gt 0 ]]; then tmpfile=$(mktemp) # drop trailing closing brace sed '$d' "${hardware_file}" > "${tmpfile}" if [[ "$USE_LUKS" -eq 1 ]]; then echo " boot.initrd.luks.devices.cryptroot.device = \"${part_root}\";" >> "${tmpfile}" fi if [[ "${SWAP_GB}" -gt 0 ]]; then echo " swapDevices = [ { device = \"/swap/swapfile\"; } ];" >> "${tmpfile}" fi echo "}" >> "${tmpfile}" mv "${tmpfile}" "${hardware_file}" fi host_file="/mnt/etc/nixos/hosts/${HOSTNAME}.nix" if [ ! -e "${host_file}" ]; then cat > "${host_file}" <