about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--doc/default.nix1
-rw-r--r--lib/default.nix1
-rw-r--r--lib/generators.nix8
-rw-r--r--lib/gvariant.nix290
-rw-r--r--lib/tests/modules/gvariant.nix93
-rw-r--r--nixos/modules/programs/dconf.nix237
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix40
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/dconf.nix34
-rw-r--r--pkgs/desktops/gnome/core/gdm/default.nix38
10 files changed, 660 insertions, 83 deletions
diff --git a/doc/default.nix b/doc/default.nix
index f4270ae856d5f..58b945c692fc3 100644
--- a/doc/default.nix
+++ b/doc/default.nix
@@ -21,6 +21,7 @@ let
       { name = "filesystem"; description = "filesystem functions"; }
       { name = "sources"; description = "source filtering functions"; }
       { name = "cli"; description = "command-line serialization functions"; }
+      { name = "gvariant"; description = "GVariant formatted string serialization functions"; }
     ];
   };
 
diff --git a/lib/default.nix b/lib/default.nix
index 136f4a4a4637c..509636452b2b5 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 c37be1942d82f..0368577d5a512 100644
--- a/lib/generators.nix
+++ b/lib/generators.nix
@@ -230,6 +230,14 @@ rec {
     in
       toINI_ (gitFlattenAttrs 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. See https://help.gnome.org/admin/system-admin-guide/stable/dconf-keyfiles.html.en
+  # for details.
+  toDconfINI = toINI { mkKeyValue = mkDconfKeyValue; };
+
   /* 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 0000000000000..3142ffc5f1494
--- /dev/null
+++ b/lib/gvariant.nix
@@ -0,0 +1,290 @@
+# This file is based on https://github.com/nix-community/home-manager
+# Copyright (c) 2017-2022 Home Manager contributors
+#
+
+
+{ lib }:
+
+/* A partial and basic implementation of GVariant formatted strings.
+   See https://docs.gtk.org/glib/gvariant-format-strings.html for detauls.
+
+   Note, this API is not considered fully stable and it might therefore
+   change in backwards incompatible ways without prior notice.
+*/
+let
+  inherit (lib)
+    concatMapStringsSep concatStrings escape head replaceStrings;
+
+  mkPrimitive = t: v: {
+    _type = "gvariant";
+    type = t;
+    value = v;
+    __toString = self: "@${self.type} ${toString self.value}"; # https://docs.gtk.org/glib/gvariant-text.html
+  };
+
+  type = {
+    arrayOf = t: "a${t}";
+    maybeOf = t: "m${t}";
+    tupleOf = ts: "(${concatStrings ts})";
+    dictionaryEntryOf = nameType: valueType: "{${nameType}${valueType}}";
+    string = "s";
+    boolean = "b";
+    uchar = "y";
+    int16 = "n";
+    uint16 = "q";
+    int32 = "i";
+    uint32 = "u";
+    int64 = "x";
+    uint64 = "t";
+    double = "d";
+    variant = "v";
+  };
+
+  /* Check if a value is a GVariant value
+
+     Type:
+       isGVariant :: Any -> Bool
+  */
+  isGVariant = v: v._type or "" == "gvariant";
+
+in
+rec {
+
+  inherit type isGVariant;
+
+  /* Returns the GVariant value that most closely matches the given Nix value.
+     If no GVariant value can be found unambiguously then error is thrown.
+
+     Type:
+       mkValue :: Any -> gvariant
+  */
+  mkValue = v:
+    if builtins.isBool v then
+      mkBoolean v
+    else if builtins.isFloat v then
+      mkDouble v
+    else if builtins.isString v then
+      mkString v
+    else if builtins.isList v then
+      mkArray v
+    else if isGVariant v then
+      v
+    else
+      throw "The GVariant type of ${v} can't be inferred.";
+
+  /* Returns the GVariant array from the given type of the elements and a Nix list.
+
+     Type:
+       mkArray :: [Any] -> gvariant
+
+     Example:
+       # Creating a string array
+       lib.gvariant.mkArray [ "a" "b" "c" ]
+  */
+  mkArray = elems:
+    let
+      vs = map mkValue (lib.throwIf (elems == [ ]) "Please create empty array with mkEmptyArray." elems);
+      elemType = lib.throwIfNot (lib.all (t: (head vs).type == t) (map (v: v.type) vs))
+        "Elements in a list should have same type."
+        (head vs).type;
+    in
+    mkPrimitive (type.arrayOf elemType) vs // {
+      __toString = self:
+        "@${self.type} [${concatMapStringsSep "," toString self.value}]";
+    };
+
+  /* Returns the GVariant array from the given empty Nix list.
+
+     Type:
+       mkEmptyArray :: gvariant.type -> gvariant
+
+     Example:
+       # Creating an empty string array
+       lib.gvariant.mkEmptyArray (lib.gvariant.type.string)
+  */
+  mkEmptyArray = elemType: mkPrimitive (type.arrayOf elemType) [ ] // {
+    __toString = self: "@${self.type} []";
+  };
+
+
+  /* Returns the GVariant variant from the given Nix value. Variants are containers
+     of different GVariant type.
+
+     Type:
+       mkVariant :: Any -> gvariant
+
+     Example:
+       lib.gvariant.mkArray [
+         (lib.gvariant.mkVariant "a string")
+         (lib.gvariant.mkVariant (lib.gvariant.mkInt32 1))
+       ]
+  */
+  mkVariant = elem:
+    let gvarElem = mkValue elem;
+    in mkPrimitive type.variant gvarElem // {
+      __toString = self: "<${toString self.value}>";
+    };
+
+  /* Returns the GVariant dictionary entry from the given key and value.
+
+     Type:
+       mkDictionaryEntry :: String -> Any -> gvariant
+
+     Example:
+       # A dictionary describing an Epiphany’s search provider
+       [
+         (lib.gvariant.mkDictionaryEntry "url" (lib.gvariant.mkVariant "https://duckduckgo.com/?q=%s&t=epiphany"))
+         (lib.gvariant.mkDictionaryEntry "bang" (lib.gvariant.mkVariant "!d"))
+         (lib.gvariant.mkDictionaryEntry "name" (lib.gvariant.mkVariant "DuckDuckGo"))
+       ]
+  */
+  mkDictionaryEntry =
+    # The key of the entry
+    name:
+    # The value of the entry
+    value:
+    let
+      name' = mkValue name;
+      value' = mkValue value;
+      dictionaryType = type.dictionaryEntryOf name'.type value'.type;
+    in
+    mkPrimitive dictionaryType { inherit name value; } // {
+      __toString = self: "@${self.type} {${name'},${value'}}";
+    };
+
+  /* Returns the GVariant maybe from the given element type.
+
+     Type:
+       mkMaybe :: gvariant.type -> Any -> gvariant
+  */
+  mkMaybe = elemType: elem:
+    mkPrimitive (type.maybeOf elemType) elem // {
+      __toString = self:
+        if self.value == null then
+          "@${self.type} nothing"
+        else
+          "just ${toString self.value}";
+    };
+
+  /* Returns the GVariant nothing from the given element type.
+
+     Type:
+       mkNothing :: gvariant.type -> gvariant
+  */
+  mkNothing = elemType: mkMaybe elemType null;
+
+  /* Returns the GVariant just from the given Nix value.
+
+     Type:
+       mkJust :: Any -> gvariant
+  */
+  mkJust = elem: let gvarElem = mkValue elem; in mkMaybe gvarElem.type gvarElem;
+
+  /* Returns the GVariant tuple from the given Nix list.
+
+     Type:
+       mkTuple :: [Any] -> gvariant
+  */
+  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})";
+    };
+
+  /* Returns the GVariant boolean from the given Nix bool value.
+
+     Type:
+       mkBoolean :: Bool -> gvariant
+  */
+  mkBoolean = v:
+    mkPrimitive type.boolean v // {
+      __toString = self: if self.value then "true" else "false";
+    };
+
+  /* Returns the GVariant string from the given Nix string value.
+
+     Type:
+       mkString :: String -> gvariant
+  */
+  mkString = v:
+    let sanitize = s: replaceStrings [ "\n" ] [ "\\n" ] (escape [ "'" "\\" ] s);
+    in mkPrimitive type.string v // {
+      __toString = self: "'${sanitize self.value}'";
+    };
+
+  /* Returns the GVariant object path from the given Nix string value.
+
+     Type:
+       mkObjectpath :: String -> gvariant
+  */
+  mkObjectpath = v:
+    mkPrimitive type.string v // {
+      __toString = self: "objectpath '${escape [ "'" ] self.value}'";
+    };
+
+  /* Returns the GVariant uchar from the given Nix int value.
+
+     Type:
+       mkUchar :: Int -> gvariant
+  */
+  mkUchar = mkPrimitive type.uchar;
+
+  /* Returns the GVariant int16 from the given Nix int value.
+
+     Type:
+       mkInt16 :: Int -> gvariant
+  */
+  mkInt16 = mkPrimitive type.int16;
+
+  /* Returns the GVariant uint16 from the given Nix int value.
+
+     Type:
+       mkUint16 :: Int -> gvariant
+  */
+  mkUint16 = mkPrimitive type.uint16;
+
+  /* Returns the GVariant int32 from the given Nix int value.
+
+     Type:
+       mkInt32 :: Int -> gvariant
+  */
+  mkInt32 = v:
+    mkPrimitive type.int32 v // {
+      __toString = self: toString self.value;
+    };
+
+  /* Returns the GVariant uint32 from the given Nix int value.
+
+     Type:
+       mkUint32 :: Int -> gvariant
+  */
+  mkUint32 = mkPrimitive type.uint32;
+
+  /* Returns the GVariant int64 from the given Nix int value.
+
+     Type:
+       mkInt64 :: Int -> gvariant
+  */
+  mkInt64 = mkPrimitive type.int64;
+
+  /* Returns the GVariant uint64 from the given Nix int value.
+
+     Type:
+       mkUint64 :: Int -> gvariant
+  */
+  mkUint64 = mkPrimitive type.uint64;
+
+  /* Returns the GVariant double from the given Nix float value.
+
+     Type:
+       mkDouble :: Float -> gvariant
+  */
+  mkDouble = v:
+    mkPrimitive type.double v // {
+      __toString = self: toString self.value;
+    };
+}
diff --git a/lib/tests/modules/gvariant.nix b/lib/tests/modules/gvariant.nix
new file mode 100644
index 0000000000000..a792ebf85b743
--- /dev/null
+++ b/lib/tests/modules/gvariant.nix
@@ -0,0 +1,93 @@
+{ config, lib, ... }:
+
+let inherit (lib) concatStringsSep mapAttrsToList mkMerge mkOption types gvariant;
+in {
+  options.examples = mkOption { type = types.attrsOf gvariant; };
+
+  config = {
+    examples = with gvariant;
+      mkMerge [
+        { bool = true; }
+        { bool = true; }
+
+        { float = 3.14; }
+
+        { int32 = mkInt32 (- 42); }
+        { int32 = mkInt32 (- 42); }
+
+        { uint32 = mkUint32 42; }
+        { uint32 = mkUint32 42; }
+
+        { int16 = mkInt16 (-42); }
+        { int16 = mkInt16 (-42); }
+
+        { uint16 = mkUint16 42; }
+        { uint16 = mkUint16 42; }
+
+        { int64 = mkInt64 (-42); }
+        { int64 = mkInt64 (-42); }
+
+        { uint64 = mkUint64 42; }
+        { uint64 = mkUint64 42; }
+
+        { array1 = [ "one" ]; }
+        { array1 = mkArray [ "two" ]; }
+        { array2 = mkArray [ (mkInt32 1) ]; }
+        { array2 = mkArray [ (nkUint32 2) ]; }
+
+        { emptyArray1 = [ ]; }
+        { emptyArray2 = mkEmptyArray type.uint32; }
+
+        { string = "foo"; }
+        { string = "foo"; }
+        {
+          escapedString = ''
+            '\
+          '';
+        }
+
+        { tuple = mkTuple [ (mkInt32 1) [ "foo" ] ]; }
+
+        { maybe1 = mkNothing type.string; }
+        { maybe2 = mkJust (mkUint32 4); }
+
+        { variant1 = mkVariant "foo"; }
+        { variant2 = mkVariant 42; }
+
+        { dictionaryEntry = mkDictionaryEntry (mkInt32 1) [ "foo" ]; }
+      ];
+
+    assertions = [
+      {
+        assertion = (
+          let
+            mkLine = n: v: "${n} = ${toString (gvariant.mkValue v)}";
+            result = concatStringsSep "\n" (mapAttrsToList mkLine config.examples);
+          in
+          result + "\n"
+        ) == ''
+          array1 = @as ['one','two']
+          array2 = @au [1,2]
+          bool = true
+          dictionaryEntry = @{ias} {1,@as ['foo']}
+          emptyArray1 = @as []
+          emptyArray2 = @au []
+          escapedString = '\'\\\n'
+          float = 3.140000
+          int = -42
+          int16 = @n -42
+          int64 = @x -42
+          maybe1 = @ms nothing
+          maybe2 = just @u 4
+          string = 'foo'
+          tuple = @(ias) (1,@as ['foo'])
+          uint16 = @q 42
+          uint32 = @u 42
+          uint64 = @t 42
+          variant1 = @v <'foo'>
+          variant2 = @v <42>
+        '';
+      }
+    ];
+  };
+}
diff --git a/nixos/modules/programs/dconf.nix b/nixos/modules/programs/dconf.nix
index 7261a143528ff..cf53658c4fada 100644
--- a/nixos/modules/programs/dconf.nix
+++ b/nixos/modules/programs/dconf.nix
@@ -1,55 +1,217 @@
 { 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 = [ (lib.getBin pkgs.dconf) ];
+    } "dconf compile $out ${dir}";
+
+  # Check if dconf keyfiles are valid
+  checkDconfKeyfiles = dir: pkgs.runCommand "check-dconf-keyfiles"
+    {
+      nativeBuildInputs = [ (lib.getBin pkgs.dconf) ];
+    } ''
+    if [[ -f ${dir} ]]; then
+      echo "dconf keyfiles should be a directory but a file is provided: ${dir}"
+      exit 1
+    fi
+
+    dconf compile db ${dir} || (
+      echo "The dconf keyfiles are invalid: ${dir}"
+      exit 1
+    )
+    cp -R ${dir} $out
+  '';
+
+  mkAllLocks = settings: lib.flatten (
+    lib.mapAttrsToList (k: v: lib.mapAttrsToList (k': _: "/${k}/${k'}") v) settings);
+
+  # Generate dconf DB from dconfDatabase 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.concatStringsSep "\n"
+        (if val.lockAll then mkAllLocks val.settings else val.locks)
+      ))
+    ] ++ (map checkDconfKeyfiles val.keyfiles);
+  });
+
+  # Check if a dconf DB file is valid. The dconf cli doesn't return 1 when it can't
+  # open the database file so we have to check if the output is empty.
+  checkDconfDb = file: pkgs.runCommand "check-dconf-db"
+    {
+      nativeBuildInputs = [ (lib.getBin pkgs.dconf) ];
+    } ''
+    if [[ -d ${file} ]]; then
+      echo "dconf DB should be a file but a directory is provided: ${file}"
+      exit 1
+    fi
+
+    echo "file-db:${file}" > profile
+    DCONF_PROFILE=$(pwd)/profile dconf dump / > output 2> error
+    if [[ ! -s output ]] && [[ -s error ]]; then
+      cat error
+      echo "The dconf DB file is invalid: ${file}"
+      exit 1
+    fi
+
+    cp ${file} $out
+  '';
+
+  # Generate dconf profile
+  mkDconfProfile = name: value:
+    if lib.isDerivation value || lib.isPath value then
+      pkgs.runCommand "dconf-profile" { } ''
+        if [[ -d ${value} ]]; then
+          echo "Dconf profile should be a file but a directory is provided."
+          exit 1
+        fi
+        mkdir -p $out/etc/dconf/profile/
+        cp ${value} $out/etc/dconf/profile/${name}
+      ''
+    else
+      pkgs.writeTextDir "etc/dconf/profile/${name}" (
+        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 checkDconfDb value;
+              in
+              "file-db:${db}")
+            value.databases
+        ))
+      );
+
+  dconfDatabase = with lib.types; submodule {
+    options = {
+      keyfiles = lib.mkOption {
+        type = listOf (oneOf [ path package ]);
+        default = [ ];
+        description = lib.mdDoc "A list of dconf keyfile directories.";
+      };
+      settings = lib.mkOption {
+        type = 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; listOf str;
+        default = [ ];
+        description = lib.mdDoc ''
+          A list of dconf keys to be lockdown. This doesn't take effect if `lockAll`
+          is set.
+        '';
+        example = literalExpression ''
+          [ "/org/gnome/desktop/background/picture-uri" ]
+        '';
+      };
+      lockAll = lib.mkOption {
+        type = lib.types.bool;
+        default = false;
+        description = lib.mdDoc "Lockdown all dconf keys in `settings`.";
+      };
+    };
+  };
+
+  dconfProfile = with lib.types; 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
+          dconfDatabase
+        ]);
+        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
+          priority. If a lock for a particular key is installed then the value from
+          the last database in the profile where the key is locked will be used.
+          This can be used to enforce mandatory settings.
+        '';
+      };
+    };
   };
