# Nixpkgs/NixOS option handling. let lib = import ./default.nix; in with { inherit (builtins) head tail; }; with import ./trivial.nix; with import ./lists.nix; with import ./misc.nix; with import ./attrsets.nix; rec { hasType = x: isAttrs x && x ? _type; typeOf = x: if hasType x then x._type else ""; isOption = attrs: (typeOf attrs) == "option"; mkOption = attrs: attrs // { _type = "option"; # name (this is the name of the attributem it is automatically generated by the traversal) # default (value used when no definition exists) # example (documentation) # description (documentation) # type (option type, provide a default merge function and ensure type correctness) # merge (function used to merge definitions into one definition: [ /type/ ] -> /type/) # apply (convert the option value to ease the manipulation of the option result) # options (set of sub-options declarations & definitions) }; # Make the option declaration more user-friendly by adding default # settings and some verifications based on the declaration content (like # type correctness). addOptionMakeUp = {name, recurseInto}: decl: let init = { inherit name; merge = mergeDefaultOption; apply = lib.id; }; mergeFromType = opt: if decl ? type && decl.type ? merge then opt // { merge = decl.type.merge; } else opt; addDeclaration = opt: opt // decl; ensureMergeInputType = opt: if decl ? type then opt // { merge = list: if all decl.type.check list then opt.merge list else throw "One of the definitions has a bad type."; } else opt; ensureDefaultType = opt: if decl ? type && decl ? default then opt // { default = if decl.type.check decl.default then decl.default else throw "The default value has a bad type."; } else opt; handleOptionSets = opt: if decl ? type && decl.type.hasOptions then let optionConfig = opts: config: map (f: applyIfFunction f config) (decl.options ++ [opts]); in opt // { merge = list: decl.type.iter (path: opts: fixMergeFun (recurseInto path) (optionConfig opts) ) opt.name (opt.merge list); options = recurseInto (decl.type.docPath opt.name) decl.options; } else opt; in foldl (opt: f: f opt) init [ # default settings mergeFromType # user settings addDeclaration # override settings ensureMergeInputType ensureDefaultType handleOptionSets ]; # Merge a list of options containning different field. This is useful to # separate the merge & apply fields from the interface. mergeOptionDecls = opts: if opts == [] then {} else if tail opts == [] then let opt = head opts; in if opt ? options then opt // { options = toList opt.options; } else opt else fold (opt1: opt2: lib.addErrorContext "opt1 = ${lib.showVal opt1}\nopt2 = ${lib.showVal opt2}" ( # You cannot merge if two options have the same field. assert opt1 ? default -> ! opt2 ? default; assert opt1 ? example -> ! opt2 ? example; assert opt1 ? description -> ! opt2 ? description; assert opt1 ? merge -> ! opt2 ? merge; assert opt1 ? apply -> ! opt2 ? apply; assert opt1 ? type -> ! opt2 ? type; if opt1 ? options || opt2 ? options then opt1 // opt2 // { options = (toList (attrByPath ["options"] [] opt1)) ++ (toList (attrByPath ["options"] [] opt2)); } else opt1 // opt2 )) {} opts; # name (name of the type) # check (boolean function) # merge (default merge function) # iter (iterate on all elements contained in this type) # fold (fold all elements contained in this type) # hasOptions (boolean: whatever this option contains an option set) # path (path contatenated to the option name contained contained in the option set) isOptionType = attrs: (typeOf attrs) == "option-type"; mkOptionType = attrs@{ name , check ? (x: true) , merge ? mergeDefaultOption # Handle complex structure types. , iter ? (f: path: v: f path v) , fold ? (op: nul: v: op v nul) , docPath ? lib.id # If the type can contains option sets. , hasOptions ? false }: { _type = "option-type"; inherit name check merge iter fold docPath hasOptions; }; types = { inferred = mkOptionType { name = "inferred type"; }; enable = mkOptionType { name = "boolean"; check = builtins.isBool; merge = fold lib.or false; }; int = mkOptionType { name = "integer"; check = builtins.isInt; }; string = mkOptionType { name = "string"; check = x: builtins ? isString -> builtins.isString x; merge = lib.concatStrings; }; attrs = mkOptionType { name = "attribute set"; check = builtins.isAttrs; merge = fold lib.mergeAttrs {}; }; # derivation is a reserved keyword. package = mkOptionType { name = "derivation"; check = x: builtins.isAttrs x && x ? outPath; }; list = elemType: mkOptionType { name = "list of ${elemType.name}s"; check = value: isList value && all elemType.check value; merge = concatLists; iter = f: path: list: map (elemType.iter f (path + ".*")) list; fold = op: nul: list: lib.fold (e: l: elemType.fold op l e) nul list; docPath = path: elemType.docPath (path + ".*"); inherit (elemType) hasOptions; }; attrsOf = elemType: mkOptionType { name = "attribute set of ${elemType}s"; check = x: builtins.isAttrs x && fold (e: v: v && elemType.check e) true (lib.attrValues x); merge = fold lib.mergeAttrs {}; iter = f: path: set: lib.mapAttrs (name: elemType.iter f (path + "." + name)) set; fold = op: nul: set: fold (e: l: elemType.fold op l e) nul (lib.attrValues set); docPath = path: elemType.docPath (path + "."); inherit (elemType) hasOptions; }; uniq = elemType: mkOptionType { inherit (elemType) name check iter fold docPath hasOptions; merge = list: if tail list == [] then head list else throw "Multiple definitions. Only one is allowed for this option."; }; nullOr = elemType: mkOptionType { inherit (elemType) name merge docPath hasOptions; check = x: builtins.isNull x || elemType.check x; iter = f: path: v: if v == null then v else elemType.iter f path v; fold = op: nul: v: if v == null then nul else elemType.fold op nul v; }; optionSet = mkOptionType { name = "option set"; check = x: builtins.isAttrs x; hasOptions = true; }; }; # !!! This function will be removed because this can be done with the # multiple option declarations. addDefaultOptionValues = defs: opts: opts // builtins.listToAttrs (map (defName: { name = defName; value = let defValue = builtins.getAttr defName defs; optValue = builtins.getAttr defName opts; in if typeOf defValue == "option" then # `defValue' is an option. if hasAttr defName opts then builtins.getAttr defName opts else defValue.default else # `defValue' is an attribute set containing options. # So recurse. if hasAttr defName opts && isAttrs optValue then addDefaultOptionValues defValue optValue else addDefaultOptionValues defValue {}; } ) (attrNames defs)); mergeDefaultOption = list: if list != [] && tail list == [] then head list else if all builtins.isFunction list then x: mergeDefaultOption (map (f: f x) list) else if all isList list then concatLists list else if all isAttrs list then fold lib.mergeAttrs {} list else if all (x: true == x || false == x) list then fold lib.or false list else if all (x: x == toString x) list then lib.concatStrings list else throw "Cannot merge values."; mergeTypedOption = typeName: predicate: merge: list: if all predicate list then merge list else throw "Expect a ${typeName}."; mergeEnableOption = mergeTypedOption "boolean" (x: true == x || false == x) (fold lib.or false); mergeListOption = mergeTypedOption "list" isList concatLists; mergeStringOption = mergeTypedOption "string" (x: if builtins ? isString then builtins.isString x else x + "") lib.concatStrings; mergeOneOption = list: if list == [] then abort "This case should never happen." else if tail list != [] then throw "Multiple definitions. Only one is allowed for this option." else head list; # Handle the traversal of option sets. All sets inside 'opts' are zipped # and options declaration and definition are separated. If no option are # declared at a specific depth, then the function recurse into the values. # Other cases are handled by the optionHandler which contains two # functions that are used to defined your goal. # - export is a function which takes two arguments which are the option # and the list of values. # - notHandle is a function which takes the list of values are not handle # by this function. handleOptionSets = optionHandler@{export, notHandle, ...}: path: opts: if all isAttrs opts then lib.zip (attr: opts: let recurseInto = name: attrs: handleOptionSets optionHandler name attrs; # Compute the path to reach the attribute. name = if path == "" then attr else path + "." + attr; # Divide the definitions of the attribute "attr" between # declaration (isOption) and definitions (!isOption). test = partition isOption opts; decls = test.right; defs = test.wrong; # Make the option declaration more user-friendly by adding default # settings and some verifications based on the declaration content # (like type correctness). opt = addOptionMakeUp { inherit name recurseInto; } (mergeOptionDecls decls); # Return the list of option sets. optAttrs = map delayProperties defs; # return the list of option values. # Remove undefined values that are coming from evalIf. optValues = evalProperties defs; in if decls == [] then recurseInto name optAttrs else lib.addErrorContext "while evaluating the option ${name}:" ( export opt optValues ) ) opts else lib.addErrorContext "while evaluating ${path}:" (notHandle opts); # Merge option sets and produce a set of values which is the merging of # all options declare and defined. If no values are defined for an # option, then the default value is used otherwise it use the merge # function of each option to get the result. mergeOptionSets = handleOptionSets { export = opt: values: opt.apply ( if values == [] then if opt ? default then opt.default else throw "Not defined." else opt.merge values ); notHandle = opts: throw "Used without option declaration."; }; # Keep all option declarations. filterOptionSets = handleOptionSets { export = opt: values: opt; notHandle = opts: {}; }; # Unfortunately this can also be a string. isPath = x: !( builtins.isFunction x || builtins.isAttrs x || builtins.isInt x || builtins.isBool x || builtins.isList x ); applyIfFunction = f: arg: if builtins.isFunction f then f arg else f; moduleClosure = initModules: args: let moduleImport = path: (applyIfFunction (import path) args) // { # used by generic closure to avoid duplicated imports. key = path; }; in builtins.genericClosure { startSet = map moduleImport initModules; operator = m: map moduleImport (attrByPath ["imports"] [] m); }; selectDeclsAndDefs = modules: lib.concatMap (m: attrByPath ["options"] [] m ++ attrByPath ["config"] [] m ) modules; fixMergeFun = merge: optFun: lib.fix (config: merge ( # Delay top-level properties like mkIf map delayProperties ( # generate the list of option sets. optFun config ) ) ); fixMergeModules = merge: initModules: {...}@args: fixMergeFun (config: selectDeclsAndDefs ( moduleClosure initModules (args // { inherit config; }) ) ); fixModulesConfig = initModules: {...}@args: fixMergeModules (mergeOptionSets "") initModules args; fixOptionsConfig = initModules: {...}@args: fixMergeModules (filterOptionSets "") initModules args; # Evaluate a list of option sets that would be merged with the # function "merge" which expects two arguments. The attribute named # "require" is used to imports option declarations and bindings. # # * cfg[0-9]: configuration # * cfgSet[0-9]: configuration set # # merge: the function used to merge options sets. # pkgs: is the set of packages available. (nixpkgs) # opts: list of option sets or option set functions. # config: result of this evaluation. fixOptionSetsFun = merge: pkgs: opts: config: let # remove possible mkIf to access the require attribute. noImportConditions = cfgSet0: let cfgSet1 = delayProperties cfgSet0; in if cfgSet1 ? require then cfgSet1 // { require = rmProperties cfgSet1.require; } else cfgSet1; filenameHandler = cfg: if isPath cfg then import cfg else cfg; # call configuration "files" with one of the existing convention. argumentHandler = cfg: let # {..} cfg0 = cfg; # {pkgs, config, ...}: {..} cfg1 = cfg { inherit pkgs config merge; }; # pkgs: config: {..} cfg2 = cfg {} {}; in if builtins.isFunction cfg0 then if isAttrs cfg1 then cfg1 else builtins.trace "Use '{pkgs, config, ...}:'." cfg2 else cfg0; preprocess = cfg0: let cfg1 = filenameHandler cfg0; cfg2 = argumentHandler cfg1; cfg3 = noImportConditions cfg2; in cfg3; getRequire = x: toList (attrByPath ["require"] [] (preprocess x)); getRecursiveRequire = x: fold (cfg: l: if isPath cfg then [ cfg ] ++ l else [ cfg ] ++ (getRecursiveRequire cfg) ++ l ) [] (getRequire x); getRequireSets = x: filter (x: ! isPath x) (getRecursiveRequire x); getRequirePaths = x: filter isPath (getRecursiveRequire x); rmRequire = x: removeAttrs (preprocess x) ["require"]; inlineRequiredSets = cfgs: fold (cfg: l: [ cfg ] ++ (getRequireSets cfg) ++ l) [] cfgs; in merge "" ( map rmRequire ( inlineRequiredSets ((toList opts) ++ lib.uniqFlatten getRequirePaths [] [] (lib.concatMap getRequirePaths (toList opts))) ) ); fixOptionSets = merge: pkgs: opts: lib.fix (fixOptionSetsFun merge pkgs opts); # Generate documentation template from the list of option declaration like # the set generated with filterOptionSets. optionAttrSetToDocList = ignore: newOptionAttrSetToDocList; newOptionAttrSetToDocList = attrs: let options = collect isOption attrs; in fold (opt: rest: let docOption = { inherit (opt) name; description = if opt ? description then opt.description else throw "Option ${opt.name}: No description."; } // (if opt ? example then {inherit(opt) example;} else {}) // (if opt ? default then {inherit(opt) default;} else {}); subOptions = if opt ? options then newOptionAttrSetToDocList opt.options else []; in [ docOption ] ++ subOptions ++ rest ) [] options; /* Option Properties */ # Generalize the problem of delayable properties. Any property can be created # Tell that nothing is defined. When properties are evaluated, this type # is used to remove an entry. Thus if your property evaluation semantic # implies that you have to mute the content of an attribute, then your # property should produce this value. isNotdef = attrs: (typeOf attrs) == "notdef"; mkNotdef = {_type = "notdef";}; # General property type, it has a property attribute and a content # attribute. The property attribute refer to an attribute set which # contains a _type attribute and a list of functions which are used to # evaluate this property. The content attribute is used to stack property # on top of each other. # # The optional functions which may be contained in the property attribute # are: # - onDelay: run on a copied property. # - onGlobalDelay: run on all copied properties. # - onEval: run on an evaluated property. # - onGlobalEval: run on a list of property stack on top of their values. isProperty = attrs: (typeOf attrs) == "property"; mkProperty = p@{property, content, ...}: p // { _type = "property"; }; # Go throw the stack of properties and apply the function `op' on all # property and call the function `nul' on the final value which is not a # property. The stack is traversed in reversed order. The `op' function # should expect a property with a content which have been modified. # # Warning: The `op' function expects only one argument in order to avoid # calls to mkProperties as the argument is already a valid property which # contains the result of the folding inside the content attribute. foldProperty = op: nul: attrs: if isProperty attrs then op (attrs // { content = foldProperty op nul attrs.content; }) else nul attrs; # Simple function which can be used as the `op' argument of the # foldProperty function. Properties that you don't want to handle can be # ignored with the `id' function. `isSearched' is a function which should # check the type of a property and return a boolean value. `thenFun' and # `elseFun' are functions which behave as the `op' argument of the # foldProperty function. foldFilter = isSearched: thenFun: elseFun: attrs: if isSearched attrs.property then thenFun attrs else elseFun attrs; # Move properties from the current attribute set to the attribute # contained in this attribute set. This trigger property handlers called # `onDelay' and `onGlobalDelay'. delayProperties = attrs: let cleanAttrs = rmProperties attrs; in if isProperty attrs then lib.mapAttrs (a: v: lib.addErrorContext "while moving properties on the attribute `${a}'." ( triggerPropertiesGlobalDelay a ( triggerPropertiesDelay a ( copyProperties attrs v )))) cleanAttrs else attrs; # Call onDelay functions. triggerPropertiesDelay = name: attrs: let callOnDelay = p@{property, ...}: lib.addErrorContext "while calling a onDelay function." ( if property ? onDelay then property.onDelay name p else p ); in foldProperty callOnDelay id attrs; # Call onGlobalDelay functions. triggerPropertiesGlobalDelay = name: attrs: let globalDelayFuns = uniqListExt { getter = property: property._type; inputList = foldProperty (p@{property, content, ...}: if property ? onGlobalDelay then [ property ] ++ content else content ) (a: []) attrs; }; callOnGlobalDelay = property: content: lib.addErrorContext "while calling a onGlobalDelay function." ( property.onGlobalDelay name content ); in fold callOnGlobalDelay attrs globalDelayFuns; # Expect a list of values which may have properties and return the same # list of values where all properties have been evaluated and where all # ignored values are removed. This trigger property handlers called # `onEval' and `onGlobalEval'. evalProperties = valList: if valList != [] then filter (x: !isNotdef x) ( lib.addErrorContext "while evaluating properties an attribute." ( triggerPropertiesGlobalEval ( map triggerPropertiesEval valList ))) else valList; # Call onEval function triggerPropertiesEval = val: foldProperty (p@{property, ...}: lib.addErrorContext "while calling a onEval function." ( if property ? onEval then property.onEval p else p ) ) id val; # Call onGlobalEval function triggerPropertiesGlobalEval = valList: let globalEvalFuns = uniqListExt { getter = property: property._type; inputList = fold (attrs: list: foldProperty (p@{property, content, ...}: if property ? onGlobalEval then [ property ] ++ content else content ) (a: list) attrs ) [] valList; }; callOnGlobalEval = property: valList: lib.addErrorContext "while calling a onGlobalEval function." ( property.onGlobalEval valList ); in fold callOnGlobalEval valList globalEvalFuns; # Remove all properties on top of a value and return the value. rmProperties = foldProperty (p@{content, ...}: content) id; # Copy properties defined on a value on another value. copyProperties = attrs: newAttrs: foldProperty id (x: newAttrs) attrs; /* If. ThenElse. Always. */ # create "if" statement that can be delayed on sets until a "then-else" or # "always" set is reached. When an always set is reached the condition # is ignore. # Create a "If" property which only contains a condition. isIf = attrs: (typeOf attrs) == "if"; mkIf = condition: content: mkProperty { property = { _type = "if"; onGlobalDelay = onIfGlobalDelay; onEval = onIfEval; inherit condition; }; inherit content; }; # Create a "ThenElse" property which contains choices which can choosed by # the evaluation of an "If" statement. isThenElse = attrs: (typeOf attrs) == "then-else"; mkThenElse = attrs: assert attrs ? thenPart && attrs ? elsePart; mkProperty { property = { _type = "then-else"; onEval = val: throw "Missing mkIf statement."; inherit (attrs) thenPart elsePart; }; content = mkNotdef; }; # Create an "Always" property remove ignore all "If" statement. isAlways = attrs: (typeOf attrs) == "always"; mkAlways = value: mkProperty { property = { _type = "always"; onEval = p@{content, ...}: content; inherit value; }; content = mkNotdef; }; # Remove all "If" statement defined on a value. rmIf = foldProperty ( foldFilter isIf ({content, ...}: content) id ) id; # Evaluate the "If" statements when either "ThenElse" or "Always" # statement is encounter. Otherwise it remove multiple If statement and # replace them by one "If" staement where the condition is the list of all # conditions joined with a "and" operation. onIfGlobalDelay = name: content: let # extract if statements and non-if statements and repectively put them # in the attribute list and attrs. ifProps = foldProperty (foldFilter (p: isIf p || isThenElse p || isAlways p) # then, push the codition inside the list list (p@{property, content, ...}: { inherit (content) attrs; list = [property] ++ content.list; } ) # otherwise, add the propertie. (p@{property, content, ...}: { inherit (content) list; attrs = p // { content = content.attrs; }; } ) ) (attrs: { list = []; inherit attrs; }) content; # compute the list of if statements. evalIf = content: condition: list: if list == [] then mkIf condition content else let p = head list; in # evaluate the condition. if isThenElse p then if condition then copyProperties content p.thenPart else copyProperties content p.elsePart # ignore the condition. else if isAlways p then copyProperties content p.value # otherwise (isIf) else evalIf content (condition && p.condition) (tail list); in evalIf ifProps.attrs true ifProps.list; # Evaluate the condition of the "If" statement to either get the value or # to ignore the value. onIfEval = p@{property, content, ...}: if property.condition then content else mkNotdef; /* mkOverride */ # Create an "Override" statement which allow the user to define # prioprities between values. The default priority is 100 and the lowest # priorities are kept. The template argument must reproduce the same # attribute set hierachy to override leaves of the hierarchy. isOverride = attrs: (typeOf attrs) == "override"; mkOverride = priority: template: content: mkProperty { property = { _type = "override"; onDelay = onOverrideDelay; onGlobalEval = onOverrideGlobalEval; inherit priority template; }; inherit content; }; # Make the template traversal in function of the property traversal. If # the template define a non-empty attribute set, then the property is # copied only on all mentionned attributes inside the template. # Otherwise, the property is kept on all sub-attribute definitions. onOverrideDelay = name: p@{property, content, ...}: let inherit (property) template; in if isAttrs template && template != {} then if hasAttr name template then p // { property = p.property // { template = builtins.getAttr name template; }; } # Do not override the attribute \name\ else content # Override values defined inside the attribute \name\. else p; # Ignore all values which have a higher value of the priority number. onOverrideGlobalEval = valList: let defaultPrio = 100; inherit (builtins) lessThan; getPrioVal = foldProperty (foldFilter isOverride (p@{property, content, ...}: if lessThan content.priority property.priority then content else content // { inherit (property) priority; } ) (p@{property, content, ...}: content // { value = p // { content = content.value; }; } ) ) (value: { priority = defaultPrio; inherit value; }); prioValList = map getPrioVal valList; higherPrio = fold (x: y: if lessThan x.priority y then x.priority else y ) defaultPrio prioValList; in map (x: if x.priority == higherPrio then x.value else mkNotdef ) prioValList; }