tvl-depot/blog/content/english/lets-learn-nix-dotfiles.md
William Carroll 862c695900 Work on "Let's Learn Nix: Dotfiles" blog post
It has been awhile since I have written a tutorial. I have spent 2-3 hours
working on this post, but I think I need to spend another 2-3 hours before I
publish it.

I expect to be able to write these posts faster as I practice.

I would like to create a few resources that I can reuse in each article for
things like:
- "Let's Learn Nix" reproducibility: Where I list all of the tutorials'
  dependencies: nix version, <nixpkgs> version, OS type and version, etc.
- Haskell type signature convention for Nix
- Ad hoc vs. declarative configuration for Nix
- Troubleshooting Nix: <nixpkgs> search, nix repl, searching the Nix codebase
2020-03-16 20:17:24 +00:00

9.2 KiB

title date draft
Let's Learn Nix: Dotfiles 2020-03-13T22:23:02Z true

Let's Learn Nix: Dotfiles

Dependencies

Speaking of dependencies, here's what you should know before reading this tutorial.

  • Basic Nix syntax: Nix 1p

What version of Nix are we using? What version of <nixpkgs> are we using? What operating system are we using? So many variables...

Cartesian product of all possibilities...

TODO(wpcarro): Create a graphic of the options.

The problems of dotfiles

How do you manage your dependencies?

You can use stow to install the dotfiles.

home-manager

What we are going to write is most likely less preferable to the following alternatives:

  • using Nix home-manager
  • committing your .gitconfig into your

In the next tutorial, we will use home-manager to replace the functionality that we wrote.

So why bother completing this?

Let's begin

Welcome to the first tutorial in the Let's Learn Nix series. Today we are going to create a Nix derivation for one of your dotfiles.

"Dotfiles" refers to a user's collection of configuration files. Typically these files look like:

  • .vimrc
  • .xsessionrc
  • .bashrc

The leading "dot" at the beginning gives dotfiles their name.

You probably have amassed a collection of dotfiles whether or not you are aware. For example, if you use git, the file ~/.gitconfig should exist on your machine. You can verify this with:

$ stat ~/.gitconfig

When I was first learning git, I learned to configure it using commands I found in books and tutorials that often looked like:

$ git config user.email

The ~/.gitconfig file on your machine may look something like this:

[user]
	name = John Cleese
	email = john@flying-circus.com
	username = jcleese
[core]
	editor = emacs
[web]
	browser = google-chrome
[rerere]
	enabled = 1
	autoupdate = 1
[push]
	default = matching
[color]
	ui = auto
[alias]
	a = add --all
	ai = add -i
	b = branch
	cl = clone
	cp = cherry-pick
	d = diff
	fo = fetch origin
	lg = log --oneline --graph --decorate
	ps = push
	pb = pull --rebase
	s = status

As I ran increasingly more git config commands to configure my git preferences, the size of my .gitconfig increased, and the less likely I was to remember which options I set to which values.

Thankfully a coworker at the time, Ryan (@rschmukler), told me that he version-controlled his .gitconfig file along with his other configuration files (e.g. .vimrc) in a repository he called "dotfiles".

Version-controlling your dotfiles improves upon a workflow where you have a variety of configuration files scattered around your machine.

If you look at the above .gitconfig, can you spot the dependencies?

We explicitly depend emacs and google-chrome. We also implicitly depend on git: there is not much value of having a .gitconfig file if you also do not have git installed on your machine.

Dependencies:

  • emacs
  • google-chrome

Let's use Nix to generate this .gitconfig file. Here is what I would like our API to be:

Let's create a file gitconfig.nix and build our function section-by-section:

TODO(wpcarro): Link to sections here

  • options.user
  • options.core
  • options.web
  • options.rerere
  • options.push
  • options.color
  • options.alias
$ touch gitconfig.nix

options.user

AttrSet -> String
user = {
  name = "John Cleese";
  email = "john@flying-circus.com";
  username = "jcleese";
};
[user]
	name = John Cleese
	email = john@flying-circus.com
	username = jcleese

options.core

core = {
  editor = "${pkgs.emacs}/bin/emacs";
};
[core]
	editor = /nix/store/<hash>-emacs-<version>/bin/emacs

options.web

web.browser = "${pkgs.google-chrome}/bin/google-chrome";
[web]
	browser = /nix/store/<hash>-google-chrome-<version>/bin/google-chrome

options.rerere

rerere = {
  enabled = true;
  autoupdate = true;
};
[rerere]
	enabled = 1
	autoupdate = 1

