Compare commits
28
Commits
2db9b77272
..
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 | ||
|
|
e556cb31af | ||
|
|
35970bfd49 | ||
|
|
a013737fbf | ||
|
|
4535616799 | ||
|
|
ad459e2b4b |
@@ -1 +1,6 @@
|
|||||||
.session
|
.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,84 @@
|
|||||||
|
{...}: {
|
||||||
|
# `nix run .#sync-hardware`
|
||||||
|
#
|
||||||
|
# Refreshes the calling host's generated hardware module in-repo, replacing the
|
||||||
|
# old habit of importing /etc/nixos/hardware-configuration.nix by absolute path.
|
||||||
|
#
|
||||||
|
# Why bother: that absolute import left the flake impure (every eval needed
|
||||||
|
# --impure, since the file lives outside the store) and — worse — meant *both*
|
||||||
|
# hosts resolved to whichever machine's hardware file was on disk at build
|
||||||
|
# time. Building t480 from shadow silently baked in shadow's disk UUIDs.
|
||||||
|
#
|
||||||
|
# Run this on a host after changing its hardware, then commit the result.
|
||||||
|
perSystem = {pkgs, ...}: let
|
||||||
|
script = pkgs.writeShellApplication {
|
||||||
|
name = "sync-hardware";
|
||||||
|
runtimeInputs = [pkgs.nixos-install-tools pkgs.git pkgs.coreutils pkgs.nettools];
|
||||||
|
text = ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
host="''${1:-$(hostname)}"
|
||||||
|
|
||||||
|
root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
|
if [ -z "$root" ]; then
|
||||||
|
echo "sync-hardware: run this from inside the config repo." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
dir="$root/modules/hosts/$host"
|
||||||
|
if [ ! -d "$dir" ]; then
|
||||||
|
echo "sync-hardware: no host directory at $dir" >&2
|
||||||
|
echo " known hosts: $(cd "$root/modules/hosts" && echo *)" >&2
|
||||||
|
echo " pass an explicit name with: nix run .#sync-hardware -- <host>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
out="$dir/_hardware.nix"
|
||||||
|
|
||||||
|
# --no-filesystems is load-bearing: fileSystems/swapDevices are declared
|
||||||
|
# by hand in the host's disks.nix. Without the flag this would emit
|
||||||
|
# entries for whatever is mounted right now, duplicating (and conflicting
|
||||||
|
# with) those definitions.
|
||||||
|
header=$(cat <<'EOF'
|
||||||
|
# GENERATED FILE — do not edit by hand.
|
||||||
|
#
|
||||||
|
# Regenerate on the host itself with: nix run .#sync-hardware
|
||||||
|
#
|
||||||
|
# Produced by `nixos-generate-config --show-hardware-config --no-filesystems`.
|
||||||
|
# The --no-filesystems flag is deliberate: fileSystems and swapDevices are
|
||||||
|
# declared by hand in ./disks.nix, so regenerating this file can never clobber
|
||||||
|
# them or emit duplicates for whatever happened to be mounted at the time.
|
||||||
|
#
|
||||||
|
# Leading underscore keeps import-tree from auto-importing this as a
|
||||||
|
# flake-parts module (its filter drops any path containing "/_"); it is
|
||||||
|
# imported explicitly from ./default.nix instead.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
tmp=$(mktemp)
|
||||||
|
trap 'rm -f "$tmp"' EXIT
|
||||||
|
|
||||||
|
# Strip the upstream 3-line "Do not modify" banner; ours replaces it.
|
||||||
|
{
|
||||||
|
printf '%s\n' "$header" | sed 's/^ //'
|
||||||
|
nixos-generate-config --show-hardware-config --no-filesystems | tail -n +4
|
||||||
|
} > "$tmp"
|
||||||
|
|
||||||
|
if [ -f "$out" ] && cmp -s "$tmp" "$out"; then
|
||||||
|
echo "sync-hardware: $out already up to date."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$tmp" "$out"
|
||||||
|
echo "sync-hardware: wrote $out"
|
||||||
|
echo " review with: git diff -- $out"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
apps.sync-hardware = {
|
||||||
|
type = "app";
|
||||||
|
program = "${script}/bin/sync-hardware";
|
||||||
|
meta.description = "Regenerate modules/hosts/<host>/_hardware.nix from the running machine";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
fuzzel
|
fuzzel
|
||||||
ghostty
|
ghostty
|
||||||
waybar
|
waybar
|
||||||
|
quickshell
|
||||||
|
screenshot
|
||||||
];
|
];
|
||||||
in {
|
in {
|
||||||
imports = modules;
|
imports = modules;
|
||||||
@@ -27,6 +29,106 @@
|
|||||||
services.xserver.enable = true;
|
services.xserver.enable = true;
|
||||||
security.polkit.enable = true;
|
security.polkit.enable = true;
|
||||||
|
|
||||||
|
# Removable-media automounting.
|
||||||
|
#
|
||||||
|
# udiskie is only a tray frontend to the udisks2 D-Bus daemon, so the daemon
|
||||||
|
# has to be enabled or udiskie runs but can never mount anything. Note this
|
||||||
|
# covers removable media *only*: udisks2 reports the internal SATA disks as
|
||||||
|
# HintSystem=true, so udiskie leaves them alone. Permanently attached drives
|
||||||
|
# are declared in the host's disks.nix instead.
|
||||||
|
services.udisks2.enable = true;
|
||||||
|
|
||||||
|
# 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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
# hypr* ecosystem (keybinds / hypridle / autostart)
|
# hypr* ecosystem (keybinds / hypridle / autostart)
|
||||||
hyprlock
|
hyprlock
|
||||||
@@ -47,7 +149,6 @@
|
|||||||
udiskie
|
udiskie
|
||||||
brightnessctl
|
brightnessctl
|
||||||
# clipboard
|
# clipboard
|
||||||
wl-clipboard
|
|
||||||
cliphist
|
cliphist
|
||||||
# theming
|
# theming
|
||||||
gsettings-desktop-schemas
|
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; [
|
environment.systemPackages = with pkgs; [
|
||||||
quickshell
|
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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# GENERATED FILE — do not edit by hand.
|
||||||
|
#
|
||||||
|
# Regenerate on the host itself with: nix run .#sync-hardware
|
||||||
|
#
|
||||||
|
# Produced by `nixos-generate-config --show-hardware-config --no-filesystems`.
|
||||||
|
# The --no-filesystems flag is deliberate: fileSystems and swapDevices are
|
||||||
|
# declared by hand in ./disks.nix, so regenerating this file can never clobber
|
||||||
|
# them or emit duplicates for whatever happened to be mounted at the time.
|
||||||
|
#
|
||||||
|
# Leading underscore keeps import-tree from auto-importing this as a
|
||||||
|
# flake-parts module (its filter drops any path containing "/_"); it is
|
||||||
|
# imported explicitly from ./default.nix instead.
|
||||||
|
{ config, lib, pkgs, modulesPath, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports =
|
||||||
|
[ (modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usbhid" "uas" "sd_mod" ];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-amd" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
@@ -5,8 +5,12 @@
|
|||||||
}: {
|
}: {
|
||||||
flake.nixosConfigurations.shadow = inputs.nixpkgs.lib.nixosSystem {
|
flake.nixosConfigurations.shadow = inputs.nixpkgs.lib.nixosSystem {
|
||||||
modules = with self.nixosModules; [
|
modules = with self.nixosModules; [
|
||||||
|
# Generated hardware scan, in-repo so the flake stays pure.
|
||||||
|
# Refresh with `nix run .#sync-hardware` (run on shadow itself).
|
||||||
|
./_hardware.nix
|
||||||
desktop
|
desktop
|
||||||
shadowConfiguration
|
shadowConfiguration
|
||||||
|
shadowDisks
|
||||||
cpuAmd
|
cpuAmd
|
||||||
gpuNvidia
|
gpuNvidia
|
||||||
development
|
development
|
||||||
@@ -15,7 +19,12 @@
|
|||||||
discord
|
discord
|
||||||
gaming
|
gaming
|
||||||
bottles
|
bottles
|
||||||
|
netops
|
||||||
|
prusa-slicer
|
||||||
|
printing
|
||||||
|
via
|
||||||
|
nextcloud
|
||||||
|
sunshine
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
{...}: {
|
||||||
|
# Every filesystem on shadow lives here, hand-maintained.
|
||||||
|
#
|
||||||
|
# hardware.nix is generated with `--no-filesystems` precisely so it never
|
||||||
|
# emits fileSystems/swapDevices entries of its own. That keeps regeneration
|
||||||
|
# (`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 = {pkgs, ...}: let
|
||||||
|
# Secondary data drives, addressed by filesystem label.
|
||||||
|
#
|
||||||
|
# nofail on all of them: it stops a missing or dying disk from becoming a
|
||||||
|
# failed boot dependency that drops the machine into emergency mode.
|
||||||
|
#
|
||||||
|
# On-disk ownership is already ahmza mode 755 on all three, so no uid mapping
|
||||||
|
# is needed — ext4 stores ownership in the filesystem and ignores uid=/gid=.
|
||||||
|
|
||||||
|
# Spinning disks (sda/sdc, both WD, rotational=1). noauto +
|
||||||
|
# x-systemd.automount makes systemd generate an .automount unit and mount on
|
||||||
|
# first access to the path rather than at boot, so they stay parked until
|
||||||
|
# something actually touches them; idle-timeout unmounts them again once
|
||||||
|
# quiet so they can spin back down.
|
||||||
|
#
|
||||||
|
# Note the idle unmount fails with "target is busy" while any shell has its
|
||||||
|
# cwd inside the mount. That is harmless — it simply stays mounted and retries.
|
||||||
|
spinningDrive = label: {
|
||||||
|
device = "/dev/disk/by-label/${label}";
|
||||||
|
fsType = "ext4";
|
||||||
|
options = [
|
||||||
|
"nofail"
|
||||||
|
"noauto"
|
||||||
|
"x-systemd.automount"
|
||||||
|
"x-systemd.idle-timeout=600"
|
||||||
|
"x-systemd.device-timeout=10"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Always-on drive. Used for the games library: it is an SSD (Samsung 870 QVO,
|
||||||
|
# rotational=0) so there is no spin-down to gain, and a Steam library is
|
||||||
|
# better off never sitting behind an automount trigger.
|
||||||
|
#
|
||||||
|
# Unrelated but worth knowing, since it looks like a mount problem: Steam's
|
||||||
|
# FHS sandbox builds its bind list at launch by iterating /*, so a top-level
|
||||||
|
# directory that does not exist when Steam starts is invisible to that
|
||||||
|
# instance for its whole lifetime, and Steam reports it as "not writable".
|
||||||
|
# /mnt only comes into existence when these entries are activated, so Steam
|
||||||
|
# must be restarted after a fresh boot-time change here.
|
||||||
|
eagerDrive = label: {
|
||||||
|
device = "/dev/disk/by-label/${label}";
|
||||||
|
fsType = "ext4";
|
||||||
|
options = [
|
||||||
|
"nofail"
|
||||||
|
"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:
|
||||||
|
# these two must resolve unambiguously, and a label is only as unique as
|
||||||
|
# the last disk someone plugged in.
|
||||||
|
"/" = {
|
||||||
|
device = "/dev/disk/by-uuid/deff63fc-1738-4cfc-a79c-06e58c78471a";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
|
||||||
|
"/boot" = {
|
||||||
|
device = "/dev/disk/by-uuid/5873-AD05";
|
||||||
|
fsType = "vfat";
|
||||||
|
options = ["fmask=0077" "dmask=0077"];
|
||||||
|
};
|
||||||
|
|
||||||
|
"/mnt/dump" = spinningDrive "DUMP";
|
||||||
|
"/mnt/bak" = spinningDrive "BAK";
|
||||||
|
"/mnt/games" = eagerDrive "GAMES";
|
||||||
|
"/mnt/nas" = nfsShare "/srv/nas";
|
||||||
|
"/mnt/media" = nfsShare "/srv/media";
|
||||||
|
};
|
||||||
|
|
||||||
|
# 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 = {
|
networking = {
|
||||||
hostName = "shadow";
|
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
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# GENERATED FILE — do not edit by hand.
|
||||||
|
#
|
||||||
|
# Regenerate on the host itself with: nix run .#sync-hardware
|
||||||
|
#
|
||||||
|
# Produced by `nixos-generate-config --show-hardware-config --no-filesystems`.
|
||||||
|
# The --no-filesystems flag is deliberate: fileSystems and swapDevices are
|
||||||
|
# declared by hand in ./disks.nix, so regenerating this file can never clobber
|
||||||
|
# them or emit duplicates for whatever happened to be mounted at the time.
|
||||||
|
#
|
||||||
|
# Leading underscore keeps import-tree from auto-importing this as a
|
||||||
|
# flake-parts module (its filter drops any path containing "/_"); it is
|
||||||
|
# imported explicitly from ./default.nix instead.
|
||||||
|
{ config, lib, pkgs, modulesPath, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports =
|
||||||
|
[ (modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [ "xhci_pci" "nvme" "usb_storage" "usbhid" "sd_mod" ];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@
|
|||||||
}: {
|
}: {
|
||||||
flake.nixosConfigurations.t480 = inputs.nixpkgs.lib.nixosSystem {
|
flake.nixosConfigurations.t480 = inputs.nixpkgs.lib.nixosSystem {
|
||||||
modules = with self.nixosModules; [
|
modules = with self.nixosModules; [
|
||||||
desktop
|
./_hardware.nix
|
||||||
|
t480Disks
|
||||||
t480Configuration
|
t480Configuration
|
||||||
|
desktop
|
||||||
# CPU microcode + integrated Intel GPU. The T480 also has an NVIDIA MX150;
|
# CPU microcode + integrated Intel GPU. The T480 also has an NVIDIA MX150;
|
||||||
# to enable it in PRIME offload mode, swap gpuIntel for gpuNvidiaPrime
|
# to enable it in PRIME offload mode, swap gpuIntel for gpuNvidiaPrime
|
||||||
# (which includes gpuIntel). Left on Intel-only by default to save power.
|
# (which includes gpuIntel). Left on Intel-only by default to save power.
|
||||||
@@ -17,6 +19,13 @@
|
|||||||
obsidian
|
obsidian
|
||||||
discord
|
discord
|
||||||
gaming
|
gaming
|
||||||
|
winbox
|
||||||
|
cad
|
||||||
|
prusa-slicer
|
||||||
|
netops
|
||||||
|
via
|
||||||
|
t480UsbWakeup
|
||||||
|
moonlight
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{...}: {
|
||||||
|
flake.nixosModules.t480Disks = {...}: {
|
||||||
|
fileSystems = {
|
||||||
|
"/" = {
|
||||||
|
device = "/dev/disk/by-uuid/25316a27-6846-4922-8a45-854b9e40a19f";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
"/boot" = {
|
||||||
|
device = "/dev/disk/by-uuid/562C-B760";
|
||||||
|
fsType = "vfat";
|
||||||
|
options = [ "fmask=0077" "dmask=0077" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
swapDevices = [
|
||||||
|
{ device = "/dev/disk/by-uuid/54e31e3a-af6a-4fd6-8101-4c168d3e482a"; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,6 +7,13 @@
|
|||||||
networking = {
|
networking = {
|
||||||
hostName = "t480";
|
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 = {
|
efi = {
|
||||||
canTouchEfiVariables = true;
|
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 = {
|
grub = {
|
||||||
|
enable = true;
|
||||||
efiSupport = true;
|
efiSupport = true;
|
||||||
device = "nodev";
|
device = "nodev";
|
||||||
theme = pkgs.catppuccin-grub;
|
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 = {
|
plymouth = {
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
nix-ld
|
nix-ld
|
||||||
];
|
];
|
||||||
in {
|
in {
|
||||||
imports =
|
# Hardware/disk config is deliberately NOT imported here. It is per-host and
|
||||||
[
|
# lives in modules/hosts/<host>/{_hardware.nix,disks.nix}, wired up by that
|
||||||
/etc/nixos/hardware-configuration.nix
|
# host's default.nix. Importing it from this shared module used to mean both
|
||||||
]
|
# hosts resolved to whichever machine's /etc/nixos file existed at build time.
|
||||||
++ modules;
|
imports = modules;
|
||||||
services = {
|
services = {
|
||||||
openssh.enable = true;
|
openssh.enable = true;
|
||||||
avahi.enable = true;
|
avahi.enable = true;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
extraGroups = [
|
extraGroups = [
|
||||||
"networkmanager"
|
"networkmanager"
|
||||||
"wheel"
|
"wheel"
|
||||||
|
"uinput"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
hyprland
|
hyprland
|
||||||
sddm
|
sddm
|
||||||
network
|
network
|
||||||
|
yazi
|
||||||
];
|
];
|
||||||
in {
|
in {
|
||||||
imports = modules;
|
imports = modules;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# For Intel+NVIDIA laptops (e.g. the T480), use gpuNvidiaPrime instead.
|
# For Intel+NVIDIA laptops (e.g. the T480), use gpuNvidiaPrime instead.
|
||||||
flake.nixosModules.gpuNvidia = {
|
flake.nixosModules.gpuNvidia = {
|
||||||
config,
|
config,
|
||||||
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}: {
|
}: {
|
||||||
@@ -15,15 +16,147 @@
|
|||||||
|
|
||||||
hardware.nvidia = {
|
hardware.nvidia = {
|
||||||
modesetting.enable = true;
|
modesetting.enable = true;
|
||||||
# Proprietary kernel modules. Set to true only for Turing+ (RTX 20xx and
|
# Open kernel modules. Mandatory to set explicitly on driver >= 560 —
|
||||||
# newer) where the open modules are well supported.
|
# the option defaults to null there and nixpkgs asserts against null, so
|
||||||
open = false;
|
# 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;
|
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;
|
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;
|
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; [
|
environment.systemPackages = with pkgs; [
|
||||||
vulkan-tools
|
vulkan-tools
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
stdenv.cc.cc
|
stdenv.cc.cc
|
||||||
zlib
|
zlib
|
||||||
libusb1
|
libusb1
|
||||||
|
# required for qmlls in nvim
|
||||||
|
brotli
|
||||||
|
unixodbc
|
||||||
|
glib
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user