at 21.11-pre 9.2 kB view raw
1# Nixpkgs/NixOS option handling. 2{ lib }: 3 4let 5 inherit (lib) 6 all 7 collect 8 concatLists 9 concatMap 10 elemAt 11 filter 12 foldl' 13 head 14 isAttrs 15 isBool 16 isDerivation 17 isFunction 18 isInt 19 isList 20 isString 21 length 22 mapAttrs 23 optional 24 optionals 25 take 26 ; 27 inherit (lib.attrsets) 28 optionalAttrs 29 ; 30 inherit (lib.strings) 31 concatMapStrings 32 concatStringsSep 33 ; 34 inherit (lib.types) 35 mkOptionType 36 ; 37in 38rec { 39 40 /* Returns true when the given argument is an option 41 42 Type: isOption :: a -> bool 43 44 Example: 45 isOption 1 // => false 46 isOption (mkOption {}) // => true 47 */ 48 isOption = lib.isType "option"; 49 50 /* Creates an Option attribute set. mkOption accepts an attribute set with the following keys: 51 52 All keys default to `null` when not given. 53 54 Example: 55 mkOption { } // => { _type = "option"; } 56 mkOption { defaultText = "foo"; } // => { _type = "option"; defaultText = "foo"; } 57 */ 58 mkOption = 59 { 60 # Default value used when no definition is given in the configuration. 61 default ? null, 62 # Textual representation of the default, for the manual. 63 defaultText ? null, 64 # Example value used in the manual. 65 example ? null, 66 # String describing the option. 67 description ? null, 68 # Related packages used in the manual (see `genRelatedPackages` in ../nixos/lib/make-options-doc/default.nix). 69 relatedPackages ? null, 70 # Option type, providing type-checking and value merging. 71 type ? null, 72 # Function that converts the option value to something else. 73 apply ? null, 74 # Whether the option is for NixOS developers only. 75 internal ? null, 76 # Whether the option shows up in the manual. 77 visible ? null, 78 # Whether the option can be set only once 79 readOnly ? null, 80 # Deprecated, used by types.optionSet. 81 options ? 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 = "Whether to enable ${name}."; 98 type = lib.types.bool; 99 }; 100 101 /* This option accepts anything, but it does not produce any result. 102 103 This is useful for sharing a module across different module sets 104 without having to implement similar features as long as the 105 values of the options are not accessed. */ 106 mkSinkUndeclaredOptions = attrs: mkOption ({ 107 internal = true; 108 visible = false; 109 default = false; 110 description = "Sink for option definitions."; 111 type = mkOptionType { 112 name = "sink"; 113 check = x: true; 114 merge = loc: defs: false; 115 }; 116 apply = x: throw "Option value is not readable because the option is not declared."; 117 } // attrs); 118 119 mergeDefaultOption = loc: defs: 120 let list = getValues defs; in 121 if length list == 1 then head list 122 else if all isFunction list then x: mergeDefaultOption loc (map (f: f x) list) 123 else if all isList list then concatLists list 124 else if all isAttrs list then foldl' lib.mergeAttrs {} list 125 else if all isBool list then foldl' lib.or false list 126 else if all isString list then lib.concatStrings list 127 else if all isInt list && all (x: x == head list) list then head list 128 else throw "Cannot merge definitions of `${showOption loc}'. Definition values:${showDefs defs}"; 129 130 mergeOneOption = loc: defs: 131 if defs == [] then abort "This case should never happen." 132 else if length defs != 1 then 133 throw "The unique option `${showOption loc}' is defined multiple times. Definition values:${showDefs defs}" 134 else (head defs).value; 135 136 /* "Merge" option definitions by checking that they all have the same value. */ 137 mergeEqualOption = loc: defs: 138 if defs == [] then abort "This case should never happen." 139 # Return early if we only have one element 140 # This also makes it work for functions, because the foldl' below would try 141 # to compare the first element with itself, which is false for functions 142 else if length defs == 1 then (head defs).value 143 else (foldl' (first: def: 144 if def.value != first.value then 145 throw "The option `${showOption loc}' has conflicting definition values:${showDefs [ first def ]}" 146 else 147 first) (head defs) defs).value; 148 149 /* Extracts values of all "value" keys of the given list. 150 151 Type: getValues :: [ { value :: a } ] -> [a] 152 153 Example: 154 getValues [ { value = 1; } { value = 2; } ] // => [ 1 2 ] 155 getValues [ ] // => [ ] 156 */ 157 getValues = map (x: x.value); 158 159 /* Extracts values of all "file" keys of the given list 160 161 Type: getFiles :: [ { file :: a } ] -> [a] 162 163 Example: 164 getFiles [ { file = "file1"; } { file = "file2"; } ] // => [ "file1" "file2" ] 165 getFiles [ ] // => [ ] 166 */ 167 getFiles = map (x: x.file); 168 169 # Generate documentation template from the list of option declaration like 170 # the set generated with filterOptionSets. 171 optionAttrSetToDocList = optionAttrSetToDocList' []; 172 173 optionAttrSetToDocList' = prefix: options: 174 concatMap (opt: 175 let 176 docOption = rec { 177 loc = opt.loc; 178 name = showOption opt.loc; 179 description = opt.description or (lib.warn "Option `${name}' has no description." "This option has no description."); 180 declarations = filter (x: x != unknownModule) opt.declarations; 181 internal = opt.internal or false; 182 visible = opt.visible or true; 183 readOnly = opt.readOnly or false; 184 type = opt.type.description or null; 185 } 186 // optionalAttrs (opt ? example) { example = scrubOptionValue opt.example; } 187 // optionalAttrs (opt ? default) { default = scrubOptionValue opt.default; } 188 // optionalAttrs (opt ? defaultText) { default = opt.defaultText; } 189 // optionalAttrs (opt ? relatedPackages && opt.relatedPackages != null) { inherit (opt) relatedPackages; }; 190 191 subOptions = 192 let ss = opt.type.getSubOptions opt.loc; 193 in if ss != {} then optionAttrSetToDocList' opt.loc ss else []; 194 in 195 [ docOption ] ++ optionals docOption.visible subOptions) (collect isOption options); 196 197 198 /* This function recursively removes all derivation attributes from 199 `x` except for the `name` attribute. 200 201 This is to make the generation of `options.xml` much more 202 efficient: the XML representation of derivations is very large 203 (on the order of megabytes) and is not actually used by the 204 manual generator. 205 */ 206 scrubOptionValue = x: 207 if isDerivation x then 208 { type = "derivation"; drvPath = x.name; outPath = x.name; name = x.name; } 209 else if isList x then map scrubOptionValue x 210 else if isAttrs x then mapAttrs (n: v: scrubOptionValue v) (removeAttrs x ["_args"]) 211 else x; 212 213 214 /* For use in the `example` option attribute. It causes the given 215 text to be included verbatim in documentation. This is necessary 216 for example values that are not simple values, e.g., functions. 217 */ 218 literalExample = text: { _type = "literalExample"; inherit text; }; 219 220 # Helper functions. 221 222 /* Convert an option, described as a list of the option parts in to a 223 safe, human readable version. 224 225 Example: 226 (showOption ["foo" "bar" "baz"]) == "foo.bar.baz" 227 (showOption ["foo" "bar.baz" "tux"]) == "foo.bar.baz.tux" 228 229 Placeholders will not be quoted as they are not actual values: 230 (showOption ["foo" "*" "bar"]) == "foo.*.bar" 231 (showOption ["foo" "<name>" "bar"]) == "foo.<name>.bar" 232 233 Unlike attributes, options can also start with numbers: 234 (showOption ["windowManager" "2bwm" "enable"]) == "windowManager.2bwm.enable" 235 */ 236 showOption = parts: let 237 escapeOptionPart = part: 238 let 239 escaped = lib.strings.escapeNixString part; 240 in if escaped == "\"${part}\"" 241 then part 242 else escaped; 243 in (concatStringsSep ".") (map escapeOptionPart parts); 244 showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files); 245 246 showDefs = defs: concatMapStrings (def: 247 let 248 # Pretty print the value for display, if successful 249 prettyEval = builtins.tryEval (lib.generators.toPretty {} def.value); 250 # Split it into its lines 251 lines = filter (v: ! isList v) (builtins.split "\n" prettyEval.value); 252 # Only display the first 5 lines, and indent them for better visibility 253 value = concatStringsSep "\n " (take 5 lines ++ optional (length lines > 5) "..."); 254 result = 255 # Don't print any value if evaluating the value strictly fails 256 if ! prettyEval.success then "" 257 # Put it on a new line if it consists of multiple 258 else if length lines > 1 then ":\n " + value 259 else ": " + value; 260 in "\n- In `${def.file}'${result}" 261 ) defs; 262 263 unknownModule = "<unknown-file>"; 264 265}