nevaforget 2fd4dcd073 feat(installer): ISO that installs a host with one command
hosts/installer builds an image (nix build .#installer-iso) carrying this
flake as /etc/moonix, disko and a wrapper. The guest runs
`moonix-install <host>`: disko formats per the host file, nixos-install
installs, and the repository is cloned into the account own ~/flakes/moonix
with /etc/nixos/flake.nix pointing at it.

The image is built from the local tree, so a state does not have to be pushed
to be installable.

disko is patched to re-ask for the LUKS passphrase on a mismatch instead of
killing the script: its `exit 1` sits inside a function called from the until
loop that was meant to repeat the prompt.
2026-08-20 14:16:39 +02:00
2026-08-14 17:48:52 +02:00
2026-08-07 12:35:33 +02:00

moonix

NixOS configuration for a scrollable-tiling Wayland desktop: niri as the compositor, Quickshell as the bar and launcher, regreet as the login greeter, swaylock as the screen locker, Catppuccin Mocha throughout.

Ships Nix packages for the components that are not in nixpkgs — stasis, sweet-cursors, the moonarch helper scripts — and NixOS modules that assemble them into a working session. Greeter, locker and power menu are no longer own programs: regreet and swaylock come from nixpkgs, the power menu is a Quickshell popout inside the moonarch config.

The XDG configuration files (niri, Quickshell, foot, GTK, Qt) come from the moonarch repository, which is consumed as a flake input. One set of config files therefore stays valid on both NixOS and Arch.

Layout

flake.nix            Inputs and outputs
pkgs/                Build recipes
  stasis.nix         idle manager
  sweet-cursors.nix  cursor theme
  moonarch-scripts.nix  battery and camera helpers
modules/
  user.nix           moonarch.user option, set per host
  disk.nix           partitioning: GPT, optional LUKS2, btrfs subvolumes
  desktop.nix        /etc deployment, package set, theme
  greetd.nix         greetd + regreet, autologin per host
  services.nix       systemd system and user services
hosts/
  testvm/            QEMU test VM
  thinkpad/          ThinkPad T14 Gen 3 (AMD)
  desktop/           self-built AMD desktop, second disk for /home

nixosModules.moonarch is the entry point: it imports disko's NixOS module alongside the five above and hands them the packages and the config source.

Usage

Build a single package:

nix build .#stasis

Build a whole system without activating it, which is also how a host is checked for evaluation errors without touching anything:

nix build .#nixosConfigurations.testvm.config.system.build.toplevel

Activate. This runs on the machine being changed, out of a checkout that sits there — see "Keeping a machine up to date" for how that checkout gets there:

sudo nixos-rebuild switch --flake .#testvm

Installing on a machine

The disk layout is declared in modules/disk.nix, so nothing has to be generated on the target machine and no file travels back into this repo. Boot the NixOS ISO and run one command.

This erases the disk given to --disk. Check the path against lsblk on the machine you are standing in front of. Do not copy the path below without looking — it is this ThinkPad's SSD.

  1. Partition and install in one go:

    sudo nix --experimental-features "nix-command flakes" run \
      'github:nix-community/disko/latest#disko-install' -- \
      --write-efi-boot-entries \
      --flake 'git+https://gitea.moonarch.de/nevaforget/moonix.git#thinkpad' \
      --disk main /dev/disk/by-id/nvme-SKHynix_HFS001TEJ9X102N_SDB7N7740101A8708
    

    The LUKS passphrase is asked for twice while formatting. The repo is fetched from the flake reference, so the ISO needs no git and nothing is cloned.

    --write-efi-boot-entries is not optional here. Without it disko-install forces boot.loader.efi.canTouchEfiVariables off, overriding the true in the host file, and the install finishes without an NVRAM boot entry. Leave it out only when the disk is installed in one machine and then moved to another.

  2. Reboot.

The account comes up with the initial password declared in the host file (users.users.<name>.initialPassword, currently moonarch). Autologin gets past the greeter either way, but swaylock authenticates through pam_unix and cannot unlock a screen for an account without a password, so one has to exist from the first boot. Change it with passwd afterwards — mutableUsers is at its default, so that sticks and the declared value is only ever used to create the account.

hardware-configuration.nix is not used and not needed. NixOS puts nvme, ahci, sd_mod and the USB HID modules into the initrd by default (boot.initrd.includeDefaultModules), nixpkgs.hostPlatform comes from the flake, and the nixos-hardware profile for the T14 Gen 3 (AMD) covers firmware, microcode, power management and graphics quirks.

Only the partitioning step reads moonarch.disk.device. Nothing the booted system uses depends on it: the ESP entry and the generated boot.initrd.luks.devices point at /dev/disk/by-partlabel/, and the filesystems inside the container come up as /dev/mapper/crypted. Either way the disk may enumerate differently next time.

Adding another machine

  1. New directory under hosts/, with a default.nix holding the machine specific settings — hostname, users, locale, bootloader.

  2. Point the shared layout at its disk:

    moonarch.disk = {
      enable = true;
      device = "/dev/disk/by-id/...";   # check against lsblk
    };
    

    encrypt = false drops the LUKS layer, swapSize = "" the swapfile, and espSize resizes the ESP. Everything else is shared.

    A machine with a second disk for /home adds home, which moves the subvolume off the first disk:

    moonarch.disk.home = {
      enable = true;
      device = "/dev/disk/by-id/...";
    };
    

    Set the login name in the same file. moonarch.user has no default and is read by users.users, the autologin session and snapper, so the name is declared once per machine.

  3. Add a nixosConfigurations.<name> entry in flake.nix.

  4. Install with the same disko-install command, with #<name> and that machine's disk. A second disk is another flag:

    --disk main /dev/disk/by-id/... --disk home /dev/disk/by-id/...
    

    Both disks are erased. disko-install overrides whatever paths the host file declares, which is why hosts/desktop carries invalid placeholders: forgetting a flag fails the install instead of erasing a disk that was not meant to be touched.

Without a matching nixos-hardware profile, set hardware.cpu.intel.updateMicrocode or hardware.cpu.amd.updateMicrocode in the host file — the profile is what provides it for the ThinkPad.

Disk layout

Partition / subvolume Mountpoint Notes
ESP, espSize (1G) /boot vfat, umask=0077
root / btrfs, compress=zstd, noatime
home /home same; on the second disk where home.enable is set
nix /nix same; separate to keep the store out of snapshots
swap /.swapvol swapfile, swapSize (8G)

With encrypt = true everything below the ESP sits in a LUKS2 container named crypted. Hibernation is not set up — see DECISIONS.md.

A second disk gets its own container, crypted-home, and no ESP. Both are formatted separately, so disko-install asks for a passphrase four times instead of two. Give them the same one: both are unlocked from the initrd, where systemd caches the first passphrase and retries it on the second container, so booting prompts once.

Worth doing once the disks are formatted, though nothing here depends on it: back up the LUKS headers. They hold the encrypted keyslots, and a damaged header loses the data whatever the passphrase is. A header only changes when its keyslots do, so once after the install and again after a passphrase change is enough:

sudo cryptsetup luksHeaderBackup /dev/disk/by-partlabel/disk-main-root \
  --header-backup-file /run/media/<medium>/<host>-main.header

A second disk carries its own header on disk-home-home. Whoever holds such a file can attack the passphrase offline, so it belongs on a medium that gets put away — not in this repository and not in a synced directory.

Keeping a machine up to date

disko-install built the system from the flake reference and left no checkout behind, so a freshly installed machine runs its configuration without holding it. The checkout goes to ~/flakes/moonix and belongs to the account, so commits and pushes carry the account's identity. It is not read as configuration by anything on the machine — it is the source a system is built from, which is why it sits outside ~/.config:

git clone git@gitea.moonarch.de:nevaforget/moonix.git ~/flakes/moonix
sudo ln -s ~/flakes/moonix/flake.nix /etc/nixos/flake.nix

nixos-rebuild treats /etc/nixos/flake.nix as if --flake had been given and resolves the flake to the directory the symlink points into, so /etc/nixos itself is neither a repository nor a flake. Without an attribute the configuration is picked by hostname, which is why the nixosConfigurations are named after the machines.

Changing the machine is then an edit, a build and a commit:

cd ~/flakes/moonix
$EDITOR hosts/thinkpad/default.nix
sudo nixos-rebuild switch
git commit -am 'thinkpad: ...'
git push

The build reads the working tree, so a change is testable before it is committed. Nix only sees tracked files though — a new file under hosts/ or modules/ has to be git added or evaluation fails on the missing path.

A machine that is never edited locally can skip the checkout and build from the reference, which is what disko-install did:

sudo nixos-rebuild switch \
  --flake 'git+https://gitea.moonarch.de/nevaforget/moonix.git#thinkpad'

Every switch leaves a generation behind. A bad one is left through the boot menu, where the previous generation is still listed, or with sudo nixos-rebuild switch --rollback from a system that still comes up.

Version bumps

stasis is pinned to a release tag. After a new upstream release:

# adjust the tag in flake.nix, then
nix flake update stasis

moonarch tracks main, so its config changes arrive with nix flake update moonarch.

Configuration files

Config files are deployed unchanged wherever possible. These are generated instead, because they contain a path that does not exist under Nix, or because a NixOS module owns the destination:

File Reason
greetd/regreet.toml, regreet.css written by services.displayManager.regreet from module options; the wallpaper points at the store path
swaylock/config wallpaper path, rewritten and seeded into $HOME — swaylock's sysconfdir is inside the store, so a system-wide copy is never read
waypaper/config.ini wallpaper path, rewritten and seeded into $HOME
fontconfig defaults /etc/fonts/conf.d is owned by the NixOS module

kanshi/config is not deployed: the file in the config source is empty, and monitor profiles are machine specific — they belong in ~/.config/kanshi/config. A missing config file is what kanshi treats as an error (failed to parse config file), which with Restart=on-failure would spin, so the service starts only once that file exists.

Programs that read only from $HOME and have no system-wide fallback get a copy seeded through systemd.user.tmpfiles with the C directive, which never overwrites later edits.

Test VM

hosts/testvm targets a QEMU guest. The launcher script lives outside this repo at ~/VMs/moonix-vm.sh.

It uses the same layout module as real hardware, pointed at /dev/vda — the launcher attaches the qcow2 as if=virtio and there is no by-id path for it. LUKS stays enabled and the swapfile shrinks to 2G. Installing the guest is therefore also the test for modules/disk.nix.

Recreating the disk from scratch:

qemu-img create -f qcow2 ~/VMs/moonix.qcow2 60G
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd ~/VMs/moonix-vars.fd
~/VMs/moonix-vm.sh ~/Downloads/nixos-graphical-*.iso

Then run the disko-install command from above inside the live session, with #testvm and --disk main /dev/vda.

sudo asks for a password here like on every other host. SSH accepts one: services.openssh.settings.PasswordAuthentication is at its NixOS default of true, and the account's password is the initialPassword from the host file, which is in this repository. The guest is reachable on the launcher's forward on 127.0.0.1:2222 with it. Anything a test needs beyond that belongs at runtime — a drop-in under /run/systemd/system/, a key in ~/.ssh/authorized_keys — so it does not end up in version control.

The guest needs a 3D capable virtio GPU: niri's TTY backend fails every buffer import with Error::DeviceMissing otherwise, and software rendering is not a workaround. On the current host that path has twice triggered an amdgpu hard recovery, killing the QEMU process — shut the VM down when not testing.

S
Description
No description provided
Readme
360 KiB
Languages
Nix 100%