home-manager/modules/systemd.nix

329 lines
10 KiB
Nix

{ config, lib, pkgs, ... }:
let
cfg = config.systemd.user;
inherit (lib) getAttr hm isBool literalExample mkIf mkMerge mkOption types;
# From <nixpkgs/nixos/modules/system/boot/systemd-lib.nix>
mkPathSafeName =
lib.replaceChars [ "@" ":" "\\" "[" "]" ] [ "-" "-" "-" "" "" ];
enabled = cfg.services != { } # \
|| cfg.slices != { } # \
|| cfg.sockets != { } # \
|| cfg.targets != { } # \
|| cfg.timers != { } # \
|| cfg.paths != { } # \
|| cfg.mounts != { } # \
|| cfg.automounts != { } # \
|| cfg.sessionVariables != { };
toSystemdIni = lib.generators.toINI {
listsAsDuplicateKeys = true;
mkKeyValue = key: value:
let
value' = if isBool value then
(if value then "true" else "false")
else
toString value;
in "${key}=${value'}";
};
buildService = style: name: serviceCfg:
let
filename = "${name}.${style}";
pathSafeName = mkPathSafeName filename;
# Needed because systemd derives unit names from the ultimate
# link target.
source = pkgs.writeTextFile {
name = pathSafeName;
text = toSystemdIni serviceCfg;
destination = lib.escapeShellArg "/${filename}";
} + "/${filename}";
wantedBy = target: {
name = "systemd/user/${target}.wants/${filename}";
value = { inherit source; };
};
in lib.singleton {
name = "systemd/user/${filename}";
value = { inherit source; };
} ++ map wantedBy (serviceCfg.Install.WantedBy or [ ]);
buildServices = style: serviceCfgs:
lib.concatLists (lib.mapAttrsToList (buildService style) serviceCfgs);
servicesStartTimeoutMs = builtins.toString cfg.servicesStartTimeoutMs;
unitType = unitKind:
with types;
let primitive = either bool (either int str);
in attrsOf (attrsOf (attrsOf (either primitive (listOf primitive)))) // {
description = "systemd ${unitKind} unit configuration";
};
unitDescription = type: ''
Definition of systemd per-user ${type} units. Attributes are
merged recursively.
</para><para>
Note that the attributes follow the capitalization and naming used
by systemd. More details can be found in
<citerefentry>
<refentrytitle>systemd.${type}</refentrytitle>
<manvolnum>5</manvolnum>
</citerefentry>.
'';
unitExample = type:
literalExample ''
{
${lib.toLower type}-name = {
Unit = {
Description = "Example description";
Documentation = [ "man:example(1)" "man:example(5)" ];
};
${type} = {
};
};
};
'';
sessionVariables = mkIf (cfg.sessionVariables != { }) {
"environment.d/10-home-manager.conf".text = lib.concatStringsSep "\n"
(lib.mapAttrsToList (n: v: "${n}=${toString v}") cfg.sessionVariables)
+ "\n";
};
in {
meta.maintainers = [ lib.maintainers.rycee ];
options = {
systemd.user = {
systemctlPath = mkOption {
default = "${pkgs.systemd}/bin/systemctl";
defaultText = "\${pkgs.systemd}/bin/systemctl";
type = types.str;
description = ''
Absolute path to the <command>systemctl</command> tool. This
option may need to be set if running Home Manager on a
non-NixOS distribution.
'';
};
services = mkOption {
default = { };
type = unitType "service";
description = unitDescription "service";
example = unitExample "Service";
};
slices = mkOption {
default = { };
type = unitType "slices";
description = unitDescription "slices";
example = unitExample "Slices";
};
sockets = mkOption {
default = { };
type = unitType "socket";
description = unitDescription "socket";
example = unitExample "Socket";
};
targets = mkOption {
default = { };
type = unitType "target";
description = unitDescription "target";
example = unitExample "Target";
};
timers = mkOption {
default = { };
type = unitType "timer";
description = unitDescription "timer";
example = unitExample "Timer";
};
paths = mkOption {
default = { };
type = unitType "path";
description = unitDescription "path";
example = unitExample "Path";
};
mounts = mkOption {
default = { };
type = unitType "mount";
description = unitDescription "mount";
example = unitExample "Mount";
};
automounts = mkOption {
default = { };
type = unitType "automount";
description = unitDescription "automount";
example = unitExample "Automount";
};
startServices = mkOption {
default = "suggest";
type = with types;
either bool (enum [ "suggest" "legacy" "sd-switch" ]);
apply = p: if isBool p then if p then "legacy" else "suggest" else p;
description = ''
Whether new or changed services that are wanted by active targets
should be started. Additionally, stop obsolete services from the
previous generation.
</para><para>
The alternatives are
<variablelist>
<varlistentry>
<term><literal>suggest</literal> (or <literal>false</literal>)</term>
<listitem><para>
Use a very simple shell script to print suggested
<command>systemctl</command> commands to run. You will have to
manually run those commands after the switch.
</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>legacy</literal> (or <literal>true</literal>)</term>
<listitem><para>
Use a Ruby script to, in a more robust fashion, determine the
necessary changes and automatically run the
<command>systemctl</command> commands.
</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>sd-switch</literal></term>
<listitem><para>
Use sd-switch, a third party application, to perform the service
updates. This tool offers more features while having a small
closure size. Note, it requires a fully functional user D-Bus
session. Once tested and deemed sufficiently robust, this will
become the default.
</para></listitem>
</varlistentry>
</variablelist>
'';
};
servicesStartTimeoutMs = mkOption {
default = 0;
type = types.ints.unsigned;
description = ''
How long to wait for started services to fail until their start is
considered successful. The value 0 indicates no timeout.
'';
};
sessionVariables = mkOption {
default = { };
type = with types; attrsOf (either int str);
example = { EDITOR = "vim"; };
description = ''
Environment variables that will be set for the user session.
The variable values must be as described in
<citerefentry>
<refentrytitle>environment.d</refentrytitle>
<manvolnum>5</manvolnum>
</citerefentry>.
'';
};
};
};
config = mkMerge [
{
assertions = [{
assertion = enabled -> pkgs.stdenv.isLinux;
message = let
names = lib.concatStringsSep ", " (lib.attrNames ( # \
cfg.services # \
// cfg.slices # \
// cfg.sockets # \
// cfg.targets # \
// cfg.timers # \
// cfg.paths # \
// cfg.mounts # \
// cfg.sessionVariables));
in "Must use Linux for modules that require systemd: " + names;
}];
}
# If we run under a Linux system we assume that systemd is
# available, in particular we assume that systemctl is in PATH.
(mkIf pkgs.stdenv.isLinux {
xdg.configFile = mkMerge [
(lib.listToAttrs ((buildServices "service" cfg.services)
++ (buildServices "slices" cfg.slices)
++ (buildServices "socket" cfg.sockets)
++ (buildServices "target" cfg.targets)
++ (buildServices "timer" cfg.timers)
++ (buildServices "path" cfg.paths)
++ (buildServices "mount" cfg.mounts)
++ (buildServices "automount" cfg.automounts)))
sessionVariables
];
# Run systemd service reload if user is logged in. If we're
# running this from the NixOS module then XDG_RUNTIME_DIR is not
# set and systemd commands will fail. We'll therefore have to
# set it ourselves in that case.
home.activation.reloadSystemd = hm.dag.entryAfter [ "linkGeneration" ]
(let
cmd = {
suggest = ''
PATH=${dirOf cfg.systemctlPath}:$PATH \
bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath"
'';
legacy = ''
PATH=${dirOf cfg.systemctlPath}:$PATH \
${pkgs.ruby}/bin/ruby ${./systemd-activate.rb} \
"''${oldGenPath=}" "$newGenPath" "${servicesStartTimeoutMs}"
'';
sd-switch = let
timeoutArg = if cfg.servicesStartTimeoutMs != 0 then
"--timeout " + servicesStartTimeoutMs
else
"";
in ''
${pkgs.sd-switch}/bin/sd-switch \
''${DRY_RUN:+--dry-run} $VERBOSE_ARG ${timeoutArg} \
''${oldGenPath:+--old-units $oldGenPath/home-files/.config/systemd/user} \
--new-units $newGenPath/home-files/.config/systemd/user
'';
};
ensureRuntimeDir =
"XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$(id -u)}";
systemctl = "${ensureRuntimeDir} ${cfg.systemctlPath}";
in ''
systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true)
if [[ $systemdStatus == 'running' || $systemdStatus == 'degraded' ]]; then
if [[ $systemdStatus == 'degraded' ]]; then
warnEcho "The user systemd session is degraded:"
${systemctl} --user --no-pager --state=failed
warnEcho "Attempting to reload services anyway..."
fi
${ensureRuntimeDir} \
${getAttr cfg.startServices cmd}
else
echo "User systemd daemon not running. Skipping reload."
fi
unset systemdStatus
'');
})
];
}