hosts/desktop carried two SET-VIA-disko-install placeholders, so the file did not say which disks the machine has. The invalid paths only ever caught a missing --disk flag, never a wrong one, and paid for it by leaving the hardware undocumented. The system disk is the 512G XPG, /home goes on the 1T Crucial; disko-install --disk main|home still overrides both. The 1T currently holds an ext4 Data partition, which the install erases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
305 lines
12 KiB
Markdown
305 lines
12 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
nix build .#stasis
|
|
```
|
|
|
|
Build a whole system without activating it, which is also how a host is checked
|
|
for evaluation errors without touching anything:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```nix
|
|
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:
|
|
|
|
```nix
|
|
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:
|
|
|
|
```bash
|
|
--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, so the flags are what decides which disks are written —
|
|
compare them against `lsblk` and `ls -l /dev/disk/by-id/` first.
|
|
|
|
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:
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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 add`ed 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
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.
|
|
|