options.push

push.default = "matching";
[push]
	default = matching

options.color

color.ui = "auto";
[color]
	ui = auto

We need to define a function named gitconfig that creates a Nix derivation:

# file: gitconfig.nix
let
  # Import the <nixpkgs> package repository.
  pkgs = import <nixpkgs> {};

  # Stringify the attribute set, `xs`, as a multilined string formatted as "<key> = <value>".
  # See attrsets.nix for more functions that work with attribute sets.
  encodeAttrSet = xs: lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}") xs);

  # Define out function name `gitconfig` that accepts an `options` argument.
  gitconfig = options: pkgs.stdenv.mkDerivation {
    # The gitconfig file that Nix builds will be located /nix/store/some-hash-gitconfig.
    name = "gitconfig";
    src = pkgs.writeTextFile ".gitconfig" ''
      [user]
          name = ${options.user.name}
          email = ${options.user.email}
          username = ${options.user.username}
      [core]
          editor = ${options.core.editor}
      [web]
          editor = ${options.web.browser}
      [rerere]
          enabled = ${if options.rerere.enabled "1" else "0"}
          autoupdate = ${if options.rerere.autoupdate "1" else "0"}
      [push]
          default = ${options.push.default}
      [color]
          ui = ${options.color.ui}
      [alias]
          ${encodeAttrSet options.aliases}
    '';
    buildPhase = ''
      ${pkgs.coreutils}/bin/cp $src $out
    '';
    installPhase = ''
      ${pkgs.coreutils}/bin/ln -s $out ~/.gitconfig
    '';
  };
} in gitconfig {
  user = {
    name = "John Cleese";
    email = "john@flying-circus.com";
    username = "jcleese";
  };
  core = {
    editor = "${pkgs.emacs}/bin/emacs";
  };
  web.browser = "${pkgs.google-chrome}/bin/google-chrome";
  rerere = {
    enabled = true;
    autoupdate = true;
  };
  push.default = "matching";
  color.ui = "auto";
  aliases = {
	a  = "add --all";
	ai = "add -i";
	b  = "branch";
	cl = "clone";
	cp = "cherry-pick";
	d  = "diff";
	fo = "fetch origin";
	lg = "log --oneline --graph --decorate";
	ps = "push";
	pb = "pull --rebase";
	s  = "status";
  };
}

options.alias

We want to write a function that accepts an attribute set and returns a string. While Nix is a dynamically typed programming language, thinking in types helps me clarify what I'm trying to write.

encodeAttrSet :: AttrSet -> String

I prefer using a Haskell-inspired syntax for describing type signatures. Even if you haven't written Haskell before, you may find the syntax intuitive.

Here is a non comprehensive, but demonstrative list of example type signatures:

  • [String]: A list of strings (i.e. [ "cogito" "ergo" "sum" ])
  • AttrSet: A nix attribute set (i.e. { name = "John Cleese"; age = 80; }).
  • add :: Integer -> Integer -> Integer: A function named add that accepts two integers and returns an integer.

Specifically, we want to make sure that when we call:

encodeAttrSet {
  a = "add --all";
  b = "branch";
}

...it returns a string that looks like this:

a = "add --all"
b = "branch"

TODO(wpcarro): @tazjin's nix-1p mentions this. Link to it. Nix has useful functions scattered all over the place:

  • lib.nix
  • list.nix
  • lib.attrSet

But I cannot recall exactly which functions we will need to write encodeAttrSet. In these cases, I do the following:

  1. Run nix repl.
  2. Browse the Nix source code.

Google "nix attribute sets" and find the Github link to attrsets.nix.

You should consider repeating this search but instead of searching for "attribute sets" search for "lists" and "strings". That is how I found the functions needed to write encodeAttrSet. Let's return to our nix repl.

Load the nixpkgs set:

nix-repl> :l <nixpkgs>
Added 11484 variables.

Define a test input called attrs:

nix-repl> attrs = { fname = "John"; lname = "Cleese"; }

Map the attribute set into [String] using lib.mapAttrsToList:

nix-repl> lib.mapAttrsToList (k: v: "${k} = ${toString v}") attrs
[ "fname = John" "lname = Cleese" ]

Now join the [String] together using lib.concatStringsSep:

nix-repl> lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}") attrs)
"fname = John\nlname = Cleese"

Now let's use this to define our function encodeAttrSet:

# file: gitconfig.nix
encodeAttrSet = xs: lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}") xs);

Nixpkgs search.

Conclusion

We learned how to help ourselves.

We used Nix to create our first derivation.