diff --git a/.claude/tasks/0001-skeleton-and-building-host.md b/.claude/tasks/0001-skeleton-and-building-host.md new file mode 100644 index 0000000..2a61383 --- /dev/null +++ b/.claude/tasks/0001-skeleton-and-building-host.md @@ -0,0 +1,37 @@ +--- +spec: laptop-mvi +--- + +## What to build + +Stand up the `Skeleton` and a minimal `neogaia` `Host` that evaluates and whose system toplevel builds — the walking skeleton every later slice extends and re-verifies against. + +The `Skeleton` is a hand-rolled flake (no flake-parts): `nixos-unstable` base channel, an `unstable overlay` exposing `nixpkgs-unstable` as `unstable.`, a `stable overlay` exposing `nixos-25.05` as `stable.`, and chaotic-nyx wired as an input with its overlay and binary cache. +The helper lib is trimmed to three pieces: the `Auto-loader` (recursively discovers and imports every `Module` under `modules/` and every `Host` under `hosts/` with no null-placeholder traversal hack), the host-builder, and the script-from-file helper. +`with lib.my` is not used — dependencies are `inherit`ed explicitly. +The `Enable convention` uses the stdlib enable-option helper; every `Module` is imported unconditionally and guards its body with `mkIf config.modules..enable`. +home-manager is sourced from `nix-community` (master, nixpkgs followed) and integrated as a NixOS module with global packages and user packages. +The `user` is an explicit option defaulting to `alexion` (no impure environment lookup), placed in `wheel`, driving the system user and the home-manager user in lockstep. +The `neogaia` `Host` carries only enough (placeholder `hardware-configuration.nix`, filesystems/bootloader stubs, `stateVersion`) to make `nixosConfigurations.neogaia.config.system.build.toplevel` evaluate and build; real disk/kernel/networking arrive in later slices. + +Design the per-`Host` layout so disk layout, kernel, and channel are all per-`Host` concerns from the start (story 20), so the desktop and servers extend this foundation without restructuring. + +## Acceptance criteria + +- [x] `nix flake check` succeeds and the flake exposes `nixosConfigurations.neogaia`. +- [x] `nixosConfigurations.neogaia.config.system.build.toplevel` builds. +- [x] Adding a new `.nix` file under `modules/` is auto-discovered and imported without editing any `imports` list, and stays inert until its `enable` flag is set. +- [x] All three overlays resolve: `unstable.`, `stable.`, and a chaotic-nyx package are each reachable in a `Host`. +- [x] home-manager builds as part of the same `nixos-rebuild switch` toplevel (system + user environment atomic). +- [x] The `user` option defaults to `alexion`, has no impure environment lookup, places the user in `wheel`, and drives both the system and home-manager user. +- [x] The old `nixosModules` flake output and the `with lib.my` idiom are absent. + +## Implementation Notes + +- **Verification.** `nix flake check` builds `checks.x86_64-linux.neogaia` = the Host toplevel (the spec's primary seam). Overlays confirmed via `nix eval` of `pkgs.unstable.hello` (2.12.3), `pkgs.stable.hello` (2.12.1), and `pkgs.linuxPackages_cachyos.kernel` (7.1.3, from chaotic). Auto-loader inertness confirmed both ways: the reference `modules/example.nix` is off by default, and `extendModules` with `modules.example.enable = true` activates its body. +- **chaotic binary cache.** Wired via `inputs.chaotic.nixosModules.default`, which puts both the `nyx-cache.chaotic.cx` substituter and its trusted public key into the built config (verified by evaluating `config.nix.settings.substituters`/`trusted-public-keys`). chaotic deliberately does **not** follow our nixpkgs, so the cache stays usable. Making the substituter/key explicit is task 0003's concern; here it is inherited from the module. +- **Shared base lives in `system/`.** The Skeleton's shared base config (overlays, `user`, flakes, home-manager wiring) is a `system/` module always imported by the host-builder, kept separate from the auto-loaded feature `Module`s under `modules/` so the base is never gated by an `enable` flag. +- **`modules/example.nix` kept intentionally.** It is the Auto-loader / Enable-convention reference every real Module copies; remove it once a real Module supersedes its teaching value. +- **`scriptFromFile` present but unused.** The task mandates the helper lib carry it ("the script-from-file helper"); its first caller lands with a later Module. +- **Home-manager base user only.** The base sets `home.username`/`homeDirectory`/`stateVersion` for the `user`; `extraSpecialArgs` passes both `inputs` and `my` (the flake lib) so upcoming HM Modules (fish/tmux/nvim) can reach `scriptFromFile`. +- **Deviations from plan.** Added an `options.user.description` (GECOS) alongside `user.name` — small and expected for a real account. Baseline `git` + global `allowUnfree` are set in the base (git is required for flakes; unfree is needed by chaotic/home-manager and later Claude Code). Placeholder `fileSystems`/bootloader and `hardware-configuration.nix` in `neogaia` are stubs that task 0002 (disko) replaces. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a4ab215 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,17 @@ +# dotfiles-nixos + +One flake that builds every machine the user owns. +The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overlays) lives in `.claude/CONTEXT.md`; the current deliverable's spec is `.claude/spec/laptop-mvi.md`. + +## Gotchas + +- Nix on the dev host needs experimental features passed per-command. + This repo is developed on `neogaia` while it still runs **CachyOS** (the migration target), where Nix is the distro package at `/usr/bin/nix` in multi-user daemon mode. + The system `/etc/nix/nix.conf` does not enable flakes, so export `NIX_CONFIG="experimental-features = nix-command flakes"` (or pass `--extra-experimental-features 'nix-command flakes'`) for every command. +- The dev user is a non-trusted daemon client (`nix store info` reports `Trusted: 0`). + You cannot add substituters from the CLI, so rely on what the flake/config declares (e.g. the chaotic cache is wired by the chaotic module, not a CLI flag). +- If `/nix/store` is missing or `nix-daemon` is inactive after a fresh Nix install, initialise it with `sudo systemd-tmpfiles --create nix-daemon.conf && sudo systemctl enable --now nix-daemon.socket`. +- The primary build/verify seam for any Host is `nix flake check`, which builds `checks.x86_64-linux.` (the system toplevel); cheap targeted checks use `nix eval .#nixosConfigurations..config...`. +- chaotic-nyx must **not** follow our `nixpkgs`, and its packages are built against chaotic's own pinned nixpkgs (its overlay defaults to `onTopOf = "flake-nixpkgs"`, the cache-friendly path). + That is what lets the `nyx-cache.chaotic.cx` binary cache hit instead of compiling the CachyOS kernel from source; the tradeoff is that chaotic packages do not see our `unstable`/`stable` overlays. +- The remote is self-hosted Gitea (`git.alexion.dev`); the forge CLI is `tea` (login `axi`), and `gh` is not installed. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..821e5bc --- /dev/null +++ b/flake.lock @@ -0,0 +1,155 @@ +{ + "nodes": { + "chaotic": { + "inputs": { + "flake-schemas": "flake-schemas", + "home-manager": "home-manager", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1784318604, + "narHash": "sha256-P/N5ZbGWITiTfmiWpE/1uyXdOCagpgw/YAZLZJSzx/I=", + "owner": "chaotic-cx", + "repo": "nyx", + "rev": "21a8ef816f34558a438d778057a8809322ea2415", + "type": "github" + }, + "original": { + "owner": "chaotic-cx", + "ref": "nyxpkgs-unstable", + "repo": "nyx", + "type": "github" + } + }, + "flake-schemas": { + "locked": { + "lastModified": 1780327564, + "narHash": "sha256-HiRPtA0spK+Dkgbhz/1zW9glXxNVB+L4Rj2VYmdawb8=", + "rev": "6cc9bd98891b1fc6bb2b8cb3277df8bc72799ca6", + "revCount": 149, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.5.0/019e83cf-9af3-78b1-ac5b-70e68ad1efe1/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/%3D0.5.0.tar.gz" + } + }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "chaotic", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1784129366, + "narHash": "sha256-N5JiyICSeQF14x+OQebNyPpYowOT9Rs1iKyeCylSzOA=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "165228b0efefc3e635e5174020c40ea64271dc25", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "home-manager_2": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1784351324, + "narHash": "sha256-By+kuRJZRqs2TuXgtR8vJ8cTKWXw33YG/Yollu5cO1U=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "460108009ca1ff69ca2ff19079ca2c838d6e3080", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1784120854, + "narHash": "sha256-KesHgItiZPgGX740axSiQLcIQ8D24MDqNpkKYWIek8k=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "753cc8a3a87467296ddd1fa93f0cc3e81120ee46", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1767313136, + "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1784347607, + "narHash": "sha256-VI5cdo27nEZ3m1SlgB8RvBbrqFUO2/dUgrrLWe407oA=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "31cd72fdba8fa052e437ce7e6879c4fe62def10f", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1784120854, + "narHash": "sha256-KesHgItiZPgGX740axSiQLcIQ8D24MDqNpkKYWIek8k=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "753cc8a3a87467296ddd1fa93f0cc3e81120ee46", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "chaotic": "chaotic", + "home-manager": "home-manager_2", + "nixpkgs": "nixpkgs_2", + "nixpkgs-stable": "nixpkgs-stable", + "nixpkgs-unstable": "nixpkgs-unstable" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4b733c5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,42 @@ +{ + description = "Alexion's NixOS configuration — one flake for every Host"; + + inputs = { + # Base channel: nixos-unstable (rolling, but gated by the NixOS test suite). + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + # Fresher-than-base packages, reachable per-package as `unstable.`. + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + + # Latest stable release, reachable per-package as `stable.`. + nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-25.05"; + + home-manager = { + url = "github:nix-community/home-manager"; + 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"; + }; + + outputs = + { self, nixpkgs, ... }@inputs: + let + inherit (nixpkgs) lib; + my = import ./lib { inherit lib inputs self; }; + in + { + # The trimmed helper lib: the Auto-loader, the host-builder, the script-from-file helper. + lib = my; + + # Every Host under hosts/ is auto-discovered and built. + nixosConfigurations = my.mkHosts (self + "/hosts"); + + # `nix flake check` builds each Host's toplevel — the primary test seam. + checks.x86_64-linux = lib.mapAttrs ( + _name: host: host.config.system.build.toplevel + ) self.nixosConfigurations; + }; +} diff --git a/hosts/neogaia/default.nix b/hosts/neogaia/default.nix new file mode 100644 index 0000000..ef88452 --- /dev/null +++ b/hosts/neogaia/default.nix @@ -0,0 +1,26 @@ +{ ... }: +# neogaia — Dell XPS 13 9380 laptop. +# +# Minimum viable Host: enough to evaluate and build the system toplevel. The +# real disk layout (disko: LUKS + btrfs + zram), the CachyOS kernel, networking, +# and the terminal Modules arrive in later tasks; the placeholders below are +# replaced by disko in task 0002. +{ + imports = [ ./hardware-configuration.nix ]; + + system.stateVersion = "25.05"; + + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + # Placeholder filesystems so the toplevel builds; superseded by the disko + # layout in task 0002. + fileSystems."/" = { + device = "/dev/disk/by-label/nixos"; + fsType = "ext4"; + }; + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + }; +} diff --git a/hosts/neogaia/hardware-configuration.nix b/hosts/neogaia/hardware-configuration.nix new file mode 100644 index 0000000..7585936 --- /dev/null +++ b/hosts/neogaia/hardware-configuration.nix @@ -0,0 +1,20 @@ +{ lib, modulesPath, ... }: +# Placeholder hardware profile — regenerated on the target machine at install +# time (`nixos-generate-config` / `disko-install`). Carries only enough for the +# toplevel to evaluate: the host platform and the XPS 13's initrd modules. +{ + imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; + + boot.initrd.availableKernelModules = [ + "xhci_pci" + "thunderbolt" + "nvme" + "usb_storage" + "sd_mod" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..f6373d7 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,83 @@ +{ + lib, + inputs, + self, +}: +let + inherit (lib) + attrNames + filterAttrs + genAttrs + flatten + hasSuffix + mapAttrsToList + ; + + # --- Auto-loader --------------------------------------------------------- + # Recursively collect every `.nix` file under `dir`, returned as a flat list + # of paths suitable for a module `imports`. No null-placeholder traversal + # hack: a directory recurses, a `.nix` file is taken, anything else is skipped. + collectNixFiles = + dir: + flatten ( + mapAttrsToList ( + name: type: + let + path = dir + "/${name}"; + in + if type == "directory" then + collectNixFiles path + else if type == "regular" && hasSuffix ".nix" name then + [ path ] + else + [ ] + ) (builtins.readDir dir) + ); + + # --- Host-builder -------------------------------------------------------- + # Build one Host: every Module is imported unconditionally (inert until its + # `enable` flag is set), alongside home-manager, chaotic, the shared base, + # and the Host's own directory. + mkHost = + { + hostName, + system ? "x86_64-linux", + }: + inputs.nixpkgs.lib.nixosSystem { + inherit system; + specialArgs = { + inherit inputs; + my = self.lib; + }; + modules = + (collectNixFiles (self + "/modules")) + ++ [ + inputs.home-manager.nixosModules.home-manager + inputs.chaotic.nixosModules.default + (self + "/system") + (self + "/hosts/${hostName}") + { networking.hostName = hostName; } + ]; + }; + + # Discover every Host (a subdirectory of `hostsDir`) and build each one. + mkHosts = + hostsDir: + let + hostNames = attrNames (filterAttrs (_name: type: type == "directory") (builtins.readDir hostsDir)); + in + genAttrs hostNames (hostName: mkHost { inherit hostName; }); + + # --- Script-from-file helper -------------------------------------------- + # Turn a standalone script file into a package on PATH, keeping the script + # itself editable as a real file rather than an inlined heredoc. + scriptFromFile = pkgs: name: path: pkgs.writeShellScriptBin name (builtins.readFile path); +in +{ + inherit + collectNixFiles + mkHost + mkHosts + scriptFromFile + ; +} diff --git a/modules/example.nix b/modules/example.nix new file mode 100644 index 0000000..a149901 --- /dev/null +++ b/modules/example.nix @@ -0,0 +1,14 @@ +{ config, lib, ... }: +# The Auto-loader reference example. Every real Module copies this shape: it is +# imported unconditionally but its body stays inert until a Host sets the +# `enable` flag, so each Host reads as a checklist of `enable = true` lines. +let + cfg = config.modules.example; +in +{ + options.modules.example.enable = lib.mkEnableOption "the Auto-loader reference example Module"; + + config = lib.mkIf cfg.enable { + environment.etc."skeleton-example".text = "This Module is enabled.\n"; + }; +} diff --git a/system/default.nix b/system/default.nix new file mode 100644 index 0000000..74e601d --- /dev/null +++ b/system/default.nix @@ -0,0 +1,84 @@ +{ + config, + lib, + pkgs, + inputs, + ... +}: +# The Skeleton's shared base config: the pieces every Host carries regardless of +# which Modules it enables — overlays, the `user`, flakes, and home-manager. +let + inherit (lib) mkOption types; + user = config.user; + + # Instantiate an extra nixpkgs source for the same platform as the base pkgs. + pinArgs = prev: { + inherit (prev.stdenv.hostPlatform) system; + config.allowUnfree = true; + }; +in +{ + options.user = { + name = mkOption { + type = types.str; + default = "alexion"; + description = '' + The primary interactive user this Host is built for. An explicit option + with no impure environment lookup, so the config is reproducible and + honest about who the user is. Drives both the system account and the + home-manager user in lockstep. + ''; + }; + description = mkOption { + type = types.str; + default = "Alexion"; + description = "Human-readable description (GECOS field) for the primary user."; + }; + }; + + config = { + # Base is nixos-unstable; reach a package fresher with `unstable.` or + # pin it rock-solid with `stable.`. chaotic's overlay is added by its + # own NixOS module, imported by the host-builder. + nixpkgs.overlays = [ + (_final: prev: { + unstable = import inputs.nixpkgs-unstable (pinArgs prev); + stable = import inputs.nixpkgs-stable (pinArgs prev); + }) + ]; + nixpkgs.config.allowUnfree = true; + + # Flakes + a baseline so `nixos-rebuild switch` works from the console. + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; + environment.systemPackages = [ pkgs.git ]; + + # Primary user, in wheel. The bootstrap password is set by hand at install + # time and never committed; moving it to a sops-backed hashedPasswordFile is + # the first post-boot task (out of scope for the MVI). + users.users.${user.name} = { + isNormalUser = true; + description = user.description; + extraGroups = [ "wheel" ]; + }; + + # home-manager as a NixOS module: one `nixos-rebuild switch` builds the + # system and the user environment atomically, sharing the system's pkgs + # (with our overlays) and installing user packages into the system profile. + home-manager = { + useGlobalPkgs = true; + useUserPackages = true; + extraSpecialArgs = { + inherit inputs; + my = inputs.self.lib; + }; + users.${user.name} = { + home.username = user.name; + home.homeDirectory = "/home/${user.name}"; + home.stateVersion = "25.05"; + }; + }; + }; +}