query
On this page

mergeModules'

lib.modules.mergeModules'

Docs pulled from | This Revision | 24 minutes ago


Contribute
Enhance the ecosystem with your expertise! Contribute to fill the gaps in documentation. Your input can make a difference.

Noogle detected

Aliases

Implementation

The following is the current implementation of this function.

prefix: modules: configs:
    let
      # an attrset 'name' => list of submodules that declare ‘name’.
      declsByName = zipAttrs (
        map (
          module:
          let
            subtree = module.options;
          in
          if !(isAttrs subtree) then
            throw ''
              An option declaration for `${concatStringsSep "." prefix}' has type
              `${typeOf subtree}' rather than an attribute set.
              Did you mean to define this outside of `options'?
            ''
          else
            mapAttrs (n: option: {
              inherit (module) _file;
              pos = unsafeGetAttrPos n subtree;
              options = option;
            }) subtree
        ) modules
      );

      # The root of any module definition must be an attrset.
      checkedConfigs =
        assert all (
          c:
          # TODO: I have my doubts that this error would occur when option definitions are not matched.
          #       The implementation of this check used to be tied to a superficially similar check for
          #       options, so maybe that's why this is here.
          isAttrs c.config
          || throw ''
            In module `${c.file}', you're trying to define a value of type `${typeOf c.config}'
            rather than an attribute set for the option
            `${concatStringsSep "." prefix}'!

            This usually happens if `${concatStringsSep "." prefix}' has option
            definitions inside that are not matched. Please check how to properly define
            this option by e.g. referring to `man 5 configuration.nix'!
          ''
        ) configs;
        configs;

      # an attrset 'name' => list of submodules that define ‘name’.
      pushedDownDefinitionsByName = zipAttrsWith (n: concatLists) (
        map (
          module:
          mapAttrs (
            n: value:
            map (config: {
              inherit (module) file;
              inherit config;
            }) (pushDownProperties value)
          ) module.config
        ) checkedConfigs
      );
      # extract the definitions for each loc
      rawDefinitionsByName = zipAttrs (
        map (
          module:
          mapAttrs (n: value: {
            inherit (module) file;
            inherit value;
          }) module.config
        ) checkedConfigs
      );

      # Convert an option tree decl to a submodule option decl
      optionTreeToOption =
        decl:
        if isOption decl.options then
          decl
        else
          decl
          // {
            options = mkOption {
              type = types.submoduleWith {
                modules = [ { options = decl.options; } ];
                # `null` is not intended for use by modules. It is an internal
                # value that means "whatever the user has declared elsewhere".
                # This might become obsolete with https://github.com/NixOS/nixpkgs/issues/162398
                shorthandOnlyDefinesConfig = null;
              };
            };
          };

      resultsByName = mapAttrs (
        name: decls:
        # We're descending into attribute ‘name’.
        let
          loc = prefix ++ [ name ];
          defns = pushedDownDefinitionsByName.${name} or [ ];
          defns' = rawDefinitionsByName.${name} or [ ];
          optionDecls = filter (
            m:
            m.options ? _type
            && (m.options._type == "option" || throwDeclarationTypeError loc m.options._type m._file)
          ) decls;
        in
        if length optionDecls == length decls then
          let
            opt = fixupOptionType loc (mergeOptionDecls loc decls);
          in
          {
            matchedOptions = evalOptionValue loc opt defns';
            unmatchedDefns = [ ];
          }
        else if optionDecls != [ ] then
          if
            all (x: x.options.type.name or null == "submodule") optionDecls
          # Raw options can only be merged into submodules. Merging into
          # attrsets might be nice, but ambiguous. Suppose we have
          # attrset as a `attrsOf submodule`. User declares option
          # attrset.foo.bar, this could mean:
          #  a. option `bar` is only available in `attrset.foo`
          #  b. option `foo.bar` is available in all `attrset.*`
          #  c. reject and require "<name>" as a reminder that it behaves like (b).
          #  d. magically combine (a) and (c).
          # All of the above are merely syntax sugar though.
          then
            let
              opt = fixupOptionType loc (mergeOptionDecls loc (map optionTreeToOption decls));
            in
            {
              matchedOptions = evalOptionValue loc opt defns';
              unmatchedDefns = [ ];
            }
          else
            let
              nonOptions = filter (m: !isOption m.options) decls;
            in
            throw "The option `${showOption loc}' in module `${(head optionDecls)._file}' would be a parent of the following options, but its type `${
              (head optionDecls).options.type.description or "<no description>"
            }' does not support nested options.\n${showRawDecls loc nonOptions}"
        else
          mergeModules' loc decls defns
      ) declsByName;

      matchedOptions = mapAttrs (n: v: v.matchedOptions) resultsByName;

      # an attrset 'name' => list of unmatched definitions for 'name'
      unmatchedDefnsByName =
        # Propagate all unmatched definitions from nested option sets
        mapAttrs (n: v: v.unmatchedDefns) resultsByName
        # Plus the definitions for the current prefix that don't have a matching option
        // removeAttrs rawDefinitionsByName (attrNames matchedOptions);
    in
    {
      inherit matchedOptions;

      # Transforms unmatchedDefnsByName into a list of definitions
      unmatchedDefns =
        if configs == [ ] then
          # When no config values exist, there can be no unmatched config, so
          # we short circuit and avoid evaluating more _options_ than necessary.
          [ ]
        else
          concatLists (
            mapAttrsToList (
              name: defs:
              map (
                def:
                def
                // {
                  # Set this so we know when the definition first left unmatched territory
                  prefix = [ name ] ++ (def.prefix or [ ]);
                }
              ) defs
            ) unmatchedDefnsByName
          );
    }