Need help addressing mysterious bug in refactored home-manager configuration

Howdy!

I’ve recently refactored my home-manager configuration and have encountered an issue I can’t figure out (thankfully it is simple to explain).

Basically, it seems like for whatever reason that adding a package (home-manager) works in one module, but does not work in another. I am certain that both modules are at least being evaluated.

Here is where it works (smkuehnhold@smk-desktop a.k.a. …/users/fullUsername), and here is where it doesn’t (lib).

Does anyone have a guess as to why this is? Or even better can anyone instruct me how to debug this on my own?

Please let me know if you need more context or if I am doing something dumb.

Thanks,
smkuehnhold

# flake.nix

{
  inputs = {
    # system-config
    # FIXME: what happens if system does not have a flake located at /etc/nixos ???
    # FIXME: this also means I have to keep my hosts in sink??? that sounds bogus...
    system-flake.url = "/etc/nixos";

    # nixpkgs (depends on system-flake)
    nixpkgs.follows = "system-flake/nixpkgs";

    # home-manager
    home-manager.url = "github:nix-community/home-manager/release-22.11";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";

    # nix-user-repository
    nur.url = "github:nix-community/NUR";
  
    # vscode-extensions
    # Pin nix-vscode-extensions because latest seems to rely on unstable?                
    # https://github.com/nix-community/nix-vscode-extensions/issues/11
    nix-vscode-extensions.url = "github:nix-community/nix-vscode-extensions/83b9f149ffc2a6cdd44d8083050e7e245706ae2f";
    nix-vscode-extensions.inputs.nixpkgs.follows = "nixpkgs";

    # wired
    wired.url = "github:Toqozz/wired-notify";
    wired.inputs.nixpkgs.follows = "nixpkgs";

    # FIXME: KNOCK DELETED?!?!
    # knock.url = "github:BentonEdmondson/knock";
    # knock.inputs.nixpkgs.follows = "system-config/nixpkgs";
  };

  outputs = { self, system-flake, nixpkgs, home-manager, nur, nix-vscode-extensions, wired }:

  let
    lib = (import ./lib { inherit system-flake nixpkgs home-manager; });

  in {
    homeConfigurations = builtins.foldl' (acc: nextUser: 
      acc // (lib.mkUser { 
        fullUsername = nextUser;
        extraBaseModules = [
          ({pkgs, ... }: {
            imports = [
              wired.homeManagerModules.default
            ];

            nixpkgs.overlays = [
              nix-vscode-extensions.overlays.default
              nur.overlay
              wired.overlays.default
            ];
          })
        ];
      })
    ) {} lib.const.users;
  };
}
# lib/default.nix

{ system-flake, nixpkgs, home-manager, ... }:

let
  unfreePkgWhitelist = (import ./unfree-pkg-whitelist.nix);

  mkSimplePredicate = list: check: item: builtins.elem (check item) list;
in {
  const = {
    users = nixpkgs.lib.mapAttrsToList (username: _: username) (builtins.readDir ../users);
  };

  mkUser = {
    fullUsername,
    extraBaseModules ? []
  }:
    let
      user = builtins.listToAttrs (nixpkgs.lib.zipListsWith (name: value: 
        { 
          inherit name value; 
        }) [ "name" "host" ] (nixpkgs.lib.splitString "@" fullUsername));
      system = system-flake.outputs.nixosConfigurations."${user.host}".pkgs.system;
      basePkgs = system-flake.inputs.nixpkgs.legacyPackages."${system}";
      systemConfig = system-flake.outputs.nixosConfigurations."${user.host}".config;
    in {
      "${fullUsername}" = home-manager.lib.homeManagerConfiguration {
        pkgs = basePkgs; # Use the host's pkgs as a base.
        modules = [
          ../modules
          ../pkgs
          (../users + "/${fullUsername}")
          ({ pkgs, ... }: {
            nixpkgs = {
              config.allowUnfreePredicate = mkSimplePredicate unfreePkgWhitelist nixpkgs.lib.getName;
            };

            _module.args = {
              system-config = systemConfig; # pass system configuration to modules
            };

            # FIXME: why doesn't this work here???
            # home.packages = with pkgs; [ home-manager ];
            # home.enableNixpkgsReleaseCheck = true;
          })
        ] ++ extraBaseModules;
      };
    };
}
# users/smkuehnhold@smk-desktop/default.nix

