diff --git a/.claude/tasks/0002-neogaia-disk-and-boot.md b/.claude/tasks/0002-neogaia-disk-and-boot.md new file mode 100644 index 0000000..4ab55b3 --- /dev/null +++ b/.claude/tasks/0002-neogaia-disk-and-boot.md @@ -0,0 +1,31 @@ +--- +spec: laptop-mvi +blocked-by: 0001-skeleton-and-building-host +--- + +## What to build + +Declare the `neogaia` laptop's disk with `disko` and make it unlock and boot on real hardware: a LUKS-encrypted btrfs volume with subvolumes plus zram swap, on an EFI system partition using systemd-boot, with the LUKS passphrase prompted at boot via the initrd. + +The layout must build from the same tree as the `Host` toplevel (so the whole-`Host` build exercises it), and must be expressed as a per-`Host` disk concern so other machines can declare their own layouts later. + +## Acceptance criteria + +- [x] `neogaia` declares a `disko` layout: LUKS-encrypted btrfs with subvolumes plus zram swap on an EFI system partition. +- [x] systemd-boot is the bootloader; the initrd prompts for the LUKS passphrase so a normal boot unlocks the encrypted disk. +- [x] The `disko` layout builds as part of the `neogaia` toplevel build (no separate invocation needed to catch layout errors). +- [x] The disk layout is a per-`Host` concern, expressible differently for future `Host`s without restructuring the `Skeleton`. + +## Implementation Notes + +- **Layout.** One GPT disk at `/dev/nvme0n1`: a 512M EF00 ESP (vfat, `umask=0077`) mounted at `/boot`, and a 100%-fill LUKS partition (`cryptroot`, `allowDiscards`) holding a btrfs filesystem with three subvolumes — `@root` → `/`, `@home` → `/home`, `@nix` → `/nix` — each mounted `compress=zstd,noatime`. + There is deliberately no on-disk swap partition; swap is RAM-backed zram. +- **Skeleton vs. per-Host split.** The disko *module* (`inputs.disko.nixosModules.disko`) is wired into the host-builder in `lib/default.nix`, so every `Host` can interpret a `disko.devices` declaration; the *layout itself* lives in `hosts/neogaia/disk.nix`. + A future `Host` declares a different layout, or none at all (an undeclared `disko.devices` is a no-op), so servers that preserve an existing pool by import need no `Skeleton` change. +- **disko input follows nixpkgs.** Unlike chaotic (which must not), disko follows our `nixpkgs` so it builds against the same base. +- **Boot unlock.** disko's `type = "luks"` (no key file) generates `boot.initrd.luks.devices.cryptroot`, so the classic initrd prompts for the passphrase on a normal boot; the `nvme` initrd module was already present in `hardware-configuration.nix`. +- **zram enabled directly, not yet a Module.** Criterion 1 requires "plus zram swap," so `zramSwap.enable = true` is set on the `Host` now. + Task 0003 owns the reusable zram toggle `Module` and will lift this line into it; the placeholder `fileSystems`/bootloader stubs from task 0001 are removed here since disko now derives `fileSystems`. +- **Verification.** `nix flake check` (the `checks.x86_64-linux.neogaia` toplevel) builds green. + Confirmed via `nix eval`: disko-derived `fileSystems` = `/`,`/home`,`/nix` on btrfs `/dev/mapper/cryptroot` + `/boot` on the ESP; `boot.initrd.luks.devices` = `["cryptroot"]`; `systemd-boot.enable` and `zramSwap.enable` both true; `swapDevices` empty. + The genuine end-to-end confirmation is the manual `disko-install` reimage, which is irreversible by nature and not automated. diff --git a/flake.lock b/flake.lock index 088b99a..c65b3e1 100644 --- a/flake.lock +++ b/flake.lock @@ -21,6 +21,26 @@ "type": "github" } }, + "disko": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781152676, + "narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=", + "owner": "nix-community", + "repo": "disko", + "rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, "flake-schemas": { "locked": { "lastModified": 1780327564, @@ -143,6 +163,7 @@ "root": { "inputs": { "chaotic": "chaotic", + "disko": "disko", "home-manager": "home-manager_2", "nixpkgs": "nixpkgs_2", "nixpkgs-stable": "nixpkgs-stable", diff --git a/flake.nix b/flake.nix index bbad6ea..e4a224e 100644 --- a/flake.nix +++ b/flake.nix @@ -16,6 +16,13 @@ inputs.nixpkgs.follows = "nixpkgs"; }; + # Declarative disk partitioning. Each Host declares its own layout; a Host + # that preserves an existing pool simply declares none. + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + # CachyOS kernel + binary cache. Deliberately NOT following our nixpkgs, so the # chaotic cache stays usable and the kernel is fetched rather than compiled. chaotic.url = "github:chaotic-cx/nyx/nyxpkgs-unstable"; diff --git a/hosts/neogaia/default.nix b/hosts/neogaia/default.nix index 4102e3f..e4f6858 100644 --- a/hosts/neogaia/default.nix +++ b/hosts/neogaia/default.nix @@ -1,23 +1,23 @@ { ... }: # neogaia — Dell XPS 13 9380 laptop. # -# The filesystems and hardware profile below are placeholder values, not the -# machine's real encrypted layout. +# The disk layout lives in ./disk.nix (disko); the resulting `fileSystems` are +# derived from it, so none are declared by hand here. { - imports = [ ./hardware-configuration.nix ]; + imports = [ + ./hardware-configuration.nix + ./disk.nix + ]; system.stateVersion = "26.05"; + # systemd-boot on the EFI system partition disko creates. The initrd prompts + # for the LUKS passphrase (disko wires up boot.initrd.luks.devices), so a + # normal boot unlocks the encrypted root. boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true; - # Placeholder label-based filesystems. - fileSystems."/" = { - device = "/dev/disk/by-label/nixos"; - fsType = "ext4"; - }; - fileSystems."/boot" = { - device = "/dev/disk/by-label/BOOT"; - fsType = "vfat"; - }; + # Swap is RAM-backed zram rather than an on-disk partition. Task 0003 lifts + # this into the zram toggle Module; enabled directly here for now. + zramSwap.enable = true; } diff --git a/hosts/neogaia/disk.nix b/hosts/neogaia/disk.nix new file mode 100644 index 0000000..024b062 --- /dev/null +++ b/hosts/neogaia/disk.nix @@ -0,0 +1,66 @@ +{ ... }: +# neogaia's disk layout, declared with disko and interpreted by the disko module +# the host-builder wires in. This is a per-Host concern: another Host declares a +# different `disko.devices` (or none, preserving an existing pool by importing it). +# +# One NVMe disk, GPT: an EFI system partition for systemd-boot, and a LUKS +# container holding a btrfs filesystem with subvolumes. Swap is zram (RAM-backed), +# so there is deliberately no on-disk swap partition. disko derives the matching +# `fileSystems.*` and `boot.initrd.luks.devices.*` from this, so a normal boot +# prompts for the passphrase in the initrd and unlocks the encrypted root. +{ + disko.devices.disk.main = { + type = "disk"; + device = "/dev/nvme0n1"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "cryptroot"; + settings.allowDiscards = true; + content = { + type = "btrfs"; + extraArgs = [ "-f" ]; + subvolumes = { + "@root" = { + mountpoint = "/"; + mountOptions = [ + "compress=zstd" + "noatime" + ]; + }; + "@home" = { + mountpoint = "/home"; + mountOptions = [ + "compress=zstd" + "noatime" + ]; + }; + "@nix" = { + mountpoint = "/nix"; + mountOptions = [ + "compress=zstd" + "noatime" + ]; + }; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/lib/default.nix b/lib/default.nix index 2a2de64..525c2be 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -54,6 +54,7 @@ let ++ [ inputs.home-manager.nixosModules.home-manager inputs.chaotic.nixosModules.default + inputs.disko.nixosModules.disko (self + "/system") (self + "/hosts/${hostName}") { networking.hostName = hostName; }