website/content/posts/2023-03-05-combining-traditional-dotfiles-and-nixos-configurations-with-nix-flakes/index.adoc

15 KiB
Raw Blame History


title: "Combining traditional dotfiles and NixOS configurations with Nix flakes" publishdate: 2023-03-05T00:00:00+00:00

tags: - Nix ---

Combining traditional dotfiles and NixOS configurations with Nix flakes

Gabriel Arazas <foodogsquared@foodogsquared.one> 2023-03-05 :foodogsquared-dotfiles-rev: 5862afecaf045175891550c1020c09cd2dbb32ed

If youre in a similar situation like mine where…

  • You still use other distributions that are not NixOS alongside NixOS.

  • You want your most important parts of your configuration (dotfiles) to be separate from your NixOS configurations for reasons (e.g., easier to manage, you just find modularization satisfying).

  • You want a way to integrate them together nicely.

then youre in luck because Ill share a nice way to do exactly those.

In my case, I still have github:foo-dogsquared/dotfiles[my "traditional" dotfiles in a separate repository] instead of combining them under a monorepo. Fortunately in NixOS, there are a variety of ways to combine them but in this post, Im focusing on doing this with Nix flakes which is a newfangled way to manage dependencies in a Nix environment.

This is also a nice use case for those who are still in edge using NixOS which has the impression of requiring you to fully commit the configuration only with NixOS which isnt always the case. I hope to show that it is possible to meet the middle ground.

Warning

As of Nix v2.14, Nix flakes are still considered experimental so treat it as if disruptive changes are the norm (e.g., nix flake subcommands and options may change). However, this feature has been in place for at least years now which gained some parts of the Nix ecosystem to use it (e.g., github:NixOS/nixpkgs[nixpkgs], github:nix-community/home-manager[home-manager], github:divnix/digga[digga]).

I chose to feature flakes since I have used it for the past year. Also, I just think its better compared to the traditional way which Ive also delved into more details at Why flakes anyways?.

Prerequisites

For this post, Im assuming that you have already set your home-manager and NixOS configurations with Nix flakes. This means you would have enabled flakes and the new Nix CLI (i.e., experimental-features = nix-command flakes in nix.conf).

Specifically, well be assuming that you have something like the following configuration where you have one NixOS configuration (i.e., nixosConfigurations.desktop) and a home-manager configuration (i.e., homeConfigurations.foodogsquared).

Note

You dont need the exact same setup, it is just for us to get on the same page.

Unresolved directive in <stdin> - include::./assets/initial-flake.nix[]

If youre not familiar with flakes and only looking for an example, I recommend to look into github:Misterio77/nix-starter-configs[this flake template by Misterio77]. I recommend especially that it has a minimal and standard NixOS and home-manager setup with flakes and it gives you starting points for using flakes. Pretty nifty.

Adding the dotfiles in the flake

Flakes essentially takes in inputs and exports outputs which we usually define in a file named flake.nix at the project root. These inputs are commonly other flakes such as nixpkgs and home-manager.

flake.nix
Unresolved directive in <stdin> - include::./assets/initial-flake.nix[lines=2..11, indent=0]

However, flake inputs can also be non-flakes which is what enables me to include my "traditional" dotfiles.

flake.nix
inputs = {
  # ...

  dotfiles = {
    url = "github:foo-dogsquared/dotfiles";
    flake = false;
  };
}

Integrating with home-manager and NixOS

Now that the dotfiles is included in the flake, it is just a matter of using it. In this case, I want to include part of my dotfiles such as my github:foo-dogsquared/dotfiles[Wezterm, rev={foodogsquared-dotfiles-rev}, path=wezterm], github:foo-dogsquared/dotfiles[Neovim, rev={foodogsquared-dotfiles-rev}, path=nvim], and github:foo-dogsquared/dotfiles[Doom Emacs, rev={foodogsquared-dotfiles-rev}, path=emacs] configuration alongside the home-manager environment.

First, well have to include the dotfiles flake input as part of the home-manager profile. In the case of enabling it in the home-manager configurations, we have to pass it through the extraSpecialArgs attribute from the function that creates a home-manager profile (i.e., home-manager.lib.homeManagerConfiguration).

flake.nix
{
  # ...

  outputs = { nixpkgs, home-manager, ... }@inputs: {
    homeConfigurations.foodogsquared = home-manager.lib.homeManagerConfiguration {
      # The home-manager configuration.
      modules = [ ./home.nix ];

      extraSpecialArgs = {
        inherit (inputs) dotfiles;
      };
    };
  };
}

