feat(install): LUKS by default → auto-login, snapper snapshots, hibernation swapfile

- 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 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-06-10 16:19:14 +01:00
parent f3707357a6
commit 71614fa6ca
7 changed files with 146 additions and 19 deletions

View File

@@ -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} \

View File

@@ -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
{

View File

@@ -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 <<NIX
# Swapfile (hibernation-ready: resume points into it).
swapDevices = [{ device = "/swap/swapfile"; }];
boot.resumeDevice = "/dev/disk/by-uuid/$root_uuid";
boot.kernelParams = [ "resume_offset=$resume_offset" ];
NIX
)
success "Swapfile created (resume offset $resume_offset)"
fi
# ─── Configuration generation ───────────────────────────────────────────
section "Generating configuration"
@@ -284,6 +322,17 @@ cat > "$FLAKE_DIR/flake.nix" <<EOF
}
EOF
AUTOLOGIN_CONFIG=""
if [[ $WITH_LUKS == true ]]; then
AUTOLOGIN_CONFIG=$(cat <<NIX
# The LUKS passphrase already gates this machine — skip the second
# password prompt and boot straight into the desktop.
nomarchy.system.greeter.autoLogin = "$USERNAME";
NIX
)
fi
# initialHashedPassword is safe to template: mkpasswd's alphabet is
# [a-zA-Z0-9./$] — no Nix string metacharacters.
cat > "$FLAKE_DIR/system.nix" <<EOF
@@ -306,6 +355,9 @@ cat > "$FLAKE_DIR/system.nix" <<EOF
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
initialHashedPassword = "$HASHED_PASSWORD";
};
$AUTOLOGIN_CONFIG$RESUME_CONFIG
# Hourly/daily BTRFS timeline snapshots + nixos-rebuild-snap.
nomarchy.system.snapper.enable = true;
system.stateVersion = "26.05";
}