Is it possible to use custom arguments in Nix configuration?

Hello fellow Nix users,

I’m trying to modularize my Home Manager configuration to avoid redundancy when using multiple package channels (e.g., nixpkgs-unstable). My current setup might lead to repetitive use of let other-channel = import <other-channel> {}; blocks, which I’d like to replace with custom arguments passed to child modules. Here’s what I’ve tried:

Current Setup (home.nix)

{ config, pkgs, ... }:

let
  unstable = import <nixpkgs-unstable> { };
in {
  imports = [ ./devbox.nix ];
  
  home.packages = (with pkgs;[ddcutil])++(with unstable; [typst]);
}

Child Module (devbox.nix)

{ pkgs, lib, config, unstable, ... }: {
  options.devbox.enable = lib.mkEnableOption "Enable devbox";
  config = lib.mkIf config.devbox.enable {
    home.packages = with unstable; [ devbox ];
  };
}

This fails with error: the option 'unstable' does not exist when imported.

Attempted Solutions

  1. Passing arguments via imports (unsuccessful):
    imports = [ ./devbox.nix { unstable = import <nixpkgs-unstable> {}; } ];
    
  2. Default argument in module (also unsuccessful):
    { unstable ? import <nixpkgs-unstable> {}, ... }: { ... }
    

So basically, can I pass custom arguments like unstable to child modules without repeating let blocks ?

NB

  • I’m using Home Manager without Flakes.
  • The nixpkgs-unstable channel is already added via nix-channel.

Any guidance would be appreciated!

Yes, you can add it to _module.args:

# configuration.nix
{
  # You should only use this in unusual scenarios, like here when
  # adding a second channel you want to access across multiple
  # modules.
  #
  # Most of the time, you want to define a custom option instead.
  _module.args = {
    unstable = import <nixpkgs-unstable> { };
  };
}

This will make it available in the args of all modules, i.e., this will work, and without specifying a default value:

DO NOT overuse this pattern, most of the time when someone wants to pass arguments to modules they should be using custom options instead.

This particular scenario is a pretty legitimate use case, though; if nothing else, it’ll significantly speed up evaluation times because you’re not evaluating all of nixpkgs over and over again.

Alternatively, you could also use an overlay on pkgs, and access unstable via pkgs.unstable. Personally I prefer the module args, and consider using overlays like that as misusing the concept, especially since flakes exist and their concept of inputs map more cleanly to the _module.args abstraction. You’d only be using pkgs as a vehicle to put another argument into the module args, after all. That’s just silly.

Finally, in case you ever do switch to using flakes, you can use the specialArgs to effectively write things to _module.args. You would use that to pass your unstable input into NixOS.

Click here for bonus recommendations...

I’d also recommend reducing your use of the unstable channel as much as possible, or switching to unstable fully. Incompatibilities can and do happen, and those aren’t fun. A responsible way to use NixOS with multiple channels is to switch to using only stable every time there is a new NixOS release, and only pull in unstable stuff when you find you need some new feature that won’t hit stable until the next release cycle.

If you follow the above, you should have quite few unstable dependencies - if you depend on unstable a lot, that almost certainly means you’re pulling in stuff just because you want to feel like you’re using something cutting edge, without really understanding why you might or might not want to. NixOS stable is already very cutting edge wherever doing so is reasonable, so I’d caution against that kind of thinking.

2 Likes