Last active
December 26, 2025 11:12
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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 | |
| next | |
| } | |
| inboot && $0 ~ /^[[:space:]]*fsType[[:space:]]*=[[:space:]]*"vfat";/ { | |
| sawVfat=1 | |
| 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 | |
| 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