Now, we have to do what we want to do: include part of the dotfiles in the home directory. Fortunately, this part is already handled for us since home-manager has several options to include files inside of the home directory (i.e., home.file, xdg.configFile). The following code listing is one way to do what I want to do.

Note

Dont forget to add the additional dotfiles attribute we passed in extraSpecialArgs to make this work.

home.nix
{ config, options, lib, pkgs, dotfiles, ... }:

{
  # The rest of your home-manager configuration.
  # ...

  # Putting the dotfiles in their rightful place.
  xdg.configFile = {
    doom.source = "${dotfiles}/emacs";
    wezterm.source = "${dotfiles}/wezterm";
    nvim.source = "${dotfiles}/nvim";
  };
}
What exactly is inputs.dotfiles?

inputs.dotfiles is one of the inputs from our flake. Each input is an attribute set containing some metadata including the path of the input on the store directory.

{
  lastModified = 1668655246;
  lastModifiedDate = "20221117032046";
  narHash = "sha256-V7Js99Pyg0UvP6RNg3Isv3MgCKZO9cqVxiiVa9ZZiFU=";
  outPath = "/nix/store/lgflzj8grdxpyp1inil6c96253c06b24-source";
  rev = "5862afecaf045175891550c1020c09cd2dbb32ed";
  shortRev = "5862afe";
}

While we can easily create a string value of the output path with inputs.dotfiles.outPath, the flake input will simply evaluate to the output path when coerced into a string. In other words, "${inputs.dotfiles.outPath}" and "${inputs.dotfiles}" are equivalent. This is why the code from home.nix works just fine.

If you want to be explicit about your intent of dotfiles as a path to the dotfiles, you could also just pass dotfiles to the extraSpecialArgs like the following.

{
  extraSpecialArgs = {
    dotfiles = inputs.dotfiles.outPath;
    # or...
    # dotfiles = ./path/to/my/dotfiles;
  };
}

If you have a NixOS configuration that makes use of home-manager, dont forget to set home-manager.extraSpecialArgs somewhere in it.

flake.nix
{
  # ...

  outputs = { nixpkgs, home-manager, ... }@inputs: {
    nixosConfigurations.desktop = nixpkgs.lib.nixosSystem {
      modules = [
        # The NixOS configuration.
        ./configuration.nix

        # Make sure to import the home-manager NixOS module somewhere.
        home-manager.nixosModules.home-manager

        {
          # Make sure to import the home-manager configuration somewhere.
          home-manager.users.foodogsquared = { ... }:
            imports = [ ./home.nix ];
          };

          # Make sure to not forget the extra arguments.
          home-manager.extraSpecialArgs = {
            inherit (inputs) dotfiles;
          };
        }
      ];
    };
  };
}

You can also make use of this other than putting part of the dotfiles in the home directory. For instance, NixOS has options to put files in /etc/ with environment.etc which is where system-wide configurations usually live if you have part of the dotfiles that are meant to be system-wide.

Say, you have an i3 window manager configuration in your traditional dotfiles that you want to be used system-wide (for whatever reason).

Similarly to setting it in the home-manager configuration, you have to pass the flake input through a similar attribute, specialArgs, in the function that creates a NixOS configuration (i.e., inputs.nixpkgs.lib.nixosSystem).

flake.nix
{
  # ...

  outputs = { nixpkgs, home-manager, ... }@inputs: {
    # ...

    nixosConfigurations.desktop = nixpkgs.lib.nixosSystem {
      # ...

      specialArgs = {
        inherit (inputs) dotfiles;
      };
    };
  };
}

Then you can use it in your NixOS configuration file somewhere.

Note

Again, take note of the dotfiles as one of the function attribute in the following code.

configuration.nix
{ config, lib, pkgs, dotfiles, ... }:

{
  # ...

  # System-wide i3 configuration from our dotfiles...
  # HOORAH!
  environment.etc.i3.source = "${dotfiles}/i3";
}

Workflow and its caveats

Once you got it running, it is just a matter of managing flake inputs.

For example, to update your dotfiles to its latest revision, you can run the following command.

nix flake lock --update-input dotfiles

You could also just update the dotfiles along the rest of the inputs with…

nix flake update
Tip

You could also add --commit-lock-file flag to the above commands to automatically commit changes to the lockfile which is handy if you want to track your lockfile properly.

