Files
Nomarchy/docs/MIGRATION.md
Bernardo Magri eb38008ebb
All checks were successful
Check / eval (push) Successful in 3m7s
feat(menu): one fingerprint switch, an auto-login toggle, and the state bridge they needed
Bernardo, post-reboot: "Use for login" was the wrong question. Whether the
finger works is one decision, not two, and whether login prompts at all is
a different decision that was never in the menu.

System › Fingerprint is now a single Fingerprint (on/off) switch, leading
the menu with enroll/list/verify/delete as the plumbing behind it. It
writes the one settings.fingerprint.pam key, and modules/home/idle.nix now
defaults idle.fingerprint from that same key — so the lock screen and
login/sudo move together instead of drifting apart the way they did until
e2de906. nomarchy-fingerprint does the two rebuilds this needs (sudo
system for PAM, home switch for hyprlock) and refuses to turn on with no
finger enrolled.

System › Auto-login is new (nomarchy-autologin), and it is what decides
whether anything is asked at boot: auto-login on means no prompt whatever
the fingerprint switch says; off means the greeter asks, for a password or
a finger. Installer-seeded ON for LUKS machines — the passphrase already
gates the disk — and off without it, where the greeter is the only thing
between power-on and the desktop.

Both had to become state-owned to be toggleable at all, which surfaced two
real bugs:

  * nomarchy.system.greeter.autoLogin defaulted from
    `config.nomarchy.settings…` — an attribute that exists ONLY on the Home
    Manager side. On NixOS it is absent and `or null` swallowed the error,
    so the default silently evaluated to null on every machine ever built.
    That is why the installer baked a Nix line: the state path never
    worked. Now read via theme-state-read.nix (the hardware.nix/timezone.nix
    pattern) and mkDefault'd, so the menu owns it and a hand-set line still
    pins it. Two more options read the same phantom bridge — BACKLOG #116.
  * `theme-sync get` printed Python's "None" for a JSON null, so every
    `case … null)` a caller writes would miss. Now prints "null", as the
    comment above it already promised for booleans.

The installer seeds the state instead of emitting the system.nix line,
because that line outranks the state and would strand the toggle.

V1 (V3 pending: HARDWARE-QUEUE). nix flake check --no-build, installer-
safety and option-docs all pass. Proved by eval/build, not assumed: a state
carrying autoLogin yields greetd initial_session {"user":"bernardo"}, the
template state (no autoLogin) yields none, and a hand-set null beats a state
that says otherwise; a state with only fingerprint.pam=true — nothing set by
hand — renders the hyprlock auth.fingerprint block; both new tools pass
bash -n and land in systemPackages (nomarchy-fingerprint only with a
reader); the patcher writes settings.greeter.autoLogin and no system.nix
line; and the get round trip prints null, so the menu reads "Auto-login
(off)" where it would have read "(on)".

The reader itself, the two rebuilds, and the reboot are hardware — queued.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:34:56 +01:00

18 KiB
Raw Blame History

Migrating an existing NixOS machine to Nomarchy (no reinstall)

If your machine already runs NixOS, you do not need the installer or a reformat to adopt Nomarchy. Nomarchy is a flake; "installing" it onto an existing NixOS box means pointing nixos-rebuild at a Nomarchybased flake that reuses your current hardware-configuration.nix. Nothing repartitions, and your /home is never written to by an activation.

This guide is written generically, with TuringMachine — a Lenovo AMD Ryzen 7840U / Radeon 780M laptop, LUKS + btrfs, systemdboot — as the concrete worked example. Substitute your own values where called out.

The promise: every step below is reversible. You keep three independent rollback nets (NixOS generations, a btrfs snapshot, and the fact that nixos-rebuild test never changes the boot default), and your files live on a separate subvolume that no config switch touches.


0. Is your machine a good candidate?

Migration is cleanest when your machine already matches Nomarchy's assumptions. Check each:

Nomarchy expects Check it If it differs
systemdboot bootctl status → "Product: systemdboot" GRUB works too; keep your loader config in system.nix
btrfs with @/@home subvolumes findmnt -t btrfs ext4/xfs boot fine — you just don't get snapshots
@snapshots + @home-snapshots subvols in findmnt output snapper features need them; create them or skip snapshots
LUKS (optional, themed prompt) lsblk -f shows crypto_LUKS none — LUKS is optional
Already a flake config test -f /etc/nixos/flake.nix fine either way; you'll write a fresh flake regardless

