2023-09-12 04:44:07 +00:00
{ config , lib , options , pkgs , sane-lib , utils , . . . }:
2023-02-02 12:31:13 +00:00
let
2023-11-18 22:06:08 +00:00
saneCfg = config . sane ;
2023-02-02 12:31:13 +00:00
cfg = config . sane . programs ;
2023-07-03 07:55:05 +00:00
2023-07-03 07:04:57 +00:00
# create a map:
# {
# "${pkgName}" = {
# system = true|false;
2023-07-03 07:16:24 +00:00
# user = {
2023-07-03 07:04:57 +00:00
# "${name}" = true|false;
# };
# };
# }
# for every ${pkgName} in pkgSpecs.
# `system = true|false` is a computed expression over all the other programs, as evaluated.
solveDefaultEnableFor = pkgSpecs : lib . foldlAttrs (
acc : pname : pval : (
# add "${enableName}".system |= areSuggestionsEnabled pval
2023-07-03 07:16:24 +00:00
# for each `enableName` in pval.suggestedPrograms.
# do the same for `user` field.
2023-07-03 07:04:57 +00:00
lib . foldl ( acc' : enableName : acc' // {
2023-07-03 07:16:24 +00:00
" ${ enableName } " = let
super = acc' . " ${ enableName } " ;
in {
system = super . system || ( pval . enableFor . system && pval . enableSuggested ) ;
user = super . user // lib . filterAttrs ( _u : en : en && pval . enableSuggested ) pval . enableFor . user ;
} ;
2023-07-03 07:04:57 +00:00
} ) acc pval . suggestedPrograms
)
2023-07-03 07:16:24 +00:00
) ( mkDefaultEnables pkgSpecs ) pkgSpecs ;
mkDefaultEnables = lib . mapAttrs ( _pname : _pval : { system = false ; user = { } ; } ) ;
2023-07-03 07:04:57 +00:00
defaultEnables = solveDefaultEnableFor cfg ;
2024-01-20 11:11:12 +00:00
# wrap a package so that its binaries (maybe) run in a sandbox
2024-01-22 03:50:28 +00:00
wrapPkg = { fs , net , persist , sandbox , . . . }: package : (
2024-01-21 23:59:15 +00:00
if sandbox . method == null then
2024-01-20 11:11:12 +00:00
package
2024-01-21 23:59:15 +00:00
else if sandbox . method == " f i r e j a i l " then
2024-01-21 01:04:31 +00:00
let
2024-01-21 04:28:48 +00:00
# XXX: firejail needs suid bit for some (not all) of its sandboxing methods. hence, rely on the user installing it system-wide and call it by suid path.
firejailBin = " / r u n / w r a p p e r s / b i n / f i r e j a i l " ;
2024-01-22 03:50:28 +00:00
2024-01-21 23:59:15 +00:00
allowPath = p : [
" - - n o b l a c k l i s t = ${ p } "
" - - w h i t e l i s t = ${ p } "
] ;
2024-01-22 03:50:28 +00:00
allowHomePath = p : allowPath '' ''$ { H O M E } / ${ p } '' ;
allowPaths = paths : lib . flatten ( builtins . map allowPath paths ) ;
allowHomePaths = paths : lib . flatten ( builtins . map allowHomePath paths ) ;
fsFlags = allowHomePaths ( builtins . attrNames fs ) ;
persistFlags = allowHomePaths ( builtins . attrNames persist . byPath ) ;
vpn = lib . findSingle ( v : v . default ) null null ( builtins . attrValues config . sane . vpn ) ;
vpnFlags = [
" - - n e t = ${ vpn . bridgeDevice } "
] ++ ( builtins . map ( addr : " - - d n s = ${ addr } " ) vpn . dns ) ;
2024-01-21 23:59:15 +00:00
firejailFlags = [
# "--quiet" #< TODO: enable
# "--tracelog" # logs blacklist violations to syslog (but default firejail disallows this)
] ++ allowPath " / r u n / c u r r e n t - s y s t e m " #< for basics like `ls`, and all this program's `suggestedPrograms`
2024-01-22 03:50:28 +00:00
# ++ allowPath "/bin/sh" #< to allow `firejail --join=...` (doesn't work)
++ allowPath " / r u n / s y s t e m d / r e s o l v e " #< to allow reading /etc/resolv.conf, which ultimately symlinks here
++ allowPaths [ " / r u n / o p e n g l - d r i v e r " " / r u n / o p e n g l - d r i v e r - 3 2 " ] #< symlinks to /nix/store; needed by e.g. mpv
2024-01-21 23:59:15 +00:00
++ fsFlags
2024-01-22 03:50:28 +00:00
++ persistFlags
2024-01-21 23:59:15 +00:00
++ lib . optionals ( net == " v p n " ) vpnFlags ;
firejailBase = pkgs . writeShellScript
2024-01-22 03:50:28 +00:00
" f i r e j a i l - ${ package . pname or package . name or " u n k n o w n " } - b a s e "
2024-01-21 23:59:15 +00:00
'' e x e c ${ firejailBin } ${ lib . escapeShellArgs firejailFlags } \ '' ;
2024-01-21 04:28:48 +00:00
# two ways i could wrap a package in a sandbox:
# 1. package.overrideAttrs, with `postFixup`.
# 2. pkgs.symlinkJoin, or pkgs.runCommand, creating an entirely new package which calls into the inner binaries.
#
# no.2 would require special-casing for .desktop files, to ensure they refer to the jailed version.
# no.1 may require extra care for recursive binaries, or symlink-heavy binaries (like busybox)
# but even no.2 has to consider such edge-cases, just less frequently.
# no.1 may bloat rebuild times.
#
# ultimately, no.1 is probably more reliable, but i expect i'll factor out a switch to allow either approach -- particularly when debugging package buld failures.
2024-01-21 23:59:15 +00:00
packageWrapped = package . overrideAttrs ( unwrapped : {
2024-01-21 04:28:48 +00:00
postFixup = ( unwrapped . postFixup or " " ) + ''
2024-01-22 03:50:28 +00:00
tryFirejailProfile ( ) {
2024-01-21 04:28:48 +00:00
_maybeProfile = " ${ pkgs . firejail } / e t c / f i r e j a i l / $ 1 . p r o f i l e "
2024-01-22 03:50:28 +00:00
echo " c h e c k i n g f o r f i r e j a i l p r o f i l e a t : $ _ m a y b e P r o f i l e "
2024-01-21 04:28:48 +00:00
if [ - e " $ _ m a y b e P r o f i l e " ] ; then
2024-01-22 03:50:28 +00:00
firejailProfilePath = " $ _ m a y b e P r o f i l e "
firejailProfileName = " $ 1 "
true
2024-01-21 04:28:48 +00:00
else
2024-01-22 03:50:28 +00:00
false
2024-01-21 04:28:48 +00:00
fi
}
2024-01-22 03:50:28 +00:00
tryFirejailProfileFromBinMap ( ) {
case " $ 1 " in
$ { builtins . concatStringsSep " \n " ( lib . mapAttrsToList
( bin : profile : ''
( $ { bin } )
tryFirejailProfile " ${ profile } "
; ;
'' )
sandbox . binMap
) }
( * )
echo " n o s p e c i a l - c a s e p r o f i l e f o r $ 1 "
false
; ;
esac
}
getFirejailProfile ( ) {
tryFirejailProfileFromBinMap " $ 1 " \
|| tryFirejailProfile " $ 1 " \
|| tryFirejailProfile " ${ unwrapped . pname or " " } " \
|| tryFirejailProfile " ${ unwrapped . name or " " } " \
|| ( echo " f a i l e d t o l o c a t e f i r e j a i l p r o f i l e f o r $ 1 : a b o r t i n g ! " && false )
}
2024-01-21 04:28:48 +00:00
firejailWrap ( ) {
name = " $ 1 "
getFirejailProfile " $ n a m e "
mv " $ o u t / b i n / $ n a m e " " $ o u t / b i n / . $ n a m e - f i r e j a i l e d "
2024-01-21 23:59:15 +00:00
cat < < EOF > > " t m p - f i r e j a i l - $ n a m e "
2024-01-22 03:50:28 +00:00
- - profile = " $ f i r e j a i l P r o f i l e P a t h " \
- - join-or-start = " $ f i r e j a i l P r o f i l e N a m e " \
2024-01-21 23:59:15 +00:00
- - " $ o u t / b i n / . $ n a m e - f i r e j a i l e d " " \$ @ "
2024-01-21 04:28:48 +00:00
EOF
2024-01-21 23:59:15 +00:00
cat $ { firejailBase } " t m p - f i r e j a i l - $ n a m e " > " $ o u t / b i n / $ n a m e "
chmod + x " $ o u t / b i n / $ n a m e "
2024-01-21 04:28:48 +00:00
}
2024-01-21 23:59:15 +00:00
for _p in $ ( ls " $ o u t / b i n / " ) ; do
firejailWrap " $ _ p "
2024-01-21 01:04:31 +00:00
done
2024-01-21 23:59:15 +00:00
# stamp file which can be consumed to ensure this wrapping code was actually called.
mkdir - p $ out/nix-support
touch $ out/nix-support/sandboxed-firejail
2024-01-21 01:04:31 +00:00
'' ;
2024-01-21 04:28:48 +00:00
meta = ( unwrapped . meta or { } ) // {
# take precedence over non-sandboxed versions of the same binary.
priority = ( ( unwrapped . meta or { } ) . priority or 0 ) - 1 ;
} ;
2024-01-21 23:59:15 +00:00
passthru = ( unwrapped . passthru or { } ) // {
2024-01-22 03:50:28 +00:00
checkSandboxed = pkgs . runCommand " ${ unwrapped . name or unwrapped . pname or " u n k n o w n " } - c h e c k - s a n d b o x e d " { } ''
2024-01-21 23:59:15 +00:00
# this pseudo-package gets "built" as part of toplevel system build.
# if the build is failing here, that means the program isn't properly sandboxed:
# make sure that "postFixup" gets called as part of the package's build script
test - f " ${ packageWrapped } / n i x - s u p p o r t / s a n d b o x e d - ${ sandbox . method } " \
&& touch " $ o u t "
'' ;
} ;
} ) ;
in
packageWrapped
2024-01-20 11:11:12 +00:00
else
throw " u n k n o w n n e t t y p e ' ${ net } ' "
) ;
2023-07-03 07:55:05 +00:00
pkgSpec = with lib ; types . submodule ( { config , name , . . . }: {
2023-02-02 12:31:13 +00:00
options = {
2024-01-20 11:11:12 +00:00
packageUnwrapped = mkOption {
2023-02-03 03:58:23 +00:00
type = types . nullOr types . package ;
description = ''
package , or ` null ` if the program is some sort of meta set ( in which case it much EXPLICITLY be set null ) .
'' ;
2023-02-03 04:23:26 +00:00
default =
let
2023-07-03 07:55:05 +00:00
pkgPath = lib . splitString " . " name ;
2023-02-03 04:23:26 +00:00
in
# package can be inferred by the attr name, allowing shorthand like
2023-02-03 05:26:57 +00:00
# `sane.programs.nano.enable = true;`
2023-02-03 04:23:26 +00:00
# this indexing will throw if the package doesn't exist and the user forgets to specify
# a valid source explicitly.
2023-07-03 07:55:05 +00:00
lib . getAttrFromPath pkgPath pkgs ;
2023-02-02 12:31:13 +00:00
} ;
2024-01-20 11:11:12 +00:00
package = mkOption {
type = types . nullOr types . package ;
description = ''
assigned internally .
this is ` packageUnwrapped ` , but with the binaries possibly wrapped in sandboxing measures .
'' ;
} ;
2023-02-02 12:31:13 +00:00
enableFor . system = mkOption {
type = types . bool ;
2023-07-03 07:16:24 +00:00
default = defaultEnables . " ${ name } " . system ;
2023-02-02 12:31:13 +00:00
description = ''
place this program on the system PATH
'' ;
} ;
enableFor . user = mkOption {
type = types . attrsOf types . bool ;
2023-07-03 07:16:24 +00:00
default = defaultEnables . " ${ name } " . user ;
2023-02-02 12:31:13 +00:00
description = ''
place this program on the PATH for some specified user ( s ) .
'' ;
} ;
2023-04-23 23:21:08 +00:00
enabled = mkOption {
type = types . bool ;
description = ''
generated ( i . e . read-only ) value indicating if the program is enabled either for any user or for the system .
'' ;
} ;
2023-02-02 12:31:13 +00:00
suggestedPrograms = mkOption {
type = types . listOf types . str ;
default = [ ] ;
description = ''
list of other programs a user may want to enable alongside this one .
for example , the gnome desktop environment would suggest things like its settings app .
'' ;
} ;
enableSuggested = mkOption {
type = types . bool ;
default = true ;
} ;
2023-07-15 10:04:22 +00:00
mime . priority = mkOption {
type = types . int ;
default = 100 ;
description = ''
program with the numerically lower priority takes precedence whenever two mime associations overlap .
'' ;
} ;
mime . associations = mkOption {
2023-07-15 08:44:18 +00:00
type = types . attrsOf types . str ;
default = { } ;
description = ''
mime associations . each entry takes the form of :
" < m i m e t y p e > " = " < l a u n c h e r > . d e s k t o p "
e . g .
{
" a u d i o / f l a c " = " v l c . d e s k t o p " ;
" a p p l i c a t i o n / p d f " = " o r g . g n o m e . E v i n c e . d e s k t o p " ;
}
'' ;
} ;
2023-12-11 03:03:22 +00:00
mime . urlAssociations = mkOption {
# TODO: it'd be cool to have the value part of this be `.desktop` files.
# mimeo doesn't quite do that well. would need a wrapper script which does `mimeo --desk2field Exec mpv.desktop` to get the command
# and then interpolate the paths into it (%U)
type = types . attrsOf types . str ;
default = { } ;
description = ''
map of regex -> command .
e . g . " ^ h t t p s ? : / / ( w w w . ) ? y o u t u b e . c o m / w a t c h \? . * v = " = " m p v % U "
'' ;
} ;
2023-07-13 07:17:09 +00:00
persist = mkOption {
type = options . sane . persist . sys . type ;
default = { } ;
description = ''
entries to pass onto ` sane . persist . sys ` or ` sane . user . persist `
when this program is enabled .
'' ;
2023-02-02 12:31:13 +00:00
} ;
2023-04-24 06:49:56 +00:00
fs = mkOption {
2023-09-12 05:44:53 +00:00
# funny type to allow deferring the option merging down to the layer below
2023-07-18 11:25:27 +00:00
type = types . attrsOf ( types . coercedTo types . attrs ( a : [ a ] ) ( types . listOf types . attrs ) ) ;
2023-04-24 06:49:56 +00:00
default = { } ;
description = " f i l e s t o p o p u l a t e w h e n t h i s p r o g r a m i s e n a b l e d " ;
} ;
2023-04-26 00:17:04 +00:00
secrets = mkOption {
type = types . attrsOf types . path ;
default = { } ;
description = ''
fs paths to link to some decrypted secret .
the secret will have same owner as the user under which the program is enabled .
'' ;
} ;
2023-06-27 10:24:48 +00:00
env = mkOption {
type = types . attrsOf types . str ;
default = { } ;
description = " e n v i r o n m e n t v a r i a b l e s t o s e t w h e n t h i s p r o g r a m i s e n a b l e d " ;
} ;
2023-09-12 04:44:07 +00:00
services = mkOption {
# see: <repo:nixos/nixpkgs:nixos/lib/utils.nix>
# type = utils.systemdUtils.types.services;
2023-09-12 05:44:53 +00:00
# map to listOf attrs so that we can allow multiple assigners to the same service
# w/o worrying about merging at this layer, and defer merging to modules/users instead.
type = types . attrsOf ( types . coercedTo types . attrs ( a : [ a ] ) ( types . listOf types . attrs ) ) ;
2023-09-12 04:44:07 +00:00
default = { } ;
description = ''
systemd services to define if this package is enabled .
currently only defines USER services - - acts as noop for root-enabled packages .
2023-09-12 05:44:53 +00:00
conventions are similar to ` systemd . services ` or ` sane . users . <user> . services ` .
the type at this level is obscured only to as to allow passthrough to ` sane . users ` w / proper option merging
2023-09-12 04:44:07 +00:00
'' ;
} ;
2023-11-18 22:06:08 +00:00
slowToBuild = mkOption {
type = types . bool ;
default = false ;
description = ''
whether this package is very slow , or has unique dependencies which are very slow to build .
marking packages like this can be used to achieve faster , but limited , rebuilds/deploys ( by omitting the package ) .
'' ;
} ;
2024-01-20 11:11:12 +00:00
net = mkOption {
type = types . enum [ " c l e a r n e t " " v p n " ] ;
default = " c l e a r n e t " ;
description = ''
how this app should have its network traffic routed .
- " c l e a r n e t " for unsandboxed network .
- " v p n " to route all traffic over the default VPN .
'' ;
} ;
2024-01-21 23:59:15 +00:00
sandbox . method = mkOption {
type = types . nullOr ( types . enum [ " f i r e j a i l " ] ) ;
default = null ; #< TODO: default to firejail
description = ''
how/whether to sandbox all binaries in the package .
'' ;
} ;
2024-01-22 03:50:28 +00:00
sandbox . binMap = mkOption {
type = types . attrsOf types . str ;
default = { } ;
description = ''
map binName -> sandboxAs .
for example ,
if the package ships ` bin/mpv ` and ` bin/umpv ` , this module might know how to sandbox ` mpv ` but not ` umpv ` .
then set ` sandbox . binMap . umpv = " m p v " ; ` to sandbox ` bin/umpv ` with the same rules as ` bin/mpv `
'' ;
} ;
2023-05-08 09:49:58 +00:00
configOption = mkOption {
type = types . raw ;
default = mkOption {
type = types . submodule { } ;
default = { } ;
} ;
description = ''
declare any other options the program may be configured with .
you probably want this to be a submodule .
the option * definitions * can be set with ` sane . programs . " f o o " . config = . . . ` .
'' ;
} ;
config = config . configOption ;
2023-02-02 12:31:13 +00:00
} ;
2023-11-18 22:06:08 +00:00
config = let
enabledForUser = builtins . any ( en : en ) ( lib . attrValues config . enableFor . user ) ;
passesSlowTest = saneCfg . enableSlowPrograms || ! config . slowToBuild ;
in {
enabled = ( config . enableFor . system || enabledForUser ) && passesSlowTest ;
2024-01-20 11:11:12 +00:00
package = if config . packageUnwrapped == null then
null
else
2024-01-21 23:59:15 +00:00
wrapPkg config config . packageUnwrapped
2024-01-20 11:11:12 +00:00
;
2023-04-23 23:21:08 +00:00
} ;
2023-02-02 12:31:13 +00:00
} ) ;
2023-07-03 07:55:05 +00:00
toPkgSpec = with lib ; types . coercedTo types . package ( p : { package = p ; } ) pkgSpec ;
2023-02-02 12:31:13 +00:00
2023-07-03 07:55:05 +00:00
configs = lib . mapAttrsToList ( name : p : {
2024-01-21 23:59:15 +00:00
assertions = [
{
assertion = ( p . net == " c l e a r n e t " ) || p . sandbox . method != null ;
message = '' p r o g r a m " ${ name } " r e q u e s t s n e t " ${ p . net } " , w h i c h r e q u i r e s s a n d b o x i n g , b u t s a n d b o x i n g w a s d i s a b l e d '' ;
}
] ++ builtins . map ( sug : {
2023-02-04 00:43:00 +00:00
assertion = cfg ? " ${ sug } " ;
message = '' p r o g r a m " ${ sug } " r e f e r e n c e d b y " ${ name } " , b u t n o t d e f i n e d '' ;
} ) p . suggestedPrograms ;
2024-01-21 23:59:15 +00:00
system . checks = lib . optionals ( p . enabled && p . sandbox . method != null && p . package != null ) [
p . package . passthru . checkSandboxed
] ;
2023-06-27 10:24:48 +00:00
# conditionally add to system PATH and env
2023-11-18 22:06:08 +00:00
environment = lib . optionalAttrs ( p . enabled && p . enableFor . system ) {
2023-06-27 10:24:48 +00:00
systemPackages = lib . optional ( p . package != null ) p . package ;
variables = p . env ;
} ;
2023-04-24 06:49:56 +00:00
2023-02-02 12:31:13 +00:00
# conditionally add to user(s) PATH
2023-07-03 07:55:05 +00:00
users . users = lib . mapAttrs ( user : en : {
2023-11-18 22:06:08 +00:00
packages = lib . optional ( p . package != null && en && p . enabled ) p . package ;
2023-02-02 12:31:13 +00:00
} ) p . enableFor . user ;
2023-04-24 06:49:56 +00:00
# conditionally persist relevant user dirs and create files
2023-11-18 22:06:08 +00:00
sane . users = lib . mapAttrs ( user : en : lib . optionalAttrs ( en && p . enabled ) {
2023-09-12 05:44:53 +00:00
inherit ( p ) persist ;
services = lib . mapAttrs ( _ : lib . mkMerge ) p . services ;
2023-06-30 08:50:58 +00:00
environment = p . env ;
2023-07-03 07:55:05 +00:00
fs = lib . mkMerge [
2023-07-08 02:06:44 +00:00
p . fs
2023-05-08 21:41:02 +00:00
# link every secret into the fs:
2023-07-03 07:55:05 +00:00
( lib . mapAttrs
2023-04-26 00:17:04 +00:00
# TODO: user the user's *actual* home directory, don't guess.
( homePath : _src : sane-lib . fs . wantedSymlinkTo " / r u n / s e c r e t s / h o m e / ${ user } / ${ homePath } " )
p . secrets
)
] ;
2023-02-02 12:31:13 +00:00
} ) p . enableFor . user ;
2023-04-26 00:17:04 +00:00
# make secrets available for each user
2023-07-03 07:55:05 +00:00
sops . secrets = lib . concatMapAttrs
2023-11-18 22:06:08 +00:00
( user : en : lib . optionalAttrs ( en && p . enabled ) (
2023-07-03 07:55:05 +00:00
lib . mapAttrs'
2023-04-26 00:17:04 +00:00
( homePath : src : {
# TODO: user the user's *actual* home directory, don't guess.
2023-04-26 03:46:18 +00:00
# XXX: name CAN'T START WITH '/', else sops creates the directories funny.
# TODO: report this upstream.
name = " h o m e / ${ user } / ${ homePath } " ;
2023-04-26 00:17:04 +00:00
value = {
owner = user ;
sopsFile = src ;
format = " b i n a r y " ;
} ;
} )
p . secrets
) )
p . enableFor . user ;
2023-02-02 12:31:13 +00:00
} ) cfg ;
in
{
2023-07-03 07:55:05 +00:00
options = with lib ; {
2023-11-18 22:06:08 +00:00
# TODO: consolidate these options under one umbrella attrset
2023-02-02 12:31:13 +00:00
sane . programs = mkOption {
type = types . attrsOf toPkgSpec ;
default = { } ;
} ;
2023-11-18 22:06:08 +00:00
sane . enableSlowPrograms = mkOption {
type = types . bool ;
default = true ;
description = ''
whether to ship programs which are uniquely slow to build .
'' ;
} ;
2023-02-02 12:31:13 +00:00
} ;
config =
let
take = f : {
2023-02-04 00:43:00 +00:00
assertions = f . assertions ;
2023-02-02 12:31:13 +00:00
environment . systemPackages = f . environment . systemPackages ;
2023-06-27 10:24:48 +00:00
environment . variables = f . environment . variables ;
2023-02-02 12:31:13 +00:00
users . users = f . users . users ;
sane . users = f . sane . users ;
2023-04-26 00:17:04 +00:00
sops . secrets = f . sops . secrets ;
2024-01-21 23:59:15 +00:00
system . checks = f . system . checks ;
2023-02-02 12:31:13 +00:00
} ;
2023-07-03 07:55:05 +00:00
in lib . mkMerge [
2023-02-05 19:34:32 +00:00
( take ( sane-lib . mkTypedMerge take configs ) )
{
# expose the pkgs -- as available to the system -- as a build target.
system . build . pkgs = pkgs ;
2023-07-03 07:49:44 +00:00
sane . programs = lib . mkMerge [
# make a program for every (toplevel) package
( lib . mapAttrs ( _pkgName : _pkg : { } ) pkgs )
# do the same for programs in known groups
( lib . mapAttrs' ( pkgName : _pkg : { name = " c a c e r t . ${ pkgName } " ; value = { } ; } ) pkgs . cacert )
( lib . mapAttrs' ( pkgName : _pkg : { name = " g n o m e . ${ pkgName } " ; value = { } ; } ) pkgs . gnome )
( lib . mapAttrs' ( pkgName : _pkg : { name = " l i b s F o r Q t 5 . ${ pkgName } " ; value = { } ; } ) pkgs . libsForQt5 )
2023-08-07 03:43:37 +00:00
( lib . mapAttrs' ( pkgName : _pkg : { name = " m a t e . ${ pkgName } " ; value = { } ; } ) pkgs . mate )
2023-07-03 07:49:44 +00:00
( lib . mapAttrs' ( pkgName : _pkg : { name = " p l a s m a 5 P a c k a g e s . ${ pkgName } " ; value = { } ; } ) pkgs . plasma5Packages )
( lib . mapAttrs' ( pkgName : _pkg : { name = " p y t h o n 3 P a c k a g e s . ${ pkgName } " ; value = { } ; } ) pkgs . python3Packages )
2023-08-02 21:11:49 +00:00
( lib . mapAttrs' ( pkgName : _pkg : { name = " s a n e - s c r i p t s . ${ pkgName } " ; value = { } ; } ) pkgs . sane-scripts )
2023-07-03 07:49:44 +00:00
( lib . mapAttrs' ( pkgName : _pkg : { name = " s w a y - c o n t r i b . ${ pkgName } " ; value = { } ; } ) pkgs . sway-contrib )
] ;
2023-02-05 19:34:32 +00:00
}
] ;
2023-02-02 12:31:13 +00:00
}