Files
Nomarchy/docs/MIGRATION.md
Bernardo Magri e324876eb7
Some checks failed
Check / eval (push) Has been cancelled
feat(doctor): #147 — flag machine state the flake never set; linger is the worked example
/var/lib survives a migration untouched, and a stray pre-Nomarchy
Linger=yes marker shaped the QA box's session behavior for six months
with nothing in the flake to show for it. Ship the class as guards:

- nomarchy-doctor: new machine-state-drift check after the flake-checkout
  check — loginctl show-user -p Linger; "yes" warns (nothing is broken —
  it is drift) and prints the disable-linger fix; unknown-to-logind skips.
- docs/MIGRATION.md § Post-migration cleanup: "/var/lib machine state
  survives migration" — the worked example and the general suspicion rule.
- checks.doctor: linger sub-case — warn fires under enable-linger, exit
  stays 0, warning clears after disable-linger.

Deliberately NOT hardening the session wiring against the portal-gtk
restart race linger enabled: a clean install cannot reach that state.
The Waybar exec-once loose thread survives as BACKLOG #149.

Verification: V2 — shellcheck clean, nix flake check --no-build green,
nix build .#checks.x86_64-linux.doctor green (both branches exercised).
Implementation by a Sonnet subagent; design and review on Fable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 07:44:43 +01:00

22 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 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, …
  ];
}

state.json

Copy the template's 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-state-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 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.
  • Thunderbird / Firefox — “all my email is gone” (it isnt): Mozilla apps find your data through one small text file, ~/.thunderbird/profiles.ini (and ~/.mozilla/firefox/profiles.ini), which names the profile directory to open. Lose that file and the app does not error — it does what it does on a brand-new machine: creates an empty profile and cheerfully opens that. Every account, folder and message is still on disk, in the profile dir it stopped looking at. This bit us on a real migration (20260716): a 30 GB profile with 19 GB of ImapMail went “missing”; the fix was eight lines of profiles.ini. Before first launch, check the file exists and names your real profile:
    $ ls ~/.thunderbird/            # bernardo/  ← the fat one is your profile
    $ cat ~/.thunderbird/profiles.ini
    
    If an app opens empty, do not restore a snapshot — look first:
    $ du -sh ~/.thunderbird/*/      # a multi-GB dir = your data is fine
    
    If a fat profile is sitting there, this is a pointer problem, not data loss. Close the app, then write (Path= is the directory name, relative to the .thunderbird dir):
    [Profile0]
    Name=default
    IsRelative=1
    Path=<your-profile-dir>
    Default=1
    
    [General]
    StartWithLastProfile=1
    Version=2
    
    Keep the empty profile listed as [Profile1] (no Default=) if you want it out of the way rather than deleted. The restore is the risk here, not the bug: rolling a snapshot over a good 30 GB profile to “recover” data that was never lost can cost you the mail that arrived since. Same shape as the Chromium bullet above — a file HM used to manage disappears and the app reads its own absence as “first run” — but inverted: Chromium deletes quietly, Thunderbird loses nothing and looks catastrophic.
  • 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.

/var/lib machine state survives migration

A migration reconciles the flake — packages, services, dotfiles — and nothing else. Everything under /var/lib is left exactly as the previous OS wrote it, including settings the flake can neither see nor express. That state doesn't show up in git diff, doesn't get touched by sys-rebuild, and can sit there for months quietly shaping how the machine behaves.

Worked example: user lingering. Run loginctl show-user $USER -p Linger — on a Nomarchy machine the answer should be no, because the flake never sets it. On our own QA box a leftover Linger=yes from a preNomarchy setup kept the user's systemd instance alive across logouts, and it took six months to trace a string of subtle session bugs back to that one marker. Fix: sudo loginctl disable-linger <user>.

nomarchy-doctor now flags this specific case as a warning. More broadly: after migrating, if the machine ever behaves in a way the flake can't explain, suspect inherited state under /var/lib before you suspect the flake.


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.


State file rename (theme-state.jsonstate.json, #107)

The machine flake's git-tracked state file is state.json (appearance + menu settings). Older checkouts may still have theme-state.json.

  • Eval: lib.mkFlake and the reader accept either name (prefer state.json).
  • Write: nomarchy-state-sync (and the menu) always write state.json and remove a leftover theme-state.json so you never have two sources.
  • CLI: nomarchy-state-sync is the real name; nomarchy-theme-sync remains a symlink for scripts and muscle memory. Drop the alias after the next stable release notes call it out.

No action required on pull: the next theme apply or menu write migrates you. To migrate by hand: mv theme-state.json state.json && git add state.json (and git rm theme-state.json if it was tracked).


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)