Skip to content

Instantly share code, notes, and snippets.

@fadenb
Last active December 26, 2025 11:12
Show Gist options
  • Select an option

  • Save fadenb/4b918893b81a6028c5fb55e7b084e202 to your computer and use it in GitHub Desktop.

Select an option

Save fadenb/4b918893b81a6028c5fb55e7b084e202 to your computer and use it in GitHub Desktop.
I was curios whether ChatGPT can write a somewhat useful NixOS installer script. This is what it generated after being prompted with some notes about my usual setup.
#!/usr/bin/env bash
#
# nixos_zfs_sway_install.sh
#
# Fully automated (destructive) NixOS install to an encrypted ZFS root + EFI,
# configured for Wayland + Sway + PipeWire screen sharing.
#
# Included fixes:
# - Enable flakes/nix-command for this run via NIX_CONFIG (no writes required).
# - Best-effort write to /etc/nix/nix.conf if possible (ignored if read-only).
# - Aggressive cleanup to avoid "device is busy" on reruns.
# - Do NOT swapon during install.
# - Do NOT define fileSystems."/boot" in configuration.nix (avoid conflict with hardware-configuration.nix).
# - Pin kernel/ZFS: Linux 6.18 + ZFS 2.4.
# - Remove sound.enable (option removed).
# - Set user password from a direct password prompt (NOT a hash) without storing it in the Nix store:
# runs chpasswd inside nixos-enter after nixos-install.
# - Fix systemd-boot random-seed warning by enforcing restrictive vfat mount perms for /boot
# by patching hardware-configuration.nix using awk (no perl required).
#
# Usage:
# sudo DISK=/dev/nvme0n1 HOSTNAME=dell USERNAME=tristan ./nixos_zfs_sway_install.sh
#
# Optional env:
# SWAP_SIZE_GB=8
# TIMEZONE=Europe/Berlin
# LOCALE=en_US.UTF-8
# KEYMAP=us
# POOL=rpool
# ASK_DISK_CONFIRM=0
# ZFS_PASSPHRASE='...' # if unset, prompt
# USER_PASSWORD='...' # if unset, prompt (recommended to leave unset)
#
set -euo pipefail
# Force-enable flakes for any nix invocation during this run (live ISO).
export NIX_CONFIG="experimental-features = nix-command flakes"
# Best-effort persist in live environment (ignored if /etc is read-only).
mkdir -p /etc/nix 2>/dev/null || true
if ! grep -q '^experimental-features' /etc/nix/nix.conf 2>/dev/null; then
printf '%s\n' 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf 2>/dev/null || true
fi
need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; }
need sgdisk
need wipefs
need partprobe
need udevadm
need mkfs.fat
need zpool
need zfs
need nixos-generate-config
need nixos-install
need nixos-enter
need cryptsetup
need lsblk
need awk
need sort
need mv
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
echo "Run as root (sudo)." >&2
exit 1
fi
DISK="${DISK:-}"
if [[ -z "$DISK" ]]; then
echo "ERROR: Set DISK=/dev/XXX (e.g. /dev/nvme0n1 or /dev/sda)" >&2
exit 1
fi
if [[ ! -b "$DISK" ]]; then
echo "ERROR: DISK '$DISK' is not a block device" >&2
exit 1
fi
HOSTNAME="${HOSTNAME:-nixos}"
USERNAME="${USERNAME:-nixos}"
SWAP_SIZE_GB="${SWAP_SIZE_GB:-4}"
TIMEZONE="${TIMEZONE:-Europe/Berlin}"
LOCALE="${LOCALE:-en_US.UTF-8}"
KEYMAP="${KEYMAP:-us}"
POOL="${POOL:-rpool}"
ASK_DISK_CONFIRM="${ASK_DISK_CONFIRM:-1}"
# Partition naming (nvme uses p1/p2, sata uses 1/2)
if [[ "$DISK" =~ nvme ]]; then
EFI_PART="${DISK}p1"
ZFS_PART="${DISK}p2"
SWAP_PART="${DISK}p3"
else
EFI_PART="${DISK}1"
ZFS_PART="${DISK}2"
SWAP_PART="${DISK}3"
fi
echo "Target disk: $DISK"
echo "EFI partition: $EFI_PART"
echo "ZFS partition: $ZFS_PART"
echo "Swap partition: $SWAP_PART"
echo "Hostname: $HOSTNAME"
echo "Username: $USERNAME"
echo "Pool name: $POOL"
echo "Swap size (GiB): $SWAP_SIZE_GB"
echo
if [[ "$ASK_DISK_CONFIRM" == "1" ]]; then
read -r -p "Type WIPE to confirm DESTROYING ALL DATA on $DISK: " confirm
if [[ "$confirm" != "WIPE" ]]; then
echo "Aborted."
exit 1
fi
fi
# Prompt for ZFS encryption passphrase (used to unlock at boot)
if [[ -z "${ZFS_PASSPHRASE:-}" ]]; then
read -rs -p "Enter ZFS passphrase (will be required at boot): " pass1; echo
read -rs -p "Confirm passphrase: " pass2; echo
if [[ "$pass1" != "$pass2" ]]; then
echo "Passphrases do not match." >&2
exit 1
fi
ZFS_PASSPHRASE="$pass1"
fi
# Prompt for user password (direct, not hash) unless provided in USER_PASSWORD
if [[ -z "${USER_PASSWORD:-}" ]]; then
read -rs -p "Enter password for user '$USERNAME': " up1; echo
read -rs -p "Confirm password for user '$USERNAME': " up2; echo
if [[ "$up1" != "$up2" ]]; then
echo "User passwords do not match." >&2
exit 1
fi
USER_PASSWORD="$up1"
fi
# Generate a stable 8-hex hostId (required for ZFS import at boot)
HOSTID_HEX="$(head -c 4 /dev/urandom | od -An -tx4 | tr -d ' \n')"
cleanup_everything_on_disk() {
set +e
swapoff -a >/dev/null 2>&1 || true
cd / >/dev/null 2>&1 || true
umount -R /mnt >/dev/null 2>&1 || true
for m in $(lsblk -nrpo MOUNTPOINT "$DISK" 2>/dev/null | awk 'NF{print}' | sort -r); do
umount -l "$m" >/dev/null 2>&1 || true
done
zpool export -a >/dev/null 2>&1 || true
for name in cryptswap cryptroot; do
cryptsetup close "$name" >/dev/null 2>&1 || true
done
udevadm settle >/dev/null 2>&1 || true
set -e
}
cleanup_existing_pool() {
set +e
if zpool list -H -o name 2>/dev/null | grep -qx "$POOL"; then
echo "[*] Existing pool '$POOL' found, destroying it"
zpool destroy -f "$POOL" >/dev/null 2>&1 || true
fi
set -e
}
finalize_and_detach() {
set +e
cd / >/dev/null 2>&1 || true
sync >/dev/null 2>&1 || true
umount -R /mnt >/dev/null 2>&1 || true
zpool export -f "$POOL" >/dev/null 2>&1 || true
umount -lR /mnt >/dev/null 2>&1 || true
zpool export -f "$POOL" >/dev/null 2>&1 || true
set -e
}
trap cleanup_everything_on_disk EXIT
echo "[+] Cleanup (unmount/export/swapoff) to avoid device busy issues"
cleanup_everything_on_disk
cleanup_existing_pool
echo "[+] Wiping partition table and filesystem signatures"
sgdisk --zap-all "$DISK"
wipefs -a "$DISK"
blkdiscard -f "$DISK" >/dev/null 2>&1 || true
echo "[+] Partitioning (GPT): EFI (1GiB), ZFS (rest - swap), swap (${SWAP_SIZE_GB}GiB)"
sgdisk -n1:1MiB:+1GiB -t1:EF00 -c1:EFI "$DISK"
sgdisk -n2:0:-"${SWAP_SIZE_GB}"GiB -t2:BF01 -c2:ZFS "$DISK"
sgdisk -n3:0:0 -t3:8200 -c3:SWAP "$DISK"
partprobe "$DISK"
udevadm settle
echo "[+] Formatting EFI partition (FAT32)"
mkfs.fat -F32 -n EFI "$EFI_PART"
echo "[+] Initializing swap partition (do NOT swapon during install)"
mkswap "$SWAP_PART" >/dev/null
echo "[+] Creating encrypted ZFS pool '$POOL' on $ZFS_PART"
TMP_KEYFILE="$(mktemp)"
chmod 600 "$TMP_KEYFILE"
printf '%s' "$ZFS_PASSPHRASE" > "$TMP_KEYFILE"
zpool create -f \
-o ashift=12 \
-o autotrim=on \
-O mountpoint=none \
-O compression=lz4 \
-O xattr=sa \
-O acltype=posixacl \
-O encryption=aes-256-gcm \
-O keyformat=passphrase \
-O keylocation="file://$TMP_KEYFILE" \
"$POOL" "$ZFS_PART"
# Load key using temp keyfile, then switch to prompt for real boot behavior
zfs load-key -a
zfs set keylocation=prompt "$POOL"
rm -f "$TMP_KEYFILE"
echo "[+] Creating datasets"
zfs create -o mountpoint=legacy "$POOL/root"
zfs create -o mountpoint=legacy -o atime=off "$POOL/nix"
zfs create -o mountpoint=legacy "$POOL/home"
zfs create -o mountpoint=legacy "$POOL/var"
echo "[+] Mounting filesystems under /mnt"
mount -t zfs "$POOL/root" /mnt
mkdir -p /mnt/{boot,nix,home,var}
mount -t zfs "$POOL/nix" /mnt/nix
mount -t zfs "$POOL/home" /mnt/home
mount -t zfs "$POOL/var" /mnt/var
mount "$EFI_PART" /mnt/boot
echo "[+] Generating hardware configuration"
nixos-generate-config --root /mnt
# Fix systemd-boot random-seed warning by enforcing restrictive vfat mount perms for /boot
HWC="/mnt/etc/nixos/hardware-configuration.nix"
if grep -q 'fileSystems\."\/boot"' "$HWC" 2>/dev/null; then
awk '
BEGIN { inboot=0; haveOpt=0; sawVfat=0 }
$0 ~ /^[[:space:]]*fileSystems\."\/boot"[[:space:]]*=[[:space:]]*\{/ {
inboot=1; haveOpt=0; sawVfat=0
print
next
}
inboot && $0 ~ /^[[:space:]]*fsType[[:space:]]*=[[:space:]]*"vfat";/ {
sawVfat=1
print
next
}
inboot && $0 ~ /^[[:space:]]*options[[:space:]]*=/ {
print " options = [ \"fmask=0077\" \"dmask=0077\" ];"
haveOpt=1
next
}
inboot && $0 ~ /^[[:space:]]*\};[[:space:]]*$/ {
if (sawVfat && !haveOpt) {
print " options = [ \"fmask=0077\" \"dmask=0077\" ];"
}
inboot=0
print
next
}
{ print }
' "$HWC" > "${HWC}.tmp" && mv "${HWC}.tmp" "$HWC"
fi
CONF="/mnt/etc/nixos/configuration.nix"
echo "[+] Writing $CONF"
cat > "$CONF" <<EOF
{ config, pkgs, lib, ... }:
{
imports = [ ./hardware-configuration.nix ];
networking.hostName = "$HOSTNAME";
networking.hostId = "$HOSTID_HEX";
networking.networkmanager.enable = true;
time.timeZone = "$TIMEZONE";
i18n.defaultLocale = "$LOCALE";
console.keyMap = "$KEYMAP";
nix.settings.experimental-features = [ "nix-command" "flakes" ];
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.supportedFilesystems = [ "zfs" ];
boot.zfs.requestEncryptionCredentials = true;
# PIN: Linux 6.18 + ZFS 2.4
boot.kernelPackages = pkgs.linuxPackages_6_18;
boot.zfs.package = pkgs.zfs_2_4;
boot.zfs.modulePackage = lib.mkForce pkgs.linuxPackages_6_18.zfs_2_4;
services.zfs.autoScrub.enable = true;
services.zfs.trim.enable = true;
fileSystems."/" = { device = "$POOL/root"; fsType = "zfs"; };
fileSystems."/nix" = { device = "$POOL/nix"; fsType = "zfs"; };
fileSystems."/home" = { device = "$POOL/home"; fsType = "zfs"; };
fileSystems."/var" = { device = "$POOL/var"; fsType = "zfs"; };
# IMPORTANT: Do NOT define fileSystems."/boot" here (hardware-configuration.nix owns it).
swapDevices = [{
device = "$SWAP_PART";
randomEncryption.enable = true;
}];
services.xserver.enable = false;
hardware.graphics.enable = true;
boot.initrd.kernelModules = [ "xe" ];
hardware.enableRedistributableFirmware = true;
hardware.cpu.intel.updateMicrocode = true;
services.dbus.enable = true;
security.polkit.enable = true;
hardware.pulseaudio.enable = false;
services.pipewire = {
enable = true;
alsa.enable = true;
pulse.enable = true;
jack.enable = true;
};
xdg.portal = {
enable = true;
wlr.enable = true;
extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
};
programs.sway = {
enable = true;
wrapperFeatures.gtk = true;
extraPackages = with pkgs; [ grim slurp wl-clipboard mako ];
};
environment.sessionVariables = {
XDG_SESSION_TYPE = "wayland";
XDG_CURRENT_DESKTOP = "sway:wlroots";
};
services.gnome.gnome-keyring.enable = true;
services.greetd = {
enable = true;
settings = {
default_session = {
command = "\${pkgs.tuigreet}/bin/tuigreet --time --cmd sway";
user = "greeter";
};
};
};
nixpkgs.config.allowUnfree = true;
environment.systemPackages = with pkgs; [
curl wget git neovim firefox
pciutils usbutils lm_sensors
foot waybar
dbus xdg-utils
];
users.users.$USERNAME = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "video" "audio" ];
};
services.openssh.enable = true;
system.stateVersion = "25.11";
}
EOF
echo "[+] Installing NixOS"
nixos-install --root /mnt --no-root-passwd
echo "[+] Setting password for user '$USERNAME' (not stored in Nix config)"
nixos-enter --root /mnt -c "chpasswd" <<< "${USERNAME}:${USER_PASSWORD}"
# Remove secrets from environment as soon as possible
unset USER_PASSWORD
unset ZFS_PASSPHRASE
echo "[+] Finalizing (detach pool and unmount)"
finalize_and_detach
echo
echo "DONE. Reboot now."
echo "On boot, you will be prompted for the ZFS passphrase to unlock $POOL."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment