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 = ''
" b u n d l e s " to which this service belongs .
e . g . ` partOf = [ " d e f a u l t " ] ; ` describes services which should be started " b y d e f a u l t " .
'' ;
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 " f a i l e d " , 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 " r e a d y " .
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 [ " a l w a y s " " o n - f a i l u r e " ] ;
default = " a l w a y s " ;
description = ''
when ` command ` exits , under which condition should it be restarted v . s . should the service be considered down .
- " a l w a y s " : restart the service whenever it exits .
- " o n - f a i l u r e " restart the service only if ` command ` exits non-zero .
note that service restarts are not instantaneous , but have some delay ( e . g . 1 s ) .
'' ;
} ;
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 } / b i n / b u s c t l - - u s e r s t a t u s " ${ config . readiness . waitDbus } " > / d e v / n u l l ''
)
2024-03-23 17:28:29 +00:00
( lib . mkIf ( config . readiness . waitExists != [ ] )
# e.g.: test -e /foo -a -e /bar
( " t e s t - e " + ( lib . concatStringsSep " - a - e " config . readiness . waitExists ) )
2024-03-23 13:02:47 +00:00
)
] ;
2024-03-21 16:59:04 +00:00
} ;
} ) ;
2024-05-30 03:39:32 +00:00
userOptions = 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 . " / . c o n f i g / a e r c " = X `
= > ` sane . fs . " / h o m e / c o l i n / . c o n f i g / a e r c " = { wantedBy = [ " m u l t i - u s e r . t a r g e t " ] ; } // X ;
2023-06-28 03:46:29 +00:00
2024-05-30 03:39:32 +00:00
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
2024-05-30 03:39:32 +00:00
option merging happens inside ` sane . fs ` , so ` sane . users . colin . fs . " f o o " = A ` and ` sane . fs . " / h o m e / c o l i n / f o o " = B `
behaves identically to ` sane . fs . " / h o m e / c o l i n / f o o " = lib . mkMerge [ A B ] ;
( the unusual signature for this type is how we delay option merging )
'' ;
} ;
2023-01-30 10:34:36 +00:00
2024-05-30 03:39:32 +00:00
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 .
'' ;
} ;
2023-06-30 08:50:58 +00:00
2024-05-30 03:39:32 +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
2024-05-30 03:39:32 +00:00
services = mkOption {
type = types . attrsOf serviceType ;
default = { } ;
description = ''
services to define for this user .
'' ;
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 , . . . }: {
2024-05-30 06:00:32 +00:00
options = userOptions // {
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 = " / h o m e / ${ 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
2024-05-30 08:27:42 +00:00
# - e.g. `1`, or `2`. these aren't supposed to be reused during the same power cycle (whatever reuse means), and are used by things like `pipewire`'s context
# - doesn't have to be numeric, could be "colin"
2024-05-29 13:26:03 +00:00
# - XDG_SESSION_CLASS
2024-05-30 08:27:42 +00:00
# - e.g. "user"
2024-05-29 13:26:03 +00:00
# - XDG_SESSION_TYPE
2024-05-30 08:27:42 +00:00
# - e.g. "tty", "wayland"
2024-05-29 13:26:03 +00:00
# - 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 . " . c o n f i g / e n v i r o n m e n t . d / 1 0 - s a n e - b a s e l i n e . c o n f " . symlink . text = ''
2024-06-16 06:01:20 +00:00
HOME = /home / $ { name }
2024-05-29 13:26:03 +00:00
XDG_RUNTIME_DIR = /run/user / $ { name }
'' ;
fs . " . c o n f i g / e n v i r o n m e n t . d / 2 0 - s a n e - n i x o s - u s e r s . c o n f " . 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 . " . p r o f i l e " . 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 " $ X D G _ V T N R " ] ; 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 " $ e n v "
set + a
done
}
sessionCommands + = ( ' sourceEnv' )
'' )
( lib . mkAfter ''
sessionCommands + = ( ' maybeInitPrimarySession' )
initSession
'' )
] ;
2024-03-23 13:04:12 +00:00
2024-05-30 06:00:32 +00:00
# a few common targets one can depend on or declare a `partOf` to.
# for example, a wayland session provider should:
# - declare `myService.partOf = [ "wayland" ];`
# - and `graphical-session.partOf = [ "default" ];`
2024-03-23 13:04:12 +00:00
services . " d e f a u l t " = {
description = " s e r v i c e ( b u n d l e ) w h i c h i s s t a r t e d b y d e f a u l t u p o n l o g i n " ;
} ;
services . " g r a p h i c a l - s e s s i o n " = {
description = " s e r v i c e ( b u n d l e ) w h i c h i s s t a r t e d u p o n s u c c e s s f u l g r a p h i c a l l o g i n " ;
2024-05-30 06:00:32 +00:00
# partOf = [ "default" ];
2024-03-23 13:04:12 +00:00
} ;
services . " s o u n d " = {
description = " s e r v i c e ( b u n d l e ) w h i c h r e p r e s e n t s f u n c t i o n a l s o u n d i n p u t / o u t p u t w h e n a c t i v e " ;
2024-05-30 06:00:32 +00:00
# partOf = [ "default" ];
} ;
services . " w a y l a n d " = {
description = " s e r v i c e ( b u n d l e ) w h i c h p r o v i d e s a w a y l a n d s e s s i o n " ;
} ;
services . " x 1 1 " = {
description = " s e r v i c e ( b u n d l e ) w h i c h p r o v i d e s a x 1 1 s e s s i o n ( p o s s i b l y v i a x w a y l a n d ) " ;
2024-03-23 13:04:12 +00:00
} ;
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 [ " m u l t i - u s e r . t a r g e t " ] ;
} ] ) ) ;
2023-01-30 10:48:32 +00:00
in
{
2024-05-29 13:26:03 +00:00
sane . fs = makeWanted ( {
" / r u n / u s e r / ${ 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-05-30 03:39:32 +00:00
type = types . nullOr ( types . submodule { options = userOptions ; } ) ;
2023-01-30 08:53:40 +00:00
default = null ;
description = ''
options to pass down to the default user
'' ;
2024-05-30 03:39:32 +00:00
} // {
_options = userOptions ;
2023-01-30 08:53:40 +00:00
} ;
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 = " c a n n o t s e t ` s a n e . u s e r ` w i t h o u t f i r s t s e t t i n g ` s a n e . u s e r s . < u s e r > . d e f a u l t = t r u e ` f o r s o m e u s e r " ;
}
{
assertion = num-default-users <= 1 ;
message = " c a n n o t s e t m o r e t h a n o n e d e f a u l t u s e r " ;
}
] ;
}
] ;
2023-01-30 08:32:55 +00:00
}