{ pkgs, ... }: 

{
  imports = [
    ./noisetorch
  ];

  config = {
    # works here???
    home.packages = with pkgs; [ home-manager ];

    home = {
      username = "smkuehnhold";
      homeDirectory = "/home/smkuehnhold";
      stateVersion = "22.05";
    };

    my.desktop.environment = {
      enable = true;
      base = "bspwm+sxhkd";
      includesOptionalUserSoftware = true;
    };

    my.development = {
      editors = [ "codium" ];
      suites = [ "nix" "swift" ];
    };

    my.games.suites = [ "lutris" "minecraft" "steam" ];
  };

}

To perhaps clarify my issue a bit more concisely (and garner a bit more traction), I have a home-manager configuration that looks roughly like this:

# fullUsername = A string that looks like username@host
# basePkgs = A set of pkgs
# systemConfig = A nixos config
# unfreePkgWhitelistPredicate = A predicate of unfree pkgs to whitelist

"${fullUsername}" = home-manager.lib.homeManagerConfiguration {
  pkgs = basePkgs; # Use the host's pkgs as a base.
  modules = [
    (../users + "/${fullUsername}") # load modules associated with username
    ({ pkgs, ... }: {
      nixpkgs = {
        config.allowUnfreePredicate = unfreePkgWhitelistPredicate
      };

      _module.args = {
        system-config = systemConfig; # pass system configuration to modules
      };
    })
  ];
};

The mystery is that placing home.packages = with pkgs; [ home-manager ]; directly in the list of modules like so:

"${fullUsername}" = home-manager.lib.homeManagerConfiguration {
  pkgs = basePkgs; # Use the host's pkgs as a base.
  modules = [
    (../users + "/${fullUsername}") # load modules associated with username
    ({ pkgs, ... }: {
      nixpkgs = {
        config.allowUnfreePredicate = unfreePkgWhitelistPredicate
      };

      _module.args = {
        system-config = systemConfig; # pass system configuration to modules
      };

      # install home-manager cli
      home.packages = with pkgs; [ home-manager ];
    })
  ];
};

results in an environment without the home-manager cli.

Conversely, placing home.packages = with pkgs; [ home-manager ]; in the (../users + "/${fullUsername}") module results in an environment with the home-manager cli.

My only guess would be that somehow the module literal was not being evaluated. But I don’t think that is the case because removing packages from the whitelist results in an invalid configuration due to the use of unfree pkgs…

Since there is a lot I do not understand about the module system, I can’t quite figure out what the difference is here. I am hoping that someone in the community can at least help explain the difference.

Thanks,
smkuehnhold

Another weird quirk

"${fullUsername}" = home-manager.lib.homeManagerConfiguration {
  pkgs = basePkgs; # Use the host's pkgs as a base.
  modules = [
    (../users + "/${fullUsername}") # load modules associated with username
    ({ pkgs, ... }: {
      nixpkgs = {
        config.allowUnfreePredicate = unfreePkgWhitelistPredicate
      };

      _module.args = {
        system-config = systemConfig; # pass system configuration to modules
      };

      # install home-manager cli
      home.packages = with pkgs; [ cowsay home-manager ];
    })
  ];
};

results in a user environment that has cowsay but does not have home-manager :thinking:

Just figured it out, turns out I was placing the home-manger module rather than the pkg into my environment.

i.e. I had a function that looked something like this

{ home-manager, nixpkgs, ... }: 

let
  pkgs = import <nixpkgs> {};
in home-manager.lib.homeManagerConfiguration {
  inherit pkgs;
  modules = [
     { pkgs, ... } : {
        home.packages = with pkgs; [ home-manager ];
     }
  ]
}

where the home-manger in home.packages = with pkgs; [ home-manager ] did not resolve to pkgs.home-manager like I would have expected, but the home-manger module in the argument list.

TBH it kind of surprises me that the with pkgs; statement doesn’t shadow any identifiers in the outer scope. Seems like I need to brush up a bit more on nix-the-language. Is this behavior documented anywhere?