2023-09-12 04:43:23 +00:00
|
|
|
{ config, lib, options, sane-lib, utils, ... }:
|
2023-01-30 08:32:55 +00:00
|
|
|
|
|
|
|
let
|
2023-01-30 08:53:40 +00:00
|
|
|
inherit (builtins) attrValues;
|
|
|
|
inherit (lib) count mapAttrs' mapAttrsToList mkIf mkMerge mkOption types;
|
|
|
|
sane-user-cfg = config.sane.user;
|
2023-01-30 08:32:55 +00:00
|
|
|
cfg = config.sane.users;
|
|
|
|
path-lib = sane-lib.path;
|
2023-01-30 08:53:40 +00:00
|
|
|
userOptions = {
|
2023-01-30 08:32:55 +00:00
|
|
|
options = {
|
|
|
|
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 {
|
|
|
|
# see: <repo:nixos/nixpkgs:nixos/lib/utils.nix>
|
|
|
|
type = utils.systemdUtils.types.services;
|
|
|
|
default = {};
|
|
|
|
description = ''
|
|
|
|
systemd user-services to define for this user.
|
|
|
|
populates files in ~/.config/systemd.
|
|
|
|
'';
|
|
|
|
};
|
2023-01-30 08:32:55 +00:00
|
|
|
};
|
|
|
|
};
|
2023-10-08 17:12:53 +00:00
|
|
|
defaultUserOptions = {
|
|
|
|
options = userOptions.options // {
|
|
|
|
services = mkOption {
|
|
|
|
# type = utils.systemdUtils.types.services;
|
|
|
|
# map to listOf attrs so that we can pass through
|
|
|
|
# w/o worrying about merging at this layer
|
|
|
|
type = types.attrsOf (types.coercedTo types.attrs (a: [ a ]) (types.listOf types.attrs));
|
|
|
|
default = {};
|
|
|
|
inherit (userOptions.options.services) description;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
2023-07-14 23:56:01 +00:00
|
|
|
userModule = let nixConfig = config; in 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-01-30 08:53:40 +00:00
|
|
|
# if we're the default user, inherit whatever settings were routed to the default user
|
2023-10-08 17:12:53 +00:00
|
|
|
(mkIf config.default {
|
|
|
|
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 = {
|
|
|
|
user = name;
|
|
|
|
group = nixConfig.users.users."${name}".group;
|
|
|
|
mode = nixConfig.users.users."${name}".homeMode;
|
|
|
|
};
|
2023-06-30 08:50:58 +00:00
|
|
|
fs.".profile".symlink.text =
|
|
|
|
let
|
|
|
|
env = lib.mapAttrsToList
|
|
|
|
(key: value: ''export ${key}="${value}"'')
|
|
|
|
config.environment
|
|
|
|
;
|
|
|
|
in
|
|
|
|
lib.concatStringsSep "\n" env;
|
|
|
|
}
|
2023-09-12 04:43:23 +00:00
|
|
|
{
|
|
|
|
fs = lib.mkMerge (mapAttrsToList (name: value:
|
|
|
|
let
|
|
|
|
# see: <repo:nixos/nixpkgs:nixos/lib/utils.nix>
|
|
|
|
# see: <repo:nix-community/home-manager:modules/systemd.nix>
|
|
|
|
cleanName = utils.systemdUtils.lib.mkPathSafeName name;
|
|
|
|
generatedUnit = utils.systemdUtils.lib.serviceToUnit name value;
|
|
|
|
#^ generatedUnit contains keys:
|
|
|
|
# - text
|
|
|
|
# - aliases (IGNORED)
|
|
|
|
# - wantedBy
|
|
|
|
# - requiredBy
|
|
|
|
# - enabled (IGNORED)
|
|
|
|
# - overrideStrategy (IGNORED)
|
|
|
|
# TODO: error if one of the above ignored fields are set
|
|
|
|
symlinkData = {
|
|
|
|
text = generatedUnit.text;
|
|
|
|
targetName = "${cleanName}.service"; # systemd derives unit name from symlink target
|
|
|
|
};
|
|
|
|
serviceEntry = {
|
|
|
|
".config/systemd/user/${name}.service".symlink = symlinkData;
|
|
|
|
};
|
|
|
|
wants = builtins.map (wantedBy: {
|
|
|
|
".config/systemd/user/${wantedBy}.wants/${name}.service".symlink = symlinkData;
|
|
|
|
}) generatedUnit.wantedBy;
|
|
|
|
requires = builtins.map (requiredBy: {
|
|
|
|
".config/systemd/user/${requiredBy}.requires/${name}.service".symlink = symlinkData;
|
|
|
|
}) generatedUnit.requiredBy;
|
|
|
|
in lib.mkMerge ([ serviceEntry ] ++ wants ++ requires)
|
|
|
|
) config.services);
|
|
|
|
}
|
2023-06-30 08:50:58 +00:00
|
|
|
];
|
2023-01-30 08:53:40 +00:00
|
|
|
});
|
2023-01-30 10:48:32 +00:00
|
|
|
processUser = user: defn:
|
|
|
|
let
|
|
|
|
prefixWithHome = 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
|
|
|
|
{
|
2023-06-28 03:46:29 +00:00
|
|
|
sane.fs = makeWanted (prefixWithHome defn.fs);
|
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
|
|
|
|
{
|
|
|
|
options = {
|
|
|
|
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 {
|
2023-10-08 17:12:53 +00:00
|
|
|
type = types.nullOr (types.submodule defaultUserOptions);
|
2023-01-30 08:53:40 +00:00
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
options to pass down to the default user
|
|
|
|
'';
|
|
|
|
};
|
2023-01-30 08:32:55 +00:00
|
|
|
};
|
|
|
|
config =
|
|
|
|
let
|
|
|
|
configs = mapAttrsToList processUser cfg;
|
2023-01-30 08:53:40 +00:00
|
|
|
num-default-users = count (u: u.default) (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;
|
2023-01-30 08:32:55 +00:00
|
|
|
};
|
2023-01-30 08:53:40 +00:00
|
|
|
in mkMerge [
|
|
|
|
(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
|
|
|
}
|