How to fix this infinite recursion?

I asked already in matrix, but probably here it will be easier to reference for the future. I’m getting an infinite recursion. Here’s the code. Just copy the flake.nix to an empty folder and run nix flake check --show-trace; you’ll get the error:

{
  outputs = {
    self,
    nixpkgs,
  }: rec {
    # A module that defines some args for other modules
    nixosModules.args = {
      # This arg lets you see that there's no infinite recursion here
      _module.args.disk = "/dev/sda";
      # This arg is a hosts inventory; infinite recursion happens here
      _module.args.inventory = rec {
        all = {
          host1 = {
            provider_name = "a";
            deployment_version = "v1";
          };
        };
        get = hostName: all.${hostName};
      };
    };

    # Another module that uses the prvious one's inputs
    nixosModules.fileSystem = {
      disk,
      inventory,
      ...
    }: let
      host = inventory.get "host1";
      pinnedConfigs.a.v1 = {
        # Old deployments didn't use custom partitions
        fileSystems."/" = {
          device = disk;
          fsType = "ext4";
        };
      };
    in
      # If you uncomment this line and comment the next one, the infinite recursion disappears
      # pinnedConfigs.a.v1;
      pinnedConfigs.${host.provider_name}.${host.deployment_version}
      // {
        # However, this one is using `disk`, which won't fail with recursion, but it comes from the same place
        boot.loader.grub.device = disk;
      };

    # The host configuration
    nixosConfigurations.host1 = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = with nixosModules; [
        args
        fileSystem
        {networking.hostName = "host1";}
      ];
    };
  };
}
flake.lock
{
  "nodes": {
    "nixpkgs": {
      "locked": {
        "lastModified": 1677080879,
        "narHash": "sha256-0SjW4/d3Rkw6C7hHZ5lxT4r6Pw9vzQb6Il6zYWwe2Bo=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "f5dad40450d272a1ea2413f4a67ac08760649e89",
        "type": "github"
      },
      "original": {
        "id": "nixpkgs",
        "type": "indirect"
      }
    },
    "root": {
      "inputs": {
        "nixpkgs": "nixpkgs"
      }
    }
  },
  "root": "root",
  "version": 7
}
Error logs
error: infinite recursion encountered

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:483:28:

          482|         builtins.addErrorContext (context name)
          483|           (args.${name} or config._module.args.${name})
             |                            ^
          484|       ) (lib.functionArgs f);

       … while evaluating the module argument `inventory' in ":anon-1395:anon-1":

       … while calling anonymous lambda

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:481:44:

          480|       context = name: ''while evaluating the module argument `${name}' in "${key}":'';
          481|       extraArgs = builtins.mapAttrs (name: _:
             |                                            ^
          482|         builtins.addErrorContext (context name)

       … from call site

       … while calling 'fileSystem'

       at /nix/store/xixw4qsdq4ay1ggg7shkpa4mi5pmam50-source/flake.nix:23:31:

           22|     # Another module that uses the prvious one's inputs
           23|     nixosModules.fileSystem = {
             |                               ^
           24|       disk,

       … from call site

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:490:8:

          489|       # works.
          490|     in f (args // extraArgs)
             |        ^
          491|   else

       … while calling 'applyModuleArgsIfFunction'

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:466:39:

          465|
          466|   applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then
             |                                       ^
          467|     let

       … from call site

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:361:55:

          360|         if isFunction m || isAttrs m then
          361|           unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgsIfFunction fallbackKey m args)
             |                                                       ^
          362|         else if isList m then

       … while calling 'unifyModuleSyntax'

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:434:34:

          433|      of ‘options’, ‘config’ and ‘imports’ attributes. */
          434|   unifyModuleSyntax = file: key: m:
             |                                  ^
          435|     let

       … from call site

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:361:11:

          360|         if isFunction m || isAttrs m then
          361|           unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgsIfFunction fallbackKey m args)
             |           ^
          362|         else if isList m then

       … while calling 'loadModule'

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:359:53:

          358|       # Like unifyModuleSyntax, but also imports paths and calls functions if necessary
          359|       loadModule = args: fallbackFile: fallbackKey: m:
             |                                                     ^
          360|         if isFunction m || isAttrs m then

       … from call site

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:400:22:

          399|           let
          400|             module = loadModule args parentFile "${parentKey}:anon-${toString n}" x;
             |                      ^
          401|             collectedImports = collectStructuredModules module._file module.key module.imports args;

       … while evaluating the attribute 'disabled'

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:395:13:

          394|           collectResults = modules: {
          395|             disabled = concatLists (catAttrs "disabled" modules);
             |             ^
          396|             inherit modules;

       … while calling anonymous lambda

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:419:31:

          418|           disabledKeys = map moduleKey disabled;
          419|           keyFilter = filter (attrs: ! elem attrs.key disabledKeys);
             |                               ^
          420|         in map (attrs: attrs.module) (builtins.genericClosure {

       … from call site

       … while calling 'filterModules'

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:413:36:

          412|       # modules recursively. It returns the final list of unique-by-key modules
          413|       filterModules = modulesPath: { disabled, modules }:
             |                                    ^
          414|         let

       … from call site

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:426:7:

          425|     in modulesPath: initialModules: args:
          426|       filterModules modulesPath (collectStructuredModules unknownModule "" initialModules args);
             |       ^
          427|

       … while calling anonymous lambda

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:425:37:

          424|
          425|     in modulesPath: initialModules: args:
             |                                     ^
          426|       filterModules modulesPath (collectStructuredModules unknownModule "" initialModules args);

       … from call site

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:258:25:

          257|       merged =
          258|         let collected = collectModules
             |                         ^
          259|           (specialArgs.modulesPath or "")

       … while calling 'reverseList'

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/lists.nix:406:17:

          405|   */
          406|   reverseList = xs:
             |                 ^
          407|     let l = length xs; in genList (n: elemAt xs (l - n - 1)) l;

       … from call site

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:262:33:

          261|           ({ inherit lib options config specialArgs; } // specialArgs);
          262|         in mergeModules prefix (reverseList collected);
             |                                 ^
          263|

       … while calling 'byName'

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:542:25:

          541|       */
          542|       byName = attr: f: modules:
             |                         ^
          543|         zipAttrsWith (n: concatLists)

       … from call site

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:559:21:

          558|       # an attrset 'name' => list of submodules that declare ‘name’.
          559|       declsByName = byName "options" (module: option:
             |                     ^
          560|           [{ inherit (module) _file; options = option; }]

       … while evaluating the attribute 'matchedOptions'

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:637:14:

          636|     in {
          637|       inherit matchedOptions;
             |              ^
          638|

       … while calling 'mapAttrsRecursiveCond'

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/attrsets.nix:530:5:

          529|     # Attribute set to recursively map over.
          530|     set:
             |     ^
          531|     let

       … from call site

       at /nix/store/4nxnb4jsbk18vxvzmmy109zlj9kmw5x1-source/lib/modules.nix:270:28:

          269|           # For definitions that have an associated option
          270|           declaredConfig = mapAttrsRecursiveCond (v: ! isOption v) (_: v: v.value) options;
             |                            ^
          271|

       … while checking the NixOS configuration 'nixosConfigurations.host1'

       at /nix/store/xixw4qsdq4ay1ggg7shkpa4mi5pmam50-source/flake.nix:46:5:

           45|     # The host configuration
           46|     nixosConfigurations.host1 = nixpkgs.lib.nixosSystem {
             |     ^
           47|       system = "x86_64-linux";

       … while checking flake output 'nixosConfigurations'

       at /nix/store/xixw4qsdq4ay1ggg7shkpa4mi5pmam50-source/flake.nix:46:5:

           45|     # The host configuration
           46|     nixosConfigurations.host1 = nixpkgs.lib.nixosSystem {
             |     ^
           47|       system = "x86_64-linux";

Questions:

  1. Why is this producing an infinite recursion?
  2. How to fix it?
1 Like

You cannot use _module.args to determine what to import. That always produces infrec.

That’s because the imported module might mkForce the _module.args in use. Basically, in order to know the value of any option, nix must first import all modules, so if the list of modules depends on an option, you get infrec.

The solution is generally to use specialArgs instead, which exist in addition to _module.args for exactly this reason.

specialArgs are passed through the nixosSystem call, rather than being an option, so they avoid the issue.

1 Like

Thanks for the explanation, I was becoming crazy with this! This patch fixes the exercise indeed:

--- old/flake.nix       2023-02-24 08:40:31.146932549 +0000
+++ new/flake.nix       2023-02-24 08:40:05.152387396 +0000
@@ -45,8 +45,8 @@
     # The host configuration
     nixosConfigurations.host1 = nixpkgs.lib.nixosSystem {
       system = "x86_64-linux";
+      specialArgs = nixosModules.args._module.args;
       modules = with nixosModules; [
-        args
         fileSystem
         {networking.hostName = "host1";}
       ];

FTR I found this comment that explains more or less the same: nixpkgs/modules.nix at f03c72db1fa0d8757d46db285130793c8b2b6325 · NixOS/nixpkgs · GitHub