From e14de56613fc8e42fb6249031efe9e7abbb65286 Mon Sep 17 00:00:00 2001 From: Eric Sagnes Date: Wed, 7 Sep 2016 10:03:32 +0900 Subject: [PATCH] module system: extensible option types --- lib/modules.nix | 32 ++-- lib/options.nix | 2 +- lib/types.nix | 142 ++++++++++++++---- .../development/option-declarations.xml | 88 +++++++++++ nixos/doc/manual/development/option-types.xml | 116 ++++++++++---- nixos/doc/manual/release-notes/rl-1703.xml | 5 +- nixos/modules/installer/tools/nixos-option.sh | 2 +- 7 files changed, 317 insertions(+), 70 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 8db17c605799..e66d6a6926cb 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -231,12 +231,20 @@ rec { correspond to the definition of 'loc' in 'opt.file'. */ mergeOptionDecls = loc: opts: foldl' (res: opt: - if opt.options ? default && res ? default || - opt.options ? example && res ? example || - opt.options ? description && res ? description || - opt.options ? apply && res ? apply || - # Accept to merge options which have identical types. - opt.options ? type && res ? type && opt.options.type.name != res.type.name + let t = res.type; + t' = opt.options.type; + mergedType = t.typeMerge t'.functor; + typesMergeable = mergedType != null; + typeSet = if (bothHave "type") && typesMergeable + then { type = mergedType; } + else {}; + bothHave = k: opt.options ? ${k} && res ? ${k}; + in + if bothHave "default" || + bothHave "example" || + bothHave "description" || + bothHave "apply" || + (bothHave "type" && (! typesMergeable)) then throw "The option `${showOption loc}' in `${opt.file}' is already declared in ${showFiles res.declarations}." else @@ -258,7 +266,7 @@ rec { in opt.options // res // { declarations = res.declarations ++ [opt.file]; options = submodules; - } + } // typeSet ) { inherit loc; declarations = []; options = []; } opts; /* Merge all the definitions of an option to produce the final @@ -422,12 +430,14 @@ rec { options = opt.options or (throw "Option `${showOption loc'}' has type optionSet but has no option attribute, in ${showFiles opt.declarations}."); f = tp: + let optionSetIn = type: (tp.name == type) && (tp.functor.wrapped.name == "optionSet"); + in if tp.name == "option set" || tp.name == "submodule" then throw "The option ${showOption loc} uses submodules without a wrapping type, in ${showFiles opt.declarations}." - else if tp.name == "attribute set of option sets" then types.attrsOf (types.submodule options) - else if tp.name == "list or attribute set of option sets" then types.loaOf (types.submodule options) - else if tp.name == "list of option sets" then types.listOf (types.submodule options) - else if tp.name == "null or option set" then types.nullOr (types.submodule options) + else if optionSetIn "attrsOf" then types.attrsOf (types.submodule options) + else if optionSetIn "loaOf" then types.loaOf (types.submodule options) + else if optionSetIn "listOf" then types.listOf (types.submodule options) + else if optionSetIn "nullOr" then types.nullOr (types.submodule options) else tp; in if opt.type.getSubModules or null == null diff --git a/lib/options.nix b/lib/options.nix index 444ec37e6eaf..2092b65bbc3a 100644 --- a/lib/options.nix +++ b/lib/options.nix @@ -92,7 +92,7 @@ rec { internal = opt.internal or false; visible = opt.visible or true; readOnly = opt.readOnly or false; - type = opt.type.name or null; + type = opt.type.description or null; } // (if opt ? example then { example = scrubOptionValue opt.example; } else {}) // (if opt ? default then { default = scrubOptionValue opt.default; } else {}) diff --git a/lib/types.nix b/lib/types.nix index 991fa0e5c291..26523f59f256 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -17,10 +17,43 @@ rec { }; + # Default type merging function + # takes two type functors and return the merged type + defaultTypeMerge = f: f': + let wrapped = f.wrapped.typeMerge f'.wrapped.functor; + payload = f.binOp f.payload f'.payload; + in + # cannot merge different types + if f.name != f'.name + then null + # simple types + else if (f.wrapped == null && f'.wrapped == null) + && (f.payload == null && f'.payload == null) + then f.type + # composed types + else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null) + then f.type wrapped + # value types + else if (f.payload != null && f'.payload != null) && (payload != null) + then f.type payload + else null; + + # Default type functor + defaultFunctor = name: { + inherit name; + type = types."${name}" or null; + wrapped = null; + payload = null; + binOp = a: b: null; + }; + isOptionType = isType "option-type"; mkOptionType = - { # Human-readable representation of the type. + { # Human-readable representation of the type, should be equivalent to + # the type function name. name + , # Description of the type, defined recursively by embedding the the wrapped type if any. + description ? null , # Function applied to each definition that should return true if # its type-correct, false otherwise. check ? (x: true) @@ -36,12 +69,26 @@ rec { getSubOptions ? prefix: {} , # List of modules if any, or null if none. getSubModules ? null - , # Function for building the same option type with a different list of + , # Function for building the same option type with a different list of # modules. substSubModules ? m: null + , # Function that merge type declarations. + # internal, takes a functor as argument and returns the merged type. + # returning null means the type is not mergeable + typeMerge ? defaultTypeMerge functor + , # The type functor. + # internal, representation of the type as an attribute set. + # name: name of the type + # type: type function. + # wrapped: the type wrapped in case of compound types. + # payload: values of the type, two payloads of the same type must be + # combinable with the binOp binary operation. + # binOp: binary operation that merge two payloads of the same type. + functor ? defaultFunctor name }: { _type = "option-type"; - inherit name check merge getSubOptions getSubModules substSubModules; + inherit name check merge getSubOptions getSubModules substSubModules typeMerge functor; + description = if description == null then name else description; }; @@ -52,29 +99,39 @@ rec { }; bool = mkOptionType { - name = "boolean"; + name = "bool"; + description = "boolean"; check = isBool; merge = mergeEqualOption; }; - int = mkOptionType { - name = "integer"; + int = mkOptionType rec { + name = "int"; + description = "integer"; check = isInt; merge = mergeOneOption; }; str = mkOptionType { - name = "string"; + name = "str"; + description = "string"; check = isString; merge = mergeOneOption; }; # Merge multiple definitions by concatenating them (with the given # separator between the values). - separatedString = sep: mkOptionType { - name = "string"; + separatedString = sep: mkOptionType rec { + name = "separatedString"; + description = "string"; check = isString; merge = loc: defs: concatStringsSep sep (getValues defs); + functor = (defaultFunctor name) // { + payload = sep; + binOp = sepLhs: sepRhs: + if sepLhs == sepRhs then sepLhs + else null; + }; }; lines = separatedString "\n"; @@ -86,7 +143,8 @@ rec { string = separatedString ""; attrs = mkOptionType { - name = "attribute set"; + name = "attrs"; + description = "attribute set"; check = isAttrs; merge = loc: foldl' (res: def: mergeAttrs res def.value) {}; }; @@ -114,8 +172,9 @@ rec { # drop this in the future: list = builtins.trace "`types.list' is deprecated; use `types.listOf' instead" types.listOf; - listOf = elemType: mkOptionType { - name = "list of ${elemType.name}s"; + listOf = elemType: mkOptionType rec { + name = "listOf"; + description = "list of ${elemType.description}s"; check = isList; merge = loc: defs: map (x: x.value) (filter (x: x ? value) (concatLists (imap (n: def: @@ -132,10 +191,12 @@ rec { getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); getSubModules = elemType.getSubModules; substSubModules = m: listOf (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; }; - attrsOf = elemType: mkOptionType { - name = "attribute set of ${elemType.name}s"; + attrsOf = elemType: mkOptionType rec { + name = "attrsOf"; + description = "attribute set of ${elemType.description}s"; check = isAttrs; merge = loc: defs: mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: @@ -147,6 +208,7 @@ rec { getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); getSubModules = elemType.getSubModules; substSubModules = m: attrsOf (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; }; # List or attribute set of ... @@ -165,18 +227,21 @@ rec { def; listOnly = listOf elemType; attrOnly = attrsOf elemType; - in mkOptionType { - name = "list or attribute set of ${elemType.name}s"; + in mkOptionType rec { + name = "loaOf"; + description = "list or attribute set of ${elemType.description}s"; check = x: isList x || isAttrs x; merge = loc: defs: attrOnly.merge loc (imap convertIfList defs); getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); getSubModules = elemType.getSubModules; substSubModules = m: loaOf (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; }; # List or element of ... - loeOf = elemType: mkOptionType { - name = "element or list of ${elemType.name}s"; + loeOf = elemType: mkOptionType rec { + name = "loeOf"; + description = "element or list of ${elemType.description}s"; check = x: isList x || elemType.check x; merge = loc: defs: let @@ -189,18 +254,22 @@ rec { else if !isString res then throw "The option `${showOption loc}' does not have a string value, in ${showFiles (getFiles defs)}." else res; + functor = (defaultFunctor name) // { wrapped = elemType; }; }; - uniq = elemType: mkOptionType { - inherit (elemType) name check; + uniq = elemType: mkOptionType rec { + name = "uniq"; + inherit (elemType) description check; merge = mergeOneOption; getSubOptions = elemType.getSubOptions; getSubModules = elemType.getSubModules; substSubModules = m: uniq (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; }; - nullOr = elemType: mkOptionType { - name = "null or ${elemType.name}"; + nullOr = elemType: mkOptionType rec { + name = "nullOr"; + description = "null or ${elemType.description}"; check = x: x == null || elemType.check x; merge = loc: defs: let nrNulls = count (def: def.value == null) defs; in @@ -211,6 +280,7 @@ rec { getSubOptions = elemType.getSubOptions; getSubModules = elemType.getSubModules; substSubModules = m: nullOr (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; }; submodule = opts: @@ -236,6 +306,12 @@ rec { args = { name = ""; }; }).options; getSubModules = opts'; substSubModules = m: submodule m; + functor = (defaultFunctor name) // { + # Merging of submodules is done as part of mergeOptionDecls, as we have to annotate + # each submodule with its location. + payload = []; + binOp = lhs: rhs: []; + }; }; enum = values: @@ -245,23 +321,35 @@ rec { else if builtins.isInt v then builtins.toString v else ''<${builtins.typeOf v}>''; in - mkOptionType { - name = "one of ${concatMapStringsSep ", " show values}"; + mkOptionType rec { + name = "enum"; + description = "one of ${concatMapStringsSep ", " show values}"; check = flip elem values; merge = mergeOneOption; + functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); }; }; - either = t1: t2: mkOptionType { - name = "${t1.name} or ${t2.name}"; + either = t1: t2: mkOptionType rec { + name = "either"; + description = "${t1.description} or ${t2.description}"; check = x: t1.check x || t2.check x; merge = mergeOneOption; + typeMerge = f': + let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor; + mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor; + in + if (name == f'.name) && (mt1 != null) && (mt2 != null) + then functor.type mt1 mt2 + else null; + functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; }; # Obsolete alternative to configOf. It takes its option # declarations from the ‘options’ attribute of containing option # declaration. optionSet = mkOptionType { - name = builtins.trace "types.optionSet is deprecated; use types.submodule instead" "option set"; + name = builtins.trace "types.optionSet is deprecated; use types.submodule instead" "optionSet"; + description = "option set"; }; # Augment the given type with an additional type check function. diff --git a/nixos/doc/manual/development/option-declarations.xml b/nixos/doc/manual/development/option-declarations.xml index 7be5e9d51d46..ce432a7fa6ca 100644 --- a/nixos/doc/manual/development/option-declarations.xml +++ b/nixos/doc/manual/development/option-declarations.xml @@ -65,4 +65,92 @@ options = { +
Extensible Option + Types + + Extensible option types is a feature that allow to extend certain types + declaration through multiple module files. + This feature only work with a restricted set of types, namely + enum and submodules and any composed + forms of them. + + Extensible option types can be used for enum options + that affects multiple modules, or as an alternative to related + enable options. + + As an example, we will take the case of display managers. There is a + central display manager module for generic display manager options and a + module file per display manager backend (slim, kdm, gdm ...). + + + There are two approach to this module structure: + + + Managing the display managers independently by adding an + enable option to every display manager module backend. (NixOS) + + Managing the display managers in the central module by + adding an option to select which display manager backend to use. + + + + + Both approachs have problems. + + Making backends independent can quickly become hard to manage. For + display managers, there can be only one enabled at a time, but the type + system can not enforce this restriction as there is no relation between + each backend enable option. As a result, this restriction + has to be done explicitely by adding assertions in each display manager + backend module. + + On the other hand, managing the display managers backends in the + central module will require to change the central module option every time + a new backend is added or removed. + + By using extensible option types, it is possible to create a placeholder + option in the central module (), and to extend it in each backend module (, ). + + As a result, displayManager.enable option values can + be added without changing the main service module file and the type system + automatically enforce that there can only be a single display manager + enabled. + +Extensible type + placeholder in the service module + +services.xserver.displayManager.enable = mkOption { + description = "Display manager to use"; + type = with types; nullOr (enum [ ]); +}; + +Extending + <literal>services.xserver.displayManager.enable</literal> in the + <literal>slim</literal> module + +services.xserver.displayManager.enable = mkOption { + type = with types; nullOr (enum [ "slim" ]); +}; + +Extending + <literal>services.foo.backend</literal> in the <literal>kdm</literal> + module + +services.xserver.displayManager.enable = mkOption { + type = with types; nullOr (enum [ "kdm" ]); +}; + +The placeholder declaration is a standard mkOption + declaration, but it is important that extensible option declarations only use + the type argument. + +Extensible option types work with any of the composed variants of + enum such as + with types; nullOr (enum [ "foo" "bar" ]) + or with types; listOf (enum [ "foo" "bar" ]). + +
diff --git a/nixos/doc/manual/development/option-types.xml b/nixos/doc/manual/development/option-types.xml index 9ef7bb30a576..8e6ac53ad480 100644 --- a/nixos/doc/manual/development/option-types.xml +++ b/nixos/doc/manual/development/option-types.xml @@ -62,23 +62,45 @@ A string. Multiple definitions are concatenated with a collon ":". - - types.separatedString - sep - A string with a custom separator - sep, e.g. types.separatedString - "|". - +
Value Types + + Value types are type that take a value parameter. The only value type + in the library is enum. + + + + types.enum l + One element of the list l, e.g. + types.enum [ "left" "right" ]. Multiple definitions + cannot be merged. + + + types.separatedString + sep + A string with a custom separator + sep, e.g. types.separatedString + "|". + + + types.submodule o + A set of sub options o. + o can be an attribute set or a function + returning an attribute set. Submodules are used in composed types to + create modular options. Submodule are detailed in . + + +
+
Composed Types - Composed types allow to create complex types by taking another type(s) - or value(s) as parameter(s). - It is possible to compose types multiple times, e.g. with types; - nullOr (enum [ "left" "right" ]). + Composed types are types that take a type as parameter. listOf + int and either int str are examples of + composed types. @@ -111,12 +133,6 @@ merged. It is used to ensure option definitions are declared only once. - - types.enum l - One element of the list l, e.g. - types.enum [ "left" "right" ]. Multiple definitions - cannot be merged - types.either t1 t2 @@ -125,14 +141,6 @@ str. Multiple definitions cannot be merged. - - types.submodule o - A set of sub options o. - o can be an attribute set or a function - returning an attribute set. Submodules are used in composed types to - create modular options. Submodule are detailed in . -
@@ -191,7 +199,6 @@ options.mod = mkOption { type = with types; listOf (submodule modOptions); }; -
Composed with <literal>listOf</literal> When composed with listOf, submodule allows multiple @@ -317,9 +324,13 @@ code before creating a new type. name - A string representation of the type function name, name - usually changes accordingly parameters passed to - types. + A string representation of the type function + name. + + + definition + Description of the type used in documentation. Give + information of the type and any of its arguments. check @@ -382,6 +393,53 @@ code before creating a new type. type parameter, this function should be defined as m: composedType (elemType.substSubModules m). + + typeMerge + A function to merge multiple type declarations. Takes the + type to merge functor as parameter. A + null return value means that type cannot be + merged. + + + f + The type to merge + functor. + + + Note: There is a generic defaultTypeMerge that + work with most of value and composed types. + + + + functor + An attribute set representing the type. It is used for type + operations and has the following keys: + + + type + The type function. + + + wrapped + Holds the type parameter for composed types. + + + + payload + Holds the value parameter for value types. + The types that have a payload are the + enum, separatedString and + submodule types. + + + binOp + A binary operation that can merge the payloads of two + same types. Defined as a function that take two payloads as + parameters and return the payloads merged. + + + +
diff --git a/nixos/doc/manual/release-notes/rl-1703.xml b/nixos/doc/manual/release-notes/rl-1703.xml index efff8b895a1a..743f3dce2302 100644 --- a/nixos/doc/manual/release-notes/rl-1703.xml +++ b/nixos/doc/manual/release-notes/rl-1703.xml @@ -75,7 +75,10 @@ following incompatible changes: - + Module type system have a new extensible option types feature that + allow to extend certain types, such as enum, through multiple option + declarations of the same option across multiple modules. + diff --git a/nixos/modules/installer/tools/nixos-option.sh b/nixos/modules/installer/tools/nixos-option.sh index 17c17d05e288..27eacda48a87 100644 --- a/nixos/modules/installer/tools/nixos-option.sh +++ b/nixos/modules/installer/tools/nixos-option.sh @@ -256,7 +256,7 @@ if isOption opt then // optionalAttrs (opt ? default) { inherit (opt) default; } // optionalAttrs (opt ? example) { inherit (opt) example; } // optionalAttrs (opt ? description) { inherit (opt) description; } - // optionalAttrs (opt ? type) { typename = opt.type.name; } + // optionalAttrs (opt ? type) { typename = opt.type.description; } // optionalAttrs (opt ? options) { inherit (opt) options; } // { # to disambiguate the xml output.