Merge pull request #300257 from mschwaig/fix-mivisionx
[NixPkgs.git] / lib / generators.nix
blob5f42a98de70988d1579e33c44802f62de2dba048
1 /* Functions that generate widespread file
2  * formats from nix data structures.
3  *
4  * They all follow a similar interface:
5  * generator { config-attrs } data
6  *
7  * `config-attrs` are “holes” in the generators
8  * with sensible default implementations that
9  * can be overwritten. The default implementations
10  * are mostly generators themselves, called with
11  * their respective default values; they can be reused.
12  *
13  * Tests can be found in ./tests/misc.nix
14  * Documentation in the manual, #sec-generators
15  */
16 { lib }:
18 let
19   inherit (lib)
20     addErrorContext
21     assertMsg
22     attrNames
23     concatLists
24     concatMapStringsSep
25     concatStrings
26     concatStringsSep
27     const
28     elem
29     escape
30     filter
31     flatten
32     foldl
33     functionArgs  # Note: not the builtin; considers `__functor` in attrsets.
34     gvariant
35     hasInfix
36     head
37     id
38     init
39     isAttrs
40     isBool
41     isDerivation
42     isFloat
43     isFunction    # Note: not the builtin; considers `__functor` in attrsets.
44     isInt
45     isList
46     isPath
47     isString
48     last
49     length
50     mapAttrs
51     mapAttrsToList
52     optionals
53     recursiveUpdate
54     replaceStrings
55     reverseList
56     splitString
57     tail
58     toList
59     ;
61   inherit (lib.strings)
62     escapeNixIdentifier
63     floatToString
64     match
65     split
66     toJSON
67     typeOf
68     ;
70   ## -- HELPER FUNCTIONS & DEFAULTS --
72   /* Convert a value to a sensible default string representation.
73    * The builtin `toString` function has some strange defaults,
74    * suitable for bash scripts but not much else.
75    */
76   mkValueStringDefault = {}: v:
77     let err = t: v: abort
78           ("generators.mkValueStringDefault: " +
79            "${t} not supported: ${toPretty {} v}");
80     in   if isInt      v then toString v
81     # convert derivations to store paths
82     else if isDerivation v then toString v
83     # we default to not quoting strings
84     else if isString   v then v
85     # isString returns "1", which is not a good default
86     else if true  ==   v then "true"
87     # here it returns to "", which is even less of a good default
88     else if false ==   v then "false"
89     else if null  ==   v then "null"
90     # if you have lists you probably want to replace this
91     else if isList     v then err "lists" v
92     # same as for lists, might want to replace
93     else if isAttrs    v then err "attrsets" v
94     # functions can’t be printed of course
95     else if isFunction v then err "functions" v
96     # Floats currently can't be converted to precise strings,
97     # condition warning on nix version once this isn't a problem anymore
98     # See https://github.com/NixOS/nix/pull/3480
99     else if isFloat    v then floatToString v
100     else err "this value is" (toString v);
103   /* Generate a line of key k and value v, separated by
104    * character sep. If sep appears in k, it is escaped.
105    * Helper for synaxes with different separators.
106    *
107    * mkValueString specifies how values should be formatted.
108    *
109    * mkKeyValueDefault {} ":" "f:oo" "bar"
110    * > "f\:oo:bar"
111    */
112   mkKeyValueDefault = {
113     mkValueString ? mkValueStringDefault {}
114   }: sep: k: v:
115     "${escape [sep] k}${sep}${mkValueString v}";
118   ## -- FILE FORMAT GENERATORS --
121   /* Generate a key-value-style config file from an attrset.
122    *
123    * mkKeyValue is the same as in toINI.
124    */
125   toKeyValue = {
126     mkKeyValue ? mkKeyValueDefault {} "=",
127     listsAsDuplicateKeys ? false,
128     indent ? ""
129   }:
130   let mkLine = k: v: indent + mkKeyValue k v + "\n";
131       mkLines = if listsAsDuplicateKeys
132         then k: v: map (mkLine k) (if isList v then v else [v])
133         else k: v: [ (mkLine k v) ];
134   in attrs: concatStrings (concatLists (mapAttrsToList mkLines attrs));
137   /* Generate an INI-style config file from an
138    * attrset of sections to an attrset of key-value pairs.
139    *
140    * generators.toINI {} {
141    *   foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
142    *   baz = { "also, integers" = 42; };
143    * }
144    *
145    *> [baz]
146    *> also, integers=42
147    *>
148    *> [foo]
149    *> ciao=bar
150    *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
151    *
152    * The mk* configuration attributes can generically change
153    * the way sections and key-value strings are generated.
154    *
155    * For more examples see the test cases in ./tests/misc.nix.
156    */
157   toINI = {
158     # apply transformations (e.g. escapes) to section names
159     mkSectionName ? (name: escape [ "[" "]" ] name),
160     # format a setting line from key and value
161     mkKeyValue    ? mkKeyValueDefault {} "=",
162     # allow lists as values for duplicate keys
163     listsAsDuplicateKeys ? false
164   }: attrsOfAttrs:
165     let
166         # map function to string for each key val
167         mapAttrsToStringsSep = sep: mapFn: attrs:
168           concatStringsSep sep
169             (mapAttrsToList mapFn attrs);
170         mkSection = sectName: sectValues: ''
171           [${mkSectionName sectName}]
172         '' + toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } sectValues;
173     in
174       # map input to ini sections
175       mapAttrsToStringsSep "\n" mkSection attrsOfAttrs;
177   /* Generate an INI-style config file from an attrset
178    * specifying the global section (no header), and an
179    * attrset of sections to an attrset of key-value pairs.
180    *
181    * generators.toINIWithGlobalSection {} {
182    *   globalSection = {
183    *     someGlobalKey = "hi";
184    *   };
185    *   sections = {
186    *     foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
187    *     baz = { "also, integers" = 42; };
188    * }
189    *
190    *> someGlobalKey=hi
191    *>
192    *> [baz]
193    *> also, integers=42
194    *>
195    *> [foo]
196    *> ciao=bar
197    *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
198    *
199    * The mk* configuration attributes can generically change
200    * the way sections and key-value strings are generated.
201    *
202    * For more examples see the test cases in ./tests/misc.nix.
203    *
204    * If you don’t need a global section, you can also use
205    * `generators.toINI` directly, which only takes
206    * the part in `sections`.
207    */
208   toINIWithGlobalSection = {
209     # apply transformations (e.g. escapes) to section names
210     mkSectionName ? (name: escape [ "[" "]" ] name),
211     # format a setting line from key and value
212     mkKeyValue    ? mkKeyValueDefault {} "=",
213     # allow lists as values for duplicate keys
214     listsAsDuplicateKeys ? false
215   }: { globalSection, sections ? {} }:
216     ( if globalSection == {}
217       then ""
218       else (toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } globalSection)
219            + "\n")
220     + (toINI { inherit mkSectionName mkKeyValue listsAsDuplicateKeys; } sections);
222   /* Generate a git-config file from an attrset.
223    *
224    * It has two major differences from the regular INI format:
225    *
226    * 1. values are indented with tabs
227    * 2. sections can have sub-sections
228    *
229    * generators.toGitINI {
230    *   url."ssh://git@github.com/".insteadOf = "https://github.com";
231    *   user.name = "edolstra";
232    * }
233    *
234    *> [url "ssh://git@github.com/"]
235    *>   insteadOf = "https://github.com"
236    *>
237    *> [user]
238    *>   name = "edolstra"
239    */
240   toGitINI = attrs:
241     let
242       mkSectionName = name:
243         let
244           containsQuote = hasInfix ''"'' name;
245           sections = splitString "." name;
246           section = head sections;
247           subsections = tail sections;
248           subsection = concatStringsSep "." subsections;
249         in if containsQuote || subsections == [ ] then
250           name
251         else
252           ''${section} "${subsection}"'';
254       mkValueString = v:
255         let
256           escapedV = ''
257             "${
258               replaceStrings [ "\n" "   " ''"'' "\\" ] [ "\\n" "\\t" ''\"'' "\\\\" ] v
259             }"'';
260         in mkValueStringDefault { } (if isString v then escapedV else v);
262       # generation for multiple ini values
263       mkKeyValue = k: v:
264         let mkKeyValue = mkKeyValueDefault { inherit mkValueString; } " = " k;
265         in concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (toList v));
267       # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI
268       gitFlattenAttrs = let
269         recurse = path: value:
270           if isAttrs value && !isDerivation value then
271             mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value
272           else if length path > 1 then {
273             ${concatStringsSep "." (reverseList (tail path))}.${head path} = value;
274           } else {
275             ${head path} = value;
276           };
277       in attrs: foldl recursiveUpdate { } (flatten (recurse [ ] attrs));
279       toINI_ = toINI { inherit mkKeyValue mkSectionName; };
280     in
281       toINI_ (gitFlattenAttrs attrs);
283   # mkKeyValueDefault wrapper that handles dconf INI quirks.
284   # The main differences of the format is that it requires strings to be quoted.
285   mkDconfKeyValue = mkKeyValueDefault { mkValueString = v: toString (gvariant.mkValue v); } "=";
287   # Generates INI in dconf keyfile style. See https://help.gnome.org/admin/system-admin-guide/stable/dconf-keyfiles.html.en
288   # for details.
289   toDconfINI = toINI { mkKeyValue = mkDconfKeyValue; };
291   withRecursion =
292     {
293       /* If this option is not null, the given value will stop evaluating at a certain depth */
294       depthLimit
295       /* If this option is true, an error will be thrown, if a certain given depth is exceeded */
296     , throwOnDepthLimit ? true
297     }:
298       assert isInt depthLimit;
299       let
300         specialAttrs = [
301           "__functor"
302           "__functionArgs"
303           "__toString"
304           "__pretty"
305         ];
306         stepIntoAttr = evalNext: name:
307           if elem name specialAttrs
308             then id
309             else evalNext;
310         transform = depth:
311           if depthLimit != null && depth > depthLimit then
312             if throwOnDepthLimit
313               then throw "Exceeded maximum eval-depth limit of ${toString depthLimit} while trying to evaluate with `generators.withRecursion'!"
314               else const "<unevaluated>"
315           else id;
316         mapAny = depth: v:
317           let
318             evalNext = x: mapAny (depth + 1) (transform (depth + 1) x);
319           in
320             if isAttrs v then mapAttrs (stepIntoAttr evalNext) v
321             else if isList v then map evalNext v
322             else transform (depth + 1) v;
323       in
324         mapAny 0;
326   /* Pretty print a value, akin to `builtins.trace`.
327    * Should probably be a builtin as well.
328    * The pretty-printed string should be suitable for rendering default values
329    * in the NixOS manual. In particular, it should be as close to a valid Nix expression
330    * as possible.
331    */
332   toPretty = {
333     /* If this option is true, attrsets like { __pretty = fn; val = …; }
334        will use fn to convert val to a pretty printed representation.
335        (This means fn is type Val -> String.) */
336     allowPrettyValues ? false,
337     /* If this option is true, the output is indented with newlines for attribute sets and lists */
338     multiline ? true,
339     /* Initial indentation level */
340     indent ? ""
341   }:
342     let
343     go = indent: v:
344     let     introSpace = if multiline then "\n${indent}  " else " ";
345             outroSpace = if multiline then "\n${indent}" else " ";
346     in if   isInt      v then toString v
347     # toString loses precision on floats, so we use toJSON instead. This isn't perfect
348     # as the resulting string may not parse back as a float (e.g. 42, 1e-06), but for
349     # pretty-printing purposes this is acceptable.
350     else if isFloat    v then builtins.toJSON v
351     else if isString   v then
352       let
353         lines = filter (v: ! isList v) (split "\n" v);
354         escapeSingleline = escape [ "\\" "\"" "\${" ];
355         escapeMultiline = replaceStrings [ "\${" "''" ] [ "''\${" "'''" ];
356         singlelineResult = "\"" + concatStringsSep "\\n" (map escapeSingleline lines) + "\"";
357         multilineResult = let
358           escapedLines = map escapeMultiline lines;
359           # The last line gets a special treatment: if it's empty, '' is on its own line at the "outer"
360           # indentation level. Otherwise, '' is appended to the last line.
361           lastLine = last escapedLines;
362         in "''" + introSpace + concatStringsSep introSpace (init escapedLines)
363                 + (if lastLine == "" then outroSpace else introSpace + lastLine) + "''";
364       in
365         if multiline && length lines > 1 then multilineResult else singlelineResult
366     else if true  ==   v then "true"
367     else if false ==   v then "false"
368     else if null  ==   v then "null"
369     else if isPath     v then toString v
370     else if isList     v then
371       if v == [] then "[ ]"
372       else "[" + introSpace
373         + concatMapStringsSep introSpace (go (indent + "  ")) v
374         + outroSpace + "]"
375     else if isFunction v then
376       let fna = functionArgs v;
377           showFnas = concatStringsSep ", " (mapAttrsToList
378                        (name: hasDefVal: if hasDefVal then name + "?" else name)
379                        fna);
380       in if fna == {}    then "<function>"
381                          else "<function, args: {${showFnas}}>"
382     else if isAttrs    v then
383       # apply pretty values if allowed
384       if allowPrettyValues && v ? __pretty && v ? val
385          then v.__pretty v.val
386       else if v == {} then "{ }"
387       else if v ? type && v.type == "derivation" then
388         "<derivation ${v.name or "???"}>"
389       else "{" + introSpace
390           + concatStringsSep introSpace (mapAttrsToList
391               (name: value:
392                 "${escapeNixIdentifier name} = ${
393                   addErrorContext "while evaluating an attribute `${name}`"
394                     (go (indent + "  ") value)
395                 };") v)
396         + outroSpace + "}"
397     else abort "generators.toPretty: should never happen (v = ${v})";
398   in go indent;
400   # PLIST handling
401   toPlist = {}: v: let
402     expr = ind: x:
403       if x == null  then "" else
404       if isBool x   then bool ind x else
405       if isInt x    then int ind x else
406       if isString x then str ind x else
407       if isList x   then list ind x else
408       if isAttrs x  then attrs ind x else
409       if isPath x   then str ind (toString x) else
410       if isFloat x  then float ind x else
411       abort "generators.toPlist: should never happen (v = ${v})";
413     literal = ind: x: ind + x;
415     bool = ind: x: literal ind  (if x then "<true/>" else "<false/>");
416     int = ind: x: literal ind "<integer>${toString x}</integer>";
417     str = ind: x: literal ind "<string>${x}</string>";
418     key = ind: x: literal ind "<key>${x}</key>";
419     float = ind: x: literal ind "<real>${toString x}</real>";
421     indent = ind: expr "\t${ind}";
423     item = ind: concatMapStringsSep "\n" (indent ind);
425     list = ind: x: concatStringsSep "\n" [
426       (literal ind "<array>")
427       (item ind x)
428       (literal ind "</array>")
429     ];
431     attrs = ind: x: concatStringsSep "\n" [
432       (literal ind "<dict>")
433       (attr ind x)
434       (literal ind "</dict>")
435     ];
437     attr = let attrFilter = name: value: name != "_module" && value != null;
438     in ind: x: concatStringsSep "\n" (flatten (mapAttrsToList
439       (name: value: optionals (attrFilter name value) [
440       (key "\t${ind}" name)
441       (expr "\t${ind}" value)
442     ]) x));
444   in ''<?xml version="1.0" encoding="UTF-8"?>
445 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
446 <plist version="1.0">
447 ${expr "" v}
448 </plist>'';
450   /* Translate a simple Nix expression to Dhall notation.
451    * Note that integers are translated to Integer and never
452    * the Natural type.
453   */
454   toDhall = { }@args: v:
455     let concatItems = concatStringsSep ", ";
456     in if isAttrs v then
457       "{ ${
458         concatItems (mapAttrsToList
459           (key: value: "${key} = ${toDhall args value}") v)
460       } }"
461     else if isList v then
462       "[ ${concatItems (map (toDhall args) v)} ]"
463     else if isInt v then
464       "${if v < 0 then "" else "+"}${toString v}"
465     else if isBool v then
466       (if v then "True" else "False")
467     else if isFunction v then
468       abort "generators.toDhall: cannot convert a function to Dhall"
469     else if v == null then
470       abort "generators.toDhall: cannot convert a null to Dhall"
471     else
472       toJSON v;
474   /*
475    Translate a simple Nix expression to Lua representation with occasional
476    Lua-inlines that can be constructed by mkLuaInline function.
478    Configuration:
479      * multiline - by default is true which results in indented block-like view.
480      * indent - initial indent.
481      * asBindings - by default generate single value, but with this use attrset to set global vars.
483    Attention:
484      Regardless of multiline parameter there is no trailing newline.
486    Example:
487      generators.toLua {}
488        {
489          cmd = [ "typescript-language-server" "--stdio" ];
490          settings.workspace.library = mkLuaInline ''vim.api.nvim_get_runtime_file("", true)'';
491        }
492      ->
493       {
494         ["cmd"] = {
495           "typescript-language-server",
496           "--stdio"
497         },
498         ["settings"] = {
499           ["workspace"] = {
500             ["library"] = (vim.api.nvim_get_runtime_file("", true))
501           }
502         }
503       }
505    Type:
506      toLua :: AttrSet -> Any -> String
507   */
508   toLua = {
509     /* If this option is true, the output is indented with newlines for attribute sets and lists */
510     multiline ? true,
511     /* Initial indentation level */
512     indent ? "",
513     /* Interpret as variable bindings */
514     asBindings ? false,
515   }@args: v:
516     let
517       innerIndent = "${indent}  ";
518       introSpace = if multiline then "\n${innerIndent}" else " ";
519       outroSpace = if multiline then "\n${indent}" else " ";
520       innerArgs = args // {
521         indent = if asBindings then indent else innerIndent;
522         asBindings = false;
523       };
524       concatItems = concatStringsSep ",${introSpace}";
525       isLuaInline = { _type ? null, ... }: _type == "lua-inline";
527       generatedBindings =
528           assert assertMsg (badVarNames == []) "Bad Lua var names: ${toPretty {} badVarNames}";
529           concatStrings (
530             mapAttrsToList (key: value: "${indent}${key} = ${toLua innerArgs value}\n") v
531             );
533       # https://en.wikibooks.org/wiki/Lua_Programming/variable#Variable_names
534       matchVarName = match "[[:alpha:]_][[:alnum:]_]*(\\.[[:alpha:]_][[:alnum:]_]*)*";
535       badVarNames = filter (name: matchVarName name == null) (attrNames v);
536     in
537     if asBindings then
538       generatedBindings
539     else if v == null then
540       "nil"
541     else if isInt v || isFloat v || isString v || isBool v then
542       toJSON v
543     else if isList v then
544       (if v == [ ] then "{}" else
545       "{${introSpace}${concatItems (map (value: "${toLua innerArgs value}") v)}${outroSpace}}")
546     else if isAttrs v then
547       (
548         if isLuaInline v then
549           "(${v.expr})"
550         else if v == { } then
551           "{}"
552         else if isDerivation v then
553           ''"${toString v}"''
554         else
555           "{${introSpace}${concatItems (
556             mapAttrsToList (key: value: "[${toJSON key}] = ${toLua innerArgs value}") v
557             )}${outroSpace}}"
558       )
559     else
560       abort "generators.toLua: type ${typeOf v} is unsupported";
562   /*
563    Mark string as Lua expression to be inlined when processed by toLua.
565    Type:
566      mkLuaInline :: String -> AttrSet
567   */
568   mkLuaInline = expr: { _type = "lua-inline"; inherit expr; };
572 # Everything in this attrset is the public interface of the file.
574   inherit
575     mkDconfKeyValue
576     mkKeyValueDefault
577     mkLuaInline
578     mkValueStringDefault
579     toDconfINI
580     toDhall
581     toGitINI
582     toINI
583     toINIWithGlobalSection
584     toKeyValue
585     toLua
586     toPlist
587     toPretty
588     withRecursion
589     ;
591   /* Generates JSON from an arbitrary (non-function) value.
592     * For more information see the documentation of the builtin.
593     */
594   toJSON = {}: toJSON;
596   /* YAML has been a strict superset of JSON since 1.2, so we
597     * use toJSON. Before it only had a few differences referring
598     * to implicit typing rules, so it should work with older
599     * parsers as well.
600     */
601   toYAML = {}: toJSON;