{ config, lib, options, sane-lib, ... }: let sane-user-cfg = config.sane.user; cfg = config.sane.users; path-lib = sane-lib.path; serviceType = with lib; types.submodule { options = { # these aoptions are mostly copied from systemd. could be improved. 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. ''; }; after = mkOption { type = types.listOf types.str; default = []; }; bindsTo = mkOption { type = types.listOf types.str; default = []; }; before = mkOption { type = types.listOf types.str; default = []; }; wantedBy = mkOption { type = types.listOf types.str; default = []; }; wants = mkOption { type = types.listOf types.str; default = []; }; script = mkOption { type = types.nullOr types.lines; default = null; }; environment = mkOption { type = types.attrsOf types.str; default = {}; description = '' environment variables to set within the service. ''; }; serviceConfig.Type = mkOption { type = types.enum [ "dbus" "oneshot" "simple" ]; }; serviceConfig.ExecStart = mkOption { type = types.nullOr (types.coercedTo types.package toString types.str); default = null; }; serviceConfig.ExecStartPre = mkOption { type = types.nullOr (types.coercedTo types.package toString types.str); default = null; }; serviceConfig.ExecStartPost = mkOption { type = types.nullOr (types.coercedTo types.package toString types.str); default = null; }; serviceConfig.ExecStopPost = mkOption { type = types.nullOr types.str; default = null; }; serviceConfig.BusName = mkOption { type = types.nullOr types.str; default = null; description = '' name of the dbus name this service is expected to register. only once the name is registered will the service be considered "active". ''; }; serviceConfig.RemainAfterExit = mkOption { type = types.bool; default = false; }; serviceConfig.Restart = mkOption { type = types.nullOr (types.enum [ "always" "on-failure" ]); default = null; # N.B.: systemd doesn't allow always/on-failure for Type="oneshot" services }; serviceConfig.RestartSec = mkOption { type = types.str; default = "20s"; }; unitConfig.ConditionEnvironment = mkOption { type = types.nullOr types.str; default = null; }; }; }; userOptions = { options = with lib; { fs = mkOption { # 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. type = types.attrsOf (types.coercedTo types.attrs (a: [ a ]) (types.listOf types.attrs)); default = {}; description = '' entries to pass onto `sane.fs` after prepending the user's home-dir to the path and marking them as wanted. e.g. `sane.users.colin.fs."/.config/aerc" = X` => `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. 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) ''; }; persist = mkOption { type = options.sane.persist.sys.type; default = {}; description = '' entries to pass onto `sane.persist.sys` after prepending the user's home-dir to the path. ''; }; environment = mkOption { type = types.attrsOf types.str; default = {}; description = '' environment variables to place in user's shell profile. these end up in ~/.profile ''; }; services = mkOption { # see: # type = utils.systemdUtils.types.services; # `utils.systemdUtils.types.services` is nearly what we want, but remove `stage2ServiceConfig`, # as we don't want to force a PATH for every service. type = types.attrsOf serviceType; default = {}; description = '' services to define for this user. populates files in ~/.config/systemd. ''; }; }; }; defaultUserOptions = with lib; { 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; }; }; }; userModule = let nixConfig = config; in with lib; types.submodule ({ name, config, ... }: { options = userOptions.options // { default = mkOption { type = types.bool; default = false; description = '' only one default user may exist. this option determines what the `sane.user` shorthand evaluates to. ''; }; 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}"; }; }; config = lib.mkMerge [ # if we're the default user, inherit whatever settings were routed to the default user (lib.mkIf config.default { inherit (sane-user-cfg) fs persist environment; services = lib.mapAttrs (_: lib.mkMerge) sane-user-cfg.services; }) { fs."/".dir.acl = { 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; }; # ~/.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` fs.".config/environment.d/10-sane-nixos-users.conf".symlink.text = let env = lib.mapAttrsToList (key: value: ''${key}=${value}'') config.environment ; in lib.concatStringsSep "\n" env + "\n"; fs.".profile".symlink.text = '' # 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 ''; } ]; }); processUser = user: defn: let prefixWithHome = lib.mapAttrs' (path: value: { name = path-lib.concat [ defn.home path ]; inherit value; }); makeWanted = lib.mapAttrs (_path: values: lib.mkMerge (values ++ [{ # default if not otherwise provided wantedBeforeBy = lib.mkDefault [ "multi-user.target" ]; }])); in { sane.fs = makeWanted (prefixWithHome defn.fs); sane.defaultUser = lib.mkIf defn.default user; # `byPath` is the actual output here, computed from the other keys. sane.persist.sys.byPath = prefixWithHome defn.persist.byPath; }; in { imports = [ ./s6-rc.nix ./systemd.nix ]; options = with lib; { 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. ''; }; sane.user = mkOption { type = types.nullOr (types.submodule defaultUserOptions); default = null; description = '' options to pass down to the default user ''; }; sane.defaultUser = mkOption { type = types.nullOr types.str; 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}".`. ''; }; }; config = let configs = lib.mapAttrsToList processUser cfg; num-default-users = lib.count (u: u.default) (lib.attrValues cfg); take = f: { sane.fs = f.sane.fs; sane.persist.sys.byPath = f.sane.persist.sys.byPath; sane.defaultUser = f.sane.defaultUser; }; in lib.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..default = true` for some user"; } { assertion = num-default-users <= 1; message = "cannot set more than one default user"; } ]; } ]; }