Compare commits
2
Commits
656d1c70bb
...
14d16ee8ec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14d16ee8ec | ||
|
|
205e3eb60f |
+110
-12
@@ -1,8 +1,106 @@
|
||||
# Decisions
|
||||
|
||||
## 2026-08-20 – The checkout on a machine lives in `~/flakes/moonix`
|
||||
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
- **Why**: `disko-install` leaves no checkout behind, and the README documented
|
||||
one that was invented rather than derived: a clone in the home root plus a
|
||||
symlink, justified with reasons that did not hold. The configuration has to be
|
||||
editable and committable on the machine it describes, with the account's
|
||||
identity on the commits and the account's key on the push.
|
||||
- **Tradeoffs**: `/etc/nixos` is `nixos-rebuild`'s default path, but a repository
|
||||
there belongs to root: commits would be root's and pushing would need root's
|
||||
key. Handing the directory to the account with `install -d -o` fixes that by
|
||||
making a system directory user-writable, which is a workaround rather than a
|
||||
layout. `/opt/moonix` would mirror `moonarch`, which archinstall clones to
|
||||
`/opt/moonarch`, but that copy is root-owned and never committed from —
|
||||
these files are. `~/Repos` is the scratch area for development checkouts on
|
||||
the workstation, not a permanent location. `~/.config/flakes` is
|
||||
configuration space for programs reading it on this machine; the repository is
|
||||
not read as configuration, it is the source a system is built from.
|
||||
- **How**: The checkout is `~/flakes/moonix`, owned by the account, and
|
||||
`/etc/nixos/flake.nix` is a symlink to its `flake.nix`. `man 8 nixos-rebuild`
|
||||
documents exactly this: the file "may be a symlink to a flake.nix in an actual
|
||||
flake; thus /etc/nixos need not be a flake", and `--flake` "defaults to the
|
||||
directory containing the target of the symlink". `sudo nixos-rebuild switch`
|
||||
therefore needs no arguments and picks the host by hostname. The `flakes`
|
||||
directory is plural on purpose: `moonarch` leaving its `flake = false` state,
|
||||
dev flakes for the compiled projects and devShells would live beside it. A
|
||||
machine that is never edited locally keeps no checkout and builds from the
|
||||
Gitea reference instead, which is what `disko-install` did.
|
||||
|
||||
## 2026-08-20 – Desktop host, /home on a second disk
|
||||
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
- **Why**: The desktop has a 512G system disk and a 1T disk, and `/home`
|
||||
belongs on the large one. `modules/disk.nix` only knew a single disk, so the
|
||||
layout had nowhere to put the second.
|
||||
- **Tradeoffs**: A single btrfs spanning both disks would be one filesystem and
|
||||
one LUKS container, but it ties the two together: losing either loses the
|
||||
pool, and the system disk stops being restorable on its own. Mounting the 1T
|
||||
somewhere under `/mnt` and leaving `/home` small keeps the module untouched,
|
||||
at the price of configs and data living apart and snapper needing two configs
|
||||
with different meanings. A second disk carrying `/home` as its own btrfs
|
||||
keeps both disks independent and leaves the existing subvolume names alone.
|
||||
- **How**: `moonarch.disk.home.enable` adds a second GPT with one partition,
|
||||
wrapped in LUKS `crypted-home` when `encrypt` is set, holding the `/home`
|
||||
subvolume. The main disk drops `/home` in that case, so the subvolume exists
|
||||
exactly once. No second ESP: the firmware boots from the main disk. Both
|
||||
disks want the same passphrase — systemd's initrd caches the first one and
|
||||
retries it on the second container, so the boot prompts once even though
|
||||
formatting asks four times. `hosts/desktop` declares deliberately invalid
|
||||
device paths, because `disko-install --disk main|home` overrides them and a
|
||||
forgotten flag should fail rather than erase the wrong disk. Without a
|
||||
`nixos-hardware` profile the host sets `enableRedistributableFirmware` and
|
||||
`hardware.cpu.amd.updateMicrocode` itself, and forces `services.fwupd` off —
|
||||
desktop mainboard vendors mostly do not publish to the LVFS.
|
||||
|
||||
## 2026-08-20 – ThinkPad: redistributable firmware and the fingerprint reader
|
||||
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
- **Why**: `hardware.enableRedistributableFirmware` was never set. The
|
||||
`nixos-hardware` profile does not set it either — `common/cpu/amd`
|
||||
reads it (`hardware.cpu.amd.updateMicrocode = lib.mkDefault
|
||||
config.hardware.enableRedistributableFirmware`), so with the option off the
|
||||
machine ran on the microcode in its BIOS and without the firmware the WLAN
|
||||
card and amdgpu load.
|
||||
- **Tradeoffs**: The firmware is redistributable but not free, so enabling it
|
||||
is a deliberate step rather than a default. Declining it means no microcode
|
||||
updates on a CPU whose errata are fixed exactly there.
|
||||
- **How**: `hardware.enableRedistributableFirmware = true` in the host file,
|
||||
which the profile's `mkDefault` then turns into a microcode update.
|
||||
`services.fprintd.enable` alongside it — the NixOS module wires `pam_fprintd`
|
||||
into the PAM stacks it owns; a finger is enrolled with `fprintd-enroll` after
|
||||
installing.
|
||||
|
||||
## 2026-08-19 – Initial password in the host file, account name as an option
|
||||
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
- **Why**: An install that leaves the account without a password produces a
|
||||
system whose lock screen never opens — swaylock authenticates through
|
||||
pam_unix. The documented remedy, `nixos-enter --root /mnt -c passwd`, is the
|
||||
path that fails after `disko-install` with `chmod: changing permission of
|
||||
'/mnt/dev': Operation not permitted` (disko issue #638, closed as not
|
||||
planned). Alongside that, the host files carried a real name in five places.
|
||||
- **Tradeoffs**: `initialPassword` puts a plaintext password in the repository.
|
||||
Weighed against `hashedPasswordFile`, which keeps the secret out of the repo
|
||||
but needs the file on the target disk before the first boot — either through
|
||||
`disko-install --extra-files` or a `cp` into the still-mounted `/mnt`. Both
|
||||
are extra steps in exchange for protecting a value that is meant to be
|
||||
replaced on first login anyway. The 2026-08-11 decision to keep any password
|
||||
out of the repository is therefore reversed for the initial one.
|
||||
- **How**: `initialPassword = "moonarch"` in both hosts, `mutableUsers` left at
|
||||
its default so `passwd` overrides it permanently. New `modules/user.nix`
|
||||
declares `moonarch.user` without a default, because the name belongs to the
|
||||
machine; `users.users`, the autologin `initial_session.user` and snapper's
|
||||
`ALLOW_USERS` all read it, so the login name exists once per host instead of
|
||||
five times. README loses the `nixos-enter` step and the stale "Known gaps"
|
||||
section — `Super+C` reaches the Quickshell clipboard popout, which is wired up
|
||||
and whose `cliphist` backend is installed.
|
||||
|
||||
## 2026-08-14 – moonset and moonlock dropped; the power menu is a Quickshell popout
|
||||
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
- **Why**: The power menu became `PowerPopout.qml` in the moonarch Quickshell
|
||||
config (see `moonarch/DECISIONS.md`, same date), and moonset was the last thing
|
||||
invoking moonlock. Both packages therefore leave this flake with nothing to
|
||||
@@ -21,7 +119,7 @@
|
||||
|
||||
## 2026-08-14 – regreet and swaylock from nixpkgs, autologin after LUKS
|
||||
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
- **Why**: moongreet and moonlock were dropped as self-maintained projects; the
|
||||
reasoning is in `moonarch/DECISIONS.md` for the same date. Here the change is
|
||||
mostly subtraction: two flake inputs, two build recipes and a generated config
|
||||
@@ -69,7 +167,7 @@
|
||||
|
||||
## 2026-08-11 – Disk layout via disko after all, as a shared module
|
||||
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
- **Why**: The Calamares route decided below turned out to be six manual steps,
|
||||
one of which — copying `hardware-configuration.nix` into the repo — fails
|
||||
silently unless the file is also `git add`ed, because Nix only sees tracked
|
||||
@@ -91,12 +189,12 @@
|
||||
generated file was deleted, having described the UUIDs of a qcow2 that no
|
||||
longer exists.
|
||||
- **Consequence**: The user password no longer arrives with the installer.
|
||||
`users.users.dkressler` declares none, so it is set once via `nixos-enter`
|
||||
`users.users.<name>` declares none, so it is set once via `nixos-enter`
|
||||
after installing rather than committing a hash to the repository.
|
||||
|
||||
## 2026-08-11 – Swapfile without hibernation
|
||||
|
||||
- **Who**: ClaudeCode, on Dominik's request for a recommendation
|
||||
- **Who**: ClaudeCode, on the maintainer's request for a recommendation
|
||||
- **Why**: The layout module needs a swap default.
|
||||
- **Tradeoffs**: Hibernation needs swap at least the size of RAM plus a
|
||||
`resume_offset` kernel parameter. On btrfs that offset changes whenever the
|
||||
@@ -110,7 +208,7 @@
|
||||
|
||||
## 2026-08-07 – Separate repo instead of Nix files in the application repos
|
||||
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
- **Why**: The Nix build recipes had to live somewhere without disturbing the
|
||||
existing repos.
|
||||
- **Tradeoffs**: Files inside each project repo would keep version, code and
|
||||
@@ -122,7 +220,7 @@
|
||||
|
||||
## 2026-08-07 – Configs deployed as files, not translated to Nix options
|
||||
|
||||
- **Who**: Dominik
|
||||
- **Who**: Maintainer
|
||||
- **Why**: One set of config files should stay valid on NixOS and Arch alike.
|
||||
- **Tradeoffs**: Native Nix options (`programs.niri.settings`) are idiomatic and
|
||||
type checked, but every config would exist twice and drift on each change.
|
||||
@@ -131,7 +229,7 @@
|
||||
|
||||
## 2026-08-07 – No Home Manager
|
||||
|
||||
- **Who**: Dominik
|
||||
- **Who**: Maintainer
|
||||
- **Why**: Initially proposed for the three home-directory tasks in
|
||||
`post-install.sh` (GTK4 symlinks, `gsettings`, seeding the stasis config).
|
||||
- **Tradeoffs**: None in favour, as it turned out. Every one of those tasks is
|
||||
@@ -146,7 +244,7 @@
|
||||
|
||||
## 2026-08-07 – FHS paths fixed in the source, not bridged with symlinks
|
||||
|
||||
- **Who**: Dominik
|
||||
- **Who**: Maintainer
|
||||
- **Why**: moonlock, moongreet and moonset invoked `systemctl`, `loginctl` and
|
||||
`moonlock` through absolute `/usr/bin` paths, and moongreet scanned
|
||||
`/usr/share/{wayland-,x}sessions`. All of it fails on NixOS.
|
||||
@@ -163,7 +261,7 @@
|
||||
|
||||
## 2026-08-07 – waybar and swaync not installed
|
||||
|
||||
- **Who**: Dominik
|
||||
- **Who**: Maintainer
|
||||
- **Why**: Quickshell provides both the bar and the notifications. The config
|
||||
source still carries waybar and swaync, marked "kept as a reserve" — a note
|
||||
left over from an earlier migration.
|
||||
@@ -176,7 +274,7 @@
|
||||
|
||||
## 2026-08-07 – QEMU/KVM as the test host, with a caveat
|
||||
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
- **Why**: VirtualBox was the first choice, but its 3D acceleration is
|
||||
documented as broken with Wayland compositors.
|
||||
- **Tradeoffs**: QEMU needs no DKMS module on `linux-zen` and virtio-gpu
|
||||
@@ -190,7 +288,7 @@
|
||||
|
||||
## 2026-08-07 – Disk layout via Calamares, not disko
|
||||
|
||||
- **Who**: Dominik
|
||||
- **Who**: Maintainer
|
||||
- **Why**: Stage 3 targets bare metal, but no target machine exists yet —
|
||||
neither the number of disks nor their device paths are known.
|
||||
- **Tradeoffs**: `disko` would put partitioning and encryption under version
|
||||
|
||||
@@ -23,6 +23,7 @@ pkgs/ Build recipes
|
||||
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
|
||||
@@ -30,10 +31,11 @@ modules/
|
||||
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 all three modules and
|
||||
hands them the packages and the config source.
|
||||
`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
|
||||
|
||||
@@ -43,13 +45,15 @@ Build a single package:
|
||||
nix build .#stasis
|
||||
```
|
||||
|
||||
Build a whole system without activating it:
|
||||
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:
|
||||
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
|
||||
@@ -70,6 +74,7 @@ the NixOS ISO and run one command.
|
||||
```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
|
||||
```
|
||||
@@ -77,20 +82,20 @@ the NixOS ISO and run one command.
|
||||
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.
|
||||
|
||||
2. Set the user password. Nothing has created one yet. Autologin gets past the
|
||||
greeter without it, but swaylock authenticates through pam_unix and cannot
|
||||
unlock a screen for an account that has no password:
|
||||
`--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.
|
||||
|
||||
```bash
|
||||
sudo nixos-enter --root /mnt -c 'passwd dkressler'
|
||||
```
|
||||
2. Reboot.
|
||||
|
||||
3. Reboot.
|
||||
|
||||
To skip step 2, put a `mkpasswd -m sha-512` hash into
|
||||
`users.users.dkressler.initialHashedPassword` — the commented-out line in
|
||||
`hosts/thinkpad/default.nix` shows where. That commits a password hash to the
|
||||
repository.
|
||||
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
|
||||
@@ -98,9 +103,11 @@ repository.
|
||||
flake, and the `nixos-hardware` profile for the T14 Gen 3 (AMD) covers
|
||||
firmware, microcode, power management and graphics quirks.
|
||||
|
||||
The generated `fileSystems` entries point at `/dev/disk/by-partlabel/`, so the
|
||||
booted system does not care whether the disk enumerates the same way next time.
|
||||
Only the partitioning step reads `moonarch.disk.device`.
|
||||
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
|
||||
|
||||
@@ -117,9 +124,32 @@ Only the partitioning step reads `moonarch.disk.device`.
|
||||
|
||||
`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.
|
||||
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, 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
|
||||
@@ -131,13 +161,79 @@ the host file — the profile is what provides it for the ThinkPad.
|
||||
|---|---|---|
|
||||
| ESP, `espSize` (1G) | `/boot` | vfat, `umask=0077` |
|
||||
| `root` | `/` | btrfs, `compress=zstd`, `noatime` |
|
||||
| `home` | `/home` | same |
|
||||
| `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:
|
||||
@@ -163,9 +259,11 @@ NixOS module owns the destination:
|
||||
| `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 is empty, kanshi rejects it and
|
||||
restarts in a loop. Monitor profiles are machine specific and belong in
|
||||
`~/.config/kanshi/config`; the service starts only when that file exists.
|
||||
`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
|
||||
@@ -192,18 +290,16 @@ cp /usr/share/edk2/x64/OVMF_VARS.4m.fd ~/VMs/moonix-vars.fd
|
||||
Then run the `disko-install` command from above inside the live session, with
|
||||
`#testvm` and `--disk main /dev/vda`.
|
||||
|
||||
It deliberately contains no testing shortcuts — no passwordless sudo, no SSH
|
||||
password authentication. Set those at runtime when needed, for example through
|
||||
a drop-in under `/run/systemd/system/`, so they do not end up in version
|
||||
control.
|
||||
`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.
|
||||
|
||||
## Known gaps
|
||||
|
||||
`Super+C` opens the clipboard history through a launcher that is not installed
|
||||
here, so the binding does nothing. `cliphist` records the clipboard, only the
|
||||
picker front-end is missing.
|
||||
|
||||
Generated
+4
-4
@@ -24,11 +24,11 @@
|
||||
"moonarch": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1786722426,
|
||||
"narHash": "sha256-zJyJC/7GWLj5DOblOVEsJ/Ki+bZBh+2Uws07pA6r4No=",
|
||||
"lastModified": 1787067355,
|
||||
"narHash": "sha256-2n1XcruV9FxUHOh05HdETpep6IIk1PNpcl58MjMcrhA=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "fe552fc7a5160597b9b73e5ec39b4fce4f63bcd1",
|
||||
"revCount": 162,
|
||||
"rev": "e21c046e3251317703e9dd2f2d4a76f3a5211329",
|
||||
"revCount": 166,
|
||||
"type": "git",
|
||||
"url": "https://gitea.moonarch.de/nevaforget/moonarch.git"
|
||||
},
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
nixosModules.moonarch = { ... }: {
|
||||
imports = [
|
||||
inputs.disko.nixosModules.disko
|
||||
./modules/user.nix
|
||||
./modules/disk.nix
|
||||
./modules/desktop.nix
|
||||
./modules/greetd.nix
|
||||
@@ -98,9 +99,16 @@
|
||||
];
|
||||
};
|
||||
|
||||
# Desktop: 512G system disk, 1T disk for /home.
|
||||
desktop = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
modules = [
|
||||
./hosts/desktop
|
||||
self.nixosModules.moonarch
|
||||
];
|
||||
};
|
||||
|
||||
# ThinkPad T14 Gen 3 (AMD Ryzen 7 PRO 6850U).
|
||||
# Needs hosts/thinkpad/hardware-configuration.nix from the installer
|
||||
# before it evaluates — see README.
|
||||
thinkpad = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
modules = [
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# ABOUTME: Host configuration for the desktop: 512G system disk, 1T disk for /home.
|
||||
# ABOUTME: Disk layout comes from modules/disk.nix, nothing is generated on the machine.
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
# Both device paths are placeholders. disko-install overrides them with
|
||||
# `--disk main <path>` and `--disk home <path>`, so the real paths are read
|
||||
# from lsblk on the machine and never travel into this repository. The values
|
||||
# here are deliberately invalid: without the flags the install fails instead
|
||||
# of erasing whatever happens to be first in the enumeration.
|
||||
moonarch.disk = {
|
||||
enable = true;
|
||||
device = "/dev/disk/by-id/SET-VIA-disko-install--disk-main";
|
||||
home = {
|
||||
enable = true;
|
||||
device = "/dev/disk/by-id/SET-VIA-disko-install--disk-home";
|
||||
};
|
||||
};
|
||||
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_zen;
|
||||
boot.kernelParams = [ "quiet" ];
|
||||
|
||||
networking.hostName = "desktop";
|
||||
networking.networkmanager.enable = true;
|
||||
|
||||
time.timeZone = "Europe/Berlin";
|
||||
i18n.defaultLocale = "de_DE.UTF-8";
|
||||
i18n.extraLocaleSettings = {
|
||||
LC_ADDRESS = "de_DE.UTF-8";
|
||||
LC_IDENTIFICATION = "de_DE.UTF-8";
|
||||
LC_MEASUREMENT = "de_DE.UTF-8";
|
||||
LC_MONETARY = "de_DE.UTF-8";
|
||||
LC_NAME = "de_DE.UTF-8";
|
||||
LC_NUMERIC = "de_DE.UTF-8";
|
||||
LC_PAPER = "de_DE.UTF-8";
|
||||
LC_TELEPHONE = "de_DE.UTF-8";
|
||||
LC_TIME = "de_DE.UTF-8";
|
||||
};
|
||||
console.keyMap = "de";
|
||||
services.xserver.xkb.layout = "de";
|
||||
|
||||
moonarch.user = "dkressler";
|
||||
|
||||
users.users.${config.moonarch.user} = {
|
||||
isNormalUser = true;
|
||||
extraGroups = [ "networkmanager" "wheel" "video" "input" "plugdev" "docker" ];
|
||||
shell = pkgs.zsh;
|
||||
# Autologin gets past the greeter without a password, but swaylock
|
||||
# authenticates through pam_unix and cannot unlock a screen for an account
|
||||
# that has none. `mutableUsers` is at its default, so this applies when the
|
||||
# account is created and `passwd` overrides it afterwards. Change it after
|
||||
# the first boot — this value is in the repository.
|
||||
initialPassword = "moonarch";
|
||||
};
|
||||
|
||||
# LUKS already asked for a passphrase at boot, so the greeter would only ask
|
||||
# for a second one. initial_session runs once per boot — the greeter comes
|
||||
# back on logout, which is also where a session gets switched. greetd's
|
||||
# restart option flips itself off when this is set.
|
||||
services.greetd.settings.initial_session = {
|
||||
command = "${pkgs.niri}/bin/niri-session";
|
||||
user = config.moonarch.user;
|
||||
};
|
||||
|
||||
# Snapshots of root and home. snap-pac has no counterpart here: NixOS keeps
|
||||
# its own generations, so pre/post package snapshots are redundant. /home is
|
||||
# its own btrfs on the second disk, which snapper does not care about — a
|
||||
# subvolume is a subvolume.
|
||||
services.snapper = {
|
||||
configs = {
|
||||
root = {
|
||||
SUBVOLUME = "/";
|
||||
ALLOW_USERS = [ config.moonarch.user ];
|
||||
TIMELINE_CREATE = true;
|
||||
TIMELINE_CLEANUP = true;
|
||||
};
|
||||
home = {
|
||||
SUBVOLUME = "/home";
|
||||
ALLOW_USERS = [ config.moonarch.user ];
|
||||
TIMELINE_CREATE = true;
|
||||
TIMELINE_CLEANUP = true;
|
||||
};
|
||||
};
|
||||
snapshotInterval = "hourly";
|
||||
cleanupInterval = "1d";
|
||||
};
|
||||
|
||||
# No nixos-hardware profile applies to a self-built desktop, so the firmware
|
||||
# this machine needs is enabled here instead.
|
||||
hardware.enableRedistributableFirmware = true;
|
||||
|
||||
# The nixos-hardware profile derives this from enableRedistributableFirmware
|
||||
# on the ThinkPad; here nothing does, so without this line the CPU runs on the
|
||||
# microcode in its BIOS.
|
||||
hardware.cpu.amd.updateMicrocode = true;
|
||||
|
||||
# The GPU is AMD, so amdgpu and Mesa cover it and there is nothing to declare:
|
||||
# the kernel module is in the default initrd and Mesa comes with the graphics
|
||||
# stack. An NVIDIA card would have needed videoDrivers and hardware.nvidia.
|
||||
|
||||
services.fstrim.enable = true;
|
||||
|
||||
# modules/services.nix turns fwupd on for every host. Lenovo publishes to the
|
||||
# LVFS, so it earns its place on the ThinkPad; desktop mainboard vendors
|
||||
# mostly do not, which leaves a daemon with nothing to update. mkForce because
|
||||
# the shared module sets it unconditionally.
|
||||
services.fwupd.enable = lib.mkForce false;
|
||||
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
# The release this machine is first installed with. It stays at this value
|
||||
# for the life of the installation and is never raised by an update.
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# ABOUTME: Host configuration for the QEMU/KVM test VM.
|
||||
# ABOUTME: Launcher script lives outside this repo at ~/VMs/moonix-vm.sh.
|
||||
|
||||
{ pkgs, modulesPath, ... }:
|
||||
{ config, pkgs, modulesPath, ... }:
|
||||
|
||||
{
|
||||
# virtio_blk and virtio_pci are not in boot.initrd.availableKernelModules by
|
||||
@@ -32,23 +32,25 @@
|
||||
i18n.defaultLocale = "de_DE.UTF-8";
|
||||
console.keyMap = "de";
|
||||
|
||||
users.users.kresdo = {
|
||||
moonarch.user = "moon";
|
||||
|
||||
users.users.${config.moonarch.user} = {
|
||||
isNormalUser = true;
|
||||
description = "Dominik Kressler";
|
||||
extraGroups = [ "networkmanager" "wheel" "video" "input" ];
|
||||
shell = pkgs.zsh;
|
||||
# Throwaway password for a throwaway guest. `mutableUsers` is at its
|
||||
# default, so this applies when the account is created and `passwd`
|
||||
# overrides it afterwards.
|
||||
initialPassword = "moonarch";
|
||||
};
|
||||
|
||||
# LUKS already asked for a passphrase at boot, so the greeter would only ask
|
||||
# for a second one. initial_session runs once per boot — the greeter comes
|
||||
# back on logout, which is also where a session gets switched. greetd's
|
||||
# restart option flips itself off when this is set.
|
||||
#
|
||||
# The account still needs a password: swaylock authenticates through
|
||||
# pam_unix, and without a /etc/shadow entry the screen cannot be unlocked.
|
||||
services.greetd.settings.initial_session = {
|
||||
command = "${pkgs.niri}/bin/niri-session";
|
||||
user = "kresdo";
|
||||
user = config.moonarch.user;
|
||||
};
|
||||
|
||||
# Development access from the host via the QEMU port forward on 127.0.0.1:2222.
|
||||
|
||||
+22
-15
@@ -1,7 +1,7 @@
|
||||
# ABOUTME: Host configuration for the ThinkPad T14 Gen 3 (AMD).
|
||||
# ABOUTME: Disk layout comes from modules/disk.nix, nothing is generated on the machine.
|
||||
|
||||
{ pkgs, ... }:
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
# Partitioning, LUKS and the btrfs subvolumes. The device path is the only
|
||||
@@ -35,20 +35,18 @@
|
||||
console.keyMap = "de";
|
||||
services.xserver.xkb.layout = "de";
|
||||
|
||||
users.users.dkressler = {
|
||||
moonarch.user = "dkressler";
|
||||
|
||||
users.users.${config.moonarch.user} = {
|
||||
isNormalUser = true;
|
||||
description = "Dominik Kressler";
|
||||
extraGroups = [ "networkmanager" "wheel" "video" "input" "plugdev" "docker" ];
|
||||
shell = pkgs.zsh;
|
||||
# No password is declared here, so a freshly installed system has no
|
||||
# /etc/shadow entry for this account. Autologin gets past the greeter
|
||||
# without one, but swaylock then cannot unlock the screen, so the password
|
||||
# is still set once after installing, see README.
|
||||
#
|
||||
# To make it declarative instead, put the output of `mkpasswd -m sha-512`
|
||||
# in initialHashedPassword. That commits a hash to the repository, which is
|
||||
# offline attackable if the repository ever leaks:
|
||||
# initialHashedPassword = "$6$...";
|
||||
# Autologin gets past the greeter without a password, but swaylock
|
||||
# authenticates through pam_unix and cannot unlock a screen for an account
|
||||
# that has none. `mutableUsers` is at its default, so this applies when the
|
||||
# account is created and `passwd` overrides it afterwards. Change it after
|
||||
# the first boot — this value is in the repository.
|
||||
initialPassword = "moonarch";
|
||||
};
|
||||
|
||||
# LUKS already asked for a passphrase at boot, so the greeter would only ask
|
||||
@@ -57,7 +55,7 @@
|
||||
# restart option flips itself off when this is set.
|
||||
services.greetd.settings.initial_session = {
|
||||
command = "${pkgs.niri}/bin/niri-session";
|
||||
user = "dkressler";
|
||||
user = config.moonarch.user;
|
||||
};
|
||||
|
||||
# Snapshots of root and home. snap-pac has no counterpart here: NixOS keeps
|
||||
@@ -66,13 +64,13 @@
|
||||
configs = {
|
||||
root = {
|
||||
SUBVOLUME = "/";
|
||||
ALLOW_USERS = [ "dkressler" ];
|
||||
ALLOW_USERS = [ config.moonarch.user ];
|
||||
TIMELINE_CREATE = true;
|
||||
TIMELINE_CLEANUP = true;
|
||||
};
|
||||
home = {
|
||||
SUBVOLUME = "/home";
|
||||
ALLOW_USERS = [ "dkressler" ];
|
||||
ALLOW_USERS = [ config.moonarch.user ];
|
||||
TIMELINE_CREATE = true;
|
||||
TIMELINE_CLEANUP = true;
|
||||
};
|
||||
@@ -81,6 +79,15 @@
|
||||
cleanupInterval = "1d";
|
||||
};
|
||||
|
||||
# The nixos-hardware profile only sets hardware.cpu.amd.updateMicrocode as a
|
||||
# default derived from this option, so without it there is neither a microcode
|
||||
# update nor the redistributable firmware the WLAN card and amdgpu load.
|
||||
hardware.enableRedistributableFirmware = true;
|
||||
|
||||
# Fingerprint reader. The NixOS module wires pam_fprintd into the PAM stacks
|
||||
# it owns; enrol a finger with `fprintd-enroll` after installing.
|
||||
services.fprintd.enable = true;
|
||||
|
||||
services.fstrim.enable = true;
|
||||
services.fwupd.enable = true;
|
||||
|
||||
|
||||
+5
-4
@@ -52,10 +52,11 @@ in
|
||||
"xdg/quickshell/moonarch".source = "${xdg}/quickshell/moonarch";
|
||||
"xdg/foot/foot.ini".source = "${xdg}/foot/foot.ini";
|
||||
"xdg/fastfetch/config.jsonc".source = "${xdg}/fastfetch/config.jsonc";
|
||||
# defaults/xdg/kanshi/config is an empty placeholder — on Arch too. kanshi
|
||||
# rejects it ("failed to parse config file") and restarts in a loop, so it
|
||||
# is not deployed. Monitor profiles are machine specific and belong in
|
||||
# ~/.config/kanshi/config.
|
||||
# defaults/xdg/kanshi/config is an empty placeholder — on Arch too. Not
|
||||
# deployed: monitor profiles are machine specific and belong in
|
||||
# ~/.config/kanshi/config. An empty file parses fine; a missing one is what
|
||||
# kanshi reports as "failed to parse config file", which is why the unit in
|
||||
# modules/services.nix waits for the home config to exist.
|
||||
"xdg/Kvantum/kvantum.kvconfig".source = "${xdg}/Kvantum/kvantum.kvconfig";
|
||||
"xdg/qt6ct/qt6ct.conf".source = "${xdg}/qt6ct/qt6ct.conf";
|
||||
"xdg/gtk-3.0/settings.ini".source = "${xdg}/gtk-3.0/settings.ini";
|
||||
|
||||
+84
-29
@@ -8,12 +8,15 @@ let
|
||||
|
||||
mountOptions = [ "compress=zstd" "noatime" ];
|
||||
|
||||
subvolumes = {
|
||||
systemSubvolumes = {
|
||||
"/root" = { mountpoint = "/"; inherit mountOptions; };
|
||||
"/home" = { mountpoint = "/home"; inherit mountOptions; };
|
||||
# Separate so the store stays out of the snapper snapshots of /. It is
|
||||
# reproducible from the flake and would only inflate them.
|
||||
"/nix" = { mountpoint = "/nix"; inherit mountOptions; };
|
||||
} // lib.optionalAttrs (!cfg.home.enable) {
|
||||
# Only here when there is no second disk. With one, /home lives over there
|
||||
# and the main disk carries the system alone.
|
||||
"/home" = { mountpoint = "/home"; inherit mountOptions; };
|
||||
} // lib.optionalAttrs (cfg.swapSize != "") {
|
||||
# No compression or noatime here: disko creates the swapfile inside this
|
||||
# subvolume, and btrfs requires it to be nodatacow, which it sets itself.
|
||||
@@ -23,7 +26,11 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
btrfs = {
|
||||
homeSubvolumes = {
|
||||
"/home" = { mountpoint = "/home"; inherit mountOptions; };
|
||||
};
|
||||
|
||||
btrfs = subvolumes: {
|
||||
type = "btrfs";
|
||||
# Overwrite an existing signature instead of asking. The device is erased
|
||||
# by this point either way.
|
||||
@@ -34,14 +41,24 @@ let
|
||||
# Neither passwordFile nor settings.keyFile is set, which makes disko's
|
||||
# askPassword default to true: it prompts for the passphrase twice while
|
||||
# formatting and never puts it on a command line.
|
||||
luks = {
|
||||
#
|
||||
# With a second disk each container is asked for separately while formatting.
|
||||
# Give both the same passphrase: systemd stage 1 caches the first one and
|
||||
# tries it on the second container, so the boot prompts once. If it ever asks
|
||||
# twice, boot.initrd.systemd.enable = false falls back to the script initrd,
|
||||
# whose boot.initrd.luks.reusePassphrases does the same thing explicitly.
|
||||
luks = name: content: {
|
||||
type = "luks";
|
||||
name = "crypted";
|
||||
inherit name;
|
||||
# Lets TRIM reach the SSD through the container. The tradeoff is that the
|
||||
# pattern of used blocks becomes visible on the raw device.
|
||||
settings.allowDiscards = true;
|
||||
content = btrfs;
|
||||
inherit content;
|
||||
};
|
||||
|
||||
# Wraps a filesystem in LUKS unless the host opted out of encryption.
|
||||
maybeEncrypted = name: subvolumes:
|
||||
if cfg.encrypt then luks name (btrfs subvolumes) else btrfs subvolumes;
|
||||
in
|
||||
{
|
||||
options.moonarch.disk = {
|
||||
@@ -63,6 +80,26 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
home = {
|
||||
enable = lib.mkEnableOption "a second disk carrying /home";
|
||||
|
||||
device = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "/dev/disk/by-id/nvme-eui.fedcba9876543210";
|
||||
description = ''
|
||||
Second whole disk, erased and partitioned like the first one. It takes
|
||||
the /home subvolume, which then no longer exists on the main disk.
|
||||
|
||||
Encryption follows moonarch.disk.encrypt, so both disks are either
|
||||
encrypted or neither is. The container is named crypted-home to keep
|
||||
it apart from the system one.
|
||||
|
||||
disko-install addresses it as `--disk home <path>`, the same way the
|
||||
main disk is `--disk main <path>`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
encrypt = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
@@ -97,31 +134,49 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
disko.devices.disk.main = {
|
||||
type = "disk";
|
||||
device = cfg.device;
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
ESP = {
|
||||
size = cfg.espSize;
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "umask=0077" ];
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
disko.devices.disk.main = {
|
||||
type = "disk";
|
||||
device = cfg.device;
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
ESP = {
|
||||
size = cfg.espSize;
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "umask=0077" ];
|
||||
};
|
||||
};
|
||||
# Named the same whether or not it is encrypted, so the partlabel the
|
||||
# fileSystems entries point at does not depend on cfg.encrypt.
|
||||
root = {
|
||||
size = "100%";
|
||||
content = maybeEncrypted "crypted" systemSubvolumes;
|
||||
};
|
||||
};
|
||||
# Named the same whether or not it is encrypted, so the partlabel the
|
||||
# fileSystems entries point at does not depend on cfg.encrypt.
|
||||
root = {
|
||||
size = "100%";
|
||||
content = if cfg.encrypt then luks else btrfs;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
# Second disk. No ESP on it: the firmware boots from the main disk, and a
|
||||
# second one would only be another thing to keep in sync.
|
||||
(lib.mkIf cfg.home.enable {
|
||||
disko.devices.disk.home = {
|
||||
type = "disk";
|
||||
device = cfg.home.device;
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions.home = {
|
||||
size = "100%";
|
||||
content = maybeEncrypted "crypted-home" homeSubvolumes;
|
||||
};
|
||||
};
|
||||
};
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# ABOUTME: Declares the moonarch.user option; every host sets its own login name.
|
||||
# ABOUTME: Autologin, group membership and snapper read moonarch.user from here.
|
||||
|
||||
{ lib, ... }:
|
||||
|
||||
{
|
||||
options.moonarch.user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "moon";
|
||||
description = ''
|
||||
Login name of the primary account, and therefore the directory name under
|
||||
/home. Autologin, the group memberships and snapper's ALLOW_USERS all read
|
||||
this, so the name is declared once instead of once per reference.
|
||||
|
||||
There is no default: the name belongs to the machine, so every host sets
|
||||
it. Changing it on an installed system does not rename the home directory —
|
||||
that stays behind under the old name.
|
||||
'';
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user