Accessing target system when building options for a module

I’m working on a Nix module that exposes different options based on content retrieved from a flake using a path that involves specifying a particular system. That is to say, theFlake.x86_64-linux.packages will have different content from theFlake.aarch64-darwin.packages, and I’m populating options from my module based on that content.

Using builtins.currentSystem isn’t appropriate, because we want to be building a system with options appropriate to the target; however, trying to refer to pkgs.system from inside options (instead of config) throws an infinite recursion error (as makes sense: reviewing nixos/lib/eval-config.nix we see that pkgs.system is set up via lib.mkDefault so it’s able to be overridden, and doesn’t yet have a known value early in evaluation).

Trying to pass something like config._module.args.forceSystem runs into the same problem – we don’t have a config yet when options are being evaluated. Do I need to restructure my code to be able to pass an extra copy of the target architecture via specialArgs to nixos/lib/eval-config.nix?

What’s the Right Way around this issue?

To be clear, you can define options in terms of config, so long as the relevant bits are sufficiently separate. I’ve done it.

Are you trying to import different modules based on system? or just defined different options? I would expect the former to fail but the latter to succeed.

Here’s a simple reproducer:

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

…which, when evaluated on a darwin host (and possibly others, haven’t checked), results in:

$ 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);

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 Stack Overflow; see nixos - Infinite recursion when referring to pkgs.system from Nix module options section - Stack Overflow)

Here’s a simple non-reproducer, demonstrating my point:

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

Remember that pkgs actually gets determined in terms of the nixpkgs.* options, so whatever you set inside options must be sufficiently separated from those to avoid infinite recursion.