Installer vs migration snapshot layout. A fresh Nomarchy install (disko) creates a top-level @snapshots subvolume mounted at /.snapshots, then a first-boot oneshot makes a nested /home/.snapshots under @home for snapper's home timeline — it does not create a separate top-level @home-snapshots. Migration machines (e.g. TuringMachine) may already use top-level @snapshots and @home-snapshots; that is fine. Snapper only needs a .snapshots path under each tracked subvolume (/ and /home), so either layout works — reuse what you have, or create the missing pieces if you want snapper without reformatting.

TuringMachine matches all of these, including the @snapshots / @home-snapshots subvolumes — so snapper works with zero disk work.

Version note. Nomarchy pins nixos-26.05. If you're on an older release (TuringMachine is on 25.11), the migration folds a onerelease upgrade into the switch. That's normal and supported — read the NixOS 26.05 release notes for option/package renames, and lean on generations if something regresses.


1. The two rules that protect your data

  1. Never bump system.stateVersion. It is an ondisk/service compatibility marker tied to when the machine was first installed — not the nixpkgs version. Nomarchy's template ships 26.05; you must change it back to your machine's original value. Find yours:

    $ nixos-option system.stateVersion   # or: nix eval .#nixosConfigurations.<host>.config.system.stateVersion
    "24.11"
    

    (TuringMachine: 24.11.)

  2. /home is never touched by an activation. A nixos-rebuild switch swaps the system generation; your data subvolume is untouched. The Phase 0 snapshot is beltandsuspenders, not a necessity for file safety.


Phase 0 — Safety net (nothing changes yet)

# Read-only btrfs snapshots of root and home — instant rollback targets.
sudo btrfs subvolume snapshot -r /       /.snapshots/pre-nomarchy-root
sudo btrfs subvolume snapshot -r /home   /home/.snapshots/pre-nomarchy-home

# Freeze your current config as a clean git baseline.
cd /etc/nixos && git add -A && git commit -m "pre-nomarchy baseline" || true

You are currently booted in a knowngood generation; it remains in the systemdboot menu throughout. Worst case at any later step: reboot and pick it.


Phase 1 — Build the Nomarchy flake alongside (no switch)

Stand the new config up in a working directory and build it without activating. This is the real safety line: iterate here until it builds green before anything touches the running system.

git clone https://git.bemagri.xyz/bernardo/nomarchy.git ~/nomarchy-migrate
cd ~/nomarchy-migrate
# Or start from the downstream template:
#   nix flake init -t "git+https://git.bemagri.xyz/bernardo/nomarchy.git?ref=v1"
#   (produces flake.nix/system.nix/home.nix/…)

A Nomarchy downstream flake owns exactly five files. Assemble them:

hardware-configuration.nix — reuse yours unchanged

Copy your existing hardware config in verbatim. This is what preserves your disks, LUKS, btrfs subvolumes and swap — the reason no reinstall is needed.

cp /etc/nixos/hosts/TuringMachine/hardware-configuration.nix ./hardware-configuration.nix

flake.nix — one mkFlake call (from the template)

{
  description = "TuringMachine — Nomarchy";
  inputs.nomarchy.url = "git+https://git.bemagri.xyz/bernardo/nomarchy.git?ref=v1";

  outputs = { nomarchy, ... }:
    nomarchy.lib.mkFlake {
      src = ./.;
      username = "bernardo";                 # <- your login name
      # Optional nixos-hardware profile(s) for your model. For an AMD laptop:
      #   hardwareProfile = [ "common-cpu-amd-pstate" "common-gpu-amd" "common-pc-laptop-ssd" ];
      # Names: https://github.com/NixOS/nixos-hardware  (verify before use)
      # Full hardware story (firmware, fingerprint, unsupported machines):
      #   docs/HARDWARE.md in the Nomarchy repo
    };
}

system.nix — machine specifics + your decisions

