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.
This commit is contained in:
2026-08-07 14:36:36 +01:00
parent f8552bc7bb
commit a009a4d984
4 changed files with 145 additions and 0 deletions
+15
View File
@@ -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,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;
};
}
+10
View File
@@ -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
];
};
}
+1
View File
@@ -19,6 +19,7 @@
discord discord
gaming gaming
bottles bottles
netops
]; ];
}; };
} }