Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
1# Nixpkgs/NixOS option handling. 2{ lib }: 3 4let 5 inherit (lib) 6 all 7 collect 8 concatLists 9 concatMap 10 concatMapStringsSep 11 filter 12 foldl' 13 head 14 tail 15 isAttrs 16 isBool 17 isDerivation 18 isFunction 19 isInt 20 isList 21 isString 22 length 23 mapAttrs 24 optional 25 optionals 26 take 27 ; 28 inherit (lib.attrsets) 29 attrByPath 30 optionalAttrs 31 ; 32 inherit (lib.strings) 33 concatMapStrings 34 concatStringsSep 35 ; 36 inherit (lib.types) 37 mkOptionType 38 ; 39 inherit (lib.lists) 40 last 41 ; 42 prioritySuggestion = '' 43 Use `lib.mkForce value` or `lib.mkDefault value` to change the priority on any of these definitions. 44 ''; 45in 46rec { 47 48 /* Returns true when the given argument is an option 49 50 Type: isOption :: a -> bool 51 52 Example: 53 isOption 1 // => false 54 isOption (mkOption {}) // => true 55 */ 56 isOption = lib.isType "option"; 57 58 /* Creates an Option attribute set. mkOption accepts an attribute set with the following keys: 59 60 All keys default to `null` when not given. 61 62 Example: 63 mkOption { } // => { _type = "option"; } 64 mkOption { default = "foo"; } // => { _type = "option"; default = "foo"; } 65 */ 66 mkOption = 67 { 68 # Default value used when no definition is given in the configuration. 69 default ? null, 70 # Textual representation of the default, for the manual. 71 defaultText ? null, 72 # Example value used in the manual. 73 example ? null, 74 # String describing the option. 75 description ? null, 76 # Related packages used in the manual (see `genRelatedPackages` in ../nixos/lib/make-options-doc/default.nix). 77 relatedPackages ? null, 78 # Option type, providing type-checking and value merging. 79 type ? null, 80 # Function that converts the option value to something else. 81 apply ? null, 82 # Whether the option is for NixOS developers only. 83 internal ? null, 84 # Whether the option shows up in the manual. Default: true. Use false to hide the option and any sub-options from submodules. Use "shallow" to hide only sub-options. 85 visible ? null, 86 # Whether the option can be set only once 87 readOnly ? null, 88 } @ attrs: 89 attrs // { _type = "option"; }; 90 91 /* Creates an Option attribute set for a boolean value option i.e an 92 option to be toggled on or off: 93 94 Example: 95 mkEnableOption "foo" 96 => { _type = "option"; default = false; description = "Whether to enable foo."; example = true; type = { ... }; } 97 */ 98 mkEnableOption = 99 # Name for the created option 100 name: mkOption { 101 default = false; 102 example = true; 103 description = 104 if name ? _type && name._type == "mdDoc" 105 then lib.mdDoc "Whether to enable ${name.text}." 106 else "Whether to enable ${name}."; 107 type = lib.types.bool; 108 }; 109 110 /* Creates an Option attribute set for an option that specifies the 111 package a module should use for some purpose. 112 113 The package is specified in the third argument under `default` as a list of strings 114 representing its attribute path in nixpkgs (or another package set). 115 Because of this, you need to pass nixpkgs itself (or a subset) as the first argument. 116 117 The second argument may be either a string or a list of strings. 118 It provides the display name of the package in the description of the generated option 119 (using only the last element if the passed value is a list) 120 and serves as the fallback value for the `default` argument. 121 122 To include extra information in the description, pass `extraDescription` to 123 append arbitrary text to the generated description. 124 You can also pass an `example` value, either a literal string or an attribute path. 125 126 The default argument can be omitted if the provided name is 127 an attribute of pkgs (if name is a string) or a 128 valid attribute path in pkgs (if name is a list). 129 130 If you wish to explicitly provide no default, pass `null` as `default`. 131 132 Type: mkPackageOption :: pkgs -> (string|[string]) -> { default? :: [string], example? :: null|string|[string], extraDescription? :: string } -> option 133 134 Example: 135 mkPackageOption pkgs "hello" { } 136 => { _type = "option"; default = «derivation /nix/store/3r2vg51hlxj3cx5vscp0vkv60bqxkaq0-hello-2.10.drv»; defaultText = { ... }; description = "The hello package to use."; type = { ... }; } 137 138 Example: 139 mkPackageOption pkgs "GHC" { 140 default = [ "ghc" ]; 141 example = "pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])"; 142 } 143 => { _type = "option"; default = «derivation /nix/store/jxx55cxsjrf8kyh3fp2ya17q99w7541r-ghc-8.10.7.drv»; defaultText = { ... }; description = "The GHC package to use."; example = { ... }; type = { ... }; } 144 145 Example: 146 mkPackageOption pkgs [ "python39Packages" "pytorch" ] { 147 extraDescription = "This is an example and doesn't actually do anything."; 148 } 149 => { _type = "option"; default = «derivation /nix/store/gvqgsnc4fif9whvwd9ppa568yxbkmvk8-python3.9-pytorch-1.10.2.drv»; defaultText = { ... }; description = "The pytorch package to use. This is an example and doesn't actually do anything."; type = { ... }; } 150 151 */ 152 mkPackageOption = 153 # Package set (a specific version of nixpkgs or a subset) 154 pkgs: 155 # Name for the package, shown in option description 156 name: 157 { 158 # Whether the package can be null, for example to disable installing a package altogether. 159 nullable ? false, 160 # The attribute path where the default package is located (may be omitted) 161 default ? name, 162 # A string or an attribute path to use as an example (may be omitted) 163 example ? null, 164 # Additional text to include in the option description (may be omitted) 165 extraDescription ? "", 166 }: 167 let 168 name' = if isList name then last name else name; 169 in mkOption ({ 170 type = with lib.types; (if nullable then nullOr else lib.id) package; 171 description = "The ${name'} package to use." 172 + (if extraDescription == "" then "" else " ") + extraDescription; 173 } // (if default != null then let 174 default' = if isList default then default else [ default ]; 175 defaultPath = concatStringsSep "." default'; 176 defaultValue = attrByPath default' 177 (throw "${defaultPath} cannot be found in pkgs") pkgs; 178 in { 179 default = defaultValue; 180 defaultText = literalExpression ("pkgs." + defaultPath); 181 } else if nullable then { 182 default = null; 183 } else { }) // lib.optionalAttrs (example != null) { 184 example = literalExpression 185 (if isList example then "pkgs." + concatStringsSep "." example else example); 186 }); 187 188 /* Like mkPackageOption, but emit an mdDoc description instead of DocBook. */ 189 mkPackageOptionMD = pkgs: name: extra: 190 let option = mkPackageOption pkgs name extra; 191 in option // { description = lib.mdDoc option.description; }; 192 193 /* This option accepts anything, but it does not produce any result. 194 195 This is useful for sharing a module across different module sets 196 without having to implement similar features as long as the 197 values of the options are not accessed. */ 198 mkSinkUndeclaredOptions = attrs: mkOption ({ 199 internal = true; 200 visible = false; 201 default = false; 202 description = "Sink for option definitions."; 203 type = mkOptionType { 204 name = "sink"; 205 check = x: true; 206 merge = loc: defs: false; 207 }; 208 apply = x: throw "Option value is not readable because the option is not declared."; 209 } // attrs); 210 211 mergeDefaultOption = loc: defs: 212 let list = getValues defs; in 213 if length list == 1 then head list 214 else if all isFunction list then x: mergeDefaultOption loc (map (f: f x) list) 215 else if all isList list then concatLists list 216 else if all isAttrs list then foldl' lib.mergeAttrs {} list 217 else if all isBool list then foldl' lib.or false list 218 else if all isString list then lib.concatStrings list 219 else if all isInt list && all (x: x == head list) list then head list 220 else throw "Cannot merge definitions of `${showOption loc}'. Definition values:${showDefs defs}"; 221 222 mergeOneOption = mergeUniqueOption { message = ""; }; 223 224 mergeUniqueOption = { message }: loc: defs: 225 if length defs == 1 226 then (head defs).value 227 else assert length defs > 1; 228 throw "The option `${showOption loc}' is defined multiple times while it's expected to be unique.\n${message}\nDefinition values:${showDefs defs}\n${prioritySuggestion}"; 229 230 /* "Merge" option definitions by checking that they all have the same value. */ 231 mergeEqualOption = loc: defs: 232 if defs == [] then abort "This case should never happen." 233 # Return early if we only have one element 234 # This also makes it work for functions, because the foldl' below would try 235 # to compare the first element with itself, which is false for functions 236 else if length defs == 1 then (head defs).value 237 else (foldl' (first: def: 238 if def.value != first.value then 239 throw "The option `${showOption loc}' has conflicting definition values:${showDefs [ first def ]}\n${prioritySuggestion}" 240 else 241 first) (head defs) (tail defs)).value; 242 243 /* Extracts values of all "value" keys of the given list. 244 245 Type: getValues :: [ { value :: a; } ] -> [a] 246 247 Example: 248 getValues [ { value = 1; } { value = 2; } ] // => [ 1 2 ] 249 getValues [ ] // => [ ] 250 */ 251 getValues = map (x: x.value); 252 253 /* Extracts values of all "file" keys of the given list 254 255 Type: getFiles :: [ { file :: a; } ] -> [a] 256 257 Example: 258 getFiles [ { file = "file1"; } { file = "file2"; } ] // => [ "file1" "file2" ] 259 getFiles [ ] // => [ ] 260 */ 261 getFiles = map (x: x.file); 262 263 # Generate documentation template from the list of option declaration like 264 # the set generated with filterOptionSets. 265 optionAttrSetToDocList = optionAttrSetToDocList' []; 266 267 optionAttrSetToDocList' = _: options: 268 concatMap (opt: 269 let 270 name = showOption opt.loc; 271 docOption = { 272 loc = opt.loc; 273 inherit name; 274 description = opt.description or null; 275 declarations = filter (x: x != unknownModule) opt.declarations; 276 internal = opt.internal or false; 277 visible = 278 if (opt?visible && opt.visible == "shallow") 279 then true 280 else opt.visible or true; 281 readOnly = opt.readOnly or false; 282 type = opt.type.description or "unspecified"; 283 } 284 // optionalAttrs (opt ? example) { 285 example = 286 builtins.addErrorContext "while evaluating the example of option `${name}`" ( 287 renderOptionValue opt.example 288 ); 289 } 290 // optionalAttrs (opt ? defaultText || opt ? default) { 291 default = 292 builtins.addErrorContext "while evaluating the ${if opt?defaultText then "defaultText" else "default value"} of option `${name}`" ( 293 renderOptionValue (opt.defaultText or opt.default) 294 ); 295 } 296 // optionalAttrs (opt ? relatedPackages && opt.relatedPackages != null) { inherit (opt) relatedPackages; }; 297 298 subOptions = 299 let ss = opt.type.getSubOptions opt.loc; 300 in if ss != {} then optionAttrSetToDocList' opt.loc ss else []; 301 subOptionsVisible = docOption.visible && opt.visible or null != "shallow"; 302 in 303 # To find infinite recursion in NixOS option docs: 304 # builtins.trace opt.loc 305 [ docOption ] ++ optionals subOptionsVisible subOptions) (collect isOption options); 306 307 308 /* This function recursively removes all derivation attributes from 309 `x` except for the `name` attribute. 310 311 This is to make the generation of `options.xml` much more 312 efficient: the XML representation of derivations is very large 313 (on the order of megabytes) and is not actually used by the 314 manual generator. 315 316 This function was made obsolete by renderOptionValue and is kept for 317 compatibility with out-of-tree code. 318 */ 319 scrubOptionValue = x: 320 if isDerivation x then 321 { type = "derivation"; drvPath = x.name; outPath = x.name; name = x.name; } 322 else if isList x then map scrubOptionValue x 323 else if isAttrs x then mapAttrs (n: v: scrubOptionValue v) (removeAttrs x ["_args"]) 324 else x; 325 326 327 /* Ensures that the given option value (default or example) is a `_type`d string 328 by rendering Nix values to `literalExpression`s. 329 */ 330 renderOptionValue = v: 331 if v ? _type && v ? text then v 332 else literalExpression (lib.generators.toPretty { 333 multiline = true; 334 allowPrettyValues = true; 335 } v); 336 337 338 /* For use in the `defaultText` and `example` option attributes. Causes the 339 given string to be rendered verbatim in the documentation as Nix code. This 340 is necessary for complex values, e.g. functions, or values that depend on 341 other values or packages. 342 */ 343 literalExpression = text: 344 if ! isString text then throw "literalExpression expects a string." 345 else { _type = "literalExpression"; inherit text; }; 346 347 literalExample = lib.warn "literalExample is deprecated, use literalExpression instead, or use literalDocBook for a non-Nix description." literalExpression; 348 349 350 /* For use in the `defaultText` and `example` option attributes. Causes the 351 given DocBook text to be inserted verbatim in the documentation, for when 352 a `literalExpression` would be too hard to read. 353 */ 354 literalDocBook = text: 355 if ! isString text then throw "literalDocBook expects a string." 356 else 357 lib.warnIf (lib.isInOldestRelease 2211) 358 "literalDocBook is deprecated, use literalMD instead" 359 { _type = "literalDocBook"; inherit text; }; 360 361 /* Transition marker for documentation that's already migrated to markdown 362 syntax. 363 */ 364 mdDoc = text: 365 if ! isString text then throw "mdDoc expects a string." 366 else { _type = "mdDoc"; inherit text; }; 367 368 /* For use in the `defaultText` and `example` option attributes. Causes the 369 given MD text to be inserted verbatim in the documentation, for when 370 a `literalExpression` would be too hard to read. 371 */ 372 literalMD = text: 373 if ! isString text then throw "literalMD expects a string." 374 else { _type = "literalMD"; inherit text; }; 375 376 # Helper functions. 377 378 /* Convert an option, described as a list of the option parts to a 379 human-readable version. 380 381 Example: 382 (showOption ["foo" "bar" "baz"]) == "foo.bar.baz" 383 (showOption ["foo" "bar.baz" "tux"]) == "foo.\"bar.baz\".tux" 384 (showOption ["windowManager" "2bwm" "enable"]) == "windowManager.\"2bwm\".enable" 385 386 Placeholders will not be quoted as they are not actual values: 387 (showOption ["foo" "*" "bar"]) == "foo.*.bar" 388 (showOption ["foo" "<name>" "bar"]) == "foo.<name>.bar" 389 */ 390 showOption = parts: let 391 escapeOptionPart = part: 392 let 393 # We assume that these are "special values" and not real configuration data. 394 # If it is real configuration data, it is rendered incorrectly. 395 specialIdentifiers = [ 396 "<name>" # attrsOf (submodule {}) 397 "*" # listOf (submodule {}) 398 "<function body>" # functionTo 399 ]; 400 in if builtins.elem part specialIdentifiers 401 then part 402 else lib.strings.escapeNixIdentifier part; 403 in (concatStringsSep ".") (map escapeOptionPart parts); 404 showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files); 405 406 showDefs = defs: concatMapStrings (def: 407 let 408 # Pretty print the value for display, if successful 409 prettyEval = builtins.tryEval 410 (lib.generators.toPretty { } 411 (lib.generators.withRecursion { depthLimit = 10; throwOnDepthLimit = false; } def.value)); 412 # Split it into its lines 413 lines = filter (v: ! isList v) (builtins.split "\n" prettyEval.value); 414 # Only display the first 5 lines, and indent them for better visibility 415 value = concatStringsSep "\n " (take 5 lines ++ optional (length lines > 5) "..."); 416 result = 417 # Don't print any value if evaluating the value strictly fails 418 if ! prettyEval.success then "" 419 # Put it on a new line if it consists of multiple 420 else if length lines > 1 then ":\n " + value 421 else ": " + value; 422 in "\n- In `${def.file}'${result}" 423 ) defs; 424 425 showOptionWithDefLocs = opt: '' 426 ${showOption opt.loc}, with values defined in: 427 ${concatMapStringsSep "\n" (defFile: " - ${defFile}") opt.files} 428 ''; 429 430 unknownModule = "<unknown-file>"; 431 432}