+
 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
+          dconfProfile
+        ]);
+        default = { };
+        description = lib.mdDoc ''
+          Attrset of dconf profiles. By default the `user` profile is used which
+          ends up in `/etc/dconf/profile/user`.
+        '';
+        example = lib.literalExpression ''
+          {
+            # A "user" profile with a database
+            user.databases = [
+              {
+                settings = { };
+              }
+            ];
+            # A "bar" profile from a package
+            bar = pkgs.bar-dconf-profile;
+            # A "foo" profile from a path
+            foo = ''${./foo}
+          };
+        '';
       };
 
-      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.profiles != { } || 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;
+        nativeBuildInputs = [ (lib.getBin pkgs.dconf) ];
+        postBuild = ''
+          if test -d $out/db; then
+            dconf update $out/db
+          fi
+        '';
+      };
     };
 
     services.dbus.packages = [ pkgs.dconf ];
@@ -59,8 +221,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 = lib.mkIf cfg.enable {
+      # Needed for unwrapped applications
+      GIO_EXTRA_MODULES = [ "${pkgs.dconf.lib}/lib/gio/modules" ];
+    };
   };
-
 }
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index 676d08b93e2c4..e6923bcbb56c0 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -231,40 +231,14 @@ 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 = 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
-      '';
-    };
+    }] ++ [ "${gdm}/share/gdm/greeter-dconf-defaults" ];
 
     # Use AutomaticLogin if delay is zero, because it's immediate.
     # Otherwise with TimedLogin with zero seconds the prompt is still
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index bef18a90775c5..c1e124bda5c7a 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -210,6 +210,7 @@ in {
   custom-ca = handleTest ./custom-ca.nix {};
   croc = handleTest ./croc.nix {};
   darling = handleTest ./darling.nix {};
+  dconf = handleTest ./dconf.nix {};
   deepin = handleTest ./deepin.nix {};
   deluge = handleTest ./deluge.nix {};
   dendrite = handleTest ./matrix/dendrite.nix {};
diff --git a/nixos/tests/dconf.nix b/nixos/tests/dconf.nix
new file mode 100644
index 0000000000000..86f703e3b98e8
--- /dev/null
+++ b/nixos/tests/dconf.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix
+  ({ lib, ... }:
+  {
+    name = "dconf";
+
+    meta.maintainers = with lib.maintainers; [
+      linsui
+    ];
+
+    nodes.machine = { config, pkgs, lib, ... }: {
+      users.extraUsers.alice = { isNormalUser = true; };
+      programs.dconf = with lib.gvariant; {
+        enable = true;
+        profiles.user.databases = [
+          {
+            settings = {
+              "test/not/locked" = mkInt32 1;
+              "test/is/locked" = "locked";
+            };
+            locks = [
+              "/test/is/locked"
+            ];
+          }
+        ];
+      };
+    };
+
+    testScript = ''
+      machine.succeed("test $(dconf read -d /test/not/locked) == 1")
+      machine.succeed("test $(dconf read -d /test/is/locked) == \"'locked'\"")
+      machine.fail("sudo -u alice dbus-run-session -- dconf write /test/is/locked \"@s 'unlocked'\"")
+      machine.succeed("sudo -u alice dbus-run-session -- dconf write /test/not/locked \"@i 2\"")
+    '';
+  })
diff --git a/pkgs/desktops/gnome/core/gdm/default.nix b/pkgs/desktops/gnome/core/gdm/default.nix
index 8faa1615dc07d..cfdde43ae776b 100644
--- a/pkgs/desktops/gnome/core/gdm/default.nix
+++ b/pkgs/desktops/gnome/core/gdm/default.nix
@@ -1,4 +1,5 @@
-{ lib, stdenv
+{ lib
+, stdenv
 , fetchurl
 , fetchpatch
 , substituteAll
@@ -8,7 +9,6 @@
 , pkg-config
 , glib
 , itstool
-, libxml2
 , xorg
 , accountsservice
 , libX11
@@ -24,12 +24,12 @@
 , audit
 , gobject-introspection
 , plymouth
-, librsvg
 , coreutils
 , xorgserver
 , xwayland
 , dbus
 , nixos-icons
+, runCommand
 }:
 
 let
@@ -41,21 +41,21 @@ let
 
 in
 
-stdenv.mkDerivation rec {
+stdenv.mkDerivation (finalAttrs: {
   pname = "gdm";
   version = "44.1";
 
   outputs = [ "out" "dev" ];
 
   src = fetchurl {
-    url = "mirror://gnome/sources/gdm/${lib.versions.major version}/${pname}-${version}.tar.xz";
+    url = "mirror://gnome/sources/gdm/${lib.versions.major finalAttrs.version}/${finalAttrs.pname}-${finalAttrs.version}.tar.xz";
     sha256 = "aCZrOr59KPxGnQBnqsnF2rsMp5UswffCOKBJUfPcWw0=";
   };
 
   mesonFlags = [
     "-Dgdm-xsession=true"
     # TODO: Setup a default-path? https://gitlab.gnome.org/GNOME/gdm/-/blob/6fc40ac6aa37c8ad87c32f0b1a5d813d34bf7770/meson_options.txt#L6
-    "-Dinitial-vt=${passthru.initialVT}"
+    "-Dinitial-vt=${finalAttrs.passthru.initialVT}"
     "-Dudev-dir=${placeholder "out"}/lib/udev/rules.d"
     "-Dsystemdsystemunitdir=${placeholder "out"}/lib/systemd/system"
     "-Dsystemduserunitdir=${placeholder "out"}/lib/systemd/user"
@@ -131,21 +131,21 @@ stdenv.mkDerivation rec {
   '';
 
   preInstall = ''
-    install -D ${override} ${DESTDIR}/$out/share/glib-2.0/schemas/org.gnome.login-screen.gschema.override
+    install -D ${override} $DESTDIR/$out/share/glib-2.0/schemas/org.gnome.login-screen.gschema.override
   '';
 
   postInstall = ''
     # Move stuff from DESTDIR to proper location.
     # We use rsync to merge the directories.
-    rsync --archive "${DESTDIR}/etc" "$out"
-    rm --recursive "${DESTDIR}/etc"
+    rsync --archive "$DESTDIR/etc" "$out"
+    rm --recursive "$DESTDIR/etc"
     for o in $(getAllOutputNames); do
         if [[ "$o" = "debug" ]]; then continue; fi
-        rsync --archive "${DESTDIR}/''${!o}" "$(dirname "''${!o}")"
-        rm --recursive "${DESTDIR}/''${!o}"
+        rsync --archive "$DESTDIR/''${!o}" "$(dirname "''${!o}")"
+        rm --recursive "$DESTDIR/''${!o}"
     done
     # Ensure the DESTDIR is removed.
-    rmdir "${DESTDIR}/nix/store" "${DESTDIR}/nix" "${DESTDIR}"
+    rmdir "$DESTDIR/nix/store" "$DESTDIR/nix" "$DESTDIR"
 
     # We are setting DESTDIR so the post-install script does not compile the schemas.
     glib-compile-schemas "$out/share/glib-2.0/schemas"
@@ -170,6 +170,18 @@ stdenv.mkDerivation rec {
     # Used in GDM NixOS module
     # Don't remove.
     initialVT = "7";
+    dconfDb = "${finalAttrs.finalPackage}/share/gdm/greeter-dconf-defaults";
+    dconfProfile = "user-db:user\nfile-db:${finalAttrs.passthru.dconfDb}";
+
+    tests = {
+      profile = runCommand "gdm-profile-test" { } ''
+        if test "${finalAttrs.passthru.dconfProfile}" != "$(cat ${finalAttrs.finalPackage}/share/dconf/profile/gdm)"; then
+          echo "GDM dconf profile changed, please update gdm.nix"
+          exit 1
+        fi
+        touch $out
+      '';
+    };
   };
 
   meta = with lib; {
@@ -179,4 +191,4 @@ stdenv.mkDerivation rec {
     maintainers = teams.gnome.members;
     platforms = platforms.linux;
   };
-}
+})