2026-02-01 19:43:03 +02:00
|
|
|
#!/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
|
2026-02-01 19:53:04 +02:00
|
|
|
HOSTNAME=${HOSTNAME:-nanuqsaurus}
|
2026-02-01 19:43:03 +02:00
|
|
|
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}")
|
|
|
|
|
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}" <<EOF
|
|
|
|
|
{ inputs, ... }:
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
imports = [
|
|
|
|
|
inputs.lix-module.nixosModules.lixFromNixpkgs
|
|
|
|
|
../modules/system/base.nix
|
2026-02-01 19:54:28 +02:00
|
|
|
../modules/users/admin.nix
|
2026-02-01 20:15:05 +02:00
|
|
|
# Uncomment to enable Secure Boot after install:
|
|
|
|
|
# inputs.lanzaboote.nixosModules.lanzaboote
|
|
|
|
|
# ../modules/system/secure-boot.nix
|
2026-02-01 19:43:03 +02:00
|
|
|
./${HOSTNAME}-hardware.nix
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
networking.hostName = "${HOSTNAME}";
|
|
|
|
|
}
|
|
|
|
|
EOF
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "[8/8] Installing ${HOSTNAME} via flake"
|
|
|
|
|
nixos-install --root /mnt --flake "/mnt/etc/nixos#${HOSTNAME}"
|
|
|
|
|
|
|
|
|
|
echo "Install complete. You can reboot now."
|