Files
moonix/modules/disk.nix
T
nevaforget 1ec12da9ff Declare the disk layout with disko
Installing used to mean six manual steps through Calamares, one of which
copied hardware-configuration.nix into the repo — a step that fails
silently unless the file is also git added, since Nix only sees tracked
files. It also did not scale past one machine.

modules/disk.nix defines moonarch.disk.{enable,device,encrypt,espSize,
swapSize} and builds disko.devices from it, so another machine is a
device path rather than a new layout. Installing is a single
disko-install run from the ISO.

hardware-configuration.nix is no longer needed: nvme, ahci, sd_mod and
the USB HID modules are in the initrd via
boot.initrd.includeDefaultModules, and nixpkgs.hostPlatform comes from
nixosSystem. The testvm copy described a qcow2 that no longer exists and
is deleted; that host now uses the module against /dev/vda.

The user password no longer arrives with the installer, so it is set
once via nixos-enter after installing instead of committing a hash.
2026-08-11 09:50:55 +02:00

128 lines
4.0 KiB
Nix

# ABOUTME: Declarative disk layout for every host: GPT, optional LUKS2, btrfs.
# ABOUTME: A host sets moonarch.disk.device; the layout itself is shared.
{ config, lib, ... }:
let
cfg = config.moonarch.disk;
mountOptions = [ "compress=zstd" "noatime" ];
subvolumes = {
"/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.swapSize != "") {
# No compression or noatime here: disko creates the swapfile inside this
# subvolume, and btrfs requires it to be nodatacow, which it sets itself.
"/swap" = {
mountpoint = "/.swapvol";
swap.swapfile.size = cfg.swapSize;
};
};
btrfs = {
type = "btrfs";
# Overwrite an existing signature instead of asking. The device is erased
# by this point either way.
extraArgs = [ "-f" ];
inherit subvolumes;
};
# 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 = {
type = "luks";
name = "crypted";
# 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;
};
in
{
options.moonarch.disk = {
enable = lib.mkEnableOption "the declarative disk layout";
device = lib.mkOption {
type = lib.types.str;
example = "/dev/disk/by-id/nvme-eui.0123456789abcdef";
description = ''
Whole disk to partition, as a /dev/disk/by-id/ path — that one survives
enumeration changes, unlike /dev/nvme0n1.
This value decides which disk gets erased. Check it against lsblk on the
target machine before installing.
Only the partitioning reads it. The generated fileSystems entries go
through /dev/disk/by-partlabel/, so a booted system does not depend on
the path staying the same.
'';
};
encrypt = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Put the filesystem inside a LUKS2 container. The layout is otherwise
identical, so a machine that does not need encryption — a throwaway VM —
gets the same subvolumes.
'';
};
espSize = lib.mkOption {
type = lib.types.str;
default = "1G";
description = ''
Size of the EFI system partition. Every boot entry keeps its kernel and
initrd here, so this scales with the number of generations kept.
'';
};
swapSize = lib.mkOption {
type = lib.types.str;
default = "8G";
example = "";
description = ''
Size of the swapfile in the /.swapvol subvolume. Empty string creates no
swap at all.
Sized as a safety valve, not as a replacement for RAM. Hibernation needs
at least as much swap as RAM plus a resume_offset kernel parameter, which
is not set up here — see DECISIONS.md.
'';
};
};
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" ];
};
};
# 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;
};
};
};
};
};
}