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 }:
17with (lib).trivial;
18let
19 libStr = lib.strings;
20 libAttr = lib.attrsets;
21
22 inherit (lib) isFunction;
23in
24
25rec {
26
27 ## -- HELPER FUNCTIONS & DEFAULTS --
28
29 /* Convert a value to a sensible default string representation.
30 * The builtin `toString` function has some strange defaults,
31 * suitable for bash scripts but not much else.
32 */
33 mkValueStringDefault = {}: v: with builtins;
34 let err = t: v: abort
35 ("generators.mkValueStringDefault: " +
36 "${t} not supported: ${toPretty {} v}");
37 in if isInt v then toString v
38 # convert derivations to store paths
39 else if lib.isDerivation v then toString v
40 # we default to not quoting strings
41 else if isString v then v
42 # isString returns "1", which is not a good default
43 else if true == v then "true"
44 # here it returns to "", which is even less of a good default
45 else if false == v then "false"
46 else if null == v then "null"
47 # if you have lists you probably want to replace this
48 else if isList v then err "lists" v
49 # same as for lists, might want to replace
50 else if isAttrs v then err "attrsets" v
51 # functions can’t be printed of course
52 else if isFunction v then err "functions" v
53 # Floats currently can't be converted to precise strings,
54 # condition warning on nix version once this isn't a problem anymore
55 # See https://github.com/NixOS/nix/pull/3480
56 else if isFloat v then libStr.floatToString v
57 else err "this value is" (toString v);
58
59
60 /* Generate a line of key k and value v, separated by
61 * character sep. If sep appears in k, it is escaped.
62 * Helper for synaxes with different separators.
63 *
64 * mkValueString specifies how values should be formatted.
65 *
66 * mkKeyValueDefault {} ":" "f:oo" "bar"
67 * > "f\:oo:bar"
68 */
69 mkKeyValueDefault = {
70 mkValueString ? mkValueStringDefault {}
71 }: sep: k: v:
72 "${libStr.escape [sep] k}${sep}${mkValueString v}";
73
74
75 ## -- FILE FORMAT GENERATORS --
76
77
78 /* Generate a key-value-style config file from an attrset.
79 *
80 * mkKeyValue is the same as in toINI.
81 */
82 toKeyValue = {
83 mkKeyValue ? mkKeyValueDefault {} "=",
84 listsAsDuplicateKeys ? false
85 }:
86 let mkLine = k: v: mkKeyValue k v + "\n";
87 mkLines = if listsAsDuplicateKeys
88 then k: v: map (mkLine k) (if lib.isList v then v else [v])
89 else k: v: [ (mkLine k v) ];
90 in attrs: libStr.concatStrings (lib.concatLists (libAttr.mapAttrsToList mkLines attrs));
91
92
93 /* Generate an INI-style config file from an
94 * attrset of sections to an attrset of key-value pairs.
95 *
96 * generators.toINI {} {
97 * foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
98 * baz = { "also, integers" = 42; };
99 * }
100 *
101 *> [baz]
102 *> also, integers=42
103 *>
104 *> [foo]
105 *> ciao=bar
106 *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
107 *
108 * The mk* configuration attributes can generically change
109 * the way sections and key-value strings are generated.
110 *
111 * For more examples see the test cases in ./tests/misc.nix.
112 */
113 toINI = {
114 # apply transformations (e.g. escapes) to section names
115 mkSectionName ? (name: libStr.escape [ "[" "]" ] name),
116 # format a setting line from key and value
117 mkKeyValue ? mkKeyValueDefault {} "=",
118 # allow lists as values for duplicate keys
119 listsAsDuplicateKeys ? false
120 }: attrsOfAttrs:
121 let
122 # map function to string for each key val
123 mapAttrsToStringsSep = sep: mapFn: attrs:
124 libStr.concatStringsSep sep
125 (libAttr.mapAttrsToList mapFn attrs);
126 mkSection = sectName: sectValues: ''
127 [${mkSectionName sectName}]
128 '' + toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } sectValues;
129 in
130 # map input to ini sections
131 mapAttrsToStringsSep "\n" mkSection attrsOfAttrs;
132
133 /* Generate an INI-style config file from an attrset
134 * specifying the global section (no header), and an
135 * attrset of sections to an attrset of key-value pairs.
136 *
137 * generators.toINIWithGlobalSection {} {
138 * globalSection = {
139 * someGlobalKey = "hi";
140 * };
141 * sections = {
142 * foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
143 * baz = { "also, integers" = 42; };
144 * }
145 *
146 *> someGlobalKey=hi
147 *>
148 *> [baz]
149 *> also, integers=42
150 *>
151 *> [foo]
152 *> ciao=bar
153 *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
154 *
155 * The mk* configuration attributes can generically change
156 * the way sections and key-value strings are generated.
157 *
158 * For more examples see the test cases in ./tests/misc.nix.
159 *
160 * If you don’t need a global section, you can also use
161 * `generators.toINI` directly, which only takes
162 * the part in `sections`.
163 */
164 toINIWithGlobalSection = {
165 # apply transformations (e.g. escapes) to section names
166 mkSectionName ? (name: libStr.escape [ "[" "]" ] name),
167 # format a setting line from key and value
168 mkKeyValue ? mkKeyValueDefault {} "=",
169 # allow lists as values for duplicate keys
170 listsAsDuplicateKeys ? false
171 }: { globalSection, sections }:
172 ( if globalSection == {}
173 then ""
174 else (toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } globalSection)
175 + "\n")
176 + (toINI { inherit mkSectionName mkKeyValue listsAsDuplicateKeys; } sections);
177
178 /* Generate a git-config file from an attrset.
179 *
180 * It has two major differences from the regular INI format:
181 *
182 * 1. values are indented with tabs
183 * 2. sections can have sub-sections
184 *
185 * generators.toGitINI {
186 * url."ssh://git@github.com/".insteadOf = "https://github.com";
187 * user.name = "edolstra";
188 * }
189 *
190 *> [url "ssh://git@github.com/"]
191 *> insteadOf = https://github.com/
192 *>
193 *> [user]
194 *> name = edolstra
195 */
196 toGitINI = attrs:
197 with builtins;
198 let
199 mkSectionName = name:
200 let
201 containsQuote = libStr.hasInfix ''"'' name;
202 sections = libStr.splitString "." name;
203 section = head sections;
204 subsections = tail sections;
205 subsection = concatStringsSep "." subsections;
206 in if containsQuote || subsections == [ ] then
207 name
208 else
209 ''${section} "${subsection}"'';
210
211 # generation for multiple ini values
212 mkKeyValue = k: v:
213 let mkKeyValue = mkKeyValueDefault { } " = " k;
214 in concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (lib.toList v));
215
216 # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI
217 gitFlattenAttrs = let
218 recurse = path: value:
219 if isAttrs value && !lib.isDerivation value then
220 lib.mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value
221 else if length path > 1 then {
222 ${concatStringsSep "." (lib.reverseList (tail path))}.${head path} = value;
223 } else {
224 ${head path} = value;
225 };
226 in attrs: lib.foldl lib.recursiveUpdate { } (lib.flatten (recurse [ ] attrs));
227
228 toINI_ = toINI { inherit mkKeyValue mkSectionName; };
229 in
230 toINI_ (gitFlattenAttrs attrs);
231
232 /* Generates JSON from an arbitrary (non-function) value.
233 * For more information see the documentation of the builtin.
234 */
235 toJSON = {}: builtins.toJSON;
236
237
238 /* YAML has been a strict superset of JSON since 1.2, so we
239 * use toJSON. Before it only had a few differences referring
240 * to implicit typing rules, so it should work with older
241 * parsers as well.
242 */
243 toYAML = toJSON;
244
245 withRecursion =
246 {
247 /* If this option is not null, the given value will stop evaluating at a certain depth */
248 depthLimit
249 /* If this option is true, an error will be thrown, if a certain given depth is exceeded */
250 , throwOnDepthLimit ? true
251 }:
252 assert builtins.isInt depthLimit;
253 let
254 specialAttrs = [
255 "__functor"
256 "__functionArgs"
257 "__toString"
258 "__pretty"
259 ];
260 stepIntoAttr = evalNext: name:
261 if builtins.elem name specialAttrs
262 then id
263 else evalNext;
264 transform = depth:
265 if depthLimit != null && depth > depthLimit then
266 if throwOnDepthLimit
267 then throw "Exceeded maximum eval-depth limit of ${toString depthLimit} while trying to evaluate with `generators.withRecursion'!"
268 else const "<unevaluated>"
269 else id;
270 mapAny = with builtins; depth: v:
271 let
272 evalNext = x: mapAny (depth + 1) (transform (depth + 1) x);
273 in
274 if isAttrs v then mapAttrs (stepIntoAttr evalNext) v
275 else if isList v then map evalNext v
276 else transform (depth + 1) v;
277 in
278 mapAny 0;
279
280 /* Pretty print a value, akin to `builtins.trace`.
281 * Should probably be a builtin as well.
282 */
283 toPretty = {
284 /* If this option is true, attrsets like { __pretty = fn; val = …; }
285 will use fn to convert val to a pretty printed representation.
286 (This means fn is type Val -> String.) */
287 allowPrettyValues ? false,
288 /* If this option is true, the output is indented with newlines for attribute sets and lists */
289 multiline ? true
290 }:
291 let
292 go = indent: v: with builtins;
293 let isPath = v: typeOf v == "path";
294 introSpace = if multiline then "\n${indent} " else " ";
295 outroSpace = if multiline then "\n${indent}" else " ";
296 in if isInt v then toString v
297 else if isFloat v then "~${toString v}"
298 else if isString v then
299 let
300 # Separate a string into its lines
301 newlineSplits = filter (v: ! isList v) (builtins.split "\n" v);
302 # For a '' string terminated by a \n, which happens when the closing '' is on a new line
303 multilineResult = "''" + introSpace + concatStringsSep introSpace (lib.init newlineSplits) + outroSpace + "''";
304 # For a '' string not terminated by a \n, which happens when the closing '' is not on a new line
305 multilineResult' = "''" + introSpace + concatStringsSep introSpace newlineSplits + "''";
306 # For single lines, replace all newlines with their escaped representation
307 singlelineResult = "\"" + libStr.escape [ "\"" ] (concatStringsSep "\\n" newlineSplits) + "\"";
308 in if multiline && length newlineSplits > 1 then
309 if lib.last newlineSplits == "" then multilineResult else multilineResult'
310 else singlelineResult
311 else if true == v then "true"
312 else if false == v then "false"
313 else if null == v then "null"
314 else if isPath v then toString v
315 else if isList v then
316 if v == [] then "[ ]"
317 else "[" + introSpace
318 + libStr.concatMapStringsSep introSpace (go (indent + " ")) v
319 + outroSpace + "]"
320 else if isFunction v then
321 let fna = lib.functionArgs v;
322 showFnas = concatStringsSep ", " (libAttr.mapAttrsToList
323 (name: hasDefVal: if hasDefVal then name + "?" else name)
324 fna);
325 in if fna == {} then "<function>"
326 else "<function, args: {${showFnas}}>"
327 else if isAttrs v then
328 # apply pretty values if allowed
329 if attrNames v == [ "__pretty" "val" ] && allowPrettyValues
330 then v.__pretty v.val
331 else if v == {} then "{ }"
332 else if v ? type && v.type == "derivation" then
333 "<derivation ${v.drvPath or "???"}>"
334 else "{" + introSpace
335 + libStr.concatStringsSep introSpace (libAttr.mapAttrsToList
336 (name: value:
337 "${libStr.escapeNixIdentifier name} = ${go (indent + " ") value};") v)
338 + outroSpace + "}"
339 else abort "generators.toPretty: should never happen (v = ${v})";
340 in go "";
341
342 # PLIST handling
343 toPlist = {}: v: let
344 isFloat = builtins.isFloat or (x: false);
345 expr = ind: x: with builtins;
346 if x == null then "" else
347 if isBool x then bool ind x else
348 if isInt x then int ind x else
349 if isString x then str ind x else
350 if isList x then list ind x else
351 if isAttrs x then attrs ind x else
352 if isFloat x then float ind x else
353 abort "generators.toPlist: should never happen (v = ${v})";
354
355 literal = ind: x: ind + x;
356
357 bool = ind: x: literal ind (if x then "<true/>" else "<false/>");
358 int = ind: x: literal ind "<integer>${toString x}</integer>";
359 str = ind: x: literal ind "<string>${x}</string>";
360 key = ind: x: literal ind "<key>${x}</key>";
361 float = ind: x: literal ind "<real>${toString x}</real>";
362
363 indent = ind: expr "\t${ind}";
364
365 item = ind: libStr.concatMapStringsSep "\n" (indent ind);
366
367 list = ind: x: libStr.concatStringsSep "\n" [
368 (literal ind "<array>")
369 (item ind x)
370 (literal ind "</array>")
371 ];
372
373 attrs = ind: x: libStr.concatStringsSep "\n" [
374 (literal ind "<dict>")
375 (attr ind x)
376 (literal ind "</dict>")
377 ];
378
379 attr = let attrFilter = name: value: name != "_module" && value != null;
380 in ind: x: libStr.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList
381 (name: value: lib.optionals (attrFilter name value) [
382 (key "\t${ind}" name)
383 (expr "\t${ind}" value)
384 ]) x));
385
386 in ''<?xml version="1.0" encoding="UTF-8"?>
387<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
388<plist version="1.0">
389${expr "" v}
390</plist>'';
391
392 /* Translate a simple Nix expression to Dhall notation.
393 * Note that integers are translated to Integer and never
394 * the Natural type.
395 */
396 toDhall = { }@args: v:
397 with builtins;
398 let concatItems = lib.strings.concatStringsSep ", ";
399 in if isAttrs v then
400 "{ ${
401 concatItems (lib.attrsets.mapAttrsToList
402 (key: value: "${key} = ${toDhall args value}") v)
403 } }"
404 else if isList v then
405 "[ ${concatItems (map (toDhall args) v)} ]"
406 else if isInt v then
407 "${if v < 0 then "" else "+"}${toString v}"
408 else if isBool v then
409 (if v then "True" else "False")
410 else if isFunction v then
411 abort "generators.toDhall: cannot convert a function to Dhall"
412 else if isNull v then
413 abort "generators.toDhall: cannot convert a null to Dhall"
414 else
415 builtins.toJSON v;
416}