Compare commits
23
Commits
e556cb31af
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9abc47a8e1 | ||
|
|
d63409dad8 | ||
|
|
66f9e08617 | ||
|
|
36b1f78886 | ||
|
|
15be29ba2a | ||
|
|
eab86bebc7 | ||
|
|
a0d9135695 | ||
|
|
d7894065f7 | ||
|
|
6a66db0b20 | ||
|
|
46271e24bb | ||
|
|
63c1037c85 | ||
|
|
64cbb91aac | ||
|
|
3220ab4300 | ||
|
|
97b5f89182 | ||
|
|
61c36b6eef | ||
|
|
91332d7e16 | ||
|
|
a009a4d984 | ||
|
|
f8552bc7bb | ||
|
|
29707ba0f6 | ||
|
|
67bafd4f73 | ||
|
|
b7e6418fa6 | ||
|
|
fc1772e5db | ||
|
|
42c805f430 |
@@ -1 +1,6 @@
|
||||
.session
|
||||
|
||||
# `nix build` / `nixos-rebuild build` output symlink. Points into /nix/store and
|
||||
# churns on every build, so it must not be tracked.
|
||||
result
|
||||
result-*
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
self,
|
||||
moduleWithSystem,
|
||||
...
|
||||
}: {
|
||||
flake.nixosModules.cad = moduleWithSystem ({...}: let
|
||||
modules = with self.nixosModules; [
|
||||
freecad
|
||||
openscad
|
||||
prusa-slicer
|
||||
];
|
||||
in {
|
||||
imports = modules;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
self,
|
||||
moduleWithSystem,
|
||||
...
|
||||
}: {
|
||||
flake.nixosModules.netops = moduleWithSystem ({...}: let
|
||||
modules = with self.nixosModules; [
|
||||
winbox
|
||||
unraid-usb-creator
|
||||
];
|
||||
in {
|
||||
imports = modules;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{...}: {
|
||||
# Screenshot capture, region selection, annotation, and clipboard support for
|
||||
# the Hyprland screenshot keybinds managed in the dotfiles repo.
|
||||
flake.nixosModules.freecad = {pkgs, ...}: {
|
||||
environment.systemPackages = with pkgs; [
|
||||
freecad
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
ghostty
|
||||
waybar
|
||||
quickshell
|
||||
screenshot
|
||||
];
|
||||
in {
|
||||
imports = modules;
|
||||
@@ -37,20 +38,94 @@
|
||||
# are declared in the host's disks.nix instead.
|
||||
services.udisks2.enable = true;
|
||||
|
||||
# Run udiskie as a user unit rather than from the hypr autostart — it was
|
||||
# previously only installed, never actually launched. Binding it to
|
||||
# graphical-session.target (which UWSM manages properly) means it starts and
|
||||
# stops with the session instead of leaking across logins.
|
||||
systemd.user.services.udiskie = {
|
||||
description = "udiskie removable media automounter";
|
||||
partOf = ["graphical-session.target"];
|
||||
after = ["graphical-session.target"];
|
||||
wantedBy = ["graphical-session.target"];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
# -s = smart tray: the icon appears only while removable media is present.
|
||||
ExecStart = "${pkgs.udiskie}/bin/udiskie -s";
|
||||
Restart = "on-failure";
|
||||
# Session daemons run as systemd user units bound to graphical-session.target
|
||||
# (which UWSM manages properly) rather than from the hypr autostart, so they
|
||||
# start and stop with the session instead of leaking across logins.
|
||||
#
|
||||
# hypridle and hyprsunset both ship their own unit, but in
|
||||
# $out/share/systemd/user — NOT $out/lib/systemd/user. That distinction is
|
||||
# why they need redeclaring rather than just enabling:
|
||||
#
|
||||
# * `systemd.packages = [pkgs.hypridle]` would collect nothing. nixpkgs'
|
||||
# generateUnits globs only $pkg/etc/systemd/user and $pkg/lib/systemd/user.
|
||||
# * The units are nonetheless *visible* to the user manager, via
|
||||
# environment.pathsToLink -> /run/current-system/sw/share/systemd/user,
|
||||
# which is on the search path because XDG_DATA_DIRS ends with
|
||||
# /run/current-system/sw/share. But visible is not enabled: nothing
|
||||
# creates the WantedBy symlink, so they sit unstarted.
|
||||
# * `systemd.user.services.<n>.wantedBy` alone would not help either — with
|
||||
# no other keys NixOS synthesises a *replacement* unit with an empty
|
||||
# [Service] section, shadowing upstream's via /etc/systemd/user (which
|
||||
# takes precedence over $XDG_DATA_DIRS).
|
||||
#
|
||||
# So the full definition below is deliberate. It is close to upstream's but
|
||||
# not identical: hyprsunset's `Requires=graphical-session.target` is dropped
|
||||
# on purpose (PartOf already propagates the stop, while Requires would also
|
||||
# make starting hyprsunset by hand pull up the whole session target), and
|
||||
# hypridle gains an explicit `path` because its config shells out.
|
||||
systemd.user.services = {
|
||||
udiskie = {
|
||||
description = "udiskie removable media automounter";
|
||||
partOf = ["graphical-session.target"];
|
||||
after = ["graphical-session.target"];
|
||||
wantedBy = ["graphical-session.target"];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
# -s = smart tray: the icon appears only while removable media is present.
|
||||
ExecStart = "${pkgs.udiskie}/bin/udiskie -s";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
|
||||
hypridle = {
|
||||
description = "Hyprland idle daemon";
|
||||
documentation = ["https://wiki.hyprland.org/Hypr-Ecosystem/hypridle"];
|
||||
partOf = ["graphical-session.target"];
|
||||
after = ["graphical-session.target"];
|
||||
wantedBy = ["graphical-session.target"];
|
||||
# ~/.config/hypr/hypridle.conf calls out to these: hyprlock (lock_cmd),
|
||||
# pidof (the `pidof hyprlock ||` guard), brightnessctl (backlight
|
||||
# listener), hyprctl (dpms), loginctl + systemctl (sleep/lock),
|
||||
# notify-send (unlock_cmd).
|
||||
#
|
||||
# MAINTENANCE HAZARD: that config lives in a *different* repo (the stow'd
|
||||
# `hypr` package in the dotfiles repo), so this list can silently drift
|
||||
# out of sync with it. A binary used there but missing here fails as a
|
||||
# bare "command not found" inside hypridle, and Restart=on-failure will
|
||||
# NOT catch it because hypridle itself stays up — the idle action just
|
||||
# quietly stops working. Add any new binary to both places.
|
||||
path = with pkgs; [
|
||||
hyprlock
|
||||
hyprland
|
||||
brightnessctl
|
||||
libnotify
|
||||
procps
|
||||
systemd
|
||||
];
|
||||
unitConfig.ConditionEnvironment = "WAYLAND_DISPLAY";
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${pkgs.hypridle}/bin/hypridle";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
|
||||
hyprsunset = {
|
||||
description = "Hyprland blue-light filter";
|
||||
documentation = ["https://wiki.hyprland.org/Hypr-Ecosystem/hyprsunset"];
|
||||
partOf = ["graphical-session.target"];
|
||||
after = ["graphical-session.target"];
|
||||
wantedBy = ["graphical-session.target"];
|
||||
unitConfig.ConditionEnvironment = "WAYLAND_DISPLAY";
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${pkgs.hyprsunset}/bin/hyprsunset";
|
||||
Restart = "on-failure";
|
||||
# Intentionally only on hyprsunset, not hypridle/udiskie: upstream's
|
||||
# hyprsunset.service sets Slice=session.slice and upstream's
|
||||
# hypridle.service does not. Not an oversight — don't "fix" it.
|
||||
Slice = "session.slice";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -74,7 +149,6 @@
|
||||
udiskie
|
||||
brightnessctl
|
||||
# clipboard
|
||||
wl-clipboard
|
||||
cliphist
|
||||
# theming
|
||||
gsettings-desktop-schemas
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{...}: {
|
||||
flake.nixosModules.moonlight = {pkgs, ...}: {
|
||||
environment.systemPackages = with pkgs; [
|
||||
moonlight-qt
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{...}: {
|
||||
# Nextcloud desktop sync client. Account and sync settings remain per-user.
|
||||
flake.nixosModules.nextcloud = {pkgs, ...}: {
|
||||
environment.systemPackages = with pkgs; [
|
||||
nextcloud-client
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{...}: {
|
||||
# Screenshot capture, region selection, annotation, and clipboard support for
|
||||
# the Hyprland screenshot keybinds managed in the dotfiles repo.
|
||||
flake.nixosModules.openscad = {pkgs, ...}: {
|
||||
environment.systemPackages = with pkgs; [
|
||||
openscad
|
||||
openscad-lsp
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{...}: {
|
||||
# CUPS printing + eSCL scanning for the networked EPSON ET-8550.
|
||||
#
|
||||
# Two queues, deliberately:
|
||||
# EPSON_ET_8550 driverless / IPP Everywhere. The default. PDF is
|
||||
# passed straight through and the printer's own RIP
|
||||
# does the work. 51 page sizes, 26 borderless, and the
|
||||
# only queue that can print CD/DVDs (MediaType Disc).
|
||||
# EPSON_ET_8550_Photo Epson ESC/P-R. Host rasterises to ESC/P-R at 360dpi.
|
||||
# 34 sizes, 12 borderless, no disc support - but it is
|
||||
# the only way to reach Epson's named media/quality
|
||||
# presets ("Epson Ultra Glossy-Max Quality" etc) that
|
||||
# IPP Everywhere cannot express. Kept for photo A/B.
|
||||
#
|
||||
# Note both PPDs are 360x360 HWResolution - escpr2 is not higher resolution.
|
||||
#
|
||||
# Both queues use the mDNS name, not 10.10.20.62, so a DHCP lease change
|
||||
# doesn't break them. services.avahi.nssmdns4 is what resolves it.
|
||||
# Port 9100 is closed on this printer, so socket:// is not an option.
|
||||
#
|
||||
# cups-browsed is off: it would auto-create a third duplicate queue for the
|
||||
# same printer. avahi stays on for mDNS resolution and scanner discovery.
|
||||
flake.nixosModules.printing = {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
programs.system-config-printer.enable = true;
|
||||
|
||||
services.avahi = {
|
||||
enable = true;
|
||||
nssmdns4 = true;
|
||||
};
|
||||
|
||||
services.printing = {
|
||||
enable = true;
|
||||
browsed.enable = false;
|
||||
drivers = with pkgs; [
|
||||
epson-escpr2
|
||||
];
|
||||
};
|
||||
|
||||
hardware.printers = {
|
||||
ensureDefaultPrinter = "EPSON_ET_8550";
|
||||
ensurePrinters = [
|
||||
{
|
||||
name = "EPSON_ET_8550";
|
||||
description = "EPSON ET-8550 (driverless)";
|
||||
location = "Network";
|
||||
deviceUri = "ipp://EPSONA70973.local:631/ipp/print";
|
||||
model = "everywhere";
|
||||
}
|
||||
{
|
||||
name = "EPSON_ET_8550_Photo";
|
||||
description = "EPSON ET-8550 (ESC/P-R photo presets)";
|
||||
location = "Network";
|
||||
deviceUri = "ipp://EPSONA70973.local:631/ipp/print";
|
||||
model = "epson-inkjet-printer-escpr2/Epson-ET-8550_Series-epson-escpr2-en.ppd";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# hardware/printers.nix orders ensure-printers only after cups.service, but
|
||||
# our deviceUri is an mDNS name and the driverless queue queries the printer
|
||||
# while it is created. Run the generated script asynchronously so an offline
|
||||
# printer cannot fail boot or nixos-rebuild, then retry until it is reachable.
|
||||
systemd.services.ensure-printers = lib.mkIf (config.hardware.printers.ensurePrinters != [] && config.services.printing.enable) {
|
||||
after = [
|
||||
"avahi-daemon.service"
|
||||
"network-online.target"
|
||||
"nss-lookup.target"
|
||||
];
|
||||
wants = [
|
||||
"avahi-daemon.service"
|
||||
"network-online.target"
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = lib.mkForce "simple";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "1min";
|
||||
};
|
||||
};
|
||||
|
||||
# eSCL/AirScan. The printer advertises _uscan._tcp with rs=eSCL and
|
||||
# mopria-certified-scan=1.3, so sane-airscan drives it with no vendor
|
||||
# backend. No scanner/lp group needed - there's no local udev device.
|
||||
#
|
||||
# sane-backends ships its own `escl` backend which also claims eSCL
|
||||
# devices. Left enabled it double-lists the scanner and, worse, GETs the
|
||||
# printer's web root and dumps the HTML response onto scanimage's stdout.
|
||||
# Disabling it leaves sane-airscan as the only eSCL backend, which reports
|
||||
# a far better device string (airscan:e0:EPSON ET-8550 Series).
|
||||
hardware.sane = {
|
||||
enable = true;
|
||||
extraBackends = [pkgs.sane-airscan];
|
||||
disabledDefaultBackends = ["escl"];
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
simple-scan
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{...}: {
|
||||
# PrusaSlicer package only; printer and filament profiles remain per-user.
|
||||
flake.nixosModules.prusa-slicer = {pkgs, ...}: {
|
||||
environment.systemPackages = with pkgs; [
|
||||
prusa-slicer
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -5,5 +5,6 @@
|
||||
environment.systemPackages = with pkgs; [
|
||||
quickshell
|
||||
];
|
||||
qt.enable = true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{...}: {
|
||||
# Screenshot capture, region selection, annotation, and clipboard support for
|
||||
# the Hyprland screenshot keybinds managed in the dotfiles repo.
|
||||
flake.nixosModules.screenshot = {pkgs, ...}: {
|
||||
environment.systemPackages = with pkgs; [
|
||||
grim
|
||||
slurp
|
||||
satty
|
||||
wl-clipboard
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{...}: {
|
||||
# TODO: previously injected SLSsteam via LD_AUDIT from the sls-steam flake. Now stock.
|
||||
flake.nixosModules.sunshine = {pkgs, ...}: {
|
||||
services.sunshine = {
|
||||
enable = true;
|
||||
autoStart = true;
|
||||
capSysAdmin = true;
|
||||
openFirewall = true;
|
||||
settings.output_name = "DP-3"
|
||||
};
|
||||
hardware.uinput.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{...}: {
|
||||
# Unraid USB Creator - writes Unraid OS straight to a USB stick, streaming the
|
||||
# image from unraid.net instead of making you download it first.
|
||||
#
|
||||
# Not in nixpkgs, and upstream ships an AppImage only, so this is a repack.
|
||||
# The AppImage is a Qt6/QML app (a fork of rpi-imager) built with linuxdeploy,
|
||||
# meaning it already vendors the entire Qt6 stack, xcb, icu, gnutls and krb5 in
|
||||
# usr/lib. We keep that bundle verbatim and only autoPatchelf it against the
|
||||
# handful of libraries linuxdeploy deliberately left out (libc, libstdc++, GL,
|
||||
# X11, fontconfig, ...). No FHS wrapper needed, so no 400MB buildFHSEnv closure.
|
||||
flake.nixosModules.unraid-usb-creator = {
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
pname = "unraid-usb-creator";
|
||||
version = "1.1.0";
|
||||
|
||||
# unraid.net/download's "stable" link is a redirect to this GitHub asset.
|
||||
# Note the v1.1.0 tag is not returned by the releases API (the API only lists
|
||||
# the v2.0.x line), but the download URL is live and matches the checksum.
|
||||
appimage = pkgs.fetchurl {
|
||||
url = "https://github.com/unraid/usb-creator-next/releases/download/v${version}/${pname}-${version}.appimage";
|
||||
hash = "sha256-NI/SMgOuzifvjhtK4Py96lbuI/uvJO0NiMr2zNtNSUk=";
|
||||
};
|
||||
|
||||
unraid-usb-creator = pkgs.stdenvNoCC.mkDerivation {
|
||||
inherit pname version;
|
||||
|
||||
src = pkgs.appimageTools.extract {
|
||||
inherit pname version;
|
||||
src = appimage;
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
autoPatchelfHook
|
||||
makeWrapper
|
||||
];
|
||||
|
||||
# Only the libraries the AppImage does *not* vendor. Everything else
|
||||
# resolves inside $out/share/unraid-usb-creator/lib.
|
||||
buildInputs = with pkgs; [
|
||||
e2fsprogs # libcom_err.so.2, pulled in by the vendored krb5
|
||||
fontconfig
|
||||
freetype
|
||||
gmp # pulled in by the vendored nettle/gnutls
|
||||
libgpg-error
|
||||
libglvnd # libGL / libGLX / libEGL / libOpenGL
|
||||
libx11 # also provides libX11-xcb
|
||||
libxcb
|
||||
stdenv.cc.cc # libstdc++ / libgcc_s
|
||||
zlib
|
||||
];
|
||||
|
||||
# Qt's openssl TLS backend resolves libssl/libcrypto with dlopen, so
|
||||
# autoPatchelf never sees the dependency. Without this the app cannot fetch
|
||||
# the release JSON or stream the image over HTTPS.
|
||||
#
|
||||
# This has to be appendRunpaths and NOT runtimeDependencies: in
|
||||
# auto-patchelf, `rpath += runtime_deps` lives inside the set-interpreter
|
||||
# branch, so it only ever reaches executables, while `rpath.extend(
|
||||
# append_rpaths)` runs for every patched file. The dlopen happens from
|
||||
# plugins/tls/libqopensslbackend.so, and glibc resolves dlopen against the
|
||||
# calling object's DT_RUNPATH - the wrapper binary's RUNPATH is not
|
||||
# consulted - so the path has to be on the plugin itself.
|
||||
appendRunpaths = ["${pkgs.openssl.out}/lib"];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
# qt.conf next to the binary declares Prefix=../ with Plugins=plugins and
|
||||
# Qml2Imports=qml, so the usr/ layout has to be preserved as a unit.
|
||||
install -Dm755 usr/bin/unraid-usb-creator -t $out/share/${pname}/bin
|
||||
install -Dm444 usr/bin/qt.conf -t $out/share/${pname}/bin
|
||||
cp -r usr/lib usr/plugins usr/qml $out/share/${pname}/
|
||||
|
||||
cp -r usr/share/icons $out/share/
|
||||
install -Dm444 usr/share/metainfo/*.xml -t $out/share/metainfo
|
||||
install -Dm444 usr/share/applications/*.desktop -t $out/share/applications
|
||||
|
||||
# mkfs.fat is shelled out to when formatting the stick; syslinux is NOT
|
||||
# needed as a runtime dep because upstream statically vendors it.
|
||||
#
|
||||
# linuxdeploy only bundled the xcb platform plugin - there is no
|
||||
# libqwayland*.so anywhere in the AppImage - so this always runs through
|
||||
# XWayland under Hyprland. Pinning the platform avoids Qt first trying
|
||||
# wayland and falling back; --set-default rather than --set so it can
|
||||
# still be overridden if upstream ever ships the Wayland plugin.
|
||||
makeWrapper $out/share/${pname}/bin/unraid-usb-creator $out/bin/${pname} \
|
||||
--prefix PATH : ${lib.makeBinPath (with pkgs; [dosfstools util-linux])} \
|
||||
--set-default QT_QPA_PLATFORM xcb
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postFixup = ''
|
||||
substituteInPlace $out/share/applications/*.desktop \
|
||||
--replace-fail "Exec=unraid-usb-creator" "Exec=$out/bin/${pname}"
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Tool for writing Unraid OS to USB drives";
|
||||
homepage = "https://github.com/unraid/usb-creator-next";
|
||||
license = lib.licenses.asl20;
|
||||
sourceProvenance = [lib.sourceTypes.binaryNativeCode];
|
||||
platforms = ["x86_64-linux"];
|
||||
mainProgram = pname;
|
||||
};
|
||||
};
|
||||
in {
|
||||
environment.systemPackages = [unraid-usb-creator];
|
||||
|
||||
# The app enumerates and unmounts removable drives over the UDisks2 D-Bus API
|
||||
# rather than poking /dev directly, which is also how it avoids needing root.
|
||||
# hyprland/default.nix already enables this for udiskie; repeated here so the
|
||||
# module is self-contained on a host that does not use Hyprland.
|
||||
services.udisks2.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{...}: {
|
||||
# QMK/VIA keyboard tooling. QMK's uaccess rule lets the active local seat talk
|
||||
# to HID devices; remote and non-seat sessions intentionally get no access.
|
||||
flake.nixosModules.via = {pkgs, ...}: {
|
||||
environment.systemPackages = with pkgs; [
|
||||
via
|
||||
qmk
|
||||
vial
|
||||
];
|
||||
|
||||
# Do not add pkgs.via or pkgs.vial to services.udev.packages: both make
|
||||
# every hidraw device world-writable. QMK's rules use seat-scoped ACLs.
|
||||
hardware.keyboard.qmk.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{...}: {
|
||||
# MikroTik RouterOS configuration client. pkgs.winbox is winbox4, which is
|
||||
# already autoPatchelf'd against the Qt/xcb stack it needs, so it does not
|
||||
# rely on nix-ld. Note the installed binary is `WinBox`, not `winbox`.
|
||||
flake.nixosModules.winbox = {pkgs, ...}: {
|
||||
environment.systemPackages = with pkgs; [
|
||||
winbox
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{...}: {
|
||||
# Yazi a cli file explorer.
|
||||
flake.nixosModules.yazi = {pkgs, ...}: {
|
||||
environment.systemPackages = with pkgs; [
|
||||
yazi
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -19,7 +19,12 @@
|
||||
discord
|
||||
gaming
|
||||
bottles
|
||||
netops
|
||||
prusa-slicer
|
||||
printing
|
||||
via
|
||||
nextcloud
|
||||
sunshine
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# (`nix run .#sync-hardware`) safe: re-running it can never clobber these
|
||||
# definitions, nor produce duplicates for whichever data drives happened to
|
||||
# be mounted at the time it ran.
|
||||
flake.nixosModules.shadowDisks = {...}: let
|
||||
flake.nixosModules.shadowDisks = {pkgs, ...}: let
|
||||
# Secondary data drives, addressed by filesystem label.
|
||||
#
|
||||
# nofail on all of them: it stops a missing or dying disk from becoming a
|
||||
@@ -53,6 +53,22 @@
|
||||
"x-systemd.device-timeout=10"
|
||||
];
|
||||
};
|
||||
|
||||
# Pin the NFS server until nas.hfx.ahmza.com points at 10.10.100.4.
|
||||
# Automounting keeps an unavailable NAS out of the boot path.
|
||||
nfsShare = export: {
|
||||
device = "10.10.100.4:${export}";
|
||||
fsType = "nfs";
|
||||
options = [
|
||||
"nfsvers=4.2"
|
||||
"noauto"
|
||||
"nofail"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"x-systemd.idle-timeout=600"
|
||||
"x-systemd.mount-timeout=10"
|
||||
];
|
||||
};
|
||||
in {
|
||||
fileSystems = {
|
||||
# Boot essentials, by UUID. UUIDs rather than labels here on purpose:
|
||||
@@ -72,8 +88,105 @@
|
||||
"/mnt/dump" = spinningDrive "DUMP";
|
||||
"/mnt/bak" = spinningDrive "BAK";
|
||||
"/mnt/games" = eagerDrive "GAMES";
|
||||
"/mnt/nas" = nfsShare "/srv/nas";
|
||||
"/mnt/media" = nfsShare "/srv/media";
|
||||
};
|
||||
|
||||
swapDevices = [];
|
||||
# Swap exists for hibernation, not for memory pressure — 31GiB of RAM on a
|
||||
# desktop rarely needs paging. The hibernation image must fit in swap, so the
|
||||
# floor is "as big as RAM"; 40GiB leaves headroom above that.
|
||||
#
|
||||
# NVIDIA video memory does NOT land here: with
|
||||
# NVreg_PreserveVideoMemoryAllocations=1 (set by
|
||||
# hardware.nvidia.powerManagement.enable) the driver spools the 3090's VRAM
|
||||
# to NVreg_TemporaryFilePath, which is unset and so defaults to /tmp. That is
|
||||
# only safe because /tmp is on this ext4 root — if /tmp ever becomes a tmpfs
|
||||
# (boot.tmp.useTmpfs) that VRAM dump goes into RAM and hibernation breaks.
|
||||
#
|
||||
# On non-btrfs filesystems NixOS creates this with `dd if=/dev/zero`, so the
|
||||
# file is fully allocated rather than sparse. That matters because the kernel
|
||||
# writes swap through the block layer using the extent map swapon built,
|
||||
# bypassing the filesystem — so the mapping has to be stable and complete.
|
||||
#
|
||||
# The failure is loud, not silent, though: swapon refuses a file with holes
|
||||
# outright ("skipping - it appears to have holes"), so a sparse file would
|
||||
# simply leave the system with no swap rather than corrupt anything. Genuine
|
||||
# silent corruption is the copy-on-write case, which is why btrfs needs
|
||||
# chattr +C and a NOCOW-from-creation file.
|
||||
swapDevices = [
|
||||
{
|
||||
device = "/var/lib/swapfile";
|
||||
size = 40 * 1024; # MiB
|
||||
}
|
||||
];
|
||||
|
||||
# Resuming from hibernation needs the kernel to be told where the image is,
|
||||
# and for a swap *file* that means both halves below.
|
||||
#
|
||||
# boot.initrd.systemd.enable is true on this host, which makes this
|
||||
# mandatory rather than optional: under systemd initrd the `resume=` kernel
|
||||
# param is emitted ONLY from boot.resumeDevice. The legacy stage-1 script
|
||||
# that scans swapDevices to find a resume device is not in play. Without
|
||||
# this, hibernate succeeds and the machine then cold-boots, silently
|
||||
# discarding the session.
|
||||
#
|
||||
# resume_offset is the physical block offset of the swapfile's first extent,
|
||||
# because the kernel addresses the file by raw block, not by path. Recompute
|
||||
# it with the following if the swapfile is ever recreated, resized, or moved
|
||||
# — a stale value resumes from the wrong blocks:
|
||||
#
|
||||
# sudo filefrag -v /var/lib/swapfile | awk '$1=="0:" {gsub(/\.\./,"",$4); print $4}'
|
||||
#
|
||||
# filefrag reports in 4096-byte filesystem blocks and the kernel wants
|
||||
# PAGE_SIZE units; on x86_64 both are 4096, so the number transfers directly.
|
||||
boot.resumeDevice = "/dev/disk/by-uuid/deff63fc-1738-4cfc-a79c-06e58c78471a";
|
||||
boot.kernelParams = ["resume_offset=499355648"];
|
||||
|
||||
# A stale resume_offset fails *silently*: the kernel finds no swsusp
|
||||
# signature at the given block, logs `PM: Image not found (code -22)`, and
|
||||
# cold-boots — the hibernated session is simply gone, with no error at
|
||||
# hibernate time. Since the value above is hand-copied, check it on every
|
||||
# activation so drift surfaces as a warning instead of as a lost session.
|
||||
#
|
||||
# Two known limits, both acceptable:
|
||||
# * nixpkgs only recreates the swapfile when `size` changes, so on the one
|
||||
# rebuild that resizes it this check still sees the old file and passes;
|
||||
# it fires on the next rebuild instead. It is a drift detector, not a
|
||||
# pre-flight gate.
|
||||
# * Warn rather than fail: activation must not be blocked over this, and it
|
||||
# is expected to read nothing at all on a fresh install where
|
||||
# mkswap-var-lib-swapfile.service has not run yet.
|
||||
system.activationScripts.checkResumeOffset.text = ''
|
||||
swapfile=/var/lib/swapfile
|
||||
want=499355648
|
||||
if [ -f "$swapfile" ]; then
|
||||
got=$(${pkgs.e2fsprogs}/bin/filefrag -v "$swapfile" 2>/dev/null \
|
||||
| ${pkgs.gawk}/bin/awk '$1=="0:" {gsub(/\.\./,"",$4); print $4; exit}') || got=""
|
||||
if [ -n "$got" ] && [ "$got" != "$want" ]; then
|
||||
echo "WARNING: resume_offset is pinned to $want in modules/hosts/shadow/disks.nix," >&2
|
||||
echo " but $swapfile now starts at block $got. Hibernation will cold-boot" >&2
|
||||
echo " and lose the session until boot.kernelParams is updated to $got." >&2
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
|
||||
# This host sleeps via suspend-then-hibernate (hypridle in the dotfiles repo
|
||||
# is what invokes it): S3 first so a quick return is instant, then a real
|
||||
# suspend-to-disk once the machine has clearly been abandoned.
|
||||
#
|
||||
# Deliberately NOT hybrid-sleep, which writes the disk image up front on
|
||||
# every single sleep. Measured here that cost ~59s of filesystem sync plus an
|
||||
# 11GiB write before the screens even went off, and because an S3 wake
|
||||
# invalidates the image the next boot always reported `PM: Image not found
|
||||
# (code -22)` — full hibernation price paid every time, redeemed only on a
|
||||
# power cut. suspend-then-hibernate defers that write until it is actually
|
||||
# worth doing, and unlike hybrid-sleep it is a mode NVIDIA explicitly
|
||||
# supports (see the system-sleep hook installed by the gpuNvidia module).
|
||||
#
|
||||
# HibernateDelaySec is set explicitly because systemd otherwise derives the
|
||||
# handoff from battery discharge estimation, and this desktop has no system
|
||||
# battery — the sole /sys/class/power_supply entry is a Logitech mouse
|
||||
# reporting scope=Device, which is ignored for that purpose.
|
||||
systemd.sleep.settings.Sleep.HibernateDelaySec = "30min";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,93 @@
|
||||
networking = {
|
||||
hostName = "shadow";
|
||||
};
|
||||
|
||||
# Windows lives on the 476G NVMe -- the one with three partitions: 16M
|
||||
# "Microsoft reserved" MSR, 476G NTFS (fs-uuid 70E4E841E4E80AE4, the C:
|
||||
# volume), 832M WinRE. Identified by layout rather than by nvmeXnY on
|
||||
# purpose; see the enumeration note below.
|
||||
#
|
||||
# That disk has NO EFI System Partition of its own -- its 16M partition is a
|
||||
# "Microsoft reserved" MSR with no filesystem. Windows was installed against
|
||||
# the ESP that NixOS now owns: the 1G vfat partition fs-uuid 5873-AD05 on the
|
||||
# 1.9T NVMe (that disk holds only two partitions, the ESP and the ext4 root
|
||||
# deff63fc-1738-4cfc-a79c-06e58c78471a). So both OSes boot from one shared
|
||||
# ESP; there is no separate Windows boot drive.
|
||||
#
|
||||
# The NixOS install reformatted that ESP, which wiped
|
||||
# EFI/Microsoft/Boot/{bootmgfw.efi,BCD} and left Windows unbootable. That has
|
||||
# since been repaired from WinRE and both files are back in place, so the
|
||||
# menuentry below is live. Keep the recovery command to hand in case the ESP
|
||||
# is ever reformatted again -- it has to be run from Windows install media or
|
||||
# WinRE, because the BCD store cannot be generated from Linux:
|
||||
#
|
||||
# bcdboot C:\Windows /s S: /f UEFI (S: = the ESP, fs-uuid 5873-AD05)
|
||||
#
|
||||
# Copying bootmgfw.efi alone is not enough; without BCD it fails 0xc000000f.
|
||||
# Note bcdboot also drops a copy of the Windows boot manager at
|
||||
# EFI/BOOT/bootx64.efi -- see the fallback-path note in core/boot.nix.
|
||||
#
|
||||
# Chainloaded explicitly rather than via grub.useOSProber: deterministic,
|
||||
# pulls in no extra tooling, and avoids probing every disk on each bootloader
|
||||
# install. The `-f` guard makes the entry self-disabling: it renders only
|
||||
# while bootmgfw.efi is actually present, so a future ESP reformat degrades
|
||||
# to a missing menu item rather than one that boots into an error.
|
||||
#
|
||||
# fs-uuid is shadow's ESP, matching the "/boot" entry in disks.nix. Searching
|
||||
# by UUID rather than (hdX,gptN) matters here: NVMe enumeration is not stable
|
||||
# on this board -- /boot has been seen as both nvme0n1p1 and nvme1n1p1. For
|
||||
# the same reason, re-derive device names before running anything
|
||||
# destructive:
|
||||
#
|
||||
# lsblk -o NAME,SIZE,FSTYPE,PARTTYPENAME,UUID,MOUNTPOINT
|
||||
#
|
||||
# Windows' boot files now occupy ~30-60MiB of that shared 1G ESP and Windows
|
||||
# Update rewrites them periodically, which is part of why
|
||||
# grub.configurationLimit is held at 10 in core/boot.nix.
|
||||
#
|
||||
# Before dual-booting, turn off Windows Fast Startup. hiberfil.sys is present
|
||||
# on the 476G NTFS volume, and Fast Startup plus this host's hibernation
|
||||
# setup will corrupt any NTFS volume mounted read-write from both sides.
|
||||
#
|
||||
# On the GRUB script itself:
|
||||
#
|
||||
# The outer `search` + `-f` test run at *boot*, while GRUB parses grub.cfg —
|
||||
# not at Nix eval time. That is why repairing Windows needed no rebuild: the
|
||||
# entry started rendering as soon as bcdboot put bootmgfw.efi back, and it
|
||||
# will stop again by itself if the file disappears.
|
||||
#
|
||||
# The `-n "$esp"` test is a *separate,
|
||||
# nested* if rather than `[ -n "$esp" -a -f ... ]`: a failed search leaves
|
||||
# $esp unset, and GRUB's `test` builtin evaluates both operands of `-a`
|
||||
# rather than short-circuiting, so the combined form would still expand the
|
||||
# `-f` to `()/...` and could complain about a bad device on every boot.
|
||||
# Nesting guarantees the path test is not reached at all.
|
||||
#
|
||||
# No `insmod` is needed for the outer search: GRUB's EFI core image already
|
||||
# embeds part_gpt/fat/search_fs_uuid, since it must find its own $prefix on
|
||||
# the FAT ESP before reading this file. NixOS' own install-grub.pl relies on
|
||||
# exactly that for the search it emits. The insmods inside the menuentry are
|
||||
# kept only because `chain` genuinely is a separate module.
|
||||
#
|
||||
# The inner `search` deliberately repeats the outer one rather than reusing
|
||||
# `set root=$esp`. It costs one extra disk scan, and only when the entry is
|
||||
# actually selected, in exchange for a menuentry that is self-contained and
|
||||
# cannot be broken by anything that clobbers $esp in between. This also
|
||||
# matches the per-entry search install-grub.pl generates.
|
||||
boot.loader.grub.extraEntries = ''
|
||||
search --no-floppy --set=esp --fs-uuid 5873-AD05
|
||||
if [ -n "$esp" ]; then
|
||||
if [ -f ($esp)/EFI/Microsoft/Boot/bootmgfw.efi ]; then
|
||||
menuentry "Windows Boot Manager" --class windows --class os {
|
||||
insmod part_gpt
|
||||
insmod fat
|
||||
insmod chain
|
||||
search --no-floppy --set=root --fs-uuid 5873-AD05
|
||||
chainloader /EFI/Microsoft/Boot/bootmgfw.efi
|
||||
}
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,13 @@
|
||||
obsidian
|
||||
discord
|
||||
gaming
|
||||
winbox
|
||||
cad
|
||||
prusa-slicer
|
||||
netops
|
||||
via
|
||||
t480UsbWakeup
|
||||
moonlight
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
networking = {
|
||||
hostName = "t480";
|
||||
};
|
||||
services.logind.settings.Login = {
|
||||
HandleLidSwitch = "suspend";
|
||||
HandleLidSwitchDocked = "ignore";
|
||||
};
|
||||
services.fwupd = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{...}: {
|
||||
flake.nixosModules.t480UsbWakeup = {...}: {
|
||||
services.udev.extraRules = ''
|
||||
# Wake the T480 from S3 suspend via a keyboard plugged into a built-in USB
|
||||
# port. The dock connects over the Thunderbolt-3 controller (3c:00.0), which
|
||||
# powers down in S3 and cannot detect a cold replug — so dock-connected input
|
||||
# cannot wake the machine after an unplug-triggered suspend. The built-in xHCI
|
||||
# (00:14.0) stays wake-capable in S3, so a keyboard on a built-in port works.
|
||||
#
|
||||
# Arm ONLY the keyboard itself (match by VID:PID — the USB bus path is
|
||||
# unstable across re-enumeration). Deliberately do NOT arm the built-in root
|
||||
# hubs: arming a whole root hub makes it wake on hub-level events (USB-C PD
|
||||
# negotiation, the SD card reader, internal peripherals), causing spurious
|
||||
# self-wakes. A leaf device's remote-wakeup propagates through the hub without
|
||||
# the hub's own wakeup flag being set.
|
||||
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="320f", ATTR{idProduct}=="5055", ATTR{power/wakeup}="enabled"
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -10,10 +10,67 @@
|
||||
efi = {
|
||||
canTouchEfiVariables = true;
|
||||
};
|
||||
# Pinned explicitly. grub.enable defaults to true, so this host silently
|
||||
# switched from systemd-boot (used at install time) to GRUB when this
|
||||
# block was added. NixOS never removes the previous bootloader's files
|
||||
# or its NVRAM entry, so both lived on the ESP and the firmware kept
|
||||
# booting the frozen systemd-boot menu. Keep this explicit, and never
|
||||
# set systemd-boot.enable alongside it.
|
||||
#
|
||||
# Note what this does and does not do. Both of these lines are no-ops
|
||||
# against nixpkgs' defaults (grub.enable is already true,
|
||||
# systemd-boot.enable already false); their value is that a conflicting
|
||||
# definition elsewhere now fails loudly instead of silently winning. They
|
||||
# delete nothing — stale EFI/systemd/, loader/ and the leftover NVRAM
|
||||
# entry have to be removed by hand on any host installed with
|
||||
# systemd-boot. Done on shadow; NOT yet verified on t480.
|
||||
#
|
||||
# This is stock UEFI GRUB: NixOS installs EFI/<distro><esp>/grubx64.efi
|
||||
# (here EFI/NixOS-boot/grubx64.efi) and registers a matching NVRAM entry
|
||||
# via efibootmgr, because canTouchEfiVariables is true. That NVRAM entry
|
||||
# is what makes GRUB the default; NixOS does not manage the removable
|
||||
# fallback path EFI/BOOT/BOOTX64.EFI and we deliberately do not ask it to
|
||||
# (grub.efiInstallAsRemovable, which would need canTouchEfiVariables=false,
|
||||
# is only for firmware that ignores NVRAM entries — neither host needs it).
|
||||
#
|
||||
# On shadow that fallback path is NOT empty, though: bcdboot left a copy of
|
||||
# the *Windows* boot manager at EFI/BOOT/bootx64.efi (byte-identical to
|
||||
# EFI/Microsoft/Boot/bootmgfw.efi). Normal boots are unaffected because the
|
||||
# firmware follows its NVRAM entry to GRUB. But it does mean that if NVRAM
|
||||
# is ever cleared — CMOS reset, battery, firmware update — the fallback
|
||||
# lands on Windows rather than GRUB. Left as-is by choice; recover by
|
||||
# rerunning `nixos-rebuild boot` to reinstate the NVRAM entry.
|
||||
systemd-boot.enable = false;
|
||||
grub = {
|
||||
enable = true;
|
||||
efiSupport = true;
|
||||
device = "nodev";
|
||||
theme = pkgs.catppuccin-grub;
|
||||
# Bound the menu so the ESP cannot fill up with old kernels.
|
||||
# install-grub.pl unlinks any $boot/kernels/* not referenced by a kept
|
||||
# generation, so this genuinely caps ESP usage rather than just
|
||||
# trimming the menu.
|
||||
#
|
||||
# Sizing, measured on shadow rather than assumed. Generations share
|
||||
# aggressively in practice: 10 kept generations referenced 1 bzImage
|
||||
# (13.3MiB) and 3 initrds (~33.4MiB each) for 114MiB total, i.e. ~11MiB
|
||||
# per generation, not ~47MiB. Only a kernel or initrd change mints a new
|
||||
# file — and the initrd does change more often than you'd expect, since
|
||||
# nixpkgs embeds /etc/modprobe.d/nixos.conf inside it, so every kernel
|
||||
# module parameter edit produces a fresh one.
|
||||
#
|
||||
# So the real bound is between ~11MiB and ~47MiB per generation
|
||||
# depending on churn: 10 generations costs ~115MiB typically and ~470MiB
|
||||
# worst case, against a 1GiB ESP that also carries grub/x86_64-efi, the
|
||||
# catppuccin theme, and Windows' own boot files (bootmgfw.efi + BCD,
|
||||
# ~30-60MiB) since Windows was bcdboot'd onto this same ESP. 10 keeps
|
||||
# the worst case under half the partition.
|
||||
#
|
||||
# Keep this conservative: overflow fails as `copy ... or die` in
|
||||
# install-grub.pl, which runs *after* the system profile has already
|
||||
# been switched, so the result is an activated but unbootable
|
||||
# generation. t480 reaches this module too and its ESP may be smaller.
|
||||
configurationLimit = 10;
|
||||
};
|
||||
};
|
||||
plymouth = {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
extraGroups = [
|
||||
"networkmanager"
|
||||
"wheel"
|
||||
"uinput"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
hyprland
|
||||
sddm
|
||||
network
|
||||
yazi
|
||||
];
|
||||
in {
|
||||
imports = modules;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# For Intel+NVIDIA laptops (e.g. the T480), use gpuNvidiaPrime instead.
|
||||
flake.nixosModules.gpuNvidia = {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
@@ -15,15 +16,147 @@
|
||||
|
||||
hardware.nvidia = {
|
||||
modesetting.enable = true;
|
||||
# Proprietary kernel modules. Set to true only for Turing+ (RTX 20xx and
|
||||
# newer) where the open modules are well supported.
|
||||
open = false;
|
||||
# Open kernel modules. Mandatory to set explicitly on driver >= 560 —
|
||||
# the option defaults to null there and nixpkgs asserts against null, so
|
||||
# this line cannot simply be dropped.
|
||||
#
|
||||
# true because every card this module targets is Turing or later (RTX 20xx
|
||||
# and newer), which is exactly what upstream recommends open modules for.
|
||||
# Set false only for a genuinely pre-Turing card, which would want the
|
||||
# gpuNvidia module pointed at a legacy package branch anyway.
|
||||
#
|
||||
# This is NOT cosmetic and NOT a version switch: `open` selects between two
|
||||
# real builds of the same release — nvidia-open (~12MB) vs the proprietary
|
||||
# nvidia-kernel-modules (~85MB). It has no effect on driver version, which
|
||||
# comes solely from `package` below.
|
||||
#
|
||||
# Kernel modules only change on reboot, so `cat /proc/driver/nvidia/version`
|
||||
# describes the RUNNING module and can disagree with this setting until the
|
||||
# next boot. Compare /run/booted-system against /run/current-system to see
|
||||
# a pending swap.
|
||||
#
|
||||
# mkDefault because "Turing or later" is an assumption about the hosts that
|
||||
# happen to import this module today, not a property of the module itself.
|
||||
# A future pre-Turing card can then override it at the host level instead of
|
||||
# having to edit this file (and note that without mkDefault, importing both
|
||||
# gpuNvidia and gpuNvidiaPrime would be an outright merge conflict rather
|
||||
# than something resolvable).
|
||||
open = lib.mkDefault true;
|
||||
nvidiaSettings = true;
|
||||
# Fixes suspend/resume corruption on many setups.
|
||||
# Required for suspend/hibernate to come back with an intact framebuffer:
|
||||
# this sets NVreg_PreserveVideoMemoryAllocations=1, so VRAM contents are
|
||||
# spooled out across sleep instead of being discarded.
|
||||
#
|
||||
# The driver spools VRAM to NVreg_TemporaryFilePath, which nixpkgs leaves
|
||||
# unset so it defaults to /tmp. Keep /tmp on a real filesystem — setting
|
||||
# boot.tmp.useTmpfs would route a full VRAM dump into RAM, which on a
|
||||
# large card is enough to break hibernation outright.
|
||||
powerManagement.enable = true;
|
||||
# Which mechanism tells the driver about sleep transitions. Two options:
|
||||
#
|
||||
# true -> NVreg_UseKernelSuspendNotifiers=1; the driver hooks the
|
||||
# in-kernel PM notifier chain and the systemd units are NOT
|
||||
# generated.
|
||||
# false -> nvidia-{suspend,hibernate,resume}.service call nvidia-sleep.sh
|
||||
# around systemd-{suspend,hibernate}.service.
|
||||
#
|
||||
# Pinned to false deliberately. nixpkgs defaults this to
|
||||
# `useOpenModules && version >= 595`, which is silently true here (open =
|
||||
# true above, driver 595), and that path wedges the GPU on resume — the
|
||||
# notifier callback warns and then the card never comes back:
|
||||
#
|
||||
# WARNING: nvidia/nv.c:4564 at nv_restore_user_channels+0x1d9/0x240
|
||||
# nv_pm_notifier+0x35/0xa0 [nvidia]
|
||||
# nvidia-modeset: ERROR: GPU:0: Failed detecting connected display devices
|
||||
# NVRM: GPU0 _issueRpcAndWait: rpcSendMessage failed with status 0x11
|
||||
#
|
||||
# followed by an unrecoverable GSP RPC storm, i.e. black screens until a
|
||||
# hard power cycle. Revisit once a newer driver fixes the notifier path;
|
||||
# test by flipping this to true and confirming a hibernate round-trip
|
||||
# relights the displays before leaving it that way.
|
||||
powerManagement.kernelSuspendNotifier = false;
|
||||
package = config.boot.kernelPackages.nvidiaPackages.stable;
|
||||
};
|
||||
|
||||
# The nvidia-{suspend,hibernate,resume}.service units nixpkgs generates are
|
||||
# wired only to systemd-{suspend,hibernate}.service. suspend-then-hibernate
|
||||
# runs as a single systemd-suspend-then-hibernate.service that performs both
|
||||
# transitions internally, so those units never fire for it and the GPU would
|
||||
# sleep with VRAM unspooled.
|
||||
#
|
||||
# NVIDIA ships a hook that handles exactly this, dispatching on
|
||||
# $SYSTEMD_SLEEP_ACTION and covering the suspend-after-failed-hibernate case
|
||||
# too. nixpkgs sets `systemd.packages = nvidia_x11.out`, but that only
|
||||
# collects lib/systemd/system/, never lib/systemd/system-sleep/, so upstream's
|
||||
# hook is silently discarded. Install it by hand.
|
||||
#
|
||||
# It must NOT be installed verbatim, though. Past its suspend-then-hibernate
|
||||
# branch the hook ends in an unconditional fallthrough:
|
||||
#
|
||||
# case "$1" in post) nvidia-sleep.sh "resume" ;; esac
|
||||
#
|
||||
# For plain suspend/hibernate that duplicates nvidia-resume.service, and the
|
||||
# duplicate is destructive rather than merely redundant: nvidia-sleep.sh's
|
||||
# RestoreVT() reads /run/nvidia-sleep/Xorg.vt_number, *deletes* it, then
|
||||
# chvt's back. `chvt` lives in pkgs.kbd, which nixpkgs puts on
|
||||
# nvidia-resume.service's `path` but which is absent from the stripped PATH
|
||||
# systemd-sleep gives its hooks. So the hook consumes the VT file, fails to
|
||||
# chvt, and the real unit then finds nothing to restore — no VT switch at
|
||||
# all, i.e. the same black-screen-on-resume this module exists to avoid.
|
||||
#
|
||||
# Hence the first line of the wrapper: bail out unless systemd is actually
|
||||
# doing suspend-then-hibernate, the one transition nixpkgs' units do not
|
||||
# cover. Upstream's own comment notes that its suspend-then-hibernate path
|
||||
# deliberately writes /proc/driver/nvidia/suspend directly and leaves the VT
|
||||
# switch to nvidia-resume.service, so gating here loses nothing.
|
||||
#
|
||||
# The wrapper also has to PATCH A HOLE in upstream's dispatch. Its
|
||||
# suspend-then-hibernate branch is:
|
||||
#
|
||||
# case "$1:$SYSTEMD_SLEEP_ACTION" in
|
||||
# post:*) echo resume > ... ;;
|
||||
# pre:hibernate) echo hibernate > ... ;;
|
||||
# pre:suspend-after-failed-hibernate) echo suspend > ... ;;
|
||||
# esac
|
||||
#
|
||||
# There is no `pre:suspend`. But that is exactly the FIRST leg of
|
||||
# suspend-then-hibernate: systemd invokes the hook with $1=pre and
|
||||
# SYSTEMD_SLEEP_ACTION=suspend before the S3 transition. It matches nothing,
|
||||
# the case falls through, and the driver never gets its VRAM-spool handshake
|
||||
# — so with NVreg_PreserveVideoMemoryAllocations=1 the GPU refuses to
|
||||
# suspend and the whole operation aborts:
|
||||
#
|
||||
# nvidia 0000:2d:00.0: PM: failed to suspend async: error -5
|
||||
# PM: Some devices failed to suspend, or early wake event detected
|
||||
# systemd-sleep: Failed to put system to sleep. System resumed again:
|
||||
# Input/output error
|
||||
#
|
||||
# Observed on this host: plain suspend and plain hibernate both worked (their
|
||||
# nvidia-*.service units did the handshake) while suspend-then-hibernate
|
||||
# failed at the S3 leg with no nvidia unit having run at all. So handle
|
||||
# pre:suspend here, the same way upstream handles pre:hibernate, then hand
|
||||
# everything else to upstream unchanged.
|
||||
#
|
||||
# Guarded on the same condition nixpkgs uses for the units: with the kernel
|
||||
# suspend notifier active the driver handles transitions in-kernel and this
|
||||
# hook must not also poke /proc/driver/nvidia/suspend.
|
||||
environment.etc."systemd/system-sleep/nvidia" = lib.mkIf (config.hardware.nvidia.powerManagement.enable
|
||||
&& !config.hardware.nvidia.powerManagement.kernelSuspendNotifier) {
|
||||
source = pkgs.writeShellScript "nvidia-system-sleep-hook" ''
|
||||
[ "$2" = "suspend-then-hibernate" ] || exit 0
|
||||
|
||||
# The gap upstream leaves: the S3 leg of suspend-then-hibernate.
|
||||
if [ "$1" = "pre" ] && [ "$SYSTEMD_SLEEP_ACTION" = "suspend" ]; then
|
||||
if [ -f /proc/driver/nvidia/suspend ]; then
|
||||
echo suspend > /proc/driver/nvidia/suspend
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec ${config.hardware.nvidia.package.out}/lib/systemd/system-sleep/nvidia "$@"
|
||||
'';
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
vulkan-tools
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user