Skip to content

Commit

Permalink
nixos/dconf: support generating from attrs
Browse files Browse the repository at this point in the history
  • Loading branch information
linsui committed May 28, 2023
1 parent 4e37b4e commit 085113e
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 92 deletions.
1 change: 1 addition & 0 deletions lib/default.nix
Expand Up @@ -41,6 +41,7 @@ let

# serialization
cli = callLibs ./cli.nix;
gvariant = callLibs ./gvariant.nix;
generators = callLibs ./generators.nix;

# misc
Expand Down
34 changes: 21 additions & 13 deletions lib/generators.nix
Expand Up @@ -175,6 +175,18 @@ rec {
+ "\n")
+ (toINI { inherit mkSectionName mkKeyValue listsAsDuplicateKeys; } sections);

# converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI
flattenAttrs = with builtins; extraIsLeafPredicate: sep: let
recurse = path: value:
if isAttrs value && !lib.isDerivation value && !extraIsLeafPredicate value then
lib.mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value
else if length path > 1 then {
${concatStringsSep sep (lib.reverseList (tail path))}.${head path} = value;
} else {
${head path} = value;
};
in attrs: lib.foldl lib.recursiveUpdate { } (lib.flatten (recurse [ ] attrs));

/* Generate a git-config file from an attrset.
*
* It has two major differences from the regular INI format:
Expand Down Expand Up @@ -213,21 +225,17 @@ 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 (_: false) "." attrs);

# mkKeyValueDefault wrapper that handles dconf INI quirks.
mkDconfKeyValue = mkKeyValueDefault { mkValueString = v: toString (lib.gvariant.mkValue v); } "=";

/* Generates INI in dconf keyfile style.
* The main differences of the format is that it requires strings to be quoted.
*/
toDconfINI = attrs: toINI { mkKeyValue = mkDconfKeyValue; } (flattenAttrs (x: lib.gvariant.isGVariant x) "/" attrs);

/* Generates JSON from an arbitrary (non-function) value.
* For more information see the documentation of the builtin.
Expand Down
181 changes: 181 additions & 0 deletions 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;
};
}
115 changes: 77 additions & 38 deletions nixos/modules/programs/dconf.nix
@@ -1,55 +1,92 @@
{ 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 or link compiled Dconf DB
compileDconfDb = dir: pkgs.runCommand "dconf-db"
{
nativeBuildInputs = [ pkgs.dconf ];
} ''
if [[ -d ${dir} ]]; then
dconf compile $out ${dir}
else
ln -s ${dir} $out
fi
'';

# Generate Dconf DB from attrs, keyfiles or link Dconf DB to output
mkDconfDb = val:
if lib.isAttrs val && !lib.isDerivation val then
compileDconfDb (pkgs.writeTextDir "db" (lib.generators.toDconfINI val))
else compileDconfDb val;

# Generate Dconf profile from a name value pair
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: "file-db:${mkDconfDb value}") 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 [ attrs str path package ]);
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 or a path to the .d
directory containing keyfiles.
'';
};
};
})
]);
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.";
};

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 ];
Expand All @@ -59,8 +96,10 @@ 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; };
};

}

0 comments on commit 085113e

Please sign in to comment.