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

540 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](https://nixos.org/manual/nixos/stable/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:
```console
$ 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)
```bash
# 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.
```bash
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.
```bash
cp /etc/nixos/hosts/TuringMachine/hardware-configuration.nix ./hardware-configuration.nix
```
### `flake.nix` — one `mkFlake` call (from the template)
```nix
{
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**.
```nix
{ 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).
```nix
{ 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
```bash
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
```bash
# 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:
```console
$ 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:
```console
$ 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):
```ini
[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:
```bash
# 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
```bash
# 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
```bash
# 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
```bash
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:
```nix
# 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
```bash
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.json` → `state.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) |