From 598088b752c6bd545fd3bd420952efe810c88bce Mon Sep 17 00:00:00 2001 From: DavHau Date: Thu, 7 May 2026 10:34:47 +0200 Subject: [PATCH 1/2] lib.modules.systemd: install units under lib/systemd/ NixOS s `systemd.packages` only scans `$pkg/etc/systemd//*` and `$pkg/lib/systemd//*` (see nixpkgs nixos/lib/systemd-lib.nix around the `for fn in $i/etc/systemd/${typeDir}/* $i/lib/systemd/...` loop). The README s documented `systemd.packages = [ x.outputs.systemd-user ]` was therefore a silent no-op: units placed at `$pkg/systemd//` were invisible to NixOS s scanner, so nothing landed under `/etc/systemd//`. Move the writeTextDir target into `lib/systemd//` so the units land where NixOS already looks. Tests updated accordingly. --- checks/systemd.nix | 16 ++++++++-------- lib/modules/systemd.nix | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/checks/systemd.nix b/checks/systemd.nix index f42b193..9fc04da 100644 --- a/checks/systemd.nix +++ b/checks/systemd.nix @@ -279,10 +279,10 @@ let } ); - readUserService = drv: name: builtins.readFile "${drv}/systemd/user/${name}.service"; - readSystemService = drv: name: builtins.readFile "${drv}/systemd/system/${name}.service"; - readUserTimer = drv: name: builtins.readFile "${drv}/systemd/user/${name}.timer"; - readSystemTimer = drv: name: builtins.readFile "${drv}/systemd/system/${name}.timer"; + readUserService = drv: name: builtins.readFile "${drv}/lib/systemd/user/${name}.service"; + readSystemService = drv: name: builtins.readFile "${drv}/lib/systemd/system/${name}.service"; + readUserTimer = drv: name: builtins.readFile "${drv}/lib/systemd/user/${name}.timer"; + readSystemTimer = drv: name: builtins.readFile "${drv}/lib/systemd/system/${name}.timer"; in pkgs.runCommand "systemd-test" { } '' echo "Testing systemd module..." @@ -314,14 +314,14 @@ pkgs.runCommand "systemd-test" { } '' # Test 3: Service name from binName echo "Test 3: Service name from binName" - test -f "${customBinName.outputs.systemd-user}/systemd/user/my-hello.service" || { + test -f "${customBinName.outputs.systemd-user}/lib/systemd/user/my-hello.service" || { echo "FAIL: user service file should be named my-hello.service" - ls -la "${customBinName.outputs.systemd-user}/systemd/user/" + ls -la "${customBinName.outputs.systemd-user}/lib/systemd/user/" exit 1 } - test -f "${customBinName.outputs.systemd-system}/systemd/system/my-hello.service" || { + test -f "${customBinName.outputs.systemd-system}/lib/systemd/system/my-hello.service" || { echo "FAIL: system service file should be named my-hello.service" - ls -la "${customBinName.outputs.systemd-system}/systemd/system/" + ls -la "${customBinName.outputs.systemd-system}/lib/systemd/system/" exit 1 } echo "PASS: service name from binName" diff --git a/lib/modules/systemd.nix b/lib/modules/systemd.nix index 1993ca9..2bd4a63 100644 --- a/lib/modules/systemd.nix +++ b/lib/modules/systemd.nix @@ -61,7 +61,7 @@ let mkOutput = type: let - unitDir = if type == "user" then "systemd/user" else "systemd/system"; + unitDir = if type == "user" then "lib/systemd/user" else "lib/systemd/system"; serviceFile = pkgs.writeTextDir "${unitDir}/${serviceName}.service" (systemdLib.serviceToUnit svc) .text; From 8332e607a193b3005a66e46f16dadb8be82ffca3 Mon Sep 17 00:00:00 2001 From: DavHau Date: Thu, 7 May 2026 10:34:47 +0200 Subject: [PATCH 2/2] modules/niri: ship wrapper-managed user unit with hot-reload Replaces the previous `filesToPatch` of upstream s `share/systemd/user/niri.service` with a unit generated through the `wlib.modules.systemd` helper (now imported by this module). The new unit: - bootstraps a mutable runtime copy of the config under `%t/niri/config.kdl` (= `$XDG_RUNTIME_DIR/niri/config.kdl`) via `ExecStartPre`, and points niri at that copy with `--config`. - declares `ExecReload` as two commands: re-stamp the runtime copy from the latest immutable nix-store snapshot, then issue `niri msg action load-config-file` so niri re-reads the file without restarting. - sets `reloadIfChanged = true`. Combined with the upstream nixpkgs `programs.niri` module, this tells NixOS s switch-to-configuration to convert every "needs restart" verdict on niri.service into a `systemctl --user reload`, letting `nixos-rebuild switch` apply config-only tweaks without dropping the live session. Shell-launched wrappers retain the upstream behaviour: `NIRI_CONFIG` points directly at the build-pinned `/nix/store/...niri.kdl`, so the wrapper script does not depend on `$XDG_RUNTIME_DIR` being set (which would break under `set -u` in non-systemd contexts -- e.g. the niri flake check). Depends on the lib/systemd/ unit-dir fix; without it the generated unit lands somewhere NixOS s `systemd.packages` does not scan and the whole pipeline is a no-op. --- modules/niri/module.nix | 49 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/modules/niri/module.nix b/modules/niri/module.nix index c7f7ce9..0f47b9b 100644 --- a/modules/niri/module.nix +++ b/modules/niri/module.nix @@ -122,6 +122,7 @@ let in { _class = "wrapper"; + imports = [ wlib.modules.systemd ]; options = { settings = lib.mkOption { type = lib.types.submodule { @@ -304,13 +305,55 @@ in ''; }; }; + # Drop the upstream user unit; we ship our own (below) with ExecReload + # wired up to the niri IPC. Keep the desktop entry patched so display + # managers still pick up the wrapper's binary. + config.filesToExclude = [ + "lib/systemd/user/niri.service" + "share/systemd/user/niri.service" + ]; config.filesToPatch = [ "share/applications/*.desktop" - "share/systemd/user/niri.service" ]; config.package = config.pkgs.niri; - config.env = { - NIRI_CONFIG = toString config."config.kdl".path; + + # Shell-launched wrapper: same behaviour as upstream -- niri reads the + # immutable, build-pinned config straight from /nix/store. + config.env.NIRI_CONFIG = toString config."config.kdl".path; + + # Systemd-launched session: bootstrap a mutable runtime copy under + # $XDG_RUNTIME_DIR (systemd specifier %t) and point niri at it via + # `--config`. ExecReload re-stamps that file with the latest immutable + # snapshot and asks niri to re-read it via IPC, so config-only switches + # apply without dropping the session. + config.systemd = { + description = "A scrollable-tiling Wayland compositor"; + bindsTo = [ "graphical-session.target" ]; + before = [ + "graphical-session.target" + "xdg-desktop-autostart.target" + ]; + wants = [ + "graphical-session-pre.target" + "xdg-desktop-autostart.target" + ]; + after = [ "graphical-session-pre.target" ]; + serviceConfig = { + Slice = "session.slice"; + Type = "notify"; + ExecStartPre = "${config.pkgs.coreutils}/bin/install -Dm644 ${config."config.kdl".path} %t/niri/config.kdl"; + ExecStart = "${config.exePath} --session --config %t/niri/config.kdl"; + ExecReload = [ + "${config.pkgs.coreutils}/bin/install -Dm644 ${config."config.kdl".path} %t/niri/config.kdl" + "${config.exePath} msg action load-config-file" + ]; + }; + # Tell NixOS's switch-to-configuration to convert every "needs + # restart" verdict into `systemctl --user reload niri.service`, so + # config-only switches reload the live session via ExecReload instead + # of killing it. The flag is read out of the unit's [Service] section + # at switch time (X-ReloadIfChanged=true). + reloadIfChanged = true; }; config.meta.maintainers = [ lib.maintainers.zimward