at 23.05-pre 15 kB view raw
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 dont 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}