diff --git a/flake.lock b/flake.lock index 220bf89..1dad87c 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,711 @@ { "nodes": { + "base16": { + "inputs": { + "fromYaml": "fromYaml" + }, + "locked": { + "lastModified": 1755819240, + "narHash": "sha256-qcMhnL7aGAuFuutH4rq9fvAhCpJWVHLcHVZLtPctPlo=", + "owner": "SenchoPens", + "repo": "base16.nix", + "rev": "75ed5e5e3fce37df22e49125181fa37899c3ccd6", + "type": "github" + }, + "original": { + "owner": "SenchoPens", + "repo": "base16.nix", + "type": "github" + } + }, + "base16-fish": { + "flake": false, + "locked": { + "lastModified": 1765809053, + "narHash": "sha256-XCUQLoLfBJ8saWms2HCIj4NEN+xNsWBlU1NrEPcQG4s=", + "owner": "tomyun", + "repo": "base16-fish", + "rev": "86cbea4dca62e08fb7fd83a70e96472f92574782", + "type": "github" + }, + "original": { + "owner": "tomyun", + "repo": "base16-fish", + "rev": "86cbea4dca62e08fb7fd83a70e96472f92574782", + "type": "github" + } + }, + "base16-helix": { + "flake": false, + "locked": { + "lastModified": 1776754714, + "narHash": "sha256-E3OAK27smtATTmX45uoTSRsVD+Y+ZiVVfgM/tjpbtYg=", + "owner": "tinted-theming", + "repo": "base16-helix", + "rev": "4d508123037e7851ad36ebf7d9c48b0e9e1eb581", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-helix", + "type": "github" + } + }, + "base16-vim": { + "flake": false, + "locked": { + "lastModified": 1732806396, + "narHash": "sha256-e0bpPySdJf0F68Ndanwm+KWHgQiZ0s7liLhvJSWDNsA=", + "owner": "tinted-theming", + "repo": "base16-vim", + "rev": "577fe8125d74ff456cf942c733a85d769afe58b7", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-vim", + "rev": "577fe8125d74ff456cf942c733a85d769afe58b7", + "type": "github" + } + }, + "chaotic": { + "inputs": { + "flake-schemas": "flake-schemas", + "home-manager": "home-manager", + "nixpkgs": "nixpkgs-chaotic" + }, + "locked": { + "lastModified": 1785327209, + "narHash": "sha256-heXGjUBU1UsTHFzedDzYct9Cblr6FGzQcYyjCykywh8=", + "owner": "chaotic-cx", + "repo": "nyx", + "rev": "90cfa9864fa08c923dddeca965103ad44663dd64", + "type": "github" + }, + "original": { + "owner": "chaotic-cx", + "ref": "nyxpkgs-unstable", + "repo": "nyx", + "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" + } + }, + "firefox-addons": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "dir": "pkgs/firefox-addons", + "lastModified": 1785384175, + "narHash": "sha256-sWSJPXpQwKJstL4rdhpAQYCYlHK5wOkEHR8/lNHBVb4=", + "owner": "rycee", + "repo": "nur-expressions", + "rev": "db607f3d0afe811bcb3b16266f28b2fc5af4e74f", + "type": "gitlab" + }, + "original": { + "dir": "pkgs/firefox-addons", + "owner": "rycee", + "repo": "nur-expressions", + "type": "gitlab" + } + }, + "firefox-gnome-theme": { + "flake": false, + "locked": { + "lastModified": 1782007937, + "narHash": "sha256-PbnJr+eB+9Czol3ReI83dUgEhcn0sDK6TSy6ODTQm88=", + "owner": "rafaelmardojai", + "repo": "firefox-gnome-theme", + "rev": "981bd332015397fb1ca033fa982bd61635160c78", + "type": "github" + }, + "original": { + "owner": "rafaelmardojai", + "repo": "firefox-gnome-theme", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixvim", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782949081, + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ + "stylix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782949081, + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "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" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "fromYaml": { + "flake": false, + "locked": { + "lastModified": 1731966426, + "narHash": "sha256-lq95WydhbUTWig/JpqiB7oViTcHFP8Lv41IGtayokA8=", + "owner": "SenchoPens", + "repo": "fromYaml", + "rev": "106af9e2f715e2d828df706c386a685698f3223b", + "type": "github" + }, + "original": { + "owner": "SenchoPens", + "repo": "fromYaml", + "type": "github" + } + }, + "gitea-axi": { + "inputs": { + "home-manager": "home-manager_2", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1785340481, + "narHash": "sha256-GSxdQ7w8yYfZnfkXUuZ2fYIKibe9ZU8xDGyqpeTb2tE=", + "ref": "refs/heads/main", + "rev": "627fc9a32bb80d97923540fc0d3e9661961462ba", + "revCount": 83, + "type": "git", + "url": "https://git.alexion.dev/alexion/gitea-axi" + }, + "original": { + "type": "git", + "url": "https://git.alexion.dev/alexion/gitea-axi" + } + }, + "gitea-axi_2": { + "inputs": { + "home-manager": "home-manager_4", + "nixpkgs": [ + "skills", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1785340481, + "narHash": "sha256-GSxdQ7w8yYfZnfkXUuZ2fYIKibe9ZU8xDGyqpeTb2tE=", + "ref": "refs/heads/main", + "rev": "627fc9a32bb80d97923540fc0d3e9661961462ba", + "revCount": 83, + "type": "git", + "url": "https://git.alexion.dev/alexion/gitea-axi" + }, + "original": { + "type": "git", + "url": "https://git.alexion.dev/alexion/gitea-axi" + } + }, + "gnome-shell": { + "flake": false, + "locked": { + "host": "gitlab.gnome.org", + "lastModified": 1776175984, + "narHash": "sha256-RJFlFW8GiMei6oqUGrMkGEvVqOH8U7Q8abc1yK4VKD8=", + "owner": "GNOME", + "repo": "gnome-shell", + "rev": "e0fdc4c13250e9a9b8ea9594c83925274f4a5dca", + "type": "gitlab" + }, + "original": { + "host": "gitlab.gnome.org", + "owner": "GNOME", + "ref": "50.1", + "repo": "gnome-shell", + "type": "gitlab" + } + }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "chaotic", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1785288465, + "narHash": "sha256-nCkxaGRtyNheNTxoc527gjOG0BN2zovsWDQVBeKDMW8=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "36662afed2fa1c9b69bdd03edb92ad572202ca20", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "home-manager_2": { + "inputs": { + "nixpkgs": [ + "gitea-axi", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1784588016, + "narHash": "sha256-ouZe80aWEhMLVMkqICFDN+JUw+0FJtCr/bh+hHtRtMg=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "deeb6b7eb7e0c44ae1819c051ce175bd92a85100", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "home-manager_3": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1785306346, + "narHash": "sha256-DScBkW0fOgpGPK2trNoX3ryLTlaC14+gglFo/BhGJ4g=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "e705714e918c3b11affcdd15db2cbe3a070420a0", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "home-manager_4": { + "inputs": { + "nixpkgs": [ + "skills", + "gitea-axi", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1784588016, + "narHash": "sha256-ouZe80aWEhMLVMkqICFDN+JUw+0FJtCr/bh+hHtRtMg=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "deeb6b7eb7e0c44ae1819c051ce175bd92a85100", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "home-manager_5": { + "inputs": { + "nixpkgs": [ + "skills", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1784725727, + "narHash": "sha256-J5+C9wsO0lhDyUalQzfplDbRjyHDYeEH5+9sdyXtwa8=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "041a999e8c1c5b731913855909e68d30ca69b8e0", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "nixos-hardware": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1785232496, + "narHash": "sha256-65EQYIRRpTdpH8lUiB6Mvo5uBkG60aBIzAJuALfx+O0=", + "owner": "NixOS", + "repo": "nixos-hardware", + "rev": "2e790b0a6be8ec2b76174ac0931b8ff11919ec98", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixos-hardware", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1785133411, + "narHash": "sha256-Yjv0WEg39KRYS0rBdTbu6Fc/or/ihAKk13W9sQ6VWd0=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "2f5a153c270b70cb0f8c11f46d96d6d3bc39f4e3", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-26.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1785301185, + "narHash": "sha256-eoS3KQTO0aPWXZvIaRbRAzSSHW3l5wdMFXtT1ISfoKA=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9bc02893134c733dd85de46ee4fb2fac696b5529", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixvim": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1785364321, + "narHash": "sha256-BLuHl+nZKb+FDq3GAM6L+UBEiyVepXANA31fT1F56pw=", + "owner": "nix-community", + "repo": "nixvim", + "rev": "acd69cc15d57004e8cb4495034320263a3d362ea", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixvim", + "type": "github" + } + }, + "nur": { + "inputs": { + "flake-parts": [ + "stylix", + "flake-parts" + ], + "nixpkgs": [ + "stylix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1783439237, + "narHash": "sha256-WUr8JF2v3n4Y30E5dxv4sAgNJXpVDBoCQNoQ/V4+n4o=", + "owner": "nix-community", + "repo": "NUR", + "rev": "b70bb66c7bcd162642f3a609bc16843c7059f503", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "NUR", + "type": "github" + } + }, + "root": { + "inputs": { + "chaotic": "chaotic", + "disko": "disko", + "firefox-addons": "firefox-addons", + "gitea-axi": "gitea-axi", + "home-manager": "home-manager_3", + "nixos-hardware": "nixos-hardware", + "nixpkgs": "nixpkgs", + "nixpkgs-stable": "nixpkgs-stable", + "nixpkgs-unstable": "nixpkgs-unstable", + "nixvim": "nixvim", + "skills": "skills", + "sops-nix": "sops-nix", + "stylix": "stylix" + } + }, + "skills": { + "inputs": { + "flake-utils": "flake-utils", + "gitea-axi": "gitea-axi_2", + "home-manager": "home-manager_5", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1787455371, + "narHash": "sha256-MJ0bX4/cksSbGl7v9o0l+Wy1xAEa3BgzG7Hs8EEGde0=", + "ref": "refs/heads/main", + "rev": "9a1d21da4d3e70165ff772967d639df244dcc2ea", + "revCount": 55, + "type": "git", + "url": "https://git.alexion.dev/alexion/skills" + }, + "original": { + "type": "git", + "url": "https://git.alexion.dev/alexion/skills" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1783174389, + "narHash": "sha256-aCWC8ngycU7OdJrU2+Je3qf+1a2ykuBvpPhZT/9tXMc=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "f1406619a3884cd5c47992a70b8b35c9c0fcb4c9", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + }, + "stylix": { + "inputs": { + "base16": "base16", + "base16-fish": "base16-fish", + "base16-helix": "base16-helix", + "base16-vim": "base16-vim", + "firefox-gnome-theme": "firefox-gnome-theme", + "flake-parts": "flake-parts_2", + "gnome-shell": "gnome-shell", + "nixpkgs": [ + "nixpkgs" + ], + "nur": "nur", + "systems": "systems_3", + "tinted-kitty": "tinted-kitty", + "tinted-schemes": "tinted-schemes", + "tinted-tmux": "tinted-tmux", + "tinted-zed": "tinted-zed" + }, + "locked": { + "lastModified": 1784676123, + "narHash": "sha256-ndyanKzw90yX2nUVFmTuYqXidUNymtMfgmIHyNdhht0=", + "owner": "danth", + "repo": "stylix", + "rev": "66714e5ce44269ecc58c20d9196da8dbe1b27a31", + "type": "github" + }, + "original": { + "owner": "danth", + "repo": "stylix", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1774449309, + "narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=", + "owner": "nix-systems", + "repo": "default", + "rev": "c29398b59d2048c4ab79345812849c9bd15e9150", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "ref": "future-26.11", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "tinted-kitty": { + "flake": false, + "locked": { + "lastModified": 1735730497, + "narHash": "sha256-4KtB+FiUzIeK/4aHCKce3V9HwRvYaxX+F1edUrfgzb8=", + "owner": "tinted-theming", + "repo": "tinted-kitty", + "rev": "de6f888497f2c6b2279361bfc790f164bfd0f3fa", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-kitty", + "type": "github" + } + }, + "tinted-schemes": { + "flake": false, + "locked": { + "lastModified": 1781968807, + "narHash": "sha256-yYO3Vw2M0y3TAUqt+9+Mj0zwP3XDTF5/PXcPhhFQ1ZM=", + "owner": "tinted-theming", + "repo": "schemes", + "rev": "2ccef2f4b22e3cab5a9292811f7133a07eeba4a7", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "schemes", + "type": "github" + } + }, + "tinted-tmux": { + "flake": false, + "locked": { + "lastModified": 1782012462, + "narHash": "sha256-2iDiD8DQLwS1lGuD9TS8WlvNyDoTs6krWntJbtB2zGo=", + "owner": "tinted-theming", + "repo": "tinted-tmux", + "rev": "8c4e750f738a742bd73377ee41d3dadedebedef4", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-tmux", + "type": "github" + } + }, + "tinted-zed": { + "flake": false, + "locked": { + "lastModified": 1782009766, + "narHash": "sha256-VUhBjpGvWqHI7rWeyMYb/u87YJSXHKfVV6S+IelWeO8=", + "owner": "tinted-theming", + "repo": "base16-zed", + "rev": "5e8350bcd354e3241ab681a265fa6ef060c40be1", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-zed", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1785318670, @@ -16,9 +722,20 @@ "type": "github" } }, - "root": { - "inputs": { - "nixpkgs": "nixpkgs" + "nixpkgs-chaotic": { + "locked": { + "lastModified": 1785090369, + "narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "624af665418d3c65d544145b4d34ad696439570e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index c27f358..c73a68c 100644 --- a/flake.nix +++ b/flake.nix @@ -4,23 +4,82 @@ inputs = { # Base nixpkgs channel. nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + # Fresher 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-26.05"; + + home-manager = { + url = "github:nix-community/home-manager"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Signed AMO extensions, pinned by version and hash. + firefox-addons = { + url = "gitlab:rycee/nur-expressions?dir=pkgs/firefox-addons"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Follows our nixpkgs so its plugins build against the same package set. + nixvim = { + url = "github:nix-community/nixvim"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Declarative disk partitioning. + # Each host declares its own layout. + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Upstream per-machine hardware profiles. + # Each host imports its own. + nixos-hardware = { + url = "github:NixOS/nixos-hardware"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Decrypts committed secrets at activation, from an age identity on the host. + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Themes the graphical layer from one base16 scheme. + # Follows our nixpkgs so it themes the same package set the host builds. + stylix = { + url = "github:danth/stylix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Agent-ergonomic CLI for Gitea, with a home-manager module for the agent context. + gitea-axi = { + url = "git+https://git.alexion.dev/alexion/gitea-axi"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Personal agent skills, packaged as per-skill derivations with a home-manager module. + skills = { + url = "git+https://git.alexion.dev/alexion/skills"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # CachyOS kernel and binary cache. + # This must not follow this flake's nixpkgs because Chaotic builds against its own pinned package set. + # Its cache serves the kernel only when the overlay stays on the cache-friendly path. + chaotic.url = "github:chaotic-cx/nyx/nyxpkgs-unstable"; }; - outputs = { nixpkgs, ... }: - let - system = "x86_64-linux"; - pkgs = import nixpkgs { - inherit system; - config.allowUnfree = true; - }; - in + outputs = + { self, nixpkgs, ... }@inputs: { - devShells.${system} = { - test = pkgs.mkShell { - shellHook = '' - echo "Hello world!" - ''; - }; - }; + nixosConfigurations = (import ./src/dotfiles { + inherit inputs self; + lib = nixpkgs.lib; + root = ./configs/nixos; + }).nixosConfigurations; }; } diff --git a/src/dotfiles/default.nix b/src/dotfiles/default.nix new file mode 100644 index 0000000..b7426d5 --- /dev/null +++ b/src/dotfiles/default.nix @@ -0,0 +1,54 @@ +{ + lib, + inputs, + self, + root, +}: +let + util = import ./util.nix { inherit lib; }; + + registry = import ./registry.nix { + inherit lib; + hosts = self.nixosConfigurations; + root = root + "/registry"; + }; + + modules = import ./modules.nix { + inherit util; + root = root + "/modules"; + }; + + guestModules = import ./guests.nix { + inherit + inputs + lib + registry + specialArgs + util + ; + repositoryModules = modules; + root = root + "/guests"; + }; + + specialArgs = { + inherit + inputs + self + util + ; + registry = registry.collected; + }; +in +{ + nixosConfigurations = import ./hosts.nix { + inherit + inputs + lib + specialArgs + guestModules + ; + registryModule = registry.module; + repositoryModules = modules; + root = root + "/hosts"; + }; +} diff --git a/src/dotfiles/guests.nix b/src/dotfiles/guests.nix new file mode 100644 index 0000000..5f8c50f --- /dev/null +++ b/src/dotfiles/guests.nix @@ -0,0 +1,279 @@ +{ + inputs, + lib, + root, + specialArgs, + repositoryModules, + registry, + util, +}: +let + mkGuest = + name: + { config, lib, ... }: + let + cfg = config.guests.${name}; + + directoryMounts = lib.filterAttrs (_: mount: mount.directory != null) cfg.mounts; + + directoryScript = lib.concatStringsSep "\n" ( + lib.mapAttrsToList ( + _: mount: + '' + ${config.system.path}/bin/install -d \ + ${lib.optionalString (mount.owner != null) "-o ${lib.escapeShellArg (toString mount.owner)}"} \ + ${lib.optionalString (mount.group != null) "-g ${lib.escapeShellArg mount.group}"} \ + ${lib.optionalString (mount.mode != null) "-m ${lib.escapeShellArg mount.mode}"} \ + ${lib.escapeShellArg mount.directory} + '' + ) directoryMounts + ); + in + { + options.guests.${name} = { + enable = lib.mkEnableOption "the ${name} guest, run in its own nested container"; + vlan = lib.mkOption { + type = lib.types.ints.between 1 4094; + example = 10; + description = '' + The tagged VLAN this guest lives on. The guest attaches to its host's + `br-vlan` bridge for that VLAN. The id must be one of the host's + `modules.network.vlans.ids`. + ''; + }; + mac = lib.mkOption { + type = lib.types.str; + default = util.deriveMac "${config.networking.hostName}.${name}"; + defaultText = lib.literalMD "a stable address derived from the host name and guest option path"; + example = "bc:24:11:00:00:01"; + description = '' + The guest's MAC address on its VLAN, pinned inside the guest by its + own networkd. Set it to reuse an existing address so a router's DHCP + reservation keeps working. Left unset, a stable address is derived + from the host name and guest option path in the locally-administered range. + ''; + }; + address = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "10.23.20.126"; + description = '' + Expected IP address assigned outside NixOS for this guest. + Left null, this guest contributes no address to the address registry. + ''; + }; + reverseProxy = { + enable = lib.mkEnableOption "a reverse-proxy route for this guest"; + + route = lib.mkOption { + type = registry.types.route; + description = "Route registry entry published for this guest."; + }; + }; + mounts = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options = { + directory = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/srv/media"; + description = "Host directory bind-mounted into the guest."; + }; + file = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/run/secrets/service-password"; + description = "Host file bind-mounted into the guest."; + }; + readOnly = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Mount the host path read-only inside the guest."; + }; + owner = lib.mkOption { + type = lib.types.nullOr ( + lib.types.oneOf [ + lib.types.str + lib.types.ints.positive + ] + ); + default = null; + description = "Owner assigned to the host directory before the guest starts."; + }; + group = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Group assigned to the host directory before the guest starts."; + }; + mode = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "0770"; + description = "Mode assigned to the host directory before the guest starts."; + }; + }; + } + ); + default = { }; + example = lib.literalExpression '' + { + "/data/media" = { directory = "/srv/media"; }; + "/data/config" = { directory = "/srv/config/jellyfin"; }; + } + ''; + description = '' + Host paths bind-mounted into the guest, keyed by the path they appear + at inside the guest, so a guest sees exactly the data it should at any + granularity — a single folder, a whole pool, or a decrypted secret. + Mounts are read-write unless `readOnly` is set. + ''; + }; + interior = lib.mkOption { + type = lib.types.deferredModule; + default = { }; + description = "NixOS module imported into the guest interior."; + }; + limits = { + memory = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "2G"; + description = '' + Cap on the guest's memory, applied to its unit as `MemoryMax`. + Accepts systemd size suffixes such as `512M` or `2G`. Left null, + the guest's memory is uncapped. + ''; + }; + cpu = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "150%"; + description = '' + Cap on the guest's CPU, applied to its unit as `CPUQuota`, where + `100%` is one full core. Left null, the guest's CPU is uncapped. + ''; + }; + tasksMax = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + example = 512; + description = '' + Cap on the number of processes and threads the guest may spawn, + applied to its unit as `TasksMax`. Left null, the task count is + uncapped. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = + let + hasDirectorySettings = mount: mount.owner != null || mount.group != null || mount.mode != null; + hasCompleteDirectorySettings = mount: mount.owner != null && mount.group != null && mount.mode != null; + hasOneSource = mount: (mount.directory != null) != (mount.file != null); + mountsWithInvalidSource = lib.attrNames (lib.filterAttrs (_: mount: !hasOneSource mount) cfg.mounts); + fileMountsWithHostSettings = lib.attrNames ( + lib.filterAttrs (_: mount: mount.file != null && hasDirectorySettings mount) cfg.mounts + ); + directoryMountsWithPartialHostSettings = lib.attrNames ( + lib.filterAttrs (_: mount: mount.directory != null && hasDirectorySettings mount && !hasCompleteDirectorySettings mount) cfg.mounts + ); + in + [ + { + assertion = mountsWithInvalidSource == [ ]; + message = '' + guests.${name} mounts ${lib.concatStringsSep ", " mountsWithInvalidSource} must set exactly one of `directory` or `file`. + ''; + } + { + assertion = fileMountsWithHostSettings == [ ]; + message = '' + guests.${name} file mounts ${lib.concatStringsSep ", " fileMountsWithHostSettings} must not set `owner`, `group`, or `mode`. + ''; + } + { + assertion = directoryMountsWithPartialHostSettings == [ ]; + message = '' + guests.${name} directory mounts ${lib.concatStringsSep ", " directoryMountsWithPartialHostSettings} must set `owner`, `group`, and `mode` together. + ''; + } + { + assertion = lib.elem cfg.vlan config.modules.network.vlans.ids; + message = '' + guests.${name}.vlan = ${toString cfg.vlan} is not among its host's modules.network.vlans.ids (${lib.concatMapStringsSep ", " toString config.modules.network.vlans.ids}). Declare the VLAN on the host or correct the guest's placement. + ''; + } + ]; + + registry.address.${config.networking.hostName}.${name} = + lib.mkIf (cfg.address != null) cfg.address; + + registry.route.${config.networking.hostName}.${name} = + lib.mkIf cfg.reverseProxy.enable cfg.reverseProxy.route; + + systemd.services."container@${name}" = + let + bridgeDevice = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] (util.bridgeName cfg.vlan)}.device"; + in + { + after = [ bridgeDevice ]; + wants = [ bridgeDevice ]; + + serviceConfig = lib.filterAttrs (_: v: v != null) { + MemoryMax = cfg.limits.memory; + CPUQuota = cfg.limits.cpu; + TasksMax = cfg.limits.tasksMax; + }; + preStart = lib.mkIf (directoryMounts != { }) directoryScript; + }; + + containers.${name} = { + autoStart = true; + privateNetwork = lib.mkDefault true; + hostBridge = util.bridgeName cfg.vlan; + privateUsers = lib.mkDefault "no"; + bindMounts = lib.mapAttrs (_: mount: { + hostPath = if mount.directory != null then mount.directory else mount.file; + isReadOnly = mount.readOnly; + }) cfg.mounts; + + inherit specialArgs; + + config = { + imports = [ + inputs.sops-nix.nixosModules.sops + inputs.stylix.nixosModules.stylix + + repositoryModules + registry.module + cfg.interior + + { + modules.guest.enable = true; + + networking.useNetworkd = true; + networking.useHostResolvConf = false; + + systemd.network.networks."20-eth0" = { + matchConfig.Name = "eth0"; + linkConfig.MACAddress = cfg.mac; + networkConfig.DHCP = "yes"; + }; + } + ]; + }; + }; + }; + }; +in +let + files = util.collectNixFiles root; + guestFiles = lib.filterAttrs (file: type: type == "regular" && lib.hasSuffix ".nix" file) (builtins.readDir root); + names = map (file: lib.removeSuffix ".nix" file) (lib.attrNames guestFiles); +in +{ + imports = files ++ map mkGuest names; +} diff --git a/src/dotfiles/hosts.nix b/src/dotfiles/hosts.nix new file mode 100644 index 0000000..a25372d --- /dev/null +++ b/src/dotfiles/hosts.nix @@ -0,0 +1,45 @@ +{ + inputs, + lib, + root, + specialArgs, + repositoryModules, + + guestModules, + registryModule, +}: +let + mkHost = + { + root, + name, + }: + lib.nixosSystem { + inherit specialArgs; + modules = [ + inputs.chaotic.nixosModules.default + inputs.disko.nixosModules.disko + inputs.sops-nix.nixosModules.sops + inputs.stylix.nixosModules.stylix + + repositoryModules + guestModules + + # TODO: refactor + registryModule (root + "/${name}") + + { + networking.hostName = name; + modules.host.enable = true; + } + ]; + }; +in +let + directories = lib.filterAttrs (_name: type: type == "directory") (builtins.readDir root); + names = lib.attrNames directories; +in +lib.genAttrs names ( + name: + mkHost { inherit root name; } +) diff --git a/src/dotfiles/modules.nix b/src/dotfiles/modules.nix new file mode 100644 index 0000000..9dead8f --- /dev/null +++ b/src/dotfiles/modules.nix @@ -0,0 +1,7 @@ +{ + root, + util, +}: +{ + imports = util.collectNixFiles root; +} diff --git a/src/dotfiles/registry.nix b/src/dotfiles/registry.nix new file mode 100644 index 0000000..67c7c05 --- /dev/null +++ b/src/dotfiles/registry.nix @@ -0,0 +1,197 @@ +{ hosts, lib, root }: +let + checkType = lib.types.submodule { + options = { + assertion = lib.mkOption { + type = lib.types.functionTo lib.types.bool; + description = '' + Predicate run against this registry's flattened collected entries. + ''; + }; + + message = lib.mkOption { + type = lib.types.str; + description = '' + Error message shown when this registry validation check fails. + ''; + }; + }; + }; + + predicates = { + uniqueValue = value: entries: builtins.length (lib.unique (map value entries)) == builtins.length entries; + }; + + registryLib = lib // { + registry = (lib.registry or { }) // { + inherit predicates; + }; + }; + + registryDefinitionModule = { + options = { + type = lib.mkOption { + type = lib.types.raw; + description = '' + NixOS option type for one registry entry. + ''; + }; + + replicate = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether collected entries are merged and published under every host key. + ''; + }; + + checks = lib.mkOption { + type = lib.types.listOf checkType; + default = [ ]; + description = '' + Validation checks for this registry's flattened collected entries. + ''; + }; + }; + }; + + registryName = fileName: lib.removeSuffix ".nix" fileName; + + registryOption = registry: lib.mkOption { + type = lib.types.attrsOf (lib.types.attrsOf registry.type); + default = { }; + description = '' + Lazily collected fleet-visible registry entries, nested by host and entry name. + ''; + }; + + mkRegistry = path: + let + evaluated = lib.evalModules { + specialArgs.lib = registryLib; + modules = [ + registryDefinitionModule + path + ]; + }; + in + { + inherit (evaluated.config) type replicate checks; + }; + + definitions = lib.mapAttrs' ( + name: type: + let + path = root + "/${name}"; + in + lib.nameValuePair (registryName name) (mkRegistry path) + ) (lib.filterAttrs (name: type: type == "regular" && lib.hasSuffix ".nix" name) (builtins.readDir root)); + + flattenRegistryEntries = registryName: data: + lib.concatLists ( + lib.mapAttrsToList ( + host: entries: + lib.mapAttrsToList (entry: value: { + path = "registry.${registryName}.${host}.${entry}"; + inherit host entry value; + }) entries + ) data + ); + + rawDeclarations = registryName: hosts: + lib.mapAttrs (_hostName: host: host.config.registry.${registryName} or { }) hosts; + + replicatedEntries = registryName: raw: + let + entries = lib.concatLists ( + lib.concatLists ( + lib.mapAttrsToList ( + _evalHost: hostDeclarations: + lib.mapAttrsToList ( + hostName: declarations: + lib.mapAttrsToList (entryName: value: { + path = "registry.${registryName}.${hostName}.${entryName}"; + inherit entryName value; + }) declarations + ) hostDeclarations + ) raw + ) + ); + in + lib.foldl' ( + acc: entry: + if lib.hasAttr entry.entryName acc then + if acc.${entry.entryName}.value == entry.value then + acc + else + throw "conflicting replicated registry entry ${entry.path} does not match ${acc.${entry.entryName}.path}" + else + acc // { + ${entry.entryName} = entry; + } + ) { } entries; + + replicatedData = registryName: hosts: raw: + let + entries = lib.mapAttrs (_entryName: entry: entry.value) (replicatedEntries registryName raw); + in + lib.mapAttrs (_hostName: _host: entries) hosts; + + validationAssertions = data: + lib.concatLists ( + lib.mapAttrsToList ( + registryName: registry: + let + entries = flattenRegistryEntries registryName data.${registryName}; + in + map (check: { + assertion = check.assertion entries; + inherit (check) message; + }) registry.checks + ) definitions + ); + + assertValid = data: + let + assertions = validationAssertions data; + failures = lib.filter (assertion: !assertion.assertion) assertions; + in + assert lib.assertMsg (lib.all (assertion: assertion.assertion) assertions) ( + lib.concatMapStringsSep "\n" (assertion: assertion.message) failures + ); + data; + + join = registry: host: entry: + lib.filterAttrs (_name: value: value != null) ( + lib.mapAttrs (registryName: _registry: (registry.${registryName}.${host} or { }).${entry} or null) definitions + ); + + collectData = + { hosts }: + let + raw = lib.mapAttrs (registryName: _registry: rawDeclarations registryName hosts) definitions; + data = lib.mapAttrs ( + registryName: registry: + if registry.replicate or false then + replicatedData registryName hosts raw.${registryName} + else + lib.mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts + ) definitions; + in + assertValid data; + + withOperations = registry: + registry + // { + join = host: entry: join registry host entry; + }; +in +{ + types = lib.mapAttrs (_name: definition: definition.type) definitions; + + module = { + options.registry = lib.mapAttrs (_name: registry: registryOption registry) definitions; + }; + + collected = withOperations (collectData { inherit hosts; }); +} diff --git a/src/dotfiles/util.nix b/src/dotfiles/util.nix new file mode 100644 index 0000000..66c1018 --- /dev/null +++ b/src/dotfiles/util.nix @@ -0,0 +1,32 @@ +{ lib }: +let + collectNixFiles = + dir: + lib.flatten ( + lib.mapAttrsToList ( + name: type: + let + path = dir + "/${name}"; + in + if type == "directory" then + collectNixFiles path + else if type == "regular" && lib.hasSuffix ".nix" name then + [ path ] + else + [ ] + ) (builtins.readDir dir) + ); +in +{ + inherit collectNixFiles; + + bridgeName = id: "br-vlan${toString id}"; + + deriveMac = + name: + let + hash = builtins.hashString "sha256" name; + octet = i: builtins.substring (i * 2) 2 hash; + in + lib.concatStringsSep ":" ([ "02" ] ++ map octet [ 0 1 2 3 4 ]); +}