Build the Skeleton and a minimal neogaia Host (task 0001)

Stand up the walking skeleton the rest of the laptop MVI extends and
re-verifies against: the whole neogaia Host evaluates and its system
toplevel builds (nix flake check green).

- flake.nix: hand-rolled flake (no flake-parts). Base nixos-unstable, plus
  nixpkgs-unstable and nixos-25.05 for the per-package unstable/stable
  overlays, home-manager (nixpkgs followed), and chaotic-nyx (deliberately
  not following our nixpkgs, to keep its binary cache usable). checks build
  each Host toplevel.
- lib/: trimmed helper lib — the Auto-loader (recursive .nix discovery, no
  null-placeholder hack), the host-builder, and the script-from-file helper.
  Deps inherited explicitly; no with lib.my, no nixosModules output.
- system/: shared base config — the unstable/stable overlays, the user
  option (defaults to alexion, in wheel, drives system + home-manager user
  in lockstep), flakes, git, and home-manager as a NixOS module.
- modules/example.nix: Auto-loader / Enable-convention reference Module,
  inert until enabled.
- hosts/neogaia/: minimal laptop Host — placeholder filesystems, bootloader,
  and hardware profile, all superseded by disko in task 0002.
- CLAUDE.md: project agent instructions with a Gotchas section (nix on the
  CachyOS dev host, the chaotic overlay/cache behaviour, tea/Gitea).
This commit is contained in:
2026-07-18 08:05:36 -04:00
parent f8a084e111
commit 8f33d14695
9 changed files with 478 additions and 0 deletions

View File

@@ -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.<name>`, a `stable overlay` exposing `nixos-25.05` as `stable.<name>`, 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.<path>.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.<pkg>`, `stable.<pkg>`, 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.

17
CLAUDE.md Normal file
View File

@@ -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.<host>` (the system toplevel); cheap targeted checks use `nix eval .#nixosConfigurations.<host>.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.

155
flake.lock generated Normal file
View File

@@ -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
}

42
flake.nix Normal file
View File

@@ -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.<name>`.
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixpkgs-unstable";
# Latest stable release, reachable per-package as `stable.<name>`.
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;
};
}

26
hosts/neogaia/default.nix Normal file
View File

@@ -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";
};
}

View File

@@ -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";
}

83
lib/default.nix Normal file
View File

@@ -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
;
}

14
modules/example.nix Normal file
View File

@@ -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";
};
}

84
system/default.nix Normal file
View File

@@ -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.<name>` or
# pin it rock-solid with `stable.<name>`. 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";
};
};
};
}