Get hostname in home-manager flake for host-dependent user configs

I have successfully flakified my system and HM config by making a flake (inspired by @gvolpe 's implementation) with

  ...
  outputs = inputs @ { self, nixpkgs, nurpkgs, nixpkgsUnstable, home-manager }:
    let
      system = "x86_64-linux";
    in
    {
      homeConfigurations = (
        import ./outputs/home-conf.nix {
          inherit system;
          inherit (inputs) nixpkgs nurpkgs nixpkgsUnstable home-manager;
        }
      );

      nixosConfigurations = (
        import ./outputs/nixos-conf.nix {
          inherit (nixpkgs) lib;
          inherit inputs system;
        }
      );
  ...

and in ./outputs/home-conf.nix

{ system, nixpkgs, nurpkgs, nixpkgsUnstable, home-manager, ... }:

let
  mkHome = username: (
    let
      homeDirectory = "/home/${username}"; # TODO: if darwin then /Users/${username}
      configHome = "${homeDirectory}/.config";

      pkgs = import nixpkgs {
        inherit system;
        config.allowUnfree = true;
        config.android_sdk.accept_license = true;
        config.xdg.configHome = configHome;
        overlays = [
          nurpkgs.overlay
        ] ++ (import ../home/${username}/overlays);
      };

      unstable = import nixpkgsUnstable {
        inherit system;
        config.allowUnfree = true;
        config.android_sdk.accept_license = true;
        config.xdg.configHome = configHome;
      };

      nur = import nurpkgs {
        inherit pkgs;
        nurpkgs = pkgs;
      };

      conf = (
        import ../home/${username}/home.nix {
        inherit nur pkgs unstable username homeDirectory;
        inherit (pkgs) config lib stdenv;
      });
    in
    home-manager.lib.homeManagerConfiguration rec {
      inherit pkgs system username homeDirectory;
      extraSpecialArgs = { inherit unstable; };
      stateVersion = "21.11";
      configuration = conf;
    });
in
{
  jeroen = mkHome "jeroen";
}

So far so good…

But now I need to override my generic home definitions with some host-specific ones. I refer to dotfiles and settings from ../home/${username}/home.nix like this:

  imports = [
    ../users-hm-common.nix # default packages for all users
    ./themes
  ] ++ (import ./programs)
    ++ (import ./services)

where the respective ./programs/default.nix is a list of references to subdirs like ./programs/autorandr/ with corresponding HM-config settings in its default.nix

But since things like autorandr are configured differently depending on host, my idea was to do something like this:

let
  hostDefaultNix = "default-${hostname}.nix";
in
{

  imports = [
    ../users-hm-common.nix # default packages for all users
    ./themes
  ] ++ (import ./programs) ++ (if builtins.pathExists ./programs/${hostDefaultNix} then import ./programs/${hostDefaultNix} else [])
    ++ (import ./services) ++ (if builtins.pathExists ./services/${hostDefaultNix} then import ./services/${hostDefaultNix} else []);

but for that I need to know the hostname that was set by this flake when doing

nixos-rebuild switch --use-remote-sudo --flake .#$(hostname -s)

when I build the HM config with

  nix build .#homeConfigurations.${USER}.activationPackage
  result/activate -b bak

How to achieve this?

BTW: to complete the transition I’d also need to get rid of the hard-coded system value from the main flake, since it should take the value of the host on which it is built. (I’m not considering remote builds for this setup)

3 Likes

Instead of checking the hostname within your configuration, use separate configurations. home-manager --flake will by default check for $USER@$(hostname) then $USER.

8 Likes

It should be explained in the manual: https://nix-community.github.io/home-manager/index.html#ch-nix-flakes

The extra logic around managing user+hostname has gone undocumented though. The scripts for this are easy to read if you need more detail.

3 Likes

@NobbZ @TLATER Thanks a lot, you have very much enlightened me (as always) :smiley:

So (for posterity):

The activation for HM (with the same flake) I now do with just

home-manager switch --flake .

and in outputs/home.conf I have the

userx@hosty = mkHome "userx" "hosty"`

specified, and in the mkHome function I just inherit down hostname so the default/specific logic is handled for ./programs/ etc. as described above.

Happy camper here, very elegant solution now.

4 Likes

Would you mind sharing the repo so I can see exactly what the implementation would look like? I’m still a noob.

I haven’t sanitised the repo to a point where I can make it public, but the gist of it is that the flake output attribute containing your home configuration is homeConfigurations."userx@hosty" = ..., where ... is the home configuration as returned by inputs.home-manager.lib.homeManagerConfiguration { ... }.

The mkHome mentioned above basically wraps the function homeManagerConfiguration in a custom function that automatically sets parameters for it based on definitions in other files in the flake.
This is totally up to you how to implement, the only requirement is that the attribute set returned to the flake output is of the form { user@host = { ... /* home configuration generated by HM's function */ }; ... }

In my case (sorry if it looks over-complicated, it probably is for your use case) it is like below.
The reason I’m doing this is because I want the HM user to be able to independently from the root user define his config, but at the same time use (refer to) the nixpkgs and systemConfig that is defined and enforced by the sys admin (root).
I also wanted to structure the flake with hosts/... and home/... dirs, where I can add hosts and users without touching flake.nix.

This resulted in (HM part only):

In the flake.nix:

    homeConfigurations =
      (import ./home/hm-builder.nix {
        inherit (nixpkgs) lib;
        inherit inputs;
        systemConfigs =
          inputs.self.nixosConfigurations
          // inputs.self.darwinConfigurations;
      })
      .getHMConfigs; # all top level user names (dirs) under ./home will be used and paired with hostnames if the user is defined in the host definition

and in hm-builder.nix:

{ inputs, systemConfigs, lib, ... }:

# Since, by definition, users run *on* a system, we will take the realised system config
# and its current config and packages state as input for  the HM config(s).
# This is even one "better" than using channels in terms of duplication, since it will
# use a common state for pkgs and unstable.

let
  mkHome = username: hostname:
    (
      let
        inherit (systemConfigs."${hostname}") pkgs config;
        inherit (systemConfigs."${hostname}"._module.args) nur nixpkgs-master std;
        inherit (pkgs) system;
        osConfig = config;
        homeDirectory = "${
          if lib.hasInfix "darwin" system then "/Users" else "/home"
        }/${username}";
        # configHome = "${homeDirectory}/.config";
      in
      inputs.home-manager.lib.homeManagerConfiguration {
        inherit pkgs;
        modules = [
          ../home/home.nix
          "${inputs.sops-nix}/modules/home-manager/sops.nix"
          (if builtins.pathExists (./. + "/${username}/overlays") then
            (./. + "/${username}/overlays")
          else
            { })
        ];
        extraSpecialArgs = {
          inherit inputs username std nur nixpkgs-master hostname homeDirectory osConfig;
        };
      }
    );
in
{
  /* getHMConfigs returns all <user>@<host> = <HMConfig> attributes according to the user lists in all machine configs
     That means: mkHome for each username@hostname that satisfies the following conditions
     - has a dir in ./home
     - occurs in <hostname>.config.users.users && isNormalUser in that set
     needs some hocus pocus to restructure ...
  */
  getHMConfigs =
    let
      hmusers = lib.attrsets.attrNames
        (lib.filterAttrs (n: v: v == "directory") (builtins.readDir ./.));
      # hostusers doesn't work well on darwin if you rely on non-nix managed users
      # so we probably need to just get the users from hostdef on darwin
      hostusers = builtins.mapAttrs (n: v: v.config.users.users) systemConfigs;
    in
    lib.attrsets.zipAttrsWith (n: v: builtins.elemAt v 0)
      (builtins.filter (i: i != { }) (lib.lists.flatten (map
        (cfghost:
          map
            (hmusr:
              if (builtins.hasAttr hmusr (builtins.getAttr cfghost hostusers)) then {
                "${hmusr}@${cfghost}" = (mkHome hmusr cfghost);
              } else
                { })
            hmusers)
        (builtins.attrNames hostusers))));
}

and home.nix

{ pkgs, username, homeDirectory # ? pkgs.config.homeDirectory
, hostname, osConfig, lib ? pkgs.lib, ... }:

{
  imports = [
    ./switch.nix # put the switch script (to apply this flake for system and HM config) in the user's ${HOME}/.local/bin
    (./.
      + "/${username}/${hostname}") # much simplified: just enforce that the user@host has indeed a defined default.nix in place
  ];

  # home.packages = [ pkgs.home-manager ];
  programs.home-manager.enable = true;

  home.sessionPath = [ "${homeDirectory}/bin" ]
    ++ (if osConfig ? "homebrew" then
      lib.optionals osConfig.homebrew.enable [ "/opt/homebrew/bin" ]
    else
      [ ]); # add to PATH
  fonts.fontconfig.enable = true;
  home.username = username;
  home.homeDirectory = homeDirectory;
  home.stateVersion = "23.11";
}
1 Like

Thank you for your incredibly detailed and clear explanation. I think I get it now.