diff --git a/.github/workflows/flake.yml b/.github/workflows/flake.yml deleted file mode 100644 index 4975bc9..0000000 --- a/.github/workflows/flake.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Flake - -on: - pull_request: - push: - branches: - - main - workflow_dispatch: - -jobs: - check: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Nix - uses: cachix/install-nix-action@v31 - with: - extra_nix_config: | - experimental-features = nix-command flakes - accept-flake-config = true - - - name: Magic Nix Cache - uses: DeterminateSystems/magic-nix-cache-action@main - - - name: nixpkgs-fmt - run: nix build --print-build-logs .#checks.x86_64-linux.format - - - name: deadnix - run: nix build --print-build-logs .#checks.x86_64-linux.deadnix - - - name: statix - run: nix build --print-build-logs .#checks.x86_64-linux.statix - - - name: nix flake check - run: nix flake check --print-build-logs --no-build - - - name: Evaluate templates - run: ./scripts/check-templates.sh diff --git a/flake.lock b/flake.lock index 183ff45..f1eeae0 100644 --- a/flake.lock +++ b/flake.lock @@ -267,11 +267,11 @@ }, "nixpkgs-codex": { "locked": { - "lastModified": 1783415080, - "narHash": "sha256-6x4GltLr71EaeQ53Z/UP4/RyXt6594T5OSlzr3ru4Hg=", + "lastModified": 1786514691, + "narHash": "sha256-9dkt1i5JNbEfPb+EjLcSDNs8zqEHQ/1JlSaKgj0SBAg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "22f7adf45b294f681a7bf98fa4b938df638d9d0e", + "rev": "867dcbc30bafe3c862ef88620f2e7a109d7d3be5", "type": "github" }, "original": { @@ -360,27 +360,6 @@ "type": "github" } }, - "opencode": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1783363839, - "narHash": "sha256-Xm6PXDzRqHS66ERKTlhEfD9enrHOsZUaQPrfGD3/WqU=", - "owner": "sst", - "repo": "opencode", - "rev": "0ed10fbd62423d86f9ef8d39e49b167d70be541d", - "type": "github" - }, - "original": { - "owner": "sst", - "ref": "v1.17.14", - "repo": "opencode", - "type": "github" - } - }, "root": { "inputs": { "home-manager": "home-manager", @@ -389,7 +368,6 @@ "nixpkgs-codex": "nixpkgs-codex", "nixpkgs-stable": "nixpkgs-stable", "nixvim": "nixvim", - "opencode": "opencode", "sops-nix": "sops-nix", "stylix": "stylix", "worktrunk": "worktrunk", diff --git a/flake.nix b/flake.nix index 2f938bd..e23f515 100644 --- a/flake.nix +++ b/flake.nix @@ -24,37 +24,48 @@ url = "github:DeclanRiedel/zed-thread-tui"; inputs.nixpkgs.follows = "nixpkgs"; }; - opencode = { - url = "github:sst/opencode/v1.17.14"; - inputs.nixpkgs.follows = "nixpkgs"; - }; }; outputs = inputs@{ self, nixpkgs, home-manager, sops-nix, nixvim, nixpkgs-codex, nixpkgs-stable, worktrunk, ... }: let - system = "x86_64-linux"; - pkgsUnfree = import nixpkgs { + defaultSystem = "x86_64-linux"; + defaultUser = { name = "declan"; home = "/home/declan"; }; + hosts = { + machine = { + modules = [ + ./misc/config.nix + ./modules/default.nix + ./server/default.nix + ]; + }; + vostro = { + modules = [ + ./hosts/vostro/default.nix + ./modules/default.nix + ./modules/ai-auto-update.nix + ./modules/memory.nix + ]; + }; + nixos-wsl = { + modules = [ + inputs.nixos-wsl.nixosModules.default + ./hosts/nixos-wsl/default.nix + ]; + }; + }; + mkPkgs = system: import nixpkgs { inherit system; config = { allowUnfree = true; android_sdk.accept_license = true; }; }; - pkgsCodex = import nixpkgs-codex { - inherit system; - config.allowUnfree = true; - overlays = [ inputs.opencode.overlays.default ]; - }; - pkgsStable = import nixpkgs-stable { - inherit system; - config.allowUnfree = true; - }; - homeManagerModule = { + mkHomeManagerModule = hostConfig: { home-manager.useGlobalPkgs = true; home-manager.useUserPackages = true; home-manager.backupFileExtension = "backup"; - home-manager.extraSpecialArgs = { inherit inputs; }; - home-manager.users.declan = { + home-manager.extraSpecialArgs = { inherit inputs hostConfig; }; + home-manager.users.${hostConfig.user.name} = { imports = [ ./home-manager/home.nix nixvim.homeModules.nixvim @@ -62,27 +73,43 @@ ]; }; }; - mkHost = modules: + mkHost = hostName: host: + let + system = host.system or defaultSystem; + pkgsUnfree = mkPkgs system; + pkgsCodex = import nixpkgs-codex { + inherit system; + config.allowUnfree = true; + }; + pkgsStable = import nixpkgs-stable { + inherit system; + config.allowUnfree = true; + }; + hostConfig = host // { + inherit hostName system; + user = host.user or defaultUser; + }; + in nixpkgs.lib.nixosSystem { inherit system; pkgs = pkgsUnfree; - modules = [ + specialArgs = { inherit inputs hostConfig pkgsCodex pkgsStable; }; + modules = host.modules ++ [ { - _module.args.pkgsCodex = pkgsCodex; - _module.args.pkgsStable = pkgsStable; + networking.hostName = nixpkgs.lib.mkDefault hostName; } - ] ++ modules ++ [ sops-nix.nixosModules.sops inputs.stylix.nixosModules.stylix home-manager.nixosModules.home-manager - homeManagerModule + (mkHomeManagerModule hostConfig) ]; }; + pkgsUnfree = mkPkgs defaultSystem; in { - formatter.${system} = pkgsUnfree.nixpkgs-fmt; + formatter.${defaultSystem} = pkgsUnfree.nixpkgs-fmt; - apps.${system} = { + apps.${defaultSystem} = { # `nix run .#switch [host]` — build and activate the config for the # given host (defaults to the current hostname) via nh. Runs against # the working tree (`.`) so uncommitted edits are picked up. @@ -106,7 +133,7 @@ }; }; - checks.${system} = { + checks.${defaultSystem} = { format = pkgsUnfree.runCommand "nixos-format-check" { nativeBuildInputs = [ pkgsUnfree.nixpkgs-fmt ]; @@ -194,22 +221,6 @@ }; }; - nixosConfigurations = { - machine = mkHost [ - ./misc/config.nix - ./modules/default.nix - ./server/default.nix - ]; - - vostro = mkHost [ - ./hosts/vostro/default.nix - ./modules/default.nix - ]; - - nixos-wsl = mkHost [ - inputs.nixos-wsl.nixosModules.default - ./hosts/nixos-wsl/default.nix - ]; - }; + nixosConfigurations = nixpkgs.lib.mapAttrs mkHost hosts; }; } diff --git a/home-manager/fuzzel.nix b/home-manager/fuzzel.nix index ab0465c..6673a73 100644 --- a/home-manager/fuzzel.nix +++ b/home-manager/fuzzel.nix @@ -65,6 +65,7 @@ "app Vesktop" \ "app Telegram" \ "app Bitwarden" \ + "app Spotify" \ "app Settings" \ "app All apps" \ "dev NixOS flake in Zed" \ @@ -105,6 +106,7 @@ "app Vesktop") exec vesktop ;; "app Telegram") exec telegram-desktop ;; "app Bitwarden") exec bitwarden ;; + "app Spotify") run_shell 'spotify_player' ;; "app Settings") exec xfce4-settings-manager ;; "app All apps") exec "$fuzzel" ;; "dev NixOS flake in Zed") exec zeditor "$HOME/.nixos" ;; diff --git a/home-manager/home.nix b/home-manager/home.nix index 19e7357..4322607 100644 --- a/home-manager/home.nix +++ b/home-manager/home.nix @@ -1,11 +1,12 @@ -{ config, inputs, lib, osConfig, pkgs, ... }: +{ config, hostConfig, inputs, lib, pkgs, ... }: let - isWsl = osConfig.networking.hostName == "nixos-wsl"; + isWsl = hostConfig.hostName == "nixos-wsl"; + flakeDir = "${hostConfig.user.home}/.nixos"; in { - home.username = "declan"; - home.homeDirectory = "/home/declan"; + home.username = hostConfig.user.name; + home.homeDirectory = hostConfig.user.home; home.stateVersion = "24.05"; home.packages = [ @@ -74,7 +75,8 @@ in cc = "claude --dangerously-skip-permissions"; codex = "codex --yolo"; ranger = "y"; - switch = "/home/declan/.nixos/scripts/switch.sh"; + switch = "${flakeDir}/scripts/switch.sh"; + update-ai = "cd ${flakeDir} && nix flake update nixpkgs-codex && ${flakeDir}/scripts/switch.sh"; yazi = "y"; }; }; @@ -103,6 +105,7 @@ in programs.nixvim = { enable = true; + nixpkgs.source = inputs.nixpkgs; imports = [ ../nixvim/config/default.nix ]; }; } diff --git a/home-manager/hyprland.nix b/home-manager/hyprland.nix index c8b1f10..b8c7c36 100644 --- a/home-manager/hyprland.nix +++ b/home-manager/hyprland.nix @@ -1,4 +1,4 @@ -{ inputs, lib, pkgs, ... }: +{ hostConfig, inputs, lib, pkgs, ... }: let zedThreadRunner = inputs.zed-thread-tui.packages.${pkgs.stdenv.hostPlatform.system}.default; @@ -78,7 +78,7 @@ in #!${pkgs.bash}/bin/bash set -euo pipefail - wall_dir="/home/declan/.nixos/wall" + wall_dir="${hostConfig.user.home}/.nixos/wall" state_dir="''${XDG_RUNTIME_DIR:-/tmp}/hyprpaper" config="$state_dir/hyprpaper.conf" wallpaper="$wall_dir/berserk.jpg" @@ -168,7 +168,7 @@ in address=$address port=$port enable_auth=true - username=declan + username=${hostConfig.user.name} password=$(cat "$password_file") private_key_file=$tls_key certificate_file=$tls_cert diff --git a/hosts/nixos-wsl/default.nix b/hosts/nixos-wsl/default.nix index 649999f..af3af03 100644 --- a/hosts/nixos-wsl/default.nix +++ b/hosts/nixos-wsl/default.nix @@ -1,4 +1,4 @@ -{ pkgs, pkgsCodex, ... }: +{ hostConfig, pkgs, pkgsCodex, ... }: { imports = [ @@ -9,11 +9,9 @@ system.stateVersion = "25.11"; nix.settings.experimental-features = [ "nix-command" "flakes" ]; - #networking.hostName = lib.mkForce "nixos-wsl"; - wsl = { enable = true; - defaultUser = "declan"; + defaultUser = hostConfig.user.name; useWindowsDriver = true; startMenuLaunchers = false; wslConf = { @@ -92,6 +90,6 @@ services.openssh.enable = true; programs.ssh.startAgent = true; - users.users.declan.linger = true; + users.users.${hostConfig.user.name}.linger = true; users.users.root.shell = pkgs.zsh; } diff --git a/hosts/vostro/default.nix b/hosts/vostro/default.nix index 2935257..c97c3cf 100644 --- a/hosts/vostro/default.nix +++ b/hosts/vostro/default.nix @@ -1,4 +1,4 @@ -{ lib, pkgs, ... }: +{ hostConfig, pkgs, ... }: { imports = [ @@ -16,9 +16,7 @@ "flakes" ]; - networking.hostName = lib.mkForce "vostro"; - - users.users.declan.openssh.authorizedKeys.keys = [ + users.users.${hostConfig.user.name}.openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM6L3iyvr7PKLMkieNUsDVuywKC3xP12uobeMo5L8chv declan@declan-NucBox-K7-PLUS" ]; diff --git a/hosts/vostro/hardware-configuration.nix b/hosts/vostro/hardware-configuration.nix index 7b669cf..442d416 100644 --- a/hosts/vostro/hardware-configuration.nix +++ b/hosts/vostro/hardware-configuration.nix @@ -30,7 +30,7 @@ lib.mkDefault config.hardware.enableRedistributableFirmware; networking.useDHCP = lib.mkDefault true; - networking.firewall.allowedTCPPorts = [ 22 4321 5432 5900 ]; + networking.firewall.allowedTCPPorts = [ 22 4321 5432 5900 3000 8080]; networking.firewall.allowedTCPPortRanges = [ { from = 5000; diff --git a/hosts/vostro/networking.nix b/hosts/vostro/networking.nix index fe95c07..fadc322 100644 --- a/hosts/vostro/networking.nix +++ b/hosts/vostro/networking.nix @@ -1,9 +1,15 @@ -{ config, lib, pkgs, ... }: +{ config, hostConfig, lib, pkgs, ... }: { networking.firewall.allowedTCPPorts = [ 443 80 3000 5173 ]; networking.firewall.allowedUDPPorts = [ 51820 ]; + # This network hands out a link-local IPv6 address but has no IPv6 route, + # so DNS returns AAAA records that glibc/browsers try first and hang on + # until they time out (the "hard-refresh every tab" symptom). IPv6 is + # unroutable here, so disable it on this host and force IPv4-only. + networking.enableIPv6 = false; + networking.networkmanager.plugins = with pkgs; [ networkmanager-openvpn ]; @@ -29,7 +35,9 @@ peers = [ { publicKey = "SL8XRNCJc4iKT3VE2p7zwoL0+FPKMS+dJzaWGvjeozE="; - allowedIPs = [ "::/0" ]; + # IPv4 full tunnel. Was "::/0" (IPv6-only) which routed no IPv4 traffic + # and would fail to add its route now that IPv6 is disabled on this host. + allowedIPs = [ "0.0.0.0/0" ]; endpoint = "office.revo.in.na:13231"; persistentKeepalive = 10; } @@ -41,12 +49,12 @@ # only comes up when toggled on demand from Waybar. systemd.targets."wireguard-wg0".wantedBy = lib.mkForce [ ]; - # Allow declan to toggle the VPN from Waybar without a password prompt. + # Allow the primary user to toggle the VPN from Waybar without a password prompt. # Starting/stopping wireguard-wg0.service brings the peer up and down too, # because the peer unit both Requires and is WantedBy the interface service. security.sudo.extraRules = [ { - users = [ "declan" ]; + users = [ hostConfig.user.name ]; commands = [ { command = "/run/current-system/sw/bin/systemctl start wireguard-wg0.service"; options = [ "NOPASSWD" ]; } { command = "/run/current-system/sw/bin/systemctl stop wireguard-wg0.service"; options = [ "NOPASSWD" ]; } @@ -60,7 +68,7 @@ dumpcap.enable = true; }; - users.users.declan.extraGroups = [ "wireshark" ]; + users.users.${hostConfig.user.name}.extraGroups = [ "wireshark" ]; environment.systemPackages = with pkgs; [ wireguard-tools diff --git a/modules/ai-auto-update.nix b/modules/ai-auto-update.nix new file mode 100644 index 0000000..b5ace6e --- /dev/null +++ b/modules/ai-auto-update.nix @@ -0,0 +1,37 @@ +{ hostConfig, pkgs, ... }: + +let + inherit (hostConfig) hostName; + inherit (hostConfig.user) home name; + flakeDir = "${home}/.nixos"; + aiAutoUpdate = pkgs.writeShellScript "ai-auto-update" '' + set -euo pipefail + cd ${flakeDir} + ${pkgs.util-linux}/bin/runuser -u ${name} -- \ + env HOME=${home} ${pkgs.nix}/bin/nix flake update nixpkgs-codex + ${pkgs.nixos-rebuild}/bin/nixos-rebuild switch \ + --flake "path:${flakeDir}#${hostName}" + ''; +in +{ + systemd.services.ai-auto-update = { + description = "Auto-update Codex from nixpkgs master"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = aiAutoUpdate; + }; + }; + + systemd.timers.ai-auto-update = { + description = "Periodically auto-update AI tools"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "5min"; + OnUnitActiveSec = "24h"; + Persistent = true; + RandomizedDelaySec = "10m"; + }; + }; +} diff --git a/modules/memory.nix b/modules/memory.nix new file mode 100644 index 0000000..d6da36b --- /dev/null +++ b/modules/memory.nix @@ -0,0 +1,45 @@ +{ lib, ... }: + +{ + # 8 GiB RAM with zero swap => the kernel OOM-killer fires during + # compile/link steps and takes the session down. Three layers here: + # compressed RAM swap, a real disk swapfile, and bounded build fan-out. + + zramSwap = { + enable = true; + algorithm = "zstd"; + memoryPercent = 50; # ~3.8 GiB of RAM backing ~10 GiB compressed + priority = 100; # preferred over the disk swapfile + }; + + swapDevices = lib.mkForce [{ + device = "/var/lib/swapfile"; + size = 8 * 1024; # MiB + priority = 10; # spillover once zram is saturated + }]; + + boot.kernel.sysctl = { + # zram is cheap to page into, so lean on it early instead of letting + # the working set grow until the OOM-killer has no choice. + "vm.swappiness" = 180; + "vm.watermark_boost_factor" = 0; + "vm.watermark_scale_factor" = 125; + "vm.page-cluster" = 0; + }; + + # Bound peak build memory. 8 parallel nix jobs each spawning 8 compiler + # processes is what actually exhausts RAM; 2 x 4 keeps the box usable. + nix.settings = { + max-jobs = 2; + cores = 4; + }; + + # When pressure does spike, kill the offending cgroup deliberately rather + # than letting the kernel OOM-killer pick a random victim (often the + # compositor, which is what reads as a "PC crash"). + systemd.oomd = { + enable = true; + enableRootSlice = true; + enableUserSlices = true; + }; +} diff --git a/modules/packages/core.nix b/modules/packages/core.nix index abb2bee..adf300e 100644 --- a/modules/packages/core.nix +++ b/modules/packages/core.nix @@ -45,5 +45,10 @@ fcitx5 gnome-disk-utility dpkg + + nodejs_24 + python3 + gnumake + gcc ]; } diff --git a/modules/secrets.nix b/modules/secrets.nix index b50b2f4..94e7543 100644 --- a/modules/secrets.nix +++ b/modules/secrets.nix @@ -1,12 +1,15 @@ -{ config, ... }: +{ config, hostConfig, ... }: +let + passwordSecret = "${hostConfig.user.name}-password"; +in { sops = { defaultSopsFile = ../secrets/default.yaml; age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; secrets = { - "declan-password".neededForUsers = true; + ${passwordSecret}.neededForUsers = true; "root-password".neededForUsers = true; "wireguard-private-key" = { sopsFile = ../secrets/wireguard.yaml; @@ -15,7 +18,7 @@ }; users.users = { - declan.hashedPasswordFile = config.sops.secrets."declan-password".path; + ${hostConfig.user.name}.hashedPasswordFile = config.sops.secrets.${passwordSecret}.path; root.hashedPasswordFile = config.sops.secrets."root-password".path; }; } diff --git a/modules/system-settings.nix b/modules/system-settings.nix index 36f4314..991ee1e 100644 --- a/modules/system-settings.nix +++ b/modules/system-settings.nix @@ -1,4 +1,4 @@ -{ lib, pkgs, ... }: { +{ hostConfig, lib, pkgs, ... }: { #systemd bootloader boot.loader = { @@ -10,7 +10,6 @@ networking = { networkmanager.enable = true; - hostName = "machine"; }; ##ssh @@ -69,7 +68,7 @@ settings = { experimental-features = [ "nix-command" "flakes" ]; - trusted-users = [ "root" "declan" ]; + trusted-users = [ "root" hostConfig.user.name ]; auto-optimise-store = true; substituters = [ "https://cache.nixos.org" @@ -89,14 +88,4 @@ systemd.services.nix-gc.preStart = '' ${pkgs.nix}/bin/nix-env --profile /nix/var/nix/profiles/system --delete-generations +6 ''; - - system = { - autoUpgrade = { - enable = true; - operation = "boot"; - flake = "github:DeclanRiedel/nixos-flake"; - dates = "weekly"; - }; - - }; } diff --git a/modules/tmux.nix b/modules/tmux.nix index c922448..1899dea 100644 --- a/modules/tmux.nix +++ b/modules/tmux.nix @@ -1,21 +1,33 @@ -{ pkgs, ... }: +{ hostConfig, pkgs, ... }: let + resurrectDir = "${hostConfig.user.home}/.local/state/tmux/resurrect"; + resurrectScripts = "${pkgs.tmuxPlugins.resurrect}/share/tmux-plugins/resurrect/scripts"; + tmuxStart = pkgs.writeShellScript "tmux-start" '' set -euo pipefail - mkdir -p "$HOME/.local/state/tmux/resurrect" + mkdir -p "${resurrectDir}" + # Ensure a server + fallback session first: this sources the config so + # @resurrect-dir and friends are set before we restore. Guarantees + # `tmux attach` always succeeds even if the restore below finds nothing. if ! ${pkgs.tmux}/bin/tmux has-session 2>/dev/null; then ${pkgs.tmux}/bin/tmux new-session -d -s main fi + + # Deterministic restore (continuum-restore is off so this is the only path). + # Layers the saved sessions on top of the fallback `main`. + if [ -e "${resurrectDir}/last" ]; then + ${pkgs.tmux}/bin/tmux run-shell "${resurrectScripts}/restore.sh" + fi ''; tmuxSave = pkgs.writeShellScript "tmux-save" '' set -euo pipefail if ${pkgs.tmux}/bin/tmux has-session 2>/dev/null; then - ${pkgs.tmux}/bin/tmux run-shell "${pkgs.tmuxPlugins.resurrect}/share/tmux-plugins/resurrect/scripts/save.sh quiet" + ${pkgs.tmux}/bin/tmux run-shell "${resurrectScripts}/save.sh quiet" fi ''; in @@ -55,8 +67,8 @@ in set -g status on set -g @continuum-save-interval '5' - set -g @continuum-restore 'on' - set -g @resurrect-dir '$HOME/.local/state/tmux/resurrect' + set -g @continuum-restore 'off' + set -g @resurrect-dir '${resurrectDir}' set -g @resurrect-capture-pane-contents 'on' set -g @resurrect-strategy-vim 'session' set -g @resurrect-strategy-nvim 'session' diff --git a/modules/user-settings.nix b/modules/user-settings.nix index fffb3e6..3205306 100644 --- a/modules/user-settings.nix +++ b/modules/user-settings.nix @@ -1,7 +1,7 @@ -{ pkgs, ... }: { - users.users.declan = { +{ hostConfig, pkgs, ... }: { + users.users.${hostConfig.user.name} = { isNormalUser = true; - home = "/home/declan"; + home = hostConfig.user.home; extraGroups = [ "wheel" "networkmanager" "audio" "video" "input" "render" "seat" "docker" "dialout" ]; uid = 1000; shell = pkgs.zsh; diff --git a/modules/zsh.nix b/modules/zsh.nix index 5d42e93..ef6bc1e 100644 --- a/modules/zsh.nix +++ b/modules/zsh.nix @@ -1,3 +1,8 @@ +{ hostConfig, ... }: + +let + flakeDir = "${hostConfig.user.home}/.nixos"; +in { programs = { zsh = { @@ -24,8 +29,8 @@ lsd = "eza -g -D -T"; lsgit = "eza --git-ignore -T --icons"; cat = "bat"; - switch = "/home/declan/.nixos/scripts/switch.sh"; - update = "/home/declan/.nixos/scripts/switch.sh vostro"; + switch = "${flakeDir}/scripts/switch.sh"; + update = "${flakeDir}/scripts/switch.sh ${hostConfig.hostName}"; c = "clear"; n = "nvim"; vim = "nvim";