# Common.nix { config, pkgs, lib, inputs, ... }: let isMacbook = config.networking.hostName == "FredOS-Macbook"; # nom's Haskell renderer measured 91% CPU during a rebuild — a full core of # the Macbook's four, spent drawing a progress tree, while nix itself wanted # 146%. Plain `-L` output there; the other hosts can afford the pretty one. buildLog = lib.optionalString (!isMacbook) " --log-format internal-json 2>&1 | nom --json"; flake = "git+https://forg.gregersen.it/rope/nixos"; # Now that the clients only copy, the copy is the whole wait. nix's own # --log-format bar prints counters but no gauge, so render one from the json # log: activity type 103 is the top-level copy-paths, and its type-105 # progress results carry [done, expected, running, failed] path counts. # Messages at warning level and above still go through to stderr — otherwise # folding nix's stderr into the filter would swallow the errors too. copyBar = pkgs.writeShellScriptBin "nix-copy-bar" '' set -o pipefail ${pkgs.jq}/bin/jq -Rr --unbuffered ' ltrimstr("@nix ") | fromjson? // empty | if .action == "start" and .type == 103 then "A \(.id)" elif .action == "result" and .type == 105 then "P \(.id) \(.fields[0]) \(.fields[1])" elif .action == "msg" and .level <= 2 then "M \(.msg)" else empty end' | { blocks='########################################' drawn= total=0 while read -r tag rest; do case "$tag" in A) copy_id=$rest ;; M) [ -n "$drawn" ] && printf '\n' && drawn= printf '%s\n' "$rest" >&2 ;; P) set -- $rest [ "$1" = "''${copy_id:-}" ] && [ "''${3:-0}" -gt 0 ] || continue total=$3 pct=$(( $2 * 100 / $3 )) printf '\r[%-40s] %3d%% %d/%d paths' \ "''${blocks:0:$(( pct * 40 / 100 ))}" "$pct" "$2" "$3" drawn=1 ;; esac done # `if`, not `[ … ] && …`: with nothing to copy the bar never draws, and a # bare test as the last statement would exit 1 and break the caller's # && chain. A real copy failure still surfaces via the caller's pipefail. # # nix stops emitting type-105 results once the last path lands, so the # bar's last live frame is always one path short. Redraw it full on EOF. # An error would have cleared $drawn via the M branch first, so this only # fires on a copy that actually finished. if [ -n "$drawn" ]; then printf '\r[%-40s] 100%% %d/%d paths\n' "$blocks" "$total" "$total" fi } ''; post = "nvd diff $OLD_SYSTEM /run/current-system && (command -v record-update &>/dev/null && record-update $OLD_SYSTEM /run/current-system || true); rm -f \"$HOME\"/Pictures/Screenshots/*"; # The server evaluates and builds for itself. localUpdate = "bash -c 'set -o pipefail && OLD_SYSTEM=$(readlink /run/current-system) && sudo nixos-rebuild switch $@ --refresh --flake ${flake} -L${buildLog} && ${post}' --"; # The clients offload the *evaluation* too, not just the build. nixos-rebuild # always evaluates on the machine it runs on (--build-host ships the # derivation, not the expression), so the only way to move eval off a client # is to run nix on the server over SSH and hand the resulting closure to # --store-path, which skips eval and build entirely. Same lantian substituter # as services/prebuild.nix, and --out-link keeps a gc root on the server so # `clean` there can't reap the closure mid-copy. SSH and the copy run as # fred, not root: root has no key on the server, and require-sigs = false # below is a daemon setting, so the daemon takes the unsigned closure from an # untrusted user just fine. Only the activation needs sudo. remoteUpdate = "bash -c 'set -o pipefail && OLD_SYSTEM=$(readlink /run/current-system) && NEW=$(ssh fred@nordhammer.it \"nix build --refresh --print-out-paths --out-link prebuild-${config.networking.hostName} --option extra-substituters https://attic.xuyh0120.win/lantian --option extra-trusted-public-keys lantian:EeAUQ+W+6r7EtwnmYjeVwx5kOGEBpjlBfPlzGlTNvHc= ${flake}#nixosConfigurations.${config.networking.hostName}.config.system.build.toplevel\") && nix copy --from ssh://fred@nordhammer.it $NEW --log-format internal-json 2>&1 | ${copyBar}/bin/nix-copy-bar && sudo nixos-rebuild switch --no-reexec --store-path $NEW && ${post}'"; in { imports = [ # Host modules are imported per-host by mkHost in flake.nix. # Generic settings # ./settings/desktop.nix ./settings/hyprland.nix ./settings/quickshell.nix ./settings/theming.nix ./settings/locale.nix ./settings/audio.nix ./settings/users.nix ./settings/shell.nix ./apps/zen.nix # Services # ./services/server-permissions.nix ./services/game-servers.nix ./services/pelican.nix # ./services/dr-server.nix ./services/qbittorrent-nox.nix ./services/nginx.nix ./services/go2rtc.nix # ./services/frigate.nix ./services/sonarr.nix ./services/radarr.nix ./services/prowlarr.nix ./services/jellyfin.nix ./services/seerr.nix ./services/bazarr.nix ./services/bazarr-sync.nix ./services/cloudflare-ddns.nix ./services/authelia.nix ./services/homepage.nix ./services/arr-interconnect.nix ./services/profilarr.nix ./services/shelfarr.nix ./services/adguard.nix ./services/router.nix ./services/router-ui.nix ./services/crowdsec.nix ./services/service-health.nix ./services/sabnzbd.nix ./services/forgejo-runner.nix ./services/code-server.nix ./services/memos.nix ./services/hardware-health.nix ./services/wan-watchdog.nix ./services/prebuild.nix ]; ### Make build time quicker documentation.nixos.enable = false; # Home Manager # home-manager.useGlobalPkgs = true; home-manager.useUserPackages = true; home-manager.backupFileExtension = "hm-bak"; home-manager.extraSpecialArgs = { inherit inputs; }; home-manager.users.fred = import ./home-manager/fred.nix; ############################################################################# # Kill all user processes on logout so systemd user services don't linger # in broken states across sessions (e.g. waybar failing to start on re-login). services.logind.settings.Login.KillUserProcesses = true; # Shorten shutdown stop timeout to avoid long "stop job" waits systemd.settings.Manager.DefaultTimeoutStopSec = "10s"; # Make boot time quicker boot.loader.timeout = lib.mkDefault 5; systemd.services.NetworkManager-wait-online.enable = false; systemd.services.systemd-udev-settle.enable = false; systemd.services.firewall = { wantedBy = lib.mkForce [ ]; after = [ "multi-user.target" ]; }; boot.initrd.verbose = false; ############################################################################# # Compressed in-memory swap as a safety net during local build storms. # Cheap when idle; without it a transient OOM during an uncached build # can stall AdGuard/Jellyfin to the point of freezing the box. zramSwap = { enable = true; memoryPercent = 50; }; # Use latest kernel boot.kernelPackages = pkgs.linuxPackages_latest; # Allow unfree packages nixpkgs.config.allowUnfree = true; # vesktop (multiple hosts) builds with pnpm via fetchPnpmDeps, which nixpkgs # marks insecure (build-time only, hash-pinned FOD — not in PATH). broadcom-sta # is Macbook-only Wi-Fi but allowing it everywhere is harmless (absent on others). nixpkgs.config.allowInsecurePredicate = pkg: lib.any (p: lib.hasPrefix p (lib.getName pkg)) [ "broadcom-sta" "pnpm" ]; # Flakes — nixos-rebuild self-enables these, but plain `nix eval` / # `nix flake check` on the hosts need them too. nix.settings.experimental-features = [ "nix-command" "flakes" ]; # Enable network-manager networking.networkmanager.enable = true; # Fish shell programs.fish.enable = true; users.defaultUserShell = pkgs.fish; # Shell aliases (work in both bash and fish) environment.shellAliases = { update = if config.networking.hostName == "FredOS-Mediaserver" then localUpdate else remoteUpdate; # Fallback for when the server is down or the alias itself changed and the # live generation still has the old one: eval and build right here. update2 = localUpdate; clean = "sudo nix-collect-garbage -d"; # Throw away the VM's disk first so every run is a real first boot — # greeter, Plasma first-run, the lot. Builds into ./result in $PWD. kaylavm = "rm -f FredOS-Kayla.qcow2 && nixos-rebuild build-vm --refresh --flake git+https://forg.gregersen.it/rope/nixos#kayla-vm && ./result/bin/run-FredOS-Kayla-vm"; ll = "ls -alh"; clear = "command clear"; reboot = "bash -c 'if [ \"$(hostname)\" = \"FredOS-Mediaserver\" ]; then read -r -p \"Reboot $(hostname)? [y/N] \" confirm; case \"$confirm\" in [Yy]) ;; *) exit 0 ;; esac; fi; sudo systemctl reboot'"; }; # Add packages environment.systemPackages = with pkgs; [ git localsend nvd nix-output-monitor jq dnsutils busybox ]; # Hard-link identical files in the store as new paths are added, so the # store de-duplicates itself on every build instead of drifting. # Off on the Macbook: it hashes and relinks every file the daemon writes, # which lands on top of the substitution writes on a slow SATA blade — # measured io pressure full avg60=57% during a rebuild. Run `nix store # optimise` by hand there if the store ever gets fat. nix.settings.auto-optimise-store = !isMacbook; # Offload builds to the media server. Excluded on the server itself to # avoid a pointless SSH round-trip to localhost. nix.distributedBuilds = lib.mkIf (config.networking.hostName != "FredOS-Mediaserver") true; nix.buildMachines = lib.mkIf (config.networking.hostName != "FredOS-Mediaserver") [{ hostName = "nordhammer.it"; systems = [ "x86_64-linux" "i686-linux" ]; sshUser = "fred"; sshKey = "/root/.ssh/id_ed25519"; maxJobs = 4; supportedFeatures = [ "nixos-test" "benchmark" "big-parallel" "kvm" ]; }]; # nix.buildMachines runs from the daemon, i.e. as root, so it needs root's own # key — fred's doesn't count, and ssh refuses a key file owned by another user # outright. Copying fred's (root can read it anyway, so this leaks nothing new) # beats committing a private key to a repo the hosts fetch over plain HTTPS. # C+ re-copies on every activation, so rotating fred's key rotates root's too. systemd.tmpfiles.rules = lib.mkIf (lib.elem config.networking.hostName [ "FredOS-Gaming" "FredOS-Macbook" ]) [ "d /root/.ssh 0700 root root -" "C+ /root/.ssh/id_ed25519 0600 root root - /home/fred/.ssh/id_ed25519" ]; # Accept unsigned paths copied back from the remote builder. nix.extraOptions = lib.mkIf (config.networking.hostName != "FredOS-Mediaserver") '' require-sigs = false ''; }