Improving a flake.nix config that configures home-manager

Hi everyone,

Nix language newbie here. After some struggle and looking at a lot of places, I was able to create an initial, single flake.nix file with a basic home-manager configuration for myself, that downloads and install a simple flake from github:

{
  description = "Home Manager config";

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

  outputs = { nixpkgs, home-manager, execpermfix, ... }:
    let
      system = "x86_64-linux";
      home = { config, pkgs, system, execpermfix, ... }:
        {
          home.username = "myuser";
          home.homeDirectory = "/home/myuser";
          home.stateVersion = "22.11";
          programs.home-manager.enable = true;
          home.packages = [
            execpermfix.packages.${system}.default
          ];
        };
    in {
      homeConfigurations.myuser = home-manager.lib.homeManagerConfiguration {
        pkgs = nixpkgs.legacyPackages.${system};
        extraSpecialArgs = { inherit nixpkgs execpermfix system; };
        modules = [ home ];
      };
    };
}

It’s apparently working, but I have some questions, mostly related to the language:

  • Is there a way to avoid repeating the execpermfix reference? I put it in the inputs, but then I have to bring it over in 3 places.
  • Is there a way to make this system-agnostic? I wonder if I could just put the same file in a raspberry pi and get the same environment.
  • Is there a way to explicitly define home inside modules instead of creating a variable and referencing it? I think in most places that sequence has been kept in a separate file and imported - I’m wondering if we could do the exact opposite.

So to answer the questions 1 and 3, sure. You just need to repeat execpermfix in the input ando output function, the other ones can be removed. extraSpecialArgs is only useful in you load a module from another file. Here since the variable is defined above in the file (or closure to be more precise) you can just refer to it without using any variable. And for 3, yes, just do it :stuck_out_tongue: This way you can simplify the above file like this (not tested):

{
  description = "Home Manager config";

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

  outputs = { nixpkgs, home-manager, execpermfix, ... }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};      
    in {
      homeConfigurations.myuser = home-manager.lib.homeManagerConfiguration {
        modules = [
          {
            home.username = "myuser";
            home.homeDirectory = "/home/myuser";
            home.stateVersion = "22.11";
            programs.home-manager.enable = true;
            home.packages = [
              execpermfix.packages.${system}.default
            ];
          }
        ];
      };
    };
}

Regarding the second question, you cannot directly make the homeConfiguration system-agnostic as pointed out in this issue. However, you can ‘fake it’ by creating a loop to create an entry myuser-${system} for instance. This flake makes it particularly easy to do by providing a list of systems. So your code would become like this (not tested, I hope I don’t have too much typos):

{
  description = "Home Manager config";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    execpermfix.url = "github:lpenz/execpermfix";
  };

  outputs = { nixpkgs, home-manager, execpermfix, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in
        {
          homeConfigurations."myuser-${system}" = home-manager.lib.homeManagerConfiguration {
            modules = [
              {
                home.username = "myuser";
                home.homeDirectory = "/home/myuser";
                home.stateVersion = "22.11";
                programs.home-manager.enable = true;
                home.packages = [
                  execpermfix.packages.${system}.default
                ];
              }
            ];
          };
        });
}

and you should be able to switch using something like

$ home-manager switch --flake .#myuser-x86_64-linux

Until the above issue is resolved I don’t think we can get a much better solution (at most you can also certainly write .#x86_64-linux.myuser if you prefer a doc by modifying accordingly homeConfigurations.${system}.myuser). Note that I can’t test this right now but I see no reasons for a failure except minor typos.

This is awesome, thank you for being so thorough

Since recently I’m happily using this “hack” (which may be over-engineered a bit but the objective is to be able to let other users on my systems manage their own home configs (later) without admin rights):

In the flake:

    homeConfigurations = (
      import ./home/hm-builder.nix {
        inherit (inputs) nixpkgs nurpkgs nixpkgsUnstable home-manager;
        inherit (nixpkgs) lib;
        inherit (home-manager);
        # TODO:See this: https://discourse.nixos.org/t/nixos-custom-module-configuration-with-flakes-and-home-manager/17360/5
        # This means we don't need this hocus pocus? Or is this only valid if HM is used as a NixOS module, which we don't do
        # to support users independently updating their own HM config (without root access)
        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

hm-builder:

    homeConfigurations = (
      import ./home/hm-builder.nix {
        inherit (inputs) nixpkgs nurpkgs nixpkgsUnstable home-manager;
        inherit (nixpkgs) lib;
        inherit (home-manager);
        # TODO:See this: https://discourse.nixos.org/t/nixos-custom-module-configuration-with-flakes-and-home-manager/17360/5
        # This means we don't need this hocus pocus? Or is this only valid if HM is used as a NixOS module, which we don't do
        # to support users independently updating their own HM config (without root access)
        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

home.nix in this is not a per-user file, but more of a common “library” that scans all configs in the home/<user>/<hostname> directory and builds the user’s config from that. The reason to do this is that a user can link common configs between different <hostname> dirs as necessary, as well as using host-specific settings simply by having a file defining these in the correct place.

{ pkgs
, unstable
, username
, homeDirectory /* ? pkgs.config.homeDirectory */
, hostname
, lib ? pkgs.lib
, ... }:

let

  inherit (import ../util.nix { inherit lib; }) listFilesRecFollow;

  # simply find the first (most specific) match for the config tree
  cfgsrcroot = with builtins; base: host: user:
    head (filter (p: pathExists p) [
        (base + "/${user}/${host}")
        # TODO: handle host profile first (before default)
        # These should probably be incremental? I.e. use mkDefault and be overriden by host-specific attributes?
        # In fact, we could also do this for the user default, which is then a common config, and we only symlink and/or add
        # specific configs for overriding or adding it. This avoids making a large number of symlinks, at the slight cost of some transparency
        (base + "/${user}/default")
      ]);
in
{
  imports = [
    ./switch.nix # put the switch script (to apply this flake for system and HM config) in the user's ${HOME}/.local/bin
  ] ++
    # just "linearly" import all default.nix under the config dir.
    # This implies that default.nix may not import another default.nix in the same config root
    /* lib.debug.traceValSeq */ (with builtins; (filter (f: (baseNameOf f) == "default.nix" ) (listFilesRecFollow (cfgsrcroot ./. hostname username))));
    # ++ (map (key: lib.getAttr key pkgs.extraModules) (lib.attrNames pkgs.extraModules)); # include extraModules used in overlay (for vscode-server)

  home.sessionPath = [ "${homeDirectory}/bin" ]; # add to PATH

  home.sessionVariables = {
    NIX_PATH = "nixpkgs=${pkgs.outPath}:unstable=${unstable.outPath}";
  };

  fonts.fontconfig.enable = true;

  home.username = username;
  home.homeDirectory = homeDirectory;
  home.stateVersion = "21.11";
}