Nix language - function arguments aren't being passed

Hello,

I have an issue. I am integrating home manger into my nixos config and I wanted to add per-user support by passing an extra username argument. For this, I created a wrapper

home = username: args: (import ./home.nix (args // {username = username;}));

Now, if I understand nix correctly, doing

import ./home.nix {arg1=1; arg2=2; username="myuser";}

Should be exactly the same as doing

(home "myuser") {arg1=1; arg2=2;}

However, my wrapper doesn’t seem to work.

My semi-full config:

flake.nix in /etc/nixos/

{
  description = "NixOS configuration";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager/master";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    plasma-manager = {
      # Due to https://github.com/nix-community/plasma-manager/issues/556
      url = "github:nix-community/plasma-manager/d4fae34";
      inputs.nixpkgs.follows = "nixpkgs";
      inputs.home-manager.follows = "home-manager";
    };
  };

  outputs =
    {
      self,
      nixpkgs,
      home-manager,
      plasma-manager,
      ...
    }@inputs:
    let 
      home = username: args: (import ./home.nix (args // {username = username;}));
    in
    {
      nixosConfigurations.victus-nix = nixpkgs.lib.nixosSystem {
        specialArgs = { inherit inputs; };
        modules = [
          ./configuration.nix

          home-manager.nixosModules.home-manager
          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.sharedModules = [ plasma-manager.homeModules.plasma-manager ];

            home-manager.users.myname = home "myname";
          }
        ];
      };
    };
}

my dummy home.nix

{ config, pkgs, ... }@args:
{
  home = {
    stateVersion = "25.05";

    packages = with pkgs; [];
  };
}

If I try `nixos-rebuild build` I get

       error: function 'anonymous lambda' called without required argument 'pkgs'
       at /etc/nixos/home.nix:1:1:
            1| { config, pkgs, ... }@args:
             | ^
            2| {
Command 'nix --extra-experimental-features 'nix-command flakes' build --print-out-paths '/etc/nixos#nixosConfigurations."victus-nix".config.system.build.toplevel'' returned non-zero exit status 1.

However if I replace home "myname" with just import ./home.nix\ it works.

HOWEVER it also works if I just leave it as import ./home.nix and change ./home.nix to

{ username }:
{ config, pkgs, ... }@args:
{
  home = {
    stateVersion = "25.05";

    packages = with pkgs; [];
  };
}

It suddenly seems to work the way I intended??

What is going on? What I suspect is something somewhere is looking up expected named arguments of the function import ./home.nix and not passing the pkgs if it isnt a named argument, (as it also works if the wrapper explicitly names pkgs). But, like, how would I even check that is what is happening?

Modules are also functions and you’re wiping out the default module args with the way you’ve written it (import-ing home.nix and then passing args to that function).

Funnily enough, I just pointed out to another user the folly of using import in the module system:

Use imports - that’s what it’s there for.

If you need additional module args, we have _module.args.ARGNAME = VALUE;, but really most of the time they should be dedicated options so they can have proper type checks, merging, overriding, etc. See https://nixos.org/manual/nixos/stable/#sec-writing-modules

3 Likes

Interesting thanks! So then does it mean that import ./file.nix is is not the same as if I just literally pasted the imported file into the place of the import?

The issue here isn’t that you’re using import; it’s that you’re eta-expanding the imported function but losing its named argument metadata. (Just to demonstrate what’s going on, not as a recommendation, I think you could write this:

  home =
    username:
    let
      importedFn = import ./home.nix;
    in
    lib.setFunctionArgs (args: importedFn (args // { inherit username; })) (lib.functionArgs importedFn)

and that would restore the metadata that the module system is using.)

As you discovered, changing ./home.nix to include an extra argument (or multiple extra arguments) up front is a decent way to resolve this, since that preserves the original function’s argument names without adding another layer of eta-expansion.

No, those two things are equivalent. The catch here is that f is not necessarily equivalent to args: f args, when f has named argument metadata.

3 Likes

It’s not, as it messes up error locations IIRC. There’s also just no reason to use import in the module system, it’s a massive antipattern.

I agree with this, using import to include expressions that are evaluated as part of the module system is unnecessary and an easy footgun. It should only be used to include separate ordinary nix expressions, like say, a derivation you have in a package.nix that you wish to add. imports should be used for anything involving module system options.

Nope, even then, for derivations you should use pkgs.callPackage.

Oops, that was a bad example lol. I still maintain that it should only be used to include separate ordinary nix expressions. In my specific case, I use it to split my flake.nix without flake-parts.

1 Like

@waffle8946 specifically limits the advice to the module system, which you’re explicitly not using in that scenario :wink:

There really are only very few edge cases where you’d use import within the module system. In general, follow @waffle8946 's advice - in fact, Imma add this to my linter.

Off the top of my head, on a system with channels, you can legitimately use it for import <NIX_PATH_ENTRY> { }, but I think idiomatically you should switch to npins by the time you’re considering doing that. Also maybe if you store data in a .nix file instead of JSON. Arguably you shouldn’t do that, either, though, just use JSON.

2 Likes