This is where your three migration decisions land: PPD power (no ryzenadj), no Secure Boot, and the stateVersion override.

{ pkgs, username, ... }:

{
  # Plain systemd-boot — no lanzaboote / Secure Boot.
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  networking.hostName = "TuringMachine";
  time.timeZone = "Europe/London";           # your zone
  i18n.defaultLocale = "en_US.UTF-8";

  users.users.${username} = {
    isNormalUser = true;
    extraGroups = [ "wheel" "networkmanager" "video" "input" ];
  };

  # ── Power: Nomarchy's PPD (drops all custom ryzenadj/TLP tuning) ──────
  nomarchy.system.power = {
    enable = true;
    backend = "ppd";                         # power-profiles-daemon
    laptop = true;
    batteryChargeLimit = 80;                 # optional longevity cap
  };

  # ── AMD 7840U / Radeon 780M ──────────────────────────────────────────
  nomarchy.hardware.amd.enable = true;       # amd-pstate + radeonsi VA-API
  # nomarchy.hardware.amd.rocm.enable = true; # opt-in GPU compute (multi-GB)

  # Auto-login is NOT set here: it lives in theme-state.json
  # (settings.greeter.autoLogin) so System  Auto-login can move it — a line
  # here would outrank the state and pin it. Turn it on after the first boot
  # with `nomarchy-autologin on` (what nomarchy-install seeds on encrypted
  # installs: the LUKS passphrase already gates access).

  # CRITICAL: keep your ORIGINAL install's value — never Nomarchy's 26.05.
  system.stateVersion = "24.11";
}

Dropped on purpose (decisions 1 & 2): your old modules/services/power-management.nix ryzenadj stack, the lanzaboote input, and the power-max/power-stealth scripts. PPD's performance/balanced/powersaver profiles (switchable from the Waybar battery/profile icons and nomarchy-menu) replace them.

home.nix — your apps on top of Nomarchy's desktop

Start from the template's home.nix (it ships the default app set) and add your personal packages/config. Carry over anything you still want (e.g. your emacs setup).

{ pkgs, lib, ... }:

{
  # Nomarchy hardcodes home.stateVersion = "26.05". Moving home-manager's
  # stateVersion is low-risk, but if you want to pin your original:
  home.stateVersion = lib.mkForce "24.11";

  home.packages = with pkgs; [
    # your extras — e.g. emacs, language toolchains, …
  ];
}

theme-state.json

Copy the template's theme-state.json (or let nomarchy-menu theme write it after the switch). Your old nomarchy-state.nix prototype (schema nomarchy.theme = "nord" …) is retired — the current distro uses this JSON. nord is a shipped Nomarchy theme, so you lose nothing.

The build gate

nixos-rebuild build --flake ~/nomarchy-migrate#default

Zero activation — this only evaluates and builds the system closure. Fix any eval/build error here, in isolation, before touching the running machine. Expect to resolve a few 25.11→26.05 option renames.


Phase 2 — Reversible activation

# Activates now, but does NOT become the boot default. If the session
# breaks, REBOOT and you are back in your old generation, untouched.
sudo nixos-rebuild test --flake ~/nomarchy-migrate#default

# Bring the desktop (home-manager) up BEFORE the first graphical login.
# Hyprland without an HM generation shows the yellow "autogenerated
# config" banner (and no Nomarchy theming) — finish this step first.
home-manager switch --flake ~/nomarchy-migrate#bernardo -b bak

Log into Hyprland and sanitycheck: Waybar renders, SUPER+M opens the menu, theming is coherent, SUPER+? shows the cheatsheet, no yellow Hyprland autogenerated banner. Confirm the machinespecific things you care about still work — suspend/hibernate, the AMD GPU (vainfo → radeonsi), display brightness.

You should also get a one-shot You're set toast (menu / themes / keys). If it never appears: systemctl --user status nomarchy-first-boot and re-try with nomarchy-theme-sync set settings.firstBootShown false --no-switch then log out/in.

If anything is wrong: reboot → old generation. Nothing is committed as default yet.


