Modifying home-manager.users in a different nix module

Hi everyone,

i have a problem with my nixos config, because i am currently trying to modularize my config in a very specific way. The final goal of this is to have a separate set of modules that each define a programm and customize it in a specific way, and a set of users that import these program modules. This shall allow me to have the exact same config for some specific tools (like e.g. my nvim config) that will be installed across several users on several machines, while installing specific software like hyprland to only my laptop and not to machines without a GUI, like my server.

Long story short, this worked fine while i was only using home-manager based configs, but broke once i needed to mix global configurations and home-manager ones. Here are the relevant parts:

#flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-24.05";

    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    hyprland.url = "git+https://github.com/hyprwm/Hyprland?submodules=1";
    split-monitor-workspaces = {
      url = "github:Duckonaut/split-monitor-workspaces";
      inputs.hyprland.follows = "hyprland"; # <- make sure this line is present for the plugin to work as intended
    };

    nur = {
        url = "github:nix-community/NUR";
        inputs.nixpkgs.follows = "nixpkgs";      
    };
  };

  outputs = {self, nixpkgs, home-manager, split-monitor-workspaces, nur, ...}@inputs: 
  let 
    system = "x86_64-linux";
    pkgs = import nixpkgs {
        inherit system split-monitor-workspaces;
    };
  in
  {
    nixosConfigurations = {
      laptop-msi-vector = nixpkgs.lib.nixosSystem {
        specialArgs = {inherit inputs;};
        modules = [
          nur.modules.nixos.default
          nur.legacyPackages."${system}".repos.iopq.modules.xraya

          ./hosts/laptop-msi-vector

          home-manager.nixosModules.home-manager {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.extraSpecialArgs = {inherit inputs; };
            home-manager.sharedModules = [./home/common];
            home-manager.backupFileExtension = "backup";
            
            imports = [ ./home/andre ];
          }
        ];
      };
    };

  };
}
#home/andre/default.nix
{config, lib, pkgs, home-manager, ...}:
{
  users.users.andre = {
    isNormalUser = true;
    extraGroups = ["networkmanager" "wheel" "qemu-libvirtd" "libvirtd" "input" "docker"];
  };

  services.getty.autologinUser = "andre";
  
  home-manager.users.andre = import ./home.nix;
  _module.args.user_config = home-manager.users.andre.home;
  imports = [
    ../../modules/gnupg 
  ];
}
#modules/gnupg/default.nix
{config, pkgs, lib, user_config, ...}:
{
  #ptions = { 
  # # This is for ssh key management with the Yubikey, so it only makes sense for systems with access to usb ports
  # gnupg.enable = lib.mkEnableOption "gnupg";
  #}

  #config = lib.mkIf config.gnupg.enable {
    user_config.packages = [
      pkgs.gnupg
    ];
 
    services.pcscd.enable = true;
    programs.gnupg.agent = {
      enable = true;
      pinentryPackage = pkgs.pinentry-curses;
      enableSSHSupport = true;
    }; 
  #}; 
}

The specific issue here is that the gnupg module requires to set some global options, like the pcscd services etc. but I was importing this inside the home-manager config. While trying to rework that I arrived at the above state. So the idea is, that I will be importing all modules globally, while the modules themselves can then modify the home-manager.user config which i will pass to them as a parameter, so that each host can decide which user will be installed. I have tried different ways to set the user_config parameter, but i always get this error:

error: The option `user_config' does not exist. Definition values:
       - In `/nix/store/sljqzvycm48vm6jaqmqixrbfasx0b4x3-source/modules/gnupg':
           {
             packages = [
               <derivation gnupg-2.4.5>
             ];
           }

Interestingly enough i can just set home-manager.users.andre.home.packages etc inside the gnupg module. So i do not get while it does not exist when i try giving it as a parameter, but i can just set it directly?

There are a few lines commented out, because of the different experiments i was doing but i think they are irrelevant to the current issue.

Any help is greatly appreciated. If you are gasping at the monstrosity i want to create and can provide a way better solution it would be great too.

Thanks in advance

What you have in mind is not possible by passing in a module arg. You have to define an option, via mkOption or such, then use that to set home-manager.users.<whatever>.

  options.user_config = lib.mkOption {
    # ...
  };

But the boilerplate involved is unnecessary and error-prone, and will likely make your config harder to debug, you might as well just write it out IMO.

Ok thank you, but can you explain why this is not possible? Am I not passing the attribute set home-manager.users.<whatever>, modifying it inside the module and then returning it? I think i still do not fully get how arguments/parameters of nix functions work.

Writing it out is undesireable because it would require the module to know which users exist and which to modify. I would rather the user to import the desired module, pass the user as an argument/parameter and the module modifying that.

If you really want it that way, let’s say _user is a module parameter, then home-manager.users.${_user} = { ... }; would work. I think you just misunderstood the syntax somehow. Attribute names (i.e. the bits on the left-hand side) are literals, and user_config is set to the value of the right-hand side (it has no idea about the attribute path, so you can’t reconstruct the path from the user_config arg).

Thank you very much. The solution with adding ${_user} and using a string interpolation works for my desired use case.

However i still do not understand why my initial plan does not really work. So to explain my thoughtprocess again:

  • home-manager.users.user is an attribute set like any other
  • I can assign attribute sets to a variable and pass them as attributes to a module
  • Therefore i should be able to assign the attribute set defined in home-manager.users.user to the user_config variable and modify it in my module?

I do realize that depending on whether nix passes by value or by reference this would not have had the desired goal, since modifying a copy of the attribute set does not modify the original value of it and there would not install the desired programs etc. But the error of “option does not exist” really does not make any sense to me.

All values in nix are essentially immutable, so there is no concept of “modifying” a value; in fact, nix does not have variables, period. The only thing that exists are local bindings whose values can get passed around via functions. It would even be a syntax error to define the same binding twice at the same scope. I think you just have to move away from the Java (or whatever) mindset here and acclimate to nix being a functional, declarative language.