There are caveats to this, of course, such as the "What if?"-situation of you wanting only the rest of the flake inputs except your dotfiles (for reasons) to be updated next time you run nix flake update. In this case, you have to change the dotfile flake URL to github:foo-dogsquared/dotfiles/$REVHASH to lock it in. By the time it is ready to update, change the URL again then run nix flake lock --update-input dotfiles.

Overall, this depends on how much your dotfiles is integrated with the rest of the configuration and how much your dotfiles interacts with outside of it. If your "traditional" dotfiles is ever-changing and conflicts with the systems youre interacting (e.g., using NixOS unstable branch and Debian stable where the version between packages may largely differ and more likely to break), it may be time to do something about it such as creating branches for each of them.

In my experience, the potential problems with this setup isnt that much that of a problem since my traditional dotfiles barely changes nowadays. In fact, my NixOS configurations change more often. If theres a change that is coming from my dotfiles, I just push the changes to the (dotfiles) remote repo and update my NixOS configuration with the update.

Appendix A: Why flakes anyways?

It is previously stated that it is possible to do what we want to do other than flakes. Out of all solutions, I chose flakes since it is better than the traditional way.

To show it clearly, well first show how to do it with the classical way which uses a fetcher function. nixpkgs has a lot of fetchers including one for GitHub, GitLab, Sourcehut, and even just any Git repo. But in this case, well use the appropriate fetcher fetchFromGitHub.

Note

Most fetchers require a hash of the thing to be stored in advance. You can refer to the appropriate chapter on how to get hashes from nixpkgs manual for details.

home.nix, this time with fetchers instead of flakes
{ config, lib, pkgs, ... }:

let
  dotfiles = pkgs.fetchFromGitHub {
    owner = "foo-dogsquared";
    repo = "dotfiles";
    rev = "5862afecaf045175891550c1020c09cd2dbb32ed";
    hash = "sha256-V7Js99Pyg0UvP6RNg3Isv3MgCKZO9cqVxiiVa9ZZiFU=";
  };
in
{
  # ...

  # Putting the dotfiles in their rightful place.
  xdg.configFile = {
    doom.source = "${dotfiles}/emacs";
    wezterm.source = "${dotfiles}/wezterm";
    nvim.source = "${dotfiles}/nvim";
  };
}
What is dotfiles this time?

As previously stated from What exactly is inputs.dotfiles?, dotfiles from home.nix is a flake input which is an attribute set that evaluates into the output path when coerced into a string. This time, fetchFromGitHub is a function that returns a derivation containing build instructions for downloading the GitHub repo.

Pretty-printed derivation of my dotfiles with nix show-derivation $DRV
Unresolved directive in <stdin> - include::./assets/github-foo-dogsquared-dotfiles-derivation.drv[]

Derivations also evaluate into the output path when coerced into a string. This is why the code for setting the dotfiles into the home directory still works unchanged.

As you might have already guessed, this has the disadvantage of manually updating the tarballs alongside the flake environment (e.g., NixOS, home-manager). For example, if you want to update the dotfiles, youll have to go through the process of updating the URL (if applicable) and then updating the hash. Pretty tedious to deal with.

By including the dotfiles as one of the flake inputs, youre also eliminating this tedious process of manually updating. All you have to do is nix flake update as mentioned from Workflow and its caveats and youre done!

Not to mention, tools such as nixos-rebuild switch and home-manager switch also supports updating with flakes at recent releases.

Ways to upgrade your flake-enabled home-manager/NixOS environments
home-manager --flake .#foodogsquared switch
nixos-rebuild --flake .#desktop switch
Introduction to nurl

nixpkgs has a lot of fetchers to choose from with appropriate fetchers call for appropriate situations: fetchgit for Git repos, fetchFromGitHub for Git repos on GitHub, and so forth. An additional benefit for using the appropriate fetcher is typically easier to use.

Luckily, there is a Nix tool called github:nix-community/nurl[nurl] that easily generates Nix code with the appropriate fetchers for different forges.

Heres an example usage to easily generate the most appropriate code for fetching my "traditional" dotfiles from my GitHub repo.

nurl https://github.com/foo-dogsquared/dotfiles

It should generate the similar output to the following listing.

fetchFromGitHub {
  owner = "foo-dogsquared";
  repo = "dotfiles";
  rev = "5862afecaf045175891550c1020c09cd2dbb32ed";
  hash = "sha256-V7Js99Pyg0UvP6RNg3Isv3MgCKZO9cqVxiiVa9ZZiFU=";
}

Very convenient tool!