Phase 3 — Reconcile

  • Power: verify powerprofilesctl get works and the Waybar battery / powerprofile icons open the power menu. Your ryzenadj scripts are gone; if you miss a specific TDP behaviour, that's a followup, not a blocker.
  • Theme: nomarchy-menu theme → pick a preset (writes theme-state.json).
  • Snapshots: nomarchy-menu → System → Snapshots should see your existing @snapshots subvolume.
  • Fingerprint: fingerprint.enable = true only starts fprintd (enrollment). Login/sudo finger auth is fingerprint.pam and is opt-in — leave it commented until you've enrolled. NixOS defaults PAM on whenever fprintd is enabled; Nomarchy forces PAM to follow the pam flag, but only after a system rebuild. Verify with grep pam_fprintd /etc/pam.d/sudo (should be empty when pam is off). With pam on, the prompt accepts password or finger in parallel by default (fingerprint.parallel = false for stock sequential) — see HARDWARE.md §5.
  • Browser profiles: Nomarchy does not manage Chromium/Firefox state. Bookmarks/extensions live under ~/.config/chromium (or ~/.config/google-chrome / ungoogled paths if that was your previous browser). If your old Home Manager config declared programs.chromium.extensions, carry that block into home.nix before first launch — HM installs those via External Extensions/*.json, and when the JSONs vanish Chromium treats every extension as externally uninstalled and deletes it together with its stored data (Local Extension Settings — wallet vaults, password-manager pairings). Bookmarks survive, which makes it look minor; it isn't. Recovery: close the browser, copy Default/Local Extension Settings/<id> (plus any Default/IndexedDB/chrome-extension_<id>_*) back from your Phase-0 pre-nomarchy-home snapshot, then reinstall each extension — from the Web Store (ids are stable, so the data reattaches) or by re-declaring the ids if you want them declarative again.
  • VPN: NetworkManager connections survive under /etc/NetworkManager and your home. System VPN lists NM vpn/wireguard profiles; import any that lived outside NM. Tailscale is opt-in (nomarchy.services.tailscale.enable).
  • Secrets/services: if you relied on agenixmanaged secrets for a service, layer agenix back into system.nix as a machinespecific import (Nomarchy doesn't manage secrets). If you don't need them, leave them out — this is a full cutover.

Phase 4 — Cutover

Once a test boot is solid:

# Move the flake to its canonical home and make it the boot default.
mv ~/nomarchy-migrate ~/.nomarchy
sudo nixos-rebuild switch --flake ~/.nomarchy#default
home-manager switch --flake ~/.nomarchy#bernardo

# Point /etc/nixos at the flake (optional but conventional).
sudo mv /etc/nixos /etc/nixos.pre-nomarchy
sudo ln -s ~/.nomarchy /etc/nixos

From here you're on the standard Nomarchy update flow: sys-update (lock + system) then home-update (desktop) — always in that order (a lock bump before the home switch, or desktop changes are skipped against the old lock).


Rollback, at any point

Net How
NixOS generation Reboot → pick the previous entry in the systemdboot menu.
nixos-rebuild test Never sets the boot default; a reboot reverts it.
btrfs snapshot Restore /.snapshots/pre-nomarchy-root (see docs/RECOVERY.md).
git baseline /etc/nixos.pre-nomarchy (and the prenomarchy commit) is your old config verbatim.

/home is untouched by all of the above.


Postmigration cleanup (once you're confident)

  • Delete the safety snapshots: sudo btrfs subvolume delete /.snapshots/pre-nomarchy-root (and the home one).
  • Remove /etc/nixos.pre-nomarchy and the old perhost modules you cut (ryzenadj powermanagement, lanzaboote, the nomarchy-state.nix prototype).
  • Prune old generations: sudo nix-collect-garbage -d.

Enabling hibernation on an existing machine (no reinstall)

New Nomarchy installs are hibernation-ready out of the box — the installer defaults the swapfile to = RAM on its own @swap subvolume and wires the resume offset. This section is for machines that have no hibernate swap: one installed with swap = 0, one migrated here whose reused hardware-configuration.nix carries no swapfile, or an older install predating the default. If swapon --show already lists /swap/swapfile, you have nothing to do.

Hibernation writes RAM to disk, and zram (compressed RAM) can't hold that image across a power-off — so you need a real disk swap ≥ the RAM you use. Nomarchy puts it on an @swap subvolume inside the encrypted volume, so the image is encrypted at rest and the initrd LUKS unlock gates resume.

Reversible: this is one subvolume plus four config lines. Remove them and rebuild — or just boot the previous generation — to undo. /home is never touched.

Worked example below is this machine: LUKS mapper cryptroot, btrfs @. Substitute your own device/UUID/offset where shown.

1. Size and locate

# Swap = RAM, rounded up to whole GiB (matches the installer default).
ram_gb=$(awk '/MemTotal/ {print int(($2 + 1048575) / 1048576)}' /proc/meminfo)

# The decrypted BTRFS device backing / (strip the [subvol] suffix) and its
# filesystem UUID — the same UUID resolves to the mapper once initrd unlocks.
dev=$(findmnt -no SOURCE / | sed 's/\[.*//')       # e.g. /dev/mapper/cryptroot
fsuuid=$(findmnt -no UUID /)                        # e.g. d8e2b02d-…
echo "swap=${ram_gb}G  dev=$dev  uuid=$fsuuid"

