401 lines
		
	
	
	
		
			9.2 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			401 lines
		
	
	
	
		
			9.2 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| ---
 | |
| title: "Let's Learn Nix: Dotfiles"
 | |
| date: 2020-03-13T22:23:02Z
 | |
| draft: 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][wtf-home-mgr] 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][wtf-lln] 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][wtf-git], the file `~/.gitconfig` should
 | |
| exist on your machine. You can verify this with:
 | |
| 
 | |
| ```shell
 | |
| $ 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:
 | |
| 
 | |
| ```shell
 | |
| $ git config user.email
 | |
| ```
 | |
| 
 | |
| The `~/.gitconfig` file on your machine may look something like this:
 | |
| 
 | |
| ```.gitconfig
 | |
| [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][who-ryan]), 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
 | |
| 
 | |
| ```shell
 | |
| $ touch gitconfig.nix
 | |
| ```
 | |
| 
 | |
| ### options.user
 | |
| 
 | |
| ```haskell
 | |
| AttrSet -> String
 | |
| ```
 | |
| 
 | |
| ```nix
 | |
| user = {
 | |
|   name = "John Cleese";
 | |
|   email = "john@flying-circus.com";
 | |
|   username = "jcleese";
 | |
| };
 | |
| ```
 | |
| 
 | |
| ```.gitconfig
 | |
| [user]
 | |
| 	name = John Cleese
 | |
| 	email = john@flying-circus.com
 | |
| 	username = jcleese
 | |
| ```
 | |
| 
 | |
| ### options.core
 | |
| 
 | |
| ```nix
 | |
| core = {
 | |
|   editor = "${pkgs.emacs}/bin/emacs";
 | |
| };
 | |
| ```
 | |
| 
 | |
| ```.gitconfig
 | |
| [core]
 | |
| 	editor = /nix/store/<hash>-emacs-<version>/bin/emacs
 | |
| ```
 | |
| 
 | |
| ### options.web
 | |
| 
 | |
| ```nix
 | |
| web.browser = "${pkgs.google-chrome}/bin/google-chrome";
 | |
| ```
 | |
| 
 | |
| ```.gitconfig
 | |
| [web]
 | |
| 	browser = /nix/store/<hash>-google-chrome-<version>/bin/google-chrome
 | |
| ```
 | |
| 
 | |
| ### options.rerere
 | |
| 
 | |
| ```nix
 | |
| rerere = {
 | |
|   enabled = true;
 | |
|   autoupdate = true;
 | |
| };
 | |
| ```
 | |
| 
 | |
| ```.gitconfig
 | |
| [rerere]
 | |
| 	enabled = 1
 | |
| 	autoupdate = 1
 | |
| ```
 | |
| 
 | |
| ### options.push
 | |
| 
 | |
| ```nix
 | |
| push.default = "matching";
 | |
| ```
 | |
| 
 | |
| ```.gitconfig
 | |
| [push]
 | |
| 	default = matching
 | |
| ```
 | |
| 
 | |
| ### options.color
 | |
| 
 | |
| ```nix
 | |
| color.ui = "auto";
 | |
| ```
 | |
| 
 | |
| ```.gitconfig
 | |
| [color]
 | |
| 	ui = auto
 | |
| ```
 | |
| 
 | |
| We need to define a function named `gitconfig` that creates a Nix [derivation][wtf-derivation]:
 | |
| 
 | |
| ```nix
 | |
| # 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.
 | |
| 
 | |
| ```haskell
 | |
| 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:
 | |
| 
 | |
| ```nix
 | |
| encodeAttrSet {
 | |
|   a = "add --all";
 | |
|   b = "branch";
 | |
| }
 | |
| ```
 | |
| 
 | |
| ...it returns a string that looks like this:
 | |
| 
 | |
| ```.gitconfig
 | |
| 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
 | |
| nix-repl> :l <nixpkgs>
 | |
| Added 11484 variables.
 | |
| ```
 | |
| 
 | |
| Define a test input called `attrs`:
 | |
| 
 | |
| ```nix
 | |
| nix-repl> attrs = { fname = "John"; lname = "Cleese"; }
 | |
| ```
 | |
| 
 | |
| Map the attribute set into `[String]` using `lib.mapAttrsToList`:
 | |
| 
 | |
| ```nix
 | |
| nix-repl> lib.mapAttrsToList (k: v: "${k} = ${toString v}") attrs
 | |
| [ "fname = John" "lname = Cleese" ]
 | |
| ```
 | |
| 
 | |
| Now join the `[String]` together using `lib.concatStringsSep`:
 | |
| 
 | |
| ```nix
 | |
| 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`:
 | |
| 
 | |
| ```nix
 | |
| # file: gitconfig.nix
 | |
| encodeAttrSet = xs: lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}") xs);
 | |
| ```
 | |
| 
 | |
| ### Using nixpkgs search
 | |
| 
 | |
| [Nixpkgs search][wtf-nixpkgs-search].
 | |
| 
 | |
| ### Conclusion
 | |
| 
 | |
| We learned how to help ourselves.
 | |
| 
 | |
| - Where does `emacs` exist? What about `google-chrome`? [nixpkgs search][wtf-nixpkgs-search]
 | |
| - Verify that I have it? [nix REPL][using-nix-repl]
 | |
| 
 | |
| We used Nix to create our first derivation.
 | |
| 
 | |
| [wtf-lln]: /lets-learn-nix
 | |
| [wtf-git]: https://git-scm.com/
 | |
| [wtf-derivation]: https://nixos.org/nixos/nix-pills/our-first-derivation.html
 | |
| [wtf-nixpkgs-search]: https://nixos.org/nixos/packages.html?channel=nixos-19.09
 | |
| [using-nix-repl]: /using-the-nix-repl
 | |
| [wtf-home-mgr]: https://github.com/rycee/home-manager
 | |
| [who-ryan]: https://twitter.com/rschmukler
 |