diff --git a/lib/default.nix b/lib/default.nix index 8fea4b8ad637459..66d2f0a09f95161 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -41,6 +41,7 @@ let # serialization cli = callLibs ./cli.nix; + gvariant = callLibs ./gvariant.nix; generators = callLibs ./generators.nix; # misc diff --git a/lib/generators.nix b/lib/generators.nix index 496845fc9ae4d7f..0412a3385f918b1 100644 --- a/lib/generators.nix +++ b/lib/generators.nix @@ -175,6 +175,41 @@ rec { + "\n") + (toINI { inherit mkSectionName mkKeyValue listsAsDuplicateKeys; } sections); + /* Apply a function to a recursive attrset to flatten it and take a additional + function that tells it whether to stop recursing. The function takes a reversed + list of the names and returns a list of flattened name value pairs. + + Example: + flattenAttrsCond + # never stop recursing + (as: true) + (path: value: [ {name = lib.concatStringsSep "." (lib.reverseList path) } ] ) + { a.b = 1; c.d = 2; } + => { "a.b" = 1; "c.d" = 2; } + + Type: + flattenAttrsCond :: (AttrSet -> Bool) -> ([String] -> a -> AttrSet) -> AttrSet -> AttrSet + */ + flattenAttrsCond = + with lib; + # A function, given the attribute set the recursion is currently at, determine if stop recursion. + cond: + # A function, given a reversed list of attribute names and a value, returns a name value pair. + f: + # Attribute set to flatten. + set: + let + recurse = path: + let + g = name: value: + if isAttrs value && !(cond value) + then (recurse ([ name ] ++ path) value) + else f ([ name ] ++ path) value; + in + set': concatLists (mapAttrsToList g set'); + in + foldl recursiveUpdate { } (recurse [ ] set); + /* Generate a git-config file from an attrset. * * It has two major differences from the regular INI format: @@ -213,21 +248,33 @@ rec { let mkKeyValue = mkKeyValueDefault { } " = " k; in concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (lib.toList v)); - # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI - gitFlattenAttrs = let - recurse = path: value: - if isAttrs value && !lib.isDerivation value then - lib.mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value - else if length path > 1 then { - ${concatStringsSep "." (lib.reverseList (tail path))}.${head path} = value; - } else { - ${head path} = value; - }; - in attrs: lib.foldl lib.recursiveUpdate { } (lib.flatten (recurse [ ] attrs)); - toINI_ = toINI { inherit mkKeyValue mkSectionName; }; in - toINI_ (gitFlattenAttrs attrs); + toINI_ (flattenAttrs lib.isDerivation + (path: value: with lib; [{ + "${if length path > 1 then + concatStringsSep "/" (reverseList (tail path)) + else head path}"."${head path}" = value; + }]) attrs); + + # mkKeyValueDefault wrapper that handles dconf INI quirks. + # The main differences of the format is that it requires strings to be quoted. + mkDconfKeyValue = mkKeyValueDefault { mkValueString = v: toString (lib.gvariant.mkValue v); } "="; + + # Generates INI in dconf keyfile style. + toDconfINI = attrs: toINI { mkKeyValue = mkDconfKeyValue; } ( + flattenAttrsCond lib.gvariant.isGVariant + (path: value: with lib; [{ + "${if length path > 1 then + concatStringsSep "/" (reverseList (tail path)) + else head path}"."${head path}" = value; + }]) + attrs); + + # Generates dconf locks file. Each key is a line. + toDconfLocks = with lib; attrs: concatStringsSep "\n" (attrNames (flattenAttrsCond isBool + (path: value: optionals value [{ "/${concatStringsSep "/" (reverseList path)}" = value; }]) + attrs)); /* Generates JSON from an arbitrary (non-function) value. * For more information see the documentation of the builtin. diff --git a/lib/gvariant.nix b/lib/gvariant.nix new file mode 100644 index 000000000000000..e112bad9966225e --- /dev/null +++ b/lib/gvariant.nix @@ -0,0 +1,181 @@ +# This file is taken from https://github.com/nix-community/home-manager +# Copyright (c) 2017-2022 Home Manager contributors +# +# A partial and basic implementation of GVariant formatted strings. +# +# Note, this API is not considered fully stable and it might therefore +# change in backwards incompatible ways without prior notice. + +{ lib }: + +let + inherit (lib) + concatMapStringsSep concatStrings escape hasPrefix head replaceStrings; + + mkPrimitive = t: v: { + _type = "gvariant"; + type = t; + value = v; + __toString = self: "@${self.type} ${toString self.value}"; + }; + + type = { + arrayOf = t: "a${t}"; + maybeOf = t: "m${t}"; + tupleOf = ts: "(${concatStrings ts})"; + dictionaryEntryOf = ts: "{${concatStrings ts}}"; + string = "s"; + boolean = "b"; + uchar = "y"; + int16 = "n"; + uint16 = "q"; + int32 = "i"; + uint32 = "u"; + int64 = "x"; + uint64 = "t"; + double = "d"; + variant = "v"; + }; + + # Returns the GVariant type of a given Nix value. If no type can be + # found for the value then the empty string is returned. + typeOf = v: + with type; + if builtins.isBool v then + boolean + else if builtins.isInt v then + int32 + else if builtins.isFloat v then + double + else if builtins.isString v then + string + else if builtins.isList v then + let elemType = elemTypeOf v; + in if elemType == "" then "" else arrayOf elemType + else if builtins.isAttrs v && v ? type then + v.type + else + ""; + + elemTypeOf = vs: + if builtins.isList vs then + if vs == [ ] then "" else typeOf (head vs) + else + ""; + + mkMaybe = elemType: elem: + mkPrimitive (type.maybeOf elemType) elem // { + __toString = self: + if self.value == null then + "@${self.type} nothing" + else + "just ${toString self.value}"; + }; + +in +rec { + + inherit type typeOf; + + isGVariant = v: v._type or "" == "gvariant"; + + isArray = hasPrefix "a"; + isDictionaryEntry = hasPrefix "{"; + isMaybe = hasPrefix "m"; + isTuple = hasPrefix "("; + + # Returns the GVariant value that most closely matches the given Nix + # value. If no GVariant value can be found then `null` is returned. + + mkValue = v: + if builtins.isBool v then + mkBoolean v + else if builtins.isInt v then + mkInt32 v + else if builtins.isFloat v then + mkDouble v + else if builtins.isString v then + mkString v + else if builtins.isList v then + if v == [ ] then mkArray type.string [ ] else mkArray (elemTypeOf v) v + else if builtins.isAttrs v && (v._type or "") == "gvariant" then + v + else + null; + + mkArray = elemType: elems: + mkPrimitive (type.arrayOf elemType) (map mkValue elems) // { + __toString = self: + "@${self.type} [${concatMapStringsSep "," toString self.value}]"; + }; + + mkEmptyArray = elemType: mkArray elemType [ ]; + + mkVariant = elem: + let gvarElem = mkValue elem; + in mkPrimitive type.variant gvarElem // { + __toString = self: "@${self.type} <${toString self.value}>"; + }; + + mkDictionaryEntry = elems: + let + gvarElems = map mkValue elems; + dictionaryType = type.dictionaryEntryOf (map (e: e.type) gvarElems); + in + mkPrimitive dictionaryType gvarElems // { + __toString = self: + "@${self.type} {${concatMapStringsSep "," toString self.value}}"; + }; + + mkNothing = elemType: mkMaybe elemType null; + + mkJust = elem: let gvarElem = mkValue elem; in mkMaybe gvarElem.type gvarElem; + + mkTuple = elems: + let + gvarElems = map mkValue elems; + tupleType = type.tupleOf (map (e: e.type) gvarElems); + in + mkPrimitive tupleType gvarElems // { + __toString = self: + "@${self.type} (${concatMapStringsSep "," toString self.value})"; + }; + + mkBoolean = v: + mkPrimitive type.boolean v // { + __toString = self: if self.value then "true" else "false"; + }; + + mkString = v: + let sanitize = s: replaceStrings [ "\n" ] [ "\\n" ] (escape [ "'" "\\" ] s); + in mkPrimitive type.string v // { + __toString = self: "'${sanitize self.value}'"; + }; + + mkObjectpath = v: + mkPrimitive type.string v // { + __toString = self: "objectpath '${escape [ "'" ] self.value}'"; + }; + + mkUchar = mkPrimitive type.uchar; + + mkInt16 = mkPrimitive type.int16; + + mkUint16 = mkPrimitive type.uint16; + + mkInt32 = v: + mkPrimitive type.int32 v // { + __toString = self: toString self.value; + }; + + mkUint32 = mkPrimitive type.uint32; + + mkInt64 = mkPrimitive type.int64; + + mkUint64 = mkPrimitive type.uint64; + + mkDouble = v: + mkPrimitive type.double v // { + __toString = self: toString self.value; + }; +} diff --git a/nixos/modules/programs/dconf.nix b/nixos/modules/programs/dconf.nix index 7261a143528ff0a..ae9db665186853d 100644 --- a/nixos/modules/programs/dconf.nix +++ b/nixos/modules/programs/dconf.nix @@ -1,55 +1,170 @@ { config, lib, pkgs, ... }: -with lib; - let cfg = config.programs.dconf; - cfgDir = pkgs.symlinkJoin { - name = "dconf-system-config"; - paths = map (x: "${x}/etc/dconf") cfg.packages; - postBuild = '' - mkdir -p $out/profile - mkdir -p $out/db - '' + ( - concatStringsSep "\n" ( - mapAttrsToList ( - name: path: '' - ln -s ${path} $out/profile/${name} - '' - ) cfg.profiles - ) - ) + '' - ${pkgs.dconf}/bin/dconf update $out/db - ''; - }; + + # Compile keyfiles to Dconf DB + compileDconfDb = dir: pkgs.runCommand "dconf-db" + { + nativeBuildInputs = [ pkgs.dconf ]; + } "dconf compile $out ${dir}"; + + # Recursively lock values in settings + recuriseLock = with lib; settings: locks: (mapAttrsRecursive + (path: value: optionalAttrs value + (mapAttrsRecursiveCond (value: !lib.gvariant.isGVariant value) + (path: value: true) + (foldl' (root: name: root."${name}") { v = settings; } path))) + { v = locks; }).v; + + # Generate Dconf DB from attrset and keyfiles + mkDconfDb = val: compileDconfDb (pkgs.symlinkJoin { + name = "nixos-generated-dconf-keyfiles"; + paths = [ + (pkgs.writeTextDir "nixos-generated-dconf-keyfiles" (lib.generators.toDconfINI val.settings)) + (pkgs.writeTextDir "locks/nixos-generated-dconf-locks" (lib.generators.toDconfLocks + (lib.recursiveUpdate (recuriseLock val.settings val.lockSubpaths) val.locks))) + val.keyfiles + ]; + }); + + # Generate Dconf profile + mkDconfProfile = name: value: pkgs.writeTextDir "etc/dconf/profile/${name}" ( + if lib.isDerivation value || lib.isPath value then + lib.readFile value + else + lib.concatMapStrings (x: "${x}\n") (( + lib.optional value.enableUserDb "user-db:user" + ) ++ ( + map + (value: + let + db = if lib.isAttrs value && !lib.isDerivation value then mkDconfDb value else value; + in + "file-db:${db}") + value.databases + )) + ); in { - ###### interface - options = { programs.dconf = { - enable = mkEnableOption (lib.mdDoc "dconf"); + enable = lib.mkEnableOption (lib.mdDoc "dconf"); - profiles = mkOption { - type = types.attrsOf types.path; - default = {}; - description = lib.mdDoc "Set of dconf profile files, installed at {file}`/etc/dconf/profiles/«name»`."; - internal = true; + profiles = lib.mkOption { + type = with lib.types; attrsOf (oneOf [ + path + package + (submodule { + options = { + enableUserDb = lib.mkOption { + type = bool; + default = true; + description = lib.mdDoc "Add `user-db:user` at the beginning of the profile."; + }; + + databases = lib.mkOption { + type = with lib.types; listOf (oneOf [ + path + package + (submodule { + options = { + keyfiles = lib.mkOption { + type = with lib.types; listOf (oneOf [ path package ]); + default = [ ]; + description = lib.mdDoc "A list of dconf keyfiles."; + }; + settings = lib.mkOption { + type = lib.types.attrs; + default = { }; + description = lib.mdDoc "An attrset used to generate dconf keyfile."; + example = literalExpression '' + with lib.gvariant; + { + com.raggesilver.BlackBox = { + scrollback-lines = mkUint32 10000; + theme-dark = "Tommorow Night"; + }; + } + ''; + }; + locks = lib.mkOption { + type = with lib.types; let + nestedBool = either bool (attrsOf nestedBool); + in + attrsOf nestedBool; + default = { }; + description = lib.mdDoc '' + An attrset used to lockdown the dconf settings. This + generates a lock file. If a value is true, the key will be locked. + ''; + example = literalExpression '' + { + com.raggesilver.BlackBox = { + scrollback-lines = true; + theme-dark = false; + }; + } + ''; + }; + lockSubpaths = lib.mkOption { + type = let nestedBool = with lib.types; either bool (attrsOf nestedBool); in nestedBool; + default = false; + description = lib.mdDoc '' + Similiar to locks but the whole subpath is locked. This + only works for the dconf settings in databases.*.settings. + ''; + example = "true"; + }; + }; + }) + ]); + default = [ ]; + description = lib.mdDoc '' + List of data sources for the profile. An element can be an attrset, + or the path of an already compiled database. Each element is converted + to a file-db. + + A key is searched from up to down and the first result takes the + priorit. If a lock for a particular key is installed then the + last locked key takes the priorit. This can be used to enforce + mandatory settings. + ''; + }; + }; + }) + ]); + description = lib.mdDoc "Attrset of dconf profiles."; + }; + + defaultProfile = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = lib.mdDoc '' + The default dconf profile. This sets the DCONF_PROFILE environment variable. + If set, dconf will attempt to open the named profile, aborting if that fails. + If the environment variable is not set, dconf will attempt to open the + profile named "user" and if that fails, it will fall back to an internal + hard-wired configuration. + ''; }; - packages = mkOption { - type = types.listOf types.package; - default = []; + packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; description = lib.mdDoc "A list of packages which provide dconf profiles and databases in {file}`/etc/dconf`."; }; }; }; - ###### implementation + config = lib.mkIf cfg.enable { + programs.dconf.packages = lib.mapAttrsToList mkDconfProfile cfg.profiles; - config = mkIf (cfg.profiles != {} || cfg.enable) { - environment.etc.dconf = mkIf (cfg.profiles != {} || cfg.packages != []) { - source = cfgDir; + environment.etc.dconf = lib.mkIf (cfg.packages != [ ]) { + source = pkgs.symlinkJoin { + name = "dconf-system-config"; + paths = map (x: "${x}/etc/dconf") cfg.packages; + }; }; services.dbus.packages = [ pkgs.dconf ]; @@ -59,8 +174,9 @@ in # For dconf executable environment.systemPackages = [ pkgs.dconf ]; - # Needed for unwrapped applications - environment.sessionVariables.GIO_EXTRA_MODULES = mkIf cfg.enable [ "${pkgs.dconf.lib}/lib/gio/modules" ]; + environment.sessionVariables = { + # Needed for unwrapped applications + GIO_EXTRA_MODULES = [ "${pkgs.dconf.lib}/lib/gio/modules" ]; + } // lib.optionalAttrs (cfg.defaultProfile != null) { DCONF_PROFILE = cfg.defaultProfile; }; }; - } diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix index f8f82bda3fa4366..7bf3eaee6fa4146 100644 --- a/nixos/modules/services/x11/display-managers/gdm.nix +++ b/nixos/modules/services/x11/display-managers/gdm.nix @@ -229,40 +229,15 @@ in systemd.user.services.dbus.wantedBy = [ "default.target" ]; - programs.dconf.profiles.gdm = - let - customDconf = pkgs.writeTextFile { - name = "gdm-dconf"; - destination = "/dconf/gdm-custom"; - text = '' - ${optionalString (!cfg.gdm.autoSuspend) '' - [org/gnome/settings-daemon/plugins/power] - sleep-inactive-ac-type='nothing' - sleep-inactive-battery-type='nothing' - sleep-inactive-ac-timeout=0 - sleep-inactive-battery-timeout=0 - ''} - ''; - }; - - customDconfDb = pkgs.stdenv.mkDerivation { - name = "gdm-dconf-db"; - buildCommand = '' - ${pkgs.dconf}/bin/dconf compile $out ${customDconf}/dconf - ''; + programs.dconf.profiles.gdm.databases = [ "${gdm}/share/gdm/greeter-dconf-defaults" ] + ++ lib.optionals cfg.gdm.autoSuspend [{ + settings.org.gnome.settings-daemon.plugins.power = { + sleep-inactive-ac-type = "nothing"; + sleep-inactive-battery-type = "nothing"; + sleep-inactive-ac-timeout = lib.gvariant.mkInt32 0; + sleep-inactive-battery-timeout = lib.gvariant.mkInt32 0; }; - in pkgs.stdenv.mkDerivation { - name = "dconf-gdm-profile"; - buildCommand = '' - # Check that the GDM profile starts with what we expect. - if [ $(head -n 1 ${gdm}/share/dconf/profile/gdm) != "user-db:user" ]; then - echo "GDM dconf profile changed, please update gdm.nix" - exit 1 - fi - # Insert our custom DB behind it. - sed '2ifile-db:${customDconfDb}' ${gdm}/share/dconf/profile/gdm > $out - ''; - }; + }]; # Use AutomaticLogin if delay is zero, because it's immediate. # Otherwise with TimedLogin with zero seconds the prompt is still