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