nixos/common.nix
rope d4b76f2b13 common: finish the copy bar at 100%
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:51:40 +01:00

257 lines
11 KiB
Nix

# 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)";
# 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/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
'';
}