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 elemAt 12 filter 13 foldl' 14 head 15 tail 16 isAttrs 17 isBool 18 isDerivation 19 isFunction 20 isInt 21 isList 22 isString 23 length 24 mapAttrs 25 optional 26 optionals 27 take 28 ; 29 inherit (lib.attrsets) 30 attrByPath 31 optionalAttrs 32 ; 33 inherit (lib.strings) 34 concatMapStrings 35 concatStringsSep 36 ; 37 inherit (lib.types) 38 mkOptionType 39 ; 40in 41rec { 42 43 /* Returns true when the given argument is an option 44 45 Type: isOption :: a -> bool 46 47 Example: 48 isOption 1 // => false 49 isOption (mkOption {}) // => true 50 */ 51 isOption = lib.isType "option"; 52 53 /* Creates an Option attribute set. mkOption accepts an attribute set with the following keys: 54 55 All keys default to `null` when not given. 56 57 Example: 58 mkOption { } // => { _type = "option"; } 59 mkOption { default = "foo"; } // => { _type = "option"; default = "foo"; } 60 */ 61 mkOption = 62 { 63 # Default value used when no definition is given in the configuration. 64 default ? null, 65 # Textual representation of the default, for the manual. 66 defaultText ? null, 67 # Example value used in the manual. 68 example ? null, 69 # String describing the option. 70 description ? null, 71 # Related packages used in the manual (see `genRelatedPackages` in ../nixos/lib/make-options-doc/default.nix). 72 relatedPackages ? null, 73 # Option type, providing type-checking and value merging. 74 type ? null, 75 # Function that converts the option value to something else. 76 apply ? null, 77 # Whether the option is for NixOS developers only. 78 internal ? null, 79 # 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. 80 visible ? null, 81 # Whether the option can be set only once 82 readOnly ? null, 83 } @ attrs: 84 attrs // { _type = "option"; }; 85 86 /* Creates an Option attribute set for a boolean value option i.e an 87 option to be toggled on or off: 88 89 Example: 90 mkEnableOption "foo" 91 => { _type = "option"; default = false; description = "Whether to enable foo."; example = true; type = { ... }; } 92 */ 93 mkEnableOption = 94 # Name for the created option 95 name: mkOption { 96 default = false; 97 example = true; 98 description = "Whether to enable ${name}."; 99 type = lib.types.bool; 100 }; 101 102 /* Creates an Option attribute set for an option that specifies the 103 package a module should use for some purpose. 104 105 Type: mkPackageOption :: pkgs -> string -> { default :: [string], example :: null | string | [string] } -> option 106 107 The package is specified as a list of strings representing its attribute path in nixpkgs. 108 109 Because of this, you need to pass nixpkgs itself as the first argument. 110 111 The second argument is the name of the option, used in the description "The <name> package to use.". 112 113 You can also pass an example value, either a literal string or a package's attribute path. 114 115 You can omit the default path if the name of the option is also attribute path in nixpkgs. 116 117 Example: 118 mkPackageOption pkgs "hello" { } 119 => { _type = "option"; default = «derivation /nix/store/3r2vg51hlxj3cx5vscp0vkv60bqxkaq0-hello-2.10.drv»; defaultText = { ... }; description = "The hello package to use."; type = { ... }; } 120 121 Example: 122 mkPackageOption pkgs "GHC" { 123 default = [ "ghc" ]; 124 example = "pkgs.haskell.packages.ghc924.ghc.withPackages (hkgs: [ hkgs.primes ])"; 125 } 126 => { _type = "option"; default = «derivation /nix/store/jxx55cxsjrf8kyh3fp2ya17q99w7541r-ghc-8.10.7.drv»; defaultText = { ... }; description = "The GHC package to use."; example = { ... }; type = { ... }; } 127 */ 128 mkPackageOption = 129 # Package set (a specific version of nixpkgs) 130 pkgs: 131 # Name for the package, shown in option description 132 name: 133 { default ? [ name ], example ? null }: 134 let default' = if !isList default then [ default ] else default; 135 in mkOption { 136 type = lib.types.package; 137 description = "The ${name} package to use."; 138 default = attrByPath default' 139 (throw "${concatStringsSep "." default'} cannot be found in pkgs") pkgs; 140 defaultText = literalExpression ("pkgs." + concatStringsSep "." default'); 141 ${if example != null then "example" else null} = literalExpression 142 (if isList example then "pkgs." + concatStringsSep "." example else example); 143 }; 144 145 /* This option accepts anything, but it does not produce any result. 146 147 This is useful for sharing a module across different module sets 148 without having to implement similar features as long as the 149 values of the options are not accessed. */ 150 mkSinkUndeclaredOptions = attrs: mkOption ({ 151 internal = true; 152 visible = false; 153 default = false; 154 description = "Sink for option definitions."; 155 type = mkOptionType { 156 name = "sink"; 157 check = x: true; 158 merge = loc: defs: false; 159 }; 160 apply = x: throw "Option value is not readable because the option is not declared."; 161 } // attrs); 162 163 mergeDefaultOption = loc: defs: 164 let list = getValues defs; in 165 if length list == 1 then head list 166 else if all isFunction list then x: mergeDefaultOption loc (map (f: f x) list) 167 else if all isList list then concatLists list 168 else if all isAttrs list then foldl' lib.mergeAttrs {} list 169 else if all isBool list then foldl' lib.or false list 170 else if all isString list then lib.concatStrings list 171 else if all isInt list && all (x: x == head list) list then head list 172 else throw "Cannot merge definitions of `${showOption loc}'. Definition values:${showDefs defs}"; 173 174 mergeOneOption = mergeUniqueOption { message = ""; }; 175 176 mergeUniqueOption = { message }: loc: defs: 177 if length defs == 1 178 then (head defs).value 179 else assert length defs > 1; 180 throw "The option `${showOption loc}' is defined multiple times.\n${message}\nDefinition values:${showDefs defs}"; 181 182 /* "Merge" option definitions by checking that they all have the same value. */ 183 mergeEqualOption = loc: defs: 184 if defs == [] then abort "This case should never happen." 185 # Return early if we only have one element 186 # This also makes it work for functions, because the foldl' below would try 187 # to compare the first element with itself, which is false for functions 188 else if length defs == 1 then (head defs).value 189 else (foldl' (first: def: 190 if def.value != first.value then 191 throw "The option `${showOption loc}' has conflicting definition values:${showDefs [ first def ]}" 192 else 193 first) (head defs) (tail defs)).value; 194 195 /* Extracts values of all "value" keys of the given list. 196 197 Type: getValues :: [ { value :: a } ] -> [a] 198 199 Example: 200 getValues [ { value = 1; } { value = 2; } ] // => [ 1 2 ] 201 getValues [ ] // => [ ] 202 */ 203 getValues = map (x: x.value); 204 205 /* Extracts values of all "file" keys of the given list 206 207 Type: getFiles :: [ { file :: a } ] -> [a] 208 209 Example: 210 getFiles [ { file = "file1"; } { file = "file2"; } ] // => [ "file1" "file2" ] 211 getFiles [ ] // => [ ] 212 */ 213 getFiles = map (x: x.file); 214 215 # Generate documentation template from the list of option declaration like 216 # the set generated with filterOptionSets. 217 optionAttrSetToDocList = optionAttrSetToDocList' []; 218 219 optionAttrSetToDocList' = prefix: options: 220 concatMap (opt: 221 let 222 docOption = rec { 223 loc = opt.loc; 224 name = showOption opt.loc; 225 description = opt.description or null; 226 declarations = filter (x: x != unknownModule) opt.declarations; 227 internal = opt.internal or false; 228 visible = 229 if (opt?visible && opt.visible == "shallow") 230 then true 231 else opt.visible or true; 232 readOnly = opt.readOnly or false; 233 type = opt.type.description or "unspecified"; 234 } 235 // optionalAttrs (opt ? example) { example = scrubOptionValue opt.example; } 236 // optionalAttrs (opt ? default) { default = scrubOptionValue opt.default; } 237 // optionalAttrs (opt ? defaultText) { default = opt.defaultText; } 238 // optionalAttrs (opt ? relatedPackages && opt.relatedPackages != null) { inherit (opt) relatedPackages; }; 239 240 subOptions = 241 let ss = opt.type.getSubOptions opt.loc; 242 in if ss != {} then optionAttrSetToDocList' opt.loc ss else []; 243 subOptionsVisible = docOption.visible && opt.visible or null != "shallow"; 244 in 245 # To find infinite recursion in NixOS option docs: 246 # builtins.trace opt.loc 247 [ docOption ] ++ optionals subOptionsVisible subOptions) (collect isOption options); 248 249 250 /* This function recursively removes all derivation attributes from 251 `x` except for the `name` attribute. 252 253 This is to make the generation of `options.xml` much more 254 efficient: the XML representation of derivations is very large 255 (on the order of megabytes) and is not actually used by the 256 manual generator. 257 */ 258 scrubOptionValue = x: 259 if isDerivation x then 260 { type = "derivation"; drvPath = x.name; outPath = x.name; name = x.name; } 261 else if isList x then map scrubOptionValue x 262 else if isAttrs x then mapAttrs (n: v: scrubOptionValue v) (removeAttrs x ["_args"]) 263 else x; 264 265 266 /* For use in the `defaultText` and `example` option attributes. Causes the 267 given string to be rendered verbatim in the documentation as Nix code. This 268 is necessary for complex values, e.g. functions, or values that depend on 269 other values or packages. 270 */ 271 literalExpression = text: 272 if ! isString text then throw "literalExpression expects a string." 273 else { _type = "literalExpression"; inherit text; }; 274 275 literalExample = lib.warn "literalExample is deprecated, use literalExpression instead, or use literalDocBook for a non-Nix description." literalExpression; 276 277 278 /* For use in the `defaultText` and `example` option attributes. Causes the 279 given DocBook text to be inserted verbatim in the documentation, for when 280 a `literalExpression` would be too hard to read. 281 */ 282 literalDocBook = text: 283 if ! isString text then throw "literalDocBook expects a string." 284 else { _type = "literalDocBook"; inherit text; }; 285 286 /* Transition marker for documentation that's already migrated to markdown 287 syntax. 288 */ 289 mdDoc = text: 290 if ! isString text then throw "mdDoc expects a string." 291 else { _type = "mdDoc"; inherit text; }; 292 293 /* For use in the `defaultText` and `example` option attributes. Causes the 294 given MD text to be inserted verbatim in the documentation, for when 295 a `literalExpression` would be too hard to read. 296 */ 297 literalMD = text: 298 if ! isString text then throw "literalMD expects a string." 299 else { _type = "literalMD"; inherit text; }; 300 301 # Helper functions. 302 303 /* Convert an option, described as a list of the option parts in to a 304 safe, human readable version. 305 306 Example: 307 (showOption ["foo" "bar" "baz"]) == "foo.bar.baz" 308 (showOption ["foo" "bar.baz" "tux"]) == "foo.bar.baz.tux" 309 310 Placeholders will not be quoted as they are not actual values: 311 (showOption ["foo" "*" "bar"]) == "foo.*.bar" 312 (showOption ["foo" "<name>" "bar"]) == "foo.<name>.bar" 313 314 Unlike attributes, options can also start with numbers: 315 (showOption ["windowManager" "2bwm" "enable"]) == "windowManager.2bwm.enable" 316 */ 317 showOption = parts: let 318 escapeOptionPart = part: 319 let 320 escaped = lib.strings.escapeNixString part; 321 in if escaped == "\"${part}\"" 322 then part 323 else escaped; 324 in (concatStringsSep ".") (map escapeOptionPart parts); 325 showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files); 326 327 showDefs = defs: concatMapStrings (def: 328 let 329 # Pretty print the value for display, if successful 330 prettyEval = builtins.tryEval 331 (lib.generators.toPretty { } 332 (lib.generators.withRecursion { depthLimit = 10; throwOnDepthLimit = false; } def.value)); 333 # Split it into its lines 334 lines = filter (v: ! isList v) (builtins.split "\n" prettyEval.value); 335 # Only display the first 5 lines, and indent them for better visibility 336 value = concatStringsSep "\n " (take 5 lines ++ optional (length lines > 5) "..."); 337 result = 338 # Don't print any value if evaluating the value strictly fails 339 if ! prettyEval.success then "" 340 # Put it on a new line if it consists of multiple 341 else if length lines > 1 then ":\n " + value 342 else ": " + value; 343 in "\n- In `${def.file}'${result}" 344 ) defs; 345 346 showOptionWithDefLocs = opt: '' 347 ${showOption opt.loc}, with values defined in: 348 ${concatMapStringsSep "\n" (defFile: " - ${defFile}") opt.files} 349 ''; 350 351 unknownModule = "<unknown-file>"; 352 353}