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
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 namedadd
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:
- Run
nix repl
. - 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);
Using nixpkgs search
Conclusion
We learned how to help ourselves.
- Where does
emacs
exist? What aboutgoogle-chrome
? nixpkgs search - Verify that I have it? nix REPL
We used Nix to create our first derivation.