mirror of
https://github.com/omacom/omarchy.git
synced 2026-09-28 06:13:13 +08:00
Premature to have this included like this
Lots of great discussions, but it's for Omarchy Cinque, if it happens. This repo is for Quattro for now.
This commit is contained in:
-150
@@ -1,150 +0,0 @@
|
||||
# Plan: Nix — replace Arch with a sovereign Nix foundation
|
||||
|
||||
Revision 2. Rev 2 incorporates adversarial review by codex (xhigh): atomicity restated as atomic selection rather than transactional activation, staged switches for major updates, password hashes kept out of the store, a legal-redistribution gate for unfree packages, precise sovereignty boundaries (mise, fwupd, Steam, Cloudflare), a signed release manifest with anti-rollback, source-rebuild proof in the continuity gate, garbage-collection policy, and a substantially hardened migration: supported-layout gating, live-probed hardware config, an explicit boot transaction with user blessing, state-divergence policy for the shared home, and two-stage rollback.
|
||||
|
||||
## Problem
|
||||
|
||||
Omarchy spends a remarkable amount of its code protecting users from its own package manager. The scars are all pacman-shaped:
|
||||
|
||||
- `omarchy-update-system-pkgs-when-conflicted` is ~150 lines of quarantine choreography — stash unowned conflicting files under `/var/lib/omarchy/replaced`, retry, restore what the upgrade didn't claim — because pacman refuses to own file conflicts.
|
||||
- The `etc-overrides/` mechanism (`docs/file-layout.md`) exists solely because pacman won't let two packages touch the same `/etc` file, so we ship copies to `/usr/share/omarchy/etc-overrides/` and `cp -f` them into place from scriptlets.
|
||||
- An ALPM hook (`00-omarchy-update-guard.hook`) aborts direct `pacman -Syu` because updates that bypass `omarchy update` skip the coordination around them — snapshots, migrations, hooks, restart checks; we built a guard to keep users away from the distribution's own tooling.
|
||||
- The keyring dance in `omarchy-update-keyring` bootstraps trust through `keys.openpgp.org`, and `etc/gnupg/dirmngr.conf` lists five more external keyservers — our signature chain roots outside our infrastructure.
|
||||
- Updates are not atomic, so we bolted atomicity on: snapper snapshots plus `limine-snapper-sync` approximate what the package manager can't promise, and `docs/update-process.md` still lists pacnew/pacsave handling as an open wound.
|
||||
- `omarchy-upgrade-to-quattro` is 2,389 lines. That is what it costs to move a fleet of mutable, individually-drifted Arch installs through one package-layout transition.
|
||||
|
||||
And sovereignty is only half-won. We already run the hosting — `mirror.omarchy.org` serves core/extra/multilib, `pkgs.omarchy.org` serves the `[omarchy]` repo, stable deliberately trails upstream Arch by a month (`manual/30-updates.md`) — but we don't own the substance. Arch decides what a "system upgrade" contains and when soname bumps land; we inherit every decision a day later and can only delay it. The AUR path (`omarchy-pkg-aur-*`, the Install menu) executes unsigned build scripts fetched live from `aur.archlinux.org`. T2 Macs add a GitHub-hosted third-party repo with `SigLevel = Never` (`install/hardware/pacman.sh`). And because every install mutates independently, no two Omarchy machines run the same bytes — "we tested this update" is a statement about our machine, not yours.
|
||||
|
||||
Nix fixes the category, not the symptoms. A NixOS system is a closure: one immutable tree of store paths containing every package, config file, and service definition, built once, signed once, and selected atomically — the running system is a symlink flip to a complete generation, and the old one stays bootable. Rollback is booting the previous generation; file conflicts and pacnew files are structurally impossible; and every machine's packages are the byte-identical store paths we built and tested (the thin top-level closure that composes them — hostname, disk UUIDs, the user's package manifest — is assembled per machine; the payload is not). To be precise about what is and isn't atomic: *selecting* a generation is atomic, *activating* one is a sequence — services stop, activation scripts run, services start — and a step in that sequence can fail. The design below stages risky switches across a reboot for exactly that reason. The catch is that the Nix ecosystem assumes nixos.org: `cache.nixos.org` as substituter, nixpkgs from GitHub, channels from `channels.nixos.org`, an install script piped from their web server. This plan takes the technology and none of the hosting.
|
||||
|
||||
## Shape
|
||||
|
||||
- Omarchy becomes a NixOS-based system whose entire supply chain runs on omarchy.org infrastructure: a pinned nixpkgs fork on our git hosting, closures built on our build farm, binaries served from our signed cache. A user's machine never contacts nixos.org, cache.nixos.org, or GitHub for OS concerns — the same posture `pkgs.omarchy.org` and the mirrors have today, extended until it covers everything.
|
||||
- Users don't learn Nix. The `omarchy` CLI keeps its verbs (`omarchy-pkg-add`, `omarchy update`, `omarchy-channel-set`), `~/.config` stays your mutable files, themes and the refresh pattern are untouched. Nix is plumbing, exactly as pacman was plumbing — it just leaks less.
|
||||
- An update is: fetch prebuilt, signed store paths from our cache, compose the new generation, activate it — across a reboot when the jump is big — and keep the old generation bootable. What we ship is what we tested, store path for store path.
|
||||
|
||||
## Sovereignty, precisely
|
||||
|
||||
"Sovereign" means two different things at two different times, and the plan should be honest about which is which:
|
||||
|
||||
- **Runtime sovereignty (absolute, for OS delivery)**: an installed machine resolves every OS need — binaries, sources, expressions, signatures, update metadata — against omarchy.org hosts only. No fallback substituters, no keyservers, no GitHub fetches, no upstream flake registry. If nixos.org vanished tomorrow, no user would notice.
|
||||
- **Build-time sovereignty (continuity)**: our infrastructure ingests from upstream nixpkgs at development time, then archives everything — the nixpkgs tree in our git mirror, every source tarball in our archive, every build product *and its build closure* (sources, patches, derivations, the compilers that made it) in our cache. If upstream vanished, we could keep building, patching, and releasing from what we hold, indefinitely. What we do not claim: re-deriving the world from a bootstrap seed. Nixpkgs' standard binary bootstrap tarballs are part of what we mirror and trust; full source-bootstrap purity is out of scope.
|
||||
|
||||
The boundary is OS delivery, and the plan names what sits outside it rather than letting "absolute" quietly overclaim. `mise`-managed tools pull from GitHub and language registries; fwupd firmware comes from LVFS; Steam downloads Valve's content; browsers update their own components. Those are application-content channels the user chose, not OS delivery, and they keep working — but each gets an explicit decision (mirror it, repoint it, or declare it outside the promise) instead of an assumption. The dev channel's GitHub clone in `omarchy-channel-set` repoints to our git hosting. And Cloudflare stays as the DDoS shield and CDN (`manual/48-security.md`), but the cache origin is storage we control, with a documented path to serve it from elsewhere — a CDN in front of sovereign infrastructure, never the only copy of it.
|
||||
|
||||
The release gate makes this testable, in two parts. Delivery: a release is publishable only if a clean machine, with outbound network restricted to omarchy.org, can install the ISO, update, and install every curated extra. Continuity: from an empty store, with binary substitution disabled and only our source archive reachable, the release closure must rebuild — proving we archived the build inputs, not just the outputs. Sovereignty becomes a CI assertion instead of an aspiration.
|
||||
|
||||
## Rejected approaches
|
||||
|
||||
- **Nix on top of Arch** (Nix as a secondary package manager, Arch stays the base): two package managers, two update pipelines, two failure modes, and the worst properties of both — pacman still owns the system, so none of the atomicity or reproducibility arrives where it matters. The halfway house costs most of the migration and delivers little of the payoff.
|
||||
- **Guix**: the same functional model with a nicer language, but its FSDG-purist stance on proprietary firmware, microcode, and NVIDIA drivers means fighting the distribution on exactly the hardware enablement (`install/hardware/` is 51 leaves deep) that Omarchy considers table stakes. Nonguix exists; building a product on an unofficial channel the project disowns is not a foundation.
|
||||
- **cache.nixos.org as fallback substituter**: the tempting hedge — use our cache first, theirs when we miss. It silently converts every gap in our build coverage into an external runtime dependency, which is precisely the failure mode this plan exists to eliminate. Misses should fail loudly and get fixed in our farm, not papered over by someone else's CDN.
|
||||
- **Hydra for the build farm**: the canonical Nix CI is a sprawling Perl application that is its own operational project. Our release matrix is a known, finite list of targets; plain `nix build` over that list in ordinary CI, followed by `nix copy` to the cache, does the job with tooling we already understand.
|
||||
- **A live binary-cache daemon** (Attic, Harmonia): a Nix binary cache is narinfo and nar files — static content. Object storage behind Cloudflare is the same shape as the pacman repo we serve today, has no attack surface, and scales for free. A daemon earns its keep only if we later want deduplicating storage across many releases; start dumb.
|
||||
- **home-manager for user configs**: it would make `~/.config` a farm of read-only symlinks into the store, which is the opposite of Omarchy's "your files" philosophy (`plans/dots.md` exists because those files are yours to edit). The declarative boundary stops at the system layer; the user layer stays mutable plain files.
|
||||
- **Image-based atomicity instead** (ostree/Silverblue-style, or A/B partitions): atomic, but at image granularity — you get our image or you get nothing, and local package additions become a bolted-on overlay mechanism. Nix gives the same atomicity at package granularity, so `omarchy-pkg-add` keeps meaning something.
|
||||
- **Staying on Arch and hardening further**: the baseline. Every mitigation above can be polished, but they remain mitigations for structural properties — mutability, non-atomicity, conflict-prone file ownership — that pacman cannot shed. We would be signing up to maintain the workaround museum forever.
|
||||
|
||||
## Design
|
||||
|
||||
### Supply chain
|
||||
|
||||
- **nixpkgs fork**: a mirror of nixpkgs on our git hosting, plus an `omarchy` branch carrying our patches (the successor to `omarchy-pkgs`' PKGBUILD patches). Each release pins an exact revision. Flake inputs reference our tarball endpoint (`https://mirror.omarchy.org/src/nixpkgs-<rev>.tar.gz`) with the lockfile's `narHash` pinning content, so even the expression source is fetched from us and integrity-checked.
|
||||
- **Source archive**: builders fetch upstream sources once, at ingestion; every fixed-output derivation's output is then held in our cache and our source mirror. `hashedMirrors` pointed at omarchy.org covers `fetchurl`, but it is a hint, not a boundary — `fetchgit`, flake fetchers, and language-ecosystem fetchers each need their own mirroring, and a cache miss makes Nix try a local build whose fetcher will happily call GitHub. So the boundary is enforced where it can't be forgotten: builder and client network policy allows omarchy.org only, and a miss *fails loudly* — a hole in our archive is a bug to fix in the farm, never a silent fallback to upstream. Rebuilds never need the original upstream URL to still exist.
|
||||
- **The omarchy flake**: lives where `omarchy-pkgs` lives today — same repo split as now (this repo is the runtime; the packaging repo owns pins, the overlay of packages nixpkgs lacks, and the NixOS modules; `omarchy-iso` owns the installer). The T2 Mac kernel and the `linux-ptl` kernel move from third-party repos and AUR-adjacent sources into our overlay, built and signed on our farm — which closes today's `SigLevel = Never` hole outright.
|
||||
|
||||
### Binary cache and trust
|
||||
|
||||
- `cache.omarchy.org`: narinfo + nar objects on object storage behind Cloudflare, populated by `nix copy` from the farm, signed with an Omarchy ed25519 cache key. Released objects are write-once (object-locked): a nondeterministic rebuild must never silently replace a narinfo the fleet already trusts.
|
||||
- Cache signatures authenticate store paths; they do not say "this is the current stable release." That job belongs to a **release manifest**: a small document per channel naming the release version, the exact top-level closure hashes per hardware variant, and an expiry — signed offline with a release key that is *separate* from the cache key, monotonically versioned so a compromised CDN cannot replay last month's release, and re-signed on a cadence so a frozen mirror goes stale loudly. `omarchy-update-available` and the update flow trust the manifest first, paths second. Key hygiene — build key, cache key, release key, rotation, and revocation — is a Phase 0 deliverable with a rehearsed compromise-recovery runbook, not an appendix.
|
||||
- Client `nix.conf` (owned by our NixOS module, not user-editable state): `substituters = https://cache.omarchy.org` — nothing else, replacing the default cache.nixos.org entirely; `trusted-public-keys` lists only our key; the flake registry is pinned to our own registry file so bare flake references cannot reach GitHub.
|
||||
- Trust roots: the cache and release public keys ship inside the ISO and the installed closure. `keys.openpgp.org`, `archlinux-keyring`, `omarchy-update-keyring`, and the five keyservers in `etc/gnupg/dirmngr.conf` all leave the OS trust path (gnupg remains for the user's own purposes).
|
||||
|
||||
### Build farm
|
||||
|
||||
Our own builders run `nix build` over the release matrix: the base system closure per hardware variant (NVIDIA open/legacy, T2, `linux-ptl`, plain), every optional package behind the Install menu and `omarchy-install-*`, and the ISO. A release job then verifies the gate: every store path in every target closure must be substitutable from `cache.omarchy.org` before the release tag is signed. Nothing a user can reach through blessed UI may miss the cache.
|
||||
|
||||
One gate is legal, not technical: nixpkgs distinguishes redistributable-unfree from unfree-you-may-not-redistribute, and serving a package from our cache *is* redistribution. NVIDIA userspace drivers (nixpkgs patches them), VS Code, Chrome, vendor firmware, and printer blobs each need a per-package answer in Phase 0: confirmed redistribution rights, a redistributable substitute (VSCodium-shaped choices), or a blessed vendor-fetch exception — which is a named, per-package hole in the runtime-sovereignty claim, recorded as such rather than discovered later. No package enters the curated set without landing in one of those three buckets.
|
||||
|
||||
### The system layer
|
||||
|
||||
- Everything under `install/config/`, `install/hardware/`, and the `etc/` tree becomes NixOS module code: `services.displayManager.sddm`, `boot.plymouth`, snapper, docker, cups hardening, the sysctl/sudoers/tmpfiles drop-ins, the NVIDIA modprobe and initrd logic that today lives as conditional bash inside `etc/mkinitcpio.conf.d/omarchy_hooks.conf`. `omarchy-apply-system` and `omarchy-apply-hardware` become module imports plus hardware-variant selection instead of sourced shell leaves — and the entire `etc-overrides/` mechanism is deleted, because composing `/etc` from multiple sources is what the module system is.
|
||||
- **Bootloader**: limine stays — NixOS ships a `boot.loader.limine` module — but its job changes: boot entries are system generations, not snapper snapshots, so `limine-snapper-sync` and `limine-mkinitcpio` retire. The UKI and fallback-entry behavior configured in `etc/limine-entry-tool.d/` and the direct-boot path (`omarchy-setup-direct-boot`) must be reproduced deliberately — upstream's limine/UKI story is still settling — and boot security is its own workstream: Secure Boot stays explicitly unsupported (as `manual/02-getting-started.md` says today) unless that workstream designs key enrollment, measurement, and recovery properly; it does not sneak in as a module default.
|
||||
- **Per-machine composition, budgeted**: the cache delivers every package prebuilt, but each machine still evaluates and assembles its thin top-level closure — `/etc`, initrd, activation scripts — locally on every switch. That cost is real on low-end hardware and gets a measured budget (time and memory, on the weakest supported machines) in the acceptance suite, not an assumption that "everything substitutes, so it's fast."
|
||||
- **A supported customization layer**: `/etc` becoming module-owned cannot mean "hope nobody needed to change it." Mounts, sudo rules, kernel parameters, and service tweaks are system concerns with no home-directory equivalent, so the machine gets a blessed local-override file the modules import — real Nix options, documented, surviving updates — and every managed `/etc` file has a named owner. Coordination that today hides behind the pacman guard (migrations, hooks, restart markers) moves into activation-time logic keyed by release version, so even a user running `nixos-rebuild` directly cannot skip it: `omarchy update` stays the pleasant path, but correctness no longer depends on being the only path.
|
||||
- **Store hygiene**: closures don't orphan, but unreferenced store paths accumulate and old generations are what rollback is made of — so garbage collection is policy, not an afterthought: automatic GC with a generation-retention window, a cap on boot-menu generations, and a free-space floor, sized so the store's steady state on a user disk compares honestly with today's pruned pacman cache.
|
||||
- **Filesystem**: btrfs stays for `/home` (snapper's remaining job: user-file snapshots, until `plans/backup.md` and `plans/dots.md` cover that ground) and for `omarchy-system-factory-reset`'s subvolume mechanics — though the reset workflow itself (the `@factory` baseline, UKI rebuild, LUKS re-key) must be ported, and the restore guarantee narrows honestly: booting an old generation restores the OS, not mutable `/var` state the old root snapshots used to carry. `omarchy-snapshot restore` for the OS becomes "boot the previous generation."
|
||||
|
||||
### The user layer stays mutable
|
||||
|
||||
Non-negotiable: `~/.config` remains plain files the user owns and edits. `/etc/skel` seeding, `omarchy-refresh-config`, themes, and the entire `default/` → `~/.config` pipeline work unchanged. The declarative world ends at the system/user boundary; crossing it (home-manager) is rejected above. This is the line that keeps Omarchy feeling like Omarchy rather than like NixOS.
|
||||
|
||||
### Package UX
|
||||
|
||||
- The machine grows a package manifest — a plain text list in the spirit of `install/omarchy-base.packages`, owned by the machine, listing what this user added. `omarchy-pkg-add <name>` resolves the name (an alias table maps established Arch names to nixpkgs attributes, so muscle memory and the menu's package names keep working), appends to the manifest, and rebuilds against our cache — prebuilt, so "rebuild" means download, a local re-evaluation, and a switch: never a compile, and held to the per-machine composition budget above rather than assumed fast. `omarchy-pkg-drop` removes and rebuilds. `pkg-present`/`pkg-missing` query the running closure.
|
||||
- The Quickshell menu's guard prelude (`shell/plugins/menu/MenuModel.js` snapshots `pacman -Qq` plus a Provides parse because forking per guard "spends over a second") gets simpler and faster: one listing of the current closure's package set, computed at activation time and cached, replaces the pacman queries.
|
||||
- **The AUR is gone, replaced by the curated extras set**: everything the Install menu offers today (Chrome, Brave, Zen, VS Code, Steam and the lib32 Vulkan stack via nixpkgs' 32-bit support, and friends) comes from nixpkgs or our overlay, built and signed on our farm — the first time Omarchy's optional software carries the same signature chain as its core. Arbitrary AUR browsing (`omarchy-pkg-aur-install`) has no sovereign equivalent and is not replaced. The escape hatch for power users — adding their own flakes or substituters — is real Nix, documented as leaving the supported, sovereign envelope, and never wired into blessed UI.
|
||||
|
||||
### Updates, channels, migrations
|
||||
|
||||
- `omarchy-update` keeps its skeleton — transcript, lock, free-space check, confirm, stay-awake, migrations, hooks, `omarchy-update-restart` — and swaps its heart: the pacman transaction becomes "download the release closure from the cache, then switch." Failure before activation leaves the running system untouched, and the failed download costs nothing. Activation itself is the sequence that can still hurt — services stop, scripts run, services start — so routine updates switch live, while kernel and other big jumps stage as the *next boot's* generation and activate through the reboot `omarchy-update-restart` already prompts for. `omarchy-update-analyze-logs` survives with a shorter beat: activation and service-restart failures still deserve forensics; package transactions no longer do. And rolling back a generation rolls back the OS, not `/var` — a service that migrated its database forward needs its own story, which is what snapshots-before-update remain for.
|
||||
- Deleted outright, with the failure modes they existed for: `omarchy-update-keyring`, `omarchy-update-pkg-prune`, `omarchy-update-system-pkgs-when-conflicted`, `omarchy-update-pacman-guard` and the ALPM hooks, `omarchy-update-orphan-pkgs` (replaced by the GC policy above), `omarchy-update-aur-pkgs`, and the pacnew concern. The guard's job — "don't update behind Omarchy's back" — is covered by the activation-time coordination described above, which runs no matter who triggers the switch.
|
||||
- **Channels**: `stable`/`rc`/`edge` become branches of the omarchy flake with their own nixpkgs pins and their own cache prefixes, mirroring today's three pacman.conf templates. `omarchy-channel-set` flips the flake reference and switches. `dev` keeps its meaning: a local checkout via `omarchy-dev-link`, with `omarchy update` fast-forwarding it as now.
|
||||
- **Version**: real at last. `omarchy-version` reports the release tag of the running closure instead of deriving it from `pacman -Q`; `omarchy-update-available` compares that against a small release-manifest JSON on the cache host instead of running `checkupdates`.
|
||||
- **Migrations** (`migrations/`, 94 files) shrink to their legitimate residue: user-space state under `$HOME`. The 14 that touch pacman/limine/mkinitcpio have no successors — system-state transitions become module code that is simply part of the next closure. The per-user marker mechanism and `omarchy-migrate-notify` survive for what remains.
|
||||
|
||||
### ISO and installer
|
||||
|
||||
`omarchy-iso` rebuilds around a NixOS ISO carrying the full release closure in its store. Offline installation becomes `nix copy` from the ISO's store to the target plus writing the hardware module selection and the machine manifest — structurally the same "offline mirror" trick the ISO does today with pacman packages, minus the post-install `pacman.conf` restore dance (`install/post-install/pacman.sh`). The ISO signature chain (`iso.omarchy.org`, `.sig`) is unchanged.
|
||||
|
||||
## Migrating from Quattro to Cinque
|
||||
|
||||
`omarchy-upgrade-to-quattro`'s 2,389 lines are the cautionary tale for what in-place transitions cost — and that one didn't change the package manager. But Quattro's standard disk layout is the opportunity: root on a btrfs subvolume (`@`) with `/home` on its own (`@home`), inside one LUKS container, under a bootloader that already knows how to offer multiple roots. That layout lets Cinque move in *beside* Quattro instead of on top of it.
|
||||
|
||||
### The parallel-root migration
|
||||
|
||||
`omarchy-upgrade-to-cinque` ships as an ordinary Quattro package update, the same delivery path the v3→v4 upgrader used. It never runs unprompted — migration is an explicit user action, announced through the usual channels, never something `omarchy update` springs on anyone.
|
||||
|
||||
1. **Preflight, running system untouched**: the migrator supports the standard layout — btrfs root on `@`, `/home` on `@home`, one LUKS container, limine — and *refuses* everything else (LVM, RAID, exotic mount graphs, hand-built boot chains) toward the reinstall path; `omarchy-system-factory-reset` already gates on the same layout for the same reason, and for boot and storage, "I don't recognize this" is a blocker, not a warning. Then: a hardware gate — the machine's variant (NVIDIA generation, T2, `linux-ptl`) must have a built Cinque closure in the cache, or the migrator refuses with "not yet" rather than "hope so"; a space gate computed from the actual NAR sizes the cache reports plus the retained Quattro root, btrfs metadata headroom, and ESP room for both systems' boot artifacts (the fixed 10 GiB check in `omarchy-update-requires-free-space` is not an estimator); hibernation detection — a suspended image or the swap-subvolume setup from `omarchy-hibernation-setup` is invalidated and its resume configuration carried or rebuilt, because resuming one OS's hibernation image from the other corrupts the filesystem; and the inventory that feeds the *won't-survive report* (see below), which the user reads before consenting.
|
||||
2. **Fetch**: the release closure downloads from `cache.omarchy.org` into a fresh `@cinque` subvolume's `/nix` store — resumable, verifiable against signatures, and entirely inert while Quattro keeps running. The sovereignty gate applies here too: the whole migration touches only omarchy.org hosts.
|
||||
3. **Carry state**: the partition table, LUKS container, and `@home` are untouched — Cinque mounts the same `/home`. The machine's module configuration is generated from the *live* system — current mounts, `fstab`, `crypttab`, `lsblk`, `/proc/cmdline` — not from a replay of historical hardware detection, and the generated initrd is validated before anything is asked to boot from it. Accounts carry as the full database, not a hash import: `/etc/passwd`, `/etc/shadow`, groups, and NixOS's ID-stability state move as root-only files with `users.mutableUsers` on — password hashes must never be interpolated into the world-readable store. `machine-id`, SSH host keys, and NetworkManager connections come along; `/var/lib` payloads that are data rather than OS (docker volumes chief among them) are copied with their services stopped — a reflink copy of a live database is cheap and worthless.
|
||||
4. **First boot, Quattro still the default**: the migrator adds a Cinque boot entry inside a deliberate boot transaction — the ESP contents and firmware boot variables are inventoried and backed up first, foreign entries (Windows, other distros, the fallback loader) are preserved, and machines using `omarchy-setup-direct-boot`'s NVRAM path get that path handled explicitly. The user boots Cinque by choosing it; limine has no proven boot-once/boot-counting mechanism today, so *blessing is a human act*: first-boot verification (graphical session reached, network up, closure healthy) presents its results and asks before Cinque becomes the default. A failed boot needs no cleverness — the default was still Quattro, and a diagnostic bundle waits for `omarchy-upload-log`.
|
||||
5. **Rollback window, then reclaim**: at cutover the migrator snapshots `@home` — the two systems share a live home from here on, and applications will migrate profiles and state forward in formats the old side may not read, so a real return to Quattro needs that anchor to offer. `omarchy-upgrade-to-cinque --rollback` is two-stage by construction: it makes Quattro the default and reboots into it; only then, from the running Quattro, does it offer to restore the home snapshot (with post-cutover writes preserved alongside, never silently discarded) and remove `@cinque` — a system never deletes the root it is running on. In the other direction, `--reclaim` (or the update pipeline, after enough clean boots — open question) deletes the Quattro root and returns the space.
|
||||
|
||||
Rollback is a reboot plus a decision about state, and the plan says so — the OS comes back untouched by menu choice; the shared home's forward drift is what the cutover snapshot exists to answer. That is still a property no in-place mechanism can offer, and it is what makes offering the migration to a fleet responsible rather than reckless.
|
||||
|
||||
One wrinkle owned explicitly: during the window, exactly one side owns the bootloader — Cinque, from the moment its entry is blessed. The Quattro root is kept bootable but frozen — the migrator's only writes into it are disabling `limine-snapper-sync` and the update timers, because two operating systems regenerating one boot configuration is how both stop booting. Booting Quattro during the window is for rescue and rollback, not for continued dual life; the way back to a *living* Quattro is `--rollback`, which returns bootloader ownership along with the default.
|
||||
|
||||
### The won't-survive report
|
||||
|
||||
Some of what a Quattro machine accumulated has no Cinque equivalent, and the preflight says so per-machine, before anything changes:
|
||||
|
||||
- **Packages the user added**: the delta of `pacman -Qqe` against the Quattro release baseline (the raw list would drown the signal in the base system), run through the alias table into the manifest; AUR packages without an overlay equivalent (`pacman -Qem` minus the curated set) are listed by name with the escape-hatch documentation linked. Not a blocker — the user decides.
|
||||
- **Custom pacman repos**: both the `pre-refresh-pacman.d` hook layer and repos hand-added to `pacman.conf`, named as unsupported since the mechanism itself retires.
|
||||
- **System-level drift**: `pacman -Qii` backup-file diffs are the start, not the whole story — the scan also covers unowned files in `/etc` (`omarchy-update-system-pkgs-when-conflicted`'s quarantine logic proves we can tell), package-file divergence via `pacman -Qkk`, locally enabled or masked systemd units and drop-ins, DKMS modules, printer configuration, and firewall rules. Everything found is listed so the user can carry the *intent* forward — into `~/.config`, an Omarchy setting, or Cinque's local-override module — instead of silently losing edits. Drift in boot or storage configuration is a blocker, per the preflight.
|
||||
|
||||
Per-user state needs no migration at all: migration markers, themes, and everything else under `/home` ride along on `@home`. Dev-link users get their checkout fast-forwarded onto the Cinque branch by the migrator rather than a package swap.
|
||||
|
||||
### Rejected migration paths
|
||||
|
||||
- **`NIXOS_LUSTRATE` in place**: the historical takeover mechanism mutates the only root the machine has — a failure mid-lustrate is an unbootable machine and a restore from backup — and upstream is deprecating it (it doesn't work with the now-default systemd initrd, and NixOS's own guidance points at install-to-another-root instead, which is exactly what the parallel subvolume is). The parallel root delivers everything lustrate promised, plus a rollback that is just a boot-menu choice. Machines without room for two roots get "free up space first," not a reason to lose the rollback.
|
||||
- **Reinstall as the only path**: always supported, documented, and cheap once `plans/backup.md` and `plans/dots.md` land (which this plan therefore treats as prerequisites, not nice-to-haves) — but a migration path only matters if the fleet actually takes it, and "back up, reflash, restore" is where fleets quietly decide to stay behind. Reinstall is the fallback, not the offer.
|
||||
- **Automatic migration through `omarchy update`**: never. Changing a user's operating system's foundation is a decision, not an update.
|
||||
|
||||
## Rollout
|
||||
|
||||
- **Phase 0 — infrastructure, zero user impact**: nixpkgs mirror and tarball endpoint, source archive, `cache.omarchy.org`, the key hierarchy (build/cache/release, offline signing workflow, compromise runbook), the signed release-manifest format, the legal-redistribution inventory for the curated set, the build farm, and a CI job that builds the current desktop's equivalent closure and proves both halves of the sovereignty gate (delivery with outbound network restricted to omarchy.org; rebuild from the source archive with substitution disabled).
|
||||
- **Phase 1 — system parity** (packaging repo, with changes here): NixOS modules covering every `install/config/`, `install/hardware/`, and `etc/` entry; the flake with per-channel pins; boots and passes the graphical acceptance suite in the VM (`agents/skills/acceptance-tests.md`).
|
||||
- **Phase 2 — CLI port** (this repo): `pkg-*`, `update-*`, `channel-*`, `version-*`, snapshot/restore semantics, menu guards; delete the pacman-only organs; port the 18 pacman/yay-mocking test files in `test/shell.d/` to the new seams.
|
||||
- **Phase 3 — ISO and installer** (`omarchy-iso`): the offline NixOS ISO, installer flow, hardware detection wiring into module selection.
|
||||
- **Phase 4 — release and overlap**: ship as the next major; maintain the Quattro channels in parallel through the overlap window; deliver `omarchy-upgrade-to-cinque` as a Quattro package update, with the reinstall-with-restore path documented as the fallback.
|
||||
- **Docs and tests**: `docs/update-process.md` rewritten around the switch model; a new `docs/` reference for the sovereignty gate and cache/mirror topology; manual chapters for updating, rollback-by-generation, and the extras set; shell tests for manifest editing, alias resolution, channel flips, and guard-free update flow; switch-time and evaluation budgets measured on the weakest supported hardware in the acceptance suite; the release-gate CI assertion is itself the sovereignty test.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Which Nix**: upstream CppNix is the safe default; Lix is an argument about governance and pace we don't strictly need to have while we're rehosting everything anyway. Whichever we pick, users get it from our ISO and our cache — never from an install script on someone else's domain.
|
||||
2. **Flakes or stable evaluation**: flakes are the ecosystem's lingua franca but formally still experimental upstream. Since we pin our own Nix, we can adopt flakes and own the flag — or use plain evaluation with explicit pins and lose some tooling. Leaning flakes; deserves a deliberate decision.
|
||||
3. **How far the curated extras set reaches**: nixpkgs holds ~100k packages; we will build and cache hundreds, not all of it. What is the story when a user wants a package outside the set — a request pipeline into the overlay, the documented unsupported escape hatch, or both?
|
||||
4. **Reclaim policy** for the migration's rollback window: does the retained Quattro root get deleted only by explicit `--reclaim`, or automatically after N clean Cinque boots — and how long is a responsible default window on space-constrained disks?
|
||||
5. **Btrfs by default, still**: with system rollback moved to generations, btrfs earns its place only through `/home` snapshots and factory reset. Keep it, or simplify the default filesystem story?
|
||||
6. **Naming and posture**: "powered by Nix" is a fact; "a NixOS derivative" is a relationship with trademark and community expectations attached. How loudly do we say which — and does sovereign rehosting change what we ought to call it?
|
||||
Reference in New Issue
Block a user