diff --git a/pkgs/lib/default.nix b/pkgs/lib/default.nix index aec2fb0bcc0..2f916d3ddd7 100644 --- a/pkgs/lib/default.nix +++ b/pkgs/lib/default.nix @@ -23,4 +23,4 @@ in # !!! don't include everything at top-level; perhaps only the most # commonly used functions. // trivial // lists // strings // stringsWithDeps // attrsets // sources - // properties // options // types // meta // debug // misc + // properties // options // types // meta // debug // misc // modules diff --git a/pkgs/lib/misc.nix b/pkgs/lib/misc.nix index 60aecd5aa63..8fe29151976 100644 --- a/pkgs/lib/misc.nix +++ b/pkgs/lib/misc.nix @@ -200,14 +200,16 @@ rec { # Merge sets of attributes and use the function f to merge # attributes values. zip = f: sets: + zipWithNames (concatMap builtins.attrNames sets) f sets; + + zipWithNames = names: f: sets: builtins.listToAttrs (map (name: { inherit name; value = f name (map (__getAttr name) (filter (__hasAttr name) sets)); - }) (concatMap builtins.attrNames sets)); - + }) names); lazyGenericClosure = {startSet, operator}: let diff --git a/pkgs/lib/modules.nix b/pkgs/lib/modules.nix index 61cf7624bc5..09f096eba18 100644 --- a/pkgs/lib/modules.nix +++ b/pkgs/lib/modules.nix @@ -7,6 +7,7 @@ with import ./trivial.nix; with import ./lists.nix; with import ./misc.nix; with import ./attrsets.nix; +with import ./options.nix; with import ./properties.nix; rec { @@ -32,6 +33,10 @@ rec { else f; + isModule = m: + (m ? config && isAttrs m.config && ! isOption m.config) + || (m ? options && isAttrs m.options && ! isOption m.options); + # Convert module to a set which has imports / options and config # attributes. unifyModuleSyntax = m: @@ -48,7 +53,7 @@ rec { getConfig = m: removeAttrs (delayProperties m) ["require"]; in - if m ? config || m ? options then + if isModule m then m else { @@ -88,4 +93,165 @@ rec { [ m ] ) modules; + + moduleApply = funs: module: + lib.mapAttrs (name: value: + if builtins.hasAttr name funs then + let fun = lib.getAttr name funs; in + fun value + else + value + ) module; + + delayModule = module: + moduleApply { config = delayProperties; } module; + + selectModule = name: m: + { inherit (m) key; + } // ( + if m ? options && builtins.hasAttr name m.options then + { options = lib.getAttr name m.options; } + else {} + ) // ( + if m ? config && builtins.hasAttr name m.config then + { config = lib.getAttr name m.config; } + else {} + ); + + filterModules = name: modules: + filter (m: m ? config || m ? options) ( + map (selectModule name) modules + ); + + modulesNames = modules: + lib.concatMap (m: [] + ++ optionals (m ? options) (lib.attrNames m.options) + ++ optionals (m ? config) (lib.attrNames m.config) + ) modules; + + moduleZip = funs: modules: + lib.mapAttrs (name: fun: + fun ( + map (lib.getAttr name) ( + filter (builtins.hasAttr name) modules + ) + ) + ) funs; + + moduleMerge = path: modules: + let modules_ = modules; in + let + addName = name: + if path == "" then name else path + "." + name; + + modules = map delayModule modules_; + + modulesOf = name: filterModules name modules; + declarationsOf = name: filter (m: m ? options) (modulesOf name); + definitionsOf = name: filter (m: m ? config ) (modulesOf name); + + recurseInto = name: modules: + moduleMerge (addName name) (modulesOf name); + + recurseForOption = name: modules: + moduleMerge name ( + map unifyModuleSyntax modules + ); + + errorSource = modules: + "The error may comes from the following files:\n" + ( + lib.concatStringsSep "\n" ( + map (m: + if m ? key then toString m.key else "" + ) modules + ) + ); + + eol = "\n"; + + errDefinedWithoutDeclaration = name: + let + badModules = + filter (m: ! isAttrs m.config) + (definitionsOf name); + in + "${eol + }Option '${addName name}' defined without option declaration.${eol + }${errorSource badModules}${eol + }"; + + endRecursion = { options = {}; config = {}; }; + + in if modules == [] then endRecursion else + + lib.fix (result: + moduleZip { + options = lib.zip (name: values: + if any isOption values then + addOptionMakeUp + { name = addName name; recurseInto = recurseForOption; } + (mergeOptionDecls values) + else if all isAttrs values then + (recurseInto name modules).options + else + throw "${eol + }Unexpected type where option declarations are expected.${eol + }${errorSource (declarationsOf name)}${eol + }" + ); + + config = lib.zipWithNames (modulesNames modules) (name: values: + let + hasOpt = builtins.hasAttr name result.options; + opt = lib.getAttr name result.options; + + in if hasOpt && isOption opt then + let defs = evalProperties values; in + lib.addErrorContext "${eol + }while evaluating the option '${addName name}'.${eol + }${errorSource (modulesOf name)}${eol + }" ( + opt.apply ( + if defs == [] then + if opt ? default then opt.default + else throw "Not defined." + else opt.merge defs + ) + ) + + else if hasOpt && lib.attrNames opt == [] then + throw (errDefinedWithoutDeclaration name) + + else if any (v: isOption (rmProperties v)) values then + let + badModules = + filter (m: isOption m.config) + (definitionsOf name); + in + throw "${eol + }Option ${addName name} is defined in the configuration section.${eol + }${errorSource badModules}${eol + }" + + else if all isAttrs values then + (recurseInto name modules).config + else + throw (errDefinedWithoutDeclaration name) + ); + + } modules + ); + + fixMergeModules = initModules: {...}@args: + lib.fix (result: + # This trick avoid an infinite loop because names of attribute are + # know and it is not require to evaluate the result of moduleMerge to + # know which attribute are present as argument. + let module = { inherit (result) options config; }; in + + moduleMerge "" ( + moduleClosure initModules (args // module) + ) + ); + } diff --git a/pkgs/lib/options.nix b/pkgs/lib/options.nix index 34492b289df..37d092ef3c5 100644 --- a/pkgs/lib/options.nix +++ b/pkgs/lib/options.nix @@ -8,7 +8,6 @@ with import ./lists.nix; with import ./misc.nix; with import ./attrsets.nix; with import ./properties.nix; -with import ./modules.nix; rec { @@ -69,22 +68,43 @@ rec { } else opt; + convertOptionsToModules = opt: + if opt ? options then + opt // { + options = map (decl: + let module = lib.applyIfFunction decl {}; in + if lib.isModule module then + decl + else + arg: { options = lib.applyIfFunction decl arg; } + ) opt.options; + } + else + opt; + handleOptionSets = opt: if decl ? type && decl.type.hasOptions then let + optionConfig = opts: config: - map (f: applyIfFunction f config) - (decl.options ++ [opts]); + map (f: lib.applyIfFunction f config) + (opt.options ++ toList opts); in opt // { merge = list: decl.type.iter (path: opts: - lib.fix (fixableMergeFun (recurseInto path) (optionConfig opts)) + (lib.fix + (fixableMergeFun (recurseInto path) (optionConfig opts)) + ).config ) opt.name (opt.merge list); - options = recurseInto (decl.type.docPath opt.name) decl.options; + options = + let path = decl.type.docPath opt.name; in + (lib.fix + (fixableMergeFun (recurseInto path) (optionConfig [])) + ).options; } else opt; @@ -99,6 +119,7 @@ rec { # override settings ensureMergeInputType ensureDefaultType + convertOptionsToModules handleOptionSets ]; @@ -186,121 +207,33 @@ rec { 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 (x: isOption (rmProperties x)) opts; - decls = map rmProperties 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: {}; - }; - - fixableMergeFun = merge: f: config: merge ( - # remove require because this is not an option. - map (m: removeAttrs m ["require"]) ( - # Delay top-level properties like mkIf - map delayProperties ( - # generate the list of option sets. - f config - ) - ) + # generate the list of option sets. + f config ); fixableMergeModules = merge: initModules: {...}@args: config: fixableMergeFun merge (config: - # filter the list of option sets. - selectDeclsAndDefs ( - # generate the list of modules from a closure of imports/require - # attribtues. - moduleClosure initModules (args // { inherit config; }) - ) + lib.moduleClosure initModules (args // { inherit config; }) ) config; fixableDefinitionsOf = initModules: {...}@args: - fixableMergeModules (mergeOptionSets "") initModules args; + fixableMergeModules (modules: (lib.moduleMerge "" modules).config) initModules args; fixableDeclarationsOf = initModules: {...}@args: - fixableMergeModules (filterOptionSets "") initModules args; + fixableMergeModules (modules: (lib.moduleMerge "" modules).options) initModules args; definitionsOf = initModules: {...}@args: - lib.fix (fixableDefinitionsOf initModules args); + (lib.fix (module: + fixableMergeModules (lib.moduleMerge "") initModules args module.config + )).config; declarationsOf = initModules: {...}@args: - lib.fix (fixableDeclarationsOf initModules args); - - - fixMergeModules = merge: initModules: {...}@args: - lib.fix (fixableMergeModules merge initModules args); - - - # old interface. - fixOptionSetsFun = merge: {...}@args: initModules: config: - fixableMergeModules (merge "") initModules args config; - - fixOptionSets = merge: args: initModules: - fixMergeModules (merge "") initModules args; + (lib.fix (module: + fixableMergeModules (lib.moduleMerge "") initModules args module.config + )).options; # Generate documentation template from the list of option declaration like