Arming the built-in xHCI root hubs made them wake on hub-level events
(USB-C PD negotiation, SD card reader, internal peripherals), causing the
laptop to immediately self-wake from suspend. Drop the root-hub rule and arm
only the keyboard leaf by VID:PID; a leaf device's remote-wakeup propagates
through the hub without the hub's own wakeup flag being set.
Add a udev rule (t480UsbWakeup) that arms USB remote-wakeup for a keyboard
on the built-in xHCI controller (00:14.0), so pressing a key wakes the T480
from S3. 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; a built-in-port
keyboard can. Match the keyboard by VID:PID since its USB bus path is unstable.
Also wires up the logind lid-switch policy (suspend undocked, ignore docked)
for the clamshell workflow.
two network/storage admin tools that only shadow needs, grouped behind one
attrset so the host list stays a list of intents rather than of packages.
winbox is a one-liner: pkgs.winbox resolves to winbox4, which nixpkgs
already builds through autoPatchelfHook against the qt/xcb stack it needs.
it does not go anywhere near nix-ld, so nothing has to be added to the
nix-ld library list for it - a comment says so, since the opposite is an
easy assumption to make about a vendored mikrotik binary. the installed
binary is WinBox, capital w, which is worth knowing at a shell prompt.
unraid-usb-creator is not in nixpkgs and upstream ships an appimage only,
so it is a repack. the appimage is a qt6/qml app - a fork of rpi-imager -
built with linuxdeploy, which means it already vendors the whole qt6 stack
plus xcb, icu, gnutls and krb5 under usr/lib. that bundle is kept verbatim
and autopatchelfed against only the handful of libraries linuxdeploy
deliberately leaves to the host: libc, libstdc++, GL, x11, fontconfig and
friends. no FHS wrapper, so no ~400mb buildFHSEnv closure. the usr/ layout
has to survive as a unit because qt.conf sitting next to the binary
declares Prefix=../ with Plugins=plugins and Qml2Imports=qml.
the non-obvious part is appendRunpaths rather than runtimeDependencies for
openssl. qt's tls backend dlopens libssl/libcrypto, so autopatchelf never
sees the dependency, and without it the app cannot fetch the release json
or stream the image. it has to be appendRunpaths because in auto-patchelf
`rpath += runtime_deps` sits inside the set-interpreter branch and so 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's runpath is not consulted - so the path must land on the plugin
itself.
QT_QPA_PLATFORM is pinned to xcb because linuxdeploy bundled only the xcb
platform plugin; there is no libqwayland*.so anywhere in the appimage, so
this always runs through xwayland under hyprland regardless. --set-default
rather than --set, so it can still be overridden if upstream ever ships the
wayland plugin.
udisks2 is enabled from this module even though hyprland/default.nix
already enables it for udiskie. the app enumerates and unmounts removable
drives over the udisks2 d-bus api rather than poking /dev, which is how it
avoids needing root, and repeating the option keeps the module standalone
on a host with no hyprland. bool options merge on equal values, so this
costs nothing.
v1.1.0 is pinned by hash. note the tag is not returned by the github
releases api - which only lists the v2.0.x line - but the asset url that
unraid.net/download redirects to is live and matches.
verified: shadow evaluates clean, both packages build, the .desktop Exec
is rewritten from the bare name to the absolute store path, ldd on the
main binary reports nothing unresolved, and the openssl lib dir is present
on libqopensslbackend.so's runpath. not yet exercised at runtime against
an actual usb stick.
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. the 3090's vram does not
land here: with NVreg_PreserveVideoMemoryAllocations=1 the driver spools it
to NVreg_TemporaryFilePath, which nixpkgs leaves unset and so defaults to
/tmp. that is only safe while /tmp is on the real root - boot.tmp.useTmpfs
would route a full vram dump into ram and break hibernation outright.
resuming needs both boot.resumeDevice and resume_offset. the device half is
mandatory rather than optional here because boot.initrd.systemd.enable is
true, and under systemd initrd the resume= parameter is emitted only from
boot.resumeDevice - the legacy stage-1 script that scans swapDevices is not
in play. without it 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,
since the kernel addresses the file by raw block. that value is hand-copied,
and a stale one fails silently: no swsusp signature at the given block means
`PM: Image not found (code -22)` and a cold boot, with no error at hibernate
time. so an activation check warns on drift. it is a detector, not a gate -
nixpkgs only recreates the swapfile when size changes, so on the rebuild
that resizes it the check still sees the old file and fires on the next one.
it warns rather than fails, and expects to read nothing on a fresh install
where mkswap-var-lib-swapfile.service has not run yet.
sleeping via suspend-then-hibernate (hypridle 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 at ~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 `Image not found`.
full hibernation price paid every time, redeemed only on a power cut.
suspend-then-hibernate defers that write until it is worth doing, and unlike
hybrid-sleep it is a mode nvidia explicitly supports.
HibernateDelaySec is explicit 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.
also corrects a comment: a sparse swapfile does not silently corrupt.
swapon refuses files with holes outright ("skipping - it appears to have
holes"), so the failure is loud and the system simply ends up with no swap.
genuine silent corruption is the copy-on-write case, which is why btrfs
needs a NOCOW-from-creation file.
windows sits on the 476g nvme - the disk with three partitions: a 16m
"microsoft reserved" msr with no filesystem, the 476g ntfs c: volume, and
a 832m winre. it has no efi system partition of its own, because it was
installed against the esp that nixos now owns (fs-uuid 5873-AD05). both
oses therefore 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, so the entry is live. the bcdboot
command is kept in the comment for the next time the esp is reformatted -
it has to run from windows media or winre because the bcd store cannot be
generated from linux, and copying bootmgfw.efi alone fails 0xc000000f.
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, so a
future esp reformat degrades to a missing menu item rather than one that
boots into an error. because grub evaluates it at boot rather than at nix
eval time, repairing windows needed no rebuild.
searching by fs-uuid rather than (hdX,gptN) is deliberate: nvme
enumeration is not stable on this board, and /boot has been seen as both
nvme0n1p1 and nvme1n1p1. the -n "$esp" test is a separate nested if
rather than `[ -n "$esp" -a -f ... ]` because grub's test builtin
evaluates both operands of -a instead of short-circuiting, so the
combined form would still expand the path test against an unset device.
sdb (GAMES) is an ssd - samsung 870 qvo, rotational=0 - so unlike the two wd
spinners there is no spin-down to gain from automounting it, and a steam library
is better off never sitting behind a trigger. keeps nofail. dump and bak stay
lazy.
splits the old dataDrive helper into spinningDrive and eagerDrive and documents
why each drive gets which.
also records the steam gotcha found while debugging "steam library folder not
writable": it was never a permissions problem - the drives are owned by uid 1000
mode 755 and were writable throughout. steam's fhs sandbox builds its bind list
at launch by iterating /*, and /mnt does not exist on nixos until these
fileSystems entries are activated, so a steam started before that has no /mnt
for its entire lifetime. restarting steam is the fix.
importing /etc/nixos/hardware-configuration.nix by absolute path from the
shared core module had two problems: it made the flake impure (every eval
needed --impure, since the file lives outside the store) and both hosts
resolved to whichever machine's file was on disk, so evaluating t480 on shadow
silently baked in shadow's disk uuids.
shadow now carries its own generated scan at modules/hosts/shadow/_hardware.nix.
the leading underscore keeps import-tree from picking it up as a flake-parts
module - its filter drops any path containing "/_" - so it is imported
explicitly from the host's default.nix.
it is generated with --no-filesystems and all of shadow's filesystems are
declared by hand in disks.nix instead. that split is what makes regeneration
safe: nixos-generate-config emits fileSystems for whatever is mounted at the
time, which would otherwise conflict with the entries below every time the data
drives happened to be mounted. `nix run .#sync-hardware` refreshes the file.
DUMP/GAMES/BAK are mounted by label at /mnt/{dump,games,bak} with
noauto + x-systemd.automount, so the two 2.7T spinners stay parked until
something touches the path and spin back down after 10 minutes idle. nofail
keeps a missing disk from dropping the boot into emergency mode.
/ and /boot are unchanged - still the same uuids, verified identical to the
previous evaluation. t480 keeps the absolute import for now with a TODO; its
scan can only be generated on the machine itself.