Flakes: NixOS and Home Manager migration

Table of contents

Introduction

I have recently migrated my entire NixOS and Home Manager (HM) configuration — including programs, services, dotfiles, etc — over to the new kid on the block: Nix flakes.

It was not as difficult as I thought it would be but there were a lot of things I had to figure out on my own or by asking more experienced folks on the NixOS matrix channel.

So let me tell you the important bits of this migration story in this short blog post ;)

How it started: NixOS

I initially had my NixOS configuration under the system directory and my Home Manager configuration under home, as you can see in the Github repo. So I decided to do the migration step by step, starting with the former.

I created a flake.nix file under /etc/nixos with the following content.

{
  description = "NixOS configuration";

  inputs.nixpkgs.url = "nixpkgs/nixos-unstable";

  outputs = inputs @ { self, nixpkgs }:
    let system = "x86_64-linux"; in {
      nixosConfigurations = {
        tongfang-amd = nixpkgs.lib.nixosSystem {
          inherit system;
          specialArgs = { inherit inputs; };
          modules = [
            ./machine/tongfang-amd
            ./configuration.nix
          ];
        };
      };
    };
}

I could then build my system using this flake! Easy, right?

$ sudo nixos-rebuild switch --flake .#tongfang-amd

By default, nixos-rebuild expects the configuration under /etc/nixos. However, we can specify a different directory, as shown below.

$ sudo nixos-rebuild switch --flake '/home/gvolpe/workspace/nix-config#tongfang-amd'

It also turns out this flake can be built via nix build.

$ nix build .#nixosConfigurations.tongfang-amd.config.system.build.toplevel
$ sudo result/bin/switch-to-configuration switch

This means we can switch the system configuration from any directory by using either command!

That’s handy if you keep all your configurations in a single directory and these are tracked by a version control system (VCS) such as git.

Next step: Home Manager

This was not as straightforward, as my HM configuration is a bit complex, but doable nonetheless. In the same way, I started creating a flake.nix under the home directory but I quickly realized having two different flakes for a single machine is not ideal.

However, since both the NixOS and HM configurations can be built from anywhere (no need to be under /etc/nixos and $HOME/.config/nixpkgs, respectively), I went with a single flake.nix where both the NixOS and HM configurations live (importing modules to make it more readable, of course).

A nice property of having a single flake, is that we can find out all the pinned versions by looking at the flake.lock file. We also get to manage everything from a single nix flake command.

One flake to rule them all!

So here’s the only flake.nix that contains both NixOS and HM configurations.

{
  description = "Home Manager (dotfiles) and NixOS configurations";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-unstable";

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

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

    tex2nix = {
      url = github:Mic92/tex2nix/4b17bc0;
      inputs.utils.follows = "nixpkgs";
    };
  };

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

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

      devShell.${system} = (
        import ./outputs/installation.nix {
          inherit system nixpkgs;
        }
      );
    };
}

It consists of a set of inputs and a set of outputs.

To make things more readable, I moved the corresponding configurations to the outputs directory. So the NixOS configuration now lives under outputs/nixos-conf.nix, and so on.

You can skip this: installation shell

There is also a devShell with two packages that I use for a fresh installation for custom build script, but that’s quite personal so feel free to skip this part.

{ system, nixpkgs }:

let
  pkgs = nixpkgs.legacyPackages.${system};
in
pkgs.mkShell {
  name = "installation-shell";
  buildInputs = with pkgs; [ wget s-tar ];
}

What’s great is that I can enter this shell without even checking out the project.

$ nix develop github:gvolpe/nix-config

Home Manager flake output

The homeConfigurations is a custom flake output, which is not recognized by Nix flakes, so when we try to display it, it shows “unknown” as a description but this will probably be supported in the future.

$ nix flake show | rg homeConfigurations
├───homeConfigurations: unknown

So what’s in the outputs/home-conf.nix? A basic HM configuration might look as follows.

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

let
  username = "gvolpe";
  homeDirectory = "/home/${username}";
  configHome = "${homeDirectory}/.config";

  pkgs = import nixpkgs {
    inherit system;
    config.allowUnfree = true;
    config.xdg.configHome = configHome;
    overlays = [ nurpkgs.overlay ];
  };

  nur = import nurpkgs {
    inherit pkgs;
    nurpkgs = pkgs;
  };
in
{
  main = home-manager.lib.homeManagerConfiguration rec {
    inherit pkgs system username homeDirectory;

    stateVersion = "21.03";
    configuration = import ./home.nix {
      inherit nur pkgs;
      inherit (pkgs) config lib stdenv;
    };
  };
}

Where ./home.nix is your usual Home Manager configuration. From there, it will more likely get more complicated, as you will always tweak one piece or another.

Mine looks very similar, except I have a few more overlays and two home configurations for two different displays. You can look at it directly on the Github repo to avoid repetition in here.

The most confusing part for me was the order of evaluation in Nix (which seems to have changed?). So the overlays I had defined in home.nix were no longer being picked up and I had to define them at the top level.

Also, I had to set the config.xdg.configHome manually when importing nixpkgs, which was before set in home.nix via the xdg.enable = true; attribute. I still haven’t figured out the right way to do this so I’m setting it myself, but if you do know, I’d appreciate if you let me know in the comments.

Switching configurations

Now to apply a new configuration I was previously running home-manager switch. However, now I prefer to directly build the flake and run the activation script from its result.

$ nix build .#homeConfigurations.gvolpe-hdmi.activationPackage
$ result/activate

It is more verbose, though, so it is a good idea to have a script for this.

Flake outputs

These are all the flake outputs I currently have (you can query the repo directly).

$ nix flake show github:gvolpe/nix-config
github:gvolpe/nix-config/962a766ab98217aba249f2614592bd5513a267a9
├───devShell
│   └───x86_64-linux: development environment 'installation-shelbash'
├───homeConfigurations: unknown
└───nixosConfigurations
    ├───dell-xps: NixOS configuration
    └───tongfang-amd: NixOS configuration

So far, I’m liking the flakes experience, and I only have words of gratitude for the thousands of contributors who have taken the Nix ecosystem where it is today, and it keeps on getting closer to perfection every day!

Conclusion

I can only say one thing about my Nix configuration affairs: it ain’t over yet. I’m still figuring things out and learning new stuff on a daily basis, so I’m sure there will be plenty of changes in the near future, specially considering that flakes are still marked as experimental.

Anyway, that’s all I have to say. Thanks for stopping by and have a look at my Nix configuration files, perhaps something in there helps you get that missing piece in yours :)

Cheers, Gabriel.