Search code examples
nixnixosnixpkgs

Infinite recursion when referring to pkgs.system from Nix module options section


The following is a minimal reproducer for an infinite recursion error when building a nixos configuration:

(import <nixpkgs/nixos>) {
  configuration = { pkgs, ... }: {
    options = builtins.trace "Building a system with system ${pkgs.system}" {};
  };
  system = "x86_64-linux";
}

When evaluated it fails as follows, unless the reference to pkgs.system is removed:

$ nix-build 
error: infinite recursion encountered

       at /Users/charles/.nix-defexpr/channels/nixpkgs/lib/modules.nix:496:28:

          495|         builtins.addErrorContext (context name)
          496|           (args.${name} or config._module.args.${name})
             |                            ^
          497|       ) (lib.functionArgs f);

If we look at the implementation of nixos/lib/eval-config.nix:33, we see that the value passed for the system argument is set as an overridable default in pkgs. Does this mean we can't access it until later in the evaluation process?

(In the real-world use case, I'm introspecting a flake -- investigating someFlake.packages.${pkgs.system} to find packages for which to generate configuration options.)

This has been cross-posted to NixOS Discourse; see https://discourse.nixos.org/t/accessing-target-system-when-building-options-for-a-module/


Solution

  • In order for the module system to construct the configuration, it needs to know which config and options items exist, at least to the degree necessary to produce the root attribute set of configuration.

    The loop is as follows:

    1. Evaluate the attribute names in config
    2. Evaluate the attribute names of the options
    3. Evaluate pkgs (your code)
    4. Evaluate config._module.args.pkgs (definition of module argument)
    5. Evaluate the attribute names in config (loop)

    It can be broken by removing or reducing the dependency on pkgs. For instance, you could define your "dynamic" options as type = attrsOf foo instead of enumerating the each item from your flake as individual options.

    Another potential solution is to move the option definitions into a submodule. A submodule without attrsOf as in attrsOf (submodule x) is generally quite useless, but it may create a necessary indirection that separates your dynamic pkgs-dependent options from the module fixpoint that has pkgs.

    (import <nixpkgs/nixos>) {
      configuration = { pkgs, lib, ... }: {
        options.foo = lib.mkOption {
          type = lib.types.submodule {
            options = builtins.trace "Building a system with system ${pkgs.system}" { };
          };
          default = { };
        };
      };
      system = "x86_64-linux";
    }
    
    nix-repl> config.foo
    trace: Building a system with system x86_64-linux
    { }