2. Create the @swap subvolume + swapfile

# Create the subvolume at the BTRFS top level (subvolid=5), beside @, @home…
sudo mkdir -p /mnt/btrfs-top
sudo mount -o subvolid=5 "$dev" /mnt/btrfs-top
sudo btrfs subvolume create /mnt/btrfs-top/@swap
sudo umount /mnt/btrfs-top && sudo rmdir /mnt/btrfs-top

# Mount it and create the swapfile. `mkswapfile` applies the BTRFS NOCOW
# requirements automatically (a copy-on-write swapfile would corrupt).
sudo mkdir -p /swap
sudo mount -o subvol=@swap,noatime "$dev" /swap
sudo btrfs filesystem mkswapfile --size "${ram_gb}g" --uuid clear /swap/swapfile

3. Read the resume offset

sudo btrfs inspect-internal map-swapfile -r /swap/swapfile   # prints the offset

4. Wire it into system.nix

Add these to your machine's system.nix, substituting your $fsuuid and the offset from step 3. The fileSystems."/swap" mount is required on this path — a fresh install inherits it from disko-generated hardware-configuration.nix, but a hand edit must declare it so /swap is mounted before swap activates:

  # Hibernation: encrypted swapfile on the @swap subvolume.
  fileSystems."/swap" = {
    device = "/dev/disk/by-uuid/<fsuuid>";
    fsType = "btrfs";
    options = [ "subvol=@swap" "noatime" ];
  };
  swapDevices = [{ device = "/swap/swapfile"; }];
  boot.resumeDevice = "/dev/disk/by-uuid/<fsuuid>";
  boot.kernelParams = [ "resume_offset=<offset>" ];

zram stays on by default (Nomarchy sets zramSwap at priority 100 in modules/nixos/oom.nix); this disk swapfile sits lower, so day-to-day paging stays in compressed RAM and the file is reserved for the hibernate image — exactly the intent.

5. Rebuild and test

sudo nixos-rebuild switch --flake ~/.nomarchy#default
systemctl hibernate      # or nomarchy-menu → Power → Hibernate

The machine powers off; on the next boot you unlock LUKS once and land back in your session. If you get a fresh boot instead, the usual cause is a wrong resume_offset or a swapfile smaller than in-use RAM — re-read the offset (step 3) and confirm swap ≥ RAM.

No LUKS? Same steps; dev/$fsuuid point at the plain BTRFS partition and the image is unencrypted at rest. swap = 0 opt-out stays valid — if you don't want hibernation, skip all of this; the Power-menu Hibernate row just reports that no swap is configured.


TuringMachine — the decisions, at a glance

Item Choice
Power Nomarchy PPD (backend = "ppd"); all ryzenadj/TLP tuning dropped
Secure Boot Off — plain systemdboot, lanzaboote dropped
Scope Full cutover to Nomarchy's structure
system.stateVersion 24.11 (preserved from original install)
Hardware nomarchy.hardware.amd.enable = true (7840U / Radeon 780M)
Bootloader systemdboot (unchanged — already matched)
Filesystem LUKS + btrfs, existing subvolumes reused (incl. snapshot subvols)