Files
cinereal 4fa2493b30 modular-services: add config files to X-Reload-Triggers
Assisted-by: Claude:claude-sonnet-4-7

Signed-off-by: cinereal <cinereal@riseup.net>
2026-05-08 15:56:35 +02:00

225 lines
7.1 KiB
Nix

{
lib,
config,
pkgs,
...
}:
let
inherit (lib) concatMapAttrs mkOption types;
portable-lib = import (pkgs.path + "/lib/services/lib.nix") { inherit lib; };
# Combine a service-name prefix with a unit name. The empty unit name
# `""` is the convention from nixpkgs' portable services for the
# service's *primary* unit (e.g. `services.foo.systemd.user.services.""`
# is the unit named `foo.service`). Sub-units use a hyphenated suffix.
dashed =
before: after:
if after == "" then
before
else if before == "" then
after
else
"${before}-${after}";
# Translate a NixOS-style systemd unit attrset (wantedBy, serviceConfig,
# unitConfig, environment, ...) into the section-based INI shape that
# Home Manager's `systemd.user.<unitType>` expects (Unit/Service/Install).
# Only the common keys are mapped; uncommon options can still be set
# explicitly via `unitConfig` / `serviceConfig` / `socketConfig`.
unitAttrKeys = [
"description"
"documentation"
"requires"
"wants"
"upholds"
"after"
"before"
"bindsTo"
"partOf"
"conflicts"
"requisite"
"onFailure"
"onSuccess"
];
pickSection =
keys: src:
lib.listToAttrs (
lib.concatMap (
k:
lib.optional (src ? ${k} && src.${k} != null && src.${k} != [ ]) {
name = lib.toSentenceCase k;
value = normalizeTargets src.${k};
}
) keys
);
envToList =
env: lib.mapAttrsToList (k: v: "${k}=${toString v}") (lib.filterAttrs (_: v: v != null) env);
normalizeTarget = t: if t == "multi-user.target" then "default.target" else t;
normalizeTargets = v: if lib.isList v then map normalizeTarget v else v;
installSection =
u:
lib.filterAttrs (_: v: v != [ ]) {
WantedBy = map normalizeTarget (u.wantedBy or [ ]);
RequiredBy = map normalizeTarget (u.requiredBy or [ ]);
};
toHmIni = unit: {
Unit = pickSection unitAttrKeys unit // (unit.unitConfig or { });
Service =
(unit.serviceConfig or { })
// lib.optionalAttrs (unit ? environment && unit.environment != { }) {
Environment = envToList unit.environment;
};
Install = installSection unit;
};
toHmIniSocket = sock: {
Unit = pickSection unitAttrKeys sock // (sock.unitConfig or { });
Socket =
(sock.socketConfig or { })
// lib.optionalAttrs (sock ? listenStreams && sock.listenStreams != [ ]) {
ListenStream = sock.listenStreams;
}
// lib.optionalAttrs (sock ? listenDatagrams && sock.listenDatagrams != [ ]) {
ListenDatagram = sock.listenDatagrams;
};
Install = installSection sock;
};
# Evaluate a deferredModule into attrs, then translate.
evalDeferred =
translator: unitModule:
translator
(lib.evalModules {
modules = [
(_: {
# Loose schema: NixOS-style unit attrs include nested sub-attrsets
# (e.g. `serviceConfig`) that need to merge across definitions.
freeformType =
with types;
attrsOf (oneOf [
(attrsOf raw)
(listOf raw)
raw
]);
})
unitModule
];
}).config;
# Collect the `source` store paths of a service's enabled `configData`
# entries. When `text` is set, the upstream `config-data-item.nix` module
# automatically derives `source` via `pkgs.writeText`, so `source` is
# always non-null for enabled entries.
configDataSources =
service:
lib.mapAttrsToList (_: cfg: cfg.source) (
lib.filterAttrs (_: cfg: cfg.enable) (service.configData or { })
);
makeUnits =
translator: unitType: prefix: service:
let
# Wire each service's `configData` sources into the primary unit's
# `X-Reload-Triggers` so `home-manager switch` restarts it whenever
# a config file changes. Preserves any triggers the unit already sets.
triggers = configDataSources service;
primaryTranslator =
if triggers == [ ] then
translator
else
(
unit:
let
ini = translator unit;
existing = lib.toList (ini.Unit.X-Reload-Triggers or [ ]);
in
ini
// {
Unit = ini.Unit // {
X-Reload-Triggers = lib.unique (existing ++ triggers);
};
}
);
in
concatMapAttrs (unitName: unitModule: {
"${dashed prefix unitName}" = evalDeferred (
if unitName == "" then primaryTranslator else translator
) unitModule;
}) service.systemd.${unitType}
// concatMapAttrs (
subName: subService: makeUnits translator unitType (dashed prefix subName) subService
) service.services;
# Lift each service's `configData` entries into `xdg.configFile` paths.
# Mirrors how `nixos/modules/system/service/systemd/system.nix` lifts
# `configData` to `environment.etc`.
makeConfigFiles =
prefix: service:
lib.mapAttrs' (_: cfg: {
name = "home-services/${prefix}/${cfg.name}";
value = lib.filterAttrs (_: v: v != null) {
source = cfg.source or null;
text = cfg.text or null;
inherit (cfg) enable;
};
}) (lib.filterAttrs (_: cfg: cfg.enable) (service.configData or { }))
// concatMapAttrs (
subName: subService: makeConfigFiles (dashed prefix subName) subService
) service.services;
modularServiceConfiguration = portable-lib.configure {
serviceManagerPkgs = pkgs;
extraRootModules = [
./service.nix
./config-data-path.nix
];
extraRootSpecialArgs = {
systemdPackage = pkgs.systemd;
nixpkgsPath = pkgs.path;
xdgConfigHome = config.xdg.configHome;
};
};
in
{
meta.maintainers = [ lib.maintainers.kiara ];
options.home.services = mkOption {
description = ''
Home Manager [modular services](https://nixos.org/manual/nixos/unstable/#modular-services).
Each entry is an abstract service that may declare a {option}`process.argv`
and Home Manager-style {option}`systemd.user.{services,sockets}` units
(INI section shape). Units are emitted under Home Manager's
{option}`systemd.user.services` (and friends) with the service name
as a prefix. Mirrors {option}`system.services` in NixOS.
'';
type = types.attrsOf modularServiceConfiguration.serviceSubmodule;
default = { };
visible = "shallow";
};
config = {
assertions = lib.concatLists (
lib.mapAttrsToList (
name: cfg: portable-lib.getAssertions [ "home" "services" name ] cfg
) config.home.services
);
warnings = lib.concatLists (
lib.mapAttrsToList (
name: cfg: portable-lib.getWarnings [ "home" "services" name ] cfg
) config.home.services
);
systemd.user.services = concatMapAttrs (
name: svc: makeUnits toHmIni "services" name svc
) config.home.services;
systemd.user.sockets = concatMapAttrs (
name: svc: makeUnits toHmIniSocket "sockets" name svc
) config.home.services;
xdg.configFile = concatMapAttrs (name: svc: makeConfigFiles name svc) config.home.services;
};
}