From 4535616799290e09e3cf4fa5c9447d1a52c39ba5 Mon Sep 17 00:00:00 2001 From: ahmza Date: Wed, 5 Aug 2026 22:06:01 +0100 Subject: [PATCH] move hardware config in-repo per-host and mount the data drives importing /etc/nixos/hardware-configuration.nix by absolute path from the shared core module had two problems: it made the flake impure (every eval needed --impure, since the file lives outside the store) and both hosts resolved to whichever machine's file was on disk, so evaluating t480 on shadow silently baked in shadow's disk uuids. shadow now carries its own generated scan at modules/hosts/shadow/_hardware.nix. the leading underscore keeps import-tree from picking it up as a flake-parts module - its filter drops any path containing "/_" - so it is imported explicitly from the host's default.nix. it is generated with --no-filesystems and all of shadow's filesystems are declared by hand in disks.nix instead. that split is what makes regeneration safe: nixos-generate-config emits fileSystems for whatever is mounted at the time, which would otherwise conflict with the entries below every time the data drives happened to be mounted. `nix run .#sync-hardware` refreshes the file. DUMP/GAMES/BAK are mounted by label at /mnt/{dump,games,bak} with noauto + x-systemd.automount, so the two 2.7T spinners stay parked until something touches the path and spin back down after 10 minutes idle. nofail keeps a missing disk from dropping the boot into emergency mode. / and /boot are unchanged - still the same uuids, verified identical to the previous evaluation. t480 keeps the absolute import for now with a TODO; its scan can only be generated on the machine itself. --- modules/apps/sync-hardware.nix | 84 ++++++++++++++++++++++++++++++ modules/hosts/shadow/_hardware.nix | 27 ++++++++++ modules/hosts/shadow/default.nix | 4 ++ modules/hosts/shadow/disks.nix | 56 ++++++++++++++++++++ modules/hosts/t480/default.nix | 11 ++++ modules/system/core/default.nix | 10 ++-- 6 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 modules/apps/sync-hardware.nix create mode 100644 modules/hosts/shadow/_hardware.nix create mode 100644 modules/hosts/shadow/disks.nix diff --git a/modules/apps/sync-hardware.nix b/modules/apps/sync-hardware.nix new file mode 100644 index 0000000..4ca3bf7 --- /dev/null +++ b/modules/apps/sync-hardware.nix @@ -0,0 +1,84 @@ +{...}: { + # `nix run .#sync-hardware` + # + # Refreshes the calling host's generated hardware module in-repo, replacing the + # old habit of importing /etc/nixos/hardware-configuration.nix by absolute path. + # + # Why bother: that absolute import left the flake impure (every eval needed + # --impure, since the file lives outside the store) and — worse — meant *both* + # hosts resolved to whichever machine's hardware file was on disk at build + # time. Building t480 from shadow silently baked in shadow's disk UUIDs. + # + # Run this on a host after changing its hardware, then commit the result. + perSystem = {pkgs, ...}: let + script = pkgs.writeShellApplication { + name = "sync-hardware"; + runtimeInputs = [pkgs.nixos-install-tools pkgs.git pkgs.coreutils pkgs.nettools]; + text = '' + set -euo pipefail + + host="''${1:-$(hostname)}" + + root=$(git rev-parse --show-toplevel 2>/dev/null || true) + if [ -z "$root" ]; then + echo "sync-hardware: run this from inside the config repo." >&2 + exit 1 + fi + + dir="$root/modules/hosts/$host" + if [ ! -d "$dir" ]; then + echo "sync-hardware: no host directory at $dir" >&2 + echo " known hosts: $(cd "$root/modules/hosts" && echo *)" >&2 + echo " pass an explicit name with: nix run .#sync-hardware -- " >&2 + exit 1 + fi + + out="$dir/_hardware.nix" + + # --no-filesystems is load-bearing: fileSystems/swapDevices are declared + # by hand in the host's disks.nix. Without the flag this would emit + # entries for whatever is mounted right now, duplicating (and conflicting + # with) those definitions. + header=$(cat <<'EOF' + # GENERATED FILE — do not edit by hand. + # + # Regenerate on the host itself with: nix run .#sync-hardware + # + # Produced by `nixos-generate-config --show-hardware-config --no-filesystems`. + # The --no-filesystems flag is deliberate: fileSystems and swapDevices are + # declared by hand in ./disks.nix, so regenerating this file can never clobber + # them or emit duplicates for whatever happened to be mounted at the time. + # + # Leading underscore keeps import-tree from auto-importing this as a + # flake-parts module (its filter drops any path containing "/_"); it is + # imported explicitly from ./default.nix instead. + EOF + ) + + tmp=$(mktemp) + trap 'rm -f "$tmp"' EXIT + + # Strip the upstream 3-line "Do not modify" banner; ours replaces it. + { + printf '%s\n' "$header" | sed 's/^ //' + nixos-generate-config --show-hardware-config --no-filesystems | tail -n +4 + } > "$tmp" + + if [ -f "$out" ] && cmp -s "$tmp" "$out"; then + echo "sync-hardware: $out already up to date." + exit 0 + fi + + cp "$tmp" "$out" + echo "sync-hardware: wrote $out" + echo " review with: git diff -- $out" + ''; + }; + in { + apps.sync-hardware = { + type = "app"; + program = "${script}/bin/sync-hardware"; + meta.description = "Regenerate modules/hosts//_hardware.nix from the running machine"; + }; + }; +} diff --git a/modules/hosts/shadow/_hardware.nix b/modules/hosts/shadow/_hardware.nix new file mode 100644 index 0000000..591ca04 --- /dev/null +++ b/modules/hosts/shadow/_hardware.nix @@ -0,0 +1,27 @@ +# GENERATED FILE — do not edit by hand. +# +# Regenerate on the host itself with: nix run .#sync-hardware +# +# Produced by `nixos-generate-config --show-hardware-config --no-filesystems`. +# The --no-filesystems flag is deliberate: fileSystems and swapDevices are +# declared by hand in ./disks.nix, so regenerating this file can never clobber +# them or emit duplicates for whatever happened to be mounted at the time. +# +# Leading underscore keeps import-tree from auto-importing this as a +# flake-parts module (its filter drops any path containing "/_"); it is +# imported explicitly from ./default.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usbhid" "uas" "sd_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-amd" ]; + boot.extraModulePackages = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/modules/hosts/shadow/default.nix b/modules/hosts/shadow/default.nix index 3195431..e67f396 100644 --- a/modules/hosts/shadow/default.nix +++ b/modules/hosts/shadow/default.nix @@ -5,8 +5,12 @@ }: { flake.nixosConfigurations.shadow = inputs.nixpkgs.lib.nixosSystem { modules = with self.nixosModules; [ + # Generated hardware scan, in-repo so the flake stays pure. + # Refresh with `nix run .#sync-hardware` (run on shadow itself). + ./_hardware.nix desktop shadowConfiguration + shadowDisks cpuAmd gpuNvidia development diff --git a/modules/hosts/shadow/disks.nix b/modules/hosts/shadow/disks.nix new file mode 100644 index 0000000..cdd2207 --- /dev/null +++ b/modules/hosts/shadow/disks.nix @@ -0,0 +1,56 @@ +{...}: { + # Every filesystem on shadow lives here, hand-maintained. + # + # hardware.nix is generated with `--no-filesystems` precisely so it never + # emits fileSystems/swapDevices entries of its own. That keeps regeneration + # (`nix run .#sync-hardware`) safe: re-running it can never clobber these + # definitions, nor produce duplicates for whichever data drives happened to + # be mounted at the time it ran. + flake.nixosModules.shadowDisks = {...}: let + # Secondary data drive, addressed by filesystem label. + # + # noauto + x-systemd.automount makes systemd generate an .automount unit and + # mount on first access to the path rather than at boot — the two 2.7T + # spinners stay parked until something actually touches them. idle-timeout + # unmounts them again once quiet so they can spin back down. + # + # nofail matters even with automount: it stops a missing or dying disk from + # becoming a failed dependency that drops the boot into emergency mode. + # + # On-disk ownership is already ahmza mode 755 on all three, so no uid mapping + # is needed — ext4 stores ownership in the filesystem and ignores uid=/gid=. + dataDrive = label: { + device = "/dev/disk/by-label/${label}"; + fsType = "ext4"; + options = [ + "nofail" + "noauto" + "x-systemd.automount" + "x-systemd.idle-timeout=600" + "x-systemd.device-timeout=10" + ]; + }; + in { + fileSystems = { + # Boot essentials, by UUID. UUIDs rather than labels here on purpose: + # these two must resolve unambiguously, and a label is only as unique as + # the last disk someone plugged in. + "/" = { + device = "/dev/disk/by-uuid/deff63fc-1738-4cfc-a79c-06e58c78471a"; + fsType = "ext4"; + }; + + "/boot" = { + device = "/dev/disk/by-uuid/5873-AD05"; + fsType = "vfat"; + options = ["fmask=0077" "dmask=0077"]; + }; + + "/mnt/dump" = dataDrive "DUMP"; + "/mnt/games" = dataDrive "GAMES"; + "/mnt/bak" = dataDrive "BAK"; + }; + + swapDevices = []; + }; +} diff --git a/modules/hosts/t480/default.nix b/modules/hosts/t480/default.nix index 4a4e954..1539b3e 100644 --- a/modules/hosts/t480/default.nix +++ b/modules/hosts/t480/default.nix @@ -5,6 +5,17 @@ }: { flake.nixosConfigurations.t480 = inputs.nixpkgs.lib.nixosSystem { modules = with self.nixosModules; [ + # TODO: still importing by absolute path, so evaluating t480 requires + # --impure and picks up the hardware file of whatever machine you build + # from (on shadow, that means shadow's disk UUIDs — wrong but harmless + # until you actually deploy). Fix by running, ON THE T480 ITSELF: + # + # nix run .#sync-hardware + # + # then create modules/hosts/t480/disks.nix (mirroring shadow's) and swap + # this line for `./_hardware.nix` + `t480Disks`. Cannot be done remotely: + # the hardware scan has to happen on the target machine. + /etc/nixos/hardware-configuration.nix desktop t480Configuration # CPU microcode + integrated Intel GPU. The T480 also has an NVIDIA MX150; diff --git a/modules/system/core/default.nix b/modules/system/core/default.nix index e0475f6..c89f2d9 100644 --- a/modules/system/core/default.nix +++ b/modules/system/core/default.nix @@ -18,11 +18,11 @@ nix-ld ]; in { - imports = - [ - /etc/nixos/hardware-configuration.nix - ] - ++ modules; + # Hardware/disk config is deliberately NOT imported here. It is per-host and + # lives in modules/hosts//{_hardware.nix,disks.nix}, wired up by that + # host's default.nix. Importing it from this shared module used to mean both + # hosts resolved to whichever machine's /etc/nixos file existed at build time. + imports = modules; services = { openssh.enable = true; avahi.enable = true;