Switching non-NixOS Home Manager to flakes

If you are a Nix user, you may have heard of Home Manager as the recommended way to manage your user environment. You may have also heard of flakes, the upcoming new way of managing dependencies and packages.

You can combine the two, and use Home Manager with flakes. Frequently, this is done by including a Home Manager configuration in the (also flakified) system configuration. Home Manager can, however, be also used on operating systems other than NixOS, in which case the NixOS system configuration is not there. Fortunately, Home Manager can also use flakes entirely on user level. Let's assume, then, that we have an existing Home Manager installation, not currently based on flakes, and want to switch it over to flakes.

Okay, but first, what are all those things?

With your usual Nix installation, each user can manage their environment with the nix-env tool. When the user installs or uninstalls things with nix-env, a new generation of the user's environment is created with these changes, providing a simple way of managing packages that resembles the classic package managers found on other operating systems. The nix-env approach does have some problems, however, broadly relating to it being imperative in a system focused on being declarative. To address these problems Home Manager was created. With Home Manager, the user can write a configuration for their environment, similar to a NixOS system configuration. This Home Manager configuration specifies the packages and home directory configuration of a single user.

Flakes, on the other hand, are an upcoming feature of Nix itself. Flakes are intended to introduce a new way of declaring dependencies in Nix packages, improving reproducibility and, from a practical standpoint, proving a better alternative to the current approach of using channels and ad-hoc pinned dependencies. If you have ever used Niv, you can think of flakes as that, but better integrated into the Nix tool itself.

The experimental Nix

Flakes are a feature of the upcoming 2.4 version of Nix. The standard Nix, even in the unstable Nixpkgs channels, is Nix 2.3, but there is also pkgs.nixUnstable, which points to 2.4 builds.

Both nix and nixUnstable come with a binary called nix, and so collide. If we want to experiment with flaky Nix without overriding stable Nix, we can make a wrapper (courtesy of the NixOS Wiki). An easy way to do this is to use our existing home.nix, the Home Manager configuration file:

{ pkgs, config, lib, ... }:
{
  home.packages = [
    (pkgs.writeScriptBin "nixFlakes" ''
      exec ${pkgs.nixUnstable}/bin/nix --experimental-features "nix-command flakes" "$@"
    '')
  ];
}
home.nix, providing us with unstable Nix wrapped in a script called nixFlakes

Note that even with Nix 2.4, we need to have the flakes experimental feature explicitly enabled. This can be done by adding experimental-features = nix-command flakes to our nix.conf (generally found in ~/.config/nix), but if we do this, regular Nix 2.3 will complain about not knowing what experimental-features is. As an alternative, we apply these settings on the command line in the wrapper.

After switching to our new configuration we will have a nixFlakes in our PATH, and so will be able to use it from the command line.

Starting a flake.nix file

Let us assume that our prior Home Manager configuration was kept in a Git repository somewhere in our home directory, with ~/.config/nixpkgs/home.nix symlinked to a relevant file within the repository.

To create a basic flake.nix, we can use execute nixFlakes flake init within our configuration repo. This command can be used to create flakes out of templates, and if we don't explicitly ask for a specific template, it uses the default one, which looks something like this:

{
  description = "A very basic flake";

  outputs = { self, nixpkgs }: {
    packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;

    defaultPackage.x86_64-linux = self.packages.x86_64-linux.hello;
  };
}
flake.nix, using the templates#defaultTemplate template

As we can see, a flake file is simply an attrset. The two important attributes in it are inputs (absent from template), and outputs. inputs defines the dependencies of our flake, and outputs defines the things our flake offers, that other flakes and tools can consume.

The template does not have explicit inputs; nixpkgs is resolved using our Nix flake registry (a locally stored map from flake names to flake URLs, that Nix populates from a global registry on the Internet). The template does have two outputs: packages, which defines one named package, and defaultPackage, which marks the one package as the flake's default.

Flakes can define various outputs, which are then used by various tools—defaultPackage, for instance, is what Nix would look at if we asked it to install this flake into our profile, without explicitly asking for any specific package from inside the flake. Home Manager, on the other hand, uses the homeConfigurations output.

A basic Home Manager configuration

homeConfigurations should be an attrset, mapping names to Home Manager configurations. The names are either usernames, or in the format of "username@hostname".

For inputs to our flake, we will want to include Home Manager. This does mean that our flake is technically independent of our current Home Manager installation. In fact, we could bootstrap Home Manager this way without having Home Manager installed in the first place! Having Home Manager already installed is also okay—our prior Home Manager generations will not be reset.

Assuming our username is someuser and our hostname somecomputer, we can come up with a basic flake like this:

{
  description = "Home Manager configurations";

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

  outputs = { self, nixpkgs, homeManager }: {
    homeConfigurations = {
      "someuser@somecomputer" = homeManager.lib.homeManagerConfiguration {
        configuration = {pkgs, ...}: {
          programs.home-manager.enable = true;
          home.packages = [ pkgs.hello ];
        };

        system = "x86_64-linux";
        homeDirectory = "/home/someuser";
        username = "someuser";
        stateVersion = "21.05";
      };
    };
  };
}
flake.nix, showing a single home manager configuration, for user someuser on host somecomputer

As we can see, "someuser@somecomputer" is mapped to a call to homeManager.lib.homeManagerConfiguration. We are calling a function exported by Home Manager's flake that will create a Home Manager configuration, out of the attrset that we pass to it.

configuration should be the most familiar bit of said attrset. This is essentially the same function as the one we would normally define in our home.nix. In fact, instead of defining our configuration inline, in the flake, we could do configuration = import ./home.nix;, which is the practical way to move our old configuration to flakes. The one caveat is that some of the home settings that might have previously been in our home.nix file are now in the flake—home.stateVersion, home.username, and home.homeDirectory now live adjacent to, instead of in configuration.

Switching to our home-manager configuration

We can build our home-manager profile with just our flakey Nix. This can be done by building the activationPackage attribute of a particular configuration. In this specific case, this means invoking nixFlakes build '.#homeConfigurations."someuser@somecomputer".activationPackage'. After the build completes and we have our result folder, switching to the newly built configuration is simple: invoke result/activate from the command line.

We should now be in our new profile. It is, of course, possible something has gone terribly wrong and our new profile is broken in some way. Fortunately, we are using Nix, so we can perform a rollback. Invoking home-manager generations should give us a list of generations, and paths to them. We can simply take the next-to-last path, append activate it to it, and run that, which should reactivate our previous generation, rolling us back. If home-manager is absent from our PATH, we can also go to /nix/var/nix/profiles/per-user/someuser, where we can find a number of home-manager symbolic links. Again, if we follow the next-to-last link, we can use the activate script within to restore that generation.

Using the home-manager tool with flakes

We could just repeat the nix build command whenever we want to switch to a new Home Manager generation, but that is a bit awkward. Fortunately, the home-manager tool, besides working with the usual ~/.config/nixpkgs/home.nix, can also work with ~/.config/nixpkgs/flake.nix.

Unfortunately, in this situation, we cannot simply create a symlink from ~/.config/nixpkgs/flake.nix to wherever our configuration repository is, as we would experience problems due to restricted mode; Nix will refuse to poke around the symlink target unless --impure is passed. One interesting—if seemingly hacky—thing we could do, however, is create another flake.

{
  description = "Local Home Manager configurations";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    homeManagerConfig = {
      url = "path:/home/someuser/stuff/home-manager-config";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, homeManagerConfig, ... }: {
    inherit (homeManagerConfig) homeConfigurations;
  };
}

~/.config/nixpkgs/flake.nix, taking another flake in the local filesystem as input.

Yes, we can use local filesystem paths as flake inputs. Of note is the fact that we could use network URLs here as well, and let Nix handle pulling our configuration from elsewhere, if we happen to have it on an easily-reachable server.

We have also declared nixpkgs as a separate input, and pinned homeManagerConfig's nixpkgs input to our nixpkgs input. This is one way to achieve a workflow similar to that we would have previously used with channels: our nixpkgs input will stay pinned where it is until it is updated. This can be useful if, say, we are changing something in our home-manager configuration and want to rebuild without necessarily downloading a lot of fresh updates.

We can update individual inputs by using --update-input: nix flake lock --update-input nixpkgs ~/.config/nixpkgs. home-manager switch and home-manager build should work with the configuration specified in ~/.config/nixpkgs/flake.nix, although we might have to make unstable Nix our main nix binary, rather than have it behind nixFlakes.

If we do not add a flake.nix to our ~/.config/nixpkgs, we can instead explicitly point home-manager at a flake: home-manager switch --flake path:/home/someuser/stuff/home-manager-config.

Non-Nixpkgs packages

One of the reasons to get into flakes is for their ability to manage inputs other than mainline Nixpkgs. With stable Nix, the options here are usually either manually managing fetch* functions that pull in some repository, or adding Niv, and letting it manage pinning the added packages. With flakes, however, this is handled natively by Nix itself.

There are two types of repositories we can encounter in the wild: ones which have their own flake.nix file, and ones which do not. Fortunately, Nix can handle even the non-flake inputs—we simply have to indicate that the input is a non-flake:

{
  description = "Home manager configurations";

  inputs = {
    nixpkgs.url = "flake:nixpkgs";
    homeManager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    nixGL = {
      url = "github:guibou/nixGL";
      flake = false;
    };
  };


  outputs = { self, nixpkgs, homeManager, nixGL }: {
    homeConfigurations = {
      "someuser@somecomputer" = homeManager.lib.homeManagerConfiguration {
        configuration = {pkgs, nixGL, ...}: {
          programs.home-manager.enable = true;
          home.packages = [
            pkgs.hello
            (import nixGL { inherit pkgs; }).nixGLIntel
          ];
        };

        extraSpecialArgs = {
          inherit nixGL;
        };
        system = "x86_64-linux";
        homeDirectory = "/home/someuser";
        username = "someuser";
        stateVersion = "21.05";
      };
    };
  };
}
Our previous flake.nix, now with an input that adds nixGL.

The main thing to point out here is extraSpecialArgs—this is how we pass extra arguments to the configuration function, outside of the pkgs which Home Manager will supply by itself. Inside configuration itself, we import nixGL just like we would if we had used Niv or called fetchTarball or fetchFromGitHub ourselves. The details of how to import a given non-flake input, and what to do with it, will vary, but generally non-flake instructions should be adaptable to use with flakes.

Flakes, on the other hand, are more structured. Conventional flakes will generally provide packages, defaultPackage, and overlay as outputs. As such, we will have two options for installing packages: either by grabbing the package from the two packages outputs directly, or by applying the flake's overlay onto our imported Nixpkgs.

The former case is pretty simple:

{
  description = "Home manager configurations";

  inputs = {
    nixpkgs.url = "flake:nixpkgs";
    homeManager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    deploy-rs.url = "github:serokell/deploy-rs";
  };


  outputs = { self, nixpkgs, homeManager, deploy-rs }: {
    homeConfigurations = {
      "someuser@somecomputer" = homeManager.lib.homeManagerConfiguration {
        configuration = {pkgs, deploy-rs, ...}: {
          programs.home-manager.enable = true;
          home.packages = [
            pkgs.hello
            deploy-rs.defaultPackage.x86_64-linux
          ];
        };

        extraSpecialArgs = {
          inherit deploy-rs;
        };
        system = "x86_64-linux";
        homeDirectory = "/home/someuser";
        username = "someuser";
        stateVersion = "21.05";
      };
    };
  };
}
flake.nix, with a home-manager configuration that includes the deploy-rs tool.

Doing the same with an overlay is slightly more complicated:

{
  description = "Home manager configurations";

  inputs = {
    nixpkgs.url = "flake:nixpkgs";
    homeManager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    deploy-rs.url = "github:serokell/deploy-rs";

  };


  outputs = { self, nixpkgs, homeManager, deploy-rs }: {
    homeConfigurations = {
      "someuser@somecomputer" = homeManager.lib.homeManagerConfiguration {
        configuration = {pkgs, ...}: {
          programs.home-manager.enable = true;
          home.packages = [
            pkgs.hello
            pkgs.deploy-rs.deploy-rs
          ];
        };

        pkgs = import nixpkgs {
          system = "x86_64-linux";
          overlays = [ deploy-rs.overlay ];
        };
        system = "x86_64-linux";
        homeDirectory = "/home/someuser";
        username = "someuser";
        stateVersion = "21.05";
      };
    };
  };
}
flake.nix, overlaying deploy-rs on home-manager's Nixpkgs.

While we could use nixpkgs.overlay inside the Home Manager configuration function, we can also apply the overlay in the flake, by defining the pkgs attribute (in fact, we can do both at the same time). The only caveat is that we will also need to supply the system when importing nixpkgs in the flake.

The deploy-rs tool being under pkgs.deploy-rs.deploy-rs is a consequence of the deploy-rs overlay being structured like that, and not a specific quirk of flake overlay use.

Further reading

Hopefully these examples give a broad overview of how Nix flakes can be used in practice, but to augment them, here is some extra stuff to read: