diff --git a/pkgs/lib/options.nix b/pkgs/lib/options.nix index 6d8bf4633de4..cde0b6df34cc 100644 --- a/pkgs/lib/options.nix +++ b/pkgs/lib/options.nix @@ -3,7 +3,9 @@ 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 { @@ -11,7 +13,8 @@ rec { mkOption = attrs: attrs // {_type = "option";}; - typeOf = x: if (__isAttrs x && x ? _type) then x._type else ""; + hasType = x: __isAttrs x && x ? _type; + typeOf = x: if hasType x then x._type else ""; isOption = attrs: (typeOf attrs) == "option"; @@ -97,11 +100,11 @@ rec { } // (head decls); # Return the list of option sets. - optAttrs = map delayIf defs; + optAttrs = map delayProperties defs; # return the list of option values. # Remove undefined values that are coming from evalIf. - optValues = filter (x: !isNotdef x) (map evalIf defs); + optValues = evalProperties defs; in if decls == [] then handleOptionSets optionHandler name optAttrs else lib.addErrorContext "while evaluating the option ${name}:" ( @@ -150,9 +153,9 @@ rec { let # remove possible mkIf to access the require attribute. noImportConditions = cfgSet0: - let cfgSet1 = delayIf cfgSet0; in + let cfgSet1 = delayProperties cfgSet0; in if cfgSet1 ? require then - cfgSet1 // { require = rmIf cfgSet1.require; } + cfgSet1 // { require = rmProperties cfgSet1.require; } else cfgSet1; @@ -176,12 +179,39 @@ rec { cfg2 = noImportConditions cfg1; in cfg2; - getRequire = x: toList (getAttr ["require"] [] (preprocess x)); + getRequire = x: + toList (getAttr ["require"] [] (preprocess x)); + getCleanRequire = x: map rmProperties (getRequire x); rmRequire = x: removeAttrs (preprocess x) ["require"]; + + duplicateIncludeProperties = list: + # iterate on all configurations + fold (cfg: l: + # iterate on all imported configuration from cfg + fold (include: l: + # clean up the included cfg to get the same result + let includedCfg = rmProperties include; in + # if the include has properties + if include != includedCfg then + # iterate on all configurations + map (cfg: + # if the imported configuration is seen + if (rmProperties cfg) == includedCfg then + # copy the properties from the import to the configuration. + delayProperties (copyProperties include cfg) + else + cfg + ) l + else + l + ) l (getRequire cfg) + ) list list; in merge "" ( map rmRequire ( - lib.uniqFlatten getRequire [] [] (toList opts) + duplicateIncludeProperties ( + lib.uniqFlatten getCleanRequire [] [] (toList opts) + ) ) ); @@ -203,65 +233,351 @@ rec { (l + (if l=="" then "" else ".") + s) (builtins.getAttr s attrs))) (builtins.attrNames attrs))); - + /* 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 cleanAttrs != 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. */ - # !!! cleanup needed # create "if" statement that can be dealyed 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: thenelse: - if isIf thenelse then - mkIf (condition && thenelse.condition) thenelse.thenelse - else { + mkIf = condition: content: mkProperty { + property = { _type = "if"; - inherit condition thenelse; + onGlobalDelay = onIfGlobalDelay; + onEval = onIfEval; + inherit condition; }; + inherit content; + }; - - isNotdef = attrs: (typeOf attrs) == "notdef"; - mkNotdef = {_type = "notdef";}; - - + # 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; - attrs // { _type = "then-else"; }; - + 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: { inherit value; _type = "always"; }; + mkAlways = value: + mkProperty { + property = { + _type = "always"; + onEval = p@{content, ...}: content; + inherit value; + }; + content = mkNotdef; + }; - pushIf = f: attrs: - if isIf attrs then pushIf f ( - let val = attrs.thenelse; in - # evaluate the condition. - if isThenElse val then - if attrs.condition then - val.thenPart + # 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 - val.elsePart - # ignore the condition. - else if isAlways val then - val.value - # otherwise - else - f attrs.condition val) + let p = head list; in + + # evaluate the condition. + if isThenElse p then + if condition then + foldProperty (a: p.thenPart) id content + else + foldProperty (a: p.elsePart) id content + # ignore the condition. + else if isAlways p then + foldProperty (a: p.value) id content + # 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 - attrs; + mkNotdef; - # take care otherwise you will have to handle this by hand. - rmIf = pushIf (condition: val: val); + /* mkOverride */ - evalIf = pushIf (condition: val: - if condition then val else mkNotdef - ); + # 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; + }; - delayIf = pushIf (condition: val: - # rewrite the condition on sub-attributes. - lib.mapAttrs (name: mkIf condition) val - ); + # 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 builtins.isAttrs template && template != {} then + if builtins.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; } \ No newline at end of file