2024-03-21 16:59:04 +00:00
|
|
|
{ config, lib, options, pkgs, sane-lib, ... }:
|
2023-01-30 08:32:55 +00:00
|
|
|
|
|
|
|
let
|
2023-01-30 08:53:40 +00:00
|
|
|
sane-user-cfg = config.sane.user;
|
2023-01-30 08:32:55 +00:00
|
|
|
cfg = config.sane.users;
|
|
|
|
path-lib = sane-lib.path;
|
2024-03-21 16:59:04 +00:00
|
|
|
serviceType = with lib; types.submodule ({ config, ... }: {
|
2024-03-16 07:35:54 +00:00
|
|
|
options = {
|
|
|
|
description = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
};
|
|
|
|
documentation = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
|
|
|
description = ''
|
|
|
|
references and links for where to find documentation about this service.
|
|
|
|
'';
|
|
|
|
};
|
2024-03-21 16:02:06 +00:00
|
|
|
depends = mkOption {
|
2024-03-16 07:35:54 +00:00
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
|
|
|
};
|
2024-03-21 16:02:06 +00:00
|
|
|
dependencyOf = mkOption {
|
2024-03-16 07:35:54 +00:00
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
|
|
|
};
|
2024-03-21 16:02:06 +00:00
|
|
|
partOf = mkOption {
|
2024-03-16 07:35:54 +00:00
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
2024-03-21 16:02:06 +00:00
|
|
|
description = ''
|
|
|
|
"bundles" to which this service belongs.
|
|
|
|
e.g. `partOf = [ "default" ];` describes services which should be started "by default".
|
|
|
|
'';
|
2024-03-16 07:35:54 +00:00
|
|
|
};
|
|
|
|
|
2024-03-21 15:05:23 +00:00
|
|
|
command = mkOption {
|
2024-03-16 07:35:54 +00:00
|
|
|
type = types.nullOr (types.coercedTo types.package toString types.str);
|
|
|
|
default = null;
|
2024-03-21 15:05:23 +00:00
|
|
|
description = ''
|
|
|
|
long-running command which represents this service.
|
|
|
|
when the command returns, the service is considered "failed", and restarted unless explicitly `down`d.
|
|
|
|
'';
|
2024-03-16 07:35:54 +00:00
|
|
|
};
|
2024-03-21 15:05:23 +00:00
|
|
|
cleanupCommand = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
2024-03-16 07:35:54 +00:00
|
|
|
default = null;
|
2024-03-21 15:05:23 +00:00
|
|
|
description = ''
|
|
|
|
command which is run after the service has exited.
|
|
|
|
restart of the service (if applicable) is blocked on this command.
|
|
|
|
'';
|
2024-03-16 07:35:54 +00:00
|
|
|
};
|
2024-04-26 21:43:51 +00:00
|
|
|
startCommand = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
command which is run to start the service.
|
|
|
|
this command is expected to exit once the service is up, contrary to the normal `command` argument.
|
|
|
|
mutually exclusive to `command`.
|
|
|
|
'';
|
|
|
|
};
|
2024-03-21 16:59:04 +00:00
|
|
|
readiness.waitCommand = mkOption {
|
2024-03-21 15:05:23 +00:00
|
|
|
type = types.nullOr (types.coercedTo types.package toString types.str);
|
2024-03-16 07:35:54 +00:00
|
|
|
default = null;
|
2024-03-21 15:05:23 +00:00
|
|
|
description = ''
|
2024-03-21 16:59:04 +00:00
|
|
|
command or path to executable which exits zero only when the service is ready.
|
2024-03-21 15:05:23 +00:00
|
|
|
this may be invoked repeatedly (with delay),
|
|
|
|
though it's not an error for it to block either (it may, though, be killed and restarted if it blocks too long)
|
|
|
|
'';
|
2024-03-16 07:35:54 +00:00
|
|
|
};
|
2024-03-21 16:59:04 +00:00
|
|
|
readiness.waitDbus = mkOption {
|
2024-03-16 07:35:54 +00:00
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
name of the dbus name this service is expected to register.
|
2024-03-21 16:59:04 +00:00
|
|
|
only once the name is registered will the service be considered "ready".
|
2024-03-16 07:35:54 +00:00
|
|
|
'';
|
|
|
|
};
|
2024-03-23 13:02:47 +00:00
|
|
|
readiness.waitExists = mkOption {
|
2024-03-23 17:28:29 +00:00
|
|
|
type = types.coercedTo types.str toList (types.listOf types.str);
|
|
|
|
default = [];
|
2024-03-23 13:02:47 +00:00
|
|
|
description = ''
|
|
|
|
path to a directory or file whose existence signals the service's readiness.
|
|
|
|
this is expanded as a shell expression, and may contain variables like `$HOME`, etc.
|
|
|
|
'';
|
|
|
|
};
|
2024-05-07 13:07:26 +00:00
|
|
|
|
|
|
|
restartCondition = mkOption {
|
|
|
|
type = types.enum [ "always" "on-failure" ];
|
|
|
|
default = "always";
|
|
|
|
description = ''
|
|
|
|
when `command` exits, under which condition should it be restarted v.s. should the service be considered down.
|
|
|
|
- "always": restart the service whenever it exits.
|
|
|
|
- "on-failure" restart the service only if `command` exits non-zero.
|
|
|
|
|
|
|
|
note that service restarts are not instantaneous, but have some delay (e.g. 1s).
|
|
|
|
'';
|
|
|
|
};
|
2024-03-16 07:35:54 +00:00
|
|
|
};
|
2024-03-21 16:59:04 +00:00
|
|
|
config = {
|
2024-03-23 13:02:47 +00:00
|
|
|
readiness.waitCommand = lib.mkMerge [
|
|
|
|
(lib.mkIf (config.readiness.waitDbus != null)
|
|
|
|
''${pkgs.systemdMinimal}/bin/busctl --user status "${config.readiness.waitDbus}" > /dev/null''
|
|
|
|
)
|
2024-03-23 17:28:29 +00:00
|
|
|
(lib.mkIf (config.readiness.waitExists != [])
|
|
|
|
# e.g.: test -e /foo -a -e /bar
|
|
|
|
("test -e " + (lib.concatStringsSep " -a -e " config.readiness.waitExists))
|
2024-03-23 13:02:47 +00:00
|
|
|
)
|
|
|
|
];
|
2024-03-21 16:59:04 +00:00
|
|
|
};
|
|
|
|
});
|
2023-01-30 08:53:40 +00:00
|
|
|
userOptions = {
|
2024-03-16 04:58:21 +00:00
|
|
|
options = with lib; {
|
2023-01-30 08:32:55 +00:00
|
|
|
fs = mkOption {
|
2023-09-12 05:44:53 +00:00
|
|
|
# map to listOf attrs so that we can allow multiple assigners to the same path
|
|
|
|
# w/o worrying about merging at this layer, and defer merging to modules/fs instead.
|
2023-07-18 11:25:27 +00:00
|
|
|
type = types.attrsOf (types.coercedTo types.attrs (a: [ a ]) (types.listOf types.attrs));
|
2023-01-30 08:53:40 +00:00
|
|
|
default = {};
|
2023-01-30 08:32:55 +00:00
|
|
|
description = ''
|
2023-07-18 11:25:27 +00:00
|
|
|
entries to pass onto `sane.fs` after prepending the user's home-dir to the path
|
|
|
|
and marking them as wanted.
|
2023-01-30 08:32:55 +00:00
|
|
|
e.g. `sane.users.colin.fs."/.config/aerc" = X`
|
2023-06-28 03:46:29 +00:00
|
|
|
=> `sane.fs."/home/colin/.config/aerc" = { wantedBy = [ "multi-user.target"]; } // X;
|
|
|
|
|
|
|
|
conventions are similar as to toplevel `sane.fs`. so `sane.users.foo.fs."/"` represents the home directory,
|
|
|
|
whereas every other entry is expected to *not* have a trailing slash.
|
2023-07-18 11:25:27 +00:00
|
|
|
|
|
|
|
option merging happens inside `sane.fs`, so `sane.users.colin.fs."foo" = A` and `sane.fs."/home/colin/foo" = B`
|
|
|
|
behaves identically to `sane.fs."/home/colin/foo" = lib.mkMerge [ A B ];
|
|
|
|
(the unusual signature for this type is how we delay option merging)
|
2023-01-30 08:32:55 +00:00
|
|
|
'';
|
|
|
|
};
|
2023-01-30 10:34:36 +00:00
|
|
|
|
|
|
|
persist = mkOption {
|
2023-01-30 10:48:32 +00:00
|
|
|
type = options.sane.persist.sys.type;
|
2023-01-30 10:34:36 +00:00
|
|
|
default = {};
|
|
|
|
description = ''
|
2023-01-30 10:48:32 +00:00
|
|
|
entries to pass onto `sane.persist.sys` after prepending the user's home-dir to the path.
|
2023-01-30 10:34:36 +00:00
|
|
|
'';
|
|
|
|
};
|
2023-06-30 08:50:58 +00:00
|
|
|
|
|
|
|
environment = mkOption {
|
|
|
|
type = types.attrsOf types.str;
|
|
|
|
default = {};
|
|
|
|
description = ''
|
|
|
|
environment variables to place in user's shell profile.
|
|
|
|
these end up in ~/.profile
|
|
|
|
'';
|
|
|
|
};
|
2023-09-12 04:43:23 +00:00
|
|
|
|
|
|
|
services = mkOption {
|
2024-03-16 07:35:54 +00:00
|
|
|
type = types.attrsOf serviceType;
|
2023-09-12 04:43:23 +00:00
|
|
|
default = {};
|
|
|
|
description = ''
|
2024-03-16 07:35:54 +00:00
|
|
|
services to define for this user.
|
2023-09-12 04:43:23 +00:00
|
|
|
'';
|
|
|
|
};
|
2023-01-30 08:32:55 +00:00
|
|
|
};
|
|
|
|
};
|
2024-03-16 04:58:21 +00:00
|
|
|
userModule = let
|
|
|
|
nixConfig = config;
|
|
|
|
in with lib; types.submodule ({ name, config, ... }: {
|
2023-01-30 09:13:43 +00:00
|
|
|
options = userOptions.options // {
|
2023-01-30 08:53:40 +00:00
|
|
|
default = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = ''
|
|
|
|
only one default user may exist.
|
|
|
|
this option determines what the `sane.user` shorthand evaluates to.
|
|
|
|
'';
|
|
|
|
};
|
2023-01-30 11:06:47 +00:00
|
|
|
|
|
|
|
home = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
# XXX: we'd prefer to set this to `config.users.users.home`, but that causes infinite recursion...
|
|
|
|
# TODO: maybe assert that this matches the actual home?
|
|
|
|
default = "/home/${name}";
|
|
|
|
};
|
2023-01-30 08:53:40 +00:00
|
|
|
};
|
|
|
|
|
2023-06-30 08:50:58 +00:00
|
|
|
config = lib.mkMerge [
|
2023-12-06 16:07:24 +00:00
|
|
|
# if we're the default user, inherit whatever settings were routed to the default user
|
2024-03-16 04:58:21 +00:00
|
|
|
(lib.mkIf config.default {
|
2023-10-08 17:12:53 +00:00
|
|
|
inherit (sane-user-cfg) fs persist environment;
|
|
|
|
services = lib.mapAttrs (_: lib.mkMerge) sane-user-cfg.services;
|
|
|
|
})
|
2023-06-30 08:50:58 +00:00
|
|
|
{
|
2023-07-14 23:56:01 +00:00
|
|
|
fs."/".dir.acl = {
|
2023-11-23 01:27:28 +00:00
|
|
|
user = lib.mkDefault name;
|
|
|
|
group = lib.mkDefault nixConfig.users.users."${name}".group;
|
|
|
|
# homeMode defaults to 700; notice: no leading 0
|
|
|
|
mode = "0" + nixConfig.users.users."${name}".homeMode;
|
2023-07-14 23:56:01 +00:00
|
|
|
};
|
2024-05-29 13:26:03 +00:00
|
|
|
|
2024-01-27 09:02:55 +00:00
|
|
|
# ~/.config/environment.d/*.conf is added to systemd user units.
|
|
|
|
# - format: lines of: `key=value`
|
|
|
|
# ~/.profile is added by *some* login shells.
|
|
|
|
# - format: lines of: `export key="value"`
|
|
|
|
# see: `man environment.d`
|
2024-05-29 13:26:03 +00:00
|
|
|
### normally a session manager (like systemd) would set these vars (at least) for me:
|
|
|
|
# - XDG_RUNTIME_DIR
|
|
|
|
# - XDG_SESSION_ID
|
|
|
|
# - XDG_SESSION_CLASS
|
|
|
|
# - XDG_SESSION_TYPE
|
|
|
|
# - XDG_VTNR
|
|
|
|
# - SYSTEMD_EXEC_PID
|
|
|
|
# some of my program-specific environment variables depend on some of these being set,
|
|
|
|
# hence do that early:
|
|
|
|
# TODO: consider moving XDG_RUNTIME_DIR to $HOME/.run
|
|
|
|
fs.".config/environment.d/10-sane-baseline.conf".symlink.text = ''
|
|
|
|
XDG_RUNTIME_DIR=/run/user/${name}
|
|
|
|
'';
|
|
|
|
fs.".config/environment.d/20-sane-nixos-users.conf".symlink.text =
|
2023-06-30 08:50:58 +00:00
|
|
|
let
|
|
|
|
env = lib.mapAttrsToList
|
2024-01-27 09:02:55 +00:00
|
|
|
(key: value: ''${key}=${value}'')
|
2023-06-30 08:50:58 +00:00
|
|
|
config.environment
|
|
|
|
;
|
|
|
|
in
|
2024-01-27 09:02:55 +00:00
|
|
|
lib.concatStringsSep "\n" env + "\n";
|
2024-05-29 13:26:03 +00:00
|
|
|
fs.".profile".symlink.text = lib.mkMerge [
|
|
|
|
(lib.mkBefore ''
|
|
|
|
# sessionCommands: ordered sequence of functions which will be called whenever this file is sourced.
|
|
|
|
# primarySessionCommands: additional functions which will be called only for the main session (i.e. login through GUI).
|
|
|
|
# GUIs are expected to install a function to `primarySessionChecks` which returns true
|
|
|
|
# if primary session initialization is desired (e.g. if this was sourced from a greeter).
|
|
|
|
sessionCommands=()
|
|
|
|
primarySessionCommands=()
|
|
|
|
primarySessionChecks=()
|
|
|
|
|
|
|
|
runCommands() {
|
|
|
|
for c in "$@"; do
|
|
|
|
eval "$c"
|
|
|
|
done
|
|
|
|
}
|
|
|
|
initSession() {
|
|
|
|
runCommands "''${sessionCommands[@]}"
|
|
|
|
}
|
|
|
|
maybeInitPrimarySession() {
|
|
|
|
for c in "''${primarySessionChecks[@]}"; do
|
|
|
|
if eval "$c"; then
|
|
|
|
runCommands "''${primarySessionCommands[@]}"
|
|
|
|
return
|
|
|
|
fi
|
|
|
|
done
|
|
|
|
}
|
|
|
|
|
|
|
|
setVTNR() {
|
|
|
|
# some desktops (e.g. sway) need to know which virtual TTY to render to
|
|
|
|
if [ -v "$XDG_VTNR" ]; then
|
|
|
|
return
|
|
|
|
fi
|
|
|
|
local ttyPath=$(tty)
|
|
|
|
case $ttyPath in
|
|
|
|
(/dev/tty*)
|
|
|
|
export XDG_VTNR=''${ttyPath#/dev/tty}
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
}
|
|
|
|
sessionCommands+=('setVTNR')
|
|
|
|
sourceEnv() {
|
|
|
|
# source env vars and the like, as systemd would. `man environment.d`
|
|
|
|
for env in ~/.config/environment.d/*.conf; do
|
|
|
|
# surround with `set -o allexport` since environment.d doesn't explicitly `export` their vars
|
|
|
|
set -a
|
|
|
|
source "$env"
|
|
|
|
set +a
|
|
|
|
done
|
|
|
|
}
|
|
|
|
sessionCommands+=('sourceEnv')
|
|
|
|
|
|
|
|
'')
|
|
|
|
(lib.mkAfter ''
|
|
|
|
sessionCommands+=('maybeInitPrimarySession')
|
|
|
|
initSession
|
|
|
|
'')
|
|
|
|
];
|
2024-03-23 13:04:12 +00:00
|
|
|
|
|
|
|
services."default" = {
|
|
|
|
description = "service (bundle) which is started by default upon login";
|
|
|
|
};
|
|
|
|
services."graphical-session" = {
|
|
|
|
description = "service (bundle) which is started upon successful graphical login";
|
|
|
|
# partOf = [ "default" ]; #< leave it to the DEs to set this
|
|
|
|
};
|
|
|
|
services."sound" = {
|
|
|
|
description = "service (bundle) which represents functional sound input/output when active";
|
|
|
|
partOf = [ "default" ];
|
|
|
|
};
|
2023-06-30 08:50:58 +00:00
|
|
|
}
|
|
|
|
];
|
2023-01-30 08:53:40 +00:00
|
|
|
});
|
2024-05-29 13:26:03 +00:00
|
|
|
processUser = name: defn:
|
2023-01-30 10:48:32 +00:00
|
|
|
let
|
2024-03-16 04:58:21 +00:00
|
|
|
prefixWithHome = lib.mapAttrs' (path: value: {
|
2023-01-30 11:06:47 +00:00
|
|
|
name = path-lib.concat [ defn.home path ];
|
2023-01-30 10:48:32 +00:00
|
|
|
inherit value;
|
|
|
|
});
|
2023-07-18 11:25:27 +00:00
|
|
|
makeWanted = lib.mapAttrs (_path: values: lib.mkMerge (values ++ [{
|
2023-06-28 03:46:29 +00:00
|
|
|
# default if not otherwise provided
|
2023-07-18 11:25:27 +00:00
|
|
|
wantedBeforeBy = lib.mkDefault [ "multi-user.target" ];
|
|
|
|
}]));
|
2023-01-30 10:48:32 +00:00
|
|
|
in
|
|
|
|
{
|
2024-05-29 13:26:03 +00:00
|
|
|
sane.fs = makeWanted ({
|
|
|
|
"/run/user/${name}" = [{
|
|
|
|
dir.acl = {
|
|
|
|
user = lib.mkDefault name;
|
|
|
|
group = lib.mkDefault config.users.users."${name}".group;
|
|
|
|
# homeMode defaults to 700; notice: no leading 0
|
|
|
|
mode = "0" + config.users.users."${name}".homeMode;
|
|
|
|
};
|
|
|
|
}];
|
|
|
|
} // prefixWithHome defn.fs);
|
|
|
|
sane.defaultUser = lib.mkIf defn.default name;
|
2023-01-30 10:48:32 +00:00
|
|
|
|
|
|
|
# `byPath` is the actual output here, computed from the other keys.
|
|
|
|
sane.persist.sys.byPath = prefixWithHome defn.persist.byPath;
|
|
|
|
};
|
2023-01-30 08:32:55 +00:00
|
|
|
in
|
|
|
|
{
|
2024-03-16 23:48:30 +00:00
|
|
|
imports = [
|
2024-03-17 05:40:31 +00:00
|
|
|
./s6-rc.nix
|
2024-03-16 23:48:30 +00:00
|
|
|
];
|
|
|
|
|
2024-03-16 04:58:21 +00:00
|
|
|
options = with lib; {
|
2023-01-30 08:32:55 +00:00
|
|
|
sane.users = mkOption {
|
|
|
|
type = types.attrsOf userModule;
|
|
|
|
default = {};
|
|
|
|
description = ''
|
|
|
|
options to apply to the given user.
|
|
|
|
the user is expected to be created externally.
|
|
|
|
configs applied at this level are simply transformed and then merged
|
|
|
|
into the toplevel `sane` options. it's merely a shorthand.
|
|
|
|
'';
|
|
|
|
};
|
2023-01-30 08:53:40 +00:00
|
|
|
|
|
|
|
sane.user = mkOption {
|
2024-03-23 13:04:12 +00:00
|
|
|
type = types.nullOr (types.submodule userOptions);
|
2023-01-30 08:53:40 +00:00
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
options to pass down to the default user
|
|
|
|
'';
|
|
|
|
};
|
2024-02-12 14:27:10 +00:00
|
|
|
|
|
|
|
sane.defaultUser = mkOption {
|
2024-02-21 00:25:44 +00:00
|
|
|
type = types.nullOr types.str;
|
2024-02-12 14:27:10 +00:00
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
the name of the default user.
|
|
|
|
other attributes of the default user may be retrieved via
|
|
|
|
`config.sane.users."''${config.sane.defaultUser}".<attr>`.
|
|
|
|
'';
|
|
|
|
};
|
2023-01-30 08:32:55 +00:00
|
|
|
};
|
|
|
|
config =
|
|
|
|
let
|
2024-03-16 04:58:21 +00:00
|
|
|
configs = lib.mapAttrsToList processUser cfg;
|
|
|
|
num-default-users = lib.count (u: u.default) (lib.attrValues cfg);
|
2023-01-30 08:32:55 +00:00
|
|
|
take = f: {
|
|
|
|
sane.fs = f.sane.fs;
|
2023-01-30 10:48:32 +00:00
|
|
|
sane.persist.sys.byPath = f.sane.persist.sys.byPath;
|
2024-02-12 14:27:10 +00:00
|
|
|
sane.defaultUser = f.sane.defaultUser;
|
2023-01-30 08:32:55 +00:00
|
|
|
};
|
2024-03-16 04:58:21 +00:00
|
|
|
in lib.mkMerge [
|
2023-01-30 08:53:40 +00:00
|
|
|
(take (sane-lib.mkTypedMerge take configs))
|
|
|
|
{
|
|
|
|
assertions = [
|
|
|
|
{
|
|
|
|
assertion = sane-user-cfg == null || num-default-users != 0;
|
|
|
|
message = "cannot set `sane.user` without first setting `sane.users.<user>.default = true` for some user";
|
|
|
|
}
|
|
|
|
{
|
|
|
|
assertion = num-default-users <= 1;
|
|
|
|
message = "cannot set more than one default user";
|
|
|
|
}
|
|
|
|
];
|
|
|
|
}
|
|
|
|
];
|
2023-01-30 08:32:55 +00:00
|
|
|
}
|