From 71614fa6cada2aaf0da5ab9a590c7526cb61ddd0 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Wed, 10 Jun 2026 16:19:14 +0100 Subject: [PATCH] =?UTF-8?q?feat(install):=20LUKS=20by=20default=20?= =?UTF-8?q?=E2=86=92=20auto-login,=20snapper=20snapshots,=20hibernation=20?= =?UTF-8?q?swapfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LUKS2 is the installer default; in exchange the generated config sets the new nomarchy.system.greeter.autoLogin (greetd initial_session) — the disk passphrase already gates the machine. - @snapshots subvolume + nomarchy.system.snapper.enable: hourly/daily timeline snapshots of / and the nixos-rebuild-snap helper, ported from the previous iteration (3bdfc35), guarded to no-op on non-BTRFS roots. - @swap subvolume with a swapfile sized to RAM by default (disko mkswapfile handles NOCOW); the installer computes the resume offset and wires boot.resumeDevice + resume_offset for hibernation. Co-Authored-By: Claude Fable 5 --- README.md | 17 ++++--- docs/TESTING.md | 6 ++- modules/nixos/default.nix | 47 ++++++++++++++++-- modules/nixos/options.nix | 19 +++++++ pkgs/nomarchy-install/default.nix | 3 +- pkgs/nomarchy-install/disko-config.nix | 13 ++++- pkgs/nomarchy-install/nomarchy-install.sh | 60 +++++++++++++++++++++-- 7 files changed, 146 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9fe3410..b47da81 100644 --- a/README.md +++ b/README.md @@ -89,13 +89,16 @@ the locked inputs into the ISO store — so theme switching (including the installed system. Verification checklist: [docs/TESTING.md](docs/TESTING.md). Like what you see? **`nomarchy-install`** (in a terminal) walks you through -installing to disk: pick a disk, optional LUKS2 full-disk encryption, user + -hostname + timezone, hardware autodetection (DMI → nixos-hardware profile), -then disko partitions (GPT + ESP + BTRFS subvolumes) and `nixos-install` -runs — **without a network** when the ISO was built from a clean tree (the -target's `flake.lock` is composed from the rev the ISO carries). The -installed machine gets the standard downstream layout: the flake at -`~/.nomarchy` (`/etc/nixos` symlinks to it), one `mkFlake` call, your +installing to disk: pick a disk, LUKS2 full-disk encryption **by default** +(in exchange the desktop logs in passwordless — the passphrase already +gates the machine), user + hostname + timezone, hardware autodetection +(DMI → nixos-hardware profile), a hibernation-ready swapfile sized to RAM, +then disko partitions (GPT + ESP + BTRFS subvolumes incl. `@snapshots` — +snapper timeline snapshots are on) and `nixos-install` runs — **without a +network** when the ISO was built from a clean tree (the target's +`flake.lock` is composed from the rev the ISO carries). The installed +machine gets the standard downstream layout: the flake at `~/.nomarchy` +(`/etc/nixos` symlinks to it), one `mkFlake` call, your `system.nix`/`home.nix`. First boot lands in the fully themed desktop — the installer pre-activates the Home Manager generation. UEFI only for now. diff --git a/docs/TESTING.md b/docs/TESTING.md index 7a7868b..34bb755 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -77,11 +77,13 @@ terminal: ```sh sudo NOMARCHY_UNATTENDED=1 NOMARCHY_DISK=/dev/vda \ NOMARCHY_USERNAME=me NOMARCHY_PASSWORD=test \ + NOMARCHY_LUKS_PASSPHRASE=testtest1 \ NOMARCHY_FINISH=poweroff nomarchy-install ``` -Then boot the VM again **from the target disk** (drop `-cdrom`). The -machine must reach the themed desktop on its own — greeter autostart, +Then boot the VM again **from the target disk** (drop `-cdrom`). After +typing the LUKS passphrase at the initrd prompt, the machine must reach +the themed desktop **without a login prompt** (LUKS implies auto-login) — wallpaper, Waybar — and `nomarchy-theme-sync apply ` must work. For the offline claim, add `restrict=on` to the `-netdev` and re-run the whole flow: it must behave identically (requires an ISO built from a clean git diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index d091826..f4e03f7 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -26,9 +26,16 @@ in services.greetd = lib.mkIf cfg.greeter.enable { enable = lib.mkDefault true; - settings.default_session = { - command = lib.mkDefault "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd Hyprland"; - user = "greeter"; + settings = { + default_session = { + command = lib.mkDefault "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd Hyprland"; + user = "greeter"; + }; + # Boot straight into the session once; logout → normal greeter. + initial_session = lib.mkIf (cfg.greeter.autoLogin != null) { + command = "Hyprland"; + user = cfg.greeter.autoLogin; + }; }; }; @@ -53,6 +60,24 @@ in hardware.bluetooth.enable = lib.mkDefault cfg.bluetooth.enable; services.blueman.enable = lib.mkDefault cfg.bluetooth.enable; + # ── BTRFS timeline snapshots (ported from the previous iteration) ─ + # Guarded on the actual filesystem so enabling it on an ext4 machine + # is a clean no-op rather than a failing timer. + services.snapper.configs = lib.mkIf + (cfg.snapper.enable && (config.fileSystems."/".fsType or "") == "btrfs") + { + root = { + SUBVOLUME = "/"; + TIMELINE_CREATE = true; + TIMELINE_CLEANUP = true; + TIMELINE_LIMIT_HOURLY = "5"; + TIMELINE_LIMIT_DAILY = "7"; + TIMELINE_LIMIT_WEEKLY = "0"; + TIMELINE_LIMIT_MONTHLY = "0"; + TIMELINE_LIMIT_YEARLY = "0"; + }; + }; + # ── Fonts ──────────────────────────────────────────────────────── fonts = { packages = with pkgs; [ @@ -82,7 +107,21 @@ in wl-clipboard grim slurp - ]; + ] ++ lib.optional (cfg.snapper.enable && (config.fileSystems."/".fsType or "") == "btrfs") + # Snapshot, then rebuild — rollback material for system changes + # (theme changes don't need it; HM generations already roll back). + (pkgs.writeShellScriptBin "nixos-rebuild-snap" '' + if [ "$(id -u)" -ne 0 ]; then + echo "This script must be run as root (use sudo)" >&2 + exit 1 + fi + echo "Creating pre-rebuild snapshot..." + ${pkgs.snapper}/bin/snapper -c root create \ + -d "Pre-rebuild $(date +'%Y-%m-%d %H:%M:%S')" \ + --cleanup-algorithm number + echo "Rebuilding..." + nixos-rebuild switch --flake /etc/nixos#default "$@" + ''); # ── Nix itself ─────────────────────────────────────────────────── nix = { diff --git a/modules/nixos/options.nix b/modules/nixos/options.nix index 97cd222..2b93139 100644 --- a/modules/nixos/options.nix +++ b/modules/nixos/options.nix @@ -8,7 +8,26 @@ { options.nomarchy.system = { greeter.enable = lib.mkEnableOption "the greetd/tuigreet login screen" // { default = true; }; + + greeter.autoLogin = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "ada"; + description = '' + Log this user straight into Hyprland on boot (greetd + initial_session); logging out lands on the normal greeter. + The installer sets it on LUKS-encrypted machines — the disk + passphrase already gates access, a second prompt is ceremony. + ''; + }; + audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; }; bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = true; }; + + snapper.enable = lib.mkEnableOption '' + hourly/daily BTRFS timeline snapshots of / via snapper, plus the + `nixos-rebuild-snap` pre-rebuild-snapshot helper. No-op unless the + root filesystem is BTRFS with a /.snapshots subvolume (the installer + creates one)''; }; } diff --git a/pkgs/nomarchy-install/default.nix b/pkgs/nomarchy-install/default.nix index 26e8c96..876c95c 100644 --- a/pkgs/nomarchy-install/default.nix +++ b/pkgs/nomarchy-install/default.nix @@ -13,6 +13,7 @@ , cryptsetup , lvm2 # dmsetup , pciutils # lspci +, btrfs-progs # inspect-internal map-swapfile (hibernation offset) # Baked metadata — what this installer installs and where it came from. , templateDir # templates/downstream (home.nix, theme-state.json) , nomarchyLock # the distro's flake.lock (for offline lock composition) @@ -51,7 +52,7 @@ stdenvNoCC.mkDerivation { wrapProgram $out/bin/nomarchy-install \ --prefix PATH : ${lib.makeBinPath [ bash gum disko whois git python3 - util-linux gptfdisk parted cryptsetup lvm2 pciutils + util-linux gptfdisk parted cryptsetup lvm2 pciutils btrfs-progs ]} \ --set NOMARCHY_INSTALL_SHARE "$share" \ --set NOMARCHY_FLAKE_URL ${lib.escapeShellArg flakeUrl} \ diff --git a/pkgs/nomarchy-install/disko-config.nix b/pkgs/nomarchy-install/disko-config.nix index 04502ca..c78e9b0 100644 --- a/pkgs/nomarchy-install/disko-config.nix +++ b/pkgs/nomarchy-install/disko-config.nix @@ -5,6 +5,7 @@ # disko --mode destroy,format,mount \ # --argstr mainDrive /dev/nvme0n1 \ # --arg withLuks true \ +# --argstr swapSize 16G \ # disko-config.nix # # Ported from the previous iteration's golden-path config (multi-disk @@ -14,6 +15,7 @@ # proved fragile twice before. { mainDrive , withLuks ? true +, swapSize ? "0" # "0" = no swap; otherwise e.g. "16G" (sized for hibernation) , ... }: @@ -28,7 +30,16 @@ let "@home" = { mountpoint = "/home"; mountOptions = btrfsMountOptions; }; "@nix" = { mountpoint = "/nix"; mountOptions = btrfsMountOptions; }; "@log" = { mountpoint = "/var/log"; mountOptions = btrfsMountOptions; }; - }; + # snapper timeline snapshots (nomarchy.system.snapper.enable) + "@snapshots" = { mountpoint = "/.snapshots"; mountOptions = btrfsMountOptions; }; + } // (if swapSize == "0" then { } else { + # Hibernation-ready swapfile on its own subvolume; disko's + # mkswapfile handles the BTRFS NOCOW requirements. + "@swap" = { + mountpoint = "/swap"; + swap.swapfile.size = swapSize; + }; + }); }; in { diff --git a/pkgs/nomarchy-install/nomarchy-install.sh b/pkgs/nomarchy-install/nomarchy-install.sh index bf655e5..98ccde7 100644 --- a/pkgs/nomarchy-install/nomarchy-install.sh +++ b/pkgs/nomarchy-install/nomarchy-install.sh @@ -16,6 +16,7 @@ # NOMARCHY_UNATTENDED=1 NOMARCHY_DISK=/dev/vda NOMARCHY_USERNAME=me \ # NOMARCHY_PASSWORD=secret [NOMARCHY_HOSTNAME=nomarchy] \ # [NOMARCHY_TIMEZONE=UTC] [NOMARCHY_LUKS_PASSPHRASE=...] \ +# [NOMARCHY_SWAP_GB=N (default: RAM size; 0 = none)] \ # [NOMARCHY_HW="auto"|"none"|"mod1 mod2"] [NOMARCHY_FINISH=none|reboot|poweroff] # nomarchy-install @@ -87,10 +88,12 @@ info "Target: $TARGET_DISK" # ─── Encryption ───────────────────────────────────────────────────────── section "Disk encryption" +# LUKS is the default: full-disk encryption, and in exchange the machine +# logs you straight into the desktop (the passphrase already gates access). LUKS_PASSPHRASE="" if [[ "$UNATTENDED" == "1" ]]; then LUKS_PASSPHRASE="${NOMARCHY_LUKS_PASSPHRASE:-}" -elif gum confirm "Encrypt the disk with LUKS? (recommended for laptops)"; then +elif gum confirm --default=yes "Encrypt the disk with LUKS? (default — also enables passwordless desktop login)"; then while true; do p1=$(gum input --password --placeholder "LUKS passphrase (min 8 chars)") [[ ${#p1} -ge 8 ]] || { warn "Too short."; continue; } @@ -100,7 +103,20 @@ elif gum confirm "Encrypt the disk with LUKS? (recommended for laptops)"; then done fi WITH_LUKS=false; [[ -n "$LUKS_PASSPHRASE" ]] && WITH_LUKS=true -info "Encryption: $([[ $WITH_LUKS == true ]] && echo LUKS2 || echo none)" +info "Encryption: $([[ $WITH_LUKS == true ]] && echo "LUKS2 (desktop auto-login)" || echo none)" + +# ─── Swap / hibernation ───────────────────────────────────────────────── +# A swapfile ≥ RAM on its own BTRFS subvolume makes hibernation possible; +# the resume offset is wired into the config below. +ram_gb=$(awk '/MemTotal/ {print int(($2 + 1048575) / 1048576)}' /proc/meminfo) +if [[ "$UNATTENDED" == "1" ]]; then + SWAP_GB="${NOMARCHY_SWAP_GB:-$ram_gb}" +else + SWAP_GB=$(gum input --value "$ram_gb" \ + --placeholder "swap size in GiB (≥ RAM enables hibernation, 0 = none)") +fi +[[ "$SWAP_GB" =~ ^[0-9]+$ ]] || fail "Swap size must be a whole number of GiB." +info "Swap: $([[ "$SWAP_GB" == "0" ]] && echo none || echo "${SWAP_GB}G swapfile (hibernation-ready)")" # ─── User account ─────────────────────────────────────────────────────── section "Your account" @@ -178,12 +194,14 @@ section "Review" gum style --border normal --padding "0 2" \ "Disk: $TARGET_DISK (WILL BE ERASED)" \ - "Encryption: $([[ $WITH_LUKS == true ]] && echo LUKS2 || echo none)" \ + "Encryption: $([[ $WITH_LUKS == true ]] && echo "LUKS2 + desktop auto-login" || echo none)" \ + "Swap: $([[ "$SWAP_GB" == "0" ]] && echo none || echo "${SWAP_GB}G (hibernation)")" \ "User: $USERNAME" \ "Hostname: $HOSTNAME_" \ "Timezone: $TIMEZONE" \ "Hardware: ${HW_PROFILES[*]:-none}" \ - "Offline: $([[ -n "${NOMARCHY_REV:-}" ]] && echo "yes (pinned $NOMARCHY_REV)" || echo "no (will fetch nomarchy from GitHub)")" + "Snapshots: snapper timeline on /" \ + "Offline: $([[ -n "${NOMARCHY_REV:-}" ]] && echo "yes (pinned ${NOMARCHY_REV:0:12})" || echo "no (will fetch nomarchy over the network)")" if [[ "$UNATTENDED" != "1" ]]; then typed=$(gum input --placeholder "type the disk name ($(basename "$TARGET_DISK")) to confirm the wipe") @@ -241,6 +259,7 @@ disko_log=$(mktemp --suffix=.disko.log) if ! disko --mode destroy,format,mount --yes-wipe-all-disks \ --argstr mainDrive "$TARGET_DISK" \ --arg withLuks "$WITH_LUKS" \ + --argstr swapSize "${SWAP_GB}G" \ "$SHARE/disko-config.nix" >"$disko_log" 2>&1; then tail -n 30 "$disko_log" fail "disko failed — full log: $disko_log" @@ -248,6 +267,25 @@ fi rm -f "$LUKS_KEY_PATH" "$disko_log" success "Disk partitioned and mounted at /mnt" +# Hibernation plumbing: the swapfile's physical offset goes into the +# kernel cmdline. Deactivate swap first so nixos-generate-config doesn't +# also emit a swapDevices entry (we write our own, with resume wiring). +RESUME_CONFIG="" +if [[ "$SWAP_GB" != "0" ]]; then + swapoff -a 2>/dev/null || true + resume_offset=$(btrfs inspect-internal map-swapfile -r /mnt/swap/swapfile) + root_uuid=$(findmnt -no UUID /mnt) + RESUME_CONFIG=$(cat < "$FLAKE_DIR/flake.nix" < "$FLAKE_DIR/system.nix" < "$FLAKE_DIR/system.nix" <