Commit Graph
17 Commits
Author SHA1 Message Date
ahmza a009a4d984 add a netops attrset with winbox and an unraid usb creator repack
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.
2026-08-07 14:36:36 +01:00
ahmza f8552bc7bb run hypridle and hyprsunset as graphical-session user units
both were installed but never launched. they ship their own units, but in
$out/share/systemd/user - not $out/lib/systemd/user - and that distinction
is why they need redeclaring rather than merely enabling:

  * systemd.packages would collect nothing, since 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.
  * wantedBy alone would not help either: with no other keys nixos
    synthesises a replacement unit with an empty [Service] section, and it
    shadows upstream's because /etc/systemd/user takes precedence over
    $XDG_DATA_DIRS.

binding them to graphical-session.target, which uwsm manages properly,
means they start and stop with the session instead of leaking across
logins - same reasoning as udiskie, so that moves into the same attrset.

close to upstream's definitions but not identical: hyprsunset's
Requires=graphical-session.target is dropped, because PartOf already
propagates the stop while Requires would additionally make starting
hyprsunset by hand pull up the whole session target. the Slice=session.slice
asymmetry is upstream's own - hyprsunset sets it, hypridle does not - and is
noted inline so it does not get "fixed" later.

hypridle gets an explicit path because its config shells out to hyprlock,
pidof, brightnessctl, hyprctl, loginctl, systemctl and notify-send. that
config lives 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. flagged in a comment at both ends.
2026-08-06 17:09:30 +01:00
ahmza 29707ba0f6 add a 40gib swapfile and wire up hibernation on shadow
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.
2026-08-06 17:09:14 +01:00
ahmza 67bafd4f73 switch to open nvidia modules and make sleep transitions actually work
three related changes, all needed together to get this card through a
sleep cycle with its framebuffer intact.

open = true. mandatory to set explicitly on driver >= 560, where the
option defaults to null and nixpkgs asserts against null. true because
every card this module targets is turing or later, which is what upstream
recommends open modules for. mkDefault so a future pre-turing host can
override at host level - and note that without it, importing both
gpuNvidia and gpuNvidiaPrime would be an outright merge conflict rather
than something resolvable. this selects between two real builds of the
same release, nvidia-open vs the proprietary kernel modules; it has no
effect on driver version. confirmed after reboot:
/proc/driver/nvidia/version reports the open module.

kernelSuspendNotifier = false. nixpkgs defaults this to
`useOpenModules && version >= 595`, which is silently true here, and that
path wedges the gpu on resume - nv_pm_notifier warns, the card never
comes back, and an unrecoverable gsp rpc storm follows. pinning it false
selects the nvidia-{suspend,hibernate,resume}.service path instead.

installing upstream's system-sleep hook, because those units are wired
only to systemd-{suspend,hibernate}.service, while suspend-then-hibernate
runs as a single systemd-suspend-then-hibernate.service that performs
both transitions internally. no nvidia unit fires for it at all - the
units cannot express it, which is exactly why nvidia ships the hook.
nixpkgs sets systemd.packages from the driver package, but that only
collects lib/systemd/system/, never lib/systemd/system-sleep/, so the
hook is silently discarded.

it cannot be installed verbatim, for two separate reasons.

first, past its suspend-then-hibernate branch it ends in an
unconditional `case "$1" in post) nvidia-sleep.sh resume`. for plain
suspend that duplicates nvidia-resume.service, and destructively:
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. the hook would consume the vt file,
fail to chvt, and the real unit would then find nothing to restore.

second, its suspend-then-hibernate dispatch handles post:*,
pre:hibernate and pre:suspend-after-failed-hibernate - but there is no
pre:suspend, which is the *first* leg of suspend-then-hibernate. it
matched nothing, fell through, and the driver never got its vram-spool
handshake, so with NVreg_PreserveVideoMemoryAllocations=1 the gpu refused
to suspend and the whole operation aborted:

  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

so the wrapper gates out everything except suspend-then-hibernate, fills
in pre:suspend the same way upstream fills in pre:hibernate, and hands
the rest to upstream unchanged. worth reporting upstream - as shipped
that hook cannot work for suspend-then-hibernate whenever
PreserveVideoMemoryAllocations is on.

verified on hardware: plain suspend and plain hibernate both clean with
no chvt failure and the vt file consumed by the right unit, and a full
suspend-then-hibernate cycle now runs all four transitions - suspend
entry (deep), rtc wake, hibernation entry, hibernation exit.
2026-08-06 17:08:55 +01:00
ahmza b7e6418fa6 chainload the windows boot manager from grub on shadow
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.
2026-08-06 17:08:30 +01:00
ahmza fc1772e5db pin the bootloader explicitly and bound the grub menu to 10 generations
grub.enable defaults to true, so adding a boot.loader block silently
switched this host from the systemd-boot it was installed with over to
grub. nixos removes neither the old bootloader's files nor its nvram
entry, so both lived on the esp and the firmware kept booting a frozen
systemd-boot menu. pinning both options means a conflicting definition
elsewhere now fails loudly instead of quietly winning. note the pins
delete nothing - stale EFI/systemd/, loader/ and the leftover nvram entry
still have to be cleared by hand. done on shadow, not verified on t480.

configurationLimit bounds esp usage for real, not just the menu:
install-grub.pl unlinks any $boot/kernels/* that no kept generation
references. sizing measured on shadow rather than assumed - 10 kept
generations referenced 1 bzImage and 3 initrds for 114mib total, about
11mib per generation, because generations share aggressively. the
worst case is higher, around 47mib each, since nixpkgs embeds
/etc/modprobe.d/nixos.conf inside the initrd and so every kernel module
parameter edit mints a fresh one. 10 therefore costs ~115mib typically
and ~470mib worst case against a 1gib esp that also carries the grub
modules, the catppuccin theme, and windows' own boot files.

keeping it conservative matters because overflow fails as `copy ... or
die` in install-grub.pl, which runs *after* the system profile has
already been switched - the result is an activated but unbootable
generation. t480 reaches this module too and its esp may be smaller.
2026-08-06 17:08:15 +01:00
ahmza e556cb31af enable quickshell in hyprland to eventually replace waybar. nix-ld also updated to enable qmlls in nvim 2026-08-06 14:02:26 +01:00
ahmza 35970bfd49 folded t480 hardware config into the flake to get rid of --impure 2026-08-06 14:00:50 +01:00
ahmza a013737fbf mount the games drive eagerly instead of on demand
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.
2026-08-05 22:17:53 +01:00
ahmza 4535616799 move hardware config in-repo per-host and mount the data drives
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.
2026-08-05 22:06:01 +01:00
ahmza ad459e2b4b enable udisks2 and run udiskie as a user unit
udiskie was installed but never actually launched - nothing in the dotfiles autostarts it, and services.udisks2 was off, so even once started it had no daemon to talk to. binds it to graphical-session.target (managed by uwsm) so it starts and stops with the session. only touches removable media: udisks2 reports the internal sata disks as HintSystem=true, so the drives declared in disks.nix are left alone.
2026-08-05 22:05:49 +01:00
ahmza 2db9b77272 rename xorg.xrandr to xrandr to fix deprecation warning
the xorg package set is deprecated in nixpkgs 26.05. pkgs.xrandr resolves to the same xrandr-1.5.4 derivation, so hyprland autostart scripts calling xrandr are unaffected.
2026-08-05 21:51:30 +01:00
ahmza 56cf8f875f allow passwordless sudo for ahmza
adds security.sudo.extraRules with NOPASSWD: ALL. the rule is emitted after the default %wheel SETENV rule and sudoers is last-match-wins, so it takes precedence. lives in the shared user module, so it applies to both shadow and t480.
2026-08-05 21:51:26 +01:00
ahmza 0a25820e48 manage global git config declaratively via programs.git.config
moves user identity and init.defaultBranch into /etc/gitconfig instead of stow-managed dotfiles. note: a per-user ~/.gitconfig still overrides these, so identity must stay out of the stow repo to avoid shadowing.
2026-08-05 21:51:22 +01:00
ahmza 9ded51c865 changes to move opencode to nixpkgs unstable - more up to date and fixes breaking bugs 2026-08-05 21:27:51 +01:00
ahmza 439925604b added shadow config 2026-08-05 20:23:33 +01:00
ahmza dda5d14d63 initial commit 2026-08-05 18